From f012815d93e48065be994c22ecd8ec58310551ef Mon Sep 17 00:00:00 2001 From: peterino2 Date: Sun, 28 Sep 2025 22:30:19 -0700 Subject: [PATCH] engine transport interface --- build.zig | 7 +- build.zig.zon | 1 + engine/core/src/engine.zig | 4 + engine/net/src/enet/EnetTransport.zig | 210 ++++++++++++++++++ engine/net/src/net.zig | 106 ++++++++- engine/net/src/netEngine.zig | 22 +- lib/enet/build.zig | 7 +- projects/build.zig.zon | 2 + projects/sampleGame/externGame/externGame.zig | 6 + 9 files changed, 353 insertions(+), 12 deletions(-) create mode 100644 engine/net/src/enet/EnetTransport.zig diff --git a/build.zig b/build.zig index 336d39e..335a36a 100644 --- a/build.zig +++ b/build.zig @@ -235,9 +235,9 @@ pub fn addExtraModule(self: *BuildSystem, mod: *std.Build.Module, moduleName: [] } pub fn addDependencyInstalls(self: *BuildSystem, b: *std.Build, optimize: std.builtin.OptimizeMode) void { - if (self.staticBuild) { - return; - } + //if (self.staticBuild) { + //return; + // } inline for (DynamicDepList) |d| { b.installArtifact(b.dependency( @@ -258,6 +258,7 @@ const DynamicDepList: []const struct { dep: []const u8, artifact: []const u8 } = .{ .dep = "lua", .artifact = "luac" }, .{ .dep = "miniaudio", .artifact = "miniaudio_c" }, .{ .dep = "zphysics", .artifact = "joltc" }, + .{ .dep = "enet", .artifact = "enet_c" }, .{ .dep = "ozz", .artifact = "ozz_cpp" }, }; diff --git a/build.zig.zon b/build.zig.zon index f1e8a8f..bd93fac 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -20,6 +20,7 @@ .zphysics = .{ .path = "lib/zphysics" }, .sdl3 = .{ .path = "lib/sdl3" }, + .enet = .{ .path = "lib/enet" }, }, .paths = .{ "", diff --git a/engine/core/src/engine.zig b/engine/core/src/engine.zig index fc7a618..8f7cd7a 100644 --- a/engine/core/src/engine.zig +++ b/engine/core/src/engine.zig @@ -159,6 +159,7 @@ pub const Engine = struct { core.engine_err("RECURSIVE OBJECT CREATION NOT ALLOWED", .{}); return error.BadInit; } + self.engineObjectLUF = self.frameNumber; // bump this every time an engine object mutation has happened self.createObjectLock = true; defer self.createObjectLock = false; @@ -173,6 +174,9 @@ pub const Engine = struct { try self.engineObjects.append(self.allocator, newObjectRef); if (vtable.singletonName) |singletonName| { + if (self.engineObjectsByName.contains(vtable.singletonName.?)) { + return error.DuplicateEngineObject; + } try self.engineObjectsByName.put(self.allocator, singletonName, newObjectRef); } diff --git a/engine/net/src/enet/EnetTransport.zig b/engine/net/src/enet/EnetTransport.zig new file mode 100644 index 0000000..dbb9dec --- /dev/null +++ b/engine/net/src/enet/EnetTransport.zig @@ -0,0 +1,210 @@ +pub var TransportInterfaceVTable = net.TransportInterface.Implement(@This()); +pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This(), "net.ENetTransport"); + +allocator: std.mem.Allocator, +peerType: net.PeerType = .offline, + +address: ?enet.ENetAddress = null, +host: ?*enet.ENetHost = null, + +clients: std.ArrayListUnmanaged(net.ClientInfo) = .{}, + +const MAX_CLIENTS = 128; +const DEFAULT_PORT = 7777; + +pub fn create(allocator: std.mem.Allocator) !*@This() { + const self = try allocator.create(@This()); + + self.* = .{ + .allocator = allocator, + }; + + try enet_mod.initialize(); + + return self; +} + +pub fn stopServer(self: *@This()) void { + // other shit + self.peerType = .offline; +} + +pub fn startClient(self: *@This(), target: []const u8) !void { + if (self.peerType != .offline) { + net.err("unable to start client, already in a different mode {s}", .{@tagName(self.peerType)}); + return error.BadInit; + } + + // Create client host + self.host = enet.enet_host_create(null, 1, 2, 0, 0) orelse { + return error.BadInit; + }; + + self.peerType = .client; + + // Connect to target will be handled separately + _ = target; +} + +pub fn startServer(self: *@This(), bindInfo: ?[]const []const u8) !void { + if (self.peerType != .offline) { + net.err("unable to start server, already in a different mode {s}", .{@tagName(self.peerType)}); + return; + } + const port: u16 = try std.fmt.parseInt(u16, bindInfo.?[0], 0); + + self.address = enet.ENetAddress{ + .host = enet.ENET_HOST_ANY, + .port = port, + }; + + self.host = enet.enet_host_create(&self.address.?, 128, 2, 0, 0) orelse { + return error.ServerStartFailed; + }; + + net.log("server started hosting on port {d}", .{port}); + + self.peerType = .server; +} + +pub fn connect(self: *@This(), target: []const u8) !void { + var i: isize = @as(isize, @intCast(target.len)) - 1; + var port: u16 = DEFAULT_PORT; + + var targetStr: ?[]const u8 = target; + var portString: ?[]const u8 = null; + + while (i > 0) : (i -= 1) { + if (target[@as(usize, @intCast(i))] == ':') { + portString = target[@as(usize, @intCast(i)) + 1 .. target.len]; + targetStr = target[0..@as(usize, @intCast(i))]; + break; + } + } + + if (portString) |ps| { + port = try std.fmt.parseInt(u16, ps, 0); + } + + var buf: [256]u8 = undefined; + const targetName = try std.fmt.bufPrintZ(&buf, "{s}", .{targetStr.?}); + + self.address = enet.ENetAddress{ + .host = 0, + .port = port, + }; + + _ = enet.enet_address_set_host(&self.address.?, targetName); +} + +pub fn sendMessage(self: *@This(), connectionId: u32, data: []const u8, reliable: bool) !void { + _ = self; + _ = connectionId; + _ = data; + _ = reliable; + // TODO: Implement message sending + return error.UnknownStatePanic; +} + +pub fn deinitialize(self: *@This()) void { + if (self.host) |host| { + enet.enet_host_destroy(host); + self.host = null; + } + self.clients.deinit(self.allocator); + enet_mod.deinitialize(); +} + +pub fn preTick(self: *@This(), dt: f64) !void { + switch (self.peerType) { + .server => { + self.tickServer(dt); + }, + .client => { + self.tickClient(dt); + }, + else => {}, + } +} + +pub fn tickServer(self: *@This(), dt: f64) void { + _ = dt; + + var event: enet.ENetEvent = undefined; + + const result = enet.enet_host_service(self.host.?, &event, 0); + + if (result > 0) { + switch (event.type) { + enet.ENET_EVENT_TYPE_CONNECT => { + // should add to clientsWaiting, + self.clients.append(self.allocator, .{ + .address = net.ip2String(self.allocator, event.peer.*.address.host) catch unreachable, + .port = event.peer.*.address.port, + .id = event.peer.*.connectID, + .data = event.peer, + }) catch unreachable; + + // Send server connection info + challenge + // const welcome_msg = "Welcome to ENet test server!"; + // if (enet.enet_packet_create(welcome_msg.ptr, welcome_msg.len, enet.ENET_PACKET_FLAG_RELIABLE)) |packet| { + //_ = enet.enet_peer_send(event.peer, 0, packet); + //} + }, + + enet.ENET_EVENT_TYPE_RECEIVE => { + const data = @as([*]u8, @ptrCast(event.packet.*.data))[0..event.packet.*.dataLength]; + _ = data; + // std.debug.print("Received message #{}: '{s}'\n", .{ message_count, data }); + + // Echo the message back to the client + // const echo_msg = std.fmt.allocPrint(std.heap.page_allocator, "Echo: {s}", .{data}) catch "Echo: (allocation failed)"; + // defer if (!std.mem.eql(u8, echo_msg, "Echo: (allocation failed)")) std.heap.page_allocator.free(echo_msg); + + // if (enet.enet_packet_create(echo_msg.ptr, echo_msg.len, enet.ENET_PACKET_FLAG_RELIABLE)) |packet| { + //_ = enet.enet_peer_send(event.peer, 0, packet); + //} + + // Destroy the received packet + enet.enet_packet_destroy(event.packet); + + // If this is a "quit" message, stop the server + // if (std.mem.eql(u8, data, "quit")) { + // std.debug.print("Received quit command, shutting down server...\n", .{}); + // } + }, + + enet.ENET_EVENT_TYPE_DISCONNECT => { + for (self.clients.items, 0..) |client, i| { + if (client.id == event.peer.*.connectID) { + _ = self.clients.swapRemove(i); + } + } + // event.peer.*.data = null; + }, + + else => {}, + } + } else if (result < 0) { + net.err("Error servicing host\n", .{}); + } +} + +pub fn tickClient(self: *@This(), dt: f64) void { + _ = self; + _ = dt; +} + +pub fn destroy(self: *@This()) void { + if (self.host) |host| { + enet.enet_host_destroy(host); + } + enet_mod.deinitialize(); + self.allocator.destroy(self); +} + +const core = @import("core"); +const net = @import("../net.zig"); +const std = @import("std"); +const enet_mod = @import("enet"); +const enet = enet_mod.c; diff --git a/engine/net/src/net.zig b/engine/net/src/net.zig index b16aef2..23e36b5 100644 --- a/engine/net/src/net.zig +++ b/engine/net/src/net.zig @@ -3,10 +3,10 @@ const std = @import("std"); const netEngine = @import("netEngine.zig"); pub const NetEngine = netEngine.NetEngine; -pub const net_err = netEngine.net_err; -pub const net_errs = netEngine.net_errs; -pub const net_log = netEngine.net_log; -pub const net_logs = netEngine.net_logs; +pub const err = netEngine.net_err; +pub const errs = netEngine.net_errs; +pub const log = netEngine.net_log; +pub const logs = netEngine.net_logs; pub fn start_module(spec: *core.SpecVariantMap, args: anytype, allocator: std.mem.Allocator) !void { _ = args; @@ -17,7 +17,8 @@ pub fn start_module(spec: *core.SpecVariantMap, args: anytype, allocator: std.me return; } - _ = core.createObject(NetEngine, .{ .can_tick = true }) catch unreachable; + const engine = try core.createObject(NetEngine, .{ .can_tick = true }); + try engine.initalizeTransport(netEngine.EnetTransport); } pub fn shutdown_module(allocator: std.mem.Allocator) void { @@ -31,3 +32,98 @@ pub const Module = core.ModuleDescription{ .name = "net", .enabledByDefault = false, }; + +pub const PeerType = enum { + offline, + server, + client, +}; + +pub const TransportType = enum { + enet, +}; + +pub const TransportError = error{ + UnknownStatePanic, + BadInit, + UnknownError, + OutOfMemory, + ServerStartFailed, +}; + +pub const ClientInfo = struct { + id: u32, + port: u16, + address: []const u8, + data: ?*anyopaque, +}; + +pub fn ip2String(allocator: std.mem.Allocator, ip: u32) ![:0]u8 { + return try std.fmt.allocPrintZ(allocator, "{d}.{d}.{d}.{d}", .{ + (ip >> 0) & 0xFF, + (ip >> 8) & 0xFF, + (ip >> 16) & 0xFF, + (ip >> 24) & 0xFF, + }); +} + +pub const TransportInterface = core.MakeInterface("TransportInterfaceVTable", struct { + startServer: *const fn (*anyopaque, bindInfo: ?[]const []const u8) TransportError!void, + stopServer: *const fn (*anyopaque) void, + + startClient: *const fn (*anyopaque, target: []const u8) TransportError!void, + + connect: *const fn (*anyopaque, target: []const u8) TransportError!void, + sendMessage: *const fn (*anyopaque, connectionId: u32, data: []const u8, reliable: bool) TransportError!void, + + deinitialize: *const fn (*anyopaque) void, + + pub fn Implement(comptime T: type) @This() { + const Wrap = struct { + pub fn startServer(p: *anyopaque, bindInfo: ?[]const []const u8) TransportError!void { + const ptr: *T = @ptrCast(@alignCast(p)); + ptr.startServer(bindInfo) catch return TransportError.ServerStartFailed; + } + + pub fn stopServer(p: *anyopaque) void { + const ptr: *T = @ptrCast(@alignCast(p)); + ptr.stopServer(); + } + + pub fn startClient(p: *anyopaque, target: []const u8) TransportError!void { + const ptr: *T = @ptrCast(@alignCast(p)); + ptr.startClient(target) catch return TransportError.UnknownStatePanic; + } + + pub fn connect(p: *anyopaque, target: []const u8) TransportError!void { + const ptr: *T = @ptrCast(@alignCast(p)); + ptr.connect(target) catch return TransportError.UnknownStatePanic; + } + + pub fn sendMessage(p: *anyopaque, connectionId: u32, data: []const u8, reliable: bool) TransportError!void { + const ptr: *T = @ptrCast(@alignCast(p)); + ptr.sendMessage(connectionId, data, reliable) catch return TransportError.UnknownStatePanic; + } + + pub fn deinitialize(p: *anyopaque) void { + const ptr: *T = @ptrCast(@alignCast(p)); + ptr.deinitialize(); + } + }; + + inline for (@typeInfo(Wrap).@"struct".decls) |d| { + if (!@hasDecl(T, d.name)) { + @compileError(@typeName(T) ++ " is missing implementation of func " ++ d.name); + } + } + + return .{ + .startServer = Wrap.startServer, + .stopServer = Wrap.stopServer, + .startClient = Wrap.startClient, + .connect = Wrap.connect, + .sendMessage = Wrap.sendMessage, + .deinitialize = Wrap.deinitialize, + }; + } +}); diff --git a/engine/net/src/netEngine.zig b/engine/net/src/netEngine.zig index b846f6f..32cc03f 100644 --- a/engine/net/src/netEngine.zig +++ b/engine/net/src/netEngine.zig @@ -1,5 +1,7 @@ const core = @import("core"); +const net = @import("net.zig"); const std = @import("std"); +pub const EnetTransport = @import("enet/EnetTransport.zig"); pub fn net_log(comptime fmt: []const u8, args: anytype) void { core.printInner("[NET ]: " ++ fmt ++ "\n", args); @@ -21,6 +23,7 @@ pub const NetEngine = struct { pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This(), "net.NetEngine"); allocator: std.mem.Allocator, + transport: ?core.Reference(net.TransportInterface) = null, pub fn init(allocator: std.mem.Allocator) !*@This() { const self = try allocator.create(@This()); @@ -28,10 +31,25 @@ pub const NetEngine = struct { .allocator = allocator, }; - net_logs("NetEngine initialized"); return self; } + pub fn initalizeTransport(self: *@This(), comptime T: type) !void { + const transport = try core.createObject(T, .{ .can_tick = true }); + self.transport = core.Reference(net.TransportInterface){ + .ptr = transport, + .vtable = T.TransportInterfaceVTable, + }; + + net.log("transport initialized {s}", .{@typeName(T)}); + } + + pub fn startServer(self: *@This(), bindInfo: ?[]const []const u8) !void { + if (self.transport) |transport| { + try transport.vtable.startServer(transport.ptr, bindInfo); + } + } + pub fn shutdown(self: *@This()) void { net_logs("NetEngine shutting down"); _ = self; @@ -46,4 +64,4 @@ pub const NetEngine = struct { _ = deltaTime; // Network tick logic will go here } -}; \ No newline at end of file +}; diff --git a/lib/enet/build.zig b/lib/enet/build.zig index 9f41b7e..0a8502b 100644 --- a/lib/enet/build.zig +++ b/lib/enet/build.zig @@ -4,10 +4,13 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const static_build = b.option(bool, "static_build", "builds backlog dependencies for static linking") orelse false; - _ = static_build; // enet is always built as static library // am i good to always have enet as static? - const enet = b.addStaticLibrary(.{ + const enet = if (static_build and false) b.addStaticLibrary(.{ + .name = "enet_c", + .target = target, + .optimize = optimize, + }) else b.addSharedLibrary(.{ .name = "enet_c", .target = target, .optimize = optimize, diff --git a/projects/build.zig.zon b/projects/build.zig.zon index 8fc9612..2491432 100644 --- a/projects/build.zig.zon +++ b/projects/build.zig.zon @@ -28,6 +28,8 @@ .sdl3 = .{ .path = "../lib/sdl3" }, .miniaudio = .{ .path = "../lib/miniaudio" }, .lua = .{ .path = "../lib/lua" }, + + .enet = .{ .path = "../lib/enet" }, }, .paths = .{ "", diff --git a/projects/sampleGame/externGame/externGame.zig b/projects/sampleGame/externGame/externGame.zig index 4d1652f..d9ef09e 100644 --- a/projects/sampleGame/externGame/externGame.zig +++ b/projects/sampleGame/externGame/externGame.zig @@ -6,6 +6,7 @@ pub export fn startup(p_allocator: *anyopaque, p_a: ?*anyopaque) bool { sys.setupFromModule(); rend.setupFromModule(); backlog.physics.setupFromModule(); + // backlog.net.setupFromModule(); // TODO backlog.setupFromModule core.engine_logs("creating externgame"); @@ -387,6 +388,10 @@ pub const ExternGameObject = struct { if (ig.sliderFloat("sliderFloat", &self.volume, 0, 100, null, .{})) { audio.context().setVolume(self.volume / 100); } + + if (ig.smallButton("host")) { + core.get(net.NetEngine).startServer(&.{"7777"}) catch unreachable; + } } ig.end(); @@ -429,6 +434,7 @@ const rend = backlog.rend; const ig = imgui.api; const sys = backlog.sys; const ui = backlog.ui; +const net = backlog.net; const audio = backlog.audio; const platform = backlog.platform; const extras = @import("gameExtras");