// zig fmt: off const std = @import("std"); const Api = @import("artifacts-api"); const Artificer = @import("artificer"); const UI = @import("./ui.zig"); const RectUtils = @import("./rect-utils.zig"); const rl = @import("raylib"); const FontFace = @import("./font-face.zig"); const srcery = @import("./srcery.zig"); const rlgl_h = @cImport({ @cInclude("rlgl.h"); }); const assert = std.debug.assert; const log = std.log.scoped(.app); const App = @This(); const MapTexture = struct { name: Api.Map.Skin, texture: rl.Texture2D, }; ui: UI, server: *Api.Server, store: *Api.Store, map_textures: std.ArrayList(MapTexture), map_texture_indexes: std.ArrayList(usize), map_position_min: Api.Position, map_position_max: Api.Position, character_skin_textures: std.EnumArray(Api.Character.Skin, rl.Texture2D), camera: rl.Camera2D, font_face: FontFace, blur_texture_original: ?rl.RenderTexture = null, blur_texture_horizontal: ?rl.RenderTexture = null, blur_texture_both: ?rl.RenderTexture = null, blur_shader: rl.Shader, simulation: bool = false, system_clock: Artificer.SystemClock, started_at: i128, artificer: Artificer.ArtificerApi, sim_artificer: Artificer.ArtificerSim, sim_server: Artificer.SimServer, sim_started_at: i128, last_sim_timestamp: i128 = 0, thread_running: bool = true, artificer_thread: std.Thread, pub fn init(allocator: std.mem.Allocator, store: *Api.Store, server: *Api.Server) !*App { var map_textures = std.ArrayList(MapTexture).init(allocator); errdefer map_textures.deinit(); errdefer { for (map_textures.items) |map_texture| { map_texture.texture.unload(); } } var map_texture_indexes = std.ArrayList(usize).init(allocator); errdefer map_texture_indexes.deinit(); // Load all map textures from api store { const map_image_ids = store.images.category_mapping.get(.map).items; try map_textures.ensureTotalCapacity(map_image_ids.len); for (map_image_ids) |image_id| { const image = store.images.get(image_id).?; map_textures.appendAssumeCapacity(MapTexture{ .name = try Api.Map.Skin.fromSlice(image.code.slice()), .texture = try loadTextureFromStore(store, image_id) }); } } var map_position_max = Api.Position.zero(); var map_position_min = Api.Position.zero(); for (store.maps.items) |map| { map_position_min.x = @min(map_position_min.x, map.position.x); map_position_min.y = @min(map_position_min.y, map.position.y); map_position_max.x = @max(map_position_max.x, map.position.x); map_position_max.y = @max(map_position_max.y, map.position.y); } const map_size = map_position_max.subtract(map_position_min); try map_texture_indexes.ensureTotalCapacity(@intCast(map_size.x * map_size.y)); for (0..@intCast(map_size.y)) |oy| { for (0..@intCast(map_size.x)) |ox| { const x = map_position_min.x + @as(i64, @intCast(ox)); const y = map_position_min.y + @as(i64, @intCast(oy)); const map = store.getMap(.{ .x = x, .y = y }).?; var found_texture = false; const map_skin = map.skin.slice(); for (0.., map_textures.items) |i, map_texture| { if (std.mem.eql(u8, map_skin, map_texture.name.slice())) { map_texture_indexes.appendAssumeCapacity(i); found_texture = true; break; } } if (!found_texture) { return error.MapImageNotFound; } } } var character_skin_textures = std.EnumArray(Api.Character.Skin, rl.Texture2D).initUndefined(); inline for (std.meta.fields(Api.Character.Skin)) |field| { const skin: Api.Character.Skin = @enumFromInt(field.value); const skin_image_id = store.images.getId(.character, skin.toString()).?; const texture = try loadTextureFromStore(store, skin_image_id); character_skin_textures.set(skin, texture); } const blur_shader = rl.loadShaderFromMemory( @embedFile("./base.vsh"), @embedFile("./blur.fsh"), ); if (!rl.isShaderReady(blur_shader)) { return error.LoadShaderFromMemory; } var fontChars: [95]i32 = undefined; for (0..fontChars.len) |i| { fontChars[i] = 32 + @as(i32, @intCast(i)); } var font = rl.loadFontFromMemory(".ttf", @embedFile("./roboto-font/Roboto-Medium.ttf"), 16, &fontChars); if (!font.isReady()) { return error.LoadFontFromMemory; } const font_face = FontFace{ .font = font }; const ui = UI.init(font_face); const character_id = store.characters.getId("Blondie").?; const sim_server = Artificer.SimServer.init(0, store); var app = try allocator.create(App); errdefer allocator.destroy(app); app.* = App{ .store = store, .server = server, .system_clock = .{}, .sim_server = sim_server, .started_at = 0, .sim_started_at = sim_server.clock.nanoTimestamp(), .ui = ui, .map_textures = map_textures, .map_texture_indexes = map_texture_indexes, .map_position_max = map_position_max, .map_position_min = map_position_min, .character_skin_textures = character_skin_textures, .blur_shader = blur_shader, .font_face = font_face, .camera = rl.Camera2D{ .offset = rl.Vector2.zero(), .target = rl.Vector2.zero(), .rotation = 0, .zoom = 1, }, .artificer = undefined, .sim_artificer = undefined, .artificer_thread = undefined, }; app.started_at = app.system_clock.nanoTimestamp(); app.sim_artificer = try Artificer.ArtificerSim.init(allocator, store, &app.sim_server.clock, &app.sim_server, character_id); errdefer app.sim_artificer.deinit(allocator); app.artificer = try Artificer.ArtificerApi.init(allocator, store, &app.system_clock, server, character_id); errdefer app.artificer.deinit(allocator); app.artificer_thread = try std.Thread.spawn(.{ .allocator = allocator }, artificer_thread_cb, .{ app }); errdefer { app.thread_running = false; app.artificer_thread.join(); } return app; } pub fn deinit(self: *App) void { const allocator = self.map_textures.allocator; self.thread_running = false; self.artificer_thread.join(); self.artificer.deinit(allocator); self.sim_artificer.deinit(allocator); for (self.map_textures.items) |map_texture| { map_texture.texture.unload(); } for (self.character_skin_textures.values) |texture| { texture.unload(); } self.map_textures.deinit(); self.map_texture_indexes.deinit(); self.font_face.font.unload(); if (self.blur_texture_horizontal) |render_texture| { render_texture.unload(); } if (self.blur_texture_both) |render_texture| { render_texture.unload(); } if (self.blur_texture_original) |render_texture| { render_texture.unload(); } allocator.destroy(self); } fn artificer_thread_cb(app: *App) void { while (app.thread_running) { if (app.simulation) { const artificer = &app.sim_artificer; const expires_in = artificer.timeUntilCooldownExpires(); if (expires_in > 0) { artificer.clock.sleep(expires_in); } artificer.tick() catch |e| { log.err("Error in .tick in thread: {}", .{e}); app.thread_running = false; }; } else { const artificer = &app.artificer; const expires_in = artificer.timeUntilCooldownExpires(); if (expires_in > 0) { artificer.clock.sleep(expires_in); } artificer.tick() catch |e| { log.err("Error in .tick in thread: {}", .{e}); app.thread_running = false; }; } } } fn loadTextureFromStore(store: *Api.Store, image_id: Api.Store.Id) !rl.Texture2D { const image = store.images.get(image_id).?; const texture = rl.loadTextureFromImage(rl.Image{ .width = @intCast(image.width), .height = @intCast(image.height), .data = store.images.getRGBA(image_id).?.ptr, .mipmaps = 1, .format = rl.PixelFormat.pixelformat_uncompressed_r8g8b8a8 }); if (!rl.isTextureReady(texture)) { return error.LoadMapTextureFromImage; } return texture; } fn cameraControls(camera: *rl.Camera2D) void { if (rl.isMouseButtonDown(.mouse_button_left)) { const mouse_delta = rl.getMouseDelta(); camera.target.x -= mouse_delta.x / camera.zoom; camera.target.y -= mouse_delta.y / camera.zoom; } const zoom_speed = 0.2; const min_zoom = 0.1; const max_zoom = 2; const zoom_delta = rl.getMouseWheelMove(); if (zoom_delta != 0) { const mouse_screen = rl.getMousePosition(); // Get the world point that is under the mouse const mouse_world = rl.getScreenToWorld2D(mouse_screen, camera.*); // Set the offset to where the mouse is camera.offset = mouse_screen; // Set the target to match, so that the camera maps the world space point // under the cursor to the screen space point under the cursor at any zoom camera.target = mouse_world; // Zoom increment camera.zoom *= (1 + zoom_delta * zoom_speed); camera.zoom = std.math.clamp(camera.zoom, min_zoom, max_zoom); } } // fn drawCharacterInfo(ui: *UI, rect: rl.Rectangle, artificer: *Artificer, brain: *Artificer.Brain) !void { // var buffer: [256]u8 = undefined; // // 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); // // var label_stack = UIStack.init(RectUtils.shrinkTop(rect, name_height + 4), .top_to_bottom); // label_stack.gap = 4; // // const now = std.time.milliTimestamp(); // const cooldown = brain.cooldown(&artificer.server); // const seconds_left: f32 = @as(f32, @floatFromInt(@max(cooldown - now, 0))) / std.time.ms_per_s; // const cooldown_label = try std.fmt.bufPrint(&buffer, "Cooldown: {d:.3}", .{seconds_left}); // UI.drawTextEx(ui.font, cooldown_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white); // // var task_label: []u8 = undefined; // if (brain.task) |task| { // task_label = try std.fmt.bufPrint(&buffer, "Task: {s}", .{@tagName(task)}); // } else { // task_label = try std.fmt.bufPrint(&buffer, "Task: -", .{}); // } // UI.drawTextEx(ui.font, task_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white); // // const actions_label = try std.fmt.bufPrint(&buffer, "Actions: {}", .{brain.action_queue.items.len}); // UI.drawTextEx(ui.font, actions_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white); // } fn createOrGetRenderTexture(maybe_render_texture: *?rl.RenderTexture) !rl.RenderTexture { const screen_width = rl.getScreenWidth(); const screen_height = rl.getScreenHeight(); if (maybe_render_texture.*) |render_texture| { if (render_texture.texture.width != screen_width or render_texture.texture.height != screen_height) { render_texture.unload(); maybe_render_texture.* = null; } } if (maybe_render_texture.* == null) { const render_texture = rl.loadRenderTexture(screen_width, screen_height); if (!rl.isRenderTextureReady(render_texture)) { return error.LoadRenderTexture; } maybe_render_texture.* = render_texture; } return maybe_render_texture.*.?; } pub fn drawWorld(self: *App) void { rl.clearBackground(srcery.black); const tile_size = rl.Vector2.init(224, 224); const map_size = self.map_position_max.subtract(self.map_position_min); for (0..@intCast(map_size.y)) |oy| { for (0..@intCast(map_size.x)) |ox| { const map_index = @as(usize, @intCast(map_size.x)) * oy + ox; const x = self.map_position_min.x + @as(i64, @intCast(ox)); const y = self.map_position_min.y + @as(i64, @intCast(oy)); const texture_index = self.map_texture_indexes.items[map_index]; const texture = self.map_textures.items[texture_index].texture; const position = rl.Vector2.init(@floatFromInt(x), @floatFromInt(y)).multiply(tile_size); rl.drawTextureV(texture, position, rl.Color.white); } } for (self.store.characters.objects.items) |optional_character| { if (optional_character != .object) continue; const character = optional_character.object; const skin_texture = self.character_skin_textures.get(character.skin); const position = rl.Vector2{ .x = @floatFromInt(character.position.x), .y = @floatFromInt(character.position.y), }; const skin_size = rl.Vector2{ .x = @floatFromInt(skin_texture.width), .y = @floatFromInt(skin_texture.height), }; rl.drawTextureV( skin_texture, position.addValue(0.5).multiply(tile_size).subtract(skin_size.divide(.{ .x = 2, .y = 2 })), rl.Color.white ); } } pub fn drawWorldAndBlur(self: *App) !void { const screen_width = rl.getScreenWidth(); const screen_height = rl.getScreenHeight(); const screen_size = rl.Vector2.init(@floatFromInt(screen_width), @floatFromInt(screen_height)); const blur_original = try createOrGetRenderTexture(&self.blur_texture_original); const blur_horizontal = try createOrGetRenderTexture(&self.blur_texture_horizontal); const blur_both = try createOrGetRenderTexture(&self.blur_texture_both); // 1 pass. Draw the all of the sprites { blur_original.begin(); defer blur_original.end(); self.camera.begin(); defer self.camera.end(); self.drawWorld(); } // 2 pass. Apply horizontal blur const kernel_radius: i32 = 16; var kernel_coeffs: [kernel_radius * 2 + 1]f32 = undefined; { const sigma = 10; for (0..kernel_coeffs.len) |i| { const i_i32: i32 = @intCast(i); const io: f32 = @floatFromInt(i_i32 - kernel_radius); kernel_coeffs[i] = @exp(-(io * io) / (sigma * sigma)); } var kernel_sum: f32 = 0; for (kernel_coeffs) |coeff| { kernel_sum += coeff; } for (&kernel_coeffs) |*coeff| { coeff.* /= kernel_sum; } } { const texture_size_loc = rl.getShaderLocation(self.blur_shader, "textureSize"); assert(texture_size_loc != -1); rl.setShaderValue(self.blur_shader, texture_size_loc, &screen_size, .shader_uniform_vec2); } { const kernel_radius_loc = rl.getShaderLocation(self.blur_shader, "kernelRadius"); assert(kernel_radius_loc != -1); rl.setShaderValue(self.blur_shader, kernel_radius_loc, &kernel_radius, .shader_uniform_int); } { const coeffs_loc = rl.getShaderLocation(self.blur_shader, "coeffs"); assert(coeffs_loc != -1); rl.setShaderValueV(self.blur_shader, coeffs_loc, &kernel_coeffs, .shader_uniform_float, kernel_coeffs.len); } { const kernel_direction_loc = rl.getShaderLocation(self.blur_shader, "kernelDirection"); assert(kernel_direction_loc != -1); const kernel_direction = rl.Vector2.init(1, 0); rl.setShaderValue(self.blur_shader, kernel_direction_loc, &kernel_direction, .shader_uniform_vec2); } { blur_horizontal.begin(); defer blur_horizontal.end(); rl.clearBackground(rl.Color.black.alpha(0)); self.blur_shader.activate(); defer self.blur_shader.deactivate(); rl.drawTextureRec( blur_original.texture, .{ .x = 0, .y = 0, .width = screen_size.x, .height = -screen_size.y, }, rl.Vector2.zero(), rl.Color.white ); } // 3 pass. Apply vertical blur { const kernel_direction_loc = rl.getShaderLocation(self.blur_shader, "kernelDirection"); assert(kernel_direction_loc != -1); const kernel_direction = rl.Vector2.init(0, 1); rl.setShaderValue(self.blur_shader, kernel_direction_loc, &kernel_direction, .shader_uniform_vec2); } { blur_both.begin(); defer blur_both.end(); self.blur_shader.activate(); defer self.blur_shader.deactivate(); rl.drawTextureRec( blur_horizontal.texture, .{ .x = 0, .y = 0, .width = screen_size.x, .height = -screen_size.y, }, rl.Vector2.zero(), rl.Color.white ); } // Last thing, draw world without blur rl.drawTextureRec( blur_original.texture, .{ .x = 0, .y = 0, .width = @floatFromInt(blur_original.texture.width), .height = @floatFromInt(-blur_original.texture.height), }, rl.Vector2.zero(), rl.Color.white ); } fn drawRatelimits(self: *App, box: rl.Rectangle) void { const Category = Api.RateLimit.Category; const ratelimits = self.server.ratelimits; const padding = 16; var stack = UI.Stack.init(RectUtils.shrink(box, padding, padding), .top_to_bottom); stack.gap = 8; inline for (.{ .{ "Account creation", Category.account_creation }, .{ "Token", Category.token }, .{ "Data", Category.data }, .{ "Actions", Category.actions }, }) |ratelimit_bar| { const title = ratelimit_bar[0]; const category = ratelimit_bar[1]; const ratelimit = ratelimits.get(category); const ratelimit_box = stack.next(24); rl.drawRectangleRec(ratelimit_box, rl.Color.white); inline for (.{ .{ ratelimit.hours , std.time.ms_per_hour, srcery.red }, .{ ratelimit.minutes, std.time.ms_per_min , srcery.blue }, .{ ratelimit.seconds, std.time.ms_per_s , srcery.green }, }) |limit_spec| { const maybe_timespan = limit_spec[0]; const timespan_size = limit_spec[1]; const color = limit_spec[2]; if (maybe_timespan) |timespan| { const limit_f32: f32 = @floatFromInt(timespan.limit); const counter_f32: f32 = @floatFromInt(timespan.counter); const timer_f32: f32 = @floatFromInt(timespan.timer_ms); const ms_per_request = timespan_size / limit_f32; const progress = std.math.clamp((counter_f32 - timer_f32 / ms_per_request) / limit_f32, 0, 1); var progress_bar = ratelimit_box; progress_bar.width *= progress; rl.drawRectangleRec(progress_bar, color); } } if (self.ui.isMouseInside(ratelimit_box)) { // TODO: Draw more detailed info about rate limits. // Show how many requests have occured } else { const title_size = self.font_face.measureText(title); self.font_face.drawText( title, .{ .x = ratelimit_box.x + 8, .y = ratelimit_box.y + title_size.y/2, }, srcery.white ); } } } fn showButton(self: *App, text: []const u8) UI.Interaction { const key_hash: u16 = @truncate(@intFromPtr(text.ptr)); var button = self.ui.getOrAppendWidget(key_hash); button.padding.vertical(8); button.padding.horizontal(16); button.flags.insert(.clickable); button.size = .{ .x = .{ .text = {} }, .y = .{ .text = {} }, }; const interaction = self.ui.getInteraction(button); var text_color: rl.Color = undefined; if (interaction.held_down) { button.background = .{ .color = srcery.hard_black }; text_color = srcery.white; } else if (interaction.hovering) { button.background = .{ .color = srcery.bright_black }; text_color = srcery.bright_white; } else { button.background = .{ .color = srcery.black }; text_color = srcery.bright_white; } button.text = .{ .content = text, .color = text_color }; return interaction; } fn showLabel(self: *App, text: []const u8) UI.Widget.Key { const key_hash: u16 = @truncate(@intFromPtr(text.ptr)); var label = self.ui.getOrAppendWidget(key_hash); label.text = .{ .content = text }; label.size = .{ .x = .{ .text = {} }, .y = .{ .text = {} } }; return label.key; } pub fn toggleSimulationMode(self: *App) void { self.simulation = !self.simulation; if (self.simulation) { const system_clock = self.system_clock; const time_passed = system_clock.nanoTimestamp() - self.started_at; const sim_clock = &self.sim_server.clock; sim_clock.timestamp_limit = self.sim_started_at + time_passed; sim_clock.timestamp = sim_clock.timestamp_limit.?; self.last_sim_timestamp = std.time.nanoTimestamp(); } } pub fn tick(self: *App) !void { for (&self.server.ratelimits.values) |*ratelimit| { ratelimit.update_timers(std.time.milliTimestamp()); } if (self.simulation) { const now = std.time.nanoTimestamp(); const time_passed = now - self.last_sim_timestamp; self.last_sim_timestamp = now; const sim_clock = &self.sim_server.clock; sim_clock.timestamp_limit.? += time_passed; } const screen_width = rl.getScreenWidth(); const screen_height = rl.getScreenHeight(); const screen_size = rl.Vector2.init(@floatFromInt(screen_width), @floatFromInt(screen_height)); if (!self.ui.isHoveringAnything()) { cameraControls(&self.camera); self.ui.disable_mouse_interaction = rl.isMouseButtonDown(.mouse_button_left); } else { self.ui.disable_mouse_interaction = false; } rl.beginDrawing(); defer rl.endDrawing(); rl.clearBackground(srcery.black); try self.drawWorldAndBlur(); const ui = &self.ui; if (self.blur_texture_both) |blur_texture_both| { ui.blur_background = blur_texture_both.texture; } ui.begin(); ui.topWidget().padding.all(16); ui.layout().gap = 8; { const panel = ui.getOrAppendWidget(ui.randomWidgetHash()); panel.size = .{ .x = .fit_children, .y = .fit_children }; panel.padding.all(16); panel.layout.gap = 8; panel.background = .blur_world; ui.pushWidget(panel.key); defer ui.popWidget(); if (self.simulation) { const clock = self.sim_server.clock; const started_at = self.sim_started_at; const time_passed: f32 = @floatFromInt(clock.nanoTimestamp() - started_at); var time_passed_buff: [128]u8 = undefined; const time_passed_str = try std.fmt.bufPrint(&time_passed_buff, "Time passed: {d:.1}s", .{ time_passed / std.time.ns_per_s }); _ = self.showLabel(time_passed_str); if (self.showButton("Turn off simulation").clicked) { self.toggleSimulationMode(); } } else { const clock = self.system_clock; const started_at = self.started_at; const time_passed: f32 = @floatFromInt(clock.nanoTimestamp() - started_at); var time_passed_buff: [128]u8 = undefined; const time_passed_str = try std.fmt.bufPrint(&time_passed_buff, "Time passed: {d:.1}s", .{ time_passed / std.time.ns_per_s }); _ = self.showLabel(time_passed_str); if (self.showButton("Turn on simulation").clicked) { self.toggleSimulationMode(); } } } ui.end(); // self.drawRatelimits( // .{ .x = 20, .y = 20, .width = 200, .height = 200 }, // ); rl.drawFPS( @as(i32, @intFromFloat(screen_size.x)) - 100, @as(i32, @intFromFloat(screen_size.y)) - 24 ); // 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)); // try drawCharacterInfo(&ui, info_stack.next(info_width), &artificer, brain); // } }