From bebc007d74c9b6258d082de455e8108ab2b55acc Mon Sep 17 00:00:00 2001 From: IntegratedQuantum Date: Mon, 5 Sep 2022 21:41:54 +0200 Subject: [PATCH] Add some networking code (namely the UDPConnection.java) and create a threadlocal allocator, to make simple allocations easier(without creating/passing around a new allocator every time). --- src/assets.zig | 42 +-- src/chunk.zig | 24 +- src/game.zig | 6 +- src/graphics.zig | 19 +- src/main.zig | 22 +- src/network.zig | 686 +++++++++++++++++++++++++++++++++++++++++++++++ src/renderer.zig | 124 ++++----- src/settings.zig | 3 + 8 files changed, 806 insertions(+), 120 deletions(-) create mode 100644 src/network.zig create mode 100644 src/settings.zig diff --git a/src/assets.zig b/src/assets.zig index 91f5a911..c6740620 100644 --- a/src/assets.zig +++ b/src/assets.zig @@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator; const json = @import("json.zig"); const JsonElement = json.JsonElement; const blocks_zig = @import("blocks.zig"); +const main = @import("main.zig"); var arena: std.heap.ArenaAllocator = undefined; var arenaAllocator: Allocator = undefined; @@ -13,11 +14,6 @@ var commonRecipes: std.ArrayList([]const u8) = undefined; /// Reads json files recursively from all subfolders. pub fn readAllJsonFilesInAddons(externalAllocator: Allocator, addons: std.ArrayList(std.fs.Dir), addonNames: std.ArrayList([]const u8), subPath: []const u8, output: *std.StringHashMap(JsonElement)) !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer if(gpa.deinit()) { - @panic("Memory leak"); - }; - var internalAllocator = gpa.allocator(); for(addons.items) |addon, addonIndex| { var dir: std.fs.IterableDir = addon.openIterableDir(subPath, .{}) catch |err| { if(err == error.FileNotFound) continue; @@ -25,7 +21,7 @@ pub fn readAllJsonFilesInAddons(externalAllocator: Allocator, addons: std.ArrayL }; defer dir.close(); - var walker = try dir.walk(internalAllocator); + var walker = try dir.walk(main.threadAllocator); defer walker.deinit(); while(try walker.next()) |entry| { @@ -39,18 +35,18 @@ pub fn readAllJsonFilesInAddons(externalAllocator: Allocator, addons: std.ArrayL std.log.info("ID: {s}", .{id}); var file = try dir.dir.openFile(entry.path, .{}); defer file.close(); - const string = try file.readToEndAlloc(internalAllocator, std.math.maxInt(usize)); - defer internalAllocator.free(string); + const string = try file.readToEndAlloc(main.threadAllocator, std.math.maxInt(usize)); + defer main.threadAllocator.free(string); try output.put(id, json.parseFromString(externalAllocator, string)); } } } } -pub fn readAssets(externalAllocator: Allocator, temporaryAllocator: Allocator, assetPath: []const u8, blocks: *std.StringHashMap(JsonElement), biomes: *std.StringHashMap(JsonElement)) !void { - var addons = std.ArrayList(std.fs.Dir).init(temporaryAllocator); +pub fn readAssets(externalAllocator: Allocator, assetPath: []const u8, blocks: *std.StringHashMap(JsonElement), biomes: *std.StringHashMap(JsonElement)) !void { + var addons = std.ArrayList(std.fs.Dir).init(main.threadAllocator); defer addons.deinit(); - var addonNames = std.ArrayList([]const u8).init(temporaryAllocator); + var addonNames = std.ArrayList([]const u8).init(main.threadAllocator); defer addonNames.deinit(); { // Find all the sub-directories to the assets folder. @@ -60,13 +56,13 @@ pub fn readAssets(externalAllocator: Allocator, temporaryAllocator: Allocator, a while(try iterator.next()) |addon| { if(addon.kind == .Directory) { try addons.append(try dir.dir.openDir(addon.name, .{})); - try addonNames.append(try temporaryAllocator.dupe(u8, addon.name)); + try addonNames.append(try main.threadAllocator.dupe(u8, addon.name)); } } } defer for(addons.items) |*dir, idx| { dir.close(); - temporaryAllocator.free(addonNames.items[idx]); + main.threadAllocator.free(addonNames.items[idx]); }; try readAllJsonFilesInAddons(externalAllocator, addons, addonNames, "blocks", blocks); @@ -74,19 +70,13 @@ pub fn readAssets(externalAllocator: Allocator, temporaryAllocator: Allocator, a } pub fn init() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - var gpaAllocator = gpa.allocator(); - defer if(gpa.deinit()) { - @panic("Memory leak"); - }; - arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); arenaAllocator = arena.allocator(); commonBlocks = std.StringHashMap(JsonElement).init(arenaAllocator); commonBiomes = std.StringHashMap(JsonElement).init(arenaAllocator); commonRecipes = std.ArrayList([]const u8).init(arenaAllocator); - try readAssets(arenaAllocator, gpaAllocator, "assets/", &commonBlocks, &commonBiomes); + try readAssets(arenaAllocator, "assets/", &commonBlocks, &commonBiomes); } pub fn registerBlock(assetFolder: []const u8, id: []const u8, info: JsonElement) !void { @@ -140,18 +130,12 @@ pub fn registerBlock(assetFolder: []const u8, id: []const u8, info: JsonElement) } pub fn loadWorldAssets(assetFolder: []const u8) !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - var gpaAllocator = gpa.allocator(); - defer if(gpa.deinit()) { - @panic("Memory leak"); - }; - - var blocks = try commonBlocks.cloneWithAllocator(gpaAllocator); + var blocks = try commonBlocks.cloneWithAllocator(main.threadAllocator); defer blocks.clearAndFree(); - var biomes = try commonBiomes.cloneWithAllocator(gpaAllocator); + var biomes = try commonBiomes.cloneWithAllocator(main.threadAllocator); defer biomes.clearAndFree(); - try readAssets(arenaAllocator, gpaAllocator, assetFolder, &blocks, &biomes); + try readAssets(arenaAllocator, assetFolder, &blocks, &biomes); var block: u32 = 0; // TODO: diff --git a/src/chunk.zig b/src/chunk.zig index 8052a4c3..63a449e4 100644 --- a/src/chunk.zig +++ b/src/chunk.zig @@ -3,6 +3,7 @@ const Allocator = std.mem.Allocator; const blocks = @import("blocks.zig"); const Block = blocks.Block; +const game = @import("game.zig"); const graphics = @import("graphics.zig"); const c = graphics.c; const Shader = graphics.Shader; @@ -88,9 +89,9 @@ pub const Chunk = struct { wasCleaned: bool = false, generated: bool = false, - width: ChunkPosition, + width: ChunkCoordinate, voxelSizeShift: u5, - voxelSizeMask: ChunkPosition, + voxelSizeMask: ChunkCoordinate, widthShift: u5, mutex: std.Thread.Mutex, @@ -476,7 +477,7 @@ pub const ChunkVisibilityData = struct { pub const meshing = struct { var shader: Shader = undefined; - var uniforms: struct { + pub var uniforms: struct { projectionMatrix: c_int, viewMatrix: c_int, modelPosition: c_int, @@ -498,8 +499,6 @@ pub const meshing = struct { var vbo: c_uint = undefined; var faces: std.ArrayList(u32) = undefined; -// TODO: public static final Matrix4f projMatrix = new Matrix4f(); - pub fn init() !void { shader = try Shader.create("assets/cubyz/shaders/chunks/chunk_vertex.vs", "assets/cubyz/shaders/chunks/chunk_fragment.fs"); uniforms = shader.bulkGetUniformLocation(@TypeOf(uniforms)); @@ -531,17 +530,16 @@ pub const meshing = struct { pub fn bindShaderAndUniforms(projMatrix: Mat4f, ambient: Vec3f, directional: Vec3f, time: u32) void { shader.bind(); -// TODO: -// shader.setUniform(loc_fog_activ, Cubyz.fog.isActive()); -// shader.setUniform(loc_fog_color, Cubyz.fog.getColor()); -// shader.setUniform(loc_fog_density, Cubyz.fog.getDensity()); + c.glUniform1i(uniforms.fog_activ, if(game.fog.active) 1 else 0); + c.glUniform3fv(uniforms.fog_color, 1, @ptrCast([*c]f32, &game.fog.color)); + c.glUniform1f(uniforms.fog_density, game.fog.density); c.glUniformMatrix4fv(uniforms.projectionMatrix, 1, c.GL_FALSE, @ptrCast([*c]f32, &projMatrix)); -// shader.setUniform(loc_texture_sampler, 0); -// shader.setUniform(loc_emissionSampler, 1); -// -// shader.setUniform(loc_viewMatrix, Camera.getViewMatrix()); + c.glUniform1i(uniforms.texture_sampler, 0); + c.glUniform1i(uniforms.emissionSampler, 1); + + c.glUniformMatrix4fv(uniforms.viewMatrix, 1, c.GL_FALSE, @ptrCast([*c]f32, &game.camera.viewMatrix)); c.glUniform3f(uniforms.ambientLight, ambient.x, ambient.y, ambient.z); c.glUniform3f(uniforms.directionalLight, directional.x, directional.y, directional.z); diff --git a/src/game.zig b/src/game.zig index bd760b91..4839265e 100644 --- a/src/game.zig +++ b/src/game.zig @@ -3,6 +3,8 @@ const std = @import("std"); const vec = @import("vec.zig"); const Vec3f = vec.Vec3f; const Mat4f = vec.Mat4f; +const graphics = @import("graphics.zig"); +const Fog = graphics.Fog; pub const camera = struct { var rotation: Vec3f = Vec3f{.x = 0, .y = 0, .z = 0}; @@ -32,4 +34,6 @@ pub var testWorld: World = 0; pub var world: ?*World = &testWorld; pub var projectionMatrix: Mat4f = Mat4f.identity(); -pub var lodProjectionMatrix: Mat4f = Mat4f.identity(); \ No newline at end of file +pub var lodProjectionMatrix: Mat4f = Mat4f.identity(); + +pub var fog = Fog{.active = true, .color=.{.x=0.5, .y=0.5, .z=0.5}, .density=0.025}; \ No newline at end of file diff --git a/src/graphics.zig b/src/graphics.zig index 10f0af67..9ae678fa 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -2,10 +2,13 @@ /// Also contains some basic 2d drawing stuff. const std = @import("std"); + const Vec4i = @import("vec.zig").Vec4i; const Vec2f = @import("vec.zig").Vec2f; +const Vec3f = @import("vec.zig").Vec3f; -const Window = @import("main.zig").Window; +const main = @import("main.zig"); +const Window = main.Window; const Allocator = std.mem.Allocator; @@ -329,11 +332,11 @@ pub const Shader = struct { id: c_uint, fn addShader(self: *const Shader, filename: []const u8, shader_stage: c_uint) !void { - const source = fileToString(std.heap.page_allocator, filename) catch |err| { + const source = fileToString(main.threadAllocator, filename) catch |err| { std.log.warn("Couldn't find file: {s}", .{filename}); return err; }; - defer std.heap.page_allocator.free(source); + defer main.threadAllocator.free(source); const ref_buffer = [_] [*c]u8 {@ptrCast([*c]u8, source.ptr)}; const shader = c.glCreateShader(shader_stage); defer c.glDeleteShader(shader); @@ -568,8 +571,8 @@ pub const Image = struct { pub fn readFromFile(allocator: Allocator, path: []const u8) !Image { var result: Image = undefined; var channel: c_int = undefined; - var buffer: [1024]u8 = undefined; - const nullTerminatedPath = try std.fmt.bufPrintZ(&buffer, "{s}", .{path}); // TODO: Find a more zig-friendly image loading library. + const nullTerminatedPath = try std.fmt.allocPrintZ(main.threadAllocator, "{s}", .{path}); // TODO: Find a more zig-friendly image loading library. + defer main.threadAllocator.free(nullTerminatedPath); const data = stb_image.stbi_load(nullTerminatedPath.ptr, @ptrCast([*c]c_int, &result.width), @ptrCast([*c]c_int, &result.height), &channel, 4) orelse { return error.FileNotFound; }; @@ -577,4 +580,10 @@ pub const Image = struct { stb_image.stbi_image_free(data); return result; } +}; + +pub const Fog = struct { + active: bool, + color: Vec3f, + density: f32, }; \ No newline at end of file diff --git a/src/main.zig b/src/main.zig index c80859e6..e42bcd8c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,6 +5,7 @@ const blocks = @import("blocks.zig"); const chunk = @import("chunk.zig"); const graphics = @import("graphics.zig"); const renderer = @import("renderer.zig"); +const network = @import("network.zig"); const Vec2f = @import("vec.zig").Vec2f; @@ -13,6 +14,8 @@ pub const c = @cImport ({ @cInclude("GLFW/glfw3.h"); }); +pub threadlocal var threadAllocator: std.mem.Allocator = undefined; + var logFile: std.fs.File = undefined; pub fn log( @@ -27,15 +30,16 @@ pub fn log( std.log.Level.warn => "\x1b[33m", std.log.Level.debug => "\x1b[37;44m", }; - var buf: [4096]u8 = undefined; std.debug.getStderrMutex().lock(); defer std.debug.getStderrMutex().unlock(); - const fileMessage = std.fmt.bufPrint(&buf, "[" ++ level.asText() ++ "]" ++ ": " ++ format ++ "\n", args) catch return; + const fileMessage = std.fmt.allocPrint(threadAllocator, "[" ++ level.asText() ++ "]" ++ ": " ++ format ++ "\n", args) catch return; + defer threadAllocator.free(fileMessage); logFile.writeAll(fileMessage) catch return; - const terminalMessage = std.fmt.bufPrint(&buf, color ++ format ++ "\x1b[0m\n", args) catch return; + const terminalMessage = std.fmt.allocPrint(threadAllocator, color ++ format ++ "\x1b[0m\n", args) catch return; + defer threadAllocator.free(terminalMessage); nosuspend std.io.getStdErr().writeAll(terminalMessage) catch return; } @@ -130,6 +134,12 @@ pub const Window = struct { }; pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + threadAllocator = gpa.allocator(); + defer if(gpa.deinit()) { + @panic("Memory leak"); + }; + // init logging. logFile = std.fs.cwd().createFile("logs/latest.log", .{}) catch unreachable; defer logFile.close(); @@ -154,6 +164,9 @@ pub fn main() !void { try assets.loadWorldAssets("saves"); + var conn = try network.ConnectionManager.init(12345, true); + defer conn.deinit(); + c.glEnable(c.GL_CULL_FACE); c.glCullFace(c.GL_BACK); c.glEnable(c.GL_BLEND); @@ -183,6 +196,9 @@ pub fn main() !void { graphics.Draw.line(Vec2f{.x = 0, .y = 0}, Vec2f{.x = 1920, .y = 1080}); } } + + var conn2 = try network.Connection.init(conn, "127.0.0.1:12345"); + conn2.deinit(); } test "abc" { diff --git a/src/network.zig b/src/network.zig new file mode 100644 index 00000000..a652d074 --- /dev/null +++ b/src/network.zig @@ -0,0 +1,686 @@ +const std = @import("std"); + +const main = @import("main.zig"); +const game = @import("game.zig"); +const settings = @import("settings.zig"); + +//TODO: Might want to use SSL or something similar to encode the message + +const LinuxSocket = struct { + const c = @cImport({ + @cInclude("sys/socket.h"); + @cInclude("netinet/in.h"); + @cInclude("sys/types.h"); + @cInclude("unistd.h"); + @cInclude("string.h"); + @cInclude("errno.h"); + @cInclude("stdio.h"); + @cInclude("arpa/inet.h"); + }); + + socketID: u31, + + fn checkError(comptime msg: []const u8, result: c_int) !u31 { + if(result == -1) { + std.log.warn(msg, .{c.__errno_location().*}); + return error.SocketError; + } + return @intCast(u31, result); + } + + fn init(localPort: u16) !LinuxSocket { + var socketID: u31 = undefined; + socketID = try checkError("Socket creation failed with error: {}", c.socket(c.AF_INET, c.SOCK_DGRAM, c.IPPROTO_UDP)); + errdefer _ = checkError("Error while closing socket: {}", c.close(socketID)) catch 0; + var bindingAddr: c.sockaddr_in = undefined; + bindingAddr.sin_family = c.AF_INET; + bindingAddr.sin_port = c.htons(localPort); + bindingAddr.sin_addr.s_addr = c.inet_addr("127.0.0.1"); + bindingAddr.sin_zero = [_]u8{0} ** 8; + _ = try checkError("Socket binding failed with error: {}", c.bind(socketID, @ptrCast(*c.sockaddr, &bindingAddr), @sizeOf(c.sockaddr_in))); // TODO: Use the next higher port, when the port is already in use. + return LinuxSocket{.socketID = socketID}; + } + + fn deinit(self: LinuxSocket) void { + _ = checkError("Error while closing socket: {}", c.close(self.socketID)) catch 0; + } +}; + +pub const Address = struct { + ip: []const u8, + port: u16, +}; + +pub const ConnectionManager = struct { + socket: LinuxSocket = undefined, + thread: std.Thread = undefined, + online: bool = false, + + pub fn init(localPort: u16, online: bool) !ConnectionManager { + _ = online; //TODO + var result = ConnectionManager{}; + result.socket = try LinuxSocket.init(localPort); + errdefer LinuxSocket.deinit(result.socket); + + result.thread = try std.Thread.spawn(.{}, run, .{result}); + if(online) { + result.makeOnline(); + } + return result; + } + + pub fn deinit(self: ConnectionManager) void { + LinuxSocket.deinit(self.socket); + self.thread.join(); + } + + pub fn makeOnline(self: *ConnectionManager) void { + if(!self.online) { + // TODO: +// externalIPPort = STUN.requestIPPort(this); +// String[] ipPort; +// if(externalIPPort.contains("?")) { +// ipPort = externalIPPort.split(":\\?"); +// } else { +// ipPort = externalIPPort.split(":"); +// } +// try { +// externalAddress = InetAddress.getByName(ipPort[0]); +// } catch(UnknownHostException e) { +// Logger.error(e); +// throw new IllegalArgumentException("externalIPPort is invalid."); +// } +// externalPort = Integer.parseInt(ipPort[1]); + self.online = true; + } + } + + pub fn run(self: ConnectionManager) void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + main.threadAllocator = gpa.allocator(); + defer if(gpa.deinit()) { + @panic("Memory leak"); + }; + + _ = self; // TODO + } + + pub fn send(self: ConnectionManager, data: []const u8, target: Address) void { + // TODO + _ = self; + _ = data; + _ = target; + } +}; +// sockID = c.socket(c.AF_INET, c.SOCK_DGRAM, c.IPPROTO_UDP); +// defer _ = c.close(sockID); +// _ = c.memset(&otherAddr, 0, @sizeOf(c.sockaddr_in)); +// otherAddr.sin_family = c.AF_INET; +// otherAddr.sin_port = c.htons(40001); +// otherAddr.sin_addr.s_addr = c.inet_addr("???.???.???.???"); +// var myAddr: c.sockaddr_in = undefined; +// _ = c.memset(&myAddr, 0, @sizeOf(c.sockaddr_in)); +// myAddr.sin_family = c.AF_INET; +// myAddr.sin_port = c.htons(40001); +// myAddr.sin_addr.s_addr = c.inet_addr("192.168.178.60"); +// +// _ = errorCheck(c.bind(sockID, @ptrCast(*c.sockaddr, &myAddr), @sizeOf(c.sockaddr_in))); +// +// _ = std.Thread.spawn(.{}, keepAlive, .{}) catch null; +//public final class UDPConnectionManager extends Thread { +// private final DatagramPacket receivedPacket; +// public final ArrayList connections = new ArrayList<>(); +// private final ArrayList requests = new ArrayList<>(); +// private volatile boolean running = true; +// public String externalIPPort = null; +// private InetAddress externalAddress = null; +// private int externalPort = 0; +// +// public void send(DatagramPacket packet) { +// try { +// socket.send(packet); +// } catch(IOException e) { +// Logger.error(e); +// } +// } +// +// public byte[] sendRequest(DatagramPacket packet, long timeout) { +// send(packet); +// byte[] request = packet.getData(); +// synchronized(requests) { +// requests.add(packet); +// } +// synchronized(packet) { +// try { +// packet.wait(timeout); +// } catch(InterruptedException e) {} +// } +// synchronized(requests) { +// requests.remove(packet); +// } +// if(packet.getData() == request) { +// return null; +// } else { +// return packet.getData(); +// } +// } +// +// public void addConnection(UDPConnection connection) { +// synchronized(connections) { +// connections.add(connection); +// } +// } +// +// public void removeConnection(UDPConnection connection) { +// synchronized(connections) { +// connections.remove(connection); +// } +// } +// +// public void cleanup() { +// while(!connections.isEmpty()) { +// connections.get(0).disconnect(); +// } +// running = false; +// if(Thread.currentThread() != this) { +// interrupt(); +// try { +// join(); +// } catch(InterruptedException e) { +// Logger.error(e); +// } +// } +// socket.close(); +// } +// +// private void onReceive() { +// byte[] data = receivedPacket.getData(); +// int len = receivedPacket.getLength(); +// InetAddress addr = receivedPacket.getAddress(); +// int port = receivedPacket.getPort(); +// for(UDPConnection connection : connections) { +// if(connection.remoteAddress.equals(addr)) { +// if(connection.bruteforcingPort) { // brute-forcing the port was successful. +// connection.remotePort = port; +// connection.bruteforcingPort = false; +// } +// if(connection.remotePort == port) { +// connection.receive(data, len); +// return; +// } +// } +// } +// // Check if it's part of an active request: +// synchronized(requests) { +// for(DatagramPacket packet : requests) { +// if(packet.getAddress().equals(addr) && packet.getPort() == port) { +// packet.setData(Arrays.copyOf(data, len)); +// synchronized(packet) { +// packet.notify(); +// } +// return; +// } +// } +// } +// if(addr.equals(externalAddress) && port == externalPort) return; +// if(addr.toString().contains("127.0.0.1")) return; +// Logger.warning("Unknown connection from address: " + addr+":"+port); +// Logger.debug("Message: "+Arrays.toString(Arrays.copyOf(data, len))); +// } +// +// @Override +// public void run() { +// assert Thread.currentThread() == this : "UDPConnectionManager.run() shouldn't be called by anyone."; +// try { +// socket.setSoTimeout(100); +// long lastTime = System.currentTimeMillis(); +// while (running) { +// try { +// socket.receive(receivedPacket); +// onReceive(); +// } catch(SocketTimeoutException e) { +// // No message within the last ~100 ms. +// } +// +// // Send a keep-alive packet roughly every 100 ms: +// if(System.currentTimeMillis() - lastTime > 100 && running) { +// lastTime = System.currentTimeMillis(); +// for(UDPConnection connection : connections.toArray(new UDPConnection[0])) { +// if(lastTime - connection.lastConnection > CONNECTION_TIMEOUT && connection.isConnected()) { +// Logger.info("timeout"); +// // Timeout a connection if it was connect at some point. New connections are not timed out because that could annoy players(having to restart the connection several times). +// connection.disconnect(); +// } else { +// connection.sendKeepAlive(); +// } +// } +// if(connections.isEmpty() && externalAddress != null) { +// // Send a message to external ip, to keep the port open: +// DatagramPacket packet = new DatagramPacket(new byte[0], 0); +// packet.setAddress(externalAddress); +// packet.setPort(externalPort); +// packet.setLength(0); +// send(packet); +// } +// } +// } +// } catch (Exception e) { +// Logger.crash(e); +// } +// } +//} + +const UnconfirmedPacket = struct { + data: []const u8, + lastKeepAliveSentBefore: u32, + id: u32, +}; + +const Protocol = struct { + id: u8, + const keepAlive: u8 = 0; + const important: u8 = 0xff; +}; // TODO + + +pub const Connection = struct { + const maxPacketSize: u32 = 65507; // max udp packet size + const importantHeaderSize: u32 = 5; + const maxImportantPacketSize: u32 = 1500 - 20 - 8; // Ethernet MTU minus IP header minus udp header + + // Statistics: + var packetsSent: u32 = 0; + var packetsResent: u32 = 0; + + manager: ConnectionManager, + + gpa: std.heap.GeneralPurposeAllocator(.{}), + allocator: std.mem.Allocator, + + remoteAddress: Address, + bruteforcingPort: bool = false, + bruteForcedPortRange: u16 = 0, + + streamBuffer: [maxImportantPacketSize]u8 = undefined, + streamPosition: u32 = importantHeaderSize, + messageID: u32 = 0, + unconfirmedPackets: std.ArrayList(UnconfirmedPacket), + receivedPackets: [3]std.ArrayList(u32), + lastReceivedPackets: [65536]?[]const u8 = undefined, + lastIndex: u32 = 0, + + lastIncompletePacket: u32 = 0, + + lastKeepAliveSent: u32 = 0, + lastKeepAliveReceived: u32 = 0, + otherKeepAliveReceived: u32 = 0, + + disconnected: bool = false, + handShakeComplete: bool = false, + lastConnection: i64 = 0, + + mutex: std.Thread.Mutex = std.Thread.Mutex{}, + + pub fn init(manager: ConnectionManager, ipPort: []const u8) !*Connection { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var result: *Connection = try gpa.allocator().create(Connection); + result.* = Connection { + .manager = manager, + .gpa = gpa, + .allocator = undefined, + .remoteAddress = undefined, + .unconfirmedPackets = std.ArrayList(UnconfirmedPacket).init(gpa.allocator()), + .receivedPackets = [3]std.ArrayList(u32){ + std.ArrayList(u32).init(gpa.allocator()), + std.ArrayList(u32).init(gpa.allocator()), + std.ArrayList(u32).init(gpa.allocator()), + }, + }; + result.allocator = result.gpa.allocator(); // The right reference(the one that isn't on the stack) needs to be used passed! + var splitter = std.mem.split(u8, ipPort, ":"); + result.remoteAddress.ip = try result.allocator.dupe(u8, splitter.first()); + var port = splitter.rest(); + if(port.len != 0 and port[0] == '?') { + result.bruteforcingPort = true; + port = port[1..]; + } + result.remoteAddress.port = std.fmt.parseUnsigned(u16, port, 10) catch blk: { + std.log.warn("Could not parse port \"{s}\". Using default port instead.", .{port}); + break :blk settings.defaultPort; + }; + + // TODO: manager.addConnection(this); + return result; + } + + pub fn deinit(self: *Connection) void { + self.unconfirmedPackets.deinit(); + self.receivedPackets[0].deinit(); + self.receivedPackets[1].deinit(); + self.receivedPackets[2].deinit(); + self.allocator.free(self.remoteAddress.ip); + var gpa = self.gpa; + gpa.allocator().destroy(self); + if(gpa.deinit()) { + @panic("Memory leak in connection."); + } + } + + fn flush(self: *Connection) !void { + self.mutex.lock(); + defer self.mutex.unlock(); + + if(self.streamPosition == importantHeaderSize) return; // Don't send empty packets. + // Fill the header: + self.streamBuffer[0] = Protocol.important; + var id = self.messageID; + self.messageID += 1; + std.mem.writeIntBig(u32, self.streamBuffer[1..5], id); // TODO: Use little endian for better hardware support. Currently the aim is interoperability with the java version which uses big endian. + + var packet = UnconfirmedPacket{ + .data = try self.allocator.dupe(u8, self.streamBuffer[0..self.streamPosition]), + .lastKeepAliveSentBefore = self.lastKeepAliveSent, + .id = id, + }; + try self.unconfirmedPackets.append(packet); + packetsSent += 1; + self.manager.send(packet.data, self.remoteAddress); + self.streamPosition = importantHeaderSize; + } + + fn writeByteToStream(self: *Connection, data: u8) void { + self.streamBuffer[self.streamPosition] = data; + self.streamPosition += 1; + if(self.streamPosition == self.streamBuffer.length) { + self.flush(); + } + } + + pub fn sendImportant(self: *Connection, source: Protocol, data: []const u8) void { + self.mutex.lock(); + defer self.mutex.unlock(); + + if(self.disconnected) return; + self.writeByteToStream(source.id); + var processedLength = data.len; + while(processedLength > 0x7f) { + self.writeByteToStream(@intCast(u8, processedLength & 0x7f) | 0x80); + processedLength >>= 7; + } + self.writeByteToStream(@intCast(u8, processedLength & 0x7f)); + + var remaining: []const u8 = data; + while(remaining.len != 0) { + var copyableSize = @minimum(remaining.len, self.streamBuffer.len - self.streamPosition); + std.mem.copy(u8, self.streamBuffer, remaining[0..copyableSize]); + remaining = remaining[copyableSize..]; + self.streamPosition += copyableSize; + if(self.streamPosition == self.streamBuffer.len) { + self.flush(); + } + } + } + + pub fn sendUnimportant(self: *Connection, source: Protocol, data: []const u8) !void { + self.mutex.lock(); + defer self.mutex.unlock(); + + if(self.disconnected) return; + std.debug.assert(data.len + 1 < maxPacketSize); + var fullData = try main.threadAllocator.alloc(u8, data.len + 1); + defer main.threadAllocator.free(fullData); + fullData[0] = source.id; + std.mem.copy(u8, fullData[1..], data); + self.manager.send(fullData, self.remoteAddress); + } + + fn receiveKeepAlive(self: *Connection, data: []const u8) void { + self.mutex.lock(); + defer self.mutex.unlock(); + + self.otherKeepAliveReceived = std.mem.readIntBig(u32, data[0..4]); + self.lastKeepAliveReceived = std.mem.readIntBig(u32, data[4..8]); + var remaining: []const u8 = data[8..]; + while(remaining.len >= 8) { + var start = std.mem.readIntBig(u32, data[0..4]); + var len = std.mem.readIntBig(u32, data[4..8]); + var j: usize = 0; + while(j < self.unconfirmedPackets.items.len): (j += 1) { + var diff = self.unconfirmedPackets.items[j].id -% start; + if(diff < len) { + _ = self.unconfirmedPackets.swapRemove(j); + j -= 1; + } + } + } + } + + fn sendKeepAlive(self: *Connection) void { + self.mutex.lock(); + defer self.mutex.unlock(); + + var runLengthEncodingStarts: std.ArrayList(u32) = std.ArrayList(u32).init(main.threadAllocator); + defer runLengthEncodingStarts.deinit(); + var runLengthEncodingLengths: std.ArrayList(u32) = std.ArrayList(u32).init(main.threadAllocator); + defer runLengthEncodingLengths.deinit(); + + for(self.receivedPackets) |list| { + for(list.items) |packetID| { + var leftRegion: ?u32 = null; + var rightRegion: ?u32 = null; + for(runLengthEncodingStarts) |start, reg| { + var diff = packetID -% start; + if(diff < runLengthEncodingLengths.items[reg]) continue; + if(diff == runLengthEncodingLengths.items[reg]) { + leftRegion = reg; + } + if(diff == std.math.maxInt(u32)) { + rightRegion == reg; + } + } + if(leftRegion) |left| { + if(rightRegion) |right| { + // Needs to combine the regions: + runLengthEncodingLengths.items[left] += runLengthEncodingLengths.items[right] + 1; + runLengthEncodingStarts.swapRemove(right); + runLengthEncodingLengths.swapRemove(right); + } else { + runLengthEncodingLengths.items[left] += 1; + } + } else if(rightRegion) |right| { + runLengthEncodingStarts.items[right] -= 1; + runLengthEncodingLengths.items[right] += 1; + } else { + try runLengthEncodingStarts.append(packetID); + try runLengthEncodingLengths.append(1); + } + } + } + { // Cycle the receivedPackets lists: + var putBackToFront: std.ArrayList(u32) = self.receivedPackets[self.receivedPackets.len - 1]; + var i: u32 = self.receivedPackets.len - 1; + while(i >= 1): (i -= 1) { + self.receivedPackets[i] = self.receivedPackets[i-1]; + } + self.receivedPackets[0] = putBackToFront; + self.receivedPackets[0].clearRetainingCapacity(); + } + var output = try main.threadAllocator.alloc(u8, runLengthEncodingStarts.items.len*8 + 9); + defer main.threadAllocator.free(output); + output[0] = Protocol.keepAlive; + std.mem.writeIntBig(u32, output[1..5], self.lastKeepAliveSent); + self.lastKeepAliveSent += 1; + std.mem.writeIntBig(u32, output[5..9], self.otherKeepAliveReceived); + var remaining: []const u8 = output[9..]; + for(runLengthEncodingStarts) |_, i| { + std.mem.writeIntBig(u32, remaining[0..4], self.runLengthEncodingStarts.items[i]); + std.mem.writeIntBig(u32, remaining[4..8], self.runLengthEncodingLengths.items[i]); + remaining = remaining[8..]; + } + self.manager.send(output, self.remoteAddress); + + // Resend packets that didn't receive confirmation within the last 2 keep-alive signals. + for(self.unconfirmedPackets.items) |*packet| { + if(self.lastKeepAliveReceived - packet.lastKeepAliveSentBefore >= 2) { + packetsSent += 1; + packetsResent += 1; + self.manager.send(packet.data, self.remoteAddress); + packet.lastKeepAliveSentBefore = self.lastKeepAliveSent; + } + } + self.flush(); + if(self.bruteforcingPort) { + // This is called every 100 ms, so if I send 10 requests it shouldn't be too bad. + var i: u16 = 0; + while(i < 5): (i += 1) { + var data = [0]u8{}; + if(self.remoteAddress.port +% self.bruteForcedPortRange != 0) { + self.manager.send(data, Address{self.remoteAddress.ip, self.remoteAddress.port +% self.bruteForcedPortRange}); + } + if(self.remoteAddress.port - self.bruteForcedPortRange != 0) { + self.manager.send(data, Address{self.remoteAddress.ip, self.remoteAddress.port -% self.bruteForcedPortRange}); + } + self.bruteForcedPortRange +%= 1; + } + } + } + + pub fn isConnected(self: *Connection) bool { + self.mutex.lock(); + defer self.mutex.unlock(); + + return self.otherKeepAliveReceived != 0; + } + + fn collectPackets(self: *Connection) !void { + self.mutex.lock(); + defer self.mutex.unlock(); + + while(true) { + var id = self.lastIncompletePacket; + var receivedPacket = self.lastReceivedPackets[id & 65535] orelse return; + var newIndex = self.lastIndex; + var protocol = receivedPacket[newIndex]; + newIndex += 1; + // TODO: + _ = protocol; +// if(Cubyz.world == null && protocol != Protocols.HANDSHAKE.id) +// return; + + // Determine the next packet length: + var len: u32 = 0; + var shift: u32 = 0; + while(true) { + if(newIndex == receivedPacket.len) { + newIndex = 0; + id += 1; + receivedPacket = self.lastReceivedPackets[id & 65535] orelse return; + } + var nextByte = receivedPacket[newIndex]; + newIndex += 1; + len |= (nextByte & 0x7f) << shift; + if(nextByte & 0x80 != 0) { + shift += 7; + } else { + break; + } + } + + // Check if there is enough data available to fill the packets needs: + var dataAvailable = receivedPacket.len - newIndex; + var idd = id + 1; + while(dataAvailable < len): (idd += 1) { + var otherPacket = self.lastReceivedPackets[idd & 65535] orelse return; + dataAvailable += otherPacket.len; + } + + // Copy the data to an array: + var data = try main.threadAllocator.alloc(u8, len); + defer main.threadAllocator.free(data); + var remaining = data[0..]; + while(remaining.len != 0) { + dataAvailable = @minimum(self.lastReceivedPackets[id & 65535].?.len - newIndex, remaining.len); + std.mem.copy(u8, remaining, self.lastReceivedPackets[id & 65535].?[newIndex..dataAvailable]); + newIndex += dataAvailable; + remaining = remaining[dataAvailable..]; + if(newIndex == self.lastReceivedPackets[id & 65535].?.len) { + id += 1; + newIndex = 0; + } + } + while(self.lastIncompletePacket != id): (self.lastIncompletePacket += 1) { + self.allocator.free(self.lastReceivedPackets[self.lastIncompletePacket & 65535].?); + self.lastReceivedPackets[self.lastIncompletePacket & 65535] = null; + } + self.lastIndex = newIndex; + // TODO: +// Protocols.bytesReceived[protocol & 0xff] += data.length + 1; +// Protocols.list[protocol].receive(this, data, 0, data.length); + } + } + + pub fn receive(self: *Connection, data: []const u8) void { + self.mutex.lock(); + defer self.mutex.unlock(); + + const protocol = data[0]; + // TODO: + //if(!self.handShakeComplete and protocol != Protocols.HANDSHAKE.id and protocol != Protocol.KEEP_ALIVE and protocol != Protocol.important) { + // return; // Reject all non-handshake packets until the handshake is done. + //} + self.lastConnection = std.time.milliTimestamp(); + // TODO: +// Protocols.bytesReceived[protocol & 0xff] += len + 20 + 8; // Including IP header and udp header +// Protocols.packetsReceived[protocol & 0xff]++; + if(protocol == Protocol.important) { + var id = std.mem.readIntBig(u32, data[1..5]); + if(self.handShakeComplete and id == 0) { // Got a new "first" packet from client. So the client tries to reconnect, but we still think it's connected. + // TODO: +// if(this instanceof User) { +// Server.disconnect((User)this); +// disconnected = true; +// manager.removeConnection(this); +// new Thread(() -> { +// try { +// Server.connect(new User(manager, remoteAddress.getHostAddress() + ":" + remotePort)); +// } catch(Throwable e) { +// Logger.error(e); +// } +// }).start(); +// return; +// } else { +// Logger.error("Server 'reconnected'? This makes no sense and the game can't handle that."); +// } + } + if(id - self.lastIncompletePacket >= 65536) { + std.log.warn("Many incomplete packages. Cannot process any more packages for now.", .{}); + return; + } + try self.receivedPackets[0].append(id); + if(id < self.lastIncompletePacket or self.lastReceivedPackets[id & 65535] != null) { + return; // Already received the package in the past. + } + self.lastReceivedPackets[id & 65535] = self.allocator.dupe(data[importantHeaderSize..]); + // Check if a message got completed: + self.collectPackets(); + } else if(protocol == Protocol.keepAlive) { + self.receiveKeepAlive(data[1..]); + } else { + // TODO: Protocols.list[protocol & 0xff].receive(this, data, 1, len - 1); + } + } + + pub fn disconnect(self: *Connection) void { + // Send 3 disconnect packages to the other side, just to be sure. + // If all of them don't get through then there is probably a network issue anyways which would lead to a timeout. + // TODO: +// Protocols.DISCONNECT.disconnect(this); +// try {Thread.sleep(10);} catch(Exception e) {} +// Protocols.DISCONNECT.disconnect(this); +// try {Thread.sleep(10);} catch(Exception e) {} +// Protocols.DISCONNECT.disconnect(this); + self.disconnected = true; + // TODO: manager.removeConnection(self); + std.log.info("Disconnected"); + } +}; \ No newline at end of file diff --git a/src/renderer.zig b/src/renderer.zig index d0f81891..a9955428 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -3,6 +3,7 @@ const std = @import("std"); const blocks = @import("blocks.zig"); const graphics = @import("graphics.zig"); const c = graphics.c; +const Fog = graphics.Fog; const Shader = graphics.Shader; const vec = @import("vec.zig"); const Vec3f = vec.Vec3f; @@ -224,7 +225,7 @@ pub fn renderWorld(world: *World, ambientLight: Vec3f, directionalLight: Vec3f, // frustumInt.set(frustumMatrix); const time = @intCast(u32, std.time.milliTimestamp() & std.math.maxInt(u32)); -//TODO: Fog waterFog = new Fog(true, new Vector3f(0.0f, 0.1f, 0.2f), 0.1f); + const waterFog = Fog{.active=true, .color=.{.x=0.0, .y=0.1, .z=0.2}, .density=0.1}; // Update the uniforms. The uniforms are needed to render the replacement meshes. chunk.meshing.bindShaderAndUniforms(game.projectionMatrix, ambientLight, directionalLight, time); @@ -242,78 +243,63 @@ pub fn renderWorld(world: *World, ambientLight: Vec3f, directionalLight: Vec3f, // } c.glDepthRange(0, 0.05); -// -// SimpleList visibleChunks = new SimpleList(new NormalChunkMesh[64]); -// SimpleList visibleReduced = new SimpleList(new ReducedChunkMesh[64]); -// for (ChunkMesh mesh : Cubyz.chunkTree.getRenderChunks(frustumInt, x0, y0, z0)) { -// if (mesh instanceof NormalChunkMesh) { -// visibleChunks.add((NormalChunkMesh)mesh); -// -// mesh.render(playerPosition); -// } else if (mesh instanceof ReducedChunkMesh) { -// visibleReduced.add((ReducedChunkMesh)mesh); -// } + +// SimpleList visibleChunks = new SimpleList(new NormalChunkMesh[64]); +// SimpleList visibleReduced = new SimpleList(new ReducedChunkMesh[64]); +// for (ChunkMesh mesh : Cubyz.chunkTree.getRenderChunks(frustumInt, x0, y0, z0)) { +// if (mesh instanceof NormalChunkMesh) { +// visibleChunks.add((NormalChunkMesh)mesh); +// +// mesh.render(playerPosition); +// } else if (mesh instanceof ReducedChunkMesh) { +// visibleReduced.add((ReducedChunkMesh)mesh); // } -// if(selected != null && !Blocks.transparent(selected.getBlock())) { -// BlockBreakingRenderer.render(selected, playerPosition); -// glActiveTexture(GL_TEXTURE0); -// Meshes.blockTextureArray.bind(); -// glActiveTexture(GL_TEXTURE1); -// Meshes.emissionTextureArray.bind(); -// } -// -// // Render the far away ReducedChunks: -// glDepthRangef(0.05f, 1.0f); // ← Used to fix z-fighting. -// ReducedChunkMesh.bindShader(ambientLight, directionalLight.getDirection(), time); -// ReducedChunkMesh.shader.setUniform(ReducedChunkMesh.loc_waterFog_activ, waterFog.isActive()); -// ReducedChunkMesh.shader.setUniform(ReducedChunkMesh.loc_waterFog_color, waterFog.getColor()); -// ReducedChunkMesh.shader.setUniform(ReducedChunkMesh.loc_waterFog_density, waterFog.getDensity()); -// +// } +// if(selected != null && !Blocks.transparent(selected.getBlock())) { +// BlockBreakingRenderer.render(selected, playerPosition); + c.glActiveTexture(c.GL_TEXTURE0); + blocks.meshes.blockTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE1); + blocks.meshes.emissionTextureArray.bind(); +// } + + // Render the far away ReducedChunks: + c.glDepthRangef(0.05, 1.0); // ← Used to fix z-fighting. + chunk.meshing.bindShaderAndUniforms(game.projectionMatrix, ambientLight, directionalLight, time); + c.glUniform1i(chunk.meshing.uniforms.waterFog_activ, if(waterFog.active) 1 else 0); + c.glUniform3fv(chunk.meshing.uniforms.waterFog_color, 1, @ptrCast([*c]f32, &waterFog.color)); + c.glUniform1f(chunk.meshing.uniforms.waterFog_density, waterFog.density); + // for(int i = 0; i < visibleReduced.size; i++) { // ReducedChunkMesh mesh = visibleReduced.array[i]; // mesh.render(playerPosition); // } -// glDepthRangef(0, 0.05f); -// + c.glDepthRangef(0, 0.05); + // EntityRenderer.render(ambientLight, directionalLight, playerPosition); -// + // BlockDropRenderer.render(frustumInt, ambientLight, directionalLight, playerPosition); -// -// /*NormalChunkMesh.shader.bind(); -// NormalChunkMesh.shader.setUniform(NormalChunkMesh.loc_fog_activ, 0); // manually disable the fog -// for (int i = 0; i < spatials.length; i++) { -// Spatial spatial = spatials[i]; -// Mesh mesh = spatial.getMesh(); -// EntityRenderer.entityShader.setUniform(EntityRenderer.loc_light, new Vector3f(1, 1, 1)); -// EntityRenderer.entityShader.setUniform(EntityRenderer.loc_materialHasTexture, mesh.getMaterial().isTextured()); -// mesh.renderOne(() -> { -// Matrix4f modelViewMatrix = Transformation.getModelViewMatrix( -// Transformation.getModelMatrix(spatial.getPosition(), spatial.getRotation(), spatial.getScale()), -// Camera.getViewMatrix()); -// EntityRenderer.entityShader.setUniform(EntityRenderer.loc_viewMatrix, modelViewMatrix); -// }); -// }*/ // TODO: Draw the sun. -// + // // Render transparent chunk meshes: // NormalChunkMesh.bindTransparentShader(ambientLight, directionalLight.getDirection(), time); -// -// buffers.bindTextures(); -// + + buffers.bindTextures(); + // NormalChunkMesh.transparentShader.setUniform(NormalChunkMesh.TransparentUniforms.loc_waterFog_activ, waterFog.isActive()); // NormalChunkMesh.transparentShader.setUniform(NormalChunkMesh.TransparentUniforms.loc_waterFog_color, waterFog.getColor()); // NormalChunkMesh.transparentShader.setUniform(NormalChunkMesh.TransparentUniforms.loc_waterFog_density, waterFog.getDensity()); -// + // NormalChunkMesh[] meshes = sortChunks(visibleChunks.toArray(), x0/Chunk.chunkSize - 0.5f, y0/Chunk.chunkSize - 0.5f, z0/Chunk.chunkSize - 0.5f); // for (NormalChunkMesh mesh : meshes) { // NormalChunkMesh.transparentShader.setUniform(NormalChunkMesh.TransparentUniforms.loc_drawFrontFace, false); // glCullFace(GL_FRONT); // mesh.renderTransparent(playerPosition); -// + // NormalChunkMesh.transparentShader.setUniform(NormalChunkMesh.TransparentUniforms.loc_drawFrontFace, true); // glCullFace(GL_BACK); // mesh.renderTransparent(playerPosition); // } -// + // if(selected != null && Blocks.transparent(selected.getBlock())) { // BlockBreakingRenderer.render(selected, playerPosition); // glActiveTexture(GL_TEXTURE0); @@ -321,9 +307,9 @@ pub fn renderWorld(world: *World, ambientLight: Vec3f, directionalLight: Vec3f, // glActiveTexture(GL_TEXTURE1); // Meshes.emissionTextureArray.bind(); // } -// -// fogShader.bind(); -// // Draw the water fog if the player is underwater: + + fogShader.bind(); + // Draw the water fog if the player is underwater: // Player player = Cubyz.player; // int block = Cubyz.world.getBlock((int)Math.round(player.getPosition().x), (int)(player.getPosition().y + player.height), (int)Math.round(player.getPosition().z)); // if (block != 0 && !Blocks.solid(block)) { @@ -333,7 +319,7 @@ pub fn renderWorld(world: *World, ambientLight: Vec3f, directionalLight: Vec3f, // fogShader.setUniform(FogUniforms.loc_fog_density, waterFog.getDensity()); // glUniform1i(FogUniforms.loc_color, 3); // glUniform1i(FogUniforms.loc_position, 4); -// + // glBindVertexArray(Graphics.rectVAO); // glDisable(GL_DEPTH_TEST); // glDisable(GL_CULL_FACE); @@ -343,22 +329,22 @@ pub fn renderWorld(world: *World, ambientLight: Vec3f, directionalLight: Vec3f, // if(ClientSettings.BLOOM) { // BloomRenderer.render(buffers, Window.getWidth(), Window.getHeight()); // TODO: Use true width/height // } - buffers.unbind(); - buffers.bindTextures(); - deferredRenderPassShader.bind(); - c.glUniform1i(deferredUniforms.color, 3); - c.glUniform1i(deferredUniforms.position, 4); + buffers.unbind(); + buffers.bindTextures(); + deferredRenderPassShader.bind(); + c.glUniform1i(deferredUniforms.color, 3); + c.glUniform1i(deferredUniforms.position, 4); -// if(Window.getRenderTarget() != null) -// Window.getRenderTarget().bind(); +// if(Window.getRenderTarget() != null) +// Window.getRenderTarget().bind(); - c.glBindVertexArray(graphics.Draw.rectVAO); - c.glDisable(c.GL_DEPTH_TEST); - c.glDisable(c.GL_CULL_FACE); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + c.glBindVertexArray(graphics.Draw.rectVAO); + c.glDisable(c.GL_DEPTH_TEST); + c.glDisable(c.GL_CULL_FACE); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); -// if(Window.getRenderTarget() != null) -// Window.getRenderTarget().unbind(); +// if(Window.getRenderTarget() != null) +// Window.getRenderTarget().unbind(); //TODO EntityRenderer.renderNames(playerPosition); } diff --git a/src/settings.zig b/src/settings.zig new file mode 100644 index 00000000..a97dce3f --- /dev/null +++ b/src/settings.zig @@ -0,0 +1,3 @@ + + +pub const defaultPort: u16 = 47649; \ No newline at end of file