diff --git a/src/Element.zig b/src/Element.zig index 808e37c..a36de47 100644 --- a/src/Element.zig +++ b/src/Element.zig @@ -3,6 +3,7 @@ pub const HBox = @import("./Element/HBox.zig"); pub const Link = @import("./Element/Link.zig"); pub const Page = @import("./Element/Page.zig"); pub const Pile = @import("./Element/Pile.zig"); +pub const ServerList = @import("./Element/ServerList.zig"); pub const Text = @import("./Element/Text.zig"); pub const View = @import("./Element/View.zig"); diff --git a/src/Element/ServerList.zig b/src/Element/ServerList.zig new file mode 100644 index 0000000..ed5fc7f --- /dev/null +++ b/src/Element/ServerList.zig @@ -0,0 +1,71 @@ +allocator: std.mem.Allocator, +discovered: *Discovered, + +pub const Discovered = struct { + allocator: std.mem.Allocator, + mutex: std.Thread.Mutex = .{}, + servers: std.StringArrayHashMapUnmanaged(void) = .{}, +}; + +pub fn create(allocator: std.mem.Allocator, discovered: *Discovered) !*@This() { + const this = try allocator.create(@This()); + errdefer allocator.destroy(this); + + this.* = .{ + .allocator = allocator, + .discovered = discovered, + }; + return this; +} + +pub fn element(this: *@This()) Element { + return Element{ + .pointer = this, + .interface = &Element.Interface{ + .destroy = &element_destroy, + .minimum_size = &element_minimum_size, + .render = &element_render, + .event = &element_event, + }, + }; +} + +pub fn element_destroy(pointer: ?*anyopaque) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + this.allocator.destroy(this); +} + +pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: Element.RenderResources) [2]f32 { + const this: *@This() = @ptrCast(@alignCast(pointer)); + _ = this; + return render_resources.font.textSize("example_server.xyz:5711", 1); +} + +pub fn element_render(pointer: ?*anyopaque, canvas: *seizer.Canvas, render_resources: Element.RenderResources, min: [2]f32, max: [2]f32) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + _ = max; + _ = render_resources; + + var pos = min; + + this.discovered.mutex.lock(); + defer this.discovered.mutex.unlock(); + for (this.discovered.servers.keys()) |url| { + const text_size = canvas.writeText(pos, url, .{}); + pos[1] += text_size[1]; + } +} + +pub fn element_event(pointer: ?*anyopaque, event: Element.Event) ?Element.Command { + const this: *@This() = @ptrCast(@alignCast(pointer)); + _ = this; + _ = event; + return null; +} + +const Element = @import("../Element.zig"); +const assets = @import("../assets.zig"); + +const seizer = @import("seizer"); +const gl = seizer.gl; +const std = @import("std"); diff --git a/src/LocalUI.zig b/src/LocalUI.zig index fdcebd0..fc904d3 100644 --- a/src/LocalUI.zig +++ b/src/LocalUI.zig @@ -31,6 +31,11 @@ const SIOCGIFADDR = 0x8915; const JoinMultiplayerGame = struct { allocator: std.mem.Allocator, discovery_thread: std.Thread, + running: *std.atomic.Value(bool), + + discovered: *Element.ServerList.Discovered, + + const Server = struct { url: []const u8, port: u16 }; pub const INTERFACE = &Handler.Interface{ .create = &create, @@ -42,11 +47,21 @@ const JoinMultiplayerGame = struct { const this = try allocator.create(@This()); errdefer allocator.destroy(this); - const discovery_thread = std.Thread.spawn(.{}, find_local_games_thread, .{allocator}) catch return error.OutOfMemory; + const discovered = try allocator.create(Element.ServerList.Discovered); + errdefer allocator.destroy(discovered); + discovered.* = .{ .allocator = allocator }; + + const running = try allocator.create(std.atomic.Value(bool)); + errdefer allocator.destroy(running); + running.store(true, .Monotonic); + + const discovery_thread = std.Thread.spawn(.{}, find_local_games_thread, .{ running, discovered }) catch return error.OutOfMemory; this.* = .{ .allocator = allocator, .discovery_thread = discovery_thread, + .running = running, + .discovered = discovered, }; return this; @@ -54,15 +69,17 @@ const JoinMultiplayerGame = struct { fn handler(pointer: ?*anyopaque, request: Request) Handler.Error!Response { const this: *@This() = @ptrCast(@alignCast(pointer)); - _ = this; var arena = std.heap.ArenaAllocator.init(request.allocator); errdefer arena.deinit(); var text = try Element.Text.create(arena.allocator(), try std.fmt.allocPrint(arena.allocator(), "Looking for multiplayer games", .{})); + var server_list = try Element.ServerList.create(arena.allocator(), this.discovered); + var page = try Element.Page.create(request.allocator); try page.addElement(.{ 0.5, 0.5 }, .{ 0.5, 0.5 }, text.element()); + try page.addElement(.{ 0.5, 0.7 }, .{ 0.5, 0.5 }, server_list.element()); return Response{ .arena = arena, .body = .{ .element = page.element() } }; } @@ -70,12 +87,23 @@ const JoinMultiplayerGame = struct { fn deinit(pointer: ?*anyopaque) void { const this: *@This() = @ptrCast(@alignCast(pointer)); + this.running.store(false, .Monotonic); + + this.discovery_thread.join(); + for (this.discovered.servers.keys()) |url| { + this.discovered.allocator.free(url); + } + this.discovered.servers.deinit(this.discovered.allocator); + + this.allocator.destroy(this.discovered); + this.allocator.destroy(this.running); + this.allocator.destroy(this); } - fn find_local_games_thread(allocator: std.mem.Allocator) !void { - const buffer = try allocator.alloc(u8, 2048); - defer allocator.free(buffer); + fn find_local_games_thread(running: *std.atomic.Value(bool), discovered: *Element.ServerList.Discovered) !void { + const buffer = try discovered.allocator.alloc(u8, 2048); + defer discovered.allocator.free(buffer); const log = std.log.scoped(.mdns); log.info("mdns discovery service started", .{}); @@ -95,7 +123,7 @@ const JoinMultiplayerGame = struct { } } - while (true) { + while (running.load(.Monotonic)) { var pollfds = [_]std.os.pollfd{ .{ .fd = mdns_ipv4_socket, .events = std.os.POLL.IN, .revents = undefined }, .{ .fd = mdns_ipv6_socket, .events = std.os.POLL.IN, .revents = undefined }, @@ -103,11 +131,11 @@ const JoinMultiplayerGame = struct { _ = std.os.poll(&pollfds, 1000) catch break; for (pollfds) |pollfd| { if (pollfd.revents & std.os.POLL.IN == 0) continue; - _ = c.mdns_query_recv(pollfd.fd, buffer.ptr, buffer.len, &find_local_games_thread_query_callback, null, 0); + _ = c.mdns_query_recv(pollfd.fd, buffer.ptr, buffer.len, &find_local_games_thread_query_callback, discovered, 0); } } - log.info("mdns service stopping", .{}); + log.info("mdns discovery service stopping", .{}); } fn find_local_games_thread_query_callback( @@ -129,8 +157,9 @@ const JoinMultiplayerGame = struct { ) callconv(.C) c_int { const log = std.log.scoped(.mdns); + const discovered: *Element.ServerList.Discovered = @ptrCast(@alignCast(userdata)); + _ = rclass; - _ = userdata; _ = ttl; _ = name_length; _ = sock; @@ -173,6 +202,15 @@ const JoinMultiplayerGame = struct { var strbuffer: [128]u8 = undefined; const record = c.mdns_record_parse_srv(data_ptr_opaque, data_len, record_offset, record_length, &strbuffer, strbuffer.len); log.info("{?} : {s} {s} \"{}\":{}", .{ std_from, entrytype_str, recordtype_str, std.zig.fmtEscapes(record.name.str[0..record.name.length]), record.port }); + + const url = std.fmt.allocPrint(discovered.allocator, "{s}:{}", .{ record.name.str[0..record.name.length], record.port }) catch unreachable; + discovered.mutex.lock(); + defer discovered.mutex.unlock(); + + const gop = discovered.servers.getOrPut(discovered.allocator, url) catch unreachable; + if (gop.found_existing) { + discovered.allocator.free(url); + } }, c.MDNS_RECORDTYPE_A => { var std_address: std.net.Ip4Address = undefined;