diff --git a/Inventory.zig b/Inventory.zig new file mode 100644 index 0000000000..d285386d45 --- /dev/null +++ b/Inventory.zig @@ -0,0 +1,2212 @@ +const std = @import("std"); + +const main = @import("main"); +const BaseItem = main.items.BaseItem; +const Block = main.blocks.Block; +const Item = main.items.Item; +const ItemStack = main.items.ItemStack; +const Tool = main.items.Tool; +const utils = main.utils; +const BinaryWriter = utils.BinaryWriter; +const BinaryReader = utils.BinaryReader; +const NeverFailingAllocator = main.heap.NeverFailingAllocator; +const vec = main.vec; +const Vec3d = vec.Vec3d; +const Vec3f = vec.Vec3f; +const Vec3i = vec.Vec3i; +const ZonElement = main.ZonElement; +const Neighbor = main.chunk.Neighbor; +const BaseItemIndex = main.items.BaseItemIndex; +const ToolTypeIndex = main.items.ToolTypeIndex; + +const Gamemode = main.game.Gamemode; + +const Side = enum {client, server}; + +pub const InventoryId = enum(u32) {_}; + +pub const Callbacks = struct { + onUpdateCallback: ?*const fn(Source) void = null, + onFirstOpenCallback: ?*const fn(Source) void = null, + onLastCloseCallback: ?*const fn(Source) void = null, +}; + +pub const Sync = struct { // MARK: Sync + + pub const ClientSide = struct { + pub var mutex: std.Thread.Mutex = .{}; + var commands: main.utils.CircularBufferQueue(Command) = undefined; + var maxId: InventoryId = @enumFromInt(0); + var freeIdList: main.List(InventoryId) = undefined; + var serverToClientMap: std.AutoHashMap(InventoryId, Inventory) = undefined; + + pub fn init() void { + commands = main.utils.CircularBufferQueue(Command).init(main.globalAllocator, 256); + freeIdList = .init(main.globalAllocator); + serverToClientMap = .init(main.globalAllocator.allocator); + } + + pub fn deinit() void { + reset(); + commands.deinit(); + freeIdList.deinit(); + serverToClientMap.deinit(); + } + + pub fn reset() void { + mutex.lock(); + while(commands.popFront()) |cmd| { + var reader = utils.BinaryReader.init(&.{}); + cmd.finalize(main.globalAllocator, .client, &reader) catch |err| { + std.log.err("Got error while cleaning remaining inventory commands: {s}", .{@errorName(err)}); + }; + } + mutex.unlock(); + std.debug.assert(freeIdList.items.len == @intFromEnum(maxId)); // leak + } + + pub fn executeCommand(payload: Command.Payload) void { + var cmd: Command = .{ + .payload = payload, + }; + + mutex.lock(); + defer mutex.unlock(); + cmd.do(main.globalAllocator, .client, null, main.game.Player.gamemode.raw) catch unreachable; + const data = cmd.serializePayload(main.stackAllocator); + defer main.stackAllocator.free(data); + main.network.protocols.inventory.sendCommand(main.game.world.?.conn, cmd.payload, data); + commands.pushBack(cmd); + } + + fn nextId() InventoryId { + mutex.lock(); + defer mutex.unlock(); + if(freeIdList.popOrNull()) |id| { + return id; + } + defer maxId = @enumFromInt(@intFromEnum(maxId) + 1); + return maxId; + } + + fn freeId(id: InventoryId) void { + main.utils.assertLocked(&mutex); + freeIdList.append(id); + } + + fn mapServerId(serverId: InventoryId, inventory: Inventory) void { + main.utils.assertLocked(&mutex); + serverToClientMap.put(serverId, inventory) catch unreachable; + } + + fn unmapServerId(serverId: InventoryId, clientId: InventoryId) void { + main.utils.assertLocked(&mutex); + std.debug.assert(serverToClientMap.fetchRemove(serverId).?.value.id == clientId); + } + + fn getInventory(serverId: InventoryId) ?Inventory { + main.utils.assertLocked(&mutex); + return serverToClientMap.get(serverId); + } + + pub fn receiveConfirmation(reader: *utils.BinaryReader) !void { + mutex.lock(); + defer mutex.unlock(); + try commands.popFront().?.finalize(main.globalAllocator, .client, reader); + } + + pub fn receiveFailure() void { + mutex.lock(); + defer mutex.unlock(); + var tempData = main.List(Command).init(main.stackAllocator); + defer tempData.deinit(); + while(commands.popBack()) |_cmd| { + var cmd = _cmd; + cmd.undo(); + tempData.append(cmd); + } + if(tempData.popOrNull()) |_cmd| { + var cmd = _cmd; + var reader = utils.BinaryReader.init(&.{}); + cmd.finalize(main.globalAllocator, .client, &reader) catch |err| { + std.log.err("Got error while cleaning rejected inventory command: {s}", .{@errorName(err)}); + }; + } + while(tempData.popOrNull()) |_cmd| { + var cmd = _cmd; + cmd.do(main.globalAllocator, .client, null, main.game.Player.gamemode.raw) catch unreachable; + commands.pushBack(cmd); + } + } + + pub fn receiveSyncOperation(reader: *utils.BinaryReader) !void { + mutex.lock(); + defer mutex.unlock(); + var tempData = main.List(Command).init(main.stackAllocator); + defer tempData.deinit(); + while(commands.popBack()) |_cmd| { + var cmd = _cmd; + cmd.undo(); + tempData.append(cmd); + } + try Command.SyncOperation.executeFromData(reader); + while(tempData.popOrNull()) |_cmd| { + var cmd = _cmd; + cmd.do(main.globalAllocator, .client, null, main.game.Player.gamemode.raw) catch unreachable; + commands.pushBack(cmd); + } + } + + fn setGamemode(gamemode: Gamemode) void { + mutex.lock(); + defer mutex.unlock(); + main.game.Player.setGamemode(gamemode); + var tempData = main.List(Command).init(main.stackAllocator); + defer tempData.deinit(); + while(commands.popBack()) |_cmd| { + var cmd = _cmd; + cmd.undo(); + tempData.append(cmd); + } + while(tempData.popOrNull()) |_cmd| { + var cmd = _cmd; + cmd.do(main.globalAllocator, .client, null, gamemode) catch unreachable; + commands.pushBack(cmd); + } + } + + fn setSpawn(newSpawnPoint: Vec3d) void { + mutex.lock(); + defer mutex.unlock(); + main.game.Player.setSpawn(newSpawnPoint); + var tempData = main.List(Command).init(main.stackAllocator); + defer tempData.deinit(); + while(commands.popBack()) |_cmd| { + var cmd = _cmd; + cmd.undo(); + tempData.append(cmd); + } + while(tempData.popOrNull()) |_cmd| { + var cmd = _cmd; + cmd.do(main.globalAllocator, .client, null, main.game.Player.gamemode.raw) catch unreachable; + commands.pushBack(cmd); + } + } + }; + + pub const ServerSide = struct { // MARK: ServerSide + const ServerInventory = struct { + inv: Inventory, + users: main.ListUnmanaged(struct {user: *main.server.User, cliendId: InventoryId}), + source: Source, + managed: Managed, + + const Managed = enum {internallyManaged, externallyManaged}; + + fn init(len: usize, typ: Inventory.Type, source: Source, managed: Managed, callbacks: Callbacks) ServerInventory { + main.utils.assertLocked(&mutex); + return .{ + .inv = Inventory._init(main.globalAllocator, len, typ, source, .server, callbacks), + .users = .{}, + .source = source, + .managed = managed, + }; + } + + fn deinit(self: *ServerInventory) void { + main.utils.assertLocked(&mutex); + while(self.users.items.len != 0) { + self.removeUser(self.users.items[0].user, self.users.items[0].cliendId); + } + self.users.deinit(main.globalAllocator); + self.inv._deinit(main.globalAllocator, .server); + self.inv._items.len = 0; + self.source = .alreadyFreed; + self.managed = .internallyManaged; + } + + fn addUser(self: *ServerInventory, user: *main.server.User, clientId: InventoryId) void { + main.utils.assertLocked(&mutex); + self.users.append(main.globalAllocator, .{.user = user, .cliendId = clientId}); + user.inventoryClientToServerIdMap.put(clientId, self.inv.id) catch unreachable; + if(self.users.items.len == 1) { + if(self.inv.callbacks.onFirstOpenCallback) |cb| { + cb(self.inv.source); + } + } + } + + fn removeUser(self: *ServerInventory, user: *main.server.User, clientId: InventoryId) void { + main.utils.assertLocked(&mutex); + var index: usize = undefined; + for(self.users.items, 0..) |userData, i| { + if(userData.user == user) { + index = i; + break; + } + } + _ = self.users.swapRemove(index); + std.debug.assert(user.inventoryClientToServerIdMap.fetchRemove(clientId).?.value == self.inv.id); + if(self.users.items.len == 0) { + if(self.inv.callbacks.onLastCloseCallback) |cb| { + cb(self.inv.source); + } + if(self.managed == .internallyManaged) { + if(self.inv.type.shouldDepositToUserOnClose()) { + const playerInventory = getInventoryFromSource(.{.playerInventory = user.id}) orelse @panic("Could not find player inventory"); + Sync.ServerSide.executeCommand(.{.depositOrDrop = .{.dest = playerInventory, .source = self.inv, .dropLocation = user.player.pos}}, null); + } + self.deinit(); + } + } + } + }; + pub var mutex: std.Thread.Mutex = .{}; + + var inventories: main.List(ServerInventory) = undefined; + var maxId: InventoryId = @enumFromInt(0); + var freeIdList: main.List(InventoryId) = undefined; + + pub fn init() void { + inventories = .initCapacity(main.globalAllocator, 256); + freeIdList = .init(main.globalAllocator); + } + + pub fn deinit() void { + for(inventories.items) |inv| { + if(inv.source != .alreadyFreed) { + std.log.err("Leaked inventory with source {}", .{inv.source}); + } + } + std.debug.assert(freeIdList.items.len == @intFromEnum(maxId)); // leak + freeIdList.deinit(); + inventories.deinit(); + maxId = @enumFromInt(0); + } + + pub fn disconnectUser(user: *main.server.User) void { + mutex.lock(); + defer mutex.unlock(); + while(true) { + // Reinitializing the iterator in the loop to allow for removal: + var iter = user.inventoryClientToServerIdMap.keyIterator(); + const clientId = iter.next() orelse break; + closeInventory(user, clientId.*) catch unreachable; + } + } + + fn nextId() InventoryId { + main.utils.assertLocked(&mutex); + if(freeIdList.popOrNull()) |id| { + return id; + } + defer maxId = @enumFromInt(@intFromEnum(maxId) + 1); + _ = inventories.addOne(); + return maxId; + } + + fn freeId(id: InventoryId) void { + main.utils.assertLocked(&mutex); + freeIdList.append(id); + } + + fn executeCommand(payload: Command.Payload, source: ?*main.server.User) void { + var command = Command{ + .payload = payload, + }; + command.do(main.globalAllocator, .server, source, if(source) |s| s.gamemode.raw else .creative) catch { + main.network.protocols.inventory.sendFailure(source.?.conn); + return; + }; + if(source != null) { + const confirmationData = command.confirmationData(main.stackAllocator); + defer main.stackAllocator.free(confirmationData); + main.network.protocols.inventory.sendConfirmation(source.?.conn, confirmationData); + } + for(command.syncOperations.items) |op| { + const syncData = op.serialize(main.stackAllocator); + defer main.stackAllocator.free(syncData); + + const users = op.getUsers(main.stackAllocator); + defer main.stackAllocator.free(users); + + for(users) |user| { + if(user == source and op.ignoreSource()) continue; + main.network.protocols.inventory.sendSyncOperation(user.conn, syncData); + } + } + if(source != null and command.payload == .open) { // Send initial items + for(command.payload.open.inv._items, 0..) |stack, slot| { + if(stack.item != .null) { + const syncOp = Command.SyncOperation{.create = .{ + .inv = .{.inv = command.payload.open.inv, .slot = @intCast(slot)}, + .amount = stack.amount, + .item = stack.item, + }}; + const syncData = syncOp.serialize(main.stackAllocator); + defer main.stackAllocator.free(syncData); + main.network.protocols.inventory.sendSyncOperation(source.?.conn, syncData); + } + } + } + var reader = utils.BinaryReader.init(&.{}); + command.finalize(main.globalAllocator, .server, &reader) catch |err| { + std.log.err("Got error while finalizing command on the server side: {s}", .{@errorName(err)}); + }; + } + + pub fn receiveCommand(source: *main.server.User, reader: *utils.BinaryReader) !void { + mutex.lock(); + defer mutex.unlock(); + const typ = try reader.readEnum(Command.PayloadType); + @setEvalBranchQuota(100000); + const payload: Command.Payload = switch(typ) { + inline else => |_typ| @unionInit(Command.Payload, @tagName(_typ), try @FieldType(Command.Payload, @tagName(_typ)).deserialize(reader, .server, source)), + }; + executeCommand(payload, source); + } + + pub fn createExternallyManagedInventory(len: usize, typ: Inventory.Type, source: Source, data: *BinaryReader, callbacks: Callbacks) InventoryId { + mutex.lock(); + defer mutex.unlock(); + const inventory = ServerInventory.init(len, typ, source, .externallyManaged, callbacks); + inventories.items[@intFromEnum(inventory.inv.id)] = inventory; + inventory.inv.fromBytes(data); + return inventory.inv.id; + } + + pub fn destroyExternallyManagedInventory(invId: InventoryId) void { + mutex.lock(); + defer mutex.unlock(); + std.debug.assert(inventories.items[@intFromEnum(invId)].managed == .externallyManaged); + inventories.items[@intFromEnum(invId)].deinit(); + } + + pub fn destroyAndDropExternallyManagedInventory(invId: InventoryId, pos: Vec3i) void { + main.utils.assertLocked(&mutex); + std.debug.assert(inventories.items[@intFromEnum(invId)].managed == .externallyManaged); + const inv = &inventories.items[@intFromEnum(invId)]; + for(inv.inv._items) |*itemStack| { + if(itemStack.amount == 0) continue; + main.server.world.?.drop( + itemStack.*, + @as(Vec3d, @floatFromInt(pos)) + main.random.nextDoubleVector(3, &main.seed), + main.random.nextFloatVectorSigned(3, &main.seed), + 0.1, + ); + itemStack.* = .{}; + } + inv.deinit(); + } + + fn createInventory(user: *main.server.User, clientId: InventoryId, len: usize, typ: Inventory.Type, source: Source) !void { + main.utils.assertLocked(&mutex); + switch(source) { + .recipe, .blockInventory, .playerInventory, .hand => { + switch(source) { + .playerInventory, .hand => |id| { + if(id != user.id) { + std.log.err("Player {s} tried to access the inventory of another player.", .{user.name}); + return error.Invalid; + } + }, + else => {}, + } + for(inventories.items) |*inv| { + if(std.meta.eql(inv.source, source)) { + inv.addUser(user, clientId); + return; + } + } + if(source != .recipe) return error.Invalid; + }, + .other => {}, + .alreadyFreed => unreachable, + } + const inventory = ServerInventory.init(len, typ, source, .internallyManaged, .{}); + + inventories.items[@intFromEnum(inventory.inv.id)] = inventory; + inventories.items[@intFromEnum(inventory.inv.id)].addUser(user, clientId); + + switch(source) { + .blockInventory => unreachable, // Should be loaded by the block entity + .playerInventory, .hand => unreachable, // Should be loaded on player creation + .recipe => |recipe| { + for(0..recipe.sourceAmounts.len) |i| { + inventory.inv._items[i].amount = recipe.sourceAmounts[i]; + inventory.inv._items[i].item = .{.baseItem = recipe.sourceItems[i]}; + } + inventory.inv._items[inventory.inv._items.len - 1].amount = recipe.resultAmount; + inventory.inv._items[inventory.inv._items.len - 1].item = .{.baseItem = recipe.resultItem}; + }, + .other => {}, + .alreadyFreed => unreachable, + } + } + + fn closeInventory(user: *main.server.User, clientId: InventoryId) !void { + main.utils.assertLocked(&mutex); + const serverId = user.inventoryClientToServerIdMap.get(clientId) orelse return error.InventoryNotFound; + inventories.items[@intFromEnum(serverId)].removeUser(user, clientId); + } + + fn getInventory(user: *main.server.User, clientId: InventoryId) ?Inventory { + main.utils.assertLocked(&mutex); + const serverId = user.inventoryClientToServerIdMap.get(clientId) orelse return null; + return inventories.items[@intFromEnum(serverId)].inv; + } + + pub fn getInventoryFromSource(source: Source) ?Inventory { + main.utils.assertLocked(&mutex); + for(inventories.items) |inv| { + if(std.meta.eql(inv.source, source)) { + return inv.inv; + } + } + return null; + } + + pub fn getInventoryFromId(serverId: InventoryId) Inventory { + return inventories.items[@intFromEnum(serverId)].inv; + } + + pub fn clearPlayerInventory(user: *main.server.User) void { + mutex.lock(); + defer mutex.unlock(); + var inventoryIdIterator = user.inventoryClientToServerIdMap.valueIterator(); + while(inventoryIdIterator.next()) |inventoryId| { + if(inventories.items[@intFromEnum(inventoryId.*)].source == .playerInventory) { + executeCommand(.{.clear = .{.inv = inventories.items[@intFromEnum(inventoryId.*)].inv}}, null); + } + } + } + + pub fn tryCollectingToPlayerInventory(user: *main.server.User, itemStack: *ItemStack) void { + if(itemStack.item == .null) return; + mutex.lock(); + defer mutex.unlock(); + var inventoryIdIterator = user.inventoryClientToServerIdMap.valueIterator(); + outer: while(inventoryIdIterator.next()) |inventoryId| { + if(inventories.items[@intFromEnum(inventoryId.*)].source == .playerInventory) { + const inv = inventories.items[@intFromEnum(inventoryId.*)].inv; + for(inv._items, 0..) |invStack, slot| { + if(std.meta.eql(invStack.item, itemStack.item)) { + const amount = @min(itemStack.item.stackSize() - invStack.amount, itemStack.amount); + if(amount == 0) continue; + executeCommand(.{.fillFromCreative = .{.dest = .{.inv = inv, .slot = @intCast(slot)}, .item = itemStack.item, .amount = invStack.amount + amount}}, null); + itemStack.amount -= amount; + if(itemStack.amount == 0) break :outer; + } + } + for(inv._items, 0..) |invStack, slot| { + if(invStack.item == .null) { + executeCommand(.{.fillFromCreative = .{.dest = .{.inv = inv, .slot = @intCast(slot)}, .item = itemStack.item, .amount = itemStack.amount}}, null); + itemStack.amount = 0; + break :outer; + } + } + } + } + if(itemStack.amount == 0) itemStack.item = .null; + } + + fn setGamemode(user: *main.server.User, gamemode: Gamemode) void { + mutex.lock(); + defer mutex.unlock(); + user.gamemode.store(gamemode, .monotonic); + main.network.protocols.genericUpdate.sendGamemode(user.conn, gamemode); + } + + fn setSpawn(user: *main.server.User, newSpawnPoint: Vec3d) void { + mutex.lock(); + defer mutex.unlock(); + user.playerSpawnPos = newSpawnPoint; + main.network.protocols.genericUpdate.sendSpawnPoint(user.conn, newSpawnPoint); + } + }; + + pub fn addHealth(health: f32, cause: main.game.DamageType, side: Side, userId: u32) void { + if(side == .client) { + Sync.ClientSide.executeCommand(.{.addHealth = .{.target = userId, .health = health, .cause = cause}}); + } else { + Sync.ServerSide.executeCommand(.{.addHealth = .{.target = userId, .health = health, .cause = cause}}, null); + } + } + + pub fn getInventory(id: InventoryId, side: Side, user: ?*main.server.User) ?Inventory { + return switch(side) { + .client => ClientSide.getInventory(id), + .server => ServerSide.getInventory(user.?, id), + }; + } + + pub fn setGamemode(user: ?*main.server.User, gamemode: Gamemode) void { + if(user == null) { + ClientSide.setGamemode(gamemode); + } else { + ServerSide.setGamemode(user.?, gamemode); + } + } + + pub fn setSpawn(user: ?*main.server.User, newSpawnPoint: Vec3d) void { + if(user == null) { + ClientSide.setSpawn(newSpawnPoint); + } else { + ServerSide.setSpawn(user.?, newSpawnPoint); + } + } +}; + +pub const Command = struct { // MARK: Command + pub const PayloadType = enum(u8) { + open = 0, + close = 1, + depositOrSwap = 2, + deposit = 3, + takeHalf = 4, + drop = 5, + fillFromCreative = 6, + depositOrDrop = 7, + depositToAny = 11, + clear = 8, + updateBlock = 9, + addHealth = 10, + }; + pub const Payload = union(PayloadType) { + open: Open, + close: Close, + depositOrSwap: DepositOrSwap, + deposit: Deposit, + takeHalf: TakeHalf, + drop: Drop, + fillFromCreative: FillFromCreative, + depositOrDrop: DepositOrDrop, + depositToAny: DepositToAny, + clear: Clear, + updateBlock: UpdateBlock, + addHealth: AddHealth, + }; + + const BaseOperationType = enum(u8) { + move = 0, + swap = 1, + delete = 2, + create = 3, + useDurability = 4, + addHealth = 5, + addEnergy = 6, + }; + + const InventoryAndSlot = struct { + inv: Inventory, + slot: u32, + + fn ref(self: InventoryAndSlot) *ItemStack { + return &self.inv._items[self.slot]; + } + + fn write(self: InventoryAndSlot, writer: *utils.BinaryWriter) void { + writer.writeEnum(InventoryId, self.inv.id); + writer.writeInt(u32, self.slot); + } + + fn read(reader: *utils.BinaryReader, side: Side, user: ?*main.server.User) !InventoryAndSlot { + const id = try reader.readEnum(InventoryId); + const result: InventoryAndSlot = .{ + .inv = Sync.getInventory(id, side, user) orelse return error.InventoryNotFound, + .slot = try reader.readInt(u32), + }; + if(result.slot >= result.inv._items.len) return error.Invalid; + return result; + } + }; + + const BaseOperation = union(BaseOperationType) { + move: struct { + dest: InventoryAndSlot, + source: InventoryAndSlot, + amount: u16, + }, + swap: struct { + dest: InventoryAndSlot, + source: InventoryAndSlot, + }, + delete: struct { + source: InventoryAndSlot, + item: Item = undefined, + amount: u16, + }, + create: struct { + dest: InventoryAndSlot, + item: Item, + amount: u16, + }, + useDurability: struct { + source: InventoryAndSlot, + item: main.items.Item = undefined, + durability: u31, + previousDurability: u32 = undefined, + }, + addHealth: struct { + target: ?*main.server.User, + health: f32, + cause: main.game.DamageType, + previous: f32, + }, + addEnergy: struct { + target: ?*main.server.User, + energy: f32, + previous: f32, + }, + }; + + const SyncOperationType = enum(u8) { + create = 0, + delete = 1, + useDurability = 2, + health = 3, + kill = 4, + energy = 5, + }; + + const SyncOperation = union(SyncOperationType) { // MARK: SyncOperation + // Since the client doesn't know about all inventories, we can only use create(+amount)/delete(-amount) and use durability operations to apply the server side updates. + create: struct { + inv: InventoryAndSlot, + amount: u16, + item: Item, + }, + delete: struct { + inv: InventoryAndSlot, + amount: u16, + }, + useDurability: struct { + inv: InventoryAndSlot, + durability: u32, + }, + health: struct { + target: ?*main.server.User, + health: f32, + }, + kill: struct { + target: ?*main.server.User, + }, + energy: struct { + target: ?*main.server.User, + energy: f32, + }, + + pub fn executeFromData(reader: *utils.BinaryReader) !void { + switch(try deserialize(reader)) { + .create => |create| { + if(create.item != .null) { + create.inv.ref().item = create.item; + } else if(create.inv.ref().item == .null) { + return error.Invalid; + } + + if(create.inv.ref().amount +| create.amount > create.inv.ref().item.stackSize()) { + return error.Invalid; + } + create.inv.ref().amount += create.amount; + + create.inv.inv.update(); + }, + .delete => |delete| { + if(delete.inv.ref().amount < delete.amount) { + return error.Invalid; + } + delete.inv.ref().amount -= delete.amount; + if(delete.inv.ref().amount == 0) { + delete.inv.ref().item = .null; + } + + delete.inv.inv.update(); + }, + .useDurability => |durability| { + durability.inv.ref().item.tool.durability -|= durability.durability; + if(durability.inv.ref().item.tool.durability == 0) { + durability.inv.ref().item = .null; + durability.inv.ref().amount = 0; + } + + durability.inv.inv.update(); + }, + .health => |health| { + main.game.Player.super.health = std.math.clamp(main.game.Player.super.health + health.health, 0, main.game.Player.super.maxHealth); + }, + .kill => { + main.game.Player.kill(); + }, + .energy => |energy| { + main.game.Player.super.energy = std.math.clamp(main.game.Player.super.energy + energy.energy, 0, main.game.Player.super.maxEnergy); + }, + } + } + + pub fn getUsers(self: SyncOperation, allocator: NeverFailingAllocator) []*main.server.User { + switch(self) { + inline .create, .delete, .useDurability => |data| { + const users = Sync.ServerSide.inventories.items[@intFromEnum(data.inv.inv.id)].users.items; + const result = allocator.alloc(*main.server.User, users.len); + for(0..users.len) |i| { + result[i] = users[i].user; + } + return result; + }, + inline .health, .kill, .energy => |data| { + const out = allocator.alloc(*main.server.User, 1); + out[0] = data.target.?; + return out; + }, + } + } + + pub fn ignoreSource(self: SyncOperation) bool { + return switch(self) { + .create, .delete, .useDurability, .health, .energy => true, + .kill => false, + }; + } + + fn deserialize(reader: *utils.BinaryReader) !SyncOperation { + const typ = try reader.readEnum(SyncOperationType); + + switch(typ) { + .create => { + const out: SyncOperation = .{.create = .{ + .inv = try InventoryAndSlot.read(reader, .client, null), + .amount = try reader.readInt(u16), + .item = if(reader.remaining.len > 0) try Item.fromBytes(reader) else .null, + }}; + return out; + }, + .delete => { + const out: SyncOperation = .{.delete = .{ + .inv = try InventoryAndSlot.read(reader, .client, null), + .amount = try reader.readInt(u16), + }}; + + return out; + }, + .useDurability => { + const out: SyncOperation = .{.useDurability = .{ + .inv = try InventoryAndSlot.read(reader, .client, null), + .durability = try reader.readInt(u32), + }}; + + return out; + }, + .health => { + return .{.health = .{ + .target = null, + .health = try reader.readFloat(f32), + }}; + }, + .kill => { + return .{.kill = .{ + .target = null, + }}; + }, + .energy => { + return .{.energy = .{ + .target = null, + .energy = try reader.readFloat(f32), + }}; + }, + } + } + + pub fn serialize(self: SyncOperation, allocator: NeverFailingAllocator) []const u8 { + var writer = utils.BinaryWriter.initCapacity(allocator, 13); + writer.writeEnum(SyncOperationType, self); + switch(self) { + .create => |create| { + create.inv.write(&writer); + writer.writeInt(u16, create.amount); + if(create.item != .null) { + create.item.toBytes(&writer); + } + }, + .delete => |delete| { + delete.inv.write(&writer); + writer.writeInt(u16, delete.amount); + }, + .useDurability => |durability| { + durability.inv.write(&writer); + writer.writeInt(u32, durability.durability); + }, + .health => |health| { + writer.writeFloat(f32, health.health); + }, + .kill => {}, + .energy => |energy| { + writer.writeFloat(f32, energy.energy); + }, + } + return writer.data.toOwnedSlice(); + } + }; + + payload: Payload, + baseOperations: main.ListUnmanaged(BaseOperation) = .{}, + syncOperations: main.ListUnmanaged(SyncOperation) = .{}, + + fn serializePayload(self: *Command, allocator: NeverFailingAllocator) []const u8 { + var writer = utils.BinaryWriter.init(allocator); + defer writer.deinit(); + switch(self.payload) { + inline else => |payload| { + payload.serialize(&writer); + }, + } + return writer.data.toOwnedSlice(); + } + + fn do(self: *Command, allocator: NeverFailingAllocator, side: Side, user: ?*main.server.User, gamemode: main.game.Gamemode) error{serverFailure}!void { // MARK: do() + std.debug.assert(self.baseOperations.items.len == 0); // do called twice without cleaning up + switch(self.payload) { + inline else => |payload| { + try payload.run(allocator, self, side, user, gamemode); + }, + } + } + + fn undo(self: *Command) void { + // Iterating in reverse order! + while(self.baseOperations.popOrNull()) |step| { + switch(step) { + .move => |info| { + if(info.amount == 0) continue; + std.debug.assert(std.meta.eql(info.source.ref().item, info.dest.ref().item) or info.source.ref().item == .null); + info.source.ref().item = info.dest.ref().item; + info.source.ref().amount += info.amount; + info.dest.ref().amount -= info.amount; + if(info.dest.ref().amount == 0) { + info.dest.ref().item = .null; + } + info.source.inv.update(); + info.dest.inv.update(); + }, + .swap => |info| { + const temp = info.dest.ref().*; + info.dest.ref().* = info.source.ref().*; + info.source.ref().* = temp; + info.source.inv.update(); + info.dest.inv.update(); + }, + .delete => |info| { + std.debug.assert(info.source.ref().item == .null or std.meta.eql(info.source.ref().item, info.item)); + info.source.ref().item = info.item; + info.source.ref().amount += info.amount; + info.source.inv.update(); + }, + .create => |info| { + std.debug.assert(info.dest.ref().amount >= info.amount); + info.dest.ref().amount -= info.amount; + if(info.dest.ref().amount == 0) { + info.dest.ref().item.deinit(); + info.dest.ref().item = .null; + } + info.dest.inv.update(); + }, + .useDurability => |info| { + std.debug.assert(info.source.ref().item == .null or std.meta.eql(info.source.ref().item, info.item)); + info.source.ref().item = info.item; + info.item.tool.durability = info.previousDurability; + info.source.inv.update(); + }, + .addHealth => |info| { + main.game.Player.super.health = info.previous; + }, + .addEnergy => |info| { + main.game.Player.super.energy = info.previous; + }, + } + } + } + + fn finalize(self: Command, allocator: NeverFailingAllocator, side: Side, reader: *utils.BinaryReader) !void { + for(self.baseOperations.items) |step| { + switch(step) { + .move, .swap, .create, .addHealth, .addEnergy => {}, + .delete => |info| { + info.item.deinit(); + }, + .useDurability => |info| { + if(info.previousDurability <= info.durability) { + info.item.deinit(); + } + }, + } + } + self.baseOperations.deinit(allocator); + if(side == .server) { + self.syncOperations.deinit(allocator); + } else { + std.debug.assert(self.syncOperations.capacity == 0); + } + + switch(self.payload) { + inline else => |payload| { + if(@hasDecl(@TypeOf(payload), "finalize")) { + try payload.finalize(side, reader); + } + }, + } + } + + fn confirmationData(self: *Command, allocator: NeverFailingAllocator) []const u8 { + switch(self.payload) { + inline else => |payload| { + if(@hasDecl(@TypeOf(payload), "confirmationData")) { + return payload.confirmationData(allocator); + } + }, + } + return &.{}; + } + + fn executeAddOperation(self: *Command, allocator: NeverFailingAllocator, side: Side, inv: InventoryAndSlot, amount: u16, item: Item) void { + if(amount == 0) return; + if(item == .null) return; + if(side == .server) { + self.syncOperations.append(allocator, .{.create = .{ + .inv = inv, + .amount = amount, + .item = if(inv.ref().amount == 0) item else .null, + }}); + } + std.debug.assert(inv.ref().item == .null or std.meta.eql(inv.ref().item, item)); + inv.ref().item = item; + inv.ref().amount += amount; + std.debug.assert(inv.ref().amount <= item.stackSize()); + } + + fn executeRemoveOperation(self: *Command, allocator: NeverFailingAllocator, side: Side, inv: InventoryAndSlot, amount: u16) void { + if(amount == 0) return; + if(side == .server) { + self.syncOperations.append(allocator, .{.delete = .{ + .inv = inv, + .amount = amount, + }}); + } + inv.ref().amount -= amount; + if(inv.ref().amount == 0) { + inv.ref().item = .null; + } + } + + fn executeDurabilityUseOperation(self: *Command, allocator: NeverFailingAllocator, side: Side, inv: InventoryAndSlot, durability: u31) void { + if(durability == 0) return; + if(side == .server) { + self.syncOperations.append(allocator, .{.useDurability = .{ + .inv = inv, + .durability = durability, + }}); + } + inv.ref().item.tool.durability -|= durability; + if(inv.ref().item.tool.durability == 0) { + inv.ref().item = .null; + inv.ref().amount = 0; + } + } + + fn executeBaseOperation(self: *Command, allocator: NeverFailingAllocator, _op: BaseOperation, side: Side) void { // MARK: executeBaseOperation() + var op = _op; + switch(op) { + .move => |info| { + self.executeAddOperation(allocator, side, info.dest, info.amount, info.source.ref().item); + self.executeRemoveOperation(allocator, side, info.source, info.amount); + info.source.inv.update(); + info.dest.inv.update(); + }, + .swap => |info| { + const oldDestStack = info.dest.ref().*; + const oldSourceStack = info.source.ref().*; + self.executeRemoveOperation(allocator, side, info.source, oldSourceStack.amount); + self.executeRemoveOperation(allocator, side, info.dest, oldDestStack.amount); + self.executeAddOperation(allocator, side, info.source, oldDestStack.amount, oldDestStack.item); + self.executeAddOperation(allocator, side, info.dest, oldSourceStack.amount, oldSourceStack.item); + info.source.inv.update(); + info.dest.inv.update(); + }, + .delete => |*info| { + info.item = info.source.ref().item; + self.executeRemoveOperation(allocator, side, info.source, info.amount); + info.source.inv.update(); + }, + .create => |info| { + self.executeAddOperation(allocator, side, info.dest, info.amount, info.item); + info.dest.inv.update(); + }, + .useDurability => |*info| { + info.item = info.source.ref().item; + info.previousDurability = info.item.tool.durability; + self.executeDurabilityUseOperation(allocator, side, info.source, info.durability); + info.source.inv.update(); + }, + .addHealth => |*info| { + if(side == .server) { + info.previous = info.target.?.player.health; + + info.target.?.player.health = std.math.clamp(info.target.?.player.health + info.health, 0, info.target.?.player.maxHealth); + + if(info.target.?.player.health <= 0) { + info.target.?.player.health = info.target.?.player.maxHealth; + info.cause.sendMessage(info.target.?.name); + + self.syncOperations.append(allocator, .{.kill = .{ + .target = info.target.?, + }}); + } else { + self.syncOperations.append(allocator, .{.health = .{ + .target = info.target.?, + .health = info.health, + }}); + } + } else { + info.previous = main.game.Player.super.health; + main.game.Player.super.health = std.math.clamp(main.game.Player.super.health + info.health, 0, main.game.Player.super.maxHealth); + } + }, + .addEnergy => |*info| { + if(side == .server) { + info.previous = info.target.?.player.energy; + + info.target.?.player.energy = std.math.clamp(info.target.?.player.energy + info.energy, 0, info.target.?.player.maxEnergy); + self.syncOperations.append(allocator, .{.energy = .{ + .target = info.target.?, + .energy = info.energy, + }}); + } else { + info.previous = main.game.Player.super.energy; + main.game.Player.super.energy = std.math.clamp(main.game.Player.super.energy + info.energy, 0, main.game.Player.super.maxEnergy); + } + }, + } + self.baseOperations.append(allocator, op); + } + + fn removeToolCraftingIngredients(self: *Command, allocator: NeverFailingAllocator, inv: Inventory, side: Side) void { + std.debug.assert(inv.type == .workbench); + for(0..25) |i| { + if(inv._items[i].amount != 0) { + self.executeBaseOperation(allocator, .{.delete = .{ + .source = .{.inv = inv, .slot = @intCast(i)}, + .amount = 1, + }}, side); + } + } + } + + fn canPutIntoWorkbench(source: InventoryAndSlot) bool { + return switch(source.ref().item) { + .null => true, + .baseItem => |item| item.material() != null, + .tool => false, + }; + } + + fn tryCraftingTo(self: *Command, allocator: NeverFailingAllocator, dest: Inventory, source: InventoryAndSlot, side: Side, user: ?*main.server.User) void { // MARK: tryCraftingTo() + std.debug.assert(source.inv.type == .crafting); + std.debug.assert(dest.type == .normal); + if(source.slot != source.inv._items.len - 1) return; + if(!dest.canHold(source.ref().*)) return; + if(source.ref().item == .null) return; // Can happen if the we didn't receive the inventory information from the server yet. + + const playerInventory: Inventory = switch(side) { + .client => main.game.Player.inventory, + .server => blk: { + if(user) |_user| { + var it = _user.inventoryClientToServerIdMap.valueIterator(); + while(it.next()) |serverId| { + const serverInventory = &Sync.ServerSide.inventories.items[@intFromEnum(serverId.*)]; + if(serverInventory.source == .playerInventory) + break :blk serverInventory.inv; + } + } + return; + }, + }; + + // Can we even craft it? + for(source.inv._items[0..source.slot]) |requiredStack| { + var amount: usize = 0; + // There might be duplicate entries: + for(source.inv._items[0..source.slot]) |otherStack| { + if(std.meta.eql(requiredStack.item, otherStack.item)) + amount += otherStack.amount; + } + for(playerInventory._items) |otherStack| { + if(std.meta.eql(requiredStack.item, otherStack.item)) + amount -|= otherStack.amount; + } + // Not enough ingredients + if(amount != 0) + return; + } + + // Craft it + for(source.inv._items[0..source.slot]) |requiredStack| { + var remainingAmount: usize = requiredStack.amount; + for(playerInventory._items, 0..) |*otherStack, i| { + if(std.meta.eql(requiredStack.item, otherStack.item)) { + const amount = @min(remainingAmount, otherStack.amount); + self.executeBaseOperation(allocator, .{.delete = .{ + .source = .{.inv = playerInventory, .slot = @intCast(i)}, + .amount = amount, + }}, side); + remainingAmount -= amount; + if(remainingAmount == 0) break; + } + } + std.debug.assert(remainingAmount == 0); + } + + var remainingAmount: u16 = source.ref().amount; + for(dest._items, 0..) |*destStack, destSlot| { + if(std.meta.eql(destStack.item, source.ref().item) or destStack.item == .null) { + const amount = @min(source.ref().item.stackSize() - destStack.amount, remainingAmount); + self.executeBaseOperation(allocator, .{.create = .{ + .dest = .{.inv = dest, .slot = @intCast(destSlot)}, + .amount = amount, + .item = source.ref().item, + }}, side); + remainingAmount -= amount; + if(remainingAmount == 0) break; + } + } + std.debug.assert(remainingAmount == 0); + } + + const Open = struct { // MARK: Open + inv: Inventory, + source: Source, + + fn run(_: Open, _: NeverFailingAllocator, _: *Command, _: Side, _: ?*main.server.User, _: Gamemode) error{serverFailure}!void {} + + fn finalize(self: Open, side: Side, reader: *utils.BinaryReader) !void { + if(side != .client) return; + if(reader.remaining.len != 0) { + const serverId = try reader.readEnum(InventoryId); + Sync.ClientSide.mapServerId(serverId, self.inv); + } + } + + fn confirmationData(self: Open, allocator: NeverFailingAllocator) []const u8 { + var writer = utils.BinaryWriter.initCapacity(allocator, 4); + writer.writeEnum(InventoryId, self.inv.id); + return writer.data.toOwnedSlice(); + } + + fn serialize(self: Open, writer: *utils.BinaryWriter) void { + writer.writeEnum(InventoryId, self.inv.id); + writer.writeInt(usize, self.inv._items.len); + writer.writeEnum(TypeEnum, self.inv.type); + writer.writeEnum(SourceType, self.source); + switch(self.source) { + .playerInventory, .hand => |val| { + writer.writeInt(u32, val); + }, + .recipe => |val| { + writer.writeInt(u16, val.resultAmount); + writer.writeWithDelimiter(val.resultItem.id(), 0); + for(0..val.sourceItems.len) |i| { + writer.writeInt(u16, val.sourceAmounts[i]); + writer.writeWithDelimiter(val.sourceItems[i].id(), 0); + } + }, + .blockInventory => |val| { + writer.writeVec(Vec3i, val); + }, + .other => {}, + .alreadyFreed => unreachable, + } + switch(self.inv.type) { + .normal, .creative, .crafting => {}, + .workbench => { + writer.writeSlice(self.inv.type.workbench.id()); + }, + } + } + + fn deserialize(reader: *utils.BinaryReader, side: Side, user: ?*main.server.User) !Open { + if(side != .server or user == null) return error.Invalid; + const id = try reader.readEnum(InventoryId); + const len = try reader.readInt(u64); + const typeEnum = try reader.readEnum(TypeEnum); + const sourceType = try reader.readEnum(SourceType); + const source: Source = switch(sourceType) { + .playerInventory => .{.playerInventory = try reader.readInt(u32)}, + .hand => .{.hand = try reader.readInt(u32)}, + .recipe => .{ + .recipe = blk: { + var itemList = main.List(struct {amount: u16, item: BaseItemIndex}).initCapacity(main.stackAllocator, len); + defer itemList.deinit(); + while(reader.remaining.len >= 2) { + const resultAmount = try reader.readInt(u16); + const itemId = try reader.readUntilDelimiter(0); + itemList.append(.{.amount = resultAmount, .item = BaseItemIndex.fromId(itemId) orelse return error.Invalid}); + } + if(itemList.items.len != len) return error.Invalid; + // Find the recipe in our list: + outer: for(main.items.recipes()) |*recipe| { + if(recipe.resultAmount == itemList.items[0].amount and recipe.resultItem == itemList.items[0].item and recipe.sourceItems.len == itemList.items.len - 1) { + for(itemList.items[1..], 0..) |item, i| { + if(item.amount != recipe.sourceAmounts[i] or item.item != recipe.sourceItems[i]) continue :outer; + } + break :blk recipe; + } + } + return error.Invalid; + }, + }, + .blockInventory => .{.blockInventory = try reader.readVec(Vec3i)}, + .other => .{.other = {}}, + .alreadyFreed => return error.Invalid, + }; + const typ: Type = switch(typeEnum) { + inline .normal, .creative, .crafting => |tag| tag, + .workbench => .{.workbench = ToolTypeIndex.fromId(reader.remaining) orelse return error.Invalid}, + }; + try Sync.ServerSide.createInventory(user.?, id, len, typ, source); + return .{ + .inv = Sync.ServerSide.getInventory(user.?, id) orelse return error.InventoryNotFound, + .source = source, + }; + } + }; + + const Close = struct { // MARK: Close + inv: Inventory, + allocator: NeverFailingAllocator, + + fn run(_: Close, _: NeverFailingAllocator, _: *Command, _: Side, _: ?*main.server.User, _: Gamemode) error{serverFailure}!void {} + + fn finalize(self: Close, side: Side, reader: *utils.BinaryReader) !void { + if(side != .client) return; + self.inv._deinit(self.allocator, .client); + if(reader.remaining.len != 0) { + const serverId = try reader.readEnum(InventoryId); + Sync.ClientSide.unmapServerId(serverId, self.inv.id); + } + } + + fn serialize(self: Close, writer: *utils.BinaryWriter) void { + writer.writeEnum(InventoryId, self.inv.id); + } + + fn deserialize(reader: *utils.BinaryReader, side: Side, user: ?*main.server.User) !Close { + if(side != .server or user == null) return error.Invalid; + const id = try reader.readEnum(InventoryId); + try Sync.ServerSide.closeInventory(user.?, id); + return undefined; + } + }; + + const DepositOrSwap = struct { // MARK: DepositOrSwap + dest: InventoryAndSlot, + source: InventoryAndSlot, + + fn run(self: DepositOrSwap, allocator: NeverFailingAllocator, cmd: *Command, side: Side, user: ?*main.server.User, gamemode: Gamemode) error{serverFailure}!void { + std.debug.assert(self.source.inv.type == .normal); + if(self.dest.inv.type == .creative) { + try FillFromCreative.run(.{.dest = self.source, .item = self.dest.ref().item}, allocator, cmd, side, user, gamemode); + return; + } + if(self.dest.inv.type == .crafting) { + cmd.tryCraftingTo(allocator, self.source.inv, self.dest, side, user); + return; + } + if(self.dest.inv.type == .workbench and self.dest.slot != 25 and self.dest.inv.type.workbench.slotInfos()[self.dest.slot].disabled) return; + if(self.dest.inv.type == .workbench and self.dest.slot == 25) { + if(self.source.ref().item == .null and self.dest.ref().item != .null) { + cmd.executeBaseOperation(allocator, .{.move = .{ + .dest = self.source, + .source = self.dest, + .amount = 1, + }}, side); + cmd.removeToolCraftingIngredients(allocator, self.dest.inv, side); + } + return; + } + if(self.dest.inv.type == .workbench and !canPutIntoWorkbench(self.source)) return; + + const itemDest = self.dest.ref().item; + const itemSource = self.source.ref().item; + if(itemDest != .null and itemSource != .null) { + if(std.meta.eql(itemDest, itemSource)) { + if(self.dest.ref().amount >= itemDest.stackSize()) return; + const amount = @min(itemDest.stackSize() - self.dest.ref().amount, self.source.ref().amount); + cmd.executeBaseOperation(allocator, .{.move = .{ + .dest = self.dest, + .source = self.source, + .amount = amount, + }}, side); + return; + } + } + if(self.source.inv.type == .workbench and !canPutIntoWorkbench(self.dest)) return; + cmd.executeBaseOperation(allocator, .{.swap = .{ + .dest = self.dest, + .source = self.source, + }}, side); + } + + fn serialize(self: DepositOrSwap, writer: *utils.BinaryWriter) void { + self.dest.write(writer); + self.source.write(writer); + } + + fn deserialize(reader: *utils.BinaryReader, side: Side, user: ?*main.server.User) !DepositOrSwap { + return .{ + .dest = try InventoryAndSlot.read(reader, side, user), + .source = try InventoryAndSlot.read(reader, side, user), + }; + } + }; + + const Deposit = struct { // MARK: Deposit + dest: InventoryAndSlot, + source: InventoryAndSlot, + amount: u16, + + fn run(self: Deposit, allocator: NeverFailingAllocator, cmd: *Command, side: Side, user: ?*main.server.User, gamemode: Gamemode) error{serverFailure}!void { + if(self.source.inv.type != .normal and (self.source.inv.type != .creative or self.dest.inv.type != .normal)) return error.serverFailure; + if(self.dest.inv.type == .crafting) return; + if(self.dest.inv.type == .workbench and (self.dest.slot == 25 or self.dest.inv.type.workbench.slotInfos()[self.dest.slot].disabled)) return; + if(self.dest.inv.type == .workbench and !canPutIntoWorkbench(self.source)) return; + const itemSource = self.source.ref().item; + if(itemSource == .null) return; + if(self.source.inv.type == .creative) { + var amount: u16 = self.amount; + if(std.meta.eql(self.dest.ref().item, itemSource)) { + amount = @min(self.dest.ref().amount + self.amount, itemSource.stackSize()); + } + try FillFromCreative.run(.{.dest = self.dest, .item = itemSource, .amount = amount}, allocator, cmd, side, user, gamemode); + return; + } + const itemDest = self.dest.ref().item; + if(itemDest != .null) { + if(std.meta.eql(itemDest, itemSource)) { + if(self.dest.ref().amount >= itemDest.stackSize()) return; + const amount = @min(itemDest.stackSize() - self.dest.ref().amount, self.source.ref().amount, self.amount); + cmd.executeBaseOperation(allocator, .{.move = .{ + .dest = self.dest, + .source = self.source, + .amount = amount, + }}, side); + } + } else { + const amount = @min(self.amount, self.source.ref().amount); + cmd.executeBaseOperation(allocator, .{.move = .{ + .dest = self.dest, + .source = self.source, + .amount = amount, + }}, side); + } + } + + fn serialize(self: Deposit, writer: *utils.BinaryWriter) void { + self.dest.write(writer); + self.source.write(writer); + writer.writeInt(u16, self.amount); + } + + fn deserialize(reader: *utils.BinaryReader, side: Side, user: ?*main.server.User) !Deposit { + return .{ + .dest = try InventoryAndSlot.read(reader, side, user), + .source = try InventoryAndSlot.read(reader, side, user), + .amount = try reader.readInt(u16), + }; + } + }; + + const TakeHalf = struct { // MARK: TakeHalf + dest: InventoryAndSlot, + source: InventoryAndSlot, + + fn run(self: TakeHalf, allocator: NeverFailingAllocator, cmd: *Command, side: Side, user: ?*main.server.User, gamemode: Gamemode) error{serverFailure}!void { + std.debug.assert(self.dest.inv.type == .normal); + if(self.source.inv.type == .creative) { + if(self.dest.ref().item == .null) { + const item = self.source.ref().item; + try FillFromCreative.run(.{.dest = self.dest, .item = item}, allocator, cmd, side, user, gamemode); + } + return; + } + if(self.source.inv.type == .crafting) { + cmd.tryCraftingTo(allocator, self.dest.inv, self.source, side, user); + return; + } + if(self.source.inv.type == .workbench and self.source.slot != 25 and self.source.inv.type.workbench.slotInfos()[self.source.slot].disabled) return; + if(self.source.inv.type == .workbench and self.source.slot == 25) { + if(self.dest.ref().item == .null and self.source.ref().item != .null) { + cmd.executeBaseOperation(allocator, .{.move = .{ + .dest = self.dest, + .source = self.source, + .amount = 1, + }}, side); + cmd.removeToolCraftingIngredients(allocator, self.source.inv, side); + } + return; + } + const itemSource = self.source.ref().item; + if(itemSource == .null) return; + const desiredAmount = (1 + self.source.ref().amount)/2; + const itemDest = self.dest.ref().item; + if(itemDest != .null) { + if(std.meta.eql(itemDest, itemSource)) { + if(self.dest.ref().amount >= itemDest.stackSize()) return; + const amount = @min(itemDest.stackSize() - self.dest.ref().amount, desiredAmount); + cmd.executeBaseOperation(allocator, .{.move = .{ + .dest = self.dest, + .source = self.source, + .amount = amount, + }}, side); + } + } else { + cmd.executeBaseOperation(allocator, .{.move = .{ + .dest = self.dest, + .source = self.source, + .amount = desiredAmount, + }}, side); + } + } + + fn serialize(self: TakeHalf, writer: *utils.BinaryWriter) void { + self.dest.write(writer); + self.source.write(writer); + } + + fn deserialize(reader: *utils.BinaryReader, side: Side, user: ?*main.server.User) !TakeHalf { + return .{ + .dest = try InventoryAndSlot.read(reader, side, user), + .source = try InventoryAndSlot.read(reader, side, user), + }; + } + }; + + const Drop = struct { // MARK: Drop + source: InventoryAndSlot, + desiredAmount: u16 = 0xffff, + + fn run(self: Drop, allocator: NeverFailingAllocator, cmd: *Command, side: Side, user: ?*main.server.User, _: Gamemode) error{serverFailure}!void { + if(self.source.inv.type == .creative) return; + if(self.source.ref().item == .null) return; + if(self.source.inv.type == .crafting) { + if(self.source.slot != self.source.inv._items.len - 1) return; + var _items: [1]ItemStack = .{.{.item = .null, .amount = 0}}; + const temp: Inventory = .{ + .type = .normal, + ._items = &_items, + .id = undefined, + .source = undefined, + .callbacks = .{}, + }; + cmd.tryCraftingTo(allocator, temp, self.source, side, user); + std.debug.assert(cmd.baseOperations.pop().create.dest.inv._items.ptr == temp._items.ptr); // Remove the extra step from undo list (we cannot undo dropped items) + if(_items[0].item != .null) { + if(side == .server) { + const direction = vec.rotateZ(vec.rotateX(Vec3f{0, 1, 0}, -user.?.player.rot[0]), -user.?.player.rot[2]); + main.server.world.?.dropWithCooldown(_items[0], user.?.player.pos, direction, 20, main.server.updatesPerSec*2); + } + } + return; + } + if(self.source.inv.type == .workbench and self.source.slot != 25 and self.source.inv.type.workbench.slotInfos()[self.source.slot].disabled) return; + if(self.source.inv.type == .workbench and self.source.slot == 25) { + cmd.removeToolCraftingIngredients(allocator, self.source.inv, side); + } + const amount = @min(self.source.ref().amount, self.desiredAmount); + if(side == .server) { + const direction = vec.rotateZ(vec.rotateX(Vec3f{0, 1, 0}, -user.?.player.rot[0]), -user.?.player.rot[2]); + main.server.world.?.dropWithCooldown(.{.item = self.source.ref().item.clone(), .amount = amount}, user.?.player.pos, direction, 20, main.server.updatesPerSec*2); + } + cmd.executeBaseOperation(allocator, .{.delete = .{ + .source = self.source, + .amount = amount, + }}, side); + } + + fn serialize(self: Drop, writer: *utils.BinaryWriter) void { + self.source.write(writer); + if(self.desiredAmount != 0xffff) { + writer.writeInt(u16, self.desiredAmount); + } + } + + fn deserialize(reader: *utils.BinaryReader, side: Side, user: ?*main.server.User) !Drop { + return .{ + .source = try InventoryAndSlot.read(reader, side, user), + .desiredAmount = reader.readInt(u16) catch 0xffff, + }; + } + }; + + const FillFromCreative = struct { // MARK: FillFromCreative + dest: InventoryAndSlot, + item: Item, + amount: u16 = 0, + + fn run(self: FillFromCreative, allocator: NeverFailingAllocator, cmd: *Command, side: Side, user: ?*main.server.User, mode: Gamemode) error{serverFailure}!void { + if(self.dest.inv.type == .workbench and (self.dest.slot == 25 or self.dest.inv.type.workbench.slotInfos()[self.dest.slot].disabled)) return; + if(side == .server and user != null and mode != .creative) return; + if(side == .client and mode != .creative) return; + + if(!self.dest.ref().empty()) { + cmd.executeBaseOperation(allocator, .{.delete = .{ + .source = self.dest, + .amount = self.dest.ref().amount, + }}, side); + } + if(self.item != .null) { + cmd.executeBaseOperation(allocator, .{.create = .{ + .dest = self.dest, + .item = self.item, + .amount = if(self.amount == 0) self.item.stackSize() else self.amount, + }}, side); + } + } + + fn serialize(self: FillFromCreative, writer: *utils.BinaryWriter) void { + self.dest.write(writer); + writer.writeInt(u16, self.amount); + if(self.item != .null) { + const zon = ZonElement.initObject(main.stackAllocator); + defer zon.deinit(main.stackAllocator); + self.item.insertIntoZon(main.stackAllocator, zon); + const string = zon.toStringEfficient(main.stackAllocator, &.{}); + defer main.stackAllocator.free(string); + writer.writeSlice(string); + } + } + + fn deserialize(reader: *utils.BinaryReader, side: Side, user: ?*main.server.User) !FillFromCreative { + const dest = try InventoryAndSlot.read(reader, side, user); + const amount = try reader.readInt(u16); + var item: Item = .null; + if(reader.remaining.len != 0) { + const zon = ZonElement.parseFromString(main.stackAllocator, null, reader.remaining); + defer zon.deinit(main.stackAllocator); + item = try Item.init(zon); + } + return .{ + .dest = dest, + .item = item, + .amount = amount, + }; + } + }; + + const DepositOrDrop = struct { // MARK: DepositOrDrop + dest: Inventory, + source: Inventory, + dropLocation: Vec3d, + + pub fn run(self: DepositOrDrop, allocator: NeverFailingAllocator, cmd: *Command, side: Side, user: ?*main.server.User, _: Gamemode) error{serverFailure}!void { + std.debug.assert(self.dest.type == .normal); + if(self.source.type == .creative) return; + if(self.source.type == .crafting) return; + var sourceItems = self.source._items; + if(self.source.type == .workbench) sourceItems = self.source._items[0..25]; + outer: for(sourceItems, 0..) |*sourceStack, sourceSlot| { + if(sourceStack.item == .null) continue; + for(self.dest._items, 0..) |*destStack, destSlot| { + if(std.meta.eql(destStack.item, sourceStack.item)) { + const amount = @min(destStack.item.stackSize() - destStack.amount, sourceStack.amount); + cmd.executeBaseOperation(allocator, .{.move = .{ + .dest = .{.inv = self.dest, .slot = @intCast(destSlot)}, + .source = .{.inv = self.source, .slot = @intCast(sourceSlot)}, + .amount = amount, + }}, side); + if(sourceStack.amount == 0) { + continue :outer; + } + } + } + for(self.dest._items, 0..) |*destStack, destSlot| { + if(destStack.item == .null) { + cmd.executeBaseOperation(allocator, .{.swap = .{ + .dest = .{.inv = self.dest, .slot = @intCast(destSlot)}, + .source = .{.inv = self.source, .slot = @intCast(sourceSlot)}, + }}, side); + continue :outer; + } + } + if(side == .server) { + const direction = if(user) |_user| vec.rotateZ(vec.rotateX(Vec3f{0, 1, 0}, -_user.player.rot[0]), -_user.player.rot[2]) else Vec3f{0, 0, 0}; + main.server.world.?.drop(sourceStack.clone(), self.dropLocation, direction, 20); + } + cmd.executeBaseOperation(allocator, .{.delete = .{ + .source = .{.inv = self.source, .slot = @intCast(sourceSlot)}, + .amount = self.source._items[sourceSlot].amount, + }}, side); + } + } + + fn serialize(self: DepositOrDrop, writer: *utils.BinaryWriter) void { + writer.writeEnum(InventoryId, self.dest.id); + writer.writeEnum(InventoryId, self.source.id); + } + + fn deserialize(reader: *utils.BinaryReader, side: Side, user: ?*main.server.User) !DepositOrDrop { + const destId = try reader.readEnum(InventoryId); + const sourceId = try reader.readEnum(InventoryId); + return .{ + .dest = Sync.getInventory(destId, side, user) orelse return error.InventoryNotFound, + .source = Sync.getInventory(sourceId, side, user) orelse return error.InventoryNotFound, + .dropLocation = (user orelse return error.Invalid).player.pos, + }; + } + }; + + const DepositToAny = struct { // MARK: DepositToAny + dest: Inventory, + source: InventoryAndSlot, + amount: u16, + + fn run(self: DepositToAny, allocator: NeverFailingAllocator, cmd: *Command, side: Side, user: ?*main.server.User, _: Gamemode) error{serverFailure}!void { + if(self.dest.type == .creative) return; + if(self.dest.type == .crafting) return; + if(self.dest.type == .workbench) return; + if(self.source.inv.type == .crafting) { + cmd.tryCraftingTo(allocator, self.dest, self.source, side, user); + return; + } + const sourceStack = self.source.ref(); + if(sourceStack.item == .null) return; + if(self.amount > sourceStack.amount) return; + + var remainingAmount = self.amount; + var selectedEmptySlot: ?u32 = null; + for(self.dest._items, 0..) |*destStack, destSlot| { + if(destStack.item == .null and selectedEmptySlot == null) { + selectedEmptySlot = @intCast(destSlot); + } + if(std.meta.eql(destStack.item, sourceStack.item)) { + const amount = @min(sourceStack.item.stackSize() - destStack.amount, remainingAmount); + if(amount == 0) continue; + cmd.executeBaseOperation(allocator, .{.move = .{ + .dest = .{.inv = self.dest, .slot = @intCast(destSlot)}, + .source = self.source, + .amount = amount, + }}, side); + remainingAmount -= amount; + if(remainingAmount == 0) break; + } + } + if(remainingAmount > 0 and selectedEmptySlot != null) { + cmd.executeBaseOperation(allocator, .{.move = .{ + .dest = .{.inv = self.dest, .slot = selectedEmptySlot.?}, + .source = self.source, + .amount = remainingAmount, + }}, side); + } + } + + fn serialize(self: DepositToAny, writer: *utils.BinaryWriter) void { + writer.writeEnum(InventoryId, self.dest.id); + self.source.write(writer); + writer.writeInt(u16, self.amount); + } + + fn deserialize(reader: *utils.BinaryReader, side: Side, user: ?*main.server.User) !DepositToAny { + const destId = try reader.readEnum(InventoryId); + return .{ + .dest = Sync.getInventory(destId, side, user) orelse return error.InventoryNotFound, + .source = try InventoryAndSlot.read(reader, side, user), + .amount = try reader.readInt(u16), + }; + } + }; + + const Clear = struct { // MARK: Clear + inv: Inventory, + + pub fn run(self: Clear, allocator: NeverFailingAllocator, cmd: *Command, side: Side, _: ?*main.server.User, _: Gamemode) error{serverFailure}!void { + if(self.inv.type == .creative) return; + if(self.inv.type == .crafting) return; + var items = self.inv._items; + if(self.inv.type == .workbench) items = self.inv._items[0..25]; + for(items, 0..) |stack, slot| { + if(stack.item == .null) continue; + + cmd.executeBaseOperation(allocator, .{.delete = .{ + .source = .{.inv = self.inv, .slot = @intCast(slot)}, + .amount = stack.amount, + }}, side); + } + } + + fn serialize(self: Clear, writer: *utils.BinaryWriter) void { + writer.writeEnum(InventoryId, self.inv.id); + } + + fn deserialize(reader: *utils.BinaryReader, side: Side, user: ?*main.server.User) !Clear { + const invId = try reader.readEnum(InventoryId); + return .{ + .inv = Sync.getInventory(invId, side, user) orelse return error.InventoryNotFound, + }; + } + }; + + const UpdateBlock = struct { // MARK: UpdateBlock + source: InventoryAndSlot, + pos: Vec3i, + dropLocation: BlockDropLocation, + oldBlock: Block, + newBlock: Block, + + const half = @as(Vec3f, @splat(0.5)); + const itemHitBoxMargin: f32 = @floatCast(main.itemdrop.ItemDropManager.radius); + const itemHitBoxMarginVec: Vec3f = @splat(itemHitBoxMargin); + + const BlockDropLocation = struct { + dir: Neighbor, + min: Vec3f, + max: Vec3f, + + pub fn drop(self: BlockDropLocation, pos: Vec3i, newBlock: Block, _drop: main.blocks.BlockDrop) void { + if(newBlock.collide()) { + self.dropOutside(pos, _drop); + } else { + self.dropInside(pos, _drop); + } + } + fn dropInside(self: BlockDropLocation, pos: Vec3i, _drop: main.blocks.BlockDrop) void { + for(_drop.items) |itemStack| { + main.server.world.?.drop(itemStack.clone(), self.insidePos(pos), self.dropDir(), self.dropVelocity()); + } + } + fn insidePos(self: BlockDropLocation, _pos: Vec3i) Vec3d { + const pos: Vec3d = @floatFromInt(_pos); + return pos + self.randomOffset(); + } + fn randomOffset(self: BlockDropLocation) Vec3f { + const max = @min(@as(Vec3f, @splat(1.0)) - itemHitBoxMarginVec, @max(itemHitBoxMarginVec, self.max - itemHitBoxMarginVec)); + const min = @min(max, @max(itemHitBoxMarginVec, self.min + itemHitBoxMarginVec)); + const center = (max + min)*half; + const width = (max - min)*half; + return center + width*main.random.nextFloatVectorSigned(3, &main.seed)*half; + } + fn dropOutside(self: BlockDropLocation, pos: Vec3i, _drop: main.blocks.BlockDrop) void { + for(_drop.items) |itemStack| { + main.server.world.?.drop(itemStack.clone(), self.outsidePos(pos), self.dropDir(), self.dropVelocity()); + } + } + fn outsidePos(self: BlockDropLocation, _pos: Vec3i) Vec3d { + const pos: Vec3d = @floatFromInt(_pos); + return pos + self.randomOffset()*self.minor() + self.directionOffset()*self.major() + self.direction()*itemHitBoxMarginVec; + } + fn directionOffset(self: BlockDropLocation) Vec3d { + return half + self.direction()*half; + } + inline fn direction(self: BlockDropLocation) Vec3d { + return @floatFromInt(self.dir.relPos()); + } + inline fn major(self: BlockDropLocation) Vec3d { + return @floatFromInt(@abs(self.dir.relPos())); + } + inline fn minor(self: BlockDropLocation) Vec3d { + return @floatFromInt(self.dir.orthogonalComponents()); + } + fn dropDir(self: BlockDropLocation) Vec3f { + const randomnessVec: Vec3f = main.random.nextFloatVectorSigned(3, &main.seed)*@as(Vec3f, @splat(0.25)); + const directionVec: Vec3f = @as(Vec3f, @floatCast(self.direction())) + randomnessVec; + const z: f32 = directionVec[2]; + return vec.normalize(Vec3f{ + directionVec[0], + directionVec[1], + if(z < -0.5) 0 else if(z < 0.0) (z + 0.5)*4.0 else z + 2.0, + }); + } + fn dropVelocity(self: BlockDropLocation) f32 { + const velocity = 3.5 + main.random.nextFloatSigned(&main.seed)*0.5; + if(self.direction()[2] < -0.5) return velocity*0.333; + return velocity; + } + }; + + fn run(self: UpdateBlock, allocator: NeverFailingAllocator, cmd: *Command, side: Side, user: ?*main.server.User, gamemode: Gamemode) error{serverFailure}!void { + if(self.source.inv.type != .normal) return; + + const stack = self.source.ref(); + + var shouldDropSourceBlockOnSuccess: bool = true; + const costOfChange = if(gamemode != .creative) self.oldBlock.canBeChangedInto(self.newBlock, stack.*, &shouldDropSourceBlockOnSuccess) else .yes; + + // Check if we can change it: + if(!switch(costOfChange) { + .no => false, + .yes => true, + .yes_costsDurability => |_| stack.item == .tool, + .yes_costsItems => |amount| stack.amount >= amount, + .yes_dropsItems => true, + }) { + if(side == .server) { + // Inform the client of the actual block: + var writer = main.utils.BinaryWriter.init(main.stackAllocator); + defer writer.deinit(); + + const actualBlock = main.server.world.?.getBlockAndBlockEntityData(self.pos[0], self.pos[1], self.pos[2], &writer) orelse return; + main.network.protocols.blockUpdate.send(user.?.conn, &.{.init(self.pos, actualBlock, writer.data.items)}); + } + return; + } + + if(side == .server) { + if(main.server.world.?.cmpxchgBlock(self.pos[0], self.pos[1], self.pos[2], self.oldBlock, self.newBlock) != null) { + // Inform the client of the actual block: + var writer = main.utils.BinaryWriter.init(main.stackAllocator); + defer writer.deinit(); + + const actualBlock = main.server.world.?.getBlockAndBlockEntityData(self.pos[0], self.pos[1], self.pos[2], &writer) orelse return; + main.network.protocols.blockUpdate.send(user.?.conn, &.{.init(self.pos, actualBlock, writer.data.items)}); + return error.serverFailure; + } + } + + // Apply inventory changes: + switch(costOfChange) { + .no => unreachable, + .yes => {}, + .yes_costsDurability => |durability| { + cmd.executeBaseOperation(allocator, .{.useDurability = .{ + .source = self.source, + .durability = durability, + }}, side); + }, + .yes_costsItems => |amount| { + cmd.executeBaseOperation(allocator, .{.delete = .{ + .source = self.source, + .amount = amount, + }}, side); + }, + .yes_dropsItems => |amount| { + if(side == .server and gamemode != .creative) { + for(0..amount) |_| { + for(self.newBlock.blockDrops()) |drop| { + if(drop.chance == 1 or main.random.nextFloat(&main.seed) < drop.chance) { + self.dropLocation.drop(self.pos, self.newBlock, drop); + } + } + } + } + }, + } + + if(side == .server and gamemode != .creative and self.oldBlock.typ != self.newBlock.typ and shouldDropSourceBlockOnSuccess) { + for(self.oldBlock.blockDrops()) |drop| { + if(drop.chance == 1 or main.random.nextFloat(&main.seed) < drop.chance) { + self.dropLocation.drop(self.pos, self.newBlock, drop); + } + } + } + } + + fn serialize(self: UpdateBlock, writer: *utils.BinaryWriter) void { + self.source.write(writer); + writer.writeVec(Vec3i, self.pos); + writer.writeEnum(Neighbor, self.dropLocation.dir); + writer.writeVec(Vec3f, self.dropLocation.min); + writer.writeVec(Vec3f, self.dropLocation.max); + writer.writeInt(u32, @as(u32, @bitCast(self.oldBlock))); + writer.writeInt(u32, @as(u32, @bitCast(self.newBlock))); + } + + fn deserialize(reader: *utils.BinaryReader, side: Side, user: ?*main.server.User) !UpdateBlock { + return .{ + .source = try InventoryAndSlot.read(reader, side, user), + .pos = try reader.readVec(Vec3i), + .dropLocation = .{ + .dir = try reader.readEnum(Neighbor), + .min = try reader.readVec(Vec3f), + .max = try reader.readVec(Vec3f), + }, + .oldBlock = @bitCast(try reader.readInt(u32)), + .newBlock = @bitCast(try reader.readInt(u32)), + }; + } + }; + + const AddHealth = struct { // MARK: AddHealth + target: u32, + health: f32, + cause: main.game.DamageType, + + pub fn run(self: AddHealth, allocator: NeverFailingAllocator, cmd: *Command, side: Side, _: ?*main.server.User, _: Gamemode) error{serverFailure}!void { + var target: ?*main.server.User = null; + + if(side == .server) { + const userList = main.server.getUserListAndIncreaseRefCount(main.stackAllocator); + defer main.server.freeUserListAndDecreaseRefCount(main.stackAllocator, userList); + for(userList) |user| { + if(user.id == self.target) { + target = user; + break; + } + } + + if(target == null) return error.serverFailure; + + if(target.?.gamemode.raw == .creative) return; + } else { + if(main.game.Player.gamemode.raw == .creative) return; + } + + cmd.executeBaseOperation(allocator, .{.addHealth = .{ + .target = target, + .health = self.health, + .cause = self.cause, + .previous = if(side == .server) target.?.player.health else main.game.Player.super.health, + }}, side); + } + + fn serialize(self: AddHealth, writer: *utils.BinaryWriter) void { + writer.writeInt(u32, self.target); + writer.writeInt(u32, @bitCast(self.health)); + writer.writeEnum(main.game.DamageType, self.cause); + } + + fn deserialize(reader: *utils.BinaryReader, _: Side, user: ?*main.server.User) !AddHealth { + const result: AddHealth = .{ + .target = try reader.readInt(u32), + .health = @bitCast(try reader.readInt(u32)), + .cause = try reader.readEnum(main.game.DamageType), + }; + if(user.?.id != result.target) return error.Invalid; + return result; + } + }; +}; + +const SourceType = enum(u8) { + alreadyFreed = 0, + playerInventory = 1, + hand = 3, + recipe = 4, + blockInventory = 5, + other = 0xff, // TODO: List every type separately here. +}; +pub const Source = union(SourceType) { + alreadyFreed: void, + playerInventory: u32, + hand: u32, + recipe: *const main.items.Recipe, + blockInventory: Vec3i, + other: void, +}; + +const Inventory = @This(); // MARK: Inventory + +const TypeEnum = enum(u8) { + normal = 0, + creative = 1, + crafting = 2, + workbench = 3, +}; +const Type = union(TypeEnum) { + normal: void, + creative: void, + crafting: void, + workbench: ToolTypeIndex, + + pub fn shouldDepositToUserOnClose(self: Type) bool { + return self == .workbench; + } +}; +type: Type, +id: InventoryId, +_items: []ItemStack, +source: Source, +callbacks: Callbacks, + +pub fn init(allocator: NeverFailingAllocator, _size: usize, _type: Type, source: Source, callbacks: Callbacks) Inventory { + const self = _init(allocator, _size, _type, source, .client, callbacks); + Sync.ClientSide.executeCommand(.{.open = .{.inv = self, .source = source}}); + return self; +} + +fn _init(allocator: NeverFailingAllocator, _size: usize, _type: Type, source: Source, side: Side, callbacks: Callbacks) Inventory { + if(_type == .workbench) std.debug.assert(_size == 26); + const self = Inventory{ + .type = _type, + ._items = allocator.alloc(ItemStack, _size), + .id = switch(side) { + .client => Sync.ClientSide.nextId(), + .server => Sync.ServerSide.nextId(), + }, + .source = source, + .callbacks = callbacks, + }; + for(self._items) |*item| { + item.* = ItemStack{}; + } + return self; +} + +pub fn deinit(self: Inventory, allocator: NeverFailingAllocator) void { + if(main.game.world.?.connected) { + Sync.ClientSide.executeCommand(.{.close = .{.inv = self, .allocator = allocator}}); + } else { + Sync.ClientSide.mutex.lock(); + defer Sync.ClientSide.mutex.unlock(); + self._deinit(allocator, .client); + } +} + +fn _deinit(self: Inventory, allocator: NeverFailingAllocator, side: Side) void { + switch(side) { + .client => Sync.ClientSide.freeId(self.id), + .server => Sync.ServerSide.freeId(self.id), + } + for(self._items) |*item| { + item.deinit(); + } + allocator.free(self._items); +} + +fn update(self: Inventory) void { + defer if(self.callbacks.onUpdateCallback) |cb| cb(self.source); + if(self.type == .workbench) { + self._items[self._items.len - 1].deinit(); + self._items[self._items.len - 1] = .{}; + var availableItems: [25]?BaseItemIndex = undefined; + const slotInfos = self.type.workbench.slotInfos(); + + for(0..25) |i| { + if(self._items[i].item == .baseItem) { + availableItems[i] = self._items[i].item.baseItem; + } else { + if(!slotInfos[i].optional and !slotInfos[i].disabled) { + return; + } + availableItems[i] = null; + } + } + var hash = std.hash.Crc32.init(); + for(availableItems) |item| { + if(item != null) { + hash.update(item.?.id()); + } else { + hash.update("none"); + } + } + self._items[self._items.len - 1].item = Item{.tool = Tool.initFromCraftingGrid(availableItems, hash.final(), self.type.workbench)}; + self._items[self._items.len - 1].amount = 1; + } +} + +pub fn depositOrSwap(dest: Inventory, destSlot: u32, carried: Inventory) void { + Sync.ClientSide.executeCommand(.{.depositOrSwap = .{.dest = .{.inv = dest, .slot = destSlot}, .source = .{.inv = carried, .slot = 0}}}); +} + +pub fn deposit(dest: Inventory, destSlot: u32, source: Inventory, sourceSlot: u32, amount: u16) void { + Sync.ClientSide.executeCommand(.{.deposit = .{.dest = .{.inv = dest, .slot = destSlot}, .source = .{.inv = source, .slot = sourceSlot}, .amount = amount}}); +} + +pub fn takeHalf(source: Inventory, sourceSlot: u32, carried: Inventory) void { + Sync.ClientSide.executeCommand(.{.takeHalf = .{.dest = .{.inv = carried, .slot = 0}, .source = .{.inv = source, .slot = sourceSlot}}}); +} + +pub fn distribute(carried: Inventory, destinationInventories: []const Inventory, destinationSlots: []const u32) void { + const amount = carried._items[0].amount/destinationInventories.len; + if(amount == 0) return; + for(0..destinationInventories.len) |i| { + destinationInventories[i].deposit(destinationSlots[i], carried, 0, @intCast(amount)); + } +} + +pub fn depositOrDrop(dest: Inventory, source: Inventory) void { + Sync.ClientSide.executeCommand(.{.depositOrDrop = .{.dest = dest, .source = source, .dropLocation = undefined}}); +} + +pub fn depositToAny(source: Inventory, sourceSlot: u32, dest: Inventory, amount: u16) void { + Sync.ClientSide.executeCommand(.{.depositToAny = .{.dest = dest, .source = .{.inv = source, .slot = sourceSlot}, .amount = amount}}); +} + +pub fn dropStack(source: Inventory, sourceSlot: u32) void { + Sync.ClientSide.executeCommand(.{.drop = .{.source = .{.inv = source, .slot = sourceSlot}}}); +} + +pub fn dropOne(source: Inventory, sourceSlot: u32) void { + Sync.ClientSide.executeCommand(.{.drop = .{.source = .{.inv = source, .slot = sourceSlot}, .desiredAmount = 1}}); +} + +pub fn fillFromCreative(dest: Inventory, destSlot: u32, item: Item) void { + Sync.ClientSide.executeCommand(.{.fillFromCreative = .{.dest = .{.inv = dest, .slot = destSlot}, .item = item}}); +} + +pub fn fillAmountFromCreative(dest: Inventory, destSlot: u32, item: Item, amount: u16) void { + Sync.ClientSide.executeCommand(.{.fillFromCreative = .{.dest = .{.inv = dest, .slot = destSlot}, .item = item, .amount = amount}}); +} + +pub fn placeBlock(self: Inventory, slot: u32) void { + main.renderer.MeshSelection.placeBlock(self, slot); +} + +pub fn breakBlock(self: Inventory, slot: u32, deltaTime: f64) void { + main.renderer.MeshSelection.breakBlock(self, slot, deltaTime); +} + +pub fn size(self: Inventory) usize { + return self._items.len; +} + +pub fn getItem(self: Inventory, slot: usize) Item { + return self._items[slot].item; +} + +pub fn getStack(self: Inventory, slot: usize) ItemStack { + return self._items[slot]; +} + +pub fn getAmount(self: Inventory, slot: usize) u16 { + return self._items[slot].amount; +} + +pub fn canHold(self: Inventory, sourceStack: ItemStack) bool { + if(sourceStack.amount == 0) return true; + + var remainingAmount = sourceStack.amount; + for(self._items) |*destStack| { + if(std.meta.eql(destStack.item, sourceStack.item) or destStack.item == .null) { + const amount = @min(sourceStack.item.stackSize() - destStack.amount, remainingAmount); + remainingAmount -= amount; + if(remainingAmount == 0) return true; + } + } + return false; +} + +pub fn toBytes(self: Inventory, writer: *BinaryWriter) void { + writer.writeVarInt(u32, @intCast(self._items.len)); + for(self._items) |stack| { + stack.toBytes(writer); + } +} + +pub fn fromBytes(self: Inventory, reader: *BinaryReader) void { + var remainingCount = reader.readVarInt(u32) catch 0; + for(self._items) |*stack| { + if(remainingCount == 0) { + stack.* = .{}; + continue; + } + remainingCount -= 1; + stack.* = ItemStack.fromBytes(reader) catch |err| { + std.log.err("Failed to read item stack from bytes: {s}", .{@errorName(err)}); + stack.* = .{}; + continue; + }; + } + for(0..remainingCount) |_| { + var stack = ItemStack.fromBytes(reader) catch continue; + if(stack.item != .null) { + std.log.err("Lost {} of {s}", .{stack.amount, stack.item.id().?}); + } + stack.deinit(); + } +} diff --git a/assets.zig b/assets.zig new file mode 100644 index 0000000000..2c6f317442 --- /dev/null +++ b/assets.zig @@ -0,0 +1,681 @@ +const std = @import("std"); + +const blocks_zig = @import("blocks.zig"); +const items_zig = @import("items.zig"); +const migrations_zig = @import("migrations.zig"); +const blueprints_zig = @import("blueprint.zig"); +const Blueprint = blueprints_zig.Blueprint; +const particles_zig = @import("particles.zig"); +const ZonElement = @import("zon.zig").ZonElement; +const main = @import("main"); +const biomes_zig = main.server.terrain.biomes; +const sbb = main.server.terrain.structure_building_blocks; +const NeverFailingAllocator = main.heap.NeverFailingAllocator; +const NeverFailingArenaAllocator = main.heap.NeverFailingArenaAllocator; +const ListUnmanaged = main.ListUnmanaged; +const files = main.files; + +var common: Assets = undefined; + +pub const Assets = struct { + pub const ZonHashMap = std.StringHashMapUnmanaged(ZonElement); + pub const BytesHashMap = std.StringHashMapUnmanaged([]const u8); + pub const AddonNameToZonMap = std.StringHashMapUnmanaged(ZonElement); + + blocks: ZonHashMap, + blockMigrations: AddonNameToZonMap, + items: ZonHashMap, + itemMigrations: ZonHashMap, + tools: ZonHashMap, + biomes: ZonHashMap, + biomeMigrations: AddonNameToZonMap, + recipes: ZonHashMap, + models: BytesHashMap, + structureBuildingBlocks: ZonHashMap, + blueprints: BytesHashMap, + particles: ZonHashMap, + + fn init() Assets { + return .{ + .blocks = .{}, + .blockMigrations = .{}, + .items = .{}, + .itemMigrations = .{}, + .tools = .{}, + .biomes = .{}, + .biomeMigrations = .{}, + .recipes = .{}, + .models = .{}, + .structureBuildingBlocks = .{}, + .blueprints = .{}, + .particles = .{}, + }; + } + fn deinit(self: *Assets, allocator: NeverFailingAllocator) void { + self.blocks.deinit(allocator.allocator); + self.blockMigrations.deinit(allocator.allocator); + self.items.deinit(allocator.allocator); + self.itemMigrations.deinit(allocator.allocator); + self.tools.deinit(allocator.allocator); + self.biomes.deinit(allocator.allocator); + self.biomeMigrations.deinit(allocator.allocator); + self.recipes.deinit(allocator.allocator); + self.models.deinit(allocator.allocator); + self.structureBuildingBlocks.deinit(allocator.allocator); + self.blueprints.deinit(allocator.allocator); + self.particles.deinit(allocator.allocator); + } + fn clone(self: Assets, allocator: NeverFailingAllocator) Assets { + return .{ + .blocks = self.blocks.clone(allocator.allocator) catch unreachable, + .blockMigrations = self.blockMigrations.clone(allocator.allocator) catch unreachable, + .items = self.items.clone(allocator.allocator) catch unreachable, + .itemMigrations = self.itemMigrations.clone(allocator.allocator) catch unreachable, + .tools = self.tools.clone(allocator.allocator) catch unreachable, + .biomes = self.biomes.clone(allocator.allocator) catch unreachable, + .biomeMigrations = self.biomeMigrations.clone(allocator.allocator) catch unreachable, + .recipes = self.recipes.clone(allocator.allocator) catch unreachable, + .models = self.models.clone(allocator.allocator) catch unreachable, + .structureBuildingBlocks = self.structureBuildingBlocks.clone(allocator.allocator) catch unreachable, + .blueprints = self.blueprints.clone(allocator.allocator) catch unreachable, + .particles = self.particles.clone(allocator.allocator) catch unreachable, + }; + } + fn read(self: *Assets, allocator: NeverFailingAllocator, assetDir: main.files.Dir, assetPath: []const u8) void { + const addons = Addon.discoverAll(main.stackAllocator, assetDir, assetPath); + defer addons.deinit(main.stackAllocator); + defer for(addons.items) |*addon| addon.deinit(main.stackAllocator); + + for(addons.items) |addon| { + addon.readAllZon(allocator, "blocks", true, &self.blocks, &self.blockMigrations); + addon.readAllZon(allocator, "items", true, &self.items, &self.itemMigrations); + addon.readAllZon(allocator, "tools", true, &self.tools, null); + addon.readAllZon(allocator, "biomes", true, &self.biomes, &self.biomeMigrations); + addon.readAllZon(allocator, "recipes", false, &self.recipes, null); + addon.readAllZon(allocator, "sbb", true, &self.structureBuildingBlocks, null); + addon.readAllBlueprints(allocator, "sbb", &self.blueprints); + addon.readAllModels(allocator, &self.models); + addon.readAllZon(allocator, "particles", true, &self.particles, null); + } + } + fn log(self: *Assets, typ: enum {common, world}) void { + std.log.info( + "Finished {s} assets reading with {} blocks, {} items, {} tools, {} biomes, {} recipes, {} structure building blocks, {} blueprints and {} particles", + .{@tagName(typ), self.blocks.count(), self.items.count(), self.tools.count(), self.biomes.count(), self.recipes.count(), self.structureBuildingBlocks.count(), self.blueprints.count(), self.particles.count()}, + ); + } + + const Addon = struct { + name: []const u8, + dir: files.Dir, + + fn discoverAll(allocator: NeverFailingAllocator, assetDir: main.files.Dir, path: []const u8) main.ListUnmanaged(Addon) { + var addons: main.ListUnmanaged(Addon) = .{}; + + var dir = assetDir.openIterableDir(path) catch |err| { + std.log.err("Can't open asset path {s}: {s}", .{path, @errorName(err)}); + return addons; + }; + defer dir.close(); + + var iterator = dir.iterate(); + while(iterator.next() catch |err| blk: { + std.log.err("Got error while iterating over asset path {s}: {s}", .{path, @errorName(err)}); + break :blk null; + }) |addon| { + if(addon.kind != .directory) continue; + + const directory = dir.openDir(addon.name) catch |err| { + std.log.err("Got error while reading addon {s} from {s}: {s}", .{addon.name, path, @errorName(err)}); + continue; + }; + addons.append(allocator, .{.name = allocator.dupe(u8, addon.name), .dir = directory}); + } + return addons; + } + + fn deinit(self: *Addon, allocator: NeverFailingAllocator) void { + self.dir.close(); + allocator.free(self.name); + } + + const Defaults = struct { + localArena: NeverFailingArenaAllocator = undefined, + localAllocator: NeverFailingAllocator = undefined, + defaults: std.StringHashMapUnmanaged(ZonElement) = .{}, + + fn init(self: *Defaults, allocator: NeverFailingAllocator) void { + self.localArena = .init(allocator); + self.localAllocator = self.localArena.allocator(); + } + + fn deinit(self: *Defaults) void { + self.localArena.deinit(); + } + + fn get(self: *Defaults, dir: main.files.Dir, dirPath: []const u8) ZonElement { + const result = self.defaults.getOrPut(self.localAllocator.allocator, dirPath) catch unreachable; + + if(!result.found_existing) { + result.key_ptr.* = self.localAllocator.dupe(u8, dirPath); + const default: ZonElement = self.read(dir) catch |err| blk: { + std.log.err("Failed to read default file: {s}", .{@errorName(err)}); + break :blk .null; + }; + + result.value_ptr.* = default; + } + + return result.value_ptr.*; + } + + fn read(self: *Defaults, dir: main.files.Dir) !ZonElement { + if(dir.readToZon(self.localAllocator, "_defaults.zig.zon")) |zon| { + return zon; + } else |err| { + if(err != error.FileNotFound) return err; + } + + if(dir.readToZon(self.localAllocator, "_defaults.zon")) |zon| { + return zon; + } else |err| { + if(err != error.FileNotFound) return err; + } + + return .null; + } + }; + + pub fn readAllZon(addon: Addon, allocator: NeverFailingAllocator, assetType: []const u8, hasDefaults: bool, output: *ZonHashMap, migrations: ?*AddonNameToZonMap) void { + var assetsDirectory = addon.dir.openIterableDir(assetType) catch |err| { + if(err != error.FileNotFound) { + std.log.err("Could not open addon directory {s}: {s}", .{assetType, @errorName(err)}); + } + return; + }; + defer assetsDirectory.close(); + + var defaultsStorage: Defaults = .{}; + defaultsStorage.init(main.stackAllocator); + defer defaultsStorage.deinit(); + + var walker = assetsDirectory.walk(main.stackAllocator); + defer walker.deinit(); + + while(walker.next() catch |err| blk: { + std.log.err("Got error while iterating addon directory {s}: {s}", .{assetType, @errorName(err)}); + break :blk null; + }) |entry| { + if(entry.kind != .file) continue; + if(std.ascii.startsWithIgnoreCase(entry.basename, "_defaults")) continue; + if(!std.ascii.endsWithIgnoreCase(entry.basename, ".zon")) continue; + if(std.ascii.startsWithIgnoreCase(entry.path, "textures")) continue; + if(std.ascii.eqlIgnoreCase(entry.basename, "_migrations.zig.zon")) continue; + + const id = createAssetStringID(allocator, addon.name, entry.path); + + const zon = assetsDirectory.readToZon(allocator, entry.path) catch |err| { + std.log.err("Could not open {s}/{s}: {s}", .{assetType, entry.path, @errorName(err)}); + continue; + }; + if(hasDefaults) { + zon.join(.preferLeft, defaultsStorage.get(main.files.Dir.init(entry.dir), entry.path[0 .. entry.path.len - entry.basename.len])); + } + output.put(allocator.allocator, id, zon) catch unreachable; + } + if(migrations != null) blk: { + const zon = assetsDirectory.readToZon(allocator, "_migrations.zig.zon") catch |err| { + if(err != error.FileNotFound) std.log.err("Cannot read {s} migration file for addon {s}", .{assetType, addon.name}); + break :blk; + }; + migrations.?.put(allocator.allocator, allocator.dupe(u8, addon.name), zon) catch unreachable; + } + } + + pub fn readAllBlueprints(addon: Addon, allocator: NeverFailingAllocator, subPath: []const u8, output: *BytesHashMap) void { + var assetsDirectory = addon.dir.openIterableDir(subPath) catch |err| { + if(err != error.FileNotFound) { + std.log.err("Could not open addon directory {s}: {s}", .{subPath, @errorName(err)}); + } + return; + }; + defer assetsDirectory.close(); + + var walker = assetsDirectory.walk(main.stackAllocator); + defer walker.deinit(); + + while(walker.next() catch |err| blk: { + std.log.err("Got error while iterating addon directory {s}: {s}", .{subPath, @errorName(err)}); + break :blk null; + }) |entry| { + if(entry.kind != .file) continue; + if(std.ascii.startsWithIgnoreCase(entry.basename, "_defaults")) continue; + if(!std.ascii.endsWithIgnoreCase(entry.basename, ".blp")) continue; + if(std.ascii.startsWithIgnoreCase(entry.basename, "_migrations")) continue; + + const id = createAssetStringID(allocator, addon.name, entry.path); + + const data = assetsDirectory.read(allocator, entry.path) catch |err| { + std.log.err("Could not open {s}/{s}: {s}", .{subPath, entry.path, @errorName(err)}); + continue; + }; + output.put(allocator.allocator, id, data) catch unreachable; + } + } + + pub fn readAllModels(addon: Addon, allocator: NeverFailingAllocator, output: *BytesHashMap) void { + const subPath = "models"; + var assetsDirectory = addon.dir.openIterableDir(subPath) catch |err| { + if(err != error.FileNotFound) { + std.log.err("Could not open addon directory {s}: {s}", .{subPath, @errorName(err)}); + } + return; + }; + defer assetsDirectory.close(); + var walker = assetsDirectory.walk(main.stackAllocator); + defer walker.deinit(); + + while(walker.next() catch |err| blk: { + std.log.err("Got error while iterating addon directory {s}: {s}", .{subPath, @errorName(err)}); + break :blk null; + }) |entry| { + if(entry.kind != .file) continue; + if(!std.ascii.endsWithIgnoreCase(entry.basename, ".obj")) continue; + + const id = createAssetStringID(allocator, addon.name, entry.path); + + const string = assetsDirectory.read(allocator, entry.path) catch |err| { + std.log.err("Could not open {s}/{s}: {s}", .{subPath, entry.path, @errorName(err)}); + continue; + }; + output.put(allocator.allocator, id, string) catch unreachable; + } + } + }; +}; + +fn createAssetStringID( + externalAllocator: NeverFailingAllocator, + addonName: []const u8, + relativeFilePath: []const u8, +) []u8 { + const baseNameEndIndex = if(std.ascii.endsWithIgnoreCase(relativeFilePath, ".zig.zon")) relativeFilePath.len - ".zig.zon".len else std.mem.lastIndexOfScalar(u8, relativeFilePath, '.') orelse relativeFilePath.len; + const pathNoExtension: []const u8 = relativeFilePath[0..baseNameEndIndex]; + + const assetId: []u8 = externalAllocator.alloc(u8, addonName.len + 1 + pathNoExtension.len); + + @memcpy(assetId[0..addonName.len], addonName); + assetId[addonName.len] = ':'; + + // Convert from windows to unix style separators. + for(0..pathNoExtension.len) |i| { + if(pathNoExtension[i] == '\\') { + assetId[addonName.len + 1 + i] = '/'; + } else { + assetId[addonName.len + 1 + i] = pathNoExtension[i]; + } + } + + return assetId; +} + +pub fn init() void { + biomes_zig.init(); + + common = .init(); + common.read(main.globalArena, main.files.cwd(), "assets/"); + common.log(.common); +} + +fn registerItem(assetFolder: []const u8, id: []const u8, zon: ZonElement) !void { + var split = std.mem.splitScalar(u8, id, ':'); + const mod = split.first(); + var texturePath: []const u8 = &.{}; + defer main.stackAllocator.free(texturePath); + var replacementTexturePath: []const u8 = &.{}; + defer main.stackAllocator.free(replacementTexturePath); + if(zon.get(?[]const u8, "texture", null)) |texture| { + texturePath = try std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{s}/items/textures/{s}", .{assetFolder, mod, texture}); + replacementTexturePath = try std.fmt.allocPrint(main.stackAllocator.allocator, "assets/{s}/items/textures/{s}", .{mod, texture}); + } + _ = items_zig.register(assetFolder, texturePath, replacementTexturePath, id, zon); +} + +fn registerTool(assetFolder: []const u8, id: []const u8, zon: ZonElement) void { + items_zig.registerTool(assetFolder, id, zon); +} + +fn registerBlock(assetFolder: []const u8, id: []const u8, zon: ZonElement) !void { + if(zon == .null) std.log.err("Missing block: {s}. Replacing it with default block.", .{id}); + + _ = blocks_zig.register(assetFolder, id, zon); + blocks_zig.meshes.register(assetFolder, id, zon); +} + +fn assignBlockItem(stringId: []const u8) !void { + const block = blocks_zig.getTypeById(stringId); + // TODO: This must be gone in PixelGuys/Cubyz#1205 + const index = items_zig.BaseItemIndex.fromId(stringId) orelse unreachable; + const item = &items_zig.itemList[@intFromEnum(index)]; + item.block = block; +} + +fn registerBiome(numericId: u32, stringId: []const u8, zon: ZonElement) void { + if(zon == .null) std.log.err("Missing biome: {s}. Replacing it with default biome.", .{stringId}); + biomes_zig.register(stringId, numericId, zon); +} + +fn registerRecipesFromZon(zon: ZonElement) void { + items_zig.registerRecipes(zon); +} + +pub const Palette = struct { // MARK: Palette + palette: main.List([]const u8), + + pub fn init(allocator: NeverFailingAllocator, zon: ZonElement, firstElement: ?[]const u8) !*Palette { + const self = switch(zon) { + .object => try loadFromZonLegacy(allocator, zon), + .array, .null => try loadFromZon(allocator, zon), + else => return error.InvalidPaletteFormat, + }; + + if(firstElement) |elem| { + if(self.palette.items.len == 0) { + self.palette.append(allocator.dupe(u8, elem)); + } + if(!std.mem.eql(u8, self.palette.items[0], elem)) { + return error.FistItemMismatch; + } + } + return self; + } + fn loadFromZon(allocator: NeverFailingAllocator, zon: ZonElement) !*Palette { + const items = zon.toSlice(); + + const self = allocator.create(Palette); + self.* = Palette{ + .palette = .initCapacity(allocator, items.len), + }; + errdefer self.deinit(); + + for(items) |name| { + const stringId = name.as(?[]const u8, null) orelse return error.InvalidPaletteFormat; + self.palette.appendAssumeCapacity(allocator.dupe(u8, stringId)); + } + return self; + } + fn loadFromZonLegacy(allocator: NeverFailingAllocator, zon: ZonElement) !*Palette { + // Using zon.object.count() here has the implication that array can not be sparse. + const paletteLength = zon.object.count(); + const translationPalette = main.stackAllocator.alloc(?[]const u8, paletteLength); + defer main.stackAllocator.free(translationPalette); + + @memset(translationPalette, null); + + var iterator = zon.object.iterator(); + while(iterator.next()) |entry| { + const numericId = entry.value_ptr.as(?usize, null) orelse return error.InvalidPaletteFormat; + const name = entry.key_ptr.*; + + if(numericId >= translationPalette.len) { + std.log.err("ID {} ('{s}') out of range. This can be caused by palette having missing block IDs.", .{numericId, name}); + return error.SparsePaletteNotAllowed; + } + translationPalette[numericId] = name; + } + + const self = allocator.create(Palette); + self.* = Palette{ + .palette = .initCapacity(allocator, paletteLength), + }; + errdefer self.deinit(); + + for(translationPalette) |val| { + self.palette.appendAssumeCapacity(allocator.dupe(u8, val orelse return error.MissingKeyInPalette)); + std.log.info("palette[{}]: {s}", .{self.palette.items.len, val.?}); + } + return self; + } + + pub fn deinit(self: *Palette) void { + for(self.palette.items) |item| { + self.palette.allocator.free(item); + } + const allocator = self.palette.allocator; + self.palette.deinit(); + allocator.destroy(self); + } + + pub fn add(self: *Palette, id: []const u8) void { + self.palette.append(self.palette.allocator.dupe(u8, id)); + } + + pub fn storeToZon(self: *Palette, allocator: NeverFailingAllocator) ZonElement { + const zon = ZonElement.initArray(allocator); + + zon.array.ensureCapacity(self.palette.items.len); + + for(self.palette.items) |item| { + zon.append(item); + } + return zon; + } + + pub fn size(self: *Palette) usize { + return self.palette.items.len; + } + + pub fn replaceEntry(self: *Palette, entryIndex: usize, newEntry: []const u8) void { + self.palette.allocator.free(self.palette.items[entryIndex]); + self.palette.items[entryIndex] = self.palette.allocator.dupe(u8, newEntry); + } +}; + +var loadedAssets: bool = false; + +pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, itemPalette: *Palette, toolPalette: *Palette, biomePalette: *Palette) !void { // MARK: loadWorldAssets() + if(loadedAssets) return; // The assets already got loaded by the server. + loadedAssets = true; + + main.Tag.initTags(); + + const worldArena = main.stackAllocator.createArena(); + defer main.stackAllocator.destroyArena(worldArena); + + var worldAssets = common.clone(worldArena); + worldAssets.read(worldArena, main.files.cubyzDir(), assetFolder); + + errdefer unloadAssets(); + + migrations_zig.registerAll(.block, &worldAssets.blockMigrations); + migrations_zig.apply(.block, blockPalette); + + migrations_zig.registerAll(.item, &worldAssets.itemMigrations); + migrations_zig.apply(.item, itemPalette); + + migrations_zig.registerAll(.biome, &worldAssets.biomeMigrations); + migrations_zig.apply(.biome, biomePalette); + + // models: + var modelIterator = worldAssets.models.iterator(); + while(modelIterator.next()) |entry| { + _ = main.models.registerModel(entry.key_ptr.*, entry.value_ptr.*); + } + + if(!main.settings.launchConfig.headlessServer) blocks_zig.meshes.registerBlockBreakingAnimation(assetFolder); + + // Blocks: + // First blocks from the palette to enforce ID values. + for(blockPalette.palette.items) |stringId| { + try registerBlock(assetFolder, stringId, worldAssets.blocks.get(stringId) orelse .null); + } + + // Then all the blocks that were missing in palette but are present in the game. + var iterator = worldAssets.blocks.iterator(); + while(iterator.next()) |entry| { + const stringId = entry.key_ptr.*; + const zon = entry.value_ptr.*; + + if(blocks_zig.hasRegistered(stringId)) continue; + + try registerBlock(assetFolder, stringId, zon); + blockPalette.add(stringId); + } + + // Items: + // First from the palette to enforce ID values. + for(itemPalette.palette.items) |stringId| { + // Some items are created automatically from blocks. + if(worldAssets.blocks.get(stringId)) |zon| { + if(!zon.get(bool, "hasItem", true)) continue; + try registerItem(assetFolder, stringId, zon.getChild("item")); + if(worldAssets.items.get(stringId) != null) { + std.log.err("Item {s} appears as standalone item and as block item.", .{stringId}); + } + continue; + } + // Items not related to blocks should appear in items hash map. + if(worldAssets.items.get(stringId)) |zon| { + try registerItem(assetFolder, stringId, zon); + continue; + } + std.log.err("Missing item: {s}. Replacing it with default item.", .{stringId}); + try registerItem(assetFolder, stringId, .null); + } + + // Then missing block-items to keep backwards compatibility of ID order. + for(blockPalette.palette.items) |stringId| { + const zon = worldAssets.blocks.get(stringId) orelse .null; + + if(!zon.get(bool, "hasItem", true)) continue; + if(items_zig.hasRegistered(stringId)) continue; + + try registerItem(assetFolder, stringId, zon.getChild("item")); + itemPalette.add(stringId); + } + + // And finally normal items. + iterator = worldAssets.items.iterator(); + while(iterator.next()) |entry| { + const stringId = entry.key_ptr.*; + const zon = entry.value_ptr.*; + + if(items_zig.hasRegistered(stringId)) continue; + std.debug.assert(zon != .null); + + try registerItem(assetFolder, stringId, zon); + itemPalette.add(stringId); + } + + // After we have registered all items and all blocks, we can assign block references to those that come from blocks. + for(blockPalette.palette.items) |stringId| { + const zon = worldAssets.blocks.get(stringId) orelse .null; + + if(!zon.get(bool, "hasItem", true)) continue; + std.debug.assert(items_zig.hasRegistered(stringId)); + + try assignBlockItem(stringId); + } + + for(toolPalette.palette.items) |id| { + registerTool(assetFolder, id, worldAssets.tools.get(id) orelse .null); + } + + // tools: + iterator = worldAssets.tools.iterator(); + while(iterator.next()) |entry| { + const id = entry.key_ptr.*; + if(items_zig.hasRegisteredTool(id)) continue; + registerTool(assetFolder, id, entry.value_ptr.*); + toolPalette.add(id); + } + + // block drops: + blocks_zig.finishBlocks(worldAssets.blocks); + + iterator = worldAssets.recipes.iterator(); + while(iterator.next()) |entry| { + registerRecipesFromZon(entry.value_ptr.*); + } + + try sbb.registerBlueprints(&worldAssets.blueprints); + try sbb.registerSBB(&worldAssets.structureBuildingBlocks); + + iterator = worldAssets.particles.iterator(); + while(iterator.next()) |entry| { + particles_zig.ParticleManager.register(assetFolder, entry.key_ptr.*, entry.value_ptr.*); + } + + // Biomes: + var nextBiomeNumericId: u32 = 0; + for(biomePalette.palette.items) |id| { + registerBiome(nextBiomeNumericId, id, worldAssets.biomes.get(id) orelse .null); + nextBiomeNumericId += 1; + } + iterator = worldAssets.biomes.iterator(); + while(iterator.next()) |entry| { + if(biomes_zig.hasRegistered(entry.key_ptr.*)) continue; + registerBiome(nextBiomeNumericId, entry.key_ptr.*, entry.value_ptr.*); + biomePalette.add(entry.key_ptr.*); + nextBiomeNumericId += 1; + } + biomes_zig.finishLoading(); + + // Register paths for asset hot reloading: + var dir = main.files.cwd().openIterableDir("assets") catch |err| { + std.log.err("Can't open asset path {s}: {s}", .{"assets", @errorName(err)}); + return; + }; + defer dir.close(); + var dirIterator = dir.iterate(); + while(dirIterator.next() catch |err| blk: { + std.log.err("Got error while iterating over asset path {s}: {s}", .{"assets", @errorName(err)}); + break :blk null; + }) |addon| { + if(addon.kind == .directory) { + const path = std.fmt.allocPrintSentinel(main.stackAllocator.allocator, "assets/{s}/blocks/textures", .{addon.name}, 0) catch unreachable; + defer main.stackAllocator.free(path); + // Check for access rights + if(!main.files.cwd().hasDir(path)) continue; + main.utils.file_monitor.listenToPath(path, main.blocks.meshes.reloadTextures, 0); + } + } + + worldAssets.log(.world); +} + +pub fn unloadAssets() void { // MARK: unloadAssets() + if(!loadedAssets) return; + loadedAssets = false; + + sbb.reset(); + blocks_zig.reset(); + items_zig.reset(); + migrations_zig.reset(); + biomes_zig.reset(); + migrations_zig.reset(); + main.models.reset(); + main.particles.ParticleManager.reset(); + main.rotation.reset(); + main.Tag.resetTags(); + + // Remove paths from asset hot reloading: + var dir = main.files.cwd().openIterableDir("assets") catch |err| { + std.log.err("Can't open asset path {s}: {s}", .{"assets", @errorName(err)}); + return; + }; + defer dir.close(); + var dirIterator = dir.iterate(); + while(dirIterator.next() catch |err| blk: { + std.log.err("Got error while iterating over asset path {s}: {s}", .{"assets", @errorName(err)}); + break :blk null; + }) |addon| { + if(addon.kind == .directory) { + const path = std.fmt.allocPrintSentinel(main.stackAllocator.allocator, "assets/{s}/blocks/textures", .{addon.name}, 0) catch unreachable; + defer main.stackAllocator.free(path); + // Check for access rights + if(!main.files.cwd().hasDir(path)) continue; + main.utils.file_monitor.removePath(path); + } + } +} diff --git a/audio.zig b/audio.zig new file mode 100644 index 0000000000..37bc32b1c5 --- /dev/null +++ b/audio.zig @@ -0,0 +1,318 @@ +const std = @import("std"); + +const main = @import("main"); +const utils = main.utils; + +const c = @cImport({ + @cDefine("_BITS_STDIO2_H", ""); // TODO: Zig fails to include this header file + @cInclude("miniaudio.h"); + @cDefine("STB_VORBIS_HEADER_ONLY", ""); + @cInclude("stb/stb_vorbis.h"); +}); + +fn handleError(miniaudioError: c.ma_result) !void { + if(miniaudioError != c.MA_SUCCESS) { + std.log.err("miniaudio error: {s}", .{c.ma_result_description(miniaudioError)}); + return error.miniaudioError; + } +} + +const AudioData = struct { + musicId: []const u8, + data: []f32 = &.{}, + + fn open_vorbis_file_by_id(id: []const u8) ?*c.stb_vorbis { + const colonIndex = std.mem.indexOfScalar(u8, id, ':') orelse { + std.log.err("Invalid music id: {s}. Must be addon:file_name", .{id}); + return null; + }; + const addon = id[0..colonIndex]; + const fileName = id[colonIndex + 1 ..]; + const path1 = std.fmt.allocPrintSentinel(main.stackAllocator.allocator, "assets/{s}/music/{s}.ogg", .{addon, fileName}, 0) catch unreachable; + defer main.stackAllocator.free(path1); + var err: c_int = 0; + if(c.stb_vorbis_open_filename(path1.ptr, &err, null)) |ogg_stream| return ogg_stream; + const path2 = std.fmt.allocPrintSentinel(main.stackAllocator.allocator, "{s}/serverAssets/{s}/music/{s}.ogg", .{main.files.cubyzDirStr(), addon, fileName}, 0) catch unreachable; + defer main.stackAllocator.free(path2); + if(c.stb_vorbis_open_filename(path2.ptr, &err, null)) |ogg_stream| return ogg_stream; + std.log.err("Couldn't find music with id \"{s}\". Searched path \"{s}\" and \"{s}\"", .{id, path1, path2}); + return null; + } + + fn init(musicId: []const u8) *AudioData { + const self = main.globalAllocator.create(AudioData); + self.* = .{.musicId = main.globalAllocator.dupe(u8, musicId)}; + + const channels = 2; + if(open_vorbis_file_by_id(musicId)) |ogg_stream| { + defer c.stb_vorbis_close(ogg_stream); + const ogg_info: c.stb_vorbis_info = c.stb_vorbis_get_info(ogg_stream); + const samples = c.stb_vorbis_stream_length_in_samples(ogg_stream); + if(sampleRate != @as(f32, @floatFromInt(ogg_info.sample_rate))) { + const tempData = main.stackAllocator.alloc(f32, samples*channels); + defer main.stackAllocator.free(tempData); + _ = c.stb_vorbis_get_samples_float_interleaved(ogg_stream, channels, tempData.ptr, @as(c_int, @intCast(samples))*ogg_info.channels); + var stepWidth = @as(f32, @floatFromInt(ogg_info.sample_rate))/sampleRate; + const newSamples: usize = @intFromFloat(@as(f32, @floatFromInt(tempData.len/2))/stepWidth); + stepWidth = @as(f32, @floatFromInt(samples))/@as(f32, @floatFromInt(newSamples)); + self.data = main.globalAllocator.alloc(f32, newSamples*channels); + for(0..newSamples) |s| { + const samplePosition = @as(f32, @floatFromInt(s))*stepWidth; + const firstSample: usize = @intFromFloat(@floor(samplePosition)); + const interpolation = samplePosition - @floor(samplePosition); + for(0..channels) |ch| { + if(firstSample >= samples - 1) { + self.data[s*channels + ch] = tempData[(samples - 1)*channels + ch]; + } else { + self.data[s*channels + ch] = tempData[firstSample*channels + ch]*(1 - interpolation) + tempData[(firstSample + 1)*channels + ch]*interpolation; + } + } + } + } else { + self.data = main.globalAllocator.alloc(f32, samples*channels); + _ = c.stb_vorbis_get_samples_float_interleaved(ogg_stream, channels, self.data.ptr, @as(c_int, @intCast(samples))*ogg_info.channels); + } + } else { + self.data = main.globalAllocator.alloc(f32, channels); + @memset(self.data, 0); + } + return self; + } + + fn deinit(self: *const AudioData) void { + main.globalAllocator.free(self.data); + main.globalAllocator.free(self.musicId); + main.globalAllocator.destroy(self); + } + + pub fn hashCode(self: *const AudioData) u32 { + var result: u32 = 0; + for(self.musicId) |char| { + result = result + char; + } + return result; + } + + pub fn equals(self: *const AudioData, _other: ?*const AudioData) bool { + if(_other) |other| { + return std.mem.eql(u8, self.musicId, other.musicId); + } else return false; + } +}; + +var activeTasks: main.ListUnmanaged([]const u8) = .{}; +var taskMutex: std.Thread.Mutex = .{}; + +var musicCache: utils.Cache(AudioData, 4, 4, AudioData.deinit) = .{}; + +fn findMusic(musicId: []const u8) ?[]f32 { + { + taskMutex.lock(); + defer taskMutex.unlock(); + if(musicCache.find(AudioData{.musicId = musicId}, null)) |musicData| { + return musicData.data; + } + for(activeTasks.items) |taskFileName| { + if(std.mem.eql(u8, musicId, taskFileName)) { + return null; + } + } + } + MusicLoadTask.schedule(musicId); + return null; +} + +const MusicLoadTask = struct { + musicId: []const u8, + + const vtable = utils.ThreadPool.VTable{ + .getPriority = main.meta.castFunctionSelfToAnyopaque(getPriority), + .isStillNeeded = main.meta.castFunctionSelfToAnyopaque(isStillNeeded), + .run = main.meta.castFunctionSelfToAnyopaque(run), + .clean = main.meta.castFunctionSelfToAnyopaque(clean), + .taskType = .misc, + }; + + pub fn schedule(musicId: []const u8) void { + const task = main.globalAllocator.create(MusicLoadTask); + task.* = MusicLoadTask{ + .musicId = main.globalAllocator.dupe(u8, musicId), + }; + main.threadPool.addTask(task, &vtable); + taskMutex.lock(); + defer taskMutex.unlock(); + activeTasks.append(main.globalAllocator, task.musicId); + } + + pub fn getPriority(_: *MusicLoadTask) f32 { + return std.math.floatMax(f32); + } + + pub fn isStillNeeded(_: *MusicLoadTask) bool { + return true; + } + + pub fn run(self: *MusicLoadTask) void { + defer self.clean(); + const data = AudioData.init(self.musicId); + const hasOld = musicCache.addToCache(data, data.hashCode()); + if(hasOld) |old| { + old.deinit(); + } + } + + pub fn clean(self: *MusicLoadTask) void { + taskMutex.lock(); + var index: usize = 0; + while(index < activeTasks.items.len) : (index += 1) { + if(activeTasks.items[index].ptr == self.musicId.ptr) break; + } + _ = activeTasks.swapRemove(index); + taskMutex.unlock(); + main.globalAllocator.free(self.musicId); + main.globalAllocator.destroy(self); + } +}; + +// TODO: Proper sound and music system + +var device: c.ma_device = undefined; + +var sampleRate: f32 = 0; + +pub fn init() error{miniaudioError}!void { + var config = c.ma_device_config_init(c.ma_device_type_playback); + config.playback.format = c.ma_format_f32; + config.playback.channels = 2; + config.sampleRate = 44100; + config.dataCallback = &miniaudioCallback; + config.pUserData = undefined; + + try handleError(c.ma_device_init(null, &config, &device)); + + try handleError(c.ma_device_start(&device)); + + sampleRate = 44100; +} + +pub fn deinit() void { + handleError(c.ma_device_stop(&device)) catch {}; + c.ma_device_uninit(&device); + mutex.lock(); + defer mutex.unlock(); + main.threadPool.closeAllTasksOfType(&MusicLoadTask.vtable); + musicCache.clear(); + activeTasks.deinit(main.globalAllocator); + main.globalAllocator.free(preferredMusic); + preferredMusic.len = 0; + main.globalAllocator.free(activeMusicId); + activeMusicId.len = 0; +} + +const currentMusic = struct { + var buffer: []const f32 = undefined; + var animationAmplitude: f32 = undefined; + var animationVelocity: f32 = undefined; + var animationDecaying: bool = undefined; + var animationProgress: f32 = undefined; + var interpolationPolynomial: [4]f32 = undefined; + var pos: u32 = undefined; + + fn init(musicBuffer: []const f32) void { + buffer = musicBuffer; + animationAmplitude = 0; + animationVelocity = 0; + animationDecaying = false; + animationProgress = 0; + interpolationPolynomial = utils.unitIntervalSpline(f32, animationAmplitude, animationVelocity, 1, 0); + pos = 0; + } + + fn evaluatePolynomial() void { + const t = animationProgress; + const t2 = t*t; + const t3 = t2*t; + const a = interpolationPolynomial; + animationAmplitude = a[0] + a[1]*t + a[2]*t2 + a[3]*t3; // value + animationVelocity = a[1] + 2*a[2]*t + 3*a[3]*t2; + } +}; + +var activeMusicId: []const u8 = &.{}; +var partialFrame: f32 = 0; +const animationLengthInSeconds = 5.0; + +var curIndex: u16 = 0; +var curEndIndex: std.atomic.Value(u16) = .{.value = sampleRate/60 & ~@as(u16, 1)}; + +var mutex: std.Thread.Mutex = .{}; +var preferredMusic: []const u8 = ""; + +pub fn setMusic(music: []const u8) void { + mutex.lock(); + defer mutex.unlock(); + if(std.mem.eql(u8, music, preferredMusic)) return; + main.globalAllocator.free(preferredMusic); + preferredMusic = main.globalAllocator.dupe(u8, music); +} + +fn addMusic(buffer: []f32) void { + mutex.lock(); + defer mutex.unlock(); + if(!std.mem.eql(u8, preferredMusic, activeMusicId)) { + if(activeMusicId.len == 0) { + if(findMusic(preferredMusic)) |musicBuffer| { + currentMusic.init(musicBuffer); + main.globalAllocator.free(activeMusicId); + activeMusicId = main.globalAllocator.dupe(u8, preferredMusic); + } + } else if(!currentMusic.animationDecaying) { + _ = findMusic(preferredMusic); // Start loading the next music into the cache ahead of time. + currentMusic.animationDecaying = true; + currentMusic.animationProgress = 0; + currentMusic.interpolationPolynomial = utils.unitIntervalSpline(f32, currentMusic.animationAmplitude, currentMusic.animationVelocity, 0, 0); + } + } else if(currentMusic.animationDecaying) { // We returned to the biome before the music faded away. + currentMusic.animationDecaying = false; + currentMusic.animationProgress = 0; + currentMusic.interpolationPolynomial = utils.unitIntervalSpline(f32, currentMusic.animationAmplitude, currentMusic.animationVelocity, 1, 0); + } + if(activeMusicId.len == 0) return; + + // Copy the music to the buffer. + var i: usize = 0; + while(i < buffer.len) : (i += 2) { + currentMusic.animationProgress += 1.0/(animationLengthInSeconds*sampleRate); + var amplitude: f32 = main.settings.musicVolume; + if(currentMusic.animationProgress > 1) { + if(currentMusic.animationDecaying) { + main.globalAllocator.free(activeMusicId); + activeMusicId = &.{}; + amplitude = 0; + } + } else { + currentMusic.evaluatePolynomial(); + amplitude *= currentMusic.animationAmplitude; + } + buffer[i] += amplitude*currentMusic.buffer[currentMusic.pos]; + buffer[i + 1] += amplitude*currentMusic.buffer[currentMusic.pos + 1]; + currentMusic.pos += 2; + if(currentMusic.pos >= currentMusic.buffer.len) { + currentMusic.pos = 0; + } + } +} + +fn miniaudioCallback( + maDevice: ?*anyopaque, + output: ?*anyopaque, + input: ?*const anyopaque, + frameCount: u32, +) callconv(.c) void { + _ = input; + _ = maDevice; + const valuesPerBuffer = 2*frameCount; // Stereo + const buffer = @as([*]f32, @ptrCast(@alignCast(output)))[0..valuesPerBuffer]; + @memset(buffer, 0); + addMusic(buffer); +} diff --git a/block_entity.zig b/block_entity.zig new file mode 100644 index 0000000000..e86ca0c2d6 --- /dev/null +++ b/block_entity.zig @@ -0,0 +1,575 @@ +const std = @import("std"); + +const main = @import("main.zig"); +const Block = main.blocks.Block; +const Chunk = main.chunk.Chunk; +const ChunkPosition = main.chunk.ChunkPosition; +const getIndex = main.chunk.getIndex; +const graphics = main.graphics; +const c = graphics.c; +const server = main.server; +const User = server.User; +const mesh_storage = main.renderer.mesh_storage; +const BinaryReader = main.utils.BinaryReader; +const BinaryWriter = main.utils.BinaryWriter; +const vec = main.vec; +const Mat4f = vec.Mat4f; +const Vec3d = vec.Vec3d; +const Vec3f = vec.Vec3f; +const Vec3i = vec.Vec3i; + +pub const BlockEntityIndex = main.utils.DenseId(u32); + +const UpdateEvent = union(enum) { + remove: void, + update: *BinaryReader, +}; + +pub const ErrorSet = BinaryReader.AllErrors || error{Invalid}; + +pub const BlockEntityType = struct { + id: []const u8, + vtable: VTable, + + const VTable = struct { + onLoadClient: *const fn(pos: Vec3i, chunk: *Chunk, reader: *BinaryReader) ErrorSet!void, + onUnloadClient: *const fn(dataIndex: BlockEntityIndex) void, + onLoadServer: *const fn(pos: Vec3i, chunk: *Chunk, reader: *BinaryReader) ErrorSet!void, + onUnloadServer: *const fn(dataIndex: BlockEntityIndex) void, + onStoreServerToDisk: *const fn(dataIndex: BlockEntityIndex, writer: *BinaryWriter) void, + onStoreServerToClient: *const fn(dataIndex: BlockEntityIndex, writer: *BinaryWriter) void, + onInteract: *const fn(pos: Vec3i, chunk: *Chunk) main.callbacks.Result, + updateClientData: *const fn(pos: Vec3i, chunk: *Chunk, event: UpdateEvent) ErrorSet!void, + updateServerData: *const fn(pos: Vec3i, chunk: *Chunk, event: UpdateEvent) ErrorSet!void, + getServerToClientData: *const fn(pos: Vec3i, chunk: *Chunk, writer: *BinaryWriter) void, + getClientToServerData: *const fn(pos: Vec3i, chunk: *Chunk, writer: *BinaryWriter) void, + }; + pub fn init(comptime BlockEntityTypeT: type) BlockEntityType { + BlockEntityTypeT.init(); + var class = BlockEntityType{ + .id = BlockEntityTypeT.id, + .vtable = undefined, + }; + + inline for(@typeInfo(BlockEntityType.VTable).@"struct".fields) |field| { + if(!@hasDecl(BlockEntityTypeT, field.name)) { + @compileError("BlockEntityType missing field '" ++ field.name ++ "'"); + } + @field(class.vtable, field.name) = &@field(BlockEntityTypeT, field.name); + } + return class; + } + pub inline fn onLoadClient(self: *const BlockEntityType, pos: Vec3i, chunk: *Chunk, reader: *BinaryReader) ErrorSet!void { + return self.vtable.onLoadClient(pos, chunk, reader); + } + pub inline fn onUnloadClient(self: *const BlockEntityType, dataIndex: BlockEntityIndex) void { + return self.vtable.onUnloadClient(dataIndex); + } + pub inline fn onLoadServer(self: *const BlockEntityType, pos: Vec3i, chunk: *Chunk, reader: *BinaryReader) ErrorSet!void { + return self.vtable.onLoadServer(pos, chunk, reader); + } + pub inline fn onUnloadServer(self: *const BlockEntityType, dataIndex: BlockEntityIndex) void { + return self.vtable.onUnloadServer(dataIndex); + } + pub inline fn onStoreServerToDisk(self: *const BlockEntityType, dataIndex: BlockEntityIndex, writer: *BinaryWriter) void { + return self.vtable.onStoreServerToDisk(dataIndex, writer); + } + pub inline fn onStoreServerToClient(self: *const BlockEntityType, dataIndex: BlockEntityIndex, writer: *BinaryWriter) void { + return self.vtable.onStoreServerToClient(dataIndex, writer); + } + pub inline fn onInteract(self: *const BlockEntityType, pos: Vec3i, chunk: *Chunk) main.callbacks.Result { + return self.vtable.onInteract(pos, chunk); + } + pub inline fn updateClientData(self: *const BlockEntityType, pos: Vec3i, chunk: *Chunk, event: UpdateEvent) ErrorSet!void { + return try self.vtable.updateClientData(pos, chunk, event); + } + pub inline fn updateServerData(self: *const BlockEntityType, pos: Vec3i, chunk: *Chunk, event: UpdateEvent) ErrorSet!void { + return try self.vtable.updateServerData(pos, chunk, event); + } + pub inline fn getServerToClientData(self: *const BlockEntityType, pos: Vec3i, chunk: *Chunk, writer: *BinaryWriter) void { + return self.vtable.getServerToClientData(pos, chunk, writer); + } + pub inline fn getClientToServerData(self: *const BlockEntityType, pos: Vec3i, chunk: *Chunk, writer: *BinaryWriter) void { + return self.vtable.getClientToServerData(pos, chunk, writer); + } +}; + +fn BlockEntityDataStorage(T: type) type { + return struct { + pub const DataT = T; + var freeIndexList: main.ListUnmanaged(BlockEntityIndex) = .{}; + var nextIndex: BlockEntityIndex = @enumFromInt(0); + var storage: main.utils.SparseSet(DataT, BlockEntityIndex) = .{}; + pub var mutex: std.Thread.Mutex = .{}; + + pub fn init() void { + storage = .{}; + freeIndexList = .{}; + } + pub fn deinit() void { + storage.deinit(main.globalAllocator); + freeIndexList.deinit(main.globalAllocator); + nextIndex = @enumFromInt(0); + } + pub fn reset() void { + storage.clear(); + freeIndexList.clearRetainingCapacity(); + } + fn createEntry(pos: Vec3i, chunk: *Chunk) BlockEntityIndex { + main.utils.assertLocked(&mutex); + const dataIndex: BlockEntityIndex = freeIndexList.popOrNull() orelse blk: { + defer nextIndex = @enumFromInt(@intFromEnum(nextIndex) + 1); + break :blk nextIndex; + }; + const localPos = chunk.getLocalBlockPos(pos); + + chunk.blockPosToEntityDataMapMutex.lock(); + chunk.blockPosToEntityDataMap.put(main.globalAllocator.allocator, localPos, dataIndex) catch unreachable; + chunk.blockPosToEntityDataMapMutex.unlock(); + return dataIndex; + } + pub fn add(pos: Vec3i, value: DataT, chunk: *Chunk) void { + mutex.lock(); + defer mutex.unlock(); + + const dataIndex = createEntry(pos, chunk); + storage.set(main.globalAllocator, dataIndex, value); + } + pub fn removeAtIndex(dataIndex: BlockEntityIndex) ?DataT { + main.utils.assertLocked(&mutex); + freeIndexList.append(main.globalAllocator, dataIndex); + return storage.fetchRemove(dataIndex) catch null; + } + pub fn remove(pos: Vec3i, chunk: *Chunk) ?DataT { + mutex.lock(); + defer mutex.unlock(); + + const localPos = chunk.getLocalBlockPos(pos); + + chunk.blockPosToEntityDataMapMutex.lock(); + const entityNullable = chunk.blockPosToEntityDataMap.fetchRemove(localPos); + chunk.blockPosToEntityDataMapMutex.unlock(); + + const entry = entityNullable orelse return null; + + const dataIndex = entry.value; + return removeAtIndex(dataIndex); + } + pub fn getByIndex(dataIndex: BlockEntityIndex) ?*DataT { + main.utils.assertLocked(&mutex); + + return storage.get(dataIndex); + } + pub fn get(pos: Vec3i, chunk: *Chunk) ?*DataT { + main.utils.assertLocked(&mutex); + + const localPos = chunk.getLocalBlockPos(pos); + + chunk.blockPosToEntityDataMapMutex.lock(); + defer chunk.blockPosToEntityDataMapMutex.unlock(); + + const dataIndex = chunk.blockPosToEntityDataMap.get(localPos) orelse return null; + return storage.get(dataIndex); + } + pub const GetOrPutResult = struct { + valuePtr: *DataT, + foundExisting: bool, + }; + pub fn getOrPut(pos: Vec3i, chunk: *Chunk) GetOrPutResult { + main.utils.assertLocked(&mutex); + if(get(pos, chunk)) |result| return .{.valuePtr = result, .foundExisting = true}; + + const dataIndex = createEntry(pos, chunk); + return .{.valuePtr = storage.add(main.globalAllocator, dataIndex), .foundExisting = false}; + } + }; +} + +pub const BlockEntityTypes = struct { + pub const Chest = struct { + const inventorySize = 20; + const StorageServer = BlockEntityDataStorage(struct { + invId: main.items.Inventory.InventoryId, + }); + + pub const id = "chest"; + pub fn init() void { + StorageServer.init(); + } + pub fn deinit() void { + StorageServer.deinit(); + } + pub fn reset() void { + StorageServer.reset(); + } + + fn onInventoryUpdateCallback(source: main.items.Inventory.Source) void { + const pos = source.blockInventory; + const simChunk = main.server.world.?.getSimulationChunkAndIncreaseRefCount(pos[0], pos[1], pos[2]) orelse return; + defer simChunk.decreaseRefCount(); + const ch = simChunk.getChunk() orelse return; + ch.mutex.lock(); + defer ch.mutex.unlock(); + ch.setChanged(); + } + + const inventoryCallbacks = main.items.Inventory.Callbacks{ + .onUpdateCallback = &onInventoryUpdateCallback, + }; + + pub fn onLoadClient(_: Vec3i, _: *Chunk, _: *BinaryReader) ErrorSet!void {} + pub fn onUnloadClient(_: BlockEntityIndex) void {} + pub fn onLoadServer(pos: Vec3i, chunk: *Chunk, reader: *BinaryReader) ErrorSet!void { + StorageServer.mutex.lock(); + defer StorageServer.mutex.unlock(); + + const data = StorageServer.getOrPut(pos, chunk); + std.debug.assert(!data.foundExisting); + data.valuePtr.invId = main.items.Inventory.Sync.ServerSide.createExternallyManagedInventory(inventorySize, .normal, .{.blockInventory = pos}, reader, inventoryCallbacks); + } + + pub fn onUnloadServer(dataIndex: BlockEntityIndex) void { + StorageServer.mutex.lock(); + const data = StorageServer.removeAtIndex(dataIndex) orelse unreachable; + StorageServer.mutex.unlock(); + main.items.Inventory.Sync.ServerSide.destroyExternallyManagedInventory(data.invId); + } + pub fn onStoreServerToDisk(dataIndex: BlockEntityIndex, writer: *BinaryWriter) void { + StorageServer.mutex.lock(); + defer StorageServer.mutex.unlock(); + const data = StorageServer.getByIndex(dataIndex) orelse return; + + const inv = main.items.Inventory.Sync.ServerSide.getInventoryFromId(data.invId); + var isEmpty: bool = true; + for(inv._items) |item| { + if(item.amount != 0) isEmpty = false; + } + if(isEmpty) return; + inv.toBytes(writer); + } + pub fn onStoreServerToClient(_: BlockEntityIndex, _: *BinaryWriter) void {} + pub fn onInteract(pos: Vec3i, _: *Chunk) main.callbacks.Result { + main.network.protocols.blockEntityUpdate.sendClientDataUpdateToServer(main.game.world.?.conn, pos); + + const inventory = main.items.Inventory.init(main.globalAllocator, inventorySize, .normal, .{.blockInventory = pos}, .{}); + + main.gui.windowlist.chest.setInventory(inventory); + main.gui.openWindow("chest"); + main.Window.setMouseGrabbed(false); + + return .handled; + } + + pub fn updateClientData(_: Vec3i, _: *Chunk, _: UpdateEvent) ErrorSet!void {} + pub fn updateServerData(pos: Vec3i, chunk: *Chunk, event: UpdateEvent) ErrorSet!void { + switch(event) { + .remove => { + const chest = StorageServer.remove(pos, chunk) orelse return; + main.items.Inventory.Sync.ServerSide.destroyAndDropExternallyManagedInventory(chest.invId, pos); + }, + .update => |_| { + StorageServer.mutex.lock(); + defer StorageServer.mutex.unlock(); + const data = StorageServer.getOrPut(pos, chunk); + if(data.foundExisting) return; + var reader = BinaryReader.init(&.{}); + data.valuePtr.invId = main.items.Inventory.Sync.ServerSide.createExternallyManagedInventory(inventorySize, .normal, .{.blockInventory = pos}, &reader, inventoryCallbacks); + }, + } + } + pub fn getServerToClientData(_: Vec3i, _: *Chunk, _: *BinaryWriter) void {} + pub fn getClientToServerData(_: Vec3i, _: *Chunk, _: *BinaryWriter) void {} + + pub fn renderAll(_: Mat4f, _: Vec3f, _: Vec3d) void {} + }; + + pub const Sign = struct { + const StorageServer = BlockEntityDataStorage(struct { + text: []const u8, + }); + const StorageClient = BlockEntityDataStorage(struct { + text: []const u8, + renderedTexture: ?main.graphics.Texture = null, + blockPos: Vec3i, + block: main.blocks.Block, + + fn deinit(self: @This()) void { + main.globalAllocator.free(self.text); + if(self.renderedTexture) |texture| { + textureDeinitLock.lock(); + defer textureDeinitLock.unlock(); + textureDeinitList.append(texture); + } + } + }); + var textureDeinitList: main.List(graphics.Texture) = undefined; + var textureDeinitLock: std.Thread.Mutex = .{}; + var pipeline: graphics.Pipeline = undefined; + var uniforms: struct { + ambientLight: c_int, + projectionMatrix: c_int, + viewMatrix: c_int, + playerPositionInteger: c_int, + playerPositionFraction: c_int, + quadIndex: c_int, + lightData: c_int, + chunkPos: c_int, + blockPos: c_int, + } = undefined; + + // TODO: Load these from some per-block settings + const textureWidth = 128; + const textureHeight = 72; + const textureMargin = 4; + + pub const id = "sign"; + pub fn init() void { + StorageServer.init(); + StorageClient.init(); + textureDeinitList = .init(main.globalAllocator); + if(!main.settings.launchConfig.headlessServer) { + pipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/block_entity/sign.vert", + "assets/cubyz/shaders/block_entity/sign.frag", + "", + &uniforms, + .{}, + .{.depthTest = true, .depthCompare = .equal, .depthWrite = false}, + .{.attachments = &.{.alphaBlending}}, + ); + } + } + pub fn deinit() void { + while(textureDeinitList.popOrNull()) |texture| { + texture.deinit(); + } + textureDeinitList.deinit(); + pipeline.deinit(); + StorageServer.deinit(); + StorageClient.deinit(); + } + pub fn reset() void { + StorageServer.reset(); + StorageClient.reset(); + } + + pub fn onUnloadClient(dataIndex: BlockEntityIndex) void { + StorageClient.mutex.lock(); + defer StorageClient.mutex.unlock(); + const entry = StorageClient.removeAtIndex(dataIndex) orelse unreachable; + entry.deinit(); + } + pub fn onUnloadServer(dataIndex: BlockEntityIndex) void { + StorageServer.mutex.lock(); + defer StorageServer.mutex.unlock(); + const entry = StorageServer.removeAtIndex(dataIndex) orelse unreachable; + main.globalAllocator.free(entry.text); + } + pub fn onInteract(pos: Vec3i, chunk: *Chunk) main.callbacks.Result { + StorageClient.mutex.lock(); + defer StorageClient.mutex.unlock(); + const data = StorageClient.get(pos, chunk); + main.gui.windowlist.sign_editor.openFromSignData(pos, if(data) |_data| _data.text else ""); + + return .handled; + } + + pub fn onLoadClient(pos: Vec3i, chunk: *Chunk, reader: *BinaryReader) ErrorSet!void { + return updateClientData(pos, chunk, .{.update = reader}); + } + pub fn updateClientData(pos: Vec3i, chunk: *Chunk, event: UpdateEvent) ErrorSet!void { + if(event == .remove or event.update.remaining.len == 0) { + const entry = StorageClient.remove(pos, chunk) orelse return; + entry.deinit(); + return; + } + + StorageClient.mutex.lock(); + defer StorageClient.mutex.unlock(); + + const data = StorageClient.getOrPut(pos, chunk); + if(data.foundExisting) { + data.valuePtr.deinit(); + } + data.valuePtr.* = .{ + .blockPos = pos, + .block = chunk.data.getValue(chunk.getLocalBlockPos(pos).toIndex()), + .renderedTexture = null, + .text = main.globalAllocator.dupe(u8, event.update.remaining), + }; + } + + pub fn onLoadServer(pos: Vec3i, chunk: *Chunk, reader: *BinaryReader) ErrorSet!void { + return updateServerData(pos, chunk, .{.update = reader}); + } + pub fn updateServerData(pos: Vec3i, chunk: *Chunk, event: UpdateEvent) ErrorSet!void { + if(event == .remove or event.update.remaining.len == 0) { + const entry = StorageServer.remove(pos, chunk) orelse return; + main.globalAllocator.free(entry.text); + return; + } + + StorageServer.mutex.lock(); + defer StorageServer.mutex.unlock(); + + const newText = event.update.remaining; + + if(!std.unicode.utf8ValidateSlice(newText)) { + std.log.err("Received sign text with invalid UTF-8 characters.", .{}); + return error.Invalid; + } + + const data = StorageServer.getOrPut(pos, chunk); + if(data.foundExisting) main.globalAllocator.free(data.valuePtr.text); + data.valuePtr.text = main.globalAllocator.dupe(u8, event.update.remaining); + } + + pub const onStoreServerToClient = onStoreServerToDisk; + pub fn onStoreServerToDisk(dataIndex: BlockEntityIndex, writer: *BinaryWriter) void { + StorageServer.mutex.lock(); + defer StorageServer.mutex.unlock(); + + const data = StorageServer.getByIndex(dataIndex) orelse return; + writer.writeSlice(data.text); + } + pub fn getServerToClientData(pos: Vec3i, chunk: *Chunk, writer: *BinaryWriter) void { + StorageServer.mutex.lock(); + defer StorageServer.mutex.unlock(); + + const data = StorageServer.get(pos, chunk) orelse return; + writer.writeSlice(data.text); + } + + pub fn getClientToServerData(pos: Vec3i, chunk: *Chunk, writer: *BinaryWriter) void { + StorageClient.mutex.lock(); + defer StorageClient.mutex.unlock(); + + const data = StorageClient.get(pos, chunk) orelse return; + writer.writeSlice(data.text); + } + + pub fn updateTextFromClient(pos: Vec3i, newText: []const u8) void { + { + const mesh = main.renderer.mesh_storage.getMesh(.initFromWorldPos(pos, 1)) orelse return; + mesh.mutex.lock(); + defer mesh.mutex.unlock(); + const localPos = mesh.chunk.getLocalBlockPos(pos); + const block = mesh.chunk.data.getValue(localPos.toIndex()); + const blockEntity = block.blockEntity() orelse return; + if(!std.mem.eql(u8, blockEntity.id, id)) return; + + StorageClient.mutex.lock(); + defer StorageClient.mutex.unlock(); + + const data = StorageClient.getOrPut(pos, mesh.chunk); + if(data.foundExisting) { + data.valuePtr.deinit(); + } + data.valuePtr.* = .{ + .blockPos = pos, + .block = mesh.chunk.data.getValue(localPos.toIndex()), + .renderedTexture = null, + .text = main.globalAllocator.dupe(u8, newText), + }; + } + + main.network.protocols.blockEntityUpdate.sendClientDataUpdateToServer(main.game.world.?.conn, pos); + } + + pub fn renderAll(projectionMatrix: Mat4f, ambientLight: Vec3f, playerPos: Vec3d) void { + var oldFramebufferBinding: c_int = undefined; + c.glGetIntegerv(c.GL_DRAW_FRAMEBUFFER_BINDING, &oldFramebufferBinding); + + StorageClient.mutex.lock(); + defer StorageClient.mutex.unlock(); + + for(StorageClient.storage.dense.items) |*signData| { + if(signData.renderedTexture != null) continue; + + c.glViewport(0, 0, textureWidth, textureHeight); + defer c.glViewport(0, 0, main.Window.width, main.Window.height); + + var finalFrameBuffer: graphics.FrameBuffer = undefined; + finalFrameBuffer.init(false, c.GL_NEAREST, c.GL_REPEAT); + finalFrameBuffer.updateSize(textureWidth, textureHeight, c.GL_RGBA8); + finalFrameBuffer.bind(); + finalFrameBuffer.clear(.{0, 0, 0, 0}); + signData.renderedTexture = .{.textureID = finalFrameBuffer.texture}; + defer c.glDeleteFramebuffers(1, &finalFrameBuffer.frameBuffer); + + const oldTranslation = graphics.draw.setTranslation(.{textureMargin, textureMargin}); + defer graphics.draw.restoreTranslation(oldTranslation); + const oldClip = graphics.draw.setClip(.{textureWidth - 2*textureMargin, textureHeight - 2*textureMargin}); + defer graphics.draw.restoreClip(oldClip); + + var textBuffer = graphics.TextBuffer.init(main.stackAllocator, signData.text, .{.color = 0x000000}, false, .center); // TODO: Make the color configurable in the zon + defer textBuffer.deinit(); + _ = textBuffer.calculateLineBreaks(16, textureWidth - 2*textureMargin); + textBuffer.renderTextWithoutShadow(0, 0, 16); + } + + c.glBindFramebuffer(c.GL_FRAMEBUFFER, @bitCast(oldFramebufferBinding)); + + pipeline.bind(null); + c.glBindVertexArray(main.renderer.chunk_meshing.vao); + + c.glUniform3f(uniforms.ambientLight, ambientLight[0], ambientLight[1], ambientLight[2]); + c.glUniformMatrix4fv(uniforms.projectionMatrix, 1, c.GL_TRUE, @ptrCast(&projectionMatrix)); + c.glUniformMatrix4fv(uniforms.viewMatrix, 1, c.GL_TRUE, @ptrCast(&main.game.camera.viewMatrix)); + c.glUniform3i(uniforms.playerPositionInteger, @intFromFloat(@floor(playerPos[0])), @intFromFloat(@floor(playerPos[1])), @intFromFloat(@floor(playerPos[2]))); + c.glUniform3f(uniforms.playerPositionFraction, @floatCast(@mod(playerPos[0], 1)), @floatCast(@mod(playerPos[1], 1)), @floatCast(@mod(playerPos[2], 1))); + + outer: for(StorageClient.storage.dense.items) |signData| { + if(main.blocks.meshes.model(signData.block).model().internalQuads.len == 0) continue; + const quad = main.blocks.meshes.model(signData.block).model().internalQuads[0]; + + signData.renderedTexture.?.bindTo(0); + + c.glUniform1i(uniforms.quadIndex, @intFromEnum(quad)); + const mesh = main.renderer.mesh_storage.getMesh(main.chunk.ChunkPosition.initFromWorldPos(signData.blockPos, 1)) orelse continue :outer; + const light: [4]u32 = main.renderer.chunk_meshing.PrimitiveMesh.getLight(mesh, signData.blockPos -% Vec3i{mesh.pos.wx, mesh.pos.wy, mesh.pos.wz}, 0, quad); + c.glUniform4ui(uniforms.lightData, light[0], light[1], light[2], light[3]); + c.glUniform3i(uniforms.chunkPos, signData.blockPos[0] & ~main.chunk.chunkMask, signData.blockPos[1] & ~main.chunk.chunkMask, signData.blockPos[2] & ~main.chunk.chunkMask); + c.glUniform3i(uniforms.blockPos, signData.blockPos[0] & main.chunk.chunkMask, signData.blockPos[1] & main.chunk.chunkMask, signData.blockPos[2] & main.chunk.chunkMask); + + c.glDrawElements(c.GL_TRIANGLES, 6, c.GL_UNSIGNED_INT, null); + } + } + }; +}; + +var blockyEntityTypes: std.StringHashMapUnmanaged(BlockEntityType) = .{}; + +pub fn init() void { + inline for(@typeInfo(BlockEntityTypes).@"struct".decls) |declaration| { + const class = BlockEntityType.init(@field(BlockEntityTypes, declaration.name)); + blockyEntityTypes.putNoClobber(main.globalAllocator.allocator, class.id, class) catch unreachable; + std.log.debug("Registered BlockEntityType '{s}'", .{class.id}); + } +} + +pub fn reset() void { + inline for(@typeInfo(BlockEntityTypes).@"struct".decls) |declaration| { + @field(BlockEntityTypes, declaration.name).reset(); + } +} + +pub fn deinit() void { + inline for(@typeInfo(BlockEntityTypes).@"struct".decls) |declaration| { + @field(BlockEntityTypes, declaration.name).deinit(); + } + blockyEntityTypes.deinit(main.globalAllocator.allocator); +} + +pub fn getByID(_id: ?[]const u8) ?*const BlockEntityType { + const id = _id orelse return null; + if(blockyEntityTypes.getPtr(id)) |cls| return cls; + std.log.err("BlockEntityType with id '{s}' not found", .{id}); + return null; +} + +pub fn renderAll(projectionMatrix: Mat4f, ambientLight: Vec3f, playerPos: Vec3d) void { + inline for(@typeInfo(BlockEntityTypes).@"struct".decls) |declaration| { + @field(BlockEntityTypes, declaration.name).renderAll(projectionMatrix, ambientLight, playerPos); + } +} diff --git a/blocks.zig b/blocks.zig new file mode 100644 index 0000000000..9cbf09884b --- /dev/null +++ b/blocks.zig @@ -0,0 +1,803 @@ +const std = @import("std"); + +const main = @import("main"); +const Tag = main.Tag; +const utils = main.utils; +const ZonElement = @import("zon.zig").ZonElement; +const chunk = @import("chunk.zig"); +const Neighbor = chunk.Neighbor; +const Chunk = chunk.Chunk; +const graphics = @import("graphics.zig"); +const SSBO = graphics.SSBO; +const Image = graphics.Image; +const Color = graphics.Color; +const TextureArray = graphics.TextureArray; +const items = @import("items.zig"); +const models = @import("models.zig"); +const ModelIndex = models.ModelIndex; +const rotation = @import("rotation.zig"); +const RotationMode = rotation.RotationMode; +const Degrees = rotation.Degrees; +const Entity = main.server.Entity; +const block_entity = @import("block_entity.zig"); +const BlockEntityType = block_entity.BlockEntityType; +const ClientBlockCallback = main.callbacks.ClientBlockCallback; +const ServerBlockCallback = main.callbacks.ServerBlockCallback; +const BlockTouchCallback = main.callbacks.BlockTouchCallback; +const sbb = main.server.terrain.structure_building_blocks; +const blueprint = main.blueprint; +const Assets = main.assets.Assets; + +pub const maxBlockCount: usize = 65536; // 16 bit limit + +pub const BlockDrop = struct { + items: []const items.ItemStack, + chance: f32, +}; + +/// Ores can be found underground in veins. +/// TODO: Add support for non-stone ores. +pub const Ore = struct { + /// average size of a vein in blocks + size: f32, + /// average density of a vein + density: f32, + /// average veins per chunk + veins: f32, + /// maximum height this ore can be generated + maxHeight: i32, + minHeight: i32, + + blockType: u16, +}; + +var _transparent: [maxBlockCount]bool = undefined; +var _collide: [maxBlockCount]bool = undefined; +var _id: [maxBlockCount][]u8 = undefined; + +var _blockHealth: [maxBlockCount]f32 = undefined; +var _blockResistance: [maxBlockCount]f32 = undefined; + +/// Whether you can replace it with another block, mainly used for fluids/gases +var _replacable: [maxBlockCount]bool = undefined; +var _selectable: [maxBlockCount]bool = undefined; +var _blockDrops: [maxBlockCount][]BlockDrop = undefined; +/// Meaning undegradable parts of trees or other structures can grow through this block. +var _degradable: [maxBlockCount]bool = undefined; +var _viewThrough: [maxBlockCount]bool = undefined; +var _alwaysViewThrough: [maxBlockCount]bool = undefined; +var _hasBackFace: [maxBlockCount]bool = undefined; +var _blockTags: [maxBlockCount][]Tag = undefined; +var _light: [maxBlockCount]u32 = undefined; +/// How much light this block absorbs if it is transparent +var _absorption: [maxBlockCount]u32 = undefined; + +var _onInteract: [maxBlockCount]ClientBlockCallback = undefined; +var _onBreak: [maxBlockCount]ServerBlockCallback = undefined; +var _onUpdate: [maxBlockCount]ServerBlockCallback = undefined; +var _decayProhibitor: [maxBlockCount]bool = undefined; +var _mode: [maxBlockCount]*const RotationMode = undefined; +var _modeData: [maxBlockCount]u16 = undefined; +var _lodReplacement: [maxBlockCount]u16 = undefined; +var _opaqueVariant: [maxBlockCount]u16 = undefined; + +var _friction: [maxBlockCount]f32 = undefined; +var _bounciness: [maxBlockCount]f32 = undefined; +var _density: [maxBlockCount]f32 = undefined; +var _terminalVelocity: [maxBlockCount]f32 = undefined; +var _mobility: [maxBlockCount]f32 = undefined; + +var _allowOres: [maxBlockCount]bool = undefined; +var _onTick: [maxBlockCount]ServerBlockCallback = undefined; +var _onTouch: [maxBlockCount]BlockTouchCallback = undefined; +var _blockEntity: [maxBlockCount]?*const BlockEntityType = undefined; + +var reverseIndices: std.StringHashMapUnmanaged(u16) = .{}; + +var size: u32 = 0; + +pub var ores: main.ListUnmanaged(Ore) = .{}; + +pub fn register(_: []const u8, id: []const u8, zon: ZonElement) u16 { + _id[size] = main.worldArena.dupe(u8, id); + reverseIndices.put(main.worldArena.allocator, _id[size], @intCast(size)) catch unreachable; + + _mode[size] = rotation.getByID(zon.get([]const u8, "rotation", "cubyz:no_rotation")); + _blockHealth[size] = zon.get(f32, "blockHealth", 1); + _blockResistance[size] = zon.get(f32, "blockResistance", 0); + const rotation_tags = _mode[size].getBlockTags(); + const block_tags = Tag.loadTagsFromZon(main.stackAllocator, zon.getChild("tags")); + defer main.stackAllocator.free(block_tags); + _blockTags[size] = std.mem.concat(main.worldArena.allocator, Tag, &.{rotation_tags, block_tags}) catch unreachable; + + if(_blockTags[size].len == 0) std.log.err("Block {s} is missing 'tags' field", .{id}); + for(_blockTags[size]) |tag| { + if(tag == Tag.sbbChild) { + sbb.registerChildBlock(@intCast(size), _id[size]); + break; + } + } + + _light[size] = zon.get(u32, "emittedLight", 0); + _absorption[size] = zon.get(u32, "absorbedLight", 0xffffff); + _degradable[size] = zon.get(bool, "degradable", false); + _selectable[size] = zon.get(bool, "selectable", true); + _replacable[size] = zon.get(bool, "replacable", false); + _onInteract[size] = blk: { + break :blk ClientBlockCallback.init(zon.getChildOrNull("onInteract") orelse break :blk .noop) orelse { + std.log.err("Failed to load onInteract event for block {s}", .{id}); + break :blk .noop; + }; + }; + + _onBreak[size] = blk: { + break :blk ServerBlockCallback.init(zon.getChildOrNull("onBreak") orelse break :blk .noop) orelse { + std.log.err("Failed to load onBreak event for block {s}", .{id}); + break :blk .noop; + }; + }; + _onUpdate[size] = blk: { + break :blk ServerBlockCallback.init(zon.getChildOrNull("onUpdate") orelse break :blk .noop) orelse { + std.log.err("Failed to load onUpdate event for block {s}", .{id}); + break :blk .noop; + }; + }; + _decayProhibitor[size] = zon.get(bool, "decayProhibitor", false); + + _transparent[size] = zon.get(bool, "transparent", false); + _collide[size] = zon.get(bool, "collide", true); + _alwaysViewThrough[size] = zon.get(bool, "alwaysViewThrough", false); + _viewThrough[size] = zon.get(bool, "viewThrough", false) or _transparent[size] or _alwaysViewThrough[size]; + _hasBackFace[size] = zon.get(bool, "hasBackFace", false); + _friction[size] = zon.get(f32, "friction", 20); + _bounciness[size] = zon.get(f32, "bounciness", 0.0); + _density[size] = zon.get(f32, "density", 0.001); + _terminalVelocity[size] = zon.get(f32, "terminalVelocity", 90); + _mobility[size] = zon.get(f32, "mobility", 1.0); + _allowOres[size] = zon.get(bool, "allowOres", false); + _onTick[size] = blk: { + break :blk ServerBlockCallback.init(zon.getChildOrNull("onTick") orelse break :blk .noop) orelse { + std.log.err("Failed to load onTick event for block {s}", .{id}); + break :blk .noop; + }; + }; + _onTouch[size] = blk: { + break :blk BlockTouchCallback.init(zon.getChildOrNull("onTouch") orelse break :blk .noop) orelse { + std.log.err("Failed to load onTouch event for block {s}", .{id}); + break :blk .noop; + }; + }; + + _blockEntity[size] = block_entity.getByID(zon.get(?[]const u8, "blockEntity", null)); + + const oreProperties = zon.getChild("ore"); + if(oreProperties != .null) blk: { + if(!std.mem.eql(u8, zon.get([]const u8, "rotation", "cubyz:no_rotation"), "cubyz:ore")) { + std.log.err("Ore must have rotation mode \"cubyz:ore\"!", .{}); + break :blk; + } + ores.append(main.worldArena, .{ + .veins = oreProperties.get(f32, "veins", 0), + .size = oreProperties.get(f32, "size", 0), + .maxHeight = oreProperties.get(i32, "height", 0), + .minHeight = oreProperties.get(i32, "minHeight", std.math.minInt(i32)), + .density = oreProperties.get(f32, "density", 0.5), + .blockType = @intCast(size), + }); + } + + defer size += 1; + std.log.debug("Registered block: {d: >5} '{s}'", .{size, id}); + return @intCast(size); +} + +fn registerBlockDrop(typ: u16, zon: ZonElement) void { + const drops = zon.getChild("drops").toSlice(); + _blockDrops[typ] = main.worldArena.alloc(BlockDrop, drops.len); + + for(drops, 0..) |blockDrop, i| { + _blockDrops[typ][i].chance = blockDrop.get(f32, "chance", 1); + const itemZons = blockDrop.getChild("items").toSlice(); + var resultItems = main.List(items.ItemStack).initCapacity(main.stackAllocator, itemZons.len); + defer resultItems.deinit(); + + for(itemZons) |itemZon| { + var string = itemZon.as([]const u8, "auto"); + string = std.mem.trim(u8, string, " "); + var iterator = std.mem.splitScalar(u8, string, ' '); + var name = iterator.first(); + var amount: u16 = 1; + while(iterator.next()) |next| { + if(next.len == 0) continue; // skip multiple spaces. + amount = std.fmt.parseInt(u16, name, 0) catch 1; + name = next; + break; + } + + if(std.mem.eql(u8, name, "auto")) { + name = _id[typ]; + } + + const item = items.BaseItemIndex.fromId(name) orelse continue; + resultItems.append(.{.item = .{.baseItem = item}, .amount = amount}); + } + + _blockDrops[typ][i].items = main.worldArena.dupe(items.ItemStack, resultItems.items); + } +} + +fn registerLodReplacement(typ: u16, zon: ZonElement) void { + if(zon.get(?[]const u8, "lodReplacement", null)) |replacement| { + _lodReplacement[typ] = getTypeById(replacement); + } else { + _lodReplacement[typ] = typ; + } +} + +fn registerOpaqueVariant(typ: u16, zon: ZonElement) void { + if(zon.get(?[]const u8, "opaqueVariant", null)) |replacement| { + _opaqueVariant[typ] = getTypeById(replacement); + } else { + _opaqueVariant[typ] = typ; + } +} + +pub fn finishBlocks(zonElements: Assets.ZonHashMap) void { + var i: u16 = 0; + while(i < size) : (i += 1) { + registerBlockDrop(i, zonElements.get(_id[i]) orelse continue); + } + i = 0; + while(i < size) : (i += 1) { + registerLodReplacement(i, zonElements.get(_id[i]) orelse continue); + registerOpaqueVariant(i, zonElements.get(_id[i]) orelse continue); + } + blueprint.registerVoidBlock(parseBlock("cubyz:void")); +} + +pub fn reset() void { + size = 0; + ores = .{}; + reverseIndices = .{}; + meshes.reset(); +} + +pub fn getTypeById(id: []const u8) u16 { + if(reverseIndices.get(id)) |result| { + return result; + } else { + std.log.err("Couldn't find block {s}. Replacing it with air...", .{id}); + return 0; + } +} + +fn parseBlockData(fullBlockId: []const u8, data: []const u8) ?u16 { + if(std.mem.containsAtLeastScalar(u8, data, 1, ':')) { + const oreChild = parseBlock(data); + if(oreChild.data != 0) { + std.log.warn("Error while parsing ore block data of '{s}': Parent block data must be 0.", .{fullBlockId}); + } + return oreChild.typ; + } + return std.fmt.parseInt(u16, data, 0) catch |err| { + std.log.err("Error while parsing block data of '{s}': {s}", .{fullBlockId, @errorName(err)}); + return null; + }; +} + +pub fn parseBlock(data: []const u8) Block { + var id: []const u8 = data; + var blockData: ?u16 = null; + if(std.mem.indexOfScalarPos(u8, data, 1 + (std.mem.indexOfScalar(u8, data, ':') orelse 0), ':')) |pos| { + id = data[0..pos]; + blockData = parseBlockData(data, data[pos + 1 ..]); + } + if(reverseIndices.get(id)) |resultType| { + var result: Block = .{.typ = resultType, .data = 0}; + result.data = blockData orelse result.mode().naturalStandard; + return result; + } else { + std.log.err("Couldn't find block {s}. Replacing it with air...", .{id}); + return .{.typ = 0, .data = 0}; + } +} + +pub fn getBlockById(idAndData: []const u8) !u16 { + const addonNameSeparatorIndex = std.mem.indexOfScalar(u8, idAndData, ':') orelse return error.MissingAddonNameSeparator; + const blockIdEndIndex = std.mem.indexOfScalarPos(u8, idAndData, 1 + addonNameSeparatorIndex, ':') orelse idAndData.len; + const id = idAndData[0..blockIdEndIndex]; + return reverseIndices.get(id) orelse return error.NotFound; +} + +pub fn getBlockByIdWithMigrations(idAndData: []const u8) !u16 { + const addonNameSeparatorIndex = std.mem.indexOfScalar(u8, idAndData, ':') orelse return error.MissingAddonNameSeparator; + const blockIdEndIndex = std.mem.indexOfScalarPos(u8, idAndData, 1 + addonNameSeparatorIndex, ':') orelse idAndData.len; + var id = idAndData[0..blockIdEndIndex]; + id = main.migrations.applySingle(.block, id); + return reverseIndices.get(id) orelse return error.NotFound; +} + +pub fn getBlockData(idLikeString: []const u8) !?u16 { + const addonNameSeparatorIndex = std.mem.indexOfScalar(u8, idLikeString, ':') orelse return error.MissingAddonNameSeparator; + const blockIdEndIndex = std.mem.indexOfScalarPos(u8, idLikeString, 1 + addonNameSeparatorIndex, ':') orelse return null; + const dataString = idLikeString[blockIdEndIndex + 1 ..]; + if(dataString.len == 0) return error.EmptyDataString; + return std.fmt.parseInt(u16, dataString, 0) catch return error.InvalidData; +} + +pub fn hasRegistered(id: []const u8) bool { + return reverseIndices.contains(id); +} + +pub const Block = packed struct { // MARK: Block + typ: u16, + data: u16, + + pub const air = Block{.typ = 0, .data = 0}; + + pub fn toInt(self: Block) u32 { + return @as(u32, self.typ) | @as(u32, self.data) << 16; + } + pub fn fromInt(self: u32) Block { + return Block{.typ = @truncate(self), .data = @intCast(self >> 16)}; + } + + pub inline fn transparent(self: Block) bool { + return _transparent[self.typ]; + } + + pub inline fn collide(self: Block) bool { + return _collide[self.typ]; + } + + pub inline fn id(self: Block) []u8 { + return _id[self.typ]; + } + + pub inline fn blockHealth(self: Block) f32 { + return _blockHealth[self.typ]; + } + + pub inline fn blockResistance(self: Block) f32 { + return _blockResistance[self.typ]; + } + + /// Whether you can replace it with another block, mainly used for fluids/gases + pub inline fn replacable(self: Block) bool { + return _replacable[self.typ]; + } + + pub inline fn selectable(self: Block) bool { + return _selectable[self.typ]; + } + + pub inline fn blockDrops(self: Block) []BlockDrop { + return _blockDrops[self.typ]; + } + + /// Meaning undegradable parts of trees or other structures can grow through this block. + pub inline fn degradable(self: Block) bool { + return _degradable[self.typ]; + } + + pub inline fn viewThrough(self: Block) bool { + return _viewThrough[self.typ]; + } + + /// shows backfaces even when next to the same block type + pub inline fn alwaysViewThrough(self: Block) bool { + return _alwaysViewThrough[self.typ]; + } + + pub inline fn hasBackFace(self: Block) bool { + return _hasBackFace[self.typ]; + } + + pub inline fn blockTags(self: Block) []const Tag { + return _blockTags[self.typ]; + } + + pub inline fn hasTag(self: Block, tag: Tag) bool { + return std.mem.containsAtLeastScalar(Tag, self.blockTags(), 1, tag); + } + + pub inline fn light(self: Block) u32 { + return _light[self.typ]; + } + + /// How much light this block absorbs if it is transparent. + pub inline fn absorption(self: Block) u32 { + return _absorption[self.typ]; + } + + pub inline fn onInteract(self: Block) ClientBlockCallback { + return _onInteract[self.typ]; + } + pub inline fn onBreak(self: Block) ServerBlockCallback { + return _onBreak[self.typ]; + } + pub inline fn onUpdate(self: Block) ServerBlockCallback { + return _onUpdate[self.typ]; + } + pub inline fn decayProhibitor(self: Block) bool { + return _decayProhibitor[self.typ]; + } + pub inline fn mode(self: Block) *const RotationMode { + return _mode[self.typ]; + } + + pub inline fn modeData(self: Block) u16 { + return _modeData[self.typ]; + } + + pub inline fn rotateZ(self: Block, angle: Degrees) Block { + return .{.typ = self.typ, .data = self.mode().rotateZ(self.data, angle)}; + } + + pub inline fn lodReplacement(self: Block) u16 { + return _lodReplacement[self.typ]; + } + + pub inline fn opaqueVariant(self: Block) u16 { + return _opaqueVariant[self.typ]; + } + + pub inline fn friction(self: Block) f32 { + return _friction[self.typ]; + } + + pub inline fn bounciness(self: Block) f32 { + return _bounciness[self.typ]; + } + + pub inline fn density(self: Block) f32 { + return _density[self.typ]; + } + + pub inline fn terminalVelocity(self: Block) f32 { + return _terminalVelocity[self.typ]; + } + + pub inline fn mobility(self: Block) f32 { + return _mobility[self.typ]; + } + + pub inline fn allowOres(self: Block) bool { + return _allowOres[self.typ]; + } + + pub inline fn onTick(self: Block) ServerBlockCallback { + return _onTick[self.typ]; + } + + pub inline fn onTouch(self: Block) BlockTouchCallback { + return _onTouch[self.typ]; + } + + pub fn blockEntity(self: Block) ?*const BlockEntityType { + return _blockEntity[self.typ]; + } + + pub fn canBeChangedInto(self: Block, newBlock: Block, item: main.items.ItemStack, shouldDropSourceBlockOnSuccess: *bool) main.rotation.RotationMode.CanBeChangedInto { + return newBlock.mode().canBeChangedInto(self, newBlock, item, shouldDropSourceBlockOnSuccess); + } +}; + +pub const meshes = struct { // MARK: meshes + const AnimationData = extern struct { + startFrame: u32, + frames: u32, + time: u32, + }; + + const FogData = extern struct { + fogDensity: f32, + fogColor: u32, + }; + var size: u32 = 0; + var _modelIndex: [maxBlockCount]ModelIndex = undefined; + var textureIndices: [maxBlockCount][16]u16 = undefined; + /// Stores the number of textures after each block was added. Used to clean additional textures when the world is switched. + var maxTextureCount: [maxBlockCount]u32 = undefined; + /// Number of loaded meshes. Used to determine if an update is needed. + var loadedMeshes: u32 = 0; + + var textureIDs: main.ListUnmanaged([]const u8) = .{}; + var animation: main.ListUnmanaged(AnimationData) = .{}; + var blockTextures: main.ListUnmanaged(Image) = .{}; + var emissionTextures: main.ListUnmanaged(Image) = .{}; + var reflectivityTextures: main.ListUnmanaged(Image) = .{}; + var absorptionTextures: main.ListUnmanaged(Image) = .{}; + var textureFogData: main.ListUnmanaged(FogData) = .{}; + pub var textureOcclusionData: main.ListUnmanaged(bool) = .{}; + + pub var blockBreakingTextures: main.ListUnmanaged(u16) = .{}; + + const sideNames = blk: { + var names: [6][]const u8 = undefined; + names[Neighbor.dirDown.toInt()] = "texture_bottom"; + names[Neighbor.dirUp.toInt()] = "texture_top"; + names[Neighbor.dirPosX.toInt()] = "texture_right"; + names[Neighbor.dirNegX.toInt()] = "texture_left"; + names[Neighbor.dirPosY.toInt()] = "texture_front"; + names[Neighbor.dirNegY.toInt()] = "texture_back"; + break :blk names; + }; + + var animationSSBO: ?SSBO = null; + var animatedTextureSSBO: ?SSBO = null; + var fogSSBO: ?SSBO = null; + + var animationComputePipeline: graphics.ComputePipeline = undefined; + var animationUniforms: struct { + time: c_int, + size: c_int, + } = undefined; + + pub var blockTextureArray: TextureArray = undefined; + pub var emissionTextureArray: TextureArray = undefined; + pub var reflectivityAndAbsorptionTextureArray: TextureArray = undefined; + pub var ditherTexture: graphics.Texture = undefined; + + const black: Color = Color{.r = 0, .g = 0, .b = 0, .a = 255}; + const magenta: Color = Color{.r = 255, .g = 0, .b = 255, .a = 255}; + var undefinedTexture = [_]Color{magenta, black, black, magenta}; + const undefinedImage = Image{.width = 2, .height = 2, .imageData = undefinedTexture[0..]}; + var emptyTexture = [_]Color{black}; + const emptyImage = Image{.width = 1, .height = 1, .imageData = emptyTexture[0..]}; + + pub fn init() void { + animationComputePipeline = graphics.ComputePipeline.init("assets/cubyz/shaders/animation_pre_processing.comp", "", &animationUniforms); + blockTextureArray = .init(); + emissionTextureArray = .init(); + reflectivityAndAbsorptionTextureArray = .init(); + ditherTexture = .initFromMipmapFiles("assets/cubyz/blocks/textures/dither/", 64, 0.5); + } + + pub fn deinit() void { + if(animationSSBO) |ssbo| { + ssbo.deinit(); + } + if(animatedTextureSSBO) |ssbo| { + ssbo.deinit(); + } + if(fogSSBO) |ssbo| { + ssbo.deinit(); + } + animationComputePipeline.deinit(); + blockTextureArray.deinit(); + emissionTextureArray.deinit(); + reflectivityAndAbsorptionTextureArray.deinit(); + ditherTexture.deinit(); + } + + pub fn reset() void { + meshes.size = 0; + loadedMeshes = 0; + textureIDs = .{}; + animation = .{}; + blockTextures = .{}; + emissionTextures = .{}; + reflectivityTextures = .{}; + absorptionTextures = .{}; + textureFogData = .{}; + textureOcclusionData = .{}; + blockBreakingTextures = .{}; + } + + pub inline fn model(block: Block) ModelIndex { + return block.mode().model(block); + } + + pub inline fn modelIndexStart(block: Block) ModelIndex { + return _modelIndex[block.typ]; + } + + pub inline fn fogDensity(block: Block) f32 { + return textureFogData.items[animation.items[textureIndices[block.typ][0]].startFrame].fogDensity; + } + + pub inline fn fogColor(block: Block) u32 { + return textureFogData.items[animation.items[textureIndices[block.typ][0]].startFrame].fogColor; + } + + pub inline fn hasFog(block: Block) bool { + return fogDensity(block) != 0.0; + } + + pub inline fn textureIndex(block: Block, orientation: usize) u16 { + if(orientation < 16) { + return textureIndices[block.typ][orientation]; + } else { + return textureIndices[block.data][orientation - 16]; + } + } + + fn extendedPath(_allocator: main.heap.NeverFailingAllocator, path: []const u8, ending: []const u8) []const u8 { + return std.fmt.allocPrint(_allocator.allocator, "{s}{s}", .{path, ending}) catch unreachable; + } + + fn readTextureFile(_path: []const u8, ending: []const u8, default: Image) Image { + const path = extendedPath(main.stackAllocator, _path, ending); + defer main.stackAllocator.free(path); + return Image.readFromFile(main.worldArena, path) catch default; + } + + fn extractAnimationSlice(image: Image, frame: usize, frames: usize) Image { + if(image.height < frames) return image; + var startHeight = image.height/frames*frame; + if(image.height%frames > frame) startHeight += frame else startHeight += image.height%frames; + var endHeight = image.height/frames*(frame + 1); + if(image.height%frames > frame + 1) endHeight += frame + 1 else endHeight += image.height%frames; + var result = image; + result.height = @intCast(endHeight - startHeight); + result.imageData = result.imageData[startHeight*image.width .. endHeight*image.width]; + return result; + } + + fn readTextureData(_path: []const u8) void { + const path = _path[0 .. _path.len - ".png".len]; + const textureInfoPath = extendedPath(main.stackAllocator, path, ".zig.zon"); + defer main.stackAllocator.free(textureInfoPath); + const textureInfoZon = main.files.cwd().readToZon(main.stackAllocator, textureInfoPath) catch .null; + defer textureInfoZon.deinit(main.stackAllocator); + const animationFrames = textureInfoZon.get(u32, "frames", 1); + const animationTime = textureInfoZon.get(u32, "time", 1); + animation.append(main.worldArena, .{.startFrame = @intCast(blockTextures.items.len), .frames = animationFrames, .time = animationTime}); + const base = readTextureFile(path, ".png", Image.defaultImage); + const emission = readTextureFile(path, "_emission.png", Image.emptyImage); + const reflectivity = readTextureFile(path, "_reflectivity.png", Image.emptyImage); + const absorption = readTextureFile(path, "_absorption.png", Image.whiteEmptyImage); + for(0..animationFrames) |i| { + blockTextures.append(main.worldArena, extractAnimationSlice(base, i, animationFrames)); + emissionTextures.append(main.worldArena, extractAnimationSlice(emission, i, animationFrames)); + reflectivityTextures.append(main.worldArena, extractAnimationSlice(reflectivity, i, animationFrames)); + absorptionTextures.append(main.worldArena, extractAnimationSlice(absorption, i, animationFrames)); + textureFogData.append(main.worldArena, .{ + .fogDensity = textureInfoZon.get(f32, "fogDensity", 0.0), + .fogColor = textureInfoZon.get(u32, "fogColor", 0xffffff), + }); + } + textureOcclusionData.append(main.worldArena, textureInfoZon.get(bool, "hasOcclusion", true)); + } + + pub fn readTexture(_textureId: ?[]const u8, assetFolder: []const u8) !u16 { + const textureId = _textureId orelse return error.NotFound; + var result: u16 = undefined; + var splitter = std.mem.splitScalar(u8, textureId, ':'); + const mod = splitter.first(); + const id = splitter.rest(); + var path = try std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{s}/blocks/textures/{s}.png", .{assetFolder, mod, id}); + defer main.stackAllocator.free(path); + // Test if it's already in the list: + for(textureIDs.items, 0..) |other, j| { + if(std.mem.eql(u8, other, path)) { + result = @intCast(j); + return result; + } + } + const file = main.files.cwd().openFile(path) catch |err| blk: { + if(err != error.FileNotFound) { + std.log.err("Could not open file {s}: {s}", .{path, @errorName(err)}); + } + main.stackAllocator.free(path); + path = try std.fmt.allocPrint(main.stackAllocator.allocator, "assets/{s}/blocks/textures/{s}.png", .{mod, id}); // Default to global assets. + break :blk main.files.cwd().openFile(path) catch |err2| { + std.log.err("File not found. Searched in \"{s}\" and also in the assetFolder \"{s}\"", .{path, assetFolder}); + return err2; + }; + }; + file.close(); // It was only openend to check if it exists. + // Otherwise read it into the list: + result = @intCast(textureIDs.items.len); + + textureIDs.append(main.worldArena, main.worldArena.dupe(u8, path)); + readTextureData(path); + return result; + } + + pub fn getTextureIndices(zon: ZonElement, assetFolder: []const u8, textureIndicesRef: *[16]u16) void { + const defaultIndex = readTexture(zon.get(?[]const u8, "texture", null), assetFolder) catch 0; + inline for(textureIndicesRef, 0..) |*ref, i| { + var textureId = zon.get(?[]const u8, std.fmt.comptimePrint("texture{}", .{i}), null); + if(i < sideNames.len) { + textureId = zon.get(?[]const u8, sideNames[i], textureId); + } + ref.* = readTexture(textureId, assetFolder) catch defaultIndex; + } + } + + pub fn register(assetFolder: []const u8, _: []const u8, zon: ZonElement) void { + _modelIndex[meshes.size] = _mode[meshes.size].createBlockModel(.{.typ = @intCast(meshes.size), .data = 0}, &_modeData[meshes.size], zon.getChild("model")); + + // The actual model is loaded later, in the rendering thread. + // But textures can be loaded here: + + getTextureIndices(zon, assetFolder, &textureIndices[meshes.size]); + + maxTextureCount[meshes.size] = @intCast(textureIDs.items.len); + + meshes.size += 1; + } + + pub fn registerBlockBreakingAnimation(assetFolder: []const u8) void { + var i: usize = 0; + while(true) : (i += 1) { + const path1 = std.fmt.allocPrint(main.stackAllocator.allocator, "assets/cubyz/blocks/textures/breaking/{}.png", .{i}) catch unreachable; + defer main.stackAllocator.free(path1); + const path2 = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/cubyz/blocks/textures/breaking/{}.png", .{assetFolder, i}) catch unreachable; + defer main.stackAllocator.free(path2); + if(!main.files.cwd().hasFile(path1) and !main.files.cwd().hasFile(path2)) break; + + const id = std.fmt.allocPrint(main.stackAllocator.allocator, "cubyz:breaking/{}", .{i}) catch unreachable; + defer main.stackAllocator.free(id); + blockBreakingTextures.append(main.worldArena, readTexture(id, assetFolder) catch break); + } + } + + pub fn preProcessAnimationData(time: u32) void { + animationComputePipeline.bind(); + graphics.c.glUniform1ui(animationUniforms.time, time); + graphics.c.glUniform1ui(animationUniforms.size, @intCast(animation.items.len)); + graphics.c.glDispatchCompute(@intCast(@divFloor(animation.items.len + 63, 64)), 1, 1); // TODO: Replace with @divCeil once available + graphics.c.glMemoryBarrier(graphics.c.GL_SHADER_STORAGE_BARRIER_BIT); + } + + pub fn reloadTextures(_: usize) void { + blockTextures.clearRetainingCapacity(); + emissionTextures.clearRetainingCapacity(); + reflectivityTextures.clearRetainingCapacity(); + absorptionTextures.clearRetainingCapacity(); + textureFogData.clearRetainingCapacity(); + textureOcclusionData.clearRetainingCapacity(); + for(textureIDs.items) |path| { + readTextureData(path); + } + generateTextureArray(); + } + + pub fn generateTextureArray() void { + const c = graphics.c; + blockTextureArray.generate(blockTextures.items, true, true); + c.glTexParameterf(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_ANISOTROPY, @floatFromInt(main.settings.anisotropicFiltering)); + emissionTextureArray.generate(emissionTextures.items, true, false); + c.glTexParameterf(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_ANISOTROPY, @floatFromInt(main.settings.anisotropicFiltering)); + const reflectivityAndAbsorptionTextures = main.stackAllocator.alloc(Image, reflectivityTextures.items.len); + defer main.stackAllocator.free(reflectivityAndAbsorptionTextures); + defer for(reflectivityAndAbsorptionTextures) |texture| { + texture.deinit(main.stackAllocator); + }; + for(reflectivityTextures.items, absorptionTextures.items, reflectivityAndAbsorptionTextures) |reflecitivityTexture, absorptionTexture, *resultTexture| { + const width = @max(reflecitivityTexture.width, absorptionTexture.width); + const height = @max(reflecitivityTexture.height, absorptionTexture.height); + resultTexture.* = Image.init(main.stackAllocator, width, height); + for(0..width) |x| { + for(0..height) |y| { + const reflectivity = reflecitivityTexture.getRGB(x*reflecitivityTexture.width/width, y*reflecitivityTexture.height/height); + const absorption = absorptionTexture.getRGB(x*absorptionTexture.width/width, y*absorptionTexture.height/height); + resultTexture.setRGB(x, y, .{.r = absorption.r, .g = absorption.g, .b = absorption.b, .a = reflectivity.r}); + } + } + } + reflectivityAndAbsorptionTextureArray.generate(reflectivityAndAbsorptionTextures, true, false); + c.glTexParameterf(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_ANISOTROPY, @floatFromInt(main.settings.anisotropicFiltering)); + + // Also generate additional buffers: + if(animationSSBO) |ssbo| { + ssbo.deinit(); + } + if(animatedTextureSSBO) |ssbo| { + ssbo.deinit(); + } + if(fogSSBO) |ssbo| { + ssbo.deinit(); + } + animationSSBO = SSBO.initStatic(AnimationData, animation.items); + animationSSBO.?.bind(0); + + animatedTextureSSBO = SSBO.initStaticSize(u32, animation.items.len); + animatedTextureSSBO.?.bind(1); + fogSSBO = SSBO.initStatic(FogData, textureFogData.items); + fogSSBO.?.bind(7); + } +}; diff --git a/blueprint.zig b/blueprint.zig new file mode 100644 index 0000000000..4d8a66eec0 --- /dev/null +++ b/blueprint.zig @@ -0,0 +1,643 @@ +const std = @import("std"); + +const main = @import("main"); +const Compression = main.utils.Compression; +const ZonElement = @import("zon.zig").ZonElement; +const vec = main.vec; +const Vec3i = vec.Vec3i; + +const Array3D = main.utils.Array3D; +const Block = main.blocks.Block; +const NeverFailingAllocator = main.heap.NeverFailingAllocator; +const User = main.server.User; +const ServerChunk = main.chunk.ServerChunk; +const Degrees = main.rotation.Degrees; +const Tag = main.Tag; + +const GameIdToBlueprintIdMapType = std.AutoHashMap(u16, u16); +const BlockIdSizeType = u32; +const BlockStorageType = u32; + +const BinaryWriter = main.utils.BinaryWriter; +const BinaryReader = main.utils.BinaryReader; + +const AliasTable = main.utils.AliasTable; +const ListUnmanaged = main.ListUnmanaged; + +pub const blueprintVersion = 0; +var voidType: ?u16 = null; + +pub const BlueprintCompression = enum(u16) { + deflate, +}; + +pub const Blueprint = struct { + blocks: Array3D(Block), + + pub fn init(allocator: NeverFailingAllocator) Blueprint { + return .{.blocks = .init(allocator, 0, 0, 0)}; + } + pub fn deinit(self: Blueprint, allocator: NeverFailingAllocator) void { + self.blocks.deinit(allocator); + } + pub fn clone(self: Blueprint, allocator: NeverFailingAllocator) Blueprint { + return .{.blocks = self.blocks.clone(allocator)}; + } + pub fn rotateZ(self: Blueprint, allocator: NeverFailingAllocator, angle: Degrees) Blueprint { + var new = Blueprint{ + .blocks = switch(angle) { + .@"0", .@"180" => .init(allocator, self.blocks.width, self.blocks.depth, self.blocks.height), + .@"90", .@"270" => .init(allocator, self.blocks.depth, self.blocks.width, self.blocks.height), + }, + }; + + for(0..self.blocks.width) |xOld| { + for(0..self.blocks.depth) |yOld| { + const xNew, const yNew = switch(angle) { + .@"0" => .{xOld, yOld}, + .@"90" => .{new.blocks.width - yOld - 1, xOld}, + .@"180" => .{new.blocks.width - xOld - 1, new.blocks.depth - yOld - 1}, + .@"270" => .{yOld, new.blocks.depth - xOld - 1}, + }; + + for(0..self.blocks.height) |z| { + const block = self.blocks.get(xOld, yOld, z); + new.blocks.set(xNew, yNew, z, block.rotateZ(angle)); + } + } + } + return new; + } + + const CaptureResult = union(enum) { + success: Blueprint, + failure: struct {pos: Vec3i, message: []const u8}, + }; + + pub fn capture(allocator: NeverFailingAllocator, pos1: Vec3i, pos2: Vec3i) CaptureResult { + const startX = @min(pos1[0], pos2[0]); + const startY = @min(pos1[1], pos2[1]); + const startZ = @min(pos1[2], pos2[2]); + + const endX = @max(pos1[0], pos2[0]); + const endY = @max(pos1[1], pos2[1]); + const endZ = @max(pos1[2], pos2[2]); + + const sizeX: u32 = @intCast(endX - startX + 1); + const sizeY: u32 = @intCast(endY - startY + 1); + const sizeZ: u32 = @intCast(endZ - startZ + 1); + + const self = Blueprint{.blocks = .init(allocator, sizeX, sizeY, sizeZ)}; + + for(0..sizeX) |x| { + const worldX = startX +% @as(i32, @intCast(x)); + + for(0..sizeY) |y| { + const worldY = startY +% @as(i32, @intCast(y)); + + for(0..sizeZ) |z| { + const worldZ = startZ +% @as(i32, @intCast(z)); + + const maybeBlock = main.server.world.?.getBlock(worldX, worldY, worldZ); + if(maybeBlock) |block| { + self.blocks.set(x, y, z, block); + } else { + return .{.failure = .{.pos = .{worldX, worldY, worldZ}, .message = "Chunk containing block not loaded."}}; + } + } + } + } + return .{.success = self}; + } + + pub const PasteMode = enum {all, degradable}; + + pub fn pasteInGeneration(self: Blueprint, pos: Vec3i, chunk: *ServerChunk, mode: PasteMode) void { + switch(mode) { + inline else => |comptimeMode| _pasteInGeneration(self, pos, chunk, comptimeMode), + } + } + + fn _pasteInGeneration(self: Blueprint, pos: Vec3i, chunk: *ServerChunk, comptime mode: PasteMode) void { + const indexEndX: i32 = @min(@as(i32, chunk.super.width) - pos[0], @as(i32, @intCast(self.blocks.width))); + const indexEndY: i32 = @min(@as(i32, chunk.super.width) - pos[1], @as(i32, @intCast(self.blocks.depth))); + const indexEndZ: i32 = @min(@as(i32, chunk.super.width) - pos[2], @as(i32, @intCast(self.blocks.height))); + + var indexX: u31 = @max(0, -pos[0]); + while(indexX < indexEndX) : (indexX += chunk.super.pos.voxelSize) { + var indexY: u31 = @max(0, -pos[1]); + while(indexY < indexEndY) : (indexY += chunk.super.pos.voxelSize) { + var indexZ: u31 = @max(0, -pos[2]); + while(indexZ < indexEndZ) : (indexZ += chunk.super.pos.voxelSize) { + const block = self.blocks.get(indexX, indexY, indexZ); + + if(block.typ == voidType) continue; + + const chunkX = indexX + pos[0]; + const chunkY = indexY + pos[1]; + const chunkZ = indexZ + pos[2]; + switch(mode) { + .all => chunk.updateBlockInGeneration(chunkX, chunkY, chunkZ, block), + .degradable => chunk.updateBlockIfDegradable(chunkX, chunkY, chunkZ, block), + } + } + } + } + } + + pub const PasteFlags = struct { + preserveVoid: bool = false, + }; + + pub fn paste(self: Blueprint, pos: Vec3i, flags: PasteFlags) void { + main.items.Inventory.Sync.ServerSide.mutex.lock(); + defer main.items.Inventory.Sync.ServerSide.mutex.unlock(); + const startX = pos[0]; + const startY = pos[1]; + const startZ = pos[2]; + + for(0..self.blocks.width) |x| { + const worldX = startX +% @as(i32, @intCast(x)); + + for(0..self.blocks.depth) |y| { + const worldY = startY +% @as(i32, @intCast(y)); + + for(0..self.blocks.height) |z| { + const worldZ = startZ +% @as(i32, @intCast(z)); + + const block = self.blocks.get(x, y, z); + if(block.typ != voidType or flags.preserveVoid) + _ = main.server.world.?.updateBlock(worldX, worldY, worldZ, block); + } + } + } + } + pub fn load(allocator: NeverFailingAllocator, inputBuffer: []const u8) !Blueprint { + var compressedReader = BinaryReader.init(inputBuffer); + const version = try compressedReader.readInt(u16); + + if(version > blueprintVersion) { + std.log.err("Blueprint version {d} is not supported. Current version is {d}.", .{version, blueprintVersion}); + return error.UnsupportedVersion; + } + const compression = try compressedReader.readEnum(BlueprintCompression); + const blockPaletteSizeBytes = try compressedReader.readInt(u32); + const paletteBlockCount = try compressedReader.readInt(u16); + const width = try compressedReader.readInt(u16); + const depth = try compressedReader.readInt(u16); + const height = try compressedReader.readInt(u16); + + const self = Blueprint{.blocks = .init(allocator, width, depth, height)}; + + const decompressedData = try self.decompressBuffer(compressedReader.remaining, blockPaletteSizeBytes, compression); + defer main.stackAllocator.free(decompressedData); + var decompressedReader = BinaryReader.init(decompressedData); + + const palette = try loadBlockPalette(main.stackAllocator, paletteBlockCount, &decompressedReader); + defer main.stackAllocator.free(palette); + + const blueprintIdToGameIdMap = makeBlueprintIdToGameIdMap(main.stackAllocator, palette); + defer main.stackAllocator.free(blueprintIdToGameIdMap); + + for(self.blocks.mem) |*block| { + const blueprintBlockRaw = try decompressedReader.readInt(BlockStorageType); + + const blueprintBlock = Block.fromInt(blueprintBlockRaw); + const gameBlockId = blueprintIdToGameIdMap[blueprintBlock.typ]; + + block.* = .{.typ = gameBlockId, .data = blueprintBlock.data}; + } + return self; + } + pub fn store(self: Blueprint, allocator: NeverFailingAllocator) []u8 { + var gameIdToBlueprintId = self.makeGameIdToBlueprintIdMap(main.stackAllocator); + defer gameIdToBlueprintId.deinit(); + std.debug.assert(gameIdToBlueprintId.count() != 0); + + var uncompressedWriter = BinaryWriter.init(main.stackAllocator); + defer uncompressedWriter.deinit(); + + const blockPaletteSizeBytes = storeBlockPalette(gameIdToBlueprintId, &uncompressedWriter); + + for(self.blocks.mem) |block| { + const blueprintBlock: BlockStorageType = Block.toInt(.{.typ = gameIdToBlueprintId.get(block.typ).?, .data = block.data}); + uncompressedWriter.writeInt(BlockStorageType, blueprintBlock); + } + + const compressed = self.compressOutputBuffer(main.stackAllocator, uncompressedWriter.data.items); + defer main.stackAllocator.free(compressed.data); + + var outputWriter = BinaryWriter.initCapacity(allocator, @sizeOf(i16) + @sizeOf(BlueprintCompression) + @sizeOf(u32) + @sizeOf(u16)*4 + compressed.data.len); + + outputWriter.writeInt(u16, blueprintVersion); + outputWriter.writeEnum(BlueprintCompression, compressed.mode); + outputWriter.writeInt(u32, @intCast(blockPaletteSizeBytes)); + outputWriter.writeInt(u16, @intCast(gameIdToBlueprintId.count())); + outputWriter.writeInt(u16, @intCast(self.blocks.width)); + outputWriter.writeInt(u16, @intCast(self.blocks.depth)); + outputWriter.writeInt(u16, @intCast(self.blocks.height)); + + outputWriter.writeSlice(compressed.data); + + return outputWriter.data.toOwnedSlice(); + } + fn makeBlueprintIdToGameIdMap(allocator: NeverFailingAllocator, palette: [][]const u8) []u16 { + var blueprintIdToGameIdMap = allocator.alloc(u16, palette.len); + + for(palette, 0..) |blockName, blueprintBlockId| { + const gameBlockId = main.blocks.getBlockByIdWithMigrations(blockName) catch |err| blk: { + std.log.err("Couldn't find block with name {s}: {s}. Replacing it with air", .{blockName, @errorName(err)}); + break :blk 0; + }; + blueprintIdToGameIdMap[blueprintBlockId] = gameBlockId; + } + return blueprintIdToGameIdMap; + } + fn makeGameIdToBlueprintIdMap(self: Blueprint, allocator: NeverFailingAllocator) GameIdToBlueprintIdMapType { + var gameIdToBlueprintId: GameIdToBlueprintIdMapType = .init(allocator.allocator); + + for(self.blocks.mem) |block| { + const result = gameIdToBlueprintId.getOrPut(block.typ) catch unreachable; + if(!result.found_existing) { + result.value_ptr.* = @intCast(gameIdToBlueprintId.count() - 1); + } + } + + return gameIdToBlueprintId; + } + fn loadBlockPalette(allocator: NeverFailingAllocator, paletteBlockCount: usize, reader: *BinaryReader) ![][]const u8 { + var palette = allocator.alloc([]const u8, paletteBlockCount); + + for(0..@intCast(paletteBlockCount)) |index| { + const blockNameSize = try reader.readInt(BlockIdSizeType); + const blockName = try reader.readSlice(blockNameSize); + palette[index] = blockName; + } + return palette; + } + fn storeBlockPalette(map: GameIdToBlueprintIdMapType, writer: *BinaryWriter) usize { + var blockPalette = main.stackAllocator.alloc([]const u8, map.count()); + defer main.stackAllocator.free(blockPalette); + + var iterator = map.iterator(); + while(iterator.next()) |entry| { + const block = Block{.typ = entry.key_ptr.*, .data = 0}; + const blockId = block.id(); + blockPalette[entry.value_ptr.*] = blockId; + } + + std.log.info("Blueprint block palette:", .{}); + + for(0..blockPalette.len) |index| { + const blockName = blockPalette[index]; + std.log.info("palette[{d}]: {s}", .{index, blockName}); + + writer.writeInt(BlockIdSizeType, @intCast(blockName.len)); + writer.writeSlice(blockName); + } + + return writer.data.items.len; + } + fn decompressBuffer(self: Blueprint, data: []const u8, blockPaletteSizeBytes: usize, compression: BlueprintCompression) ![]u8 { + const blockArraySizeBytes = self.blocks.width*self.blocks.depth*self.blocks.height*@sizeOf(BlockStorageType); + const decompressedDataSizeBytes = blockPaletteSizeBytes + blockArraySizeBytes; + + const decompressedData = main.stackAllocator.alloc(u8, decompressedDataSizeBytes); + + switch(compression) { + .deflate => { + const sizeAfterDecompression = try Compression.inflateTo(decompressedData, data); + std.debug.assert(sizeAfterDecompression == decompressedDataSizeBytes); + }, + } + return decompressedData; + } + fn compressOutputBuffer(_: Blueprint, allocator: NeverFailingAllocator, decompressedData: []u8) struct {mode: BlueprintCompression, data: []u8} { + const compressionMode: BlueprintCompression = .deflate; + switch(compressionMode) { + .deflate => { + return .{.mode = .deflate, .data = Compression.deflate(allocator, decompressedData, .default)}; + }, + } + } + pub fn replace(self: *Blueprint, whitelist: ?Mask, blacklist: ?Mask, newBlocks: Pattern) void { + for(0..self.blocks.width) |x| { + for(0..self.blocks.depth) |y| { + for(0..self.blocks.height) |z| { + const current = self.blocks.get(x, y, z); + if(whitelist) |m| if(!m.match(current)) continue; + if(blacklist) |m| if(m.match(current)) continue; + self.blocks.set(x, y, z, newBlocks.blocks.sample(&main.seed).block); + } + } + } + } +}; + +pub const Pattern = struct { + const weightSeparator = '%'; + const expressionSeparator = ','; + + blocks: AliasTable(Entry), + + const Entry = struct { + block: Block, + chance: f32, + }; + + pub fn initFromString(allocator: NeverFailingAllocator, source: []const u8) !@This() { + var specifiers = std.mem.splitScalar(u8, source, expressionSeparator); + var totalWeight: f32 = 0; + + var weightedEntries: ListUnmanaged(struct {block: Block, weight: f32}) = .{}; + defer weightedEntries.deinit(main.stackAllocator); + + while(specifiers.next()) |specifier| { + var blockId = specifier; + var weight: f32 = 1.0; + + if(std.mem.containsAtLeastScalar(u8, specifier, 1, weightSeparator)) { + var iterator = std.mem.splitScalar(u8, specifier, weightSeparator); + const weightString = iterator.first(); + blockId = iterator.rest(); + + weight = std.fmt.parseFloat(f32, weightString) catch return error.@"Weight not a valid number"; + if(weight <= 0) return error.@"Weight must be greater than 0"; + } + + _ = main.blocks.getBlockById(blockId) catch return error.@"Block not found"; + const block = main.blocks.parseBlock(blockId); + + totalWeight += weight; + weightedEntries.append(main.stackAllocator, .{.block = block, .weight = weight}); + } + + const entries = allocator.alloc(Entry, weightedEntries.items.len); + for(weightedEntries.items, 0..) |entry, i| { + entries[i] = .{.block = entry.block, .chance = entry.weight/totalWeight}; + } + + return .{.blocks = .init(allocator, entries)}; + } + + pub fn deinit(self: @This(), allocator: NeverFailingAllocator) void { + self.blocks.deinit(allocator); + allocator.free(self.blocks.items); + } +}; + +pub const Mask = struct { + const AndList = ListUnmanaged(Entry); + const OrList = ListUnmanaged(AndList); + + entries: OrList, + + const or_ = '|'; + const and_ = '&'; + const inverse = '!'; + const tag = '$'; + const property = '@'; + + const Entry = struct { + inner: Inner, + isInverse: bool, + + const Inner = union(enum) { + block: Block, + blockType: u16, + blockTag: Tag, + blockProperty: Property, + + const Property = blk: { + var tempFields: [@typeInfo(Block).@"struct".decls.len]std.builtin.Type.EnumField = undefined; + var count = 0; + + for(std.meta.declarations(Block)) |decl| { + const declInfo = @typeInfo(@TypeOf(@field(Block, decl.name))); + if(declInfo != .@"fn") continue; + if(declInfo.@"fn".return_type != bool) continue; + if(declInfo.@"fn".params.len != 1) continue; + + tempFields[count] = .{.name = decl.name, .value = count}; + count += 1; + } + + const outFields: [count]std.builtin.Type.EnumField = tempFields[0..count].*; + + break :blk @Type(.{.@"enum" = .{ + .tag_type = u8, + .fields = &outFields, + .decls = &.{}, + .is_exhaustive = true, + }}); + }; + + fn initFromString(specifier: []const u8) !Inner { + return switch(specifier[0]) { + tag => .{.blockTag = Tag.get(specifier[1..]) orelse return error.TagNotFound}, + property => .{.blockProperty = std.meta.stringToEnum(Property, specifier[1..]) orelse return error.PropertyNotFound}, + else => return try parseBlockLike(specifier), + }; + } + + fn match(self: Inner, block: Block) bool { + return switch(self) { + .block => |desired| block.typ == desired.typ and block.data == desired.data, + .blockType => |desired| block.typ == desired, + .blockTag => |desired| block.hasTag(desired), + .blockProperty => |blockProperty| switch(blockProperty) { + inline else => |prop| @field(Block, @tagName(prop))(block), + }, + }; + } + }; + + fn initFromString(specifier: []const u8) !Entry { + const isInverse = specifier[0] == '!'; + const entry = try Inner.initFromString(specifier[if(isInverse) 1 else 0..]); + return .{.inner = entry, .isInverse = isInverse}; + } + + pub fn match(self: Entry, block: Block) bool { + const isMatch = self.inner.match(block); + if(self.isInverse) { + return !isMatch; + } + return isMatch; + } + }; + + pub fn initFromString(allocator: NeverFailingAllocator, source: []const u8) !@This() { + var result: @This() = .{.entries = .{}}; + errdefer result.deinit(allocator); + + var oredExpressions = std.mem.splitScalar(u8, source, or_); + while(oredExpressions.next()) |subExpression| { + if(subExpression.len == 0) return error.MissingExpression; + + var andStorage: AndList = .{}; + errdefer andStorage.deinit(allocator); + + var andedExpressions = std.mem.splitScalar(u8, subExpression, and_); + while(andedExpressions.next()) |specifier| { + if(specifier.len == 0) return error.MissingExpression; + + const entry = try Entry.initFromString(specifier); + andStorage.append(allocator, entry); + } + std.debug.assert(andStorage.items.len != 0); + + result.entries.append(allocator, andStorage); + } + std.debug.assert(result.entries.items.len != 0); + + return result; + } + + pub fn deinit(self: @This(), allocator: NeverFailingAllocator) void { + for(self.entries.items) |andStorage| { + andStorage.deinit(allocator); + } + self.entries.deinit(allocator); + } + + pub fn match(self: @This(), block: Block) bool { + for(self.entries.items) |andedExpressions| { + const status = blk: { + for(andedExpressions.items) |expression| { + if(!expression.match(block)) break :blk false; + } + break :blk true; + }; + + if(status) return true; + } + return false; + } +}; + +fn parseBlockLike(block: []const u8) error{DataParsingFailed, IdParsingFailed}!Mask.Entry.Inner { + if(@import("builtin").is_test) return try Test.parseBlockLikeTest(block); + const typ = main.blocks.getBlockById(block) catch return error.IdParsingFailed; + const dataNullable = main.blocks.getBlockData(block) catch return error.DataParsingFailed; + if(dataNullable) |data| return .{.block = .{.typ = typ, .data = data}}; + return .{.blockType = typ}; +} + +const Test = struct { + var parseBlockLikeTest: *const @TypeOf(parseBlockLike) = &defaultParseBlockLike; + + fn defaultParseBlockLike(_: []const u8) !Mask.Entry.Inner { + unreachable; + } + + fn @"parseBlockLike 1 null"(_: []const u8) !Mask.Entry.Inner { + return .{.blockType = 1}; + } + fn @"parseBlockLike 1 1"(_: []const u8) !Mask.Entry.Inner { + return .{.block = .{.typ = 1, .data = 1}}; + } + + fn @"parseBlockLike foo or bar"(data: []const u8) !Mask.Entry.Inner { + if(std.mem.eql(u8, data, "addon:foo")) { + return .{.block = .{.typ = 1, .data = 0}}; + } + if(std.mem.eql(u8, data, "addon:bar")) { + return .{.block = .{.typ = 2, .data = 0}}; + } + unreachable; + } +}; + +test "Mask match block type with any data" { + Test.parseBlockLikeTest = &Test.@"parseBlockLike 1 null"; + defer Test.parseBlockLikeTest = &Test.defaultParseBlockLike; + + const mask = try Mask.initFromString(main.heap.testingAllocator, "addon:dummy"); + defer mask.deinit(main.heap.testingAllocator); + + try std.testing.expect(mask.match(.{.typ = 1, .data = 0})); + try std.testing.expect(mask.match(.{.typ = 1, .data = 1})); + try std.testing.expect(!mask.match(.{.typ = 2, .data = 0})); +} + +test "Mask empty negative case" { + Test.parseBlockLikeTest = &Test.@"parseBlockLike 1 null"; + defer Test.parseBlockLikeTest = &Test.defaultParseBlockLike; + + try std.testing.expectError(error.MissingExpression, Mask.initFromString(main.heap.testingAllocator, "")); +} + +test "Mask half-or negative case" { + Test.parseBlockLikeTest = &Test.@"parseBlockLike 1 null"; + defer Test.parseBlockLikeTest = &Test.defaultParseBlockLike; + + try std.testing.expectError(error.MissingExpression, Mask.initFromString(main.heap.testingAllocator, "addon:dummy|")); +} + +test "Mask half-or negative case 2" { + Test.parseBlockLikeTest = &Test.@"parseBlockLike 1 null"; + defer Test.parseBlockLikeTest = &Test.defaultParseBlockLike; + + try std.testing.expectError(error.MissingExpression, Mask.initFromString(main.heap.testingAllocator, "|addon:dummy")); +} + +test "Mask half-and negative case" { + Test.parseBlockLikeTest = &Test.@"parseBlockLike 1 null"; + defer Test.parseBlockLikeTest = &Test.defaultParseBlockLike; + + try std.testing.expectError(error.MissingExpression, Mask.initFromString(main.heap.testingAllocator, "addon:dummy&")); +} + +test "Mask half-and negative case 2" { + Test.parseBlockLikeTest = &Test.@"parseBlockLike 1 null"; + defer Test.parseBlockLikeTest = &Test.defaultParseBlockLike; + + try std.testing.expectError(error.MissingExpression, Mask.initFromString(main.heap.testingAllocator, "&addon:dummy")); +} + +test "Mask inverse match block type with any data" { + Test.parseBlockLikeTest = &Test.@"parseBlockLike 1 null"; + defer Test.parseBlockLikeTest = &Test.defaultParseBlockLike; + + const mask = try Mask.initFromString(main.heap.testingAllocator, "!addon:dummy"); + defer mask.deinit(main.heap.testingAllocator); + + try std.testing.expect(!mask.match(.{.typ = 1, .data = 0})); + try std.testing.expect(!mask.match(.{.typ = 1, .data = 1})); + try std.testing.expect(mask.match(.{.typ = 2, .data = 0})); +} + +test "Mask match block type with exact data" { + Test.parseBlockLikeTest = &Test.@"parseBlockLike 1 1"; + defer Test.parseBlockLikeTest = &Test.defaultParseBlockLike; + + const mask = try Mask.initFromString(main.heap.testingAllocator, "addon:dummy"); + defer mask.deinit(main.heap.testingAllocator); + + try std.testing.expect(!mask.match(.{.typ = 1, .data = 0})); + try std.testing.expect(mask.match(.{.typ = 1, .data = 1})); + try std.testing.expect(!mask.match(.{.typ = 2, .data = 1})); +} + +test "Mask match type 0 or type 1 with exact data" { + Test.parseBlockLikeTest = &Test.@"parseBlockLike foo or bar"; + defer Test.parseBlockLikeTest = &Test.defaultParseBlockLike; + + const mask = try Mask.initFromString(main.heap.testingAllocator, "addon:foo|addon:bar"); + defer mask.deinit(main.heap.testingAllocator); + + try std.testing.expect(mask.match(.{.typ = 1, .data = 0})); + try std.testing.expect(mask.match(.{.typ = 2, .data = 0})); + try std.testing.expect(!mask.match(.{.typ = 1, .data = 1})); + try std.testing.expect(!mask.match(.{.typ = 2, .data = 1})); +} + +pub fn registerVoidBlock(block: Block) void { + voidType = block.typ; + std.debug.assert(voidType != 0); +} + +pub fn getVoidBlock() Block { + return Block{.typ = voidType.?, .data = 0}; +} diff --git a/chunk.zig b/chunk.zig new file mode 100644 index 0000000000..2b00ac7d52 --- /dev/null +++ b/chunk.zig @@ -0,0 +1,650 @@ +const std = @import("std"); + +const blocks = @import("blocks.zig"); +const Block = blocks.Block; +const main = @import("main"); +const settings = @import("settings.zig"); +const vec = @import("vec.zig"); +const Vec3i = vec.Vec3i; +const Vec3d = vec.Vec3d; + +pub const chunkShift: u5 = 5; +pub const chunkSize: u31 = 1 << chunkShift; +pub const chunkSizeIterator: [chunkSize]u0 = undefined; +pub const chunkVolume: u31 = 1 << 3*chunkShift; +pub const chunkMask: i32 = chunkSize - 1; + +/// Contains a bunch of constants used to describe neighboring blocks. +pub const Neighbor = enum(u3) { // MARK: Neighbor + dirUp = 0, + dirDown = 1, + dirPosX = 2, + dirNegX = 3, + dirPosY = 4, + dirNegY = 5, + + pub inline fn toInt(self: Neighbor) u3 { + return @intFromEnum(self); + } + + /// Index to relative position + pub fn relX(self: Neighbor) i32 { + const arr = [_]i32{0, 0, 1, -1, 0, 0}; + return arr[@intFromEnum(self)]; + } + /// Index to relative position + pub fn relY(self: Neighbor) i32 { + const arr = [_]i32{0, 0, 0, 0, 1, -1}; + return arr[@intFromEnum(self)]; + } + /// Index to relative position + pub fn relZ(self: Neighbor) i32 { + const arr = [_]i32{1, -1, 0, 0, 0, 0}; + return arr[@intFromEnum(self)]; + } + /// Index to relative position + pub fn relPos(self: Neighbor) Vec3i { + return .{self.relX(), self.relY(), self.relZ()}; + } + + pub fn fromRelPos(pos: Vec3i) ?Neighbor { + if(@reduce(.Add, @abs(pos)) != 1) { + return null; + } + return switch(pos[0]) { + 1 => return .dirPosX, + -1 => return .dirNegX, + else => switch(pos[1]) { + 1 => return .dirPosY, + -1 => return .dirNegY, + else => switch(pos[2]) { + 1 => return .dirUp, + -1 => return .dirDown, + else => return null, + }, + }, + }; + } + + /// Index to bitMask for bitmap direction data + pub inline fn bitMask(self: Neighbor) u6 { + return @as(u6, 1) << @intFromEnum(self); + } + /// To iterate over all neighbors easily + pub const iterable = [_]Neighbor{@enumFromInt(0), @enumFromInt(1), @enumFromInt(2), @enumFromInt(3), @enumFromInt(4), @enumFromInt(5)}; + /// Marks the two dimension that are orthogonal + pub fn orthogonalComponents(self: Neighbor) Vec3i { + const arr = [_]Vec3i{ + .{1, 1, 0}, + .{1, 1, 0}, + .{0, 1, 1}, + .{0, 1, 1}, + .{1, 0, 1}, + .{1, 0, 1}, + }; + return arr[@intFromEnum(self)]; + } + pub fn textureX(self: Neighbor) Vec3i { + const arr = [_]Vec3i{ + .{-1, 0, 0}, + .{1, 0, 0}, + .{0, 1, 0}, + .{0, -1, 0}, + .{-1, 0, 0}, + .{1, 0, 0}, + }; + return arr[@intFromEnum(self)]; + } + pub fn textureY(self: Neighbor) Vec3i { + const arr = [_]Vec3i{ + .{0, -1, 0}, + .{0, -1, 0}, + .{0, 0, 1}, + .{0, 0, 1}, + .{0, 0, 1}, + .{0, 0, 1}, + }; + return arr[@intFromEnum(self)]; + } + + pub inline fn reverse(self: Neighbor) Neighbor { + return @enumFromInt(@intFromEnum(self) ^ 1); + } + + pub inline fn isPositive(self: Neighbor) bool { + return @intFromEnum(self) & 1 == 0; + } + const VectorComponentEnum = enum(u2) {x = 0, y = 1, z = 2}; + pub fn vectorComponent(self: Neighbor) VectorComponentEnum { + const arr = [_]VectorComponentEnum{.z, .z, .x, .x, .y, .y}; + return arr[@intFromEnum(self)]; + } + + pub fn extractDirectionComponent(self: Neighbor, in: anytype) @TypeOf(in[0]) { + switch(self) { + inline else => |val| { + return in[@intFromEnum(comptime val.vectorComponent())]; + }, + } + } + + // Returns the neighbor that is rotated by 90 degrees counterclockwise around the z axis. + pub inline fn rotateZ(self: Neighbor) Neighbor { + const arr = [_]Neighbor{.dirUp, .dirDown, .dirPosY, .dirNegY, .dirNegX, .dirPosX}; + return arr[@intFromEnum(self)]; + } +}; + +var memoryPool: main.heap.MemoryPool(Chunk) = undefined; +var serverPool: main.heap.MemoryPool(ServerChunk) = undefined; + +pub fn init() void { + memoryPool = .init(main.globalAllocator); + serverPool = .init(main.globalAllocator); +} + +pub fn deinit() void { + memoryPool.deinit(); + serverPool.deinit(); +} + +pub const ChunkPosition = struct { // MARK: ChunkPosition + wx: i32, + wy: i32, + wz: i32, + voxelSize: u31, + + pub fn initFromWorldPos(pos: Vec3i, voxelSize: u31) ChunkPosition { + const mask = ~@as(i32, voxelSize*chunkSize - 1); + return .{.wx = pos[0] & mask, .wy = pos[1] & mask, .wz = pos[2] & mask, .voxelSize = voxelSize}; + } + + pub fn hashCode(self: ChunkPosition) u32 { + const shift: u5 = @truncate(@min(@ctz(self.wx), @ctz(self.wy), @ctz(self.wz))); + return (((@as(u32, @bitCast(self.wx)) >> shift)*%31 +% (@as(u32, @bitCast(self.wy)) >> shift))*%31 +% (@as(u32, @bitCast(self.wz)) >> shift))*%31 +% self.voxelSize; // TODO: Can I use one of zigs standard hash functions? + } + + pub fn equals(self: ChunkPosition, other: anytype) bool { + if(@typeInfo(@TypeOf(other)) == .optional) { + if(other) |notNull| { + return self.equals(notNull); + } + return false; + } else if(@TypeOf(other) == ChunkPosition) { + return self.wx == other.wx and self.wy == other.wy and self.wz == other.wz and self.voxelSize == other.voxelSize; + } else if(@TypeOf(other.*) == ServerChunk) { + return self.wx == other.super.pos.wx and self.wy == other.super.pos.wy and self.wz == other.super.pos.wz and self.voxelSize == other.super.pos.voxelSize; + } else if(@typeInfo(@TypeOf(other)) == .pointer) { + return self.wx == other.pos.wx and self.wy == other.pos.wy and self.wz == other.pos.wz and self.voxelSize == other.pos.voxelSize; + } else @compileError("Unsupported"); + } + + pub fn getMinDistanceSquared(self: ChunkPosition, playerPosition: Vec3i) i64 { + const halfWidth: i32 = self.voxelSize*@divExact(chunkSize, 2); + var dx: i64 = @abs(self.wx +% halfWidth -% playerPosition[0]); + var dy: i64 = @abs(self.wy +% halfWidth -% playerPosition[1]); + var dz: i64 = @abs(self.wz +% halfWidth -% playerPosition[2]); + dx = @max(0, dx - halfWidth); + dy = @max(0, dy - halfWidth); + dz = @max(0, dz - halfWidth); + return dx*dx + dy*dy + dz*dz; + } + + fn getMinDistanceSquaredFloat(self: ChunkPosition, playerPosition: Vec3d) f64 { + const adjustedPosition = @mod(playerPosition + @as(Vec3d, @splat(1 << 31)), @as(Vec3d, @splat(1 << 32))) - @as(Vec3d, @splat(1 << 31)); + const halfWidth: f64 = @floatFromInt(self.voxelSize*@divExact(chunkSize, 2)); + var dx = @abs(@as(f64, @floatFromInt(self.wx)) + halfWidth - adjustedPosition[0]); + var dy = @abs(@as(f64, @floatFromInt(self.wy)) + halfWidth - adjustedPosition[1]); + var dz = @abs(@as(f64, @floatFromInt(self.wz)) + halfWidth - adjustedPosition[2]); + dx = @max(0, dx - halfWidth); + dy = @max(0, dy - halfWidth); + dz = @max(0, dz - halfWidth); + return dx*dx + dy*dy + dz*dz; + } + + pub fn getMaxDistanceSquared(self: ChunkPosition, playerPosition: Vec3d) f64 { + const adjustedPosition = @mod(playerPosition + @as(Vec3d, @splat(1 << 31)), @as(Vec3d, @splat(1 << 32))) - @as(Vec3d, @splat(1 << 31)); + const halfWidth: f64 = @floatFromInt(self.voxelSize*@divExact(chunkSize, 2)); + var dx = @abs(@as(f64, @floatFromInt(self.wx)) + halfWidth - adjustedPosition[0]); + var dy = @abs(@as(f64, @floatFromInt(self.wy)) + halfWidth - adjustedPosition[1]); + var dz = @abs(@as(f64, @floatFromInt(self.wz)) + halfWidth - adjustedPosition[2]); + dx = dx + halfWidth; + dy = dy + halfWidth; + dz = dz + halfWidth; + return dx*dx + dy*dy + dz*dz; + } + + pub fn getCenterDistanceSquared(self: ChunkPosition, playerPosition: Vec3d) f64 { + const adjustedPosition = @mod(playerPosition + @as(Vec3d, @splat(1 << 31)), @as(Vec3d, @splat(1 << 32))) - @as(Vec3d, @splat(1 << 31)); + const halfWidth: f64 = @floatFromInt(self.voxelSize*@divExact(chunkSize, 2)); + const dx = @as(f64, @floatFromInt(self.wx)) + halfWidth - adjustedPosition[0]; + const dy = @as(f64, @floatFromInt(self.wy)) + halfWidth - adjustedPosition[1]; + const dz = @as(f64, @floatFromInt(self.wz)) + halfWidth - adjustedPosition[2]; + return dx*dx + dy*dy + dz*dz; + } + + pub fn getPriority(self: ChunkPosition, playerPos: Vec3d) f32 { + return -@as(f32, @floatCast(self.getMinDistanceSquaredFloat(playerPos)))/@as(f32, @floatFromInt(self.voxelSize*self.voxelSize)) + 2*@as(f32, @floatFromInt(std.math.log2_int(u31, self.voxelSize)*chunkSize*chunkSize)); + } +}; + +/// Position of a block in chunk relative coordinates, 1 unit is equivalent to the voxel size of the chunk. +pub const BlockPos = packed struct(u15) { // MARK: BlockPos + z: u5, + y: u5, + x: u5, + + pub fn fromCoords(x: u5, y: u5, z: u5) BlockPos { + return .{ + .x = x, + .y = y, + .z = z, + }; + } + + pub fn fromWorldCoords(wx: i32, wy: i32, wz: i32) BlockPos { + return .{ + .x = @intCast(wx & chunkMask), + .y = @intCast(wy & chunkMask), + .z = @intCast(wz & chunkMask), + }; + } + + pub fn fromLodCoords(_x: i32, _y: i32, _z: i32, voxelSizeShift: u5) BlockPos { + const x = _x >> voxelSizeShift; + const y = _y >> voxelSizeShift; + const z = _z >> voxelSizeShift; + return .fromCoords(@intCast(x), @intCast(y), @intCast(z)); + } + + pub fn fromIndex(index: u15) BlockPos { + return @bitCast(index); + } + + pub fn toIndex(self: BlockPos) u15 { + return @bitCast(self); + } + + pub fn neighbor(self: BlockPos, n: Neighbor) struct {BlockPos, enum {inSameChunk, inNeighborChunk}} { + const result: BlockPos, const isInNeighborChunk: bool = switch(n) { + .dirUp => .{.fromCoords(self.x, self.y, self.z +% 1), self.z == chunkMask}, + .dirDown => .{.fromCoords(self.x, self.y, self.z -% 1), self.z == 0}, + .dirPosY => .{.fromCoords(self.x, self.y +% 1, self.z), self.y == chunkMask}, + .dirNegY => .{.fromCoords(self.x, self.y -% 1, self.z), self.y == 0}, + .dirPosX => .{.fromCoords(self.x +% 1, self.y, self.z), self.x == chunkMask}, + .dirNegX => .{.fromCoords(self.x -% 1, self.y, self.z), self.x == 0}, + }; + return .{result, if(isInNeighborChunk) .inNeighborChunk else .inSameChunk}; + } +}; + +pub const Chunk = struct { // MARK: Chunk + pos: ChunkPosition, + data: main.utils.PaletteCompressedRegion(Block, chunkVolume) = undefined, + + width: u31, + voxelSizeShift: u5, + voxelSizeMask: i32, + widthShift: u5, + + blockPosToEntityDataMap: std.AutoHashMapUnmanaged(BlockPos, main.block_entity.BlockEntityIndex), + blockPosToEntityDataMapMutex: std.Thread.Mutex, + + pub fn init(pos: ChunkPosition) *Chunk { + const self = memoryPool.create(); + std.debug.assert((pos.voxelSize - 1 & pos.voxelSize) == 0); + std.debug.assert(@mod(pos.wx, pos.voxelSize) == 0 and @mod(pos.wy, pos.voxelSize) == 0 and @mod(pos.wz, pos.voxelSize) == 0); + const voxelSizeShift: u5 = @intCast(std.math.log2_int(u31, pos.voxelSize)); + self.* = Chunk{ + .pos = pos, + .width = pos.voxelSize*chunkSize, + .voxelSizeShift = voxelSizeShift, + .voxelSizeMask = pos.voxelSize - 1, + .widthShift = voxelSizeShift + chunkShift, + .blockPosToEntityDataMap = .{}, + .blockPosToEntityDataMapMutex = .{}, + }; + self.data.init(); + return self; + } + + pub fn deinit(self: *Chunk) void { + self.deinitContent(); + memoryPool.destroy(@alignCast(self)); + } + + fn deinitContent(self: *Chunk) void { + std.debug.assert(self.blockPosToEntityDataMap.count() == 0); + self.blockPosToEntityDataMap.deinit(main.globalAllocator.allocator); + self.data.deferredDeinit(); + } + + pub fn unloadBlockEntities(self: *Chunk, comptime side: main.utils.Side) void { + self.blockPosToEntityDataMapMutex.lock(); + defer self.blockPosToEntityDataMapMutex.unlock(); + var iterator = self.blockPosToEntityDataMap.iterator(); + while(iterator.next()) |elem| { + const pos = elem.key_ptr.*; + const entityDataIndex = elem.value_ptr.*; + const block = self.data.getValue(pos.toIndex()); + const blockEntity = block.blockEntity() orelse unreachable; + switch(side) { + .client => { + blockEntity.onUnloadClient(entityDataIndex); + }, + .server => { + blockEntity.onUnloadServer(entityDataIndex); + }, + } + } + self.blockPosToEntityDataMap.clearRetainingCapacity(); + } + + /// Updates a block if it is inside this chunk. + /// Does not do any bound checks. They are expected to be done with the `liesInChunk` function. + pub fn updateBlock(self: *Chunk, x: i32, y: i32, z: i32, newBlock: Block) void { + const pos = BlockPos.fromLodCoords(x, y, z, self.voxelSizeShift); + self.data.setValue(pos.toIndex(), newBlock); + } + + /// Gets a block if it is inside this chunk. + /// Does not do any bound checks. They are expected to be done with the `liesInChunk` function. + pub fn getBlock(self: *const Chunk, x: i32, y: i32, z: i32) Block { + const pos = BlockPos.fromLodCoords(x, y, z, self.voxelSizeShift); + return self.data.getValue(pos.toIndex()); + } + + /// Checks if the given relative coordinates lie within the bounds of this chunk. + pub fn liesInChunk(self: *const Chunk, x: i32, y: i32, z: i32) bool { + return x >= 0 and x < self.width and y >= 0 and y < self.width and z >= 0 and z < self.width; + } + + pub fn getLocalBlockPos(self: *const Chunk, worldPos: Vec3i) BlockPos { + return .fromLodCoords( + (worldPos[0] - self.pos.wx), + (worldPos[1] - self.pos.wy), + (worldPos[2] - self.pos.wz), + self.voxelSizeShift, + ); + } + + pub fn localToGlobalPosition(self: *const Chunk, pos: BlockPos) Vec3i { + return .{ + (@as(i32, pos.x) << self.voxelSizeShift) + self.pos.wx, + (@as(i32, pos.y) << self.voxelSizeShift) + self.pos.wy, + (@as(i32, pos.z) << self.voxelSizeShift) + self.pos.wz, + }; + } +}; + +pub const ServerChunk = struct { // MARK: ServerChunk + super: Chunk, + + wasChanged: bool = false, + generated: bool = false, + wasStored: bool = false, + shouldStoreNeighbors: bool = false, + + mutex: std.Thread.Mutex = .{}, + refCount: std.atomic.Value(u16), + + pub fn initAndIncreaseRefCount(pos: ChunkPosition) *ServerChunk { + const self = serverPool.create(); + std.debug.assert((pos.voxelSize - 1 & pos.voxelSize) == 0); + std.debug.assert(@mod(pos.wx, pos.voxelSize) == 0 and @mod(pos.wy, pos.voxelSize) == 0 and @mod(pos.wz, pos.voxelSize) == 0); + const voxelSizeShift: u5 = @intCast(std.math.log2_int(u31, pos.voxelSize)); + self.* = ServerChunk{ + .super = .{ + .pos = pos, + .width = pos.voxelSize*chunkSize, + .voxelSizeShift = voxelSizeShift, + .voxelSizeMask = pos.voxelSize - 1, + .widthShift = voxelSizeShift + chunkShift, + .blockPosToEntityDataMap = .{}, + .blockPosToEntityDataMapMutex = .{}, + }, + .refCount = .init(1), + }; + self.super.data.init(); + return self; + } + + pub fn deinit(self: *ServerChunk) void { + std.debug.assert(self.refCount.raw == 0); + if(self.wasChanged) { + self.save(main.server.world.?); + } + self.super.unloadBlockEntities(.server); + self.super.deinitContent(); + serverPool.destroy(@alignCast(self)); + } + + pub fn setChanged(self: *ServerChunk) void { + main.utils.assertLocked(&self.mutex); + if(!self.wasChanged) { + self.wasChanged = true; + self.increaseRefCount(); + main.server.world.?.queueChunkUpdateAndDecreaseRefCount(self); + } + } + + pub fn increaseRefCount(self: *ServerChunk) void { + const prevVal = self.refCount.fetchAdd(1, .monotonic); + std.debug.assert(prevVal != 0); + } + + pub fn decreaseRefCount(self: *ServerChunk) void { + const prevVal = self.refCount.fetchSub(1, .monotonic); + std.debug.assert(prevVal != 0); + if(prevVal == 1) { + self.deinit(); + } + } + + /// Checks if the given relative coordinates lie within the bounds of this chunk. + pub fn liesInChunk(self: *const ServerChunk, x: i32, y: i32, z: i32) bool { + return self.super.liesInChunk(x, y, z); + } + + /// This is useful to convert for loops to work for reduced resolution: + /// Instead of using + /// for(int x = start; x < end; x++) + /// for(int x = chunk.startIndex(start); x < end; x += chunk.getVoxelSize()) + /// should be used to only activate those voxels that are used in Cubyz's downscaling technique. + pub fn startIndex(self: *const ServerChunk, start: i32) i32 { + return start + self.super.voxelSizeMask & ~self.super.voxelSizeMask; // Rounds up to the nearest valid voxel coordinate. + } + + /// Gets a block if it is inside this chunk. + /// Does not do any bound checks. They are expected to be done with the `liesInChunk` function. + pub fn getBlock(self: *const ServerChunk, x: i32, y: i32, z: i32) Block { + main.utils.assertLocked(&self.mutex); + const pos = BlockPos.fromLodCoords(x, y, z, self.super.voxelSizeShift); + return self.super.data.getValue(pos.toIndex()); + } + + /// Updates a block if it is inside this chunk. + /// Does not do any bound checks. They are expected to be done with the `liesInChunk` function. + pub fn updateBlockAndSetChanged(self: *ServerChunk, x: i32, y: i32, z: i32, newBlock: Block) void { + main.utils.assertLocked(&self.mutex); + const pos = BlockPos.fromLodCoords(x, y, z, self.super.voxelSizeShift); + self.super.data.setValue(pos.toIndex(), newBlock); + self.shouldStoreNeighbors = true; + self.setChanged(); + } + + /// Updates a block if current value is air or the current block is degradable. + /// Does not do any bound checks. They are expected to be done with the `liesInChunk` function. + pub fn updateBlockIfDegradable(self: *ServerChunk, x: i32, y: i32, z: i32, newBlock: Block) void { + main.utils.assertLocked(&self.mutex); + const pos = BlockPos.fromLodCoords(x, y, z, self.super.voxelSizeShift); + const oldBlock = self.super.data.getValue(pos.toIndex()); + if(oldBlock.typ == 0 or oldBlock.degradable()) { + self.super.data.setValue(pos.toIndex(), newBlock); + } + } + + /// Updates a block if it is inside this chunk. Should be used in generation to prevent accidently storing these as changes. + /// Does not do any bound checks. They are expected to be done with the `liesInChunk` function. + pub fn updateBlockInGeneration(self: *ServerChunk, x: i32, y: i32, z: i32, newBlock: Block) void { + main.utils.assertLocked(&self.mutex); + const pos = BlockPos.fromLodCoords(x, y, z, self.super.voxelSizeShift); + self.super.data.setValue(pos.toIndex(), newBlock); + } + + /// Updates a block if it is inside this chunk. Should be used in generation to prevent accidently storing these as changes. + /// Does not do any bound checks. They are expected to be done with the `liesInChunk` function. + pub fn updateBlockColumnInGeneration(self: *ServerChunk, x: i32, y: i32, zStartInclusive: i32, zEndInclusive: i32, newBlock: Block) void { + std.debug.assert(zStartInclusive <= zEndInclusive); + main.utils.assertLocked(&self.mutex); + const posStart = BlockPos.fromLodCoords(x, y, zStartInclusive, self.super.voxelSizeShift); + const posEnd = BlockPos.fromLodCoords(x, y, zEndInclusive, self.super.voxelSizeShift); + self.super.data.setValueInColumn(posStart.toIndex(), @as(usize, posEnd.toIndex()) + 1, newBlock); + } + + pub fn updateFromLowerResolution(self: *ServerChunk, other: *ServerChunk) void { + const xOffset = if(other.super.pos.wx != self.super.pos.wx) chunkSize/2 else 0; // Offsets of the lower resolution chunk in this chunk. + const yOffset = if(other.super.pos.wy != self.super.pos.wy) chunkSize/2 else 0; + const zOffset = if(other.super.pos.wz != self.super.pos.wz) chunkSize/2 else 0; + self.mutex.lock(); + defer self.mutex.unlock(); + main.utils.assertLocked(&other.mutex); + + var x: u31 = 0; + while(x < chunkSize/2) : (x += 1) { + var y: u31 = 0; + while(y < chunkSize/2) : (y += 1) { + var z: u31 = 0; + while(z < chunkSize/2) : (z += 1) { + // Count the neighbors for each subblock. An transparent block counts 5. A chunk border(unknown block) only counts 1. + var neighborCount: [8]u31 = undefined; + var octantBlocks: [8]Block = undefined; + var maxCount: i32 = 0; + var dx: u31 = 0; + while(dx <= 1) : (dx += 1) { + var dy: u31 = 0; + while(dy <= 1) : (dy += 1) { + var dz: u31 = 0; + while(dz <= 1) : (dz += 1) { + const pos = BlockPos.fromCoords(@intCast(x*2 + dx), @intCast(y*2 + dy), @intCast(z*2 + dz)); + const i = dx*4 + dz*2 + dy; + octantBlocks[i] = other.super.data.getValue(pos.toIndex()); + octantBlocks[i].typ = octantBlocks[i].lodReplacement(); + if(octantBlocks[i].typ == 0) { + neighborCount[i] = 0; + continue; // I don't care about air blocks. + } + + var count: u31 = 0; + for(Neighbor.iterable) |n| { + const neighborPos, const chunkLocation = pos.neighbor(n); + if(chunkLocation == .inSameChunk) { + if(other.super.data.getValue(neighborPos.toIndex()).transparent()) { + count += 5; + } + } else { + count += 1; + } + } + maxCount = @max(maxCount, count); + neighborCount[i] = count; + } + } + } + // Uses a specific permutation here that keeps high resolution patterns in lower resolution. + const permutationStart = (x & 1)*4 + (z & 1)*2 + (y & 1); + var block = Block{.typ = 0, .data = 0}; + for(0..8) |i| { + const appliedPermutation = permutationStart ^ i; + if(neighborCount[appliedPermutation] >= maxCount - 1) { // Avoid pattern breaks at chunk borders. + block = octantBlocks[appliedPermutation]; + } + } + // Update the block: + const thisPos = BlockPos.fromCoords(@intCast(x + xOffset), @intCast(y + yOffset), @intCast(z + zOffset)); + self.super.data.setValue(thisPos.toIndex(), block); + } + } + } + + self.setChanged(); + } + + pub fn save(self: *ServerChunk, world: *main.server.ServerWorld) void { + self.mutex.lock(); + defer self.mutex.unlock(); + if(self.shouldStoreNeighbors and self.super.pos.voxelSize == 1) { + // Store all the neighbor chunks as well: + self.mutex.unlock(); + defer self.mutex.lock(); + var dx: i32 = -@as(i32, chunkSize); + while(dx <= chunkSize) : (dx += chunkSize) { + var dy: i32 = -@as(i32, chunkSize); + while(dy <= chunkSize) : (dy += chunkSize) { + var dz: i32 = -@as(i32, chunkSize); + while(dz <= chunkSize) : (dz += chunkSize) { + if(dx == 0 and dy == 0 and dz == 0) continue; + const ch = main.server.world.?.getOrGenerateChunkAndIncreaseRefCount(.{ + .wx = self.super.pos.wx +% dx, + .wy = self.super.pos.wy +% dy, + .wz = self.super.pos.wz +% dz, + .voxelSize = 1, + }); + defer ch.decreaseRefCount(); + ch.mutex.lock(); + defer ch.mutex.unlock(); + if(!ch.wasStored) { + ch.setChanged(); + } + } + } + } + } + if(!self.wasStored and self.super.pos.voxelSize == 1) { + // Store the surrounding map pieces as well: + self.mutex.unlock(); + defer self.mutex.lock(); + const mapStartX = self.super.pos.wx -% main.server.terrain.SurfaceMap.MapFragment.mapSize/2 & ~@as(i32, main.server.terrain.SurfaceMap.MapFragment.mapMask); + const mapStartY = self.super.pos.wy -% main.server.terrain.SurfaceMap.MapFragment.mapSize/2 & ~@as(i32, main.server.terrain.SurfaceMap.MapFragment.mapMask); + for(0..2) |dx| { + for(0..2) |dy| { + const mapX = mapStartX +% main.server.terrain.SurfaceMap.MapFragment.mapSize*@as(i32, @intCast(dx)); + const mapY = mapStartY +% main.server.terrain.SurfaceMap.MapFragment.mapSize*@as(i32, @intCast(dy)); + const map = main.server.terrain.SurfaceMap.getOrGenerateFragment(mapX, mapY, self.super.pos.voxelSize); + if(!map.wasStored.swap(true, .monotonic)) { + map.save(null, .{}); + } + } + } + } + self.wasStored = true; + if(self.wasChanged) { + const pos = self.super.pos; + const regionSize = pos.voxelSize*chunkSize*main.server.storage.RegionFile.regionSize; + const regionMask: i32 = regionSize - 1; + const region = main.server.storage.loadRegionFileAndIncreaseRefCount(pos.wx & ~regionMask, pos.wy & ~regionMask, pos.wz & ~regionMask, pos.voxelSize); + defer region.decreaseRefCount(); + const data = main.server.storage.ChunkCompression.storeChunk(main.stackAllocator, &self.super, .toDisk, false); + defer main.stackAllocator.free(data); + region.storeChunk( + data, + @as(usize, @intCast(pos.wx -% region.pos.wx))/pos.voxelSize/chunkSize, + @as(usize, @intCast(pos.wy -% region.pos.wy))/pos.voxelSize/chunkSize, + @as(usize, @intCast(pos.wz -% region.pos.wz))/pos.voxelSize/chunkSize, + ); + + self.wasChanged = false; + // Update the next lod chunk: + if(pos.voxelSize != 1 << settings.highestSupportedLod) { + var nextPos = pos; + nextPos.wx &= ~@as(i32, pos.voxelSize*chunkSize); + nextPos.wy &= ~@as(i32, pos.voxelSize*chunkSize); + nextPos.wz &= ~@as(i32, pos.voxelSize*chunkSize); + nextPos.voxelSize *= 2; + const nextHigherLod = world.getOrGenerateChunkAndIncreaseRefCount(nextPos); + defer nextHigherLod.decreaseRefCount(); + nextHigherLod.updateFromLowerResolution(self); + } + } + } +}; diff --git a/entity.zig b/entity.zig new file mode 100644 index 0000000000..319bd47bbd --- /dev/null +++ b/entity.zig @@ -0,0 +1,281 @@ +const std = @import("std"); + +const chunk = @import("chunk.zig"); +const game = @import("game.zig"); +const graphics = @import("graphics.zig"); +const c = graphics.c; +const ZonElement = @import("zon.zig").ZonElement; +const main = @import("main"); +const renderer = @import("renderer.zig"); +const settings = @import("settings.zig"); +const utils = @import("utils.zig"); +const vec = @import("vec.zig"); +const Mat4f = vec.Mat4f; +const Vec3d = vec.Vec3d; +const Vec3f = vec.Vec3f; +const Vec4f = vec.Vec4f; +const NeverFailingAllocator = main.heap.NeverFailingAllocator; + +const BinaryReader = main.utils.BinaryReader; + +pub const EntityNetworkData = struct { + id: u32, + pos: Vec3d, + vel: Vec3d, + rot: Vec3f, +}; + +pub const ClientEntity = struct { + interpolatedValues: utils.GenericInterpolation(6) = undefined, + _interpolationPos: [6]f64 = undefined, + _interpolationVel: [6]f64 = undefined, + + width: f64, + height: f64, + + pos: Vec3d = undefined, + rot: Vec3f = undefined, + + id: u32, + name: []const u8, + + pub fn init(self: *ClientEntity, zon: ZonElement, allocator: NeverFailingAllocator) void { + self.* = ClientEntity{ + .id = zon.get(u32, "id", std.math.maxInt(u32)), + .width = zon.get(f64, "width", 1), + .height = zon.get(f64, "height", 1), + .name = allocator.dupe(u8, zon.get([]const u8, "name", "")), + }; + self._interpolationPos = [_]f64{ + self.pos[0], + self.pos[1], + self.pos[2], + @floatCast(self.rot[0]), + @floatCast(self.rot[1]), + @floatCast(self.rot[2]), + }; + self._interpolationVel = @splat(0); + self.interpolatedValues.init(&self._interpolationPos, &self._interpolationVel); + } + + pub fn deinit(self: ClientEntity, allocator: NeverFailingAllocator) void { + allocator.free(self.name); + } + + pub fn getRenderPosition(self: *const ClientEntity) Vec3d { + return Vec3d{self.pos[0], self.pos[1], self.pos[2]}; + } + + pub fn updatePosition(self: *ClientEntity, pos: *const [6]f64, vel: *const [6]f64, time: i16) void { + self.interpolatedValues.updatePosition(pos, vel, time); + } + + pub fn update(self: *ClientEntity, time: i16, lastTime: i16) void { + self.interpolatedValues.update(time, lastTime); + self.pos[0] = self.interpolatedValues.outPos[0]; + self.pos[1] = self.interpolatedValues.outPos[1]; + self.pos[2] = self.interpolatedValues.outPos[2]; + self.rot[0] = @floatCast(self.interpolatedValues.outPos[3]); + self.rot[1] = @floatCast(self.interpolatedValues.outPos[4]); + self.rot[2] = @floatCast(self.interpolatedValues.outPos[5]); + } +}; + +pub const ClientEntityManager = struct { + var lastTime: i16 = 0; + var timeDifference: utils.TimeDifference = utils.TimeDifference{}; + var uniforms: struct { + projectionMatrix: c_int, + viewMatrix: c_int, + light: c_int, + contrast: c_int, + ambientLight: c_int, + } = undefined; + var modelBuffer: main.graphics.SSBO = undefined; + var modelSize: c_int = 0; + var modelTexture: main.graphics.Texture = undefined; + var pipeline: graphics.Pipeline = undefined; // Entities are sometimes small and sometimes big. Therefor it would mean a lot of work to still use smooth lighting. Therefor the non-smooth shader is used for those. + pub var entities: main.utils.VirtualList(ClientEntity, 1 << 20) = undefined; + pub var mutex: std.Thread.Mutex = .{}; + + pub fn init() void { + entities = .init(); + pipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/entity_vertex.vert", + "assets/cubyz/shaders/entity_fragment.frag", + "", + &uniforms, + .{}, + .{.depthTest = true}, + .{.attachments = &.{.alphaBlending}}, + ); + + modelTexture = main.graphics.Texture.initFromFile("assets/cubyz/entity/textures/snale.png"); + const modelFile = main.files.cwd().read(main.stackAllocator, "assets/cubyz/entity/models/snale.obj") catch |err| blk: { + std.log.err("Error while reading player model: {s}", .{@errorName(err)}); + break :blk &.{}; + }; + defer main.stackAllocator.free(modelFile); + const quadInfos = main.models.Model.loadRawModelDataFromObj(main.stackAllocator, modelFile); + defer main.stackAllocator.free(quadInfos); + modelBuffer = .initStatic(main.models.QuadInfo, quadInfos); + modelBuffer.bind(11); + modelSize = @intCast(quadInfos.len); + } + + pub fn deinit() void { + for(entities.items()) |ent| { + ent.deinit(main.globalAllocator); + } + entities.deinit(); + pipeline.deinit(); + } + + pub fn clear() void { + for(entities.items()) |ent| { + ent.deinit(main.globalAllocator); + } + entities.clearRetainingCapacity(); + timeDifference = utils.TimeDifference{}; + } + + fn update() void { + main.utils.assertLocked(&mutex); + var time: i16 = @truncate(main.timestamp().toMilliseconds() -% settings.entityLookback); + time -%= timeDifference.difference.load(.monotonic); + for(entities.items()) |*ent| { + ent.update(time, lastTime); + } + lastTime = time; + } + + pub fn renderNames(projMatrix: Mat4f, playerPos: Vec3d) void { + mutex.lock(); + defer mutex.unlock(); + + const screenUnits = @as(f32, @floatFromInt(main.Window.height))/1024; + const fontBaseSize = 128.0; + const fontMinScreenSize = 16.0; + const fontScreenSize = fontBaseSize*screenUnits; + + for(entities.items()) |ent| { + if(ent.id == game.Player.id or ent.name.len == 0) continue; // don't render local player + const pos3d = ent.getRenderPosition() - playerPos; + const pos4f = Vec4f{ + @floatCast(pos3d[0]), + @floatCast(pos3d[1]), + @floatCast(pos3d[2] + 1.1), + 1, + }; + + const rotatedPos = game.camera.viewMatrix.mulVec(pos4f); + const projectedPos = projMatrix.mulVec(rotatedPos); + if(projectedPos[2] < 0) continue; + const xCenter = (1 + projectedPos[0]/projectedPos[3])*@as(f32, @floatFromInt(main.Window.width/2)); + const yCenter = (1 - projectedPos[1]/projectedPos[3])*@as(f32, @floatFromInt(main.Window.height/2)); + + const transparency = 38.0*std.math.log10(vec.lengthSquare(pos3d) + 1) - 80.0; + const alpha: u32 = @intFromFloat(std.math.clamp(0xff - transparency, 0, 0xff)); + graphics.draw.setColor(alpha << 24); + + var buf = graphics.TextBuffer.init(main.stackAllocator, ent.name, .{.color = 0xffffff}, false, .center); + defer buf.deinit(); + const fontSize = std.mem.max(f32, &.{fontMinScreenSize, fontScreenSize/projectedPos[3]}); + const size = buf.calculateLineBreaks(fontSize, @floatFromInt(main.Window.width*8)); + buf.render(xCenter - size[0]/2, yCenter - size[1], fontSize); + } + } + + pub fn render(projMatrix: Mat4f, ambientLight: Vec3f, playerPos: Vec3d) void { + mutex.lock(); + defer mutex.unlock(); + update(); + pipeline.bind(null); + c.glBindVertexArray(main.renderer.chunk_meshing.vao); + c.glUniformMatrix4fv(uniforms.projectionMatrix, 1, c.GL_TRUE, @ptrCast(&projMatrix)); + modelTexture.bindTo(0); + c.glUniform3fv(uniforms.ambientLight, 1, @ptrCast(&ambientLight)); + c.glUniform1f(uniforms.contrast, 0.12); + + for(entities.items()) |ent| { + if(ent.id == game.Player.id) continue; // don't render local player + + const blockPos: vec.Vec3i = @intFromFloat(@floor(ent.pos)); + const lightVals: [6]u8 = main.renderer.mesh_storage.getLight(blockPos[0], blockPos[1], blockPos[2]) orelse @splat(0); + const light = (@as(u32, lightVals[0] >> 3) << 25 | + @as(u32, lightVals[1] >> 3) << 20 | + @as(u32, lightVals[2] >> 3) << 15 | + @as(u32, lightVals[3] >> 3) << 10 | + @as(u32, lightVals[4] >> 3) << 5 | + @as(u32, lightVals[5] >> 3) << 0); + + c.glUniform1ui(uniforms.light, @bitCast(@as(u32, light))); + + const pos: Vec3d = ent.getRenderPosition() - playerPos; + const modelMatrix = (Mat4f.identity() + .mul(Mat4f.translation(Vec3f{ + @floatCast(pos[0]), + @floatCast(pos[1]), + @floatCast(pos[2] - 1.0 + 0.09375), + })) + .mul(Mat4f.rotationZ(-ent.rot[2]))); + const modelViewMatrix = game.camera.viewMatrix.mul(modelMatrix); + c.glUniformMatrix4fv(uniforms.viewMatrix, 1, c.GL_TRUE, @ptrCast(&modelViewMatrix)); + c.glDrawElements(c.GL_TRIANGLES, 6*modelSize, c.GL_UNSIGNED_INT, null); + } + } + + pub fn addEntity(zon: ZonElement) void { + mutex.lock(); + defer mutex.unlock(); + var ent = entities.addOne(); + ent.init(zon, main.globalAllocator); + } + + pub fn removeEntity(id: u32) void { + mutex.lock(); + defer mutex.unlock(); + for(entities.items(), 0..) |*ent, i| { + if(ent.id == id) { + ent.deinit(main.globalAllocator); + _ = entities.swapRemove(i); + if(i != entities.len) { + entities.items()[i].interpolatedValues.outPos = &entities.items()[i]._interpolationPos; + entities.items()[i].interpolatedValues.outVel = &entities.items()[i]._interpolationVel; + } + break; + } + } + } + + pub fn serverUpdate(time: i16, entityData: []EntityNetworkData) void { + mutex.lock(); + defer mutex.unlock(); + timeDifference.addDataPoint(time); + + for(entityData) |data| { + const pos = [_]f64{ + data.pos[0], + data.pos[1], + data.pos[2], + @floatCast(data.rot[0]), + @floatCast(data.rot[1]), + @floatCast(data.rot[2]), + }; + const vel = [_]f64{ + data.vel[0], + data.vel[1], + data.vel[2], + 0, + 0, + 0, + }; + for(entities.items()) |*ent| { + if(ent.id == data.id) { + ent.updatePosition(&pos, &vel, time); + break; + } + } + } + } +}; diff --git a/files.zig b/files.zig new file mode 100644 index 0000000000..1c88bf35ac --- /dev/null +++ b/files.zig @@ -0,0 +1,166 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const main = @import("main"); +const NeverFailingAllocator = main.heap.NeverFailingAllocator; +const ZonElement = main.ZonElement; + +pub fn openDirInWindow(path: []const u8) void { + const newPath = main.stackAllocator.dupe(u8, path); + defer main.stackAllocator.free(newPath); + + if(builtin.os.tag == .windows) { + std.mem.replaceScalar(u8, newPath, '/', '\\'); + } + + const command = switch(builtin.os.tag) { + .windows => .{"explorer", newPath}, + .macos => .{"open", newPath}, + else => .{"xdg-open", newPath}, + }; + const result = std.process.Child.run(.{ + .allocator = main.stackAllocator.allocator, + .argv = &command, + }) catch |err| { + std.log.err("Got error while trying to open file explorer: {s}", .{@errorName(err)}); + return; + }; + defer { + main.stackAllocator.free(result.stderr); + main.stackAllocator.free(result.stdout); + } + if(result.stderr.len != 0) { + std.log.err("Got error while trying to open file explorer: {s}", .{result.stderr}); + } +} + +pub fn cwd() Dir { + return Dir{ + .dir = std.fs.cwd(), + }; +} + +var cubyzDir_: ?std.fs.Dir = null; +var cubyzDirStr_: []const u8 = "."; + +pub fn cubyzDir() Dir { + return .{ + .dir = cubyzDir_ orelse std.fs.cwd(), + }; +} + +pub fn cubyzDirStr() []const u8 { + return cubyzDirStr_; +} + +fn flawedInit() !void { + if(main.settings.launchConfig.cubyzDir.len != 0) { + cubyzDir_ = try std.fs.cwd().makeOpenPath(main.settings.launchConfig.cubyzDir, .{}); + cubyzDirStr_ = main.globalAllocator.dupe(u8, main.settings.launchConfig.cubyzDir); + return; + } + const homePath = try std.process.getEnvVarOwned(main.stackAllocator.allocator, if(builtin.os.tag == .windows) "USERPROFILE" else "HOME"); + defer main.stackAllocator.free(homePath); + var homeDir = try std.fs.openDirAbsolute(homePath, .{}); + defer homeDir.close(); + if(builtin.os.tag == .windows) { + cubyzDir_ = try homeDir.makeOpenPath("Saved Games/Cubyz", .{}); + cubyzDirStr_ = std.mem.concat(main.globalAllocator.allocator, u8, &.{homePath, "/Saved Games/Cubyz"}) catch unreachable; + } else { + cubyzDir_ = try homeDir.makeOpenPath(".cubyz", .{}); + cubyzDirStr_ = std.mem.concat(main.globalAllocator.allocator, u8, &.{homePath, "/.cubyz"}) catch unreachable; + } +} + +pub fn init() void { + flawedInit() catch |err| { + std.log.err("Error {s} while opening global Cubyz directory. Using working directory instead.", .{@errorName(err)}); + }; +} + +pub fn deinit() void { + if(cubyzDir_ != null) { + cubyzDir_.?.close(); + } + if(cubyzDirStr_.ptr != ".".ptr) { + main.globalAllocator.free(cubyzDirStr_); + } +} + +pub const Dir = struct { + dir: std.fs.Dir, + + pub fn init(dir: std.fs.Dir) Dir { + return .{.dir = dir}; + } + + pub fn close(self: *Dir) void { + self.dir.close(); + } + + pub fn read(self: Dir, allocator: NeverFailingAllocator, path: []const u8) ![]u8 { + return self.dir.readFileAlloc(path, allocator.allocator, .unlimited); + } + + pub fn readToZon(self: Dir, allocator: NeverFailingAllocator, path: []const u8) !ZonElement { + const string = try self.read(main.stackAllocator, path); + defer main.stackAllocator.free(string); + const realPath: ?[]const u8 = self.dir.realpathAlloc(main.stackAllocator.allocator, path) catch null; + defer if(realPath) |p| main.stackAllocator.free(p); + return ZonElement.parseFromString(allocator, realPath orelse path, string); + } + + pub fn write(self: Dir, path: []const u8, data: []const u8) !void { + return self.dir.writeFile(.{.data = data, .sub_path = path}); + } + + pub fn writeZon(self: Dir, path: []const u8, zon: ZonElement) !void { + const string = zon.toString(main.stackAllocator); + defer main.stackAllocator.free(string); + try self.write(path, string); + } + + pub fn hasFile(self: Dir, path: []const u8) bool { + const file = self.dir.openFile(path, .{}) catch return false; + file.close(); + return true; + } + + pub fn hasDir(self: Dir, path: []const u8) bool { + var dir = self.dir.openDir(path, .{.iterate = false}) catch return false; + dir.close(); + return true; + } + + pub fn openDir(self: Dir, path: []const u8) !Dir { + return .{.dir = try self.dir.makeOpenPath(path, .{})}; + } + + pub fn openIterableDir(self: Dir, path: []const u8) !Dir { + return .{.dir = try self.dir.makeOpenPath(path, .{.iterate = true})}; + } + + pub fn openFile(self: Dir, path: []const u8) !std.fs.File { + return self.dir.openFile(path, .{}); + } + + pub fn deleteTree(self: Dir, path: []const u8) !void { + try self.dir.deleteTree(path); + } + + pub fn deleteFile(self: Dir, path: []const u8) !void { + try self.dir.deleteFile(path); + } + + pub fn makePath(self: Dir, path: []const u8) !void { + try self.dir.makePath(path); + } + + pub fn walk(self: Dir, allocator: NeverFailingAllocator) std.fs.Dir.Walker { + return self.dir.walk(allocator.allocator) catch unreachable; + } + + pub fn iterate(self: Dir) std.fs.Dir.Iterator { + return self.dir.iterate(); + } +}; diff --git a/game.zig b/game.zig new file mode 100644 index 0000000000..56abba9732 --- /dev/null +++ b/game.zig @@ -0,0 +1,1005 @@ +const std = @import("std"); +const Atomic = std.atomic.Value; + +const assets = @import("assets.zig"); +const itemdrop = @import("itemdrop.zig"); +const ClientItemDropManager = itemdrop.ClientItemDropManager; +const items = @import("items.zig"); +const Inventory = items.Inventory; +const ZonElement = @import("zon.zig").ZonElement; +const main = @import("main"); +const network = @import("network.zig"); +const particles = @import("particles.zig"); +const Connection = network.Connection; +const ConnectionManager = network.ConnectionManager; +const vec = @import("vec.zig"); +const Vec2f = vec.Vec2f; +const Vec3i = vec.Vec3i; +const Vec3f = vec.Vec3f; +const Vec4f = vec.Vec4f; +const Vec3d = vec.Vec3d; +const Mat4f = vec.Mat4f; +const graphics = @import("graphics.zig"); +const Fog = graphics.Fog; +const renderer = @import("renderer.zig"); +const settings = @import("settings.zig"); +const Block = main.blocks.Block; +const physics = main.physics; +const KeyBoard = main.KeyBoard; + +pub const camera = struct { // MARK: camera + pub var rotation: Vec3f = Vec3f{0, 0, 0}; + pub var direction: Vec3f = Vec3f{0, 0, 0}; + pub var viewMatrix: Mat4f = Mat4f.identity(); + pub fn moveRotation(mouseX: f32, mouseY: f32) void { + // Mouse movement along the y-axis rotates the image along the x-axis. + rotation[0] += mouseY; + if(rotation[0] > std.math.pi/2.0) { + rotation[0] = std.math.pi/2.0; + } else if(rotation[0] < -std.math.pi/2.0) { + rotation[0] = -std.math.pi/2.0; + } + // Mouse movement along the x-axis rotates the image along the z-axis. + rotation[2] += mouseX; + } + + pub fn updateViewMatrix() void { + direction = vec.rotateZ(vec.rotateX(Vec3f{0, 1, 0}, -rotation[0]), -rotation[2]); + viewMatrix = Mat4f.identity().mul(Mat4f.rotationX(rotation[0])).mul(Mat4f.rotationZ(rotation[2])); + } +}; + +pub const collision = struct { + pub const Box = struct { + min: Vec3d, + max: Vec3d, + + pub fn center(self: Box) Vec3d { + return (self.min + self.max)*@as(Vec3d, @splat(0.5)); + } + + pub fn extent(self: Box) Vec3d { + return (self.max - self.min)*@as(Vec3d, @splat(0.5)); + } + + pub fn intersects(self: Box, other: Box) bool { + return @reduce(.And, (self.max > other.min)) and @reduce(.And, (self.min < other.max)); + } + }; + + const Direction = enum(u2) {x = 0, y = 1, z = 2}; + + pub fn collideWithBlock(block: main.blocks.Block, x: i32, y: i32, z: i32, entityPosition: Vec3d, entityBoundingBoxExtent: Vec3d, directionVector: Vec3d) ?struct {box: Box, dist: f64} { + var resultBox: ?Box = null; + var minDistance: f64 = std.math.floatMax(f64); + if(block.collide()) { + const model = block.mode().model(block).model(); + + const pos = Vec3d{@floatFromInt(x), @floatFromInt(y), @floatFromInt(z)}; + const entityCollision = Box{.min = entityPosition - entityBoundingBoxExtent, .max = entityPosition + entityBoundingBoxExtent}; + + for(model.collision) |relativeBlockCollision| { + const blockCollision = Box{.min = relativeBlockCollision.min + pos, .max = relativeBlockCollision.max + pos}; + if(blockCollision.intersects(entityCollision)) { + const dotMin = vec.dot(directionVector, blockCollision.min); + const dotMax = vec.dot(directionVector, blockCollision.max); + + const distance = @min(dotMin, dotMax); + + if(distance < minDistance) { + resultBox = blockCollision; + minDistance = distance; + } else if(distance == minDistance) { + resultBox = .{.min = @min(resultBox.?.min, blockCollision.min), .max = @max(resultBox.?.max, blockCollision.max)}; + } + } + } + } + return .{.box = resultBox orelse return null, .dist = minDistance}; + } + + pub fn collides(comptime side: main.utils.Side, dir: Direction, amount: f64, pos: Vec3d, hitBox: Box) ?Box { + var boundingBox: Box = .{ + .min = pos + hitBox.min, + .max = pos + hitBox.max, + }; + switch(dir) { + .x => { + if(amount < 0) boundingBox.min[0] += amount else boundingBox.max[0] += amount; + }, + .y => { + if(amount < 0) boundingBox.min[1] += amount else boundingBox.max[1] += amount; + }, + .z => { + if(amount < 0) boundingBox.min[2] += amount else boundingBox.max[2] += amount; + }, + } + const minX: i32 = @intFromFloat(@floor(boundingBox.min[0])); + const maxX: i32 = @intFromFloat(@floor(boundingBox.max[0])); + const minY: i32 = @intFromFloat(@floor(boundingBox.min[1])); + const maxY: i32 = @intFromFloat(@floor(boundingBox.max[1])); + const minZ: i32 = @intFromFloat(@floor(boundingBox.min[2])); + const maxZ: i32 = @intFromFloat(@floor(boundingBox.max[2])); + + const boundingBoxCenter = boundingBox.center(); + const fullBoundingBoxExtent = boundingBox.extent(); + + var resultBox: ?Box = null; + var minDistance: f64 = std.math.floatMax(f64); + const directionVector: Vec3d = switch(dir) { + .x => .{-std.math.sign(amount), 0, 0}, + .y => .{0, -std.math.sign(amount), 0}, + .z => .{0, 0, -std.math.sign(amount)}, + }; + + var x: i32 = minX; + while(x <= maxX) : (x += 1) { + var y: i32 = minY; + while(y <= maxY) : (y += 1) { + var z: i32 = maxZ; + while(z >= minZ) : (z -= 1) { + const _block = if(side == .client) main.renderer.mesh_storage.getBlockFromRenderThread(x, y, z) else main.server.world.?.getBlock(x, y, z); + if(_block) |block| { + if(collideWithBlock(block, x, y, z, boundingBoxCenter, fullBoundingBoxExtent, directionVector)) |res| { + if(res.dist < minDistance) { + resultBox = res.box; + minDistance = res.dist; + } else if(res.dist == minDistance) { + resultBox.?.min = @min(resultBox.?.min, res.box.min); + resultBox.?.max = @min(resultBox.?.max, res.box.max); + } + } + } + } + } + } + + return resultBox; + } + + const SurfaceProperties = struct { + friction: f32, + bounciness: f32, + }; + + pub fn calculateSurfaceProperties(comptime side: main.utils.Side, pos: Vec3d, hitBox: Box, defaultFriction: f32) SurfaceProperties { + const boundingBox: Box = .{ + .min = pos + hitBox.min, + .max = pos + hitBox.max, + }; + const minX: i32 = @intFromFloat(@floor(boundingBox.min[0])); + const maxX: i32 = @intFromFloat(@floor(boundingBox.max[0])); + const minY: i32 = @intFromFloat(@floor(boundingBox.min[1])); + const maxY: i32 = @intFromFloat(@floor(boundingBox.max[1])); + + const z: i32 = @intFromFloat(@floor(boundingBox.min[2] - 0.01)); + + var friction: f64 = 0; + var bounciness: f64 = 0; + var totalArea: f64 = 0; + + var x = minX; + while(x <= maxX) : (x += 1) { + var y = minY; + while(y <= maxY) : (y += 1) { + const _block = if(side == .client) main.renderer.mesh_storage.getBlockFromRenderThread(x, y, z) else main.server.world.?.getBlock(x, y, z); + + if(_block) |block| { + const blockPos: Vec3d = .{@floatFromInt(x), @floatFromInt(y), @floatFromInt(z)}; + + const blockBox: Box = .{ + .min = blockPos + @as(Vec3d, @floatCast(block.mode().model(block).model().min)), + .max = blockPos + @as(Vec3d, @floatCast(block.mode().model(block).model().max)), + }; + + if(boundingBox.min[2] > blockBox.max[2] or boundingBox.max[2] < blockBox.min[2]) { + continue; + } + + const max = std.math.clamp(vec.xy(blockBox.max), vec.xy(boundingBox.min), vec.xy(boundingBox.max)); + const min = std.math.clamp(vec.xy(blockBox.min), vec.xy(boundingBox.min), vec.xy(boundingBox.max)); + + const area = (max[0] - min[0])*(max[1] - min[1]); + + if(block.collide()) { + totalArea += area; + friction += area*@as(f64, @floatCast(block.friction())); + bounciness += area*@as(f64, @floatCast(block.bounciness())); + } + } + } + } + + if(totalArea == 0) { + friction = defaultFriction; + bounciness = 0.0; + } else { + friction = friction/totalArea; + bounciness = bounciness/totalArea; + } + + return .{ + .friction = @floatCast(friction), + .bounciness = @floatCast(bounciness), + }; + } + + const VolumeProperties = struct { + terminalVelocity: f64, + density: f64, + maxDensity: f64, + mobility: f64, + }; + + fn overlapVolume(a: Box, b: Box) f64 { + const min = @max(a.min, b.min); + const max = @min(a.max, b.max); + if(@reduce(.Or, min >= max)) return 0; + return @reduce(.Mul, max - min); + } + + pub fn calculateVolumeProperties(comptime side: main.utils.Side, pos: Vec3d, hitBox: Box, defaults: VolumeProperties) VolumeProperties { + const boundingBox: Box = .{ + .min = pos + hitBox.min, + .max = pos + hitBox.max, + }; + const minX: i32 = @intFromFloat(@floor(boundingBox.min[0])); + const maxX: i32 = @intFromFloat(@floor(boundingBox.max[0])); + const minY: i32 = @intFromFloat(@floor(boundingBox.min[1])); + const maxY: i32 = @intFromFloat(@floor(boundingBox.max[1])); + const minZ: i32 = @intFromFloat(@floor(boundingBox.min[2])); + const maxZ: i32 = @intFromFloat(@floor(boundingBox.max[2])); + + var invTerminalVelocitySum: f64 = 0; + var densitySum: f64 = 0; + var maxDensity: f64 = defaults.maxDensity; + var mobilitySum: f64 = 0; + var volumeSum: f64 = 0; + + var x: i32 = minX; + while(x <= maxX) : (x += 1) { + var y: i32 = minY; + while(y <= maxY) : (y += 1) { + var z: i32 = maxZ; + while(z >= minZ) : (z -= 1) { + const _block = if(side == .client) main.renderer.mesh_storage.getBlockFromRenderThread(x, y, z) else main.server.world.?.getBlock(x, y, z); + const totalBox: Box = .{ + .min = @floatFromInt(Vec3i{x, y, z}), + .max = @floatFromInt(Vec3i{x + 1, y + 1, z + 1}), + }; + const gridVolume = overlapVolume(boundingBox, totalBox); + volumeSum += gridVolume; + + if(_block) |block| { + const collisionBox: Box = .{ // TODO: Check all AABBs individually + .min = totalBox.min + main.blocks.meshes.model(block).model().min, + .max = totalBox.min + main.blocks.meshes.model(block).model().max, + }; + const filledVolume = @min(gridVolume, overlapVolume(collisionBox, totalBox)); + const emptyVolume = gridVolume - filledVolume; + invTerminalVelocitySum += emptyVolume/defaults.terminalVelocity; + densitySum += emptyVolume*defaults.density; + mobilitySum += emptyVolume*defaults.mobility; + invTerminalVelocitySum += filledVolume/block.terminalVelocity(); + densitySum += filledVolume*block.density(); + maxDensity = @max(maxDensity, block.density()); + mobilitySum += filledVolume*block.mobility(); + } else { + invTerminalVelocitySum += gridVolume/defaults.terminalVelocity; + densitySum += gridVolume*defaults.density; + mobilitySum += gridVolume*defaults.mobility; + } + } + } + } + + return .{ + .terminalVelocity = volumeSum/invTerminalVelocitySum, + .density = densitySum/volumeSum, + .maxDensity = maxDensity, + .mobility = mobilitySum/volumeSum, + }; + } + + pub fn collideOrStep(comptime side: main.utils.Side, comptime dir: Direction, amount: f64, pos: Vec3d, hitBox: Box, steppingHeight: f64) Vec3d { + const index = @intFromEnum(dir); + + // First argument is amount we end up moving in dir, second argument is how far up we step + var resultingMovement: Vec3d = .{0, 0, 0}; + resultingMovement[index] = amount; + var checkPos = pos; + checkPos[index] += amount; + + if(collision.collides(side, dir, -amount, checkPos, hitBox)) |box| { + const newFloor = box.max[2] + hitBox.max[2]; + const heightDifference = newFloor - checkPos[2]; + if(heightDifference <= steppingHeight) { + // If we collide but might be able to step up + checkPos[2] = newFloor; + if(collision.collides(side, dir, -amount, checkPos, hitBox) == null) { + // If there's no new collision then we can execute the step-up + resultingMovement[2] = heightDifference; + return resultingMovement; + } + } + + // Otherwise move as close to the container as possible + if(amount < 0) { + resultingMovement[index] = box.max[index] - hitBox.min[index] - pos[index]; + } else { + resultingMovement[index] = box.min[index] - hitBox.max[index] - pos[index]; + } + } + + return resultingMovement; + } + + fn isBlockIntersecting(block: Block, posX: i32, posY: i32, posZ: i32, center: Vec3d, extent: Vec3d) bool { + const model = block.mode().model(block).model(); + const position = Vec3d{@floatFromInt(posX), @floatFromInt(posY), @floatFromInt(posZ)}; + const entityBox = Box{.min = center - extent, .max = center + extent}; + for(model.collision) |relativeBlockCollision| { + const blockBox = Box{.min = position + relativeBlockCollision.min, .max = position + relativeBlockCollision.max}; + if(blockBox.intersects(entityBox)) { + return true; + } + } + + return false; + } + + pub fn touchBlocks(entity: *main.server.Entity, hitBox: Box, side: main.utils.Side, deltaTime: f64) void { + const boundingBox: Box = .{.min = entity.pos + hitBox.min, .max = entity.pos + hitBox.max}; + + const minX: i32 = @intFromFloat(@floor(boundingBox.min[0] - 0.01)); + const maxX: i32 = @intFromFloat(@floor(boundingBox.max[0] + 0.01)); + const minY: i32 = @intFromFloat(@floor(boundingBox.min[1] - 0.01)); + const maxY: i32 = @intFromFloat(@floor(boundingBox.max[1] + 0.01)); + const minZ: i32 = @intFromFloat(@floor(boundingBox.min[2] - 0.01)); + const maxZ: i32 = @intFromFloat(@floor(boundingBox.max[2] + 0.01)); + + const center: Vec3d = boundingBox.center(); + const extent: Vec3d = boundingBox.extent(); + + const extentX: Vec3d = extent + Vec3d{0.01, -0.01, -0.01}; + const extentY: Vec3d = extent + Vec3d{-0.01, 0.01, -0.01}; + const extentZ: Vec3d = extent + Vec3d{-0.01, -0.01, 0.01}; + + var posX: i32 = minX; + while(posX <= maxX) : (posX += 1) { + var posY: i32 = minY; + while(posY <= maxY) : (posY += 1) { + var posZ: i32 = minZ; + while(posZ <= maxZ) : (posZ += 1) { + const block: ?Block = + if(side == .client) main.renderer.mesh_storage.getBlockFromRenderThread(posX, posY, posZ) else main.server.world.?.getBlock(posX, posY, posZ); + if(block == null or block.?.onTouch().isNoop()) + continue; + const touchX: bool = isBlockIntersecting(block.?, posX, posY, posZ, center, extentX); + const touchY: bool = isBlockIntersecting(block.?, posX, posY, posZ, center, extentY); + const touchZ: bool = isBlockIntersecting(block.?, posX, posY, posZ, center, extentZ); + if(touchX or touchY or touchZ) { + _ = block.?.onTouch().run(.{.entity = entity, .source = block.?, .blockPos = .{posX, posY, posZ}, .deltaTime = deltaTime}); + } + } + } + } + } +}; + +pub const Gamemode = enum(u8) {survival = 0, creative = 1}; + +pub const DamageType = enum(u8) { + heal = 0, // For when you are adding health + kill = 1, + fall = 2, + heat = 3, + spiky = 4, + + pub fn sendMessage(self: DamageType, name: []const u8) void { + switch(self) { + .heal => main.server.sendMessage("{s}§#ffffff was healed", .{name}), + .kill => main.server.sendMessage("{s}§#ffffff was killed", .{name}), + .fall => main.server.sendMessage("{s}§#ffffff died of fall damage", .{name}), + .heat => main.server.sendMessage("{s}§#ffffff burned to death", .{name}), + .spiky => main.server.sendMessage("{s}§#ffffff experienced death by 1000 needles", .{name}), + } + } +}; + +pub const Player = struct { // MARK: Player + pub const EyeData = struct { + pos: Vec3d = .{0, 0, 0}, + vel: Vec3d = .{0, 0, 0}, + coyote: f64 = 0.0, + step: @Vector(3, bool) = .{false, false, false}, + box: collision.Box = .{ + .min = -Vec3d{standingBoundingBoxExtent[0]*0.2, standingBoundingBoxExtent[1]*0.2, 0.6}, + .max = Vec3d{standingBoundingBoxExtent[0]*0.2, standingBoundingBoxExtent[1]*0.2, 0.9 - 0.05}, + }, + desiredPos: Vec3d = .{0, 0, 1.7 - standingBoundingBoxExtent[2]}, + }; + pub var super: main.server.Entity = .{}; + pub var playerSpawnPos: Vec3d = .{0, 0, 0}; + pub var eye: EyeData = .{}; + pub var crouching: bool = false; + pub var id: u32 = 0; + pub var gamemode: Atomic(Gamemode) = .init(.creative); + pub var isFlying: Atomic(bool) = .init(false); + pub var isGhost: Atomic(bool) = .init(false); + pub var hyperSpeed: Atomic(bool) = .init(false); + pub var mutex: std.Thread.Mutex = .{}; + pub const inventorySize = 32; + pub var inventory: Inventory = undefined; + pub var selectedSlot: u32 = 0; + pub const defaultBlockDamage: f32 = 1; + + pub var selectionPosition1: ?Vec3i = null; + pub var selectionPosition2: ?Vec3i = null; + + pub var currentFriction: f32 = 0; + pub var volumeProperties: collision.VolumeProperties = .{.density = 0, .maxDensity = 0, .mobility = 0, .terminalVelocity = 0}; + + pub var onGround: bool = false; + pub var jumpCooldown: f64 = 0; + pub var jumpCoyote: f64 = 0; + pub const jumpCooldownConstant = 0.3; + pub const jumpCoyoteTimeConstant = 0.100; + + pub const standingBoundingBoxExtent: Vec3d = .{0.3, 0.3, 0.9}; + pub const crouchingBoundingBoxExtent: Vec3d = .{0.3, 0.3, 0.725}; + pub var crouchPerc: f32 = 0; + + pub var outerBoundingBoxExtent: Vec3d = standingBoundingBoxExtent; + pub var outerBoundingBox: collision.Box = .{ + .min = -standingBoundingBoxExtent, + .max = standingBoundingBoxExtent, + }; + pub const jumpHeight = 1.25; + + fn loadFrom(zon: ZonElement) void { + super.loadFrom(zon); + } + + pub fn setPosBlocking(newPos: Vec3d) void { + mutex.lock(); + defer mutex.unlock(); + super.pos = newPos; + } + + pub fn getPosBlocking() Vec3d { + mutex.lock(); + defer mutex.unlock(); + return super.pos; + } + + pub fn getVelBlocking() Vec3d { + mutex.lock(); + defer mutex.unlock(); + return super.vel; + } + + pub fn getEyePosBlocking() Vec3d { + mutex.lock(); + defer mutex.unlock(); + return eye.pos + super.pos + eye.desiredPos; + } + + pub fn getEyeVelBlocking() Vec3d { + mutex.lock(); + defer mutex.unlock(); + return eye.vel; + } + + pub fn getEyeCoyoteBlocking() f64 { + mutex.lock(); + defer mutex.unlock(); + return eye.coyote; + } + + pub fn getJumpCoyoteBlocking() f64 { + mutex.lock(); + defer mutex.unlock(); + return jumpCoyote; + } + + pub fn setGamemode(newGamemode: Gamemode) void { + gamemode.store(newGamemode, .monotonic); + + if(newGamemode != .creative) { + isFlying.store(false, .monotonic); + isGhost.store(false, .monotonic); + hyperSpeed.store(false, .monotonic); + } + } + + pub fn setSpawn(newSpawnpoint: Vec3d) void { + playerSpawnPos = newSpawnpoint; + } + + pub fn isCreative() bool { + return gamemode.load(.monotonic) == .creative; + } + + pub fn isActuallyFlying() bool { + return isFlying.load(.monotonic) and !isGhost.load(.monotonic); + } + + pub fn steppingHeight() Vec3d { + if(onGround) { + return .{0, 0, 0.6}; + } else { + return .{0, 0, 0.1}; + } + } + + pub fn placeBlock(mods: main.Window.Key.Modifiers) void { + if(main.renderer.MeshSelection.selectedBlockPos) |blockPos| { + if(!mods.shift) { + if(main.renderer.mesh_storage.triggerOnInteractBlockFromRenderThread(blockPos[0], blockPos[1], blockPos[2]) == .handled) return; + } + const block = main.renderer.mesh_storage.getBlockFromRenderThread(blockPos[0], blockPos[1], blockPos[2]) orelse main.blocks.Block{.typ = 0, .data = 0}; + const onInteract = block.onInteract(); + if(!mods.shift) { + if(onInteract.run(.{.blockPos = blockPos, .block = block}) == .handled) return; + } + } + + inventory.placeBlock(selectedSlot); + } + + pub fn kill() void { + Player.super.pos = Player.playerSpawnPos; + Player.super.vel = .{0, 0, 0}; + + Player.super.health = Player.super.maxHealth; + Player.super.energy = Player.super.maxEnergy; + + Player.eye = .{}; + Player.jumpCoyote = 0; + } + + pub fn dropFromHand(mods: main.Window.Key.Modifiers) void { + if(mods.shift) { + inventory.dropStack(selectedSlot); + } else { + inventory.dropOne(selectedSlot); + } + } + + pub fn breakBlock(deltaTime: f64) void { + inventory.breakBlock(selectedSlot, deltaTime); + } + + pub fn acquireSelectedBlock() void { + if(main.renderer.MeshSelection.selectedBlockPos) |selectedPos| { + const block = main.renderer.mesh_storage.getBlockFromRenderThread(selectedPos[0], selectedPos[1], selectedPos[2]) orelse return; + + const item: items.Item = for(0..items.itemListSize) |idx| { + const baseItem: main.items.BaseItemIndex = @enumFromInt(idx); + if(baseItem.block() == block.typ) { + break .{.baseItem = baseItem}; + } + } else return; + + // Check if there is already a slot with that item type + for(0..12) |slotIdx| { + if(std.meta.eql(inventory.getItem(slotIdx), item)) { + if(isCreative()) { + inventory.fillFromCreative(@intCast(slotIdx), item); + } + selectedSlot = @intCast(slotIdx); + return; + } + } + + if(isCreative()) { + const targetSlot = blk: { + if(inventory.getItem(selectedSlot) == .null) break :blk selectedSlot; + // Look for an empty slot + for(0..12) |slotIdx| { + if(inventory.getItem(slotIdx) == .null) { + break :blk slotIdx; + } + } + break :blk selectedSlot; + }; + + inventory.fillFromCreative(@intCast(targetSlot), item); + selectedSlot = @intCast(targetSlot); + } + } + } +}; + +pub const World = struct { // MARK: World + pub const dayCycle: u63 = 12000; // Length of one in-game day in 100ms. Midnight is at DAY_CYCLE/2. Sunrise and sunset each take about 1/16 of the day. Currently set to 20 minutes + + conn: *Connection, + manager: *ConnectionManager, + ambientLight: f32 = 0, + name: []const u8, + milliTime: i64, + gameTime: Atomic(i64) = .init(0), + spawn: Vec3f = undefined, + connected: bool = true, + blockPalette: *assets.Palette = undefined, + itemPalette: *assets.Palette = undefined, + toolPalette: *assets.Palette = undefined, + biomePalette: *assets.Palette = undefined, + itemDrops: ClientItemDropManager = undefined, + playerBiome: Atomic(*const main.server.terrain.biomes.Biome) = undefined, + + pub fn init(self: *World, ip: []const u8, manager: *ConnectionManager) !void { + main.heap.allocators.createWorldArena(); + errdefer main.heap.allocators.destroyWorldArena(); + self.* = .{ + .conn = try Connection.init(manager, ip, null), + .manager = manager, + .name = "client", + .milliTime = main.timestamp().toMilliseconds(), + }; + errdefer self.conn.deinit(); + + self.itemDrops.init(main.globalAllocator); + errdefer self.itemDrops.deinit(); + try network.protocols.handShake.clientSide(self.conn, settings.playerName); + + main.Window.setMouseGrabbed(true); + + main.blocks.meshes.generateTextureArray(); + main.particles.ParticleManager.generateTextureArray(); + main.models.uploadModels(); + } + + pub fn deinit(self: *World) void { + self.conn.deinit(); + + self.connected = false; + + // TODO: Close all world related guis. + main.gui.inventory.deinit(); + main.gui.deinit(); + main.gui.init(); + Player.inventory.deinit(main.globalAllocator); + main.items.clearRecipeCachedInventories(); + main.items.Inventory.Sync.ClientSide.reset(); + + main.threadPool.clear(); + main.entity.ClientEntityManager.clear(); + self.itemDrops.deinit(); + self.blockPalette.deinit(); + self.itemPalette.deinit(); + self.toolPalette.deinit(); + self.biomePalette.deinit(); + self.manager.deinit(); + main.server.stop(); + if(main.server.thread) |serverThread| { + serverThread.join(); + main.server.thread = null; + } + main.threadPool.clear(); + renderer.mesh_storage.deinit(); + renderer.mesh_storage.init(); + assets.unloadAssets(); + main.heap.allocators.destroyWorldArena(); + } + + pub fn finishHandshake(self: *World, zon: ZonElement) !void { + // TODO: Consider using a per-world allocator. + self.blockPalette = try assets.Palette.init(main.globalAllocator, zon.getChild("blockPalette"), "cubyz:air"); + errdefer self.blockPalette.deinit(); + self.biomePalette = try assets.Palette.init(main.globalAllocator, zon.getChild("biomePalette"), null); + errdefer self.biomePalette.deinit(); + self.itemPalette = try assets.Palette.init(main.globalAllocator, zon.getChild("itemPalette"), null); + errdefer self.itemPalette.deinit(); + self.toolPalette = try assets.Palette.init(main.globalAllocator, zon.getChild("toolPalette"), null); + errdefer self.toolPalette.deinit(); + self.spawn = zon.get(Vec3f, "spawn", .{0, 0, 0}); + + const path = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/serverAssets", .{main.files.cubyzDirStr()}) catch unreachable; + defer main.stackAllocator.free(path); + try assets.loadWorldAssets(path, self.blockPalette, self.itemPalette, self.toolPalette, self.biomePalette); + Player.id = zon.get(u32, "player_id", std.math.maxInt(u32)); + Player.inventory = Inventory.init(main.globalAllocator, Player.inventorySize, .normal, .{.playerInventory = Player.id}, .{}); + Player.loadFrom(zon.getChild("player")); + self.playerBiome = .init(main.server.terrain.biomes.getPlaceholderBiome()); + main.audio.setMusic(self.playerBiome.raw.preferredMusic); + } + + fn dayNightLightFactor(gameTime: i64) struct {f32, Vec3f} { + const dayTime = @abs(@mod(gameTime, dayCycle) - dayCycle/2); + if(dayTime < dayCycle/4 - dayCycle/16) { + return .{0.1, @splat(0)}; + } + if(dayTime > dayCycle/4 + dayCycle/16) { + return .{1, @splat(1)}; + } + var skyColorFactor: Vec3f = undefined; + // b: + if(dayTime > dayCycle/4) { + skyColorFactor[2] = @as(f32, @floatFromInt(dayTime - dayCycle/4))/@as(f32, @floatFromInt(dayCycle/16)); + } else { + skyColorFactor[2] = 0; + } + // g: + if(dayTime > dayCycle/4 + dayCycle/32) { + skyColorFactor[1] = 1; + } else if(dayTime > dayCycle/4 - dayCycle/32) { + skyColorFactor[1] = 1 - @as(f32, @floatFromInt(dayCycle/4 + dayCycle/32 - dayTime))/@as(f32, @floatFromInt(dayCycle/16)); + } else { + skyColorFactor[1] = 0; + } + // r: + if(dayTime > dayCycle/4) { + skyColorFactor[0] = 1; + } else { + skyColorFactor[0] = 1 - @as(f32, @floatFromInt(dayCycle/4 - dayTime))/@as(f32, @floatFromInt(dayCycle/16)); + } + + const ambientLight = 0.1 + 0.9*@as(f32, @floatFromInt(dayTime - (dayCycle/4 - dayCycle/16)))/@as(f32, @floatFromInt(dayCycle/8)); + return .{ambientLight, skyColorFactor}; + } + + pub fn update(self: *World) void { + const newTime: i64 = main.timestamp().toMilliseconds(); + while(self.milliTime +% 100 -% newTime < 0) { + self.milliTime +%= 100; + var curTime = self.gameTime.load(.monotonic); + while(self.gameTime.cmpxchgWeak(curTime, curTime +% 1, .monotonic, .monotonic)) |actualTime| { + curTime = actualTime; + } + } + // Ambient light: + { + self.ambientLight, const skyColorFactor = dayNightLightFactor(self.gameTime.load(.unordered)); + fog.fogColor = biomeFog.fogColor*skyColorFactor; + fog.skyColor = biomeFog.skyColor*skyColorFactor; + fog.density = biomeFog.density; + fog.fogLower = biomeFog.fogLower; + fog.fogHigher = biomeFog.fogHigher; + } + network.protocols.playerPosition.send(self.conn, Player.getPosBlocking(), Player.getVelBlocking(), @intCast(newTime & 65535)); + } +}; +pub var testWorld: World = undefined; // TODO: +pub var world: ?*World = null; + +pub var projectionMatrix: Mat4f = Mat4f.identity(); + +var biomeFog = Fog{.skyColor = .{0.8, 0.8, 1}, .fogColor = .{0.8, 0.8, 1}, .density = 1.0/15.0/128.0, .fogLower = 100, .fogHigher = 1000}; +pub var fog = Fog{.skyColor = .{0.8, 0.8, 1}, .fogColor = .{0.8, 0.8, 1}, .density = 1.0/15.0/128.0, .fogLower = 100, .fogHigher = 1000}; + +var nextBlockPlaceTime: ?std.Io.Timestamp = null; +var nextBlockBreakTime: ?std.Io.Timestamp = null; + +pub fn pressPlace(mods: main.Window.Key.Modifiers) void { + const time = main.timestamp(); + nextBlockPlaceTime = time.addDuration(main.settings.updateRepeatDelay); + Player.placeBlock(mods); +} + +pub fn releasePlace(_: main.Window.Key.Modifiers) void { + nextBlockPlaceTime = null; +} + +pub fn pressBreak(_: main.Window.Key.Modifiers) void { + const time = main.timestamp(); + nextBlockBreakTime = time.addDuration(main.settings.updateRepeatDelay); + Player.breakBlock(0); +} + +pub fn releaseBreak(_: main.Window.Key.Modifiers) void { + nextBlockBreakTime = null; +} + +pub fn pressAcquireSelectedBlock(_: main.Window.Key.Modifiers) void { + Player.acquireSelectedBlock(); +} + +pub fn flyToggle(_: main.Window.Key.Modifiers) void { + if(!Player.isCreative()) return; + + const newIsFlying = !Player.isActuallyFlying(); + + Player.isFlying.store(newIsFlying, .monotonic); + Player.isGhost.store(false, .monotonic); +} + +pub fn ghostToggle(_: main.Window.Key.Modifiers) void { + if(!Player.isCreative()) return; + + const newIsGhost = !Player.isGhost.load(.monotonic); + + Player.isGhost.store(newIsGhost, .monotonic); + Player.isFlying.store(newIsGhost, .monotonic); +} + +pub fn hyperSpeedToggle(_: main.Window.Key.Modifiers) void { + if(!Player.isCreative()) return; + + Player.hyperSpeed.store(!Player.hyperSpeed.load(.monotonic), .monotonic); +} + +pub fn update(deltaTime: f64) void { // MARK: update() + physics.calculateProperties(); + var acc = Vec3d{0, 0, 0}; + const speedMultiplier: f32 = if(Player.hyperSpeed.load(.monotonic)) 4.0 else 1.0; + + const mobility = if(Player.isFlying.load(.monotonic)) 1.0 else Player.volumeProperties.mobility; + const density = if(Player.isFlying.load(.monotonic)) 0.0 else Player.volumeProperties.density; + const maxDensity = if(Player.isFlying.load(.monotonic)) 0.0 else Player.volumeProperties.maxDensity; + + const baseFrictionCoefficient: f32 = Player.currentFriction; + var jumping = false; + Player.jumpCooldown -= deltaTime; + // At equillibrium we want to have dv/dt = a - λv = 0 → a = λ*v + const fricMul = speedMultiplier*baseFrictionCoefficient*if(Player.isFlying.load(.monotonic)) 1.0 else mobility; + + const horizontalForward = vec.rotateZ(Vec3d{0, 1, 0}, -camera.rotation[2]); + const forward = vec.normalize(std.math.lerp(horizontalForward, camera.direction, @as(Vec3d, @splat(density/@max(1.0, maxDensity))))); + const right = Vec3d{-horizontalForward[1], horizontalForward[0], 0}; + var movementDir: Vec3d = .{0, 0, 0}; + var movementSpeed: f64 = 0; + + if(main.Window.grabbed) { + const walkingSpeed: f64 = if(Player.crouching) 2 else 4; + if(KeyBoard.key("forward").value > 0.0) { + if(KeyBoard.key("sprint").pressed and !Player.crouching) { + if(Player.isGhost.load(.monotonic)) { + movementSpeed = @max(movementSpeed, 128)*KeyBoard.key("forward").value; + movementDir += forward*@as(Vec3d, @splat(128*KeyBoard.key("forward").value)); + } else if(Player.isFlying.load(.monotonic)) { + movementSpeed = @max(movementSpeed, 32)*KeyBoard.key("forward").value; + movementDir += forward*@as(Vec3d, @splat(32*KeyBoard.key("forward").value)); + } else { + movementSpeed = @max(movementSpeed, 8)*KeyBoard.key("forward").value; + movementDir += forward*@as(Vec3d, @splat(8*KeyBoard.key("forward").value)); + } + } else { + movementSpeed = @max(movementSpeed, walkingSpeed)*KeyBoard.key("forward").value; + movementDir += forward*@as(Vec3d, @splat(walkingSpeed*KeyBoard.key("forward").value)); + } + } + if(KeyBoard.key("backward").value > 0.0) { + movementSpeed = @max(movementSpeed, walkingSpeed)*KeyBoard.key("backward").value; + movementDir += forward*@as(Vec3d, @splat(-walkingSpeed*KeyBoard.key("backward").value)); + } + if(KeyBoard.key("left").value > 0.0) { + movementSpeed = @max(movementSpeed, walkingSpeed)*KeyBoard.key("left").value; + movementDir += right*@as(Vec3d, @splat(walkingSpeed*KeyBoard.key("left").value)); + } + if(KeyBoard.key("right").value > 0.0) { + movementSpeed = @max(movementSpeed, walkingSpeed)*KeyBoard.key("right").value; + movementDir += right*@as(Vec3d, @splat(-walkingSpeed*KeyBoard.key("right").value)); + } + if(KeyBoard.key("jump").pressed) { + if(Player.isFlying.load(.monotonic)) { + if(KeyBoard.key("sprint").pressed) { + if(Player.isGhost.load(.monotonic)) { + movementSpeed = @max(movementSpeed, 60); + movementDir[2] += 60; + } else { + movementSpeed = @max(movementSpeed, 25); + movementDir[2] += 25; + } + } else { + movementSpeed = @max(movementSpeed, 5.5); + movementDir[2] += 5.5; + } + } else if((Player.onGround or Player.jumpCoyote > 0.0) and Player.jumpCooldown <= 0) { + jumping = true; + Player.jumpCooldown = Player.jumpCooldownConstant; + if(!Player.onGround) { + Player.eye.coyote = 0; + } + Player.jumpCoyote = 0; + } else if(!KeyBoard.key("fall").pressed) { + movementSpeed = @max(movementSpeed, walkingSpeed); + movementDir[2] += walkingSpeed; + } + } else { + Player.jumpCooldown = 0; + } + if(KeyBoard.key("fall").pressed) { + if(Player.isFlying.load(.monotonic)) { + if(KeyBoard.key("sprint").pressed) { + if(Player.isGhost.load(.monotonic)) { + movementSpeed = @max(movementSpeed, 60); + movementDir[2] -= 60; + } else { + movementSpeed = @max(movementSpeed, 25); + movementDir[2] -= 25; + } + } else { + movementSpeed = @max(movementSpeed, 5.5); + movementDir[2] -= 5.5; + } + } else if(!KeyBoard.key("jump").pressed) { + movementSpeed = @max(movementSpeed, walkingSpeed); + movementDir[2] -= walkingSpeed; + } + } + + if(movementSpeed != 0 and vec.lengthSquare(movementDir) != 0) { + if(vec.lengthSquare(movementDir) > movementSpeed*movementSpeed) { + movementDir = vec.normalize(movementDir); + } else { + movementDir /= @splat(movementSpeed); + } + acc += movementDir*@as(Vec3d, @splat(movementSpeed*fricMul)); + } + + const newSlot: i32 = @as(i32, @intCast(Player.selectedSlot)) -% main.Window.scrollOffsetInteger; + Player.selectedSlot = @intCast(@mod(newSlot, 12)); + + const newPos = Vec2f{ + @floatCast(main.KeyBoard.key("cameraRight").value - main.KeyBoard.key("cameraLeft").value), + @floatCast(main.KeyBoard.key("cameraDown").value - main.KeyBoard.key("cameraUp").value), + }*@as(Vec2f, @splat(std.math.pi*settings.controllerSensitivity)); + main.game.camera.moveRotation(newPos[0]/64.0, newPos[1]/64.0); + } + + Player.crouching = KeyBoard.key("crouch").pressed and !Player.isFlying.load(.monotonic); + + if(collision.collides(.client, .x, 0, Player.super.pos + Player.standingBoundingBoxExtent - Player.crouchingBoundingBoxExtent, .{ + .min = -Player.standingBoundingBoxExtent, + .max = Player.standingBoundingBoxExtent, + }) == null) { + if(Player.onGround) { + if(Player.crouching) { + Player.crouchPerc += @floatCast(deltaTime*10); + } else { + Player.crouchPerc -= @floatCast(deltaTime*10); + } + Player.crouchPerc = std.math.clamp(Player.crouchPerc, 0, 1); + } + + const smoothPerc = Player.crouchPerc*Player.crouchPerc*(3 - 2*Player.crouchPerc); + + const newOuterBox = (Player.crouchingBoundingBoxExtent - Player.standingBoundingBoxExtent)*@as(Vec3d, @splat(smoothPerc)) + Player.standingBoundingBoxExtent; + + Player.super.pos += newOuterBox - Player.outerBoundingBoxExtent + Vec3d{0.0, 0.0, 0.0001*@abs(newOuterBox[2] - Player.outerBoundingBoxExtent[2])}; + + Player.outerBoundingBoxExtent = newOuterBox; + + Player.outerBoundingBox = .{ + .min = -Player.outerBoundingBoxExtent, + .max = Player.outerBoundingBoxExtent, + }; + Player.eye.box = .{ + .min = -Vec3d{Player.outerBoundingBoxExtent[0]*0.2, Player.outerBoundingBoxExtent[1]*0.2, Player.outerBoundingBoxExtent[2] - 0.2}, + .max = Vec3d{Player.outerBoundingBoxExtent[0]*0.2, Player.outerBoundingBoxExtent[1]*0.2, Player.outerBoundingBoxExtent[2] - 0.05}, + }; + Player.eye.desiredPos = (Vec3d{0, 0, 1.3 - Player.crouchingBoundingBoxExtent[2]} - Vec3d{0, 0, 1.7 - Player.standingBoundingBoxExtent[2]})*@as(Vec3f, @splat(smoothPerc)) + Vec3d{0, 0, 1.7 - Player.standingBoundingBoxExtent[2]}; + } + + physics.update(deltaTime, acc, jumping); + + const time = main.timestamp(); + if(nextBlockPlaceTime) |*placeTime| { + if(placeTime.durationTo(time).nanoseconds >= 0) { + placeTime.* = placeTime.addDuration(main.settings.updateRepeatSpeed); + Player.placeBlock(main.KeyBoard.key("placeBlock").modsOnPress); + } + } + if(nextBlockBreakTime) |*breakTime| { + if(breakTime.durationTo(time).nanoseconds >= 0 or !Player.isCreative()) { + breakTime.* = breakTime.addDuration(main.settings.updateRepeatSpeed); + Player.breakBlock(deltaTime); + } + } + + const biome = world.?.playerBiome.load(.monotonic); + + const t = 1 - @as(f32, @floatCast(@exp(-2*deltaTime))); + + biomeFog.fogColor = (biome.fogColor - biomeFog.fogColor)*@as(Vec3f, @splat(t)) + biomeFog.fogColor; + biomeFog.skyColor = (biome.skyColor - biomeFog.skyColor)*@as(Vec3f, @splat(t)) + biomeFog.skyColor; + biomeFog.density = (biome.fogDensity - biomeFog.density)*t + biomeFog.density; + biomeFog.fogLower = (biome.fogLower - biomeFog.fogLower)*t + biomeFog.fogLower; + biomeFog.fogHigher = (biome.fogHigher - biomeFog.fogHigher)*t + biomeFog.fogHigher; + + world.?.update(); + particles.ParticleSystem.update(@floatCast(deltaTime)); +} diff --git a/graphics.zig b/graphics.zig new file mode 100644 index 0000000000..5e6db485dc --- /dev/null +++ b/graphics.zig @@ -0,0 +1,2718 @@ +/// A collection of things that should make dealing with opengl easier. +/// Also contains some basic 2d drawing stuff. +const std = @import("std"); +const builtin = @import("builtin"); + +pub const hbft = @cImport({ + @cDefine("_BITS_STDIO2_H", ""); // TODO: Zig fails to include this header file + @cInclude("freetype/ftadvanc.h"); + @cInclude("freetype/ftbbox.h"); + @cInclude("freetype/ftbitmap.h"); + @cInclude("freetype/ftcolor.h"); + @cInclude("freetype/ftlcdfil.h"); + @cInclude("freetype/ftsizes.h"); + @cInclude("freetype/ftstroke.h"); + @cInclude("freetype/fttrigon.h"); + @cInclude("freetype/ftsynth.h"); + @cInclude("hb.h"); + @cInclude("hb-ft.h"); +}); + +const vec = @import("vec.zig"); +const Mat4f = vec.Mat4f; +const Vec4i = vec.Vec4i; +const Vec4f = vec.Vec4f; +const Vec2f = vec.Vec2f; +const Vec2i = vec.Vec2i; +const Vec3f = vec.Vec3f; + +const main = @import("main"); +const Window = main.Window; + +const NeverFailingAllocator = main.heap.NeverFailingAllocator; + +pub const c = @cImport({ + @cInclude("glad/gl.h"); + // NOTE(blackedout): glad is currently not used on macOS, so use Vulkan header from the Vulkan-Headers repository instead + @cInclude(if(builtin.target.os.tag == .macos) "vulkan/vulkan.h" else "glad/vulkan.h"); +}); + +pub const stb_image = @cImport({ + @cDefine("_BITS_STDIO2_H", ""); // TODO: Zig fails to include this header file + @cInclude("stb/stb_image.h"); + @cInclude("stb/stb_image_write.h"); +}); + +const glslang = @cImport({ + @cInclude("glslang/Include/glslang_c_interface.h"); + @cInclude("glslang/Public/resource_limits_c.h"); +}); + +pub const draw = struct { // MARK: draw + var color: u32 = 0; + var clip: ?Vec4i = null; + var translation: Vec2f = Vec2f{0, 0}; + var scale: f32 = 1; + + pub fn setColor(newColor: u32) void { + color = newColor; + } + + /// Returns the previous translation. + pub fn setTranslation(newTranslation: Vec2f) Vec2f { + const oldTranslation = translation; + translation += newTranslation*@as(Vec2f, @splat(scale)); + return oldTranslation; + } + + pub fn restoreTranslation(previousTranslation: Vec2f) void { + translation = previousTranslation; + } + + /// Returns the previous scale. + pub fn setScale(newScale: f32) f32 { + std.debug.assert(newScale >= 0); + const oldScale = scale; + scale *= newScale; + return oldScale; + } + + pub fn restoreScale(previousScale: f32) void { + scale = previousScale; + } + + /// Returns the previous clip. + pub fn setClip(clipRect: Vec2f) ?Vec4i { + std.debug.assert(@reduce(.And, clipRect >= Vec2f{0, 0})); + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + var newClip = Vec4i{ + std.math.lossyCast(i32, translation[0]), + viewport[3] - std.math.lossyCast(i32, translation[1] + clipRect[1]*scale), + std.math.lossyCast(i32, clipRect[0]*scale), + std.math.lossyCast(i32, clipRect[1]*scale), + }; + if(clip) |oldClip| { + if(newClip[0] < oldClip[0]) { + newClip[2] -= oldClip[0] - newClip[0]; + newClip[0] += oldClip[0] - newClip[0]; + } + if(newClip[1] < oldClip[1]) { + newClip[3] -= oldClip[1] - newClip[1]; + newClip[1] += oldClip[1] - newClip[1]; + } + if(newClip[0] + newClip[2] > oldClip[0] + oldClip[2]) { + newClip[2] -= (newClip[0] + newClip[2]) - (oldClip[0] + oldClip[2]); + } + if(newClip[1] + newClip[3] > oldClip[1] + oldClip[3]) { + newClip[3] -= (newClip[1] + newClip[3]) - (oldClip[1] + oldClip[3]); + } + newClip[2] = @max(newClip[2], 0); + newClip[3] = @max(newClip[3], 0); + } + const oldClip = clip; + clip = newClip; + return oldClip; + } + + pub fn getScissor() ?c.VkRect2D { + const clipRect = clip orelse return null; + return .{ + .offset = .{ + .x = clipRect[0], + .y = clipRect[1], + }, + .extent = .{ + .width = @intCast(clipRect[2]), + .height = @intCast(clipRect[3]), + }, + }; + } + + /// Should be used to restore the old clip when leaving the render function. + pub fn restoreClip(previousClip: ?Vec4i) void { + clip = previousClip; + } + + // ---------------------------------------------------------------------------- + // MARK: fillRect() + var rectUniforms: struct { + screen: c_int, + start: c_int, + size: c_int, + rectColor: c_int, + } = undefined; + var rectPipeline: Pipeline = undefined; + pub var rectVAO: c_uint = undefined; + var rectVBO: c_uint = undefined; + + fn initRect() void { + rectPipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/Rect.vert", + "assets/cubyz/shaders/graphics/Rect.frag", + "", + &rectUniforms, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.alphaBlending}}, + ); + const rawData = [_]f32{ + 0, 0, + 0, 1, + 1, 0, + 1, 1, + }; + + c.glGenVertexArrays(1, &rectVAO); + c.glBindVertexArray(rectVAO); + c.glGenBuffers(1, &rectVBO); + c.glBindBuffer(c.GL_ARRAY_BUFFER, rectVBO); + c.glBufferData(c.GL_ARRAY_BUFFER, rawData.len*@sizeOf(f32), &rawData, c.GL_STATIC_DRAW); + c.glVertexAttribPointer(0, 2, c.GL_FLOAT, c.GL_FALSE, 2*@sizeOf(f32), null); + c.glEnableVertexAttribArray(0); + } + + fn deinitRect() void { + rectPipeline.deinit(); + c.glDeleteVertexArrays(1, &rectVAO); + c.glDeleteBuffers(1, &rectVBO); + } + + pub fn rect(_pos: Vec2f, _dim: Vec2f) void { + var pos = _pos; + var dim = _dim; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + + rectPipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(rectUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(rectUniforms.start, pos[0], pos[1]); + c.glUniform2f(rectUniforms.size, dim[0], dim[1]); + c.glUniform1i(rectUniforms.rectColor, @bitCast(color)); + + c.glBindVertexArray(rectVAO); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + // ---------------------------------------------------------------------------- + // MARK: fillRectBorder() + var rectBorderUniforms: struct { + screen: c_int, + start: c_int, + size: c_int, + rectColor: c_int, + lineWidth: c_int, + } = undefined; + var rectBorderPipeline: Pipeline = undefined; + var rectBorderVAO: c_uint = undefined; + var rectBorderVBO: c_uint = undefined; + + fn initRectBorder() void { + rectBorderPipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/RectBorder.vert", + "assets/cubyz/shaders/graphics/RectBorder.frag", + "", + &rectBorderUniforms, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.alphaBlending}}, + ); + const rawData = [_]f32{ + 0, 0, 0, 0, + 0, 0, 1, 1, + 0, 1, 0, 0, + 0, 1, 1, -1, + 1, 1, 0, 0, + 1, 1, -1, -1, + 1, 0, 0, 0, + 1, 0, -1, 1, + 0, 0, 0, 0, + 0, 0, 1, 1, + }; + + c.glGenVertexArrays(1, &rectBorderVAO); + c.glBindVertexArray(rectBorderVAO); + c.glGenBuffers(1, &rectBorderVBO); + c.glBindBuffer(c.GL_ARRAY_BUFFER, rectBorderVBO); + c.glBufferData(c.GL_ARRAY_BUFFER, rawData.len*@sizeOf(f32), &rawData, c.GL_STATIC_DRAW); + c.glVertexAttribPointer(0, 4, c.GL_FLOAT, c.GL_FALSE, 4*@sizeOf(f32), null); + c.glEnableVertexAttribArray(0); + } + + fn deinitRectBorder() void { + rectBorderPipeline.deinit(); + c.glDeleteVertexArrays(1, &rectBorderVAO); + c.glDeleteBuffers(1, &rectBorderVBO); + } + + pub fn rectBorder(_pos: Vec2f, _dim: Vec2f, _width: f32) void { + var pos = _pos; + var dim = _dim; + var width = _width; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + width *= scale; + + rectBorderPipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(rectBorderUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(rectBorderUniforms.start, pos[0], pos[1]); + c.glUniform2f(rectBorderUniforms.size, dim[0], dim[1]); + c.glUniform1i(rectBorderUniforms.rectColor, @bitCast(color)); + c.glUniform1f(rectBorderUniforms.lineWidth, width); + + c.glBindVertexArray(rectBorderVAO); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 10); + } + + // ---------------------------------------------------------------------------- + // MARK: drawLine() + var lineUniforms: struct { + screen: c_int, + start: c_int, + direction: c_int, + lineColor: c_int, + } = undefined; + var linePipeline: Pipeline = undefined; + var lineVAO: c_uint = undefined; + var lineVBO: c_uint = undefined; + + fn initLine() void { + linePipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/Line.vert", + "assets/cubyz/shaders/graphics/Line.frag", + "", + &lineUniforms, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.alphaBlending}}, + ); + const rawData = [_]f32{ + 0, 0, + 1, 1, + }; + + c.glGenVertexArrays(1, &lineVAO); + c.glBindVertexArray(lineVAO); + c.glGenBuffers(1, &lineVBO); + c.glBindBuffer(c.GL_ARRAY_BUFFER, lineVBO); + c.glBufferData(c.GL_ARRAY_BUFFER, rawData.len*@sizeOf(f32), &rawData, c.GL_STATIC_DRAW); + c.glVertexAttribPointer(0, 2, c.GL_FLOAT, c.GL_FALSE, 2*@sizeOf(f32), null); + c.glEnableVertexAttribArray(0); + } + + fn deinitLine() void { + linePipeline.deinit(); + c.glDeleteVertexArrays(1, &lineVAO); + c.glDeleteBuffers(1, &lineVBO); + } + + pub fn line(_pos1: Vec2f, _pos2: Vec2f) void { + var pos1 = _pos1; + var pos2 = _pos2; + pos1 *= @splat(scale); + pos1 += translation; + pos2 *= @splat(scale); + pos2 += translation; + + linePipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(lineUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(lineUniforms.start, pos1[0], pos1[1]); + c.glUniform2f(lineUniforms.direction, pos2[0] - pos1[0], pos2[1] - pos1[1]); + c.glUniform1i(lineUniforms.lineColor, @bitCast(color)); + + c.glBindVertexArray(lineVAO); + c.glDrawArrays(c.GL_LINE_STRIP, 0, 2); + } + + // ---------------------------------------------------------------------------- + // MARK: drawRect() + // Draw rect can use the same shader as drawline, because it essentially draws lines. + var drawRectVAO: c_uint = undefined; + var drawRectVBO: c_uint = undefined; + + fn initDrawRect() void { + const rawData = [_]f32{ + 0, 0, + 0, 1, + 1, 1, + 1, 0, + }; + + c.glGenVertexArrays(1, &drawRectVAO); + c.glBindVertexArray(drawRectVAO); + c.glGenBuffers(1, &drawRectVBO); + c.glBindBuffer(c.GL_ARRAY_BUFFER, drawRectVBO); + c.glBufferData(c.GL_ARRAY_BUFFER, rawData.len*@sizeOf(f32), &rawData, c.GL_STATIC_DRAW); + c.glVertexAttribPointer(0, 2, c.GL_FLOAT, c.GL_FALSE, 2*@sizeOf(f32), null); + c.glEnableVertexAttribArray(0); + } + + fn deinitDrawRect() void { + c.glDeleteVertexArrays(1, &drawRectVAO); + c.glDeleteBuffers(1, &drawRectVBO); + } + + pub fn rectOutline(_pos: Vec2f, _dim: Vec2f) void { + var pos = _pos; + var dim = _dim; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + + linePipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(lineUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(lineUniforms.start, pos[0], pos[1]); // Move the coordinates, so they are in the center of a pixel. + c.glUniform2f(lineUniforms.direction, dim[0] - 1, dim[1] - 1); // The height is a lot smaller because the inner edge of the rect is drawn. + c.glUniform1i(lineUniforms.lineColor, @bitCast(color)); + + c.glBindVertexArray(lineVAO); + c.glDrawArrays(c.GL_LINE_LOOP, 0, 5); + } + + // ---------------------------------------------------------------------------- + // MARK: fillCircle() + var circleUniforms: struct { + screen: c_int, + center: c_int, + radius: c_int, + circleColor: c_int, + } = undefined; + var circlePipeline: Pipeline = undefined; + var circleVAO: c_uint = undefined; + var circleVBO: c_uint = undefined; + + fn initCircle() void { + circlePipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/Circle.vert", + "assets/cubyz/shaders/graphics/Circle.frag", + "", + &circleUniforms, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.alphaBlending}}, + ); + const rawData = [_]f32{ + -1, -1, + -1, 1, + 1, -1, + 1, 1, + }; + + c.glGenVertexArrays(1, &circleVAO); + c.glBindVertexArray(circleVAO); + c.glGenBuffers(1, &circleVBO); + c.glBindBuffer(c.GL_ARRAY_BUFFER, circleVBO); + c.glBufferData(c.GL_ARRAY_BUFFER, rawData.len*@sizeOf(f32), &rawData, c.GL_STATIC_DRAW); + c.glVertexAttribPointer(0, 2, c.GL_FLOAT, c.GL_FALSE, 2*@sizeOf(f32), null); + c.glEnableVertexAttribArray(0); + } + + fn deinitCircle() void { + circlePipeline.deinit(); + c.glDeleteVertexArrays(1, &circleVAO); + c.glDeleteBuffers(1, &circleVBO); + } + + pub fn circle(_center: Vec2f, _radius: f32) void { + var center = _center; + var radius = _radius; + center *= @splat(scale); + center += translation; + radius *= scale; + circlePipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(circleUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(circleUniforms.center, center[0], center[1]); // Move the coordinates, so they are in the center of a pixel. + c.glUniform1f(circleUniforms.radius, radius); // The height is a lot smaller because the inner edge of the rect is drawn. + c.glUniform1i(circleUniforms.circleColor, @bitCast(color)); + + c.glBindVertexArray(circleVAO); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + // ---------------------------------------------------------------------------- + // MARK: drawImage() + // Luckily the vao of the regular rect can used. + var imageUniforms: struct { + screen: c_int, + start: c_int, + size: c_int, + color: c_int, + uvOffset: c_int, + uvDim: c_int, + } = undefined; + var imagePipeline: Pipeline = undefined; + + fn initImage() void { + imagePipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/Image.vert", + "assets/cubyz/shaders/graphics/Image.frag", + "", + &imageUniforms, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.alphaBlending}}, + ); + } + + fn deinitImage() void { + imagePipeline.deinit(); + } + + pub fn boundImage(_pos: Vec2f, _dim: Vec2f) void { + imagePipeline.bind(getScissor()); + + customShadedImage(&imageUniforms, _pos, _dim); + } + + pub fn boundSubImage(_pos: Vec2f, _dim: Vec2f, uvOffset: Vec2f, uvDim: Vec2f) void { + var pos = _pos; + var dim = _dim; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + pos = @floor(pos); + dim = @ceil(dim); + + imagePipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(imageUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(imageUniforms.start, pos[0], pos[1]); + c.glUniform2f(imageUniforms.size, dim[0], dim[1]); + c.glUniform1i(imageUniforms.color, @bitCast(color)); + c.glUniform2f(imageUniforms.uvOffset, uvOffset[0], 1 - uvOffset[1] - uvDim[1]); + c.glUniform2f(imageUniforms.uvDim, uvDim[0], uvDim[1]); + + c.glBindVertexArray(rectVAO); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + pub fn customShadedImage(uniforms: anytype, _pos: Vec2f, _dim: Vec2f) void { + var pos = _pos; + var dim = _dim; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + pos = @floor(pos); + dim = @ceil(dim); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(uniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(uniforms.start, pos[0], pos[1]); + c.glUniform2f(uniforms.size, dim[0], dim[1]); + c.glUniform1i(uniforms.color, @bitCast(color)); + c.glUniform2f(uniforms.uvOffset, 0, 0); + c.glUniform2f(uniforms.uvDim, 1, 1); + + c.glBindVertexArray(rectVAO); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + // ---------------------------------------------------------------------------- + // MARK: customShadedRect() + + pub fn customShadedRect(uniforms: anytype, _pos: Vec2f, _dim: Vec2f) void { + var pos = _pos; + var dim = _dim; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + pos = @floor(pos); + dim = @ceil(dim); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(uniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(uniforms.start, pos[0], pos[1]); + c.glUniform2f(uniforms.size, dim[0], dim[1]); + c.glUniform1i(uniforms.color, @bitCast(color)); + c.glUniform1f(uniforms.scale, scale); + + c.glBindVertexArray(rectVAO); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + // ---------------------------------------------------------------------------- + // MARK: text() + + pub fn text(_text: []const u8, x: f32, y: f32, fontSize: f32, alignment: TextBuffer.Alignment) void { + TextRendering.renderText(_text, x, y, fontSize, .{.color = @truncate(@as(u32, @bitCast(color)))}, alignment); + } + + pub inline fn print(comptime format: []const u8, args: anytype, x: f32, y: f32, fontSize: f32, alignment: TextBuffer.Alignment) void { + const string = std.fmt.allocPrint(main.stackAllocator.allocator, format, args) catch unreachable; + defer main.stackAllocator.free(string); + text(string, x, y, fontSize, alignment); + } +}; + +pub const TextBuffer = struct { // MARK: TextBuffer + + pub const Alignment = enum { + left, + center, + right, + }; + + pub const FontEffect = packed struct(u28) { + color: u24 = 0xffffff, + bold: bool = false, + italic: bool = false, + underline: bool = false, + strikethrough: bool = false, + + fn hasLine(self: FontEffect, comptime isUnderline: bool) bool { + if(isUnderline) return self.underline; + return self.strikethrough; + } + }; + + const Line = struct { + start: f32, + end: f32, + color: u24, + isUnderline: bool, + }; + + const LineBreak = struct { + index: u32, + width: f32, + }; + + const GlyphData = struct { + x_advance: f32, + y_advance: f32, + x_offset: f32, + y_offset: f32, + character: u21, + index: u32, + cluster: u32, + fontEffect: FontEffect, + characterIndex: u32, + }; + + alignment: Alignment, + width: f32, + buffer: ?*hbft.hb_buffer_t, + glyphs: []GlyphData, + lines: main.List(Line), + lineBreaks: main.List(LineBreak), + + fn addLine(self: *TextBuffer, line: Line) void { + if(line.start != line.end) { + self.lines.append(line); + } + } + + fn initLines(self: *TextBuffer, comptime isUnderline: bool) void { + var line: Line = Line{.start = 0, .end = 0, .color = 0, .isUnderline = isUnderline}; + var lastFontEffect: FontEffect = .{}; + for(self.glyphs) |glyph| { + const fontEffect = glyph.fontEffect; + if(lastFontEffect.hasLine(isUnderline)) { + if(fontEffect.color != lastFontEffect.color) { + self.addLine(line); + line.color = fontEffect.color; + line.start = line.end; + } else if(!fontEffect.hasLine(isUnderline)) { + self.addLine(line); + } + } else if(fontEffect.hasLine(isUnderline)) { + line.start = line.end; + line.color = fontEffect.color; + } + lastFontEffect = fontEffect; + line.end += glyph.x_advance; + } + if(lastFontEffect.hasLine(isUnderline)) { + self.addLine(line); + } + } + + pub const Parser = struct { + unicodeIterator: std.unicode.Utf8Iterator, + currentFontEffect: FontEffect, + parsedText: main.List(u32), + fontEffects: main.List(FontEffect), + characterIndex: main.List(u32), + showControlCharacters: bool, + curChar: u21 = undefined, + curIndex: u32 = 0, + + fn appendControlGetNext(self: *Parser) ?void { + if(self.showControlCharacters) { + self.fontEffects.append(.{.color = 0x808080}); + self.parsedText.append(self.curChar); + self.characterIndex.append(self.curIndex); + } + self.curIndex = @intCast(self.unicodeIterator.i); + self.curChar = self.unicodeIterator.nextCodepoint() orelse return null; + } + + fn appendGetNext(self: *Parser) ?void { + self.fontEffects.append(self.currentFontEffect); + self.parsedText.append(self.curChar); + self.characterIndex.append(self.curIndex); + self.curIndex = @intCast(self.unicodeIterator.i); + self.curChar = self.unicodeIterator.nextCodepoint() orelse return null; + } + + fn peekNextByte(self: *Parser) u8 { + const next = self.unicodeIterator.peek(1); + if(next.len == 0) return 0; + return next[0]; + } + + fn parse(self: *Parser) void { + self.curIndex = @intCast(self.unicodeIterator.i); + self.curChar = self.unicodeIterator.nextCodepoint() orelse return; + while(true) switch(self.curChar) { + '*' => { + self.appendControlGetNext() orelse return; + if(self.curChar == '*') { + self.appendControlGetNext() orelse return; + self.currentFontEffect.bold = !self.currentFontEffect.bold; + } else { + self.currentFontEffect.italic = !self.currentFontEffect.italic; + } + }, + '_' => { + if(self.peekNextByte() == '_') { + self.appendControlGetNext() orelse return; + self.appendControlGetNext() orelse return; + self.currentFontEffect.underline = !self.currentFontEffect.underline; + } else { + self.appendGetNext() orelse return; + } + }, + '~' => { + if(self.peekNextByte() == '~') { + self.appendControlGetNext() orelse return; + self.appendControlGetNext() orelse return; + self.currentFontEffect.strikethrough = !self.currentFontEffect.strikethrough; + } else { + self.appendGetNext() orelse return; + } + }, + '\\' => { + self.appendControlGetNext() orelse return; + self.appendGetNext() orelse return; + }, + '#' => { + self.appendControlGetNext() orelse return; + var shift: u5 = 20; + while(true) : (shift -= 4) { + self.currentFontEffect.color = (self.currentFontEffect.color & ~(@as(u24, 0xf) << shift)) | @as(u24, switch(self.curChar) { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => self.curChar - '0', + 'a', 'b', 'c', 'd', 'e', 'f' => self.curChar - 'a' + 10, + 'A', 'B', 'C', 'D', 'E', 'F' => self.curChar - 'A' + 10, + else => 0, + }) << shift; + self.appendControlGetNext() orelse return; + if(shift == 0) break; + } + }, + '§' => { + self.currentFontEffect = .{.color = self.currentFontEffect.color}; + self.appendControlGetNext() orelse return; + }, + else => { + self.appendGetNext() orelse return; + }, + }; + } + + pub fn countVisibleCharacters(text: []const u8) usize { + var unicodeIterator = std.unicode.Utf8Iterator{.bytes = text, .i = 0}; + var count: usize = 0; + var curChar = unicodeIterator.nextCodepoint() orelse return count; + outer: while(true) switch(curChar) { + '*' => { + curChar = unicodeIterator.nextCodepoint() orelse break; + }, + '_' => { + curChar = unicodeIterator.nextCodepoint() orelse break; + if(curChar == '_') { + curChar = unicodeIterator.nextCodepoint() orelse break; + } else { + count += 1; + } + }, + '~' => { + curChar = unicodeIterator.nextCodepoint() orelse break; + if(curChar == '~') { + curChar = unicodeIterator.nextCodepoint() orelse break; + } else { + count += 1; + } + }, + '\\' => { + curChar = unicodeIterator.nextCodepoint() orelse break; + curChar = unicodeIterator.nextCodepoint() orelse break; + count += 1; + }, + '#' => { + for(0..7) |_| curChar = unicodeIterator.nextCodepoint() orelse break :outer; + }, + '§' => { + curChar = unicodeIterator.nextCodepoint() orelse break; + }, + else => { + count += 1; + curChar = unicodeIterator.nextCodepoint() orelse break; + }, + }; + return count; + } + }; + + pub fn init(allocator: NeverFailingAllocator, text: []const u8, initialFontEffect: FontEffect, showControlCharacters: bool, alignment: Alignment) TextBuffer { + var self: TextBuffer = .{ + .alignment = alignment, + .width = 1e9, + .buffer = null, + .glyphs = &.{}, + .lines = .init(allocator), + .lineBreaks = .init(allocator), + }; + // Parse the input text: + var parser = Parser{ + .unicodeIterator = std.unicode.Utf8Iterator{.bytes = text, .i = 0}, + .currentFontEffect = initialFontEffect, + .parsedText = .init(main.stackAllocator), + .fontEffects = .init(allocator), + .characterIndex = .init(allocator), + .showControlCharacters = showControlCharacters, + }; + defer parser.fontEffects.deinit(); + defer parser.parsedText.deinit(); + defer parser.characterIndex.deinit(); + parser.parse(); + if(parser.parsedText.items.len == 0) { + self.lineBreaks.append(.{.index = 0, .width = 0}); + return self; + } + + // Let harfbuzz do its thing: + const buffer = hbft.hb_buffer_create() orelse @panic("Out of Memory while creating harfbuzz buffer"); + defer hbft.hb_buffer_destroy(buffer); + hbft.hb_buffer_add_utf32(buffer, parser.parsedText.items.ptr, @intCast(parser.parsedText.items.len), 0, @intCast(parser.parsedText.items.len)); + hbft.hb_buffer_set_direction(buffer, hbft.HB_DIRECTION_LTR); + hbft.hb_buffer_set_script(buffer, hbft.HB_SCRIPT_COMMON); + hbft.hb_buffer_set_language(buffer, hbft.hb_language_get_default()); + hbft.hb_shape(TextRendering.harfbuzzFont, buffer, null, 0); + var glyphInfos: []hbft.hb_glyph_info_t = undefined; + var glyphPositions: []hbft.hb_glyph_position_t = undefined; + { + var len: c_uint = 0; + glyphInfos.ptr = hbft.hb_buffer_get_glyph_infos(buffer, &len).?; + glyphPositions.ptr = hbft.hb_buffer_get_glyph_positions(buffer, &len).?; + glyphInfos.len = len; + glyphPositions.len = len; + } + + // Guess the text index from the given cluster indices. Only works if the number of glyphs and the number of characters in a cluster is the same. + const textIndexGuess = main.stackAllocator.alloc(u32, glyphInfos.len); + defer main.stackAllocator.free(textIndexGuess); + for(textIndexGuess, 0..) |*index, i| { + if(i == 0 or glyphInfos[i - 1].cluster != glyphInfos[i].cluster) { + index.* = glyphInfos[i].cluster; + } else { + index.* = @min(textIndexGuess[i - 1] + 1, @as(u32, @intCast(parser.parsedText.items.len - 1))); + for(glyphInfos[i..]) |glyphInfo| { + if(glyphInfo.cluster != glyphInfos[i].cluster) { + index.* = @min(index.*, glyphInfo.cluster - 1); + break; + } + } + } + } + + // Merge it all together: + self.glyphs = allocator.alloc(GlyphData, glyphInfos.len); + for(self.glyphs, 0..) |*glyph, i| { + glyph.x_advance = @as(f32, @floatFromInt(glyphPositions[i].x_advance))/TextRendering.fontUnitsPerPixel; + glyph.y_advance = @as(f32, @floatFromInt(glyphPositions[i].y_advance))/TextRendering.fontUnitsPerPixel; + glyph.x_offset = @as(f32, @floatFromInt(glyphPositions[i].x_offset))/TextRendering.fontUnitsPerPixel; + glyph.y_offset = @as(f32, @floatFromInt(glyphPositions[i].y_offset))/TextRendering.fontUnitsPerPixel; + glyph.character = @intCast(parser.parsedText.items[textIndexGuess[i]]); + glyph.index = glyphInfos[i].codepoint; + glyph.cluster = glyphInfos[i].cluster; + glyph.fontEffect = parser.fontEffects.items[textIndexGuess[i]]; + glyph.characterIndex = parser.characterIndex.items[textIndexGuess[i]]; + } + + // Find the lines: + self.initLines(true); + self.initLines(false); + self.lineBreaks.append(.{.index = 0, .width = 0}); + self.lineBreaks.append(.{.index = @intCast(self.glyphs.len), .width = 0}); + return self; + } + + pub fn deinit(self: TextBuffer) void { + self.lines.allocator.free(self.glyphs); + self.lines.deinit(); + self.lineBreaks.deinit(); + } + + fn getLineOffset(self: TextBuffer, line: usize) f32 { + const factor: f32 = switch(self.alignment) { + .left => 0, + .center => 0.5, + .right => 1, + }; + const diff = self.width - self.lineBreaks.items[line + 1].width; + return diff*factor; + } + + pub fn mousePosToIndex(self: TextBuffer, mousePos: Vec2f, bufferLen: usize) u32 { + var line: usize = @intFromFloat(@max(0, mousePos[1]/16.0)); + line = @min(line, self.lineBreaks.items.len - 2); + var x: f32 = self.getLineOffset(line); + const start = self.lineBreaks.items[line].index; + const end = self.lineBreaks.items[line + 1].index; + for(self.glyphs[start..end]) |glyph| { + if(mousePos[0] < x + glyph.x_advance/2) { + return @intCast(glyph.characterIndex); + } + + x += glyph.x_advance; + } + return @intCast(if(end < self.glyphs.len) self.glyphs[end - 1].characterIndex else bufferLen); + } + + pub fn indexToCursorPos(self: TextBuffer, index: u32) Vec2f { + var x: f32 = 0; + var y: f32 = 0; + var i: usize = 0; + while(true) { + x = self.getLineOffset(i); + for(self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| { + if(glyph.characterIndex == index) { + return .{x, y}; + } + + x += glyph.x_advance; + y -= glyph.y_advance; + } + i += 1; + if(i >= self.lineBreaks.items.len - 1) { + return .{x, y}; + } + y += 16; + } + } + + /// Returns the calculated dimensions of the text block. + pub fn calculateLineBreaks(self: *TextBuffer, fontSize: f32, maxLineWidth: f32) Vec2f { + self.lineBreaks.clearRetainingCapacity(); + const spaceCharacterWidth = 8; + self.lineBreaks.append(.{.index = 0, .width = 0}); + const scaledMaxWidth = maxLineWidth/fontSize*16.0; + var lineWidth: f32 = 0; + var lastSpaceWidth: f32 = 0; + var lastSpaceIndex: u32 = 0; + for(self.glyphs, 0..) |glyph, i| { + lineWidth += glyph.x_advance; + if(glyph.character == ' ') { + lastSpaceWidth = lineWidth; + lastSpaceIndex = @intCast(i + 1); + } + if(glyph.character == '\n') { + self.lineBreaks.append(.{.index = @intCast(i + 1), .width = lineWidth - spaceCharacterWidth}); + lineWidth = 0; + lastSpaceIndex = 0; + lastSpaceWidth = 0; + } + if(lineWidth > scaledMaxWidth) { + if(lastSpaceIndex != 0) { + lineWidth -= lastSpaceWidth; + self.lineBreaks.append(.{.index = lastSpaceIndex, .width = lastSpaceWidth - spaceCharacterWidth}); + lastSpaceIndex = 0; + lastSpaceWidth = 0; + } else { + self.lineBreaks.append(.{.index = @intCast(i), .width = lineWidth - glyph.x_advance}); + lineWidth = glyph.x_advance; + lastSpaceIndex = 0; + lastSpaceWidth = 0; + } + } + } + self.width = maxLineWidth; + self.lineBreaks.append(.{.index = @intCast(self.glyphs.len), .width = lineWidth}); + return Vec2f{maxLineWidth*fontSize/16.0, @as(f32, @floatFromInt(self.lineBreaks.items.len - 1))*fontSize}; + } + + pub fn drawSelection(self: TextBuffer, pos: Vec2f, selectionStart: u32, selectionEnd: u32) void { + std.debug.assert(selectionStart <= selectionEnd); + var x: f32 = self.getLineOffset(0); + var y: f32 = 0; + var i: usize = 0; + var j: usize = 0; + // Find the start row: + outer: while(i < self.lineBreaks.items.len - 1) : (i += 1) { + x = self.getLineOffset(i); + while(j < self.lineBreaks.items[i + 1].index) : (j += 1) { + const glyph = self.glyphs[j]; + if(glyph.characterIndex >= selectionStart) break :outer; + x += glyph.x_advance; + y -= glyph.y_advance; + } + y += 16; + } + while(i < self.lineBreaks.items.len - 1) { + const startX = x; + while(j < self.lineBreaks.items[i + 1].index and j < selectionEnd) : (j += 1) { + const glyph = self.glyphs[j]; + if(glyph.characterIndex >= selectionEnd) break; + x += glyph.x_advance; + y -= glyph.y_advance; + } + draw.rect(pos + Vec2f{startX, y}, .{x - startX, 16}); + i += 1; + if(i >= self.lineBreaks.items.len - 1) break; + x = self.getLineOffset(i); + y += 16; + } + } + + pub fn render(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) void { + self.renderShadow(_x, _y, _fontSize); + self.renderTextWithoutShadow(_x, _y, _fontSize); + } + + pub fn renderTextWithoutShadow(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) void { + const oldTranslation = draw.setTranslation(.{_x, _y}); + defer draw.restoreTranslation(oldTranslation); + const oldScale = draw.setScale(_fontSize/16.0); + defer draw.restoreScale(oldScale); + var x: f32 = 0; + var y: f32 = 0; + TextRendering.pipeline.bind(draw.getScissor()); + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(TextRendering.uniforms.scene, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform1f(TextRendering.uniforms.ratio, draw.scale); + c.glUniform1f(TextRendering.uniforms.alpha, @as(f32, @floatFromInt(draw.color >> 24))/255.0); + c.glActiveTexture(c.GL_TEXTURE0); + c.glBindTexture(c.GL_TEXTURE_2D, TextRendering.glyphTexture[0]); + c.glBindVertexArray(draw.rectVAO); + const lineWraps: []f32 = main.stackAllocator.alloc(f32, self.lineBreaks.items.len - 1); + defer main.stackAllocator.free(lineWraps); + var i: usize = 0; + while(i < self.lineBreaks.items.len - 1) : (i += 1) { + x = self.getLineOffset(i); + for(self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| { + if(glyph.character != '\n') { + const ftGlyph = TextRendering.getGlyph(glyph.index) catch continue; + TextRendering.drawGlyph(ftGlyph, x + glyph.x_offset, y - glyph.y_offset, @bitCast(glyph.fontEffect)); + } + x += glyph.x_advance; + y -= glyph.y_advance; + } + lineWraps[i] = x - self.getLineOffset(i); + x = 0; + y += 16; + } + + for(self.lines.items) |_line| { + var line: Line = _line; + y = 0; + y += if(line.isUnderline) 15 else 8; + const oldColor = draw.color; + draw.setColor(line.color | (@as(u32, 0xff000000) & draw.color)); + defer draw.setColor(oldColor); + for(lineWraps, 0..) |lineWrap, j| { + const lineStart = @max(0, line.start); + const lineEnd = @min(lineWrap, line.end); + if(lineStart < lineEnd) { + const start = Vec2f{lineStart + self.getLineOffset(j), y}; + const dim = Vec2f{lineEnd - lineStart, 1}; + draw.rect(start, dim); + } + line.start -= lineWrap; + line.end -= lineWrap; + y += 16; + } + } + } + + fn shadowColor(color: u24) u24 { + const r: f32 = @floatFromInt(color >> 16); + const g: f32 = @floatFromInt(color >> 8 & 255); + const b: f32 = @floatFromInt(color & 255); + const perceivedBrightness = @sqrt(0.299*r*r + 0.587*g*g + 0.114*b*b); + if(perceivedBrightness < 64) { + return 0xffffff; // Make shadows white for better readability. + } else { + return 0; + } + } + + fn renderShadow(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) void { // Basically a copy of render with some color and position changes. + const oldTranslation = draw.setTranslation(.{_x + _fontSize/16.0, _y + _fontSize/16.0}); + defer draw.restoreTranslation(oldTranslation); + const oldScale = draw.setScale(_fontSize/16.0); + defer draw.restoreScale(oldScale); + var x: f32 = 0; + var y: f32 = 0; + TextRendering.pipeline.bind(draw.getScissor()); + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(TextRendering.uniforms.scene, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform1f(TextRendering.uniforms.ratio, draw.scale); + c.glUniform1f(TextRendering.uniforms.alpha, @as(f32, @floatFromInt(draw.color >> 24))/255.0); + c.glActiveTexture(c.GL_TEXTURE0); + c.glBindTexture(c.GL_TEXTURE_2D, TextRendering.glyphTexture[0]); + c.glBindVertexArray(draw.rectVAO); + const lineWraps: []f32 = main.stackAllocator.alloc(f32, self.lineBreaks.items.len - 1); + defer main.stackAllocator.free(lineWraps); + var i: usize = 0; + while(i < self.lineBreaks.items.len - 1) : (i += 1) { + x = self.getLineOffset(i); + for(self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| { + if(glyph.character != '\n') { + const ftGlyph = TextRendering.getGlyph(glyph.index) catch continue; + var fontEffect = glyph.fontEffect; + fontEffect.color = shadowColor(fontEffect.color); + TextRendering.drawGlyph(ftGlyph, x + glyph.x_offset, y - glyph.y_offset, @bitCast(fontEffect)); + } + x += glyph.x_advance; + y -= glyph.y_advance; + } + lineWraps[i] = x - self.getLineOffset(i); + x = 0; + y += 16; + } + + for(self.lines.items) |_line| { + var line: Line = _line; + y = 0; + y += if(line.isUnderline) 15 else 8; + const oldColor = draw.color; + draw.setColor(shadowColor(line.color) | (@as(u32, 0xff000000) & draw.color)); + defer draw.setColor(oldColor); + for(lineWraps, 0..) |lineWrap, j| { + const lineStart = @max(0, line.start); + const lineEnd = @min(lineWrap, line.end); + if(lineStart < lineEnd) { + const start = Vec2f{lineStart + self.getLineOffset(j), y}; + const dim = Vec2f{lineEnd - lineStart, 1}; + draw.rect(start, dim); + } + line.start -= lineWrap; + line.end -= lineWrap; + y += 16; + } + } + } +}; + +const TextRendering = struct { // MARK: TextRendering + const Glyph = struct { + textureX: i32, + size: Vec2i, + bearing: Vec2i, + advance: f32, + }; + var pipeline: Pipeline = undefined; + var uniforms: struct { + texture_rect: c_int, + scene: c_int, + offset: c_int, + ratio: c_int, + fontEffects: c_int, + fontSize: c_int, + alpha: c_int, + } = undefined; + + var freetypeLib: hbft.FT_Library = undefined; + var freetypeFace: hbft.FT_Face = undefined; + var harfbuzzFace: ?*hbft.hb_face_t = undefined; + var harfbuzzFont: ?*hbft.hb_font_t = undefined; + var glyphMapping: main.List(u31) = undefined; + var glyphData: main.List(Glyph) = undefined; + var glyphTexture: [2]c_uint = undefined; + var textureWidth: i32 = 1024; + const textureHeight: i32 = 16; + var textureOffset: i32 = 0; + var fontUnitsPerPixel: f32 = undefined; + + fn ftError(errorCode: hbft.FT_Error) !void { + if(errorCode == 0) return; + const errorString = hbft.FT_Error_String(errorCode); + std.log.err("Got freetype error {s}", .{errorString}); + return error.freetype; + } + + fn init() !void { + pipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/Text.vert", + "assets/cubyz/shaders/graphics/Text.frag", + "", + &uniforms, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.alphaBlending}}, + ); + pipeline.bind(null); + errdefer pipeline.deinit(); + c.glUniform1f(uniforms.alpha, 1.0); + c.glUniform2f(uniforms.fontSize, @floatFromInt(textureWidth), @floatFromInt(textureHeight)); + try ftError(hbft.FT_Init_FreeType(&freetypeLib)); + try ftError(hbft.FT_New_Face(freetypeLib, "assets/cubyz/fonts/unscii-16-full.ttf", 0, &freetypeFace)); + try ftError(hbft.FT_Set_Pixel_Sizes(freetypeFace, 0, textureHeight)); + harfbuzzFace = hbft.hb_ft_face_create_referenced(freetypeFace); + harfbuzzFont = hbft.hb_font_create(harfbuzzFace); + fontUnitsPerPixel = @as(f32, @floatFromInt(freetypeFace.*.units_per_EM))/@as(f32, @floatFromInt(textureHeight)); + + glyphMapping = .init(main.globalAllocator); + glyphData = .init(main.globalAllocator); + glyphData.append(undefined); // 0 is a reserved value. + c.glGenTextures(2, &glyphTexture); + c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[0]); + c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R8, textureWidth, textureHeight, 0, c.GL_RED, c.GL_UNSIGNED_BYTE, null); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[1]); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + } + + fn deinit() void { + pipeline.deinit(); + ftError(hbft.FT_Done_FreeType(freetypeLib)) catch {}; + glyphMapping.deinit(); + glyphData.deinit(); + c.glDeleteTextures(2, &glyphTexture); + hbft.hb_font_destroy(harfbuzzFont); + } + + fn resizeTexture(newWidth: i32) void { + textureWidth = newWidth; + const swap = glyphTexture[1]; + glyphTexture[1] = glyphTexture[0]; + glyphTexture[0] = swap; + c.glActiveTexture(c.GL_TEXTURE0); + c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[0]); + c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R8, newWidth, textureHeight, 0, c.GL_RED, c.GL_UNSIGNED_BYTE, null); + c.glCopyImageSubData(glyphTexture[1], c.GL_TEXTURE_2D, 0, 0, 0, 0, glyphTexture[0], c.GL_TEXTURE_2D, 0, 0, 0, 0, textureOffset, textureHeight, 1); + pipeline.bind(draw.getScissor()); + c.glUniform2f(uniforms.fontSize, @floatFromInt(textureWidth), @floatFromInt(textureHeight)); + } + + fn uploadData(bitmap: hbft.FT_Bitmap) void { + const width: i32 = @bitCast(bitmap.width); + const height: i32 = @bitCast(bitmap.rows); + const buffer = bitmap.buffer orelse return; + if(textureOffset + width > textureWidth) { + resizeTexture(textureWidth*2); + } + c.glPixelStorei(c.GL_UNPACK_ALIGNMENT, 1); + c.glTexSubImage2D(c.GL_TEXTURE_2D, 0, textureOffset, 0, width, height, c.GL_RED, c.GL_UNSIGNED_BYTE, buffer); + textureOffset += width; + } + + fn getGlyph(index: u32) !Glyph { + if(index >= glyphMapping.items.len) { + glyphMapping.appendNTimes(0, index - glyphMapping.items.len + 1); + } + if(glyphMapping.items[index] == 0) { // glyph was not initialized yet. + try ftError(hbft.FT_Load_Glyph(freetypeFace, index, hbft.FT_LOAD_RENDER)); + const glyph = freetypeFace.*.glyph; + const bitmap = glyph.*.bitmap; + const width = bitmap.width; + const height = bitmap.rows; + glyphMapping.items[index] = @intCast(glyphData.items.len); + glyphData.addOne().* = Glyph{ + .textureX = textureOffset, + .size = Vec2i{@intCast(width), @intCast(height)}, + .bearing = Vec2i{glyph.*.bitmap_left, 16 - glyph.*.bitmap_top}, + .advance = @as(f32, @floatFromInt(glyph.*.advance.x))/@as(f32, 1 << 6), + }; + uploadData(bitmap); + } + return glyphData.items[glyphMapping.items[index]]; + } + + fn drawGlyph(glyph: Glyph, _x: f32, _y: f32, fontEffects: u28) void { + var x = _x; + var y = _y; + x *= draw.scale; + y *= draw.scale; + x += draw.translation[0]; + y += draw.translation[1]; + x = @floor(x); + y = @ceil(y); + c.glUniform1i(uniforms.fontEffects, fontEffects); + if(fontEffects & 0x1000000 != 0) { // bold + c.glUniform2f(uniforms.offset, @as(f32, @floatFromInt(glyph.bearing[0]))*draw.scale + x, @as(f32, @floatFromInt(glyph.bearing[1]))*draw.scale + y - 1); + c.glUniform4f(uniforms.texture_rect, @floatFromInt(glyph.textureX), -1, @floatFromInt(glyph.size[0]), @floatFromInt(glyph.size[1] + 1)); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + // Just draw another thing on top in x direction. The y-direction is handled in the shader. + c.glUniform2f(uniforms.offset, @as(f32, @floatFromInt(glyph.bearing[0]))*draw.scale + x + 0.5, @as(f32, @floatFromInt(glyph.bearing[1]))*draw.scale + y - 1); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } else { + c.glUniform2f(uniforms.offset, @as(f32, @floatFromInt(glyph.bearing[0]))*draw.scale + x, @as(f32, @floatFromInt(glyph.bearing[1]))*draw.scale + y); + c.glUniform4f(uniforms.texture_rect, @floatFromInt(glyph.textureX), 0, @floatFromInt(glyph.size[0]), @floatFromInt(glyph.size[1])); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + } + + fn renderText(text: []const u8, x: f32, y: f32, fontSize: f32, initialFontEffect: TextBuffer.FontEffect, alignment: TextBuffer.Alignment) void { + const buf = TextBuffer.init(main.stackAllocator, text, initialFontEffect, false, alignment); + defer buf.deinit(); + + buf.render(x, y, fontSize); + } +}; + +pub fn init() void { // MARK: init() + draw.initCircle(); + draw.initDrawRect(); + draw.initImage(); + draw.initLine(); + draw.initRect(); + draw.initRectBorder(); + TextRendering.init() catch |err| { + std.log.err("Error while initializing TextRendering: {s}", .{@errorName(err)}); + }; + block_texture.init(); + if(glslang.glslang_initialize_process() == glslang.false) std.log.err("glslang_initialize_process failed", .{}); +} + +pub fn deinit() void { + draw.deinitCircle(); + draw.deinitDrawRect(); + draw.deinitImage(); + draw.deinitLine(); + draw.deinitRect(); + draw.deinitRectBorder(); + TextRendering.deinit(); + block_texture.deinit(); + glslang.glslang_finalize_process(); +} + +const Shader = struct { // MARK: Shader + id: c_uint, + + fn compileToSpirV(allocator: NeverFailingAllocator, source: []const u8, filename: []const u8, defines: []const u8, shaderStage: glslang.glslang_stage_t) ![]c_uint { + const versionLineEnd = if(std.mem.indexOfScalar(u8, source, '\n')) |len| len + 1 else 0; + const versionLine = source[0..versionLineEnd]; + const sourceLines = source[versionLineEnd..]; + + var sourceWithDefines = main.List(u8).init(main.stackAllocator); + defer sourceWithDefines.deinit(); + sourceWithDefines.appendSlice(versionLine); + sourceWithDefines.appendSlice(defines); + sourceWithDefines.appendSlice(sourceLines); + sourceWithDefines.append(0); + + const input = glslang.glslang_input_t{ + .language = glslang.GLSLANG_SOURCE_GLSL, + .stage = shaderStage, + .client = glslang.GLSLANG_CLIENT_OPENGL, + .client_version = glslang.GLSLANG_TARGET_OPENGL_450, + .target_language = glslang.GLSLANG_TARGET_SPV, + .target_language_version = glslang.GLSLANG_TARGET_SPV_1_0, + .code = sourceWithDefines.items.ptr, + .default_version = 100, + .default_profile = glslang.GLSLANG_NO_PROFILE, + .force_default_version_and_profile = glslang.false, + .forward_compatible = glslang.false, + .messages = glslang.GLSLANG_MSG_DEFAULT_BIT, + .resource = glslang.glslang_default_resource(), + .callbacks = .{}, // TODO: Add support for shader includes + .callbacks_ctx = null, + }; + const shader = glslang.glslang_shader_create(&input); + defer glslang.glslang_shader_delete(shader); + if(glslang.glslang_shader_preprocess(shader, &input) == 0) { + std.log.err("Error preprocessing shader {s}:\n{s}\n{s}\n", .{filename, glslang.glslang_shader_get_info_log(shader), glslang.glslang_shader_get_info_debug_log(shader)}); + return error.FailedCompiling; + } + + if(glslang.glslang_shader_parse(shader, &input) == 0) { + std.log.err("Error parsing shader {s}:\n{s}\n{s}\n", .{filename, glslang.glslang_shader_get_info_log(shader), glslang.glslang_shader_get_info_debug_log(shader)}); + return error.FailedCompiling; + } + + const program = glslang.glslang_program_create(); + defer glslang.glslang_program_delete(program); + glslang.glslang_program_add_shader(program, shader); + + if(glslang.glslang_program_link(program, glslang.GLSLANG_MSG_SPV_RULES_BIT | glslang.GLSLANG_MSG_VULKAN_RULES_BIT) == 0) { + std.log.err("Error linking shader {s}:\n{s}\n{s}\n", .{filename, glslang.glslang_shader_get_info_log(shader), glslang.glslang_shader_get_info_debug_log(shader)}); + return error.FailedCompiling; + } + + glslang.glslang_program_SPIRV_generate(program, shaderStage); + const result = allocator.alloc(c_uint, glslang.glslang_program_SPIRV_get_size(program)); + glslang.glslang_program_SPIRV_get(program, result.ptr); + return result; + } + + fn addShader(self: *const Shader, filename: []const u8, defines: []const u8, shaderStage: c_uint) !void { + const source = main.files.cwd().read(main.stackAllocator, filename) catch |err| { + std.log.err("Couldn't read shader file: {s}", .{filename}); + return err; + }; + defer main.stackAllocator.free(source); + + // SPIR-V will be used for the Vulkan, now it's completely useless due to lack of support in Vulkan drivers + const glslangStage: glslang.glslang_stage_t = if(shaderStage == c.GL_VERTEX_SHADER) glslang.GLSLANG_STAGE_VERTEX else if(shaderStage == c.GL_FRAGMENT_SHADER) glslang.GLSLANG_STAGE_FRAGMENT else glslang.GLSLANG_STAGE_COMPUTE; + main.stackAllocator.free(try compileToSpirV(main.stackAllocator, source, filename, defines, glslangStage)); + + const shader = c.glCreateShader(shaderStage); + defer c.glDeleteShader(shader); + + const versionLineEnd = if(std.mem.indexOfScalar(u8, source, '\n')) |len| len + 1 else 0; + const versionLine = source[0..versionLineEnd]; + const sourceLines = source[versionLineEnd..]; + + const sourceLen: [3]c_int = .{@intCast(versionLine.len), @intCast(defines.len), @intCast(sourceLines.len)}; + c.glShaderSource(shader, 3, &[3][*c]const u8{versionLine.ptr, defines.ptr, sourceLines.ptr}, &sourceLen); + + c.glCompileShader(shader); + + var success: c_int = undefined; + c.glGetShaderiv(shader, c.GL_COMPILE_STATUS, &success); + if(success != c.GL_TRUE) { + var len: u32 = undefined; + c.glGetShaderiv(shader, c.GL_INFO_LOG_LENGTH, @ptrCast(&len)); + var buf: [4096]u8 = undefined; + c.glGetShaderInfoLog(shader, 4096, @ptrCast(&len), &buf); + std.log.err("Error compiling shader {s}:\n{s}\n", .{filename, buf[0..len]}); + return error.FailedCompiling; + } + + c.glAttachShader(self.id, shader); + } + + fn link(self: *const Shader, file: []const u8) !void { + c.glLinkProgram(self.id); + + var success: c_int = undefined; + c.glGetProgramiv(self.id, c.GL_LINK_STATUS, &success); + if(success != c.GL_TRUE) { + var len: u32 = undefined; + c.glGetProgramiv(self.id, c.GL_INFO_LOG_LENGTH, @ptrCast(&len)); + var buf: [4096]u8 = undefined; + c.glGetProgramInfoLog(self.id, 4096, @ptrCast(&len), &buf); + std.log.err("Error Linking Shader program {s}:\n{s}\n", .{file, buf[0..len]}); + return error.FailedLinking; + } + } + + fn init(vertex: []const u8, fragment: []const u8, defines: []const u8, uniformStruct: anytype) Shader { + const shader = Shader{.id = c.glCreateProgram()}; + shader.addShader(vertex, defines, c.GL_VERTEX_SHADER) catch return shader; + shader.addShader(fragment, defines, c.GL_FRAGMENT_SHADER) catch return shader; + shader.link(fragment) catch return shader; + + if(@TypeOf(uniformStruct) != @TypeOf(null)) { + inline for(@typeInfo(@TypeOf(uniformStruct.*)).@"struct".fields) |field| { + if(field.type == c_int) { + @field(uniformStruct, field.name) = c.glGetUniformLocation(shader.id, field.name[0..]); + } + } + } + return shader; + } + + fn initCompute(compute: []const u8, defines: []const u8, uniformStruct: anytype) Shader { + const shader = Shader{.id = c.glCreateProgram()}; + shader.addShader(compute, defines, c.GL_COMPUTE_SHADER) catch return shader; + shader.link(compute) catch return shader; + + if(@TypeOf(uniformStruct) != @TypeOf(null)) { + inline for(@typeInfo(@TypeOf(uniformStruct.*)).@"struct".fields) |field| { + if(field.type == c_int) { + @field(uniformStruct, field.name) = c.glGetUniformLocation(shader.id, field.name[0..]); + } + } + } + return shader; + } + + fn bind(self: *const Shader) void { + c.glUseProgram(self.id); + } + + fn deinit(self: *const Shader) void { + c.glDeleteProgram(self.id); + } +}; + +pub const Pipeline = struct { // MARK: Pipeline + shader: Shader, + rasterState: RasterizationState, + multisampleState: MultisampleState = .{}, // TODO: Not implemented + depthStencilState: DepthStencilState, + blendState: ColorBlendState, + + const RasterizationState = struct { + depthClamp: bool = true, + rasterizerDiscard: bool = false, + polygonMode: PolygonMode = .fill, + cullMode: CullModeFlags = .back, + frontFace: FrontFace = .counterClockwise, + depthBias: ?DepthBias = null, + lineWidth: f32 = 1, + + const PolygonMode = enum(c.VkPolygonMode) { + fill = c.VK_POLYGON_MODE_FILL, + line = c.VK_POLYGON_MODE_LINE, + point = c.VK_POLYGON_MODE_POINT, + }; + + const CullModeFlags = enum(c.VkCullModeFlags) { + none = c.VK_CULL_MODE_NONE, + front = c.VK_CULL_MODE_FRONT_BIT, + back = c.VK_CULL_MODE_BACK_BIT, + frontAndBack = c.VK_CULL_MODE_FRONT_AND_BACK, + }; + + const FrontFace = enum(c.VkFrontFace) { + counterClockwise = c.VK_FRONT_FACE_COUNTER_CLOCKWISE, + clockwise = c.VK_FRONT_FACE_CLOCKWISE, + }; + + const DepthBias = struct { + constantFactor: f32, + clamp: f32, + slopeFactor: f32, + }; + }; + + const MultisampleState = struct { + rasterizationSamples: Count = .@"1", + sampleShading: bool = false, + minSampleShading: f32 = undefined, + sampleMask: [*]const c.VkSampleMask = &.{0, 0}, + alphaToCoverage: bool = false, + alphaToOne: bool = false, + + const Count = enum(c.VkSampleCountFlags) { + @"1" = c.VK_SAMPLE_COUNT_1_BIT, + @"2" = c.VK_SAMPLE_COUNT_2_BIT, + @"4" = c.VK_SAMPLE_COUNT_4_BIT, + @"8" = c.VK_SAMPLE_COUNT_8_BIT, + @"16" = c.VK_SAMPLE_COUNT_16_BIT, + @"32" = c.VK_SAMPLE_COUNT_32_BIT, + @"64" = c.VK_SAMPLE_COUNT_64_BIT, + }; + }; + + const DepthStencilState = struct { + depthTest: bool, + depthWrite: bool = true, + depthCompare: CompareOp = .less, + depthBoundsTest: ?DepthBoundsTest = null, + stencilTest: ?StencilTest = null, + + const CompareOp = enum(c.VkCompareOp) { + never = c.VK_COMPARE_OP_NEVER, + less = c.VK_COMPARE_OP_LESS, + equal = c.VK_COMPARE_OP_EQUAL, + lessOrEqual = c.VK_COMPARE_OP_LESS_OR_EQUAL, + greater = c.VK_COMPARE_OP_GREATER, + notEqual = c.VK_COMPARE_OP_NOT_EQUAL, + greateOrEqual = c.VK_COMPARE_OP_GREATER_OR_EQUAL, + always = c.VK_COMPARE_OP_ALWAYS, + }; + + const StencilTest = struct { + front: StencilOpState, + back: StencilOpState, + + const StencilOpState = struct { + failOp: StencilOp, + passOp: StencilOp, + depthFailOp: StencilOp, + compareOp: CompareOp, + compareMask: u32, + writeMask: u32, + reference: u32, + + const StencilOp = enum(c.VkStencilOp) { + keep = c.VK_STENCIL_OP_KEEP, + zero = c.VK_STENCIL_OP_ZERO, + replace = c.VK_STENCIL_OP_REPLACE, + incrementAndClamp = c.VK_STENCIL_OP_INCREMENT_AND_CLAMP, + decrementAndClamp = c.VK_STENCIL_OP_DECREMENT_AND_CLAMP, + invert = c.VK_STENCIL_OP_INVERT, + incrementAndWrap = c.VK_STENCIL_OP_INCREMENT_AND_WRAP, + decrementAndWrap = c.VK_STENCIL_OP_DECREMENT_AND_WRAP, + }; + }; + }; + + const DepthBoundsTest = struct { + min: f32, + max: f32, + }; + }; + + const ColorBlendAttachmentState = struct { + enabled: bool = true, + srcColorBlendFactor: BlendFactor, + dstColorBlendFactor: BlendFactor, + colorBlendOp: BlendOp, + srcAlphaBlendFactor: BlendFactor, + dstAlphaBlendFactor: BlendFactor, + alphaBlendOp: BlendOp, + colorWriteMask: ColorComponentFlags = .all, + + pub const alphaBlending: ColorBlendAttachmentState = .{ + .srcColorBlendFactor = .srcAlpha, + .dstColorBlendFactor = .oneMinusSrcAlpha, + .colorBlendOp = .add, + .srcAlphaBlendFactor = .srcAlpha, + .dstAlphaBlendFactor = .oneMinusSrcAlpha, + .alphaBlendOp = .add, + }; + pub const noBlending: ColorBlendAttachmentState = .{ + .enabled = false, + .srcColorBlendFactor = undefined, + .dstColorBlendFactor = undefined, + .colorBlendOp = undefined, + .srcAlphaBlendFactor = undefined, + .dstAlphaBlendFactor = undefined, + .alphaBlendOp = undefined, + }; + + const BlendFactor = enum(c.VkBlendFactor) { + zero = c.VK_BLEND_FACTOR_ZERO, + one = c.VK_BLEND_FACTOR_ONE, + srcColor = c.VK_BLEND_FACTOR_SRC_COLOR, + oneMinusSrcColor = c.VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR, + dstColor = c.VK_BLEND_FACTOR_DST_COLOR, + oneMinusDstColor = c.VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR, + srcAlpha = c.VK_BLEND_FACTOR_SRC_ALPHA, + oneMinusSrcAlpha = c.VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, + dstAlpha = c.VK_BLEND_FACTOR_DST_ALPHA, + oneMinusDstAlpha = c.VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA, + constantColor = c.VK_BLEND_FACTOR_CONSTANT_COLOR, + oneMinusConstantColor = c.VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR, + constantAlpha = c.VK_BLEND_FACTOR_CONSTANT_ALPHA, + oneMinusConstantAlpha = c.VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA, + srcAlphaSaturate = c.VK_BLEND_FACTOR_SRC_ALPHA_SATURATE, + src1Color = c.VK_BLEND_FACTOR_SRC1_COLOR, + oneMinusSrc1Color = c.VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR, + src1Alpha = c.VK_BLEND_FACTOR_SRC1_ALPHA, + oneMinusSrc1Alpha = c.VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA, + + fn toGl(self: BlendFactor) c.GLenum { + return switch(self) { + .zero => c.GL_ZERO, + .one => c.GL_ONE, + .srcColor => c.GL_SRC_COLOR, + .oneMinusSrcColor => c.GL_ONE_MINUS_SRC_COLOR, + .dstColor => c.GL_DST_COLOR, + .oneMinusDstColor => c.GL_ONE_MINUS_DST_COLOR, + .srcAlpha => c.GL_SRC_ALPHA, + .oneMinusSrcAlpha => c.GL_ONE_MINUS_SRC_ALPHA, + .dstAlpha => c.GL_DST_ALPHA, + .oneMinusDstAlpha => c.GL_ONE_MINUS_DST_ALPHA, + .constantColor => c.GL_CONSTANT_COLOR, + .oneMinusConstantColor => c.GL_ONE_MINUS_CONSTANT_COLOR, + .constantAlpha => c.GL_CONSTANT_ALPHA, + .oneMinusConstantAlpha => c.GL_ONE_MINUS_CONSTANT_ALPHA, + .srcAlphaSaturate => c.GL_SRC_ALPHA_SATURATE, + .src1Color => c.GL_SRC1_COLOR, + .oneMinusSrc1Color => c.GL_ONE_MINUS_SRC1_COLOR, + .src1Alpha => c.GL_SRC1_ALPHA, + .oneMinusSrc1Alpha => c.GL_ONE_MINUS_SRC1_ALPHA, + }; + } + }; + + const BlendOp = enum(c.VkBlendOp) { + add = c.VK_BLEND_OP_ADD, + subtract = c.VK_BLEND_OP_SUBTRACT, + reverseSubtract = c.VK_BLEND_OP_REVERSE_SUBTRACT, + min = c.VK_BLEND_OP_MIN, + max = c.VK_BLEND_OP_MAX, + + fn toGl(self: BlendOp) c.GLenum { + return switch(self) { + .add => c.GL_FUNC_ADD, + .subtract => c.GL_FUNC_SUBTRACT, + .reverseSubtract => c.GL_FUNC_REVERSE_SUBTRACT, + .min => c.GL_MIN, + .max => c.GL_MAX, + }; + } + }; + + const ColorComponentFlags = packed struct { + r: bool, + g: bool, + b: bool, + a: bool, + pub const all: ColorComponentFlags = .{.r = true, .g = true, .b = true, .a = true}; + pub const none: ColorComponentFlags = .{.r = false, .g = false, .b = false, .a = false}; + }; + }; + + const ColorBlendState = struct { + logicOp: ?LogicOp = null, + attachments: []const ColorBlendAttachmentState, + blendConstants: [4]f32 = .{0, 0, 0, 0}, + + const LogicOp = enum(c.VkLogicOp) { + clear = c.VK_LOGIC_OP_CLEAR, + @"and" = c.VK_LOGIC_OP_AND, + andReverse = c.VK_LOGIC_OP_AND_REVERSE, + copy = c.VK_LOGIC_OP_COPY, + andInverted = c.VK_LOGIC_OP_AND_INVERTED, + noOp = c.VK_LOGIC_OP_NO_OP, + xor = c.VK_LOGIC_OP_XOR, + @"or" = c.VK_LOGIC_OP_OR, + nor = c.VK_LOGIC_OP_NOR, + equivalent = c.VK_LOGIC_OP_EQUIVALENT, + invert = c.VK_LOGIC_OP_INVERT, + orReverse = c.VK_LOGIC_OP_OR_REVERSE, + copyInverted = c.VK_LOGIC_OP_COPY_INVERTED, + orInverted = c.VK_LOGIC_OP_OR_INVERTED, + nand = c.VK_LOGIC_OP_NAND, + set = c.VK_LOGIC_OP_SET, + }; + }; + + pub fn init(vertexPath: []const u8, fragmentPath: []const u8, defines: []const u8, uniformStruct: anytype, rasterState: RasterizationState, depthStencilState: DepthStencilState, blendState: ColorBlendState) Pipeline { + std.debug.assert(depthStencilState.depthBoundsTest == null); // Only available in Vulkan 1.3 + std.debug.assert(depthStencilState.stencilTest == null); // TODO: Not yet implemented + std.debug.assert(rasterState.lineWidth <= 1); // Larger values are poorly supported among drivers + std.debug.assert(blendState.logicOp == null); // TODO: Not yet implemented + return .{ + .shader = .init(vertexPath, fragmentPath, defines, uniformStruct), + .rasterState = rasterState, + .multisampleState = .{}, // TODO: Not implemented + .depthStencilState = depthStencilState, + .blendState = blendState, + }; + } + + pub fn deinit(self: Pipeline) void { + self.shader.deinit(); + } + + fn conditionalEnable(typ: c.GLenum, val: bool) void { + if(val) { + c.glEnable(typ); + } else { + c.glDisable(typ); + } + } + + pub fn bind(self: Pipeline, scissor: ?c.VkRect2D) void { + self.shader.bind(); + if(scissor) |s| { + c.glEnable(c.GL_SCISSOR_TEST); + c.glScissor(s.offset.x, s.offset.y, @intCast(s.extent.width), @intCast(s.extent.height)); + } else { + c.glDisable(c.GL_SCISSOR_TEST); + } + + conditionalEnable(c.GL_DEPTH_CLAMP, self.rasterState.depthClamp); + conditionalEnable(c.GL_RASTERIZER_DISCARD, self.rasterState.rasterizerDiscard); + conditionalEnable(c.GL_RASTERIZER_DISCARD, self.rasterState.rasterizerDiscard); + c.glPolygonMode(c.GL_FRONT_AND_BACK, switch(self.rasterState.polygonMode) { + .fill => c.GL_FILL, + .line => c.GL_LINE, + .point => c.GL_POINT, + }); + if(self.rasterState.cullMode != .none) { + c.glEnable(c.GL_CULL_FACE); + c.glCullFace(switch(self.rasterState.cullMode) { + .front => c.GL_FRONT, + .back => c.GL_BACK, + .frontAndBack => c.GL_FRONT_AND_BACK, + else => unreachable, + }); + } else { + c.glDisable(c.GL_CULL_FACE); + } + c.glFrontFace(switch(self.rasterState.frontFace) { + .counterClockwise => c.GL_CCW, + .clockwise => c.GL_CW, + }); + if(self.rasterState.depthBias) |depthBias| { + c.glEnable(c.GL_POLYGON_OFFSET_FILL); + c.glEnable(c.GL_POLYGON_OFFSET_LINE); + c.glEnable(c.GL_POLYGON_OFFSET_POINT); + c.glPolygonOffset(depthBias.slopeFactor, depthBias.constantFactor); + } else { + c.glDisable(c.GL_POLYGON_OFFSET_FILL); + c.glDisable(c.GL_POLYGON_OFFSET_LINE); + c.glDisable(c.GL_POLYGON_OFFSET_POINT); + } + c.glLineWidth(self.rasterState.lineWidth); + + // TODO: Multisampling + + conditionalEnable(c.GL_DEPTH_TEST, self.depthStencilState.depthTest); + c.glDepthMask(@intFromBool(self.depthStencilState.depthWrite)); + c.glDepthFunc(switch(self.depthStencilState.depthCompare) { + .never => c.GL_NEVER, + .less => c.GL_LESS, + .equal => c.GL_EQUAL, + .lessOrEqual => c.GL_LEQUAL, + .greater => c.GL_GREATER, + .notEqual => c.GL_NOTEQUAL, + .greateOrEqual => c.GL_GEQUAL, + .always => c.GL_ALWAYS, + }); + // TODO: stencilTest + + // TODO: logicOp + for(self.blendState.attachments, 0..) |attachment, i| { + c.glColorMask(@intFromBool(attachment.colorWriteMask.r), @intFromBool(attachment.colorWriteMask.g), @intFromBool(attachment.colorWriteMask.b), @intFromBool(attachment.colorWriteMask.a)); + if(!attachment.enabled) { + c.glDisable(c.GL_BLEND); + continue; + } + c.glEnable(c.GL_BLEND); + c.glBlendEquationSeparatei(@intCast(i), attachment.colorBlendOp.toGl(), attachment.alphaBlendOp.toGl()); + c.glBlendFuncSeparatei(@intCast(i), attachment.srcColorBlendFactor.toGl(), attachment.dstColorBlendFactor.toGl(), attachment.srcAlphaBlendFactor.toGl(), attachment.dstAlphaBlendFactor.toGl()); + } + c.glBlendColor(self.blendState.blendConstants[0], self.blendState.blendConstants[1], self.blendState.blendConstants[2], self.blendState.blendConstants[3]); + } +}; + +pub const ComputePipeline = struct { // MARK: ComputePipeline + shader: Shader, + + pub fn init(computePath: []const u8, defines: []const u8, uniformStruct: anytype) ComputePipeline { + return .{ + .shader = .initCompute(computePath, defines, uniformStruct), + }; + } + + pub fn deinit(self: ComputePipeline) void { + self.shader.deinit(); + } + + pub fn bind(self: ComputePipeline) void { + self.shader.bind(); + } +}; + +pub const SSBO = struct { // MARK: SSBO + bufferID: c_uint, + pub fn init() SSBO { + var self = SSBO{.bufferID = undefined}; + c.glGenBuffers(1, &self.bufferID); + return self; + } + + pub fn initStatic(comptime T: type, data: []const T) SSBO { + var self = SSBO{.bufferID = undefined}; + c.glGenBuffers(1, &self.bufferID); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); + c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, @intCast(data.len*@sizeOf(T)), data.ptr, 0); + return self; + } + + pub fn initStaticSize(comptime T: type, len: usize) SSBO { + var self = SSBO{.bufferID = undefined}; + c.glGenBuffers(1, &self.bufferID); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); + c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, @intCast(len*@sizeOf(T)), null, 0); + return self; + } + + pub fn deinit(self: SSBO) void { + c.glDeleteBuffers(1, &self.bufferID); + } + + pub fn bind(self: SSBO, binding: c_uint) void { + c.glBindBufferBase(c.GL_SHADER_STORAGE_BUFFER, binding, self.bufferID); + } + + pub fn bufferData(self: SSBO, comptime T: type, data: []const T) void { + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); + c.glBufferData(c.GL_SHADER_STORAGE_BUFFER, @intCast(data.len*@sizeOf(T)), data.ptr, c.GL_STATIC_DRAW); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); + } + + pub fn bufferSubData(self: SSBO, comptime T: type, data: []const T, length: usize) void { + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); + c.glBufferSubData(c.GL_SHADER_STORAGE_BUFFER, 0, @intCast(length*@sizeOf(T)), data.ptr); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); + } + + pub fn createDynamicBuffer(self: SSBO, comptime T: type, size: usize) void { + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); + c.glBufferData(c.GL_SHADER_STORAGE_BUFFER, @intCast(size*@sizeOf(T)), null, c.GL_DYNAMIC_DRAW); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); + } +}; + +pub const SubAllocation = struct { + start: u31, + len: u31, +}; + +/// A big SSBO that is able to allocate/free smaller regions. +pub fn LargeBuffer(comptime Entry: type) type { // MARK: LargerBuffer + return struct { + ssbo: SSBO, + freeBlocks: main.List(SubAllocation), + fences: [3]c.GLsync, + fencedFreeLists: [3]main.List(SubAllocation), + activeFence: u8, + capacity: u31, + used: u31, + binding: c_uint, + + const Self = @This(); + + fn createBuffer(self: *Self, size: u31) void { + self.ssbo = .init(); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); + const flags = c.GL_MAP_WRITE_BIT | c.GL_DYNAMIC_STORAGE_BIT; + const bytes = @as(c.GLsizeiptr, size)*@sizeOf(Entry); + c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, bytes, null, flags); + self.ssbo.bind(self.binding); + self.capacity = size; + } + + pub fn init(self: *Self, allocator: NeverFailingAllocator, size: u31, binding: c_uint) void { + self.used = 0; + self.binding = binding; + self.createBuffer(size); + self.activeFence = 0; + for(&self.fences) |*fence| { + fence.* = c.glFenceSync(c.GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } + for(&self.fencedFreeLists) |*list| { + list.* = .init(allocator); + } + + self.freeBlocks = .init(allocator); + self.freeBlocks.append(.{.start = 0, .len = size}); + } + + pub fn deinit(self: *Self) void { + for(self.fences) |fence| { + c.glDeleteSync(fence); + } + for(self.fencedFreeLists) |list| { + list.deinit(); + } + self.ssbo.deinit(); + self.freeBlocks.deinit(); + } + + pub fn beginRender(self: *Self) void { + self.activeFence += 1; + if(self.activeFence == self.fences.len) self.activeFence = 0; + const endTime = main.timestamp().addDuration(.fromMilliseconds(5)); + while(self.fencedFreeLists[self.activeFence].popOrNull()) |allocation| { + self.finalFree(allocation); + if(main.timestamp().durationTo(endTime).nanoseconds < 0) break; // TODO: Remove after #1434 + } + _ = c.glClientWaitSync(self.fences[self.activeFence], 0, c.GL_TIMEOUT_IGNORED); // Make sure the render calls that accessed these parts of the buffer have finished. + } + + pub fn endRender(self: *Self) void { + c.glDeleteSync(self.fences[self.activeFence]); + self.fences[self.activeFence] = c.glFenceSync(c.GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } + + pub fn rawAlloc(self: *Self, size: u31) SubAllocation { + var smallestBlock: ?*SubAllocation = null; + for(self.freeBlocks.items, 0..) |*block, i| { + if(size == block.len) { + self.used += size; + return self.freeBlocks.swapRemove(i); + } + if(size < block.len and if(smallestBlock) |_smallestBlock| block.len < _smallestBlock.len else true) { + smallestBlock = block; + } + } + if(smallestBlock) |block| { + const result = SubAllocation{.start = block.start, .len = size}; + block.start += size; + block.len -= size; + self.used += size; + return result; + } else { + std.log.info("Resizing internal mesh buffer from {} MiB to {} MiB", .{@as(usize, self.capacity)*@sizeOf(Entry) >> 20, (@as(usize, self.capacity)*@sizeOf(Entry) >> 20)*2}); + if(@as(usize, self.capacity)*@sizeOf(Entry)*2 > 1 << 31) @panic("OpenGL 2 GiB buffer size limit reached. Please lower your render distance."); + const oldBuffer = self.ssbo; + defer oldBuffer.deinit(); + const oldCapacity = self.capacity; + self.createBuffer(self.capacity*|2); // TODO: Is there a way to free the old buffer before creating the new one? + self.used += self.capacity - oldCapacity; + self.finalFree(.{.start = oldCapacity, .len = self.capacity - oldCapacity}); + + c.glBindBuffer(c.GL_COPY_READ_BUFFER, oldBuffer.bufferID); + c.glBindBuffer(c.GL_COPY_WRITE_BUFFER, self.ssbo.bufferID); + c.glCopyBufferSubData(c.GL_COPY_READ_BUFFER, c.GL_COPY_WRITE_BUFFER, 0, 0, @as(c.GLsizeiptr, oldCapacity)*@sizeOf(Entry)); + return rawAlloc(self, size); + } + } + + fn finalFree(self: *Self, _allocation: SubAllocation) void { + if(_allocation.len == 0) return; + self.used -= _allocation.len; + var allocation = _allocation; + for(self.freeBlocks.items, 0..) |*block, i| { + if(allocation.start + allocation.len == block.start) { + allocation.len += block.len; + _ = self.freeBlocks.swapRemove(i); + break; + } + } + for(self.freeBlocks.items) |*block| { + if(allocation.start == block.start + block.len) { + block.len += allocation.len; + return; + } + } + self.freeBlocks.append(allocation); + } + + pub fn free(self: *Self, allocation: SubAllocation) void { + if(allocation.len == 0) return; + self.fencedFreeLists[self.activeFence].append(allocation); + } + + /// Must unmap after use! + pub fn allocateAndMapRange(self: *Self, len: usize, allocation: *SubAllocation) []Entry { + self.free(allocation.*); + if(len == 0) { + allocation.len = 0; + return &.{}; + } + allocation.* = self.rawAlloc(@intCast(len)); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); + const ptr: [*]Entry = @ptrCast(@alignCast(c.glMapBufferRange( + c.GL_SHADER_STORAGE_BUFFER, + @as(c.GLintptr, allocation.start)*@sizeOf(Entry), + @as(c.GLsizeiptr, allocation.len)*@sizeOf(Entry), + c.GL_MAP_WRITE_BIT | c.GL_MAP_INVALIDATE_RANGE_BIT, + ))); + return ptr[0..len]; + } + + pub fn unmapRange(self: *Self, range: []Entry) void { + if(range.len == 0) return; + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); + std.debug.assert(c.glUnmapBuffer(c.GL_SHADER_STORAGE_BUFFER) == c.GL_TRUE); + } + + pub fn uploadData(self: *Self, data: []const Entry, allocation: *SubAllocation) void { + self.free(allocation.*); + if(data.len == 0) { + allocation.len = 0; + return; + } + allocation.* = self.rawAlloc(@intCast(data.len)); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); + const ptr: [*]Entry = @ptrCast(@alignCast(c.glMapBufferRange( + c.GL_SHADER_STORAGE_BUFFER, + @as(c.GLintptr, allocation.start)*@sizeOf(Entry), + @as(c.GLsizeiptr, allocation.len)*@sizeOf(Entry), + c.GL_MAP_WRITE_BIT | c.GL_MAP_INVALIDATE_RANGE_BIT, + ))); + @memcpy(ptr, data); + std.debug.assert(c.glUnmapBuffer(c.GL_SHADER_STORAGE_BUFFER) == c.GL_TRUE); + } + }; +} + +pub const FrameBuffer = struct { // MARK: FrameBuffer + frameBuffer: c_uint, + texture: c_uint, + hasDepthTexture: bool, + depthTexture: c_uint, + + pub fn init(self: *FrameBuffer, hasDepthTexture: bool, textureFilter: c_int, textureWrap: c_int) void { + self.* = FrameBuffer{ + .frameBuffer = undefined, + .texture = undefined, + .depthTexture = undefined, + .hasDepthTexture = hasDepthTexture, + }; + c.glGenFramebuffers(1, &self.frameBuffer); + c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); + if(hasDepthTexture) { + c.glGenTextures(1, &self.depthTexture); + c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, textureFilter); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, textureFilter); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, textureWrap); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, textureWrap); + c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_DEPTH_ATTACHMENT, c.GL_TEXTURE_2D, self.depthTexture, 0); + } + c.glGenTextures(1, &self.texture); + c.glBindTexture(c.GL_TEXTURE_2D, self.texture); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, textureFilter); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, textureFilter); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, textureWrap); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, textureWrap); + c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_COLOR_ATTACHMENT0, c.GL_TEXTURE_2D, self.texture, 0); + + c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + } + + pub fn deinit(self: *FrameBuffer) void { + c.glDeleteFramebuffers(1, &self.frameBuffer); + if(self.hasDepthTexture) { + c.glDeleteRenderbuffers(1, &self.depthTexture); + } + c.glDeleteTextures(1, &self.texture); + } + + pub fn updateSize(self: *FrameBuffer, _width: u31, _height: u31, internalFormat: c_int) void { + const width = @max(_width, 1); + const height = @max(_height, 1); + c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); + if(self.hasDepthTexture) { + c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture); + c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_DEPTH_COMPONENT32F, width, height, 0, c.GL_DEPTH_COMPONENT, c.GL_FLOAT, null); + } + + c.glBindTexture(c.GL_TEXTURE_2D, self.texture); + c.glTexImage2D(c.GL_TEXTURE_2D, 0, internalFormat, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + } + + pub fn clear(_: FrameBuffer, clearColor: Vec4f) void { + c.glDepthFunc(c.GL_LESS); + c.glDepthMask(c.GL_TRUE); + c.glDisable(c.GL_SCISSOR_TEST); + c.glClearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); + c.glClear(c.GL_COLOR_BUFFER_BIT | c.GL_DEPTH_BUFFER_BIT); + } + + pub fn validate(self: *const FrameBuffer) bool { + c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); + defer c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + if(c.glCheckFramebufferStatus(c.GL_FRAMEBUFFER) != c.GL_FRAMEBUFFER_COMPLETE) { + std.log.err("Frame Buffer Object error: {}", .{c.glCheckFramebufferStatus(c.GL_FRAMEBUFFER)}); + return false; + } + return true; + } + + pub fn bindTexture(self: *const FrameBuffer, target: c_uint) void { + c.glActiveTexture(target); + c.glBindTexture(c.GL_TEXTURE_2D, self.texture); + } + + pub fn bindDepthTexture(self: *const FrameBuffer, target: c_uint) void { + std.debug.assert(self.hasDepthTexture); + c.glActiveTexture(target); + c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture); + } + + pub fn bind(self: *const FrameBuffer) void { + c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); + } + + pub fn unbind(_: *const FrameBuffer) void { + c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + } +}; + +pub const TextureArray = struct { // MARK: TextureArray + textureID: c_uint, + + pub fn init() TextureArray { + var self: TextureArray = undefined; + c.glGenTextures(1, &self.textureID); + return self; + } + + pub fn deinit(self: TextureArray) void { + c.glDeleteTextures(1, &self.textureID); + } + + pub fn bind(self: TextureArray) void { + c.glBindTexture(c.GL_TEXTURE_2D_ARRAY, self.textureID); + } + + fn lodColorInterpolation(colors: [4]Color, alphaCorrection: bool) Color { + var r: [4]f32 = undefined; + var g: [4]f32 = undefined; + var b: [4]f32 = undefined; + var a: [4]f32 = undefined; + for(0..4) |i| { + r[i] = @floatFromInt(colors[i].r); + g[i] = @floatFromInt(colors[i].g); + b[i] = @floatFromInt(colors[i].b); + a[i] = @floatFromInt(colors[i].a); + } + // Use gamma corrected average(https://stackoverflow.com/a/832314/13082649): + var aSum: f32 = 0; + var rSum: f32 = 0; + var gSum: f32 = 0; + var bSum: f32 = 0; + for(0..4) |i| { + const w = if(alphaCorrection) a[i]*a[i] else 1; + aSum += a[i]*a[i]; + rSum += w*r[i]*r[i]; + gSum += w*g[i]*g[i]; + bSum += w*b[i]*b[i]; + } + aSum = @sqrt(aSum)/2; + rSum = @sqrt(rSum)/2; + gSum = @sqrt(gSum)/2; + bSum = @sqrt(bSum)/2; + if(alphaCorrection and aSum != 0) { + rSum /= aSum; + gSum /= aSum; + bSum /= aSum; + } + return Color{.r = @intFromFloat(rSum), .g = @intFromFloat(gSum), .b = @intFromFloat(bSum), .a = @intFromFloat(aSum)}; + } + + /// (Re-)Generates the GPU buffer. + pub fn generate(self: TextureArray, images: []Image, mipmapping: bool, alphaCorrectMipmapping: bool) void { + var maxWidth: u31 = 1; + var maxHeight: u31 = 1; + for(images) |image| { + maxWidth = @max(maxWidth, image.width); + maxHeight = @max(maxHeight, image.height); + } + // Make sure the width and height use a power of 2: + if(maxWidth - 1 & maxWidth != 0) { + maxWidth = @as(u31, 2) << std.math.log2_int(u31, maxWidth); + } + if(maxHeight - 1 & maxHeight != 0) { + maxHeight = @as(u31, 2) << std.math.log2_int(u31, maxHeight); + } + + std.log.debug("Creating Texture Array of size {}×{} with {} layers.", .{maxWidth, maxHeight, images.len}); + + self.bind(); + + const maxLOD = if(mipmapping) 1 + std.math.log2_int(u31, @min(maxWidth, maxHeight)) else 1; + for(0..maxLOD) |i| { + c.glTexImage3D(c.GL_TEXTURE_2D_ARRAY, @intCast(i), c.GL_RGBA8, @max(0, maxWidth >> @intCast(i)), @max(0, maxHeight >> @intCast(i)), @intCast(images.len), 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + } + const arena = main.stackAllocator.createArena(); + defer main.stackAllocator.destroyArena(arena); + const lodBuffer: [][]Color = arena.alloc([]Color, maxLOD); + for(lodBuffer, 0..) |*buffer, i| { + buffer.* = arena.alloc(Color, (maxWidth >> @intCast(i))*(maxHeight >> @intCast(i))); + } + + for(images, 0..) |image, i| { + // Fill the buffer using nearest sampling. Probably not the best solutions for all textures, but that's what happens when someone doesn't use power of 2 textures... + for(0..maxWidth) |x| { + for(0..maxHeight) |y| { + const index = x + y*maxWidth; + const imageIndex = (x*image.width)/maxWidth + image.width*((y*image.height)/maxHeight); + lodBuffer[0][index] = image.imageData[imageIndex]; + } + } + + // Calculate the mipmap levels: + for(0..lodBuffer.len) |_lod| { + const lod: u5 = @intCast(_lod); + const curWidth = maxWidth >> lod; + const curHeight = maxHeight >> lod; + if(lod != 0) { + for(0..curWidth) |x| { + for(0..curHeight) |y| { + const index = x + y*curWidth; + const index2 = 2*x + 2*y*2*curWidth; + const colors = [4]Color{ + lodBuffer[lod - 1][index2], + lodBuffer[lod - 1][index2 + 1], + lodBuffer[lod - 1][index2 + curWidth*2], + lodBuffer[lod - 1][index2 + curWidth*2 + 1], + }; + lodBuffer[lod][index] = lodColorInterpolation(colors, alphaCorrectMipmapping); + } + } + } + } + // Give the correct color to alpha 0 pixels, to avoid dark pixels: + for(1..lodBuffer.len) |_lod| { + const lod: u5 = @intCast(lodBuffer.len - 1 - _lod); + const curWidth = maxWidth >> lod; + const curHeight = maxHeight >> lod; + for(0..curWidth) |x| { + for(0..curHeight) |y| { + const index = x + y*curWidth; + const index2 = x/2 + y/2*curWidth/2; + if(lodBuffer[lod][index].a == 0) { + lodBuffer[lod][index].r = lodBuffer[lod + 1][index2].r; + lodBuffer[lod][index].g = lodBuffer[lod + 1][index2].g; + lodBuffer[lod][index].b = lodBuffer[lod + 1][index2].b; + } + } + } + } + // Upload: + for(0..lodBuffer.len) |_lod| { + const lod: u5 = @intCast(lodBuffer.len - 1 - _lod); + const curWidth = maxWidth >> lod; + const curHeight = maxHeight >> lod; + c.glTexSubImage3D(c.GL_TEXTURE_2D_ARRAY, lod, 0, 0, @intCast(i), curWidth, curHeight, 1, c.GL_RGBA, c.GL_UNSIGNED_BYTE, lodBuffer[lod].ptr); + } + } + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_LOD, maxLOD); + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST_MIPMAP_LINEAR); + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST); + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + } +}; + +pub const Texture = struct { // MARK: Texture + textureID: c_uint, + + pub fn init() Texture { + var self: Texture = undefined; + c.glGenTextures(1, &self.textureID); + return self; + } + + pub fn initFromFile(path: []const u8) Texture { + const self = Texture.init(); + const image = Image.readFromFile(main.stackAllocator, path) catch |err| blk: { + std.log.err("Couldn't read image from {s}: {s}", .{path, @errorName(err)}); + break :blk Image.defaultImage; + }; + defer image.deinit(main.stackAllocator); + self.generate(image); + return self; + } + + pub fn initFromMipmapFiles(pathPrefix: []const u8, largestSize: u31, lodBias: f32) Texture { + const self = Texture.init(); + self.bind(); + + const maxLod = std.math.log2_int(u31, largestSize); + + var curSize: u31 = largestSize; + while(curSize != 0) : (curSize /= 2) { + c.glTexImage2D(c.GL_TEXTURE_2D, maxLod - std.math.log2_int(u31, curSize), c.GL_RGBA8, curSize, curSize, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + } + + curSize = largestSize; + while(curSize != 0) : (curSize /= 2) { + const path = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}{}.png", .{pathPrefix, curSize}) catch unreachable; + defer main.stackAllocator.free(path); + const image = Image.readFromFile(main.stackAllocator, path) catch |err| blk: { + std.log.err("Couldn't read image from {s}: {s}", .{path, @errorName(err)}); + break :blk Image.defaultImage; + }; + defer image.deinit(main.stackAllocator); + c.glTexSubImage2D(c.GL_TEXTURE_2D, maxLod - std.math.log2_int(u31, curSize), 0, 0, curSize, curSize, c.GL_RGBA, c.GL_UNSIGNED_BYTE, image.imageData.ptr); + } + + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_LOD, maxLod); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST_MIPMAP_LINEAR); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + c.glTexParameterf(c.GL_TEXTURE_2D, c.GL_TEXTURE_LOD_BIAS, lodBias); + return self; + } + + pub fn deinit(self: Texture) void { + c.glDeleteTextures(1, &self.textureID); + } + + pub fn bindTo(self: Texture, binding: u5) void { + c.glActiveTexture(@intCast(c.GL_TEXTURE0 + binding)); + c.glBindTexture(c.GL_TEXTURE_2D, self.textureID); + } + + pub fn bind(self: Texture) void { + c.glBindTexture(c.GL_TEXTURE_2D, self.textureID); + } + + /// (Re-)Generates the GPU buffer. + pub fn generate(self: Texture, image: Image) void { + self.bind(); + + c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_RGBA8, image.width, image.height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, image.imageData.ptr); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + } + + pub fn render(self: Texture, pos: Vec2f, dim: Vec2f) void { + self.bindTo(0); + draw.boundImage(pos, dim); + } + + pub fn size(self: Texture) Vec2i { + self.bind(); + var result: Vec2i = undefined; + c.glGetTexLevelParameteriv(c.GL_TEXTURE_2D, 0, c.GL_TEXTURE_WIDTH, &result[0]); + c.glGetTexLevelParameteriv(c.GL_TEXTURE_2D, 0, c.GL_TEXTURE_HEIGHT, &result[1]); + return result; + } +}; + +pub const CubeMapTexture = struct { // MARK: CubeMapTexture + textureID: c_uint, + + pub fn init() CubeMapTexture { + var self: CubeMapTexture = undefined; + c.glGenTextures(1, &self.textureID); + return self; + } + + pub fn deinit(self: CubeMapTexture) void { + c.glDeleteTextures(1, &self.textureID); + } + + pub fn bindTo(self: CubeMapTexture, binding: u5) void { + c.glActiveTexture(@intCast(c.GL_TEXTURE0 + binding)); + c.glBindTexture(c.GL_TEXTURE_CUBE_MAP, self.textureID); + } + + pub fn bind(self: CubeMapTexture) void { + c.glBindTexture(c.GL_TEXTURE_CUBE_MAP, self.textureID); + } + + /// (Re-)Generates the GPU buffer. + pub fn generate(self: CubeMapTexture, width: u31, height: u31) void { + self.bind(); + + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MIN_FILTER, c.GL_LINEAR); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MAG_FILTER, c.GL_LINEAR); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_S, c.GL_CLAMP_TO_EDGE); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_T, c.GL_CLAMP_TO_EDGE); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_R, c.GL_CLAMP_TO_EDGE); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_BASE_LEVEL, 0); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MAX_LEVEL, 0); + } + + pub fn faceNormal(face: usize) Vec3f { + const normals = [_]Vec3f{ + .{1, 0, 0}, // +x + .{-1, 0, 0}, // -x + .{0, 1, 0}, // +y + .{0, -1, 0}, // -y + .{0, 0, 1}, // +z + .{0, 0, -1}, // -z + }; + return normals[face]; + } + + pub fn faceUp(face: usize) Vec3f { + const ups = [_]Vec3f{ + .{0, -1, 0}, // +x + .{0, -1, 0}, // -x + .{0, 0, 1}, // +y + .{0, 0, -1}, // -y + .{0, -1, 0}, // +z + .{0, -1, 0}, // -z + }; + return ups[face]; + } + + pub fn faceRight(face: usize) Vec3f { + comptime var rights: [6]Vec3f = undefined; + inline for(0..6) |i| { + rights[i] = comptime vec.cross(faceNormal(i), faceUp(i)); + } + return rights[face]; + } + + pub fn bindToFramebuffer(self: CubeMapTexture, fb: FrameBuffer, face: c_uint) void { + fb.bind(); + c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_COLOR_ATTACHMENT0, @as(c_uint, c.GL_TEXTURE_CUBE_MAP_POSITIVE_X) + face, self.textureID, 0); + } +}; + +pub const Color = extern struct { // MARK: Color + r: u8, + g: u8, + b: u8, + a: u8, + + pub fn toARBG(self: Color) u32 { + return @as(u32, self.a) << 24 | @as(u32, self.r) << 16 | @as(u32, self.g) << 8 | @as(u32, self.b); + } +}; + +pub const Image = struct { // MARK: Image + var defaultImageData = [4]Color{ + Color{.r = 0, .g = 0, .b = 0, .a = 255}, + Color{.r = 255, .g = 0, .b = 255, .a = 255}, + Color{.r = 255, .g = 0, .b = 255, .a = 255}, + Color{.r = 0, .g = 0, .b = 0, .a = 255}, + }; + pub const defaultImage = Image{ + .width = 2, + .height = 2, + .imageData = &defaultImageData, + }; + var emptyImageData = [1]Color{ + Color{.r = 0, .g = 0, .b = 0, .a = 0}, + }; + pub const emptyImage = Image{ + .width = 1, + .height = 1, + .imageData = &emptyImageData, + }; + var whiteImageData = [1]Color{ + Color{.r = 255, .g = 255, .b = 255, .a = 255}, + }; + pub const whiteEmptyImage = Image{ + .width = 1, + .height = 1, + .imageData = &whiteImageData, + }; + width: u31, + height: u31, + imageData: []Color, + pub fn init(allocator: NeverFailingAllocator, width: u31, height: u31) Image { + return Image{ + .width = width, + .height = height, + .imageData = allocator.alloc(Color, width*height), + }; + } + pub fn deinit(self: Image, allocator: NeverFailingAllocator) void { + if(self.imageData.ptr == &defaultImageData or self.imageData.ptr == &emptyImageData or self.imageData.ptr == &whiteImageData) return; + allocator.free(self.imageData); + } + pub fn readFromFile(allocator: NeverFailingAllocator, path: []const u8) !Image { + var result: Image = undefined; + var channel: c_int = undefined; + const nullTerminatedPath = main.stackAllocator.dupeZ(u8, path); // TODO: Find a more zig-friendly image loading library. + errdefer main.stackAllocator.free(nullTerminatedPath); + stb_image.stbi_set_flip_vertically_on_load(1); + const data = stb_image.stbi_load(nullTerminatedPath.ptr, @ptrCast(&result.width), @ptrCast(&result.height), &channel, 4) orelse { + return error.FileNotFound; + }; + main.stackAllocator.free(nullTerminatedPath); + result.imageData = allocator.dupe(Color, @as([*]Color, @ptrCast(data))[0 .. result.width*result.height]); + stb_image.stbi_image_free(data); + return result; + } + pub fn readUnflippedFromFile(allocator: NeverFailingAllocator, path: []const u8) !Image { + var result: Image = undefined; + var channel: c_int = undefined; + const nullTerminatedPath = main.stackAllocator.dupeZ(u8, path); // TODO: Find a more zig-friendly image loading library. + errdefer main.stackAllocator.free(nullTerminatedPath); + const data = stb_image.stbi_load(nullTerminatedPath.ptr, @ptrCast(&result.width), @ptrCast(&result.height), &channel, 4) orelse { + return error.FileNotFound; + }; + main.stackAllocator.free(nullTerminatedPath); + result.imageData = allocator.dupe(Color, @as([*]Color, @ptrCast(data))[0 .. result.width*result.height]); + stb_image.stbi_image_free(data); + return result; + } + pub fn exportToFile(self: Image, path: []const u8) !void { + const nullTerminated = main.stackAllocator.dupeZ(u8, path); + defer main.stackAllocator.free(nullTerminated); + _ = stb_image.stbi_write_png(nullTerminated.ptr, self.width, self.height, 4, self.imageData.ptr, self.width*4); // TODO: Handle the return type. + } + pub fn getRGB(self: Image, x: usize, y: usize) Color { + std.debug.assert(x < self.width); + std.debug.assert(y < self.height); + const index = x + y*self.width; + return self.imageData[index]; + } + pub fn setRGB(self: Image, x: usize, y: usize, rgb: Color) void { + std.debug.assert(x < self.width); + std.debug.assert(y < self.height); + const index = x + y*self.width; + self.imageData[index] = rgb; + } +}; + +pub const Fog = struct { // MARK: Fog + fogColor: Vec3f, + skyColor: Vec3f, + density: f32, + fogLower: f32, + fogHigher: f32, +}; + +const block_texture = struct { // MARK: block_texture + var uniforms: struct { + transparent: c_int, + } = undefined; + var pipeline: Pipeline = undefined; + var depthTexture: Texture = undefined; + const textureSize = 128; + + fn init() void { + pipeline = Pipeline.init( + "assets/cubyz/shaders/item_texture_post.vert", + "assets/cubyz/shaders/item_texture_post.frag", + "", + &uniforms, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.noBlending}}, + ); + depthTexture = .init(); + depthTexture.bind(); + var data: [128*128]f32 = undefined; + + const z: f32 = 134; + const near = main.renderer.zNear; + const far = main.renderer.zFar; + const depth = ((far + near)/(near - far)*-z + 2*near*far/(near - far))/z*0.5 + 0.5; + + @memset(&data, depth); + c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R32F, textureSize, textureSize, 0, c.GL_RED, c.GL_FLOAT, &data); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + } + fn deinit() void { + pipeline.deinit(); + depthTexture.deinit(); + } +}; + +pub fn generateBlockTexture(blockType: u16) Texture { + const block = main.blocks.Block{.typ = blockType, .data = 0}; // TODO: Use natural standard data. + const textureSize = block_texture.textureSize; + c.glViewport(0, 0, textureSize, textureSize); + + var frameBuffer: FrameBuffer = undefined; + + frameBuffer.init(false, c.GL_NEAREST, c.GL_REPEAT); + defer frameBuffer.deinit(); + frameBuffer.updateSize(textureSize, textureSize, c.GL_RGBA16F); + frameBuffer.bind(); + if(block.transparent()) { + frameBuffer.clear(.{0.683421, 0.6854237, 0.685426, 1}); + } else { + frameBuffer.clear(.{0, 0, 0, 0}); + } + + const projMatrix = Mat4f.perspective(0.013, 1, 64, 256); + const oldViewMatrix = main.game.camera.viewMatrix; + main.game.camera.viewMatrix = Mat4f.identity().mul(Mat4f.rotationX(std.math.pi/4.0)).mul(Mat4f.rotationZ(1.0*std.math.pi/4.0)); + defer main.game.camera.viewMatrix = oldViewMatrix; + const uniforms = if(block.transparent()) &main.renderer.chunk_meshing.transparentUniforms else &main.renderer.chunk_meshing.uniforms; + + var faceData: main.ListUnmanaged(main.renderer.chunk_meshing.FaceData) = .{}; + defer faceData.deinit(main.stackAllocator); + const model = main.blocks.meshes.model(block).model(); + const pos: main.chunk.BlockPos = .fromCoords(1, 1, 1); + if(block.hasBackFace()) { + model.appendInternalQuadsToList(&faceData, main.stackAllocator, block, pos, true); + for(main.chunk.Neighbor.iterable) |neighbor| { + model.appendNeighborFacingQuadsToList(&faceData, main.stackAllocator, block, neighbor, pos, true); + } + } + model.appendInternalQuadsToList(&faceData, main.stackAllocator, block, pos, false); + for(main.chunk.Neighbor.iterable) |neighbor| { + model.appendNeighborFacingQuadsToList(&faceData, main.stackAllocator, block, neighbor, pos.neighbor(neighbor)[0], false); + } + + for(faceData.items) |*face| { + face.position.lightIndex = 0; + } + var allocation: SubAllocation = .{.start = 0, .len = 0}; + main.renderer.chunk_meshing.faceBuffers[0].uploadData(faceData.items, &allocation); + defer main.renderer.chunk_meshing.faceBuffers[0].free(allocation); + var lightAllocation: SubAllocation = .{.start = 0, .len = 0}; + main.renderer.chunk_meshing.lightBuffers[0].uploadData(&.{0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, &lightAllocation); + defer main.renderer.chunk_meshing.lightBuffers[0].free(lightAllocation); + + { + const i = 4; // Easily switch between the 8 diagonal coordinates. + var x: f64 = -65.5 + 1.5; + var y: f64 = -65.5 + 1.5; + var z: f64 = -92.631 + 1.5; + if(i & 1 != 0) x = -x + 3; + if(i & 2 != 0) y = -y + 3; + if(i & 4 != 0) z = -z + 3; + var chunkAllocation: SubAllocation = .{.start = 0, .len = 0}; + main.renderer.chunk_meshing.chunkBuffer.uploadData(&.{.{ + .position = .{0, 0, 0}, + .min = undefined, + .max = undefined, + .voxelSize = 1, + .lightStart = lightAllocation.start, + .vertexStartOpaque = undefined, + .faceCountsByNormalOpaque = undefined, + .vertexStartTransparent = undefined, + .vertexCountTransparent = undefined, + .visibilityState = 0, + .oldVisibilityState = 0, + }}, &chunkAllocation); + defer main.renderer.chunk_meshing.chunkBuffer.free(chunkAllocation); + if(block.transparent()) { + c.glBlendEquation(c.GL_FUNC_ADD); + c.glBlendFunc(c.GL_ONE, c.GL_SRC1_COLOR); + main.renderer.chunk_meshing.bindTransparentShaderAndUniforms(projMatrix, .{1, 1, 1}, .{x, y, z}); + } else { + main.renderer.chunk_meshing.bindShaderAndUniforms(projMatrix, .{1, 1, 1}, .{x, y, z}); + } + c.glUniform1f(uniforms.contrast, 0.25); + c.glActiveTexture(c.GL_TEXTURE0); + main.blocks.meshes.blockTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE1); + main.blocks.meshes.emissionTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE2); + main.blocks.meshes.reflectivityAndAbsorptionTextureArray.bind(); + block_texture.depthTexture.bindTo(5); + c.glDrawElementsInstancedBaseVertexBaseInstance(c.GL_TRIANGLES, @intCast(6*faceData.items.len), c.GL_UNSIGNED_INT, null, 1, allocation.start*4, chunkAllocation.start); + } + + c.glDisable(c.GL_CULL_FACE); + var finalFrameBuffer: FrameBuffer = undefined; + finalFrameBuffer.init(false, c.GL_NEAREST, c.GL_REPEAT); + finalFrameBuffer.updateSize(textureSize, textureSize, c.GL_RGBA8); + finalFrameBuffer.bind(); + const texture = Texture{.textureID = finalFrameBuffer.texture}; + defer c.glDeleteFramebuffers(1, &finalFrameBuffer.frameBuffer); + block_texture.pipeline.bind(null); + c.glUniform1i(block_texture.uniforms.transparent, if(block.transparent()) c.GL_TRUE else c.GL_FALSE); + frameBuffer.bindTexture(c.GL_TEXTURE3); + + c.glBindVertexArray(draw.rectVAO); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + + c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + + c.glViewport(0, 0, main.Window.width, main.Window.height); + c.glBlendFunc(c.GL_SRC_ALPHA, c.GL_ONE_MINUS_SRC_ALPHA); + return texture; +} diff --git a/itemdrop.zig b/itemdrop.zig new file mode 100644 index 0000000000..676c2ce4f4 --- /dev/null +++ b/itemdrop.zig @@ -0,0 +1,879 @@ +const std = @import("std"); + +const blocks = @import("blocks.zig"); +const chunk_zig = @import("chunk.zig"); +const ServerChunk = chunk_zig.ServerChunk; +const game = @import("game.zig"); +const World = game.World; +const ServerWorld = main.server.ServerWorld; +const graphics = @import("graphics.zig"); +const c = graphics.c; +const items = @import("items.zig"); +const ItemStack = items.ItemStack; +const ZonElement = @import("zon.zig").ZonElement; +const main = @import("main"); +const random = @import("random.zig"); +const settings = @import("settings.zig"); +const utils = @import("utils.zig"); +const vec = @import("vec.zig"); +const Mat4f = vec.Mat4f; +const Vec3d = vec.Vec3d; +const Vec3f = vec.Vec3f; +const Vec3i = vec.Vec3i; +const BinaryReader = main.utils.BinaryReader; +const BinaryWriter = main.utils.BinaryWriter; +const NeverFailingAllocator = main.heap.NeverFailingAllocator; + +const ItemDrop = struct { // MARK: ItemDrop + pos: Vec3d, + vel: Vec3d, + rot: Vec3f, + itemStack: ItemStack, + despawnTime: i32, + pickupCooldown: i32, + + reverseIndex: u16, +}; + +pub const ItemDropNetworkData = struct { + index: u16, + pos: Vec3d, + vel: Vec3d, +}; + +pub const ItemDropManager = struct { // MARK: ItemDropManager + /// Half the side length of all item entities hitboxes as a cube. + pub const radius: f64 = 0.1; + /// Side length of all item entities hitboxes as a cube. + pub const diameter: f64 = 2*radius; + + pub const pickupRange: f64 = 1.0; + + const terminalVelocity = 40.0; + const gravity = 9.81; + + const maxCapacity = 65536; + + allocator: NeverFailingAllocator, + + list: std.MultiArrayList(ItemDrop), + + indices: [maxCapacity]u16 = undefined, + + emptyMutex: std.Thread.Mutex = .{}, + isEmpty: std.bit_set.ArrayBitSet(usize, maxCapacity), + + changeQueue: main.utils.ConcurrentQueue(union(enum) {add: struct {u16, ItemDrop}, remove: u16}), + + world: ?*ServerWorld, + airDragFactor: f64, + + size: u32 = 0, + + pub fn init(self: *ItemDropManager, allocator: NeverFailingAllocator, world: ?*ServerWorld) void { + self.* = ItemDropManager{ + .allocator = allocator, + .list = std.MultiArrayList(ItemDrop){}, + .isEmpty = .initFull(), + .changeQueue = .init(allocator, 16), + .world = world, + .airDragFactor = gravity/terminalVelocity, + }; + self.list.resize(self.allocator.allocator, maxCapacity) catch unreachable; + } + + pub fn deinit(self: *ItemDropManager) void { + self.processChanges(); + self.changeQueue.deinit(); + for(self.indices[0..self.size]) |i| { + self.list.items(.itemStack)[i].item.deinit(); + } + self.list.deinit(self.allocator.allocator); + } + + pub fn loadFrom(self: *ItemDropManager, zon: ZonElement) void { + const zonArray = zon.getChild("array"); + for(zonArray.toSlice()) |elem| { + self.addFromZon(elem); + } + } + + pub fn loadFromBytes(self: *ItemDropManager, reader: *main.utils.BinaryReader) !void { + const version = try reader.readInt(u8); + if(version != 0) return error.UnsupportedVersion; + var i: u16 = 0; + while(reader.remaining.len != 0) : (i += 1) { + try self.addFromBytes(reader, i); + } + } + + pub fn storeToBytes(self: *ItemDropManager, writer: *main.utils.BinaryWriter) void { + const version = 0; + writer.writeInt(u8, version); + for(self.indices[0..self.size]) |i| { + storeSingleToBytes(writer, self.list.get(i)); + } + } + + fn addFromBytes(self: *ItemDropManager, reader: *main.utils.BinaryReader, i: u16) !void { + const despawnTime = try reader.readInt(i32); + const pos = try reader.readVec(Vec3d); + const vel = try reader.readVec(Vec3d); + const itemStack = try items.ItemStack.fromBytes(reader); + self.addWithIndex(i, pos, vel, random.nextFloatVector(3, &main.seed)*@as(Vec3f, @splat(2*std.math.pi)), itemStack, despawnTime, 0); + } + + fn storeSingleToBytes(writer: *main.utils.BinaryWriter, itemdrop: ItemDrop) void { + writer.writeInt(i32, itemdrop.despawnTime); + writer.writeVec(Vec3d, itemdrop.pos); + writer.writeVec(Vec3d, itemdrop.vel); + itemdrop.itemStack.toBytes(writer); + } + + fn addFromZon(self: *ItemDropManager, zon: ZonElement) void { + const item = items.Item.init(zon) catch |err| { + const msg = zon.toStringEfficient(main.stackAllocator, ""); + defer main.stackAllocator.free(msg); + std.log.err("Ignoring invalid item drop {s} which caused {s}", .{msg, @errorName(err)}); + return; + }; + const properties = .{ + zon.get(Vec3d, "pos", .{0, 0, 0}), + zon.get(Vec3d, "vel", .{0, 0, 0}), + random.nextFloatVector(3, &main.seed)*@as(Vec3f, @splat(2*std.math.pi)), + items.ItemStack{.item = item, .amount = zon.get(u16, "amount", 1)}, + zon.get(i32, "despawnTime", 60), + 0, + }; + if(zon.get(?u16, "i", null)) |i| { + @call(.auto, addWithIndex, .{self, i} ++ properties); + } else { + @call(.auto, add, .{self} ++ properties); + } + } + + pub fn getPositionAndVelocityData(self: *ItemDropManager, allocator: NeverFailingAllocator) []ItemDropNetworkData { + const result = allocator.alloc(ItemDropNetworkData, self.size); + for(self.indices[0..self.size], result) |i, *res| { + res.* = .{ + .index = i, + .pos = self.list.items(.pos)[i], + .vel = self.list.items(.vel)[i], + }; + } + return result; + } + + pub fn getInitialList(self: *ItemDropManager, allocator: NeverFailingAllocator) ZonElement { + self.processChanges(); // Make sure all the items from the queue are included. + var list = ZonElement.initArray(allocator); + var ii: u32 = 0; + while(ii < self.size) : (ii += 1) { + const i = self.indices[ii]; + list.array.append(self.storeSingle(allocator, i)); + } + return list; + } + + fn storeDrop(allocator: NeverFailingAllocator, itemDrop: ItemDrop, i: u16) ZonElement { + const obj = ZonElement.initObject(allocator); + obj.put("i", i); + obj.put("pos", itemDrop.pos); + obj.put("vel", itemDrop.vel); + itemDrop.itemStack.storeToZon(allocator, obj); + obj.put("despawnTime", itemDrop.despawnTime); + return obj; + } + + fn storeSingle(self: *ItemDropManager, allocator: NeverFailingAllocator, i: u16) ZonElement { + return storeDrop(allocator, self.list.get(i), i); + } + + pub fn store(self: *ItemDropManager, allocator: NeverFailingAllocator) ZonElement { + const zonArray = ZonElement.initArray(allocator); + for(self.indices[0..self.size]) |i| { + const item = self.storeSingle(allocator, i); + zonArray.array.append(item); + } + const zon = ZonElement.initObject(allocator); + zon.put("array", zonArray); + return zon; + } + + pub fn update(self: *ItemDropManager, deltaTime: f32) void { + std.debug.assert(self.world != null); + self.processChanges(); + const pos = self.list.items(.pos); + const vel = self.list.items(.vel); + const pickupCooldown = self.list.items(.pickupCooldown); + const despawnTime = self.list.items(.despawnTime); + var ii: u32 = 0; + while(ii < self.size) { + const i = self.indices[ii]; + if(self.world.?.getSimulationChunkAndIncreaseRefCount(@intFromFloat(pos[i][0]), @intFromFloat(pos[i][1]), @intFromFloat(pos[i][2]))) |simChunk| { + defer simChunk.decreaseRefCount(); + if(simChunk.getChunk()) |chunk| { + // Check collision with blocks: + self.updateEnt(chunk, &pos[i], &vel[i], deltaTime); + } + } + pickupCooldown[i] -= 1; + despawnTime[i] -= 1; + if(despawnTime[i] < 0) { + self.directRemove(i); + } else { + ii += 1; + } + } + } + + pub fn add(self: *ItemDropManager, pos: Vec3d, vel: Vec3d, rot: Vec3f, itemStack: ItemStack, despawnTime: i32, pickupCooldown: i32) void { + self.emptyMutex.lock(); + const i: u16 = @intCast(self.isEmpty.findFirstSet() orelse { + self.emptyMutex.unlock(); + std.log.err("Item drop capacitiy limit reached. Failed to add itemStack: {}×{s}", .{itemStack.amount, itemStack.item.id() orelse return}); + itemStack.item.deinit(); + return; + }); + self.isEmpty.unset(i); + const drop = ItemDrop{ + .pos = pos, + .vel = vel, + .rot = rot, + .itemStack = itemStack, + .despawnTime = despawnTime, + .pickupCooldown = pickupCooldown, + .reverseIndex = undefined, + }; + if(self.world != null) { + const list = ZonElement.initArray(main.stackAllocator); + defer list.deinit(main.stackAllocator); + list.array.append(.null); + list.array.append(storeDrop(main.stackAllocator, drop, i)); + const updateData = list.toStringEfficient(main.stackAllocator, &.{}); + defer main.stackAllocator.free(updateData); + + const userList = main.server.getUserListAndIncreaseRefCount(main.stackAllocator); + defer main.server.freeUserListAndDecreaseRefCount(main.stackAllocator, userList); + for(userList) |user| { + main.network.protocols.entity.send(user.conn, updateData); + } + } + + self.emptyMutex.unlock(); + self.changeQueue.pushBack(.{.add = .{i, drop}}); + } + + fn addWithIndex(self: *ItemDropManager, i: u16, pos: Vec3d, vel: Vec3d, rot: Vec3f, itemStack: ItemStack, despawnTime: i32, pickupCooldown: i32) void { + self.emptyMutex.lock(); + std.debug.assert(self.isEmpty.isSet(i)); + self.isEmpty.unset(i); + const drop = ItemDrop{ + .pos = pos, + .vel = vel, + .rot = rot, + .itemStack = itemStack, + .despawnTime = despawnTime, + .pickupCooldown = pickupCooldown, + .reverseIndex = undefined, + }; + if(self.world != null) { + const list = ZonElement.initArray(main.stackAllocator); + defer list.deinit(main.stackAllocator); + list.array.append(.null); + list.array.append(storeDrop(main.stackAllocator, drop, i)); + const updateData = list.toStringEfficient(main.stackAllocator, &.{}); + defer main.stackAllocator.free(updateData); + + const userList = main.server.getUserListAndIncreaseRefCount(main.stackAllocator); + defer main.server.freeUserListAndDecreaseRefCount(main.stackAllocator, userList); + for(userList) |user| { + main.network.protocols.entity.send(user.conn, updateData); + } + } + + self.emptyMutex.unlock(); + self.changeQueue.pushBack(.{.add = .{i, drop}}); + } + + fn processChanges(self: *ItemDropManager) void { + while(self.changeQueue.popFront()) |data| { + switch(data) { + .add => |addData| { + self.internalAdd(addData[0], addData[1]); + }, + .remove => |index| { + self.internalRemove(index); + }, + } + } + } + + fn internalAdd(self: *ItemDropManager, i: u16, drop_: ItemDrop) void { + var drop = drop_; + if(self.world == null) { + ClientItemDropManager.clientSideInternalAdd(self, i, drop); + } + drop.reverseIndex = @intCast(self.size); + self.list.set(i, drop); + self.indices[self.size] = i; + self.size += 1; + } + + fn internalRemove(self: *ItemDropManager, i: u16) void { + self.size -= 1; + const ii = self.list.items(.reverseIndex)[i]; + self.list.items(.itemStack)[i].deinit(); + self.list.items(.itemStack)[i] = .{}; + self.indices[ii] = self.indices[self.size]; + self.list.items(.reverseIndex)[self.indices[self.size]] = ii; + } + + fn directRemove(self: *ItemDropManager, i: u16) void { + std.debug.assert(self.world != null); + self.emptyMutex.lock(); + self.isEmpty.set(i); + + const list = ZonElement.initArray(main.stackAllocator); + defer list.deinit(main.stackAllocator); + list.array.append(.null); + list.array.append(.{.int = i}); + const updateData = list.toStringEfficient(main.stackAllocator, &.{}); + defer main.stackAllocator.free(updateData); + + const userList = main.server.getUserListAndIncreaseRefCount(main.stackAllocator); + defer main.server.freeUserListAndDecreaseRefCount(main.stackAllocator, userList); + for(userList) |user| { + main.network.protocols.entity.send(user.conn, updateData); + } + + self.emptyMutex.unlock(); + self.internalRemove(i); + } + + fn updateEnt(self: *ItemDropManager, chunk: *ServerChunk, pos: *Vec3d, vel: *Vec3d, deltaTime: f64) void { + const hitBox = main.game.collision.Box{.min = @splat(-radius), .max = @splat(radius)}; + if(main.game.collision.collides(.server, .x, 0, pos.*, hitBox) != null) { + self.fixStuckInBlock(chunk, pos, vel, deltaTime); + return; + } + vel.* += Vec3d{0, 0, -gravity*deltaTime}; + inline for(0..3) |i| { + const move = vel.*[i]*deltaTime; // + acceleration[i]*deltaTime; + if(main.game.collision.collides(.server, @enumFromInt(i), move, pos.*, hitBox)) |box| { + if(move < 0) { + pos.*[i] = box.max[i] + radius; + } else { + pos.*[i] = box.max[i] - radius; + } + vel.*[i] = 0; + } else { + pos.*[i] += move; + } + } + // Apply drag: + vel.* *= @splat(@max(0, 1 - self.airDragFactor*deltaTime)); + } + + fn fixStuckInBlock(self: *ItemDropManager, chunk: *ServerChunk, pos: *Vec3d, vel: *Vec3d, deltaTime: f64) void { + const centeredPos = pos.* - @as(Vec3d, @splat(0.5)); + const pos0: Vec3i = @intFromFloat(@floor(centeredPos)); + + var closestEmptyBlock: Vec3i = @splat(-1); + var closestDist = std.math.floatMax(f64); + var delta = Vec3i{0, 0, 0}; + while(delta[0] <= 1) : (delta[0] += 1) { + delta[1] = 0; + while(delta[1] <= 1) : (delta[1] += 1) { + delta[2] = 0; + while(delta[2] <= 1) : (delta[2] += 1) { + const isSolid = self.checkBlock(chunk, pos, pos0 + delta); + if(!isSolid) { + const dist = vec.lengthSquare(@as(Vec3d, @floatFromInt(pos0 + delta)) - centeredPos); + if(dist < closestDist) { + closestDist = dist; + closestEmptyBlock = delta; + } + } + } + } + } + + vel.* = @splat(0); + const unstuckVelocity: f64 = 1; + if(closestDist == std.math.floatMax(f64)) { + // Surrounded by solid blocks → move upwards + vel.*[2] = unstuckVelocity; + pos.*[2] += vel.*[2]*deltaTime; + } else { + vel.* = @as(Vec3d, @splat(unstuckVelocity))*(@as(Vec3d, @floatFromInt(pos0 + closestEmptyBlock)) - centeredPos); + pos.* += (vel.*)*@as(Vec3d, @splat(deltaTime)); + } + } + + fn checkBlock(self: *ItemDropManager, chunk: *ServerChunk, pos: *Vec3d, blockPos: Vec3i) bool { + // Transform to chunk-relative coordinates: + const chunkPos = blockPos & ~@as(Vec3i, @splat(main.chunk.chunkMask)); + var block: blocks.Block = undefined; + if(chunk.super.pos.wx == chunkPos[0] and chunk.super.pos.wy == chunkPos[1] and chunk.super.pos.wz == chunkPos[2]) { + chunk.mutex.lock(); + defer chunk.mutex.unlock(); + block = chunk.getBlock(blockPos[0] - chunk.super.pos.wx, blockPos[1] - chunk.super.pos.wy, blockPos[2] - chunk.super.pos.wz); + } else { + const otherChunk = self.world.?.getSimulationChunkAndIncreaseRefCount(chunkPos[0], chunkPos[1], chunkPos[2]) orelse return true; + defer otherChunk.decreaseRefCount(); + const ch = otherChunk.getChunk() orelse return true; + ch.mutex.lock(); + defer ch.mutex.unlock(); + block = ch.getBlock(blockPos[0] - ch.super.pos.wx, blockPos[1] - ch.super.pos.wy, blockPos[2] - ch.super.pos.wz); + } + return main.game.collision.collideWithBlock(block, blockPos[0], blockPos[1], blockPos[2], pos.*, @splat(radius), @splat(0)) != null; + } + + pub fn checkEntity(self: *ItemDropManager, user: *main.server.User) void { + var ii: u32 = 0; + while(ii < self.size) { + const i = self.indices[ii]; + if(self.list.items(.pickupCooldown)[i] > 0) { + ii += 1; + continue; + } + const hitbox = main.game.Player.outerBoundingBox; + const min = user.player.pos + hitbox.min; + const max = user.player.pos + hitbox.max; + const itemPos = self.list.items(.pos)[i]; + const dist = @max(min - itemPos, itemPos - max); + if(@reduce(.Max, dist) < radius + pickupRange) { + const itemStack = &self.list.items(.itemStack)[i]; + main.items.Inventory.Sync.ServerSide.tryCollectingToPlayerInventory(user, itemStack); + if(itemStack.amount == 0) { + self.directRemove(i); + continue; + } + } + ii += 1; + } + } +}; + +pub const ClientItemDropManager = struct { // MARK: ClientItemDropManager + const maxf64Capacity = ItemDropManager.maxCapacity*@sizeOf(Vec3d)/@sizeOf(f64); + + super: ItemDropManager, + + lastTime: i16, + + timeDifference: utils.TimeDifference = .{}, + + interpolation: utils.GenericInterpolation(maxf64Capacity) align(64) = undefined, + + var instance: ?*ClientItemDropManager = null; + + var mutex: std.Thread.Mutex = .{}; + + pub fn init(self: *ClientItemDropManager, allocator: NeverFailingAllocator) void { + std.debug.assert(instance == null); // Only one instance allowed. + instance = self; + self.* = .{ + .super = undefined, + .lastTime = @as(i16, @truncate(main.timestamp().toMilliseconds())) -% settings.entityLookback, + }; + self.super.init(allocator, null); + self.interpolation.init( + @ptrCast(self.super.list.items(.pos).ptr), + @ptrCast(self.super.list.items(.vel).ptr), + ); + } + + pub fn deinit(self: *ClientItemDropManager) void { + std.debug.assert(instance != null); // Double deinit. + self.super.deinit(); + instance = null; + } + + pub fn readPosition(self: *ClientItemDropManager, time: i16, itemData: []ItemDropNetworkData) void { + self.timeDifference.addDataPoint(time); + var pos: [ItemDropManager.maxCapacity]Vec3d = undefined; + var vel: [ItemDropManager.maxCapacity]Vec3d = undefined; + for(itemData) |data| { + pos[data.index] = data.pos; + vel[data.index] = data.vel; + } + mutex.lock(); + defer mutex.unlock(); + self.interpolation.updatePosition(@ptrCast(&pos), @ptrCast(&vel), time); // TODO: Only update the ones we actually changed. + } + + pub fn updateInterpolationData(self: *ClientItemDropManager) void { + self.super.processChanges(); + var time = @as(i16, @truncate(main.timestamp().toMilliseconds())) -% settings.entityLookback; + time -%= self.timeDifference.difference.load(.monotonic); + { + mutex.lock(); + defer mutex.unlock(); + self.interpolation.updateIndexed(time, self.lastTime, self.super.indices[0..self.super.size], 4); + } + self.lastTime = time; + } + + fn clientSideInternalAdd(_: *ItemDropManager, i: u16, drop: ItemDrop) void { + mutex.lock(); + defer mutex.unlock(); + for(&instance.?.interpolation.lastVel) |*lastVel| { + @as(*align(8) [ItemDropManager.maxCapacity]Vec3d, @ptrCast(lastVel))[i] = Vec3d{0, 0, 0}; + } + for(&instance.?.interpolation.lastPos) |*lastPos| { + @as(*align(8) [ItemDropManager.maxCapacity]Vec3d, @ptrCast(lastPos))[i] = drop.pos; + } + } + + pub fn remove(self: *ClientItemDropManager, i: u16) void { + self.super.emptyMutex.lock(); + self.super.isEmpty.set(i); + self.super.emptyMutex.unlock(); + self.super.changeQueue.pushBack(.{.remove = i}); + } + + pub fn loadFrom(self: *ClientItemDropManager, zon: ZonElement) void { + self.super.loadFrom(zon); + } + + pub fn addFromZon(self: *ClientItemDropManager, zon: ZonElement) void { + self.super.addFromZon(zon); + } +}; + +// Going to handle item animations and other things like - bobbing, interpolation, movement reactions +pub const ItemDisplayManager = struct { // MARK: ItemDisplayManager + pub var showItem: bool = true; + var cameraFollow: Vec3f = @splat(0); + var cameraFollowVel: Vec3f = @splat(0); + const damping: Vec3f = @splat(130); + + pub fn update(deltaTime: f64) void { + if(deltaTime == 0) return; + const dt: f32 = @floatCast(deltaTime); + + var playerVel: Vec3f = .{@floatCast((game.Player.super.vel[2]*0.009 + game.Player.eye.vel[2]*0.0075)), 0, 0}; + playerVel = vec.clampMag(playerVel, 0.32); + + // TODO: add *smooth* item sway + const n1: Vec3f = cameraFollowVel - (cameraFollow - playerVel)*damping*damping*@as(Vec3f, @splat(dt)); + const n2: Vec3f = @as(Vec3f, @splat(1)) + damping*@as(Vec3f, @splat(dt)); + cameraFollowVel = n1/(n2*n2); + + cameraFollow += cameraFollowVel*@as(Vec3f, @splat(dt)); + } +}; + +pub const ItemDropRenderer = struct { // MARK: ItemDropRenderer + var itemPipeline: graphics.Pipeline = undefined; + var itemUniforms: struct { + projectionMatrix: c_int, + modelMatrix: c_int, + viewMatrix: c_int, + ambientLight: c_int, + modelIndex: c_int, + block: c_int, + reflectionMapSize: c_int, + contrast: c_int, + glDepthRange: c_int, + } = undefined; + + var itemModelSSBO: graphics.SSBO = undefined; + var modelData: main.List(u32) = undefined; + var freeSlots: main.List(*ItemVoxelModel) = undefined; + + const ItemVoxelModel = struct { + index: u31 = undefined, + len: u31 = undefined, + item: items.Item, + + fn getSlot(len: u31) u31 { + for(freeSlots.items, 0..) |potentialSlot, i| { + if(len == potentialSlot.len) { + _ = freeSlots.swapRemove(i); + const result = potentialSlot.index; + main.globalAllocator.destroy(potentialSlot); + return result; + } + } + const result: u31 = @intCast(modelData.items.len); + modelData.resize(result + len); + return result; + } + + fn init(template: ItemVoxelModel) *ItemVoxelModel { + const self = main.globalAllocator.create(ItemVoxelModel); + self.* = ItemVoxelModel{ + .item = template.item, + }; + if(self.item == .baseItem and self.item.baseItem.block() != null and self.item.baseItem.image().imageData.ptr == graphics.Image.defaultImage.imageData.ptr) { + // Find sizes and free index: + var block = blocks.Block{.typ = self.item.baseItem.block().?, .data = 0}; + block.data = block.mode().naturalStandard; + const model = blocks.meshes.model(block).model(); + var data = main.List(u32).init(main.stackAllocator); + defer data.deinit(); + for(model.internalQuads) |quad| { + const textureIndex = blocks.meshes.textureIndex(block, quad.quadInfo().textureSlot); + data.append(@as(u32, @intFromEnum(quad)) << 16 | textureIndex); // modelAndTexture + data.append(0); // offsetByNormal + } + for(model.neighborFacingQuads) |list| { + for(list) |quad| { + const textureIndex = blocks.meshes.textureIndex(block, quad.quadInfo().textureSlot); + data.append(@as(u32, @intFromEnum(quad)) << 16 | textureIndex); // modelAndTexture + data.append(1); // offsetByNormal + } + } + self.len = @intCast(data.items.len); + self.index = getSlot(self.len); + @memcpy(modelData.items[self.index..][0..self.len], data.items); + } else { + // Find sizes and free index: + const img = self.item.getImage(); + const size = Vec3i{img.width, 1, img.height}; + self.len = @intCast(3 + @reduce(.Mul, size)); + self.index = getSlot(self.len); + var dataSection: []u32 = undefined; + dataSection = modelData.items[self.index..][0..self.len]; + dataSection[0] = @intCast(size[0]); + dataSection[1] = @intCast(size[1]); + dataSection[2] = @intCast(size[2]); + var i: u32 = 3; + var z: u32 = 0; + while(z < 1) : (z += 1) { + var x: u32 = 0; + while(x < img.width) : (x += 1) { + var y: u32 = 0; + while(y < img.height) : (y += 1) { + dataSection[i] = img.getRGB(x, y).toARBG(); + i += 1; + } + } + } + } + itemModelSSBO.bufferData(u32, modelData.items); + return self; + } + + fn deinit(self: *ItemVoxelModel) void { + freeSlots.append(self); + } + + pub fn equals(self: ItemVoxelModel, other: ?*ItemVoxelModel) bool { + if(other == null) return false; + return std.meta.eql(self.item, other.?.item); + } + + pub fn hashCode(self: ItemVoxelModel) u32 { + return self.item.hashCode(); + } + }; + + pub fn init() void { + itemPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/item_drop.vert", + "assets/cubyz/shaders/item_drop.frag", + "", + &itemUniforms, + .{}, + .{.depthTest = true}, + .{.attachments = &.{.alphaBlending}}, + ); + itemModelSSBO = .init(); + itemModelSSBO.bufferData(i32, &[3]i32{1, 1, 1}); + itemModelSSBO.bind(2); + + modelData = .init(main.globalAllocator); + freeSlots = .init(main.globalAllocator); + } + + pub fn deinit() void { + itemPipeline.deinit(); + itemModelSSBO.deinit(); + modelData.deinit(); + voxelModels.clear(); + for(freeSlots.items) |freeSlot| { + main.globalAllocator.destroy(freeSlot); + } + freeSlots.deinit(); + } + + var voxelModels: utils.Cache(ItemVoxelModel, 32, 32, ItemVoxelModel.deinit) = .{}; + + fn getModel(item: items.Item) *ItemVoxelModel { + const compareObject = ItemVoxelModel{.item = item}; + return voxelModels.findOrCreate(compareObject, ItemVoxelModel.init, null); + } + + fn bindCommonUniforms(projMatrix: Mat4f, viewMatrix: Mat4f, ambientLight: Vec3f) void { + itemPipeline.bind(null); + c.glUniform1f(itemUniforms.reflectionMapSize, main.renderer.reflectionCubeMapSize); + c.glUniformMatrix4fv(itemUniforms.projectionMatrix, 1, c.GL_TRUE, @ptrCast(&projMatrix)); + c.glUniform3fv(itemUniforms.ambientLight, 1, @ptrCast(&ambientLight)); + c.glUniformMatrix4fv(itemUniforms.viewMatrix, 1, c.GL_TRUE, @ptrCast(&viewMatrix)); + c.glUniform1f(itemUniforms.contrast, 0.12); + var depthRange: [2]f32 = undefined; + c.glGetFloatv(c.GL_DEPTH_RANGE, &depthRange); + c.glUniform2fv(itemUniforms.glDepthRange, 1, &depthRange); + } + + fn bindLightUniform(light: [6]u8, ambientLight: Vec3f) void { + c.glUniform3fv(itemUniforms.ambientLight, 1, @ptrCast(&@max( + ambientLight*@as(Vec3f, @as(Vec3f, @floatFromInt(Vec3i{light[0], light[1], light[2]}))/@as(Vec3f, @splat(255))), + @as(Vec3f, @floatFromInt(Vec3i{light[3], light[4], light[5]}))/@as(Vec3f, @splat(255)), + ))); + } + + fn bindModelUniforms(modelIndex: u31, blockType: u16) void { + c.glUniform1i(itemUniforms.modelIndex, modelIndex); + c.glUniform1i(itemUniforms.block, blockType); + } + + fn drawItem(vertices: u31, modelMatrix: Mat4f) void { + c.glUniformMatrix4fv(itemUniforms.modelMatrix, 1, c.GL_TRUE, @ptrCast(&modelMatrix)); + c.glBindVertexArray(main.renderer.chunk_meshing.vao); + c.glDrawElements(c.GL_TRIANGLES, vertices, c.GL_UNSIGNED_INT, null); + } + + pub fn renderItemDrops(projMatrix: Mat4f, ambientLight: Vec3f, playerPos: Vec3d) void { + game.world.?.itemDrops.updateInterpolationData(); + + bindCommonUniforms(projMatrix, game.camera.viewMatrix, ambientLight); + const itemDrops = &game.world.?.itemDrops.super; + for(itemDrops.indices[0..itemDrops.size]) |i| { + const item = itemDrops.list.items(.itemStack)[i].item; + if(item != .null) { + var pos = itemDrops.list.items(.pos)[i]; + const rot = itemDrops.list.items(.rot)[i]; + const blockPos: Vec3i = @intFromFloat(@floor(pos)); + const light: [6]u8 = main.renderer.mesh_storage.getLight(blockPos[0], blockPos[1], blockPos[2]) orelse @splat(0); + bindLightUniform(light, ambientLight); + pos -= playerPos; + + const model = getModel(item); + var vertices: u31 = 36; + + var scale: f32 = 0.3; + var blockType: u16 = 0; + if(item == .baseItem and item.baseItem.block() != null and item.baseItem.image().imageData.ptr == graphics.Image.defaultImage.imageData.ptr) { + blockType = item.baseItem.block().?; + vertices = model.len/2*6; + } else { + scale = 0.5; + } + bindModelUniforms(model.index, blockType); + + var modelMatrix = Mat4f.translation(@floatCast(pos)); + modelMatrix = modelMatrix.mul(Mat4f.rotationX(-rot[0])); + modelMatrix = modelMatrix.mul(Mat4f.rotationY(-rot[1])); + modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-rot[2])); + modelMatrix = modelMatrix.mul(Mat4f.scale(@splat(scale))); + modelMatrix = modelMatrix.mul(Mat4f.translation(@splat(-0.5))); + drawItem(vertices, modelMatrix); + } + } + } + + inline fn getIndex(x: u8, y: u8, z: u8) u32 { + return (z*4) + (y*2) + (x); + } + + inline fn blendColors(a: [6]f32, b: [6]f32, t: f32) [6]f32 { + var result: [6]f32 = .{0, 0, 0, 0, 0, 0}; + inline for(0..6) |i| { + result[i] = std.math.lerp(a[i], b[i], t); + } + return result; + } + + pub fn renderDisplayItems(ambientLight: Vec3f, playerPos: Vec3d) void { + if(!ItemDisplayManager.showItem) return; + + const projMatrix: Mat4f = Mat4f.perspective(std.math.degreesToRadians(65), @as(f32, @floatFromInt(main.renderer.lastWidth))/@as(f32, @floatFromInt(main.renderer.lastHeight)), 0.01, 3); + const viewMatrix = Mat4f.identity(); + bindCommonUniforms(projMatrix, viewMatrix, ambientLight); + + const item = game.Player.inventory.getItem(game.Player.selectedSlot); + if(item != .null) { + var pos: Vec3d = Vec3d{0, 0, 0}; + const rot: Vec3f = ItemDisplayManager.cameraFollow; + + const lightPos = @as(Vec3d, @floatCast(playerPos)) - @as(Vec3f, @splat(0.5)); + const blockPos: Vec3i = @intFromFloat(@floor(lightPos)); + const localBlockPos: Vec3f = @floatCast(lightPos - @as(Vec3d, @floatFromInt(blockPos))); + + var samples: [8][6]f32 = @splat(@splat(0)); + inline for(0..2) |z| { + inline for(0..2) |y| { + inline for(0..2) |x| { + const light: [6]u8 = main.renderer.mesh_storage.getLight( + blockPos[0] +% @as(i32, @intCast(x)), + blockPos[1] +% @as(i32, @intCast(y)), + blockPos[2] +% @as(i32, @intCast(z)), + ) orelse @splat(0); + + inline for(0..6) |i| { + samples[getIndex(x, y, z)][i] = @as(f32, @floatFromInt(light[i])); + } + } + } + } + + inline for(0..2) |y| { + inline for(0..2) |x| { + samples[getIndex(x, y, 0)] = blendColors(samples[getIndex(x, y, 0)], samples[getIndex(x, y, 1)], localBlockPos[2]); + } + } + + inline for(0..2) |x| { + samples[getIndex(x, 0, 0)] = blendColors(samples[getIndex(x, 0, 0)], samples[getIndex(x, 1, 0)], localBlockPos[1]); + } + + var result: [6]u8 = .{0, 0, 0, 0, 0, 0}; + inline for(0..6) |i| { + const val = std.math.lerp(samples[getIndex(0, 0, 0)][i], samples[getIndex(1, 0, 0)][i], localBlockPos[0]); + result[i] = @as(u8, @intFromFloat(@floor(val))); + } + + bindLightUniform(result, ambientLight); + + const model = getModel(item); + var vertices: u31 = 36; + + const isBlock: bool = item == .baseItem and item.baseItem.block() != null and item.baseItem.image().imageData.ptr == graphics.Image.defaultImage.imageData.ptr; + var scale: f32 = 0; + var blockType: u16 = 0; + if(isBlock) { + blockType = item.baseItem.block().?; + vertices = model.len/2*6; + scale = 0.3; + pos = Vec3d{0.4, 0.55, -0.32}; + } else { + scale = 0.57; + pos = Vec3d{0.4, 0.65, -0.3}; + } + bindModelUniforms(model.index, blockType); + + var modelMatrix = Mat4f.rotationZ(-rot[2]); + modelMatrix = modelMatrix.mul(Mat4f.rotationY(-rot[1])); + modelMatrix = modelMatrix.mul(Mat4f.rotationX(-rot[0])); + modelMatrix = modelMatrix.mul(Mat4f.translation(@floatCast(pos))); + if(!isBlock) { + if(item == .tool) { + modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-std.math.pi*0.47)); + modelMatrix = modelMatrix.mul(Mat4f.rotationY(std.math.pi*0.25)); + } else { + modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-std.math.pi*0.45)); + } + } else { + modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-std.math.pi*0.2)); + } + modelMatrix = modelMatrix.mul(Mat4f.scale(@splat(scale))); + modelMatrix = modelMatrix.mul(Mat4f.translation(@splat(-0.5))); + drawItem(vertices, modelMatrix); + } + } +}; diff --git a/items.zig b/items.zig new file mode 100644 index 0000000000..edbaa705ee --- /dev/null +++ b/items.zig @@ -0,0 +1,1284 @@ +const std = @import("std"); + +const blocks = @import("blocks.zig"); +const Block = blocks.Block; +const graphics = @import("graphics.zig"); +const Color = graphics.Color; +const Tag = main.Tag; +const ZonElement = @import("zon.zig").ZonElement; +const main = @import("main"); +const ListUnmanaged = main.ListUnmanaged; +const BinaryReader = main.utils.BinaryReader; +const BinaryWriter = main.utils.BinaryWriter; +const NeverFailingAllocator = main.heap.NeverFailingAllocator; +const chunk = main.chunk; +const random = @import("random.zig"); +const vec = @import("vec.zig"); +const Mat4f = vec.Mat4f; +const Vec2f = vec.Vec2f; +const Vec2i = vec.Vec2i; +const Vec3i = vec.Vec3i; +const Vec3f = vec.Vec3f; + +const modifierList = @import("tool/modifiers/_list.zig"); +const modifierRestrictionList = @import("tool/modifiers/restrictions/_list.zig"); + +pub const recipes_zig = @import("items/recipes.zig"); + +pub const Inventory = @import("Inventory.zig"); + +const Material = struct { // MARK: Material + massDamage: f32 = undefined, + hardnessDamage: f32 = undefined, + durability: f32 = undefined, + swingSpeed: f32 = undefined, + + textureRoughness: f32 = undefined, + colorPalette: []Color = undefined, + modifiers: []Modifier = undefined, + + pub fn init(self: *Material, allocator: NeverFailingAllocator, zon: ZonElement) void { + self.massDamage = zon.get(?f32, "massDamage", null) orelse blk: { + std.log.err("Couldn't find material attribute 'massDamage'", .{}); + break :blk 0; + }; + self.hardnessDamage = zon.get(?f32, "hardnessDamage", null) orelse blk: { + std.log.err("Couldn't find material attribute 'hardnessDamage'", .{}); + break :blk 0; + }; + self.durability = zon.get(?f32, "durability", null) orelse blk: { + std.log.err("Couldn't find material attribute 'durability'", .{}); + break :blk 0; + }; + self.swingSpeed = zon.get(?f32, "swingSpeed", null) orelse blk: { + std.log.err("Couldn't find material attribute 'swingSpeed'", .{}); + break :blk 0; + }; + self.textureRoughness = @max(0, zon.get(f32, "textureRoughness", 1.0)); + const colors = zon.getChild("colors"); + self.colorPalette = allocator.alloc(Color, colors.toSlice().len); + for(colors.toSlice(), self.colorPalette) |item, *color| { + const colorInt: u32 = @intCast(item.as(i64, 0xff000000) & 0xffffffff); + color.* = Color{ + .r = @intCast(colorInt >> 16 & 0xff), + .g = @intCast(colorInt >> 8 & 0xff), + .b = @intCast(colorInt >> 0 & 0xff), + .a = @intCast(colorInt >> 24 & 0xff), + }; + } + const modifiersZon = zon.getChild("modifiers"); + self.modifiers = allocator.alloc(Modifier, modifiersZon.toSlice().len); + for(modifiersZon.toSlice(), self.modifiers) |item, *modifier| { + const id = item.get([]const u8, "id", "not specified"); + const vTable = modifiers.get(id) orelse blk: { + std.log.err("Couldn't find modifier with id '{s}'. Replacing it with 'durable'", .{id}); + break :blk modifiers.get("durable") orelse unreachable; + }; + modifier.* = .{ + .vTable = vTable, + .data = vTable.loadData(item), + .restriction = ModifierRestriction.loadFromZon(allocator, item.getChild("restriction")), + }; + } + } + + pub fn hashCode(self: Material) u32 { + var hash: u32 = 0; + hash = 101*%hash +% @as(u32, @bitCast(self.massDamage)); + hash = 101*%hash +% @as(u32, @bitCast(self.hardnessDamage)); + hash = 101*%hash +% @as(u32, @bitCast(self.durability)); + hash = 101*%hash +% @as(u32, @bitCast(self.swingSpeed)); + hash = 101*%hash +% @as(u32, @bitCast(self.textureRoughness)); + hash ^= hash >> 24; + return hash; + } + + fn getProperty(self: Material, prop: MaterialProperty) f32 { + switch(prop) { + inline else => |field| return @field(self, @tagName(field)), + } + } + + pub fn printTooltip(self: Material, outString: *main.List(u8)) void { + if(self.modifiers.len == 0) { + outString.appendSlice("§#808080Material\n"); + } + for(self.modifiers) |modifier| { + if(modifier.restriction.vTable == modifierRestrictions.get("always") orelse unreachable) { + modifier.printTooltip(outString); + outString.appendSlice("\n"); + } else { + outString.appendSlice("§#808080if "); + modifier.restriction.printTooltip(outString); + outString.appendSlice("\n "); + modifier.printTooltip(outString); + outString.appendSlice("\n"); + } + } + } +}; + +pub const ModifierRestriction = struct { + vTable: *const VTable, + data: *anyopaque, + + pub const VTable = struct { + satisfied: *const fn(data: *anyopaque, tool: *const Tool, x: i32, y: i32) bool, + loadFromZon: *const fn(allocator: NeverFailingAllocator, zon: ZonElement) *anyopaque, + printTooltip: *const fn(data: *anyopaque, outString: *main.List(u8)) void, + }; + + pub fn satisfied(self: ModifierRestriction, tool: *const Tool, x: i32, y: i32) bool { + return self.vTable.satisfied(self.data, tool, x, y); + } + + pub fn loadFromZon(allocator: NeverFailingAllocator, zon: ZonElement) ModifierRestriction { + const id = zon.get([]const u8, "id", "always"); + const vTable = modifierRestrictions.get(id) orelse blk: { + std.log.err("Couldn't find modifier restriction with id '{s}'. Replacing it with 'always'", .{id}); + break :blk modifierRestrictions.get("always") orelse unreachable; + }; + return .{ + .vTable = vTable, + .data = vTable.loadFromZon(allocator, zon), + }; + } + + pub fn printTooltip(self: ModifierRestriction, outString: *main.List(u8)) void { + self.vTable.printTooltip(self.data, outString); + } +}; + +const Modifier = struct { + data: VTable.Data, + restriction: ModifierRestriction, + vTable: *const VTable, + + pub const VTable = struct { + const Data = packed struct(u128) {pad: u128}; + combineModifiers: *const fn(data1: Data, data2: Data) ?Data, + changeToolParameters: *const fn(tool: *Tool, data: Data) void, + changeBlockDamage: *const fn(damage: f32, block: main.blocks.Block, data: Data) f32, + printTooltip: *const fn(outString: *main.List(u8), data: Data) void, + loadData: *const fn(zon: ZonElement) Data, + priority: f32, + }; + + pub fn combineModifiers(a: Modifier, b: Modifier) ?Modifier { + std.debug.assert(a.vTable == b.vTable); + return .{ + .data = a.vTable.combineModifiers(a.data, b.data) orelse return null, + .vTable = a.vTable, + .restriction = undefined, + }; + } + + pub fn changeToolParameters(self: Modifier, tool: *Tool) void { + self.vTable.changeToolParameters(tool, self.data); + } + + pub fn changeBlockDamage(self: Modifier, damage: f32, block: main.blocks.Block) f32 { + return self.vTable.changeBlockDamage(damage, block, self.data); + } + + pub fn printTooltip(self: Modifier, outString: *main.List(u8)) void { + self.vTable.printTooltip(outString, self.data); + } +}; + +const MaterialProperty = enum { + massDamage, + hardnessDamage, + durability, + swingSpeed, + + fn fromString(string: []const u8) ?MaterialProperty { + return std.meta.stringToEnum(MaterialProperty, string) orelse { + std.log.err("Couldn't find material property {s}.", .{string}); + return null; + }; + } +}; + +pub const BaseItemIndex = enum(u16) { // MARK: BaseItemIndex + _, + + pub fn fromId(_id: []const u8) ?BaseItemIndex { + return reverseIndices.get(_id); + } + pub fn image(self: BaseItemIndex) graphics.Image { + return itemList[@intFromEnum(self)].image; + } + pub fn texture(self: BaseItemIndex) ?graphics.Texture { + return itemList[@intFromEnum(self)].texture; + } + pub fn id(self: BaseItemIndex) []const u8 { + return itemList[@intFromEnum(self)].id; + } + pub fn name(self: BaseItemIndex) []const u8 { + return itemList[@intFromEnum(self)].name; + } + pub fn tags(self: BaseItemIndex) []const Tag { + return itemList[@intFromEnum(self)].tags; + } + pub fn stackSize(self: BaseItemIndex) u16 { + return itemList[@intFromEnum(self)].stackSize; + } + pub fn material(self: BaseItemIndex) ?Material { + return itemList[@intFromEnum(self)].material; + } + pub fn block(self: BaseItemIndex) ?u16 { + return itemList[@intFromEnum(self)].block; + } + pub fn hasTag(self: BaseItemIndex, tag: Tag) bool { + return itemList[@intFromEnum(self)].hasTag(tag); + } + pub fn hashCode(self: BaseItemIndex) u32 { + return itemList[@intFromEnum(self)].hashCode(); + } + pub fn getTexture(self: BaseItemIndex) graphics.Texture { + return itemList[@intFromEnum(self)].getTexture(); + } + pub fn getTooltip(self: BaseItemIndex) []const u8 { + return itemList[@intFromEnum(self)].getTooltip(); + } +}; + +pub const BaseItem = struct { // MARK: BaseItem + image: graphics.Image, + texture: ?graphics.Texture, // TODO: Properly deinit + id: []const u8, + name: []const u8, + tags: []const Tag, + tooltip: []const u8, + + stackSize: u16, + material: ?Material, + block: ?u16, + foodValue: f32, // TODO: Effects. + + fn init(self: *BaseItem, allocator: NeverFailingAllocator, texturePath: []const u8, replacementTexturePath: []const u8, id: []const u8, zon: ZonElement) void { + self.id = allocator.dupe(u8, id); + if(texturePath.len == 0) { + self.image = graphics.Image.defaultImage; + } else { + self.image = graphics.Image.readFromFile(allocator, texturePath) catch graphics.Image.readFromFile(allocator, replacementTexturePath) catch blk: { + std.log.err("Item texture not found in {s} and {s}.", .{texturePath, replacementTexturePath}); + break :blk graphics.Image.defaultImage; + }; + } + self.name = allocator.dupe(u8, zon.get([]const u8, "name", id)); + self.tags = Tag.loadTagsFromZon(allocator, zon.getChild("tags")); + self.stackSize = zon.get(u16, "stackSize", 120); + const material = zon.getChild("material"); + if(material == .object) { + self.material = Material{}; + self.material.?.init(allocator, material); + } else { + self.material = null; + } + self.block = blk: { + break :blk blocks.getTypeById(zon.get(?[]const u8, "block", null) orelse break :blk null); + }; + self.texture = null; + self.foodValue = zon.get(f32, "food", 0); + + var tooltip: main.List(u8) = .init(allocator); + tooltip.appendSlice(self.name); + tooltip.append('\n'); + if(self.material) |mat| { + mat.printTooltip(&tooltip); + } + if(self.tags.len != 0) { + tooltip.appendSlice("§#808080"); + for(self.tags, 0..) |tag, i| { + if(i != 0) tooltip.append(' '); + tooltip.append('.'); + tooltip.appendSlice(tag.getName()); + } + } + if(tooltip.items[tooltip.items.len - 1] == '\n') { + _ = tooltip.swapRemove(tooltip.items.len - 1); + } + self.tooltip = tooltip.toOwnedSlice(); + } + + fn hashCode(self: BaseItem) u32 { + var hash: u32 = 0; + for(self.id) |char| { + hash = hash*%33 +% char; + } + return hash; + } + + pub fn getTexture(self: *BaseItem) graphics.Texture { + if(self.texture == null) { + if(self.image.imageData.ptr == graphics.Image.defaultImage.imageData.ptr) { + if(self.block) |blockType| { + self.texture = graphics.generateBlockTexture(blockType); + } else { + self.texture = graphics.Texture.init(); + self.texture.?.generate(self.image); + } + } else { + self.texture = graphics.Texture.init(); + self.texture.?.generate(self.image); + } + } + return self.texture.?; + } + + fn getTooltip(self: BaseItem) []const u8 { + return self.tooltip; + } + + pub fn hasTag(self: *const BaseItem, tag: Tag) bool { + for(self.tags) |other| { + if(other == tag) return true; + } + return false; + } +}; + +/// Generates the texture of a Tool using the material information. +const TextureGenerator = struct { // MARK: TextureGenerator + fn generateHeightMap(itemGrid: *[16][16]?BaseItemIndex, seed: *u64) [17][17]f32 { + var heightMap: [17][17]f32 = undefined; + var x: u8 = 0; + while(x < 17) : (x += 1) { + var y: u8 = 0; + while(y < 17) : (y += 1) { + heightMap[x][y] = 0; + // The heighmap basically consists of the amount of neighbors this pixel has. + // Also check if there are different neighbors. + const oneItem = itemGrid[if(x == 0) x else x - 1][if(y == 0) y else y - 1]; + var hasDifferentItems: bool = false; + var dx: i32 = -1; + while(dx <= 0) : (dx += 1) { + if(x + dx < 0 or x + dx >= 16) continue; + var dy: i32 = -1; + while(dy <= 0) : (dy += 1) { + if(y + dy < 0 or y + dy >= 16) continue; + const otherItem = itemGrid[@intCast(x + dx)][@intCast(y + dy)]; + heightMap[x][y] = if(otherItem) |item| (if(item.material()) |material| 1 + (4*random.nextFloat(seed) - 2)*material.textureRoughness else 0) else 0; + if(otherItem != oneItem) { + hasDifferentItems = true; + } + } + } + + // If there is multiple items at this junction, make it go inward to make embedded parts stick out more: + if(hasDifferentItems) { + heightMap[x][y] -= 1; + } + + // Take into account further neighbors with lower priority: + dx = -2; + while(dx <= 1) : (dx += 1) { + if(x + dx < 0 or x + dx >= 16) continue; + var dy: i32 = -2; + while(dy <= 1) : (dy += 1) { + if(y + dy < 0 or y + dy >= 16) continue; + const otherItem = itemGrid[@intCast(x + dx)][@intCast(y + dy)]; + const dVec = Vec2f{@as(f32, @floatFromInt(dx)) + 0.5, @as(f32, @floatFromInt(dy)) + 0.5}; + heightMap[x][y] += if(otherItem != null) 1.0/vec.dot(dVec, dVec) else 0; + } + } + } + } + return heightMap; + } + + pub fn generate(tool: *Tool) void { + const img = tool.image; + for(0..16) |x| { + for(0..16) |y| { + const source = tool.type.pixelSources()[x][y]; + const sourceOverlay = tool.type.pixelSourcesOverlay()[x][y]; + if(sourceOverlay < 25 and tool.craftingGrid[sourceOverlay] != null) { + tool.materialGrid[x][y] = tool.craftingGrid[sourceOverlay]; + } else if(source < 25) { + tool.materialGrid[x][y] = tool.craftingGrid[source]; + } else { + tool.materialGrid[x][y] = null; + } + } + } + + var seed: u64 = tool.seed; + random.scrambleSeed(&seed); + + // Generate a height map, which will be used for lighting calulations. + const heightMap = generateHeightMap(&tool.materialGrid, &seed); + var x: u8 = 0; + while(x < 16) : (x += 1) { + var y: u8 = 0; + while(y < 16) : (y += 1) { + if(tool.materialGrid[x][y]) |item| { + if(item.material()) |material| { + // Calculate the lighting based on the nearest free space: + const lightTL = heightMap[x][y] - heightMap[x + 1][y + 1]; + const lightTR = heightMap[x + 1][y] - heightMap[x][y + 1]; + var light = 2 - @as(i32, @intFromFloat(@round((lightTL*2 + lightTR)/6))); + light = @max(@min(light, 4), 0); + img.setRGB(x, 15 - y, material.colorPalette[@intCast(light)]); + } else { + img.setRGB(x, 15 - y, if((x ^ y) & 1 == 0) Color{.r = 255, .g = 0, .b = 255, .a = 255} else Color{.r = 0, .g = 0, .b = 0, .a = 255}); + } + } else { + img.setRGB(x, 15 - y, Color{.r = 0, .g = 0, .b = 0, .a = 0}); + } + } + } + } +}; + +/// Determines the physical properties of a tool to calculate in-game parameters such as durability and speed. +const ToolPhysics = struct { // MARK: ToolPhysics + /// Determines all the basic properties of the tool. + pub fn evaluateTool(tool: *Tool) void { + inline for(comptime std.meta.fieldNames(ToolProperty)) |name| { + @field(tool, name) = 0; + } + var tempModifiers: main.List(Modifier) = .init(main.stackAllocator); + defer tempModifiers.deinit(); + for(tool.type.properties()) |property| { + var sum: f32 = 0; + var weight: f32 = 0; + for(0..25) |i| { + const material = (tool.craftingGrid[i] orelse continue).material() orelse continue; + sum += property.weights[i]*material.getProperty(property.source orelse break); + weight += property.weights[i]; + } + if(weight == 0) continue; + switch(property.method) { + .sum => {}, + .average => { + sum /= weight; + }, + } + sum *= property.resultScale; + tool.getProperty(property.destination orelse continue).* += sum; + } + if(tool.damage < 1) tool.damage = 1/(2 - tool.damage); + if(tool.swingSpeed < 1) tool.swingSpeed = 1/(2 - tool.swingSpeed); + for(0..25) |i| { + const material = (tool.craftingGrid[i] orelse continue).material() orelse continue; + outer: for(material.modifiers) |newMod| { + if(!newMod.restriction.satisfied(tool, @intCast(i%5), @intCast(i/5))) continue; + for(tempModifiers.items) |*oldMod| { + if(oldMod.vTable == newMod.vTable) { + oldMod.* = oldMod.combineModifiers(newMod) orelse continue; + continue :outer; + } + } + tempModifiers.append(newMod); + } + } + std.sort.insertion(Modifier, tempModifiers.items, {}, struct { + fn lessThan(_: void, lhs: Modifier, rhs: Modifier) bool { + return lhs.vTable.priority < rhs.vTable.priority; + } + }.lessThan); + tool.modifiers = main.globalAllocator.dupe(Modifier, tempModifiers.items); + for(tempModifiers.items) |mod| { + mod.changeToolParameters(tool); + } + + tool.maxDurability = @round(tool.maxDurability); + if(tool.maxDurability < 1) tool.maxDurability = 1; + tool.durability = std.math.lossyCast(u32, tool.maxDurability); + + if(!checkConnectivity(tool)) { + tool.maxDurability = 0; + tool.durability = 1; + } + } + + fn checkConnectivity(tool: *Tool) bool { + var gridCellsReached: [16][16]bool = @splat(@splat(false)); + var floodfillQueue = main.utils.CircularBufferQueue(Vec2i).init(main.stackAllocator, 16); + defer floodfillQueue.deinit(); + outer: for(tool.materialGrid, 0..) |row, x| { + for(row, 0..) |entry, y| { + if(entry != null) { + floodfillQueue.pushBack(.{@intCast(x), @intCast(y)}); + gridCellsReached[x][y] = true; + break :outer; + } + } + } + while(floodfillQueue.popFront()) |pos| { + for([4]Vec2i{.{-1, 0}, .{1, 0}, .{0, -1}, .{0, 1}}) |delta| { + const newPos = pos + delta; + if(newPos[0] < 0 or newPos[0] >= gridCellsReached.len) continue; + if(newPos[1] < 0 or newPos[1] >= gridCellsReached.len) continue; + const x: usize = @intCast(newPos[0]); + const y: usize = @intCast(newPos[1]); + if(gridCellsReached[x][y]) continue; + if(tool.materialGrid[x][y] == null) continue; + gridCellsReached[x][y] = true; + floodfillQueue.pushBack(newPos); + } + } + for(tool.materialGrid, 0..) |row, x| { + for(row, 0..) |entry, y| { + if(entry != null and !gridCellsReached[x][y]) { + return false; + } + } + } + return true; + } +}; + +const SlotInfo = packed struct { // MARK: SlotInfo + disabled: bool = false, + optional: bool = false, +}; + +const PropertyMatrix = struct { // MARK: PropertyMatrix + source: ?MaterialProperty, + destination: ?ToolProperty, + weights: [25]f32, + resultScale: f32, + method: Method, + + const Method = enum { + average, + sum, + + fn fromString(string: []const u8) ?Method { + return std.meta.stringToEnum(Method, string) orelse { + std.log.err("Couldn't find property matrix method {s}.", .{string}); + return null; + }; + } + }; +}; + +pub const ToolTypeIndex = enum(u16) { + _, + + const ToolTypeIterator = struct { + i: u16 = 0, + + pub fn next(self: *ToolTypeIterator) ?ToolTypeIndex { + if(self.i >= toolTypeList.items.len) return null; + defer self.i += 1; + return @enumFromInt(self.i); + } + }; + + pub fn iterator() ToolTypeIterator { + return .{}; + } + pub fn fromId(_id: []const u8) ?ToolTypeIndex { + return toolTypeIdToIndex.get(_id); + } + pub fn id(self: ToolTypeIndex) []const u8 { + return toolTypeList.items[@intFromEnum(self)].id; + } + pub fn blockTags(self: ToolTypeIndex) []const Tag { + return toolTypeList.items[@intFromEnum(self)].blockTags; + } + pub fn properties(self: ToolTypeIndex) []const PropertyMatrix { + return toolTypeList.items[@intFromEnum(self)].properties; + } + pub fn slotInfos(self: ToolTypeIndex) *const [25]SlotInfo { + return &toolTypeList.items[@intFromEnum(self)].slotInfos; + } + pub fn pixelSources(self: ToolTypeIndex) *const [16][16]u8 { + return &toolTypeList.items[@intFromEnum(self)].pixelSources; + } + pub fn pixelSourcesOverlay(self: ToolTypeIndex) *const [16][16]u8 { + return &toolTypeList.items[@intFromEnum(self)].pixelSourcesOverlay; + } +}; + +pub const ToolType = struct { // MARK: ToolType + id: []const u8, + blockTags: []main.Tag, + properties: []PropertyMatrix, + slotInfos: [25]SlotInfo, + pixelSources: [16][16]u8, + pixelSourcesOverlay: [16][16]u8, +}; + +const ToolProperty = enum { + damage, + maxDurability, + swingSpeed, + + fn fromString(string: []const u8) ?ToolProperty { + return std.meta.stringToEnum(ToolProperty, string) orelse { + std.log.err("Couldn't find tool property {s}.", .{string}); + return null; + }; + } +}; + +pub const Tool = struct { // MARK: Tool + const craftingGridSize = 25; + const CraftingGridMask = std.meta.Int(.unsigned, craftingGridSize); + + craftingGrid: [craftingGridSize]?BaseItemIndex, + materialGrid: [16][16]?BaseItemIndex, + modifiers: []Modifier, + tooltip: main.List(u8), + image: graphics.Image, + texture: ?graphics.Texture, + seed: u32, + type: ToolTypeIndex, + + damage: f32, + + durability: u32, + maxDurability: f32, + + /// swings per second + swingSpeed: f32, + + mass: f32, + + /// Where the player holds the tool. + handlePosition: Vec2f, + /// Moment of inertia relative to the handle. + inertiaHandle: f32, + + /// Where the tool rotates around when being thrown. + centerOfMass: Vec2f, + /// Moment of inertia relative to the center of mass. + inertiaCenterOfMass: f32, + + pub fn init() *Tool { + const self = main.globalAllocator.create(Tool); + self.image = graphics.Image.init(main.globalAllocator, 16, 16); + self.texture = null; + self.tooltip = .init(main.globalAllocator); + return self; + } + + pub fn deinit(self: *const Tool) void { + // TODO: This is leaking textures! + // if(self.texture) |texture| { + // texture.deinit(); + // } + self.image.deinit(main.globalAllocator); + self.tooltip.deinit(); + main.globalAllocator.free(self.modifiers); + main.globalAllocator.destroy(self); + } + + pub fn clone(self: *const Tool) *Tool { + const result = main.globalAllocator.create(Tool); + result.* = .{ + .craftingGrid = self.craftingGrid, + .materialGrid = self.materialGrid, + .modifiers = main.globalAllocator.dupe(Modifier, self.modifiers), + .tooltip = .init(main.globalAllocator), + .image = graphics.Image.init(main.globalAllocator, self.image.width, self.image.height), + .texture = null, + .seed = self.seed, + .type = self.type, + .damage = self.damage, + .durability = self.durability, + .maxDurability = self.maxDurability, + .swingSpeed = self.swingSpeed, + .mass = self.mass, + .handlePosition = self.handlePosition, + .inertiaHandle = self.inertiaHandle, + .centerOfMass = self.centerOfMass, + .inertiaCenterOfMass = self.inertiaCenterOfMass, + }; + @memcpy(result.image.imageData, self.image.imageData); + return result; + } + + pub fn initFromCraftingGrid(craftingGrid: [25]?BaseItemIndex, seed: u32, typ: ToolTypeIndex) *Tool { + const self = init(); + self.seed = seed; + self.craftingGrid = craftingGrid; + self.type = typ; + // Produce the tool and its textures: + // The material grid, which comes from texture generation, is needed on both server and client, to generate the tool properties. + TextureGenerator.generate(self); + ToolPhysics.evaluateTool(self); + return self; + } + + pub fn initFromZon(zon: ZonElement) *Tool { + const self = initFromCraftingGrid(extractItemsFromZon(zon.getChild("grid")), zon.get(u32, "seed", 0), ToolTypeIndex.fromId(zon.get([]const u8, "type", "cubyz:pickaxe")) orelse blk: { + std.log.err("Couldn't find tool with type {s}. Replacing it with cubyz:pickaxe", .{zon.get([]const u8, "type", "cubyz:pickaxe")}); + break :blk ToolTypeIndex.fromId("cubyz:pickaxe") orelse @panic("cubyz:pickaxe tool not found. Did you load the game with the correct assets?"); + }); + self.durability = zon.get(u32, "durability", std.math.lossyCast(u32, self.maxDurability)); + return self; + } + + fn extractItemsFromZon(zonArray: ZonElement) [craftingGridSize]?BaseItemIndex { + var items: [craftingGridSize]?BaseItemIndex = undefined; + for(&items, 0..) |*item, i| { + item.* = .fromId(zonArray.getAtIndex([]const u8, i, "null")); + if(item.* != null and item.*.?.material() == null) item.* = null; + } + return items; + } + + pub fn save(self: *const Tool, allocator: NeverFailingAllocator) ZonElement { + const zonObject = ZonElement.initObject(allocator); + const zonArray = ZonElement.initArray(allocator); + for(self.craftingGrid) |nullableItem| { + if(nullableItem) |item| { + zonArray.array.append(.{.string = item.id()}); + } else { + zonArray.array.append(.null); + } + } + zonObject.put("grid", zonArray); + zonObject.put("durability", self.durability); + zonObject.put("seed", self.seed); + zonObject.put("type", self.type.id()); + return zonObject; + } + + pub fn fromBytes(reader: *BinaryReader) !*Tool { + const durability = try reader.readInt(u32); + const seed = try reader.readInt(u32); + const typ = try reader.readEnum(ToolTypeIndex); + + var craftingGridMask = try reader.readInt(CraftingGridMask); + var craftingGrid: [craftingGridSize]?BaseItemIndex = @splat(null); + + while(craftingGridMask != 0) { + const i = @ctz(craftingGridMask); + craftingGridMask &= ~(@as(CraftingGridMask, 1) << @intCast(i)); + craftingGrid[i] = try reader.readEnum(BaseItemIndex); + } + const self = initFromCraftingGrid(craftingGrid, seed, typ); + + self.durability = durability; + return self; + } + + pub fn toBytes(self: Tool, writer: *BinaryWriter) void { + writer.writeInt(u32, self.durability); + writer.writeInt(u32, self.seed); + writer.writeEnum(ToolTypeIndex, self.type); + + var craftingGridMask: CraftingGridMask = 0; + for(0..craftingGridSize) |i| { + if(self.craftingGrid[i] != null) { + craftingGridMask |= @as(CraftingGridMask, 1) << @intCast(i); + } + } + writer.writeInt(CraftingGridMask, craftingGridMask); + + for(0..craftingGridSize) |i| { + if(self.craftingGrid[i]) |baseItem| { + writer.writeEnum(BaseItemIndex, baseItem); + } + } + } + + pub fn hashCode(self: Tool) u32 { + var hash: u32 = 0; + for(self.craftingGrid) |nullItem| { + if(nullItem) |item| { + hash = 33*%hash +% item.material().?.hashCode(); + } + } + return hash; + } + + pub fn getItemAt(self: *const Tool, x: i32, y: i32) ?BaseItemIndex { + if(x < 0 or x >= 5) return null; + if(y < 0 or y >= 5) return null; + return self.craftingGrid[@intCast(x + y*5)]; + } + + fn getProperty(self: *Tool, prop: ToolProperty) *f32 { + switch(prop) { + inline else => |field| return &@field(self, @tagName(field)), + } + } + + fn getTexture(self: *Tool) graphics.Texture { + if(self.texture == null) { + self.texture = graphics.Texture.init(); + self.texture.?.generate(self.image); + } + return self.texture.?; + } + + fn getTooltip(self: *Tool) []const u8 { + self.tooltip.clearRetainingCapacity(); + self.tooltip.print( + \\{s} + \\{d:.2} swings/s + \\Damage: {d:.2} + \\Durability: {}/{} + , .{ + self.type.id(), + self.swingSpeed, + self.damage, + self.durability, + std.math.lossyCast(u32, self.maxDurability), + }); + if(self.modifiers.len != 0) { + self.tooltip.appendSlice("\nModifiers:\n"); + for(self.modifiers) |modifier| { + modifier.printTooltip(&self.tooltip); + self.tooltip.appendSlice("§\n"); + } + _ = self.tooltip.pop(); + } + return self.tooltip.items; + } + + pub fn isEffectiveOn(self: *Tool, block: main.blocks.Block) bool { + for(block.blockTags()) |blockTag| { + for(self.type.blockTags()) |toolTag| { + if(toolTag == blockTag) return true; + } + } + return false; + } + + pub fn getBlockDamage(self: *Tool, block: main.blocks.Block) f32 { + var damage = self.damage; + for(self.modifiers) |modifier| { + damage = modifier.changeBlockDamage(damage, block); + } + if(self.isEffectiveOn(block)) { + return damage; + } + return main.game.Player.defaultBlockDamage; + } + + pub fn onUseReturnBroken(self: *Tool) bool { + self.durability -|= 1; + return self.durability == 0; + } +}; + +const ItemType = enum(u7) { + baseItem, + tool, + null, +}; + +pub const Item = union(ItemType) { // MARK: Item + baseItem: BaseItemIndex, + tool: *Tool, + null: void, + + pub fn init(zon: ZonElement) !Item { + if(BaseItemIndex.fromId(zon.get([]const u8, "item", "null"))) |baseItem| { + return Item{.baseItem = baseItem}; + } else { + const toolZon = zon.getChild("tool"); + if(toolZon != .object) return error.ItemNotFound; + return Item{.tool = Tool.initFromZon(toolZon)}; + } + } + + pub fn deinit(self: Item) void { + switch(self) { + .baseItem, .null => {}, + .tool => |_tool| { + _tool.deinit(); + }, + } + } + + pub fn clone(self: Item) Item { + switch(self) { + .baseItem, .null => return self, + .tool => |tool| { + return .{.tool = tool.clone()}; + }, + } + } + + pub fn stackSize(self: Item) u16 { + switch(self) { + .baseItem => |_baseItem| { + return _baseItem.stackSize(); + }, + .tool => { + return 1; + }, + .null => { + return 0; + }, + } + } + + pub fn insertIntoZon(self: Item, allocator: NeverFailingAllocator, zonObject: ZonElement) void { + switch(self) { + .baseItem => |_baseItem| { + zonObject.put("item", _baseItem.id()); + }, + .tool => |_tool| { + zonObject.put("tool", _tool.save(allocator)); + }, + .null => unreachable, + } + } + + pub fn fromBytes(reader: *BinaryReader) !Item { + const typ = try reader.readEnum(ItemType); + switch(typ) { + .baseItem => { + return .{.baseItem = try reader.readEnum(BaseItemIndex)}; + }, + .tool => { + return .{.tool = try Tool.fromBytes(reader)}; + }, + .null => unreachable, + } + } + + pub fn toBytes(self: Item, writer: *BinaryWriter) void { + writer.writeEnum(ItemType, self); + switch(self) { + .baseItem => writer.writeEnum(BaseItemIndex, self.baseItem), + .tool => |tool| tool.toBytes(writer), + .null => unreachable, + } + } + + pub fn getTexture(self: Item) graphics.Texture { + return switch(self) { + .baseItem => |_baseItem| _baseItem.getTexture(), + .tool => |_tool| _tool.getTexture(), + .null => unreachable, + }; + } + + pub fn id(self: Item) ?[]const u8 { + switch(self) { + .tool => |tool| { + return tool.type.id(); + }, + .baseItem => |item| { + return item.id(); + }, + .null => return null, + } + } + + pub fn getTooltip(self: Item) ?[]const u8 { + switch(self) { + .baseItem => |_baseItem| { + return _baseItem.getTooltip(); + }, + .tool => |_tool| { + return _tool.getTooltip(); + }, + .null => return null, + } + } + + pub fn getImage(self: Item) graphics.Image { + switch(self) { + .baseItem => |_baseItem| { + return _baseItem.image(); + }, + .tool => |_tool| { + return _tool.image; + }, + .null => unreachable, + } + } + + pub fn hashCode(self: Item) u32 { + return switch(self) { + .null => unreachable, + inline else => |item| item.hashCode(), + }; + } +}; + +pub const ItemStack = struct { // MARK: ItemStack + item: Item = .null, + amount: u16 = 0, + + pub fn load(zon: ZonElement) !ItemStack { + return .{ + .item = try Item.init(zon), + .amount = zon.get(?u16, "amount", null) orelse return error.InvalidAmount, + }; + } + + pub fn deinit(self: *ItemStack) void { + self.item.deinit(); + } + + pub fn clone(self: *const ItemStack) ItemStack { + return .{ + .item = self.item.clone(), + .amount = self.amount, + }; + } + + pub fn empty(self: *const ItemStack) bool { + return self.amount == 0; + } + + pub fn storeToZon(self: *const ItemStack, allocator: NeverFailingAllocator, zonObject: ZonElement) void { + if(self.item != .null) { + self.item.insertIntoZon(allocator, zonObject); + zonObject.put("amount", self.amount); + } + } + + pub fn fromBytes(reader: *BinaryReader) !ItemStack { + const amount = try reader.readVarInt(u16); + if(amount == 0) { + return .{}; + } + const item = try Item.fromBytes(reader); + return .{ + .item = item, + .amount = amount, + }; + } + + pub fn toBytes(self: *const ItemStack, writer: *BinaryWriter) void { + if(self.item != .null) { + writer.writeVarInt(u16, self.amount); + self.item.toBytes(writer); + } else { + writer.writeVarInt(u16, 0); + } + } +}; + +pub const Recipe = struct { // MARK: Recipe + sourceItems: []BaseItemIndex, + sourceAmounts: []u16, + resultItem: BaseItemIndex, + resultAmount: u16, + cachedInventory: ?Inventory = null, +}; + +var toolTypeList: ListUnmanaged(ToolType) = .{}; +var toolTypeIdToIndex: std.StringHashMapUnmanaged(ToolTypeIndex) = .{}; + +var reverseIndices: std.StringHashMapUnmanaged(BaseItemIndex) = .{}; +var modifiers: std.StringHashMapUnmanaged(*const Modifier.VTable) = .{}; +var modifierRestrictions: std.StringHashMapUnmanaged(*const ModifierRestriction.VTable) = .{}; +pub var itemList: [65536]BaseItem = undefined; +pub var itemListSize: u16 = 0; + +var recipeList: main.List(Recipe) = undefined; + +pub fn hasRegistered(id: []const u8) bool { + return reverseIndices.contains(id); +} + +pub fn hasRegisteredTool(id: []const u8) bool { + return toolTypeIdToIndex.contains(id); +} + +pub fn iterator() std.StringHashMap(BaseItemIndex).ValueIterator { + return reverseIndices.valueIterator(); +} + +pub fn recipes() []Recipe { + return recipeList.items; +} + +pub fn globalInit() void { + toolTypeIdToIndex = .{}; + + recipeList = .init(main.worldArena); + itemListSize = 0; + inline for(@typeInfo(modifierList).@"struct".decls) |decl| { + const ModifierStruct = @field(modifierList, decl.name); + modifiers.put(main.globalArena.allocator, decl.name, &.{ + .changeToolParameters = @ptrCast(&ModifierStruct.changeToolParameters), + .changeBlockDamage = @ptrCast(&ModifierStruct.changeBlockDamage), + .combineModifiers = @ptrCast(&ModifierStruct.combineModifiers), + .printTooltip = @ptrCast(&ModifierStruct.printTooltip), + .loadData = @ptrCast(&ModifierStruct.loadData), + .priority = ModifierStruct.priority, + }) catch unreachable; + } + inline for(@typeInfo(modifierRestrictionList).@"struct".decls) |decl| { + const ModifierRestrictionStruct = @field(modifierRestrictionList, decl.name); + modifierRestrictions.put(main.globalArena.allocator, decl.name, &.{ + .satisfied = comptime main.meta.castFunctionSelfToAnyopaque(ModifierRestrictionStruct.satisfied), + .loadFromZon = comptime main.meta.castFunctionReturnToAnyopaque(ModifierRestrictionStruct.loadFromZon), + .printTooltip = comptime main.meta.castFunctionSelfToAnyopaque(ModifierRestrictionStruct.printTooltip), + }) catch unreachable; + } + Inventory.Sync.ClientSide.init(); +} + +pub fn register(_: []const u8, texturePath: []const u8, replacementTexturePath: []const u8, id: []const u8, zon: ZonElement) *BaseItem { + const newItem = &itemList[itemListSize]; + defer itemListSize += 1; + + newItem.init(main.worldArena, texturePath, replacementTexturePath, id, zon); + reverseIndices.put(main.worldArena.allocator, newItem.id, @enumFromInt(itemListSize)) catch unreachable; + + std.log.debug("Registered item: {d: >5} '{s}'", .{itemListSize, id}); + return newItem; +} + +fn loadPixelSources(assetFolder: []const u8, id: []const u8, layerPostfix: []const u8, pixelSources: *[16][16]u8) void { + var split = std.mem.splitScalar(u8, id, ':'); + const mod = split.first(); + const tool = split.rest(); + const path = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{s}/tools/{s}{s}.png", .{assetFolder, mod, tool, layerPostfix}) catch unreachable; + defer main.stackAllocator.free(path); + const image = main.graphics.Image.readFromFile(main.stackAllocator, path) catch |err| blk: { + if(err != error.FileNotFound) { + std.log.err("Error while reading tool image '{s}': {s}", .{path, @errorName(err)}); + } + const replacementPath = std.fmt.allocPrint(main.stackAllocator.allocator, "assets/{s}/tools/{s}{s}.png", .{mod, tool, layerPostfix}) catch unreachable; + defer main.stackAllocator.free(replacementPath); + break :blk main.graphics.Image.readFromFile(main.stackAllocator, replacementPath) catch |err2| { + if(layerPostfix.len == 0 or err2 != error.FileNotFound) + std.log.err("Error while reading tool image. Tried '{s}' and '{s}': {s}", .{path, replacementPath, @errorName(err2)}); + break :blk main.graphics.Image.emptyImage; + }; + }; + defer image.deinit(main.stackAllocator); + if((image.width != 16 or image.height != 16) and image.imageData.ptr != main.graphics.Image.emptyImage.imageData.ptr) { + std.log.err("Truncating image for {s} with incorrect dimensions. Should be 16×16.", .{id}); + } + for(0..16) |x| { + for(0..16) |y| { + const color = if(image.width != 0 and image.height != 0) image.getRGB(@min(image.width - 1, x), image.height - 1 - @min(image.height - 1, y)) else main.graphics.Color{.r = 0, .g = 0, .b = 0, .a = 0}; + pixelSources[x][y] = blk: { + if(color.a == 0) break :blk 255; + const xPos = color.r/52; + const yPos = color.b/52; + break :blk xPos + 5*yPos; + }; + } + } +} + +pub fn registerTool(assetFolder: []const u8, id: []const u8, zon: ZonElement) void { + var slotInfos: [25]SlotInfo = @splat(.{}); + for(zon.getChild("disabled").toSlice(), 0..) |zonDisabled, i| { + if(i >= 25) { + std.log.err("disabled array of {s} has too many entries", .{id}); + break; + } + slotInfos[i].disabled = zonDisabled.as(usize, 0) != 0; + } + for(zon.getChild("optional").toSlice(), 0..) |zonDisabled, i| { + if(i >= 25) { + std.log.err("disabled array of {s} has too many entries", .{id}); + break; + } + slotInfos[i].optional = zonDisabled.as(usize, 0) != 0; + } + var parameterMatrices: main.List(PropertyMatrix) = .init(main.worldArena); + for(zon.getChild("parameters").toSlice()) |paramZon| { + const val = parameterMatrices.addOne(); + val.source = MaterialProperty.fromString(paramZon.get([]const u8, "source", "not specified")); + val.destination = ToolProperty.fromString(paramZon.get([]const u8, "destination", "not specified")); + val.resultScale = paramZon.get(f32, "factor", 1.0); + val.method = PropertyMatrix.Method.fromString(paramZon.get([]const u8, "method", "not specified")) orelse .sum; + const matrixZon = paramZon.getChild("matrix"); + var total_weight: f32 = 0.0; + for(0..25) |i| { + val.weights[i] = matrixZon.getAtIndex(f32, i, 0.0); + } + for(0..25) |i| { + total_weight += val.weights[i]; + } + for(0..25) |i| { + if(val.weights[i] != 0x0) { + val.weights[i] /= total_weight; + } + } + } + var pixelSources: [16][16]u8 = undefined; + loadPixelSources(assetFolder, id, "", &pixelSources); + var pixelSourcesOverlay: [16][16]u8 = undefined; + loadPixelSources(assetFolder, id, "_overlay", &pixelSourcesOverlay); + + const idDupe = main.worldArena.dupe(u8, id); + toolTypeList.append(main.worldArena, .{ + .id = idDupe, + .blockTags = Tag.loadTagsFromZon(main.worldArena, zon.getChild("blockTags")), + .slotInfos = slotInfos, + .properties = parameterMatrices.toOwnedSlice(), + .pixelSources = pixelSources, + .pixelSourcesOverlay = pixelSourcesOverlay, + }); + toolTypeIdToIndex.put(main.worldArena.allocator, idDupe, @enumFromInt(toolTypeList.items.len - 1)) catch unreachable; + + std.log.debug("Registered tool: '{s}'", .{id}); +} + +fn parseRecipeItem(zon: ZonElement) !ItemStack { + var id = zon.as([]const u8, ""); + id = std.mem.trim(u8, id, &std.ascii.whitespace); + var result: ItemStack = .{.amount = 1}; + if(std.mem.indexOfScalar(u8, id, ' ')) |index| blk: { + result.amount = std.fmt.parseInt(u16, id[0..index], 0) catch break :blk; + id = id[index + 1 ..]; + id = std.mem.trim(u8, id, &std.ascii.whitespace); + } + result.item = .{.baseItem = BaseItemIndex.fromId(id) orelse return error.ItemNotFound}; + return result; +} + +fn parseRecipe(zon: ZonElement) !Recipe { + const inputs = zon.getChild("inputs").toSlice(); + const output = try parseRecipeItem(zon.getChild("output")); + const recipe = Recipe{ + .sourceItems = main.worldArena.alloc(BaseItemIndex, inputs.len), + .sourceAmounts = main.worldArena.alloc(u16, inputs.len), + .resultItem = output.item.baseItem, + .resultAmount = output.amount, + }; + for(inputs, 0..) |inputZon, i| { + const input = try parseRecipeItem(inputZon); + recipe.sourceItems[i] = input.item.baseItem; + recipe.sourceAmounts[i] = input.amount; + } + return recipe; +} + +pub fn registerRecipes(zon: ZonElement) void { + for(zon.toSlice()) |recipeZon| { + recipes_zig.parseRecipe(main.globalAllocator, recipeZon, &recipeList) catch |err| { + const recipeString = recipeZon.toString(main.stackAllocator); + defer main.stackAllocator.free(recipeString); + std.log.err("Skipping recipe with error {s}:\n{s}", .{@errorName(err), recipeString}); + continue; + }; + } +} + +pub fn clearRecipeCachedInventories() void { + for(recipeList.items) |recipe| { + main.globalAllocator.free(recipe.sourceItems); + main.globalAllocator.free(recipe.sourceAmounts); + if(recipe.cachedInventory) |inv| { + inv.deinit(main.globalAllocator); + } + } +} + +pub fn reset() void { + toolTypeList = .{}; + toolTypeIdToIndex = .{}; + reverseIndices = .{}; + recipeList.clearAndFree(); + itemListSize = 0; +} + +pub fn deinit() void { + Inventory.Sync.ClientSide.deinit(); +} diff --git a/main.zig b/main.zig new file mode 100644 index 0000000000..b7851f96de --- /dev/null +++ b/main.zig @@ -0,0 +1,708 @@ +const std = @import("std"); + +pub const gui = @import("gui/gui.zig"); +pub const server = @import("server/server.zig"); + +pub const audio = @import("audio.zig"); +pub const assets = @import("assets.zig"); +pub const block_entity = @import("block_entity.zig"); +pub const blocks = @import("blocks.zig"); +pub const blueprint = @import("blueprint.zig"); +pub const callbacks = @import("callbacks/callbacks.zig"); +pub const chunk = @import("chunk.zig"); +pub const entity = @import("entity.zig"); +pub const files = @import("files.zig"); +pub const game = @import("game.zig"); +pub const graphics = @import("graphics.zig"); +pub const itemdrop = @import("itemdrop.zig"); +pub const items = @import("items.zig"); +pub const meta = @import("meta.zig"); +pub const migrations = @import("migrations.zig"); +pub const models = @import("models.zig"); +pub const network = @import("network.zig"); +pub const physics = @import("physics.zig"); +pub const random = @import("random.zig"); +pub const renderer = @import("renderer.zig"); +pub const rotation = @import("rotation.zig"); +pub const settings = @import("settings.zig"); +pub const particles = @import("particles.zig"); +const tag = @import("tag.zig"); +pub const Tag = tag.Tag; +pub const utils = @import("utils.zig"); +pub const vec = @import("vec.zig"); +pub const ZonElement = @import("zon.zig").ZonElement; + +pub const Window = @import("graphics/Window.zig"); + +pub const heap = @import("utils/heap.zig"); + +pub const List = @import("utils/list.zig").List; +pub const ListUnmanaged = @import("utils/list.zig").ListUnmanaged; +pub const MultiArray = @import("utils/list.zig").MultiArray; + +const file_monitor = utils.file_monitor; + +const Vec2f = vec.Vec2f; +const Vec3d = vec.Vec3d; + +pub threadlocal var stackAllocator: heap.NeverFailingAllocator = undefined; +pub threadlocal var seed: u64 = undefined; +threadlocal var stackAllocatorBase: heap.StackAllocator = undefined; +pub const globalAllocator: heap.NeverFailingAllocator = heap.allocators.handledGpa.allocator(); +pub const globalArena = heap.allocators.globalArenaAllocator.allocator(); +pub const worldArena = heap.allocators.worldArenaAllocator.allocator(); +pub var threadPool: *utils.ThreadPool = undefined; +var threadedIo: std.Io.Threaded = undefined; +pub var io: std.Io = threadedIo.io(); + +pub fn initThreadLocals() void { + seed = @bitCast(@as(i64, @truncate(timestamp().nanoseconds))); + stackAllocatorBase = heap.StackAllocator.init(globalAllocator, 1 << 23); + stackAllocator = stackAllocatorBase.allocator(); + heap.GarbageCollection.addThread(); +} + +pub fn deinitThreadLocals() void { + stackAllocatorBase.deinit(); + heap.GarbageCollection.removeThread(); +} + +pub fn timestamp() std.Io.Timestamp { + return (std.Io.Clock.Timestamp.now(io, if(@import("builtin").os.tag == .windows) .real else .awake) catch unreachable).raw; // TODO: On windows the awake time is broken +} + +fn cacheStringImpl(comptime len: usize, comptime str: [len]u8) []const u8 { + return str[0..len]; +} + +fn cacheString(comptime str: []const u8) []const u8 { + return cacheStringImpl(str.len, str[0..].*); +} +var logFile: ?std.fs.File = undefined; +var logFileTs: ?std.fs.File = undefined; +var supportsANSIColors: bool = undefined; +var openingErrorWindow: bool = false; +// overwrite the log function: +pub const std_options: std.Options = .{ // MARK: std_options + .log_level = .debug, + .logFn = struct { + pub fn logFn( + comptime level: std.log.Level, + comptime _: @Type(.enum_literal), + comptime format: []const u8, + args: anytype, + ) void { + const color = comptime switch(level) { + std.log.Level.err => "\x1b[31m", + std.log.Level.info => "", + std.log.Level.warn => "\x1b[33m", + std.log.Level.debug => "\x1b[37;44m", + }; + const colorReset = "\x1b[0m\n"; + const filePrefix = "[" ++ comptime level.asText() ++ "]" ++ ": "; + const fileSuffix = "\n"; + comptime var formatString: []const u8 = ""; + comptime var i: usize = 0; + comptime var mode: usize = 0; + comptime var sections: usize = 0; + comptime var sectionString: []const u8 = ""; + comptime var sectionResults: []const []const u8 = &.{}; + comptime var sectionId: []const usize = &.{}; + inline while(i < format.len) : (i += 1) { + if(mode == 0) { + if(format[i] == '{') { + if(format[i + 1] == '{') { + sectionString = sectionString ++ "{{"; + i += 1; + continue; + } else { + mode = 1; + formatString = formatString ++ "{s}{"; + sectionResults = sectionResults ++ &[_][]const u8{sectionString}; + sectionString = ""; + sectionId = sectionId ++ &[_]usize{sections}; + sections += 1; + continue; + } + } else { + sectionString = sectionString ++ format[i .. i + 1]; + } + } else { + formatString = formatString ++ format[i .. i + 1]; + if(format[i] == '}') { + sections += 1; + mode = 0; + } + } + } + formatString = formatString ++ "{s}"; + sectionResults = sectionResults ++ &[_][]const u8{sectionString}; + sectionId = sectionId ++ &[_]usize{sections}; + sections += 1; + formatString = comptime cacheString("{s}" ++ formatString ++ "{s}"); + + comptime var types: []const type = &.{}; + comptime var i_1: usize = 0; + comptime var i_2: usize = 0; + inline while(types.len != sections) { + if(i_2 < sectionResults.len) { + if(types.len == sectionId[i_2]) { + types = types ++ &[_]type{[]const u8}; + i_2 += 1; + continue; + } + } + const TI = @typeInfo(@TypeOf(args[i_1])); + if(@TypeOf(args[i_1]) == comptime_int) { + types = types ++ &[_]type{i64}; + } else if(@TypeOf(args[i_1]) == comptime_float) { + types = types ++ &[_]type{f64}; + } else if(TI == .pointer and TI.pointer.size == .slice and TI.pointer.child == u8) { + types = types ++ &[_]type{[]const u8}; + } else if(TI == .int and TI.int.bits <= 64) { + if(TI.int.signedness == .signed) { + types = types ++ &[_]type{i64}; + } else { + types = types ++ &[_]type{u64}; + } + } else { + types = types ++ &[_]type{@TypeOf(args[i_1])}; + } + i_1 += 1; + } + types = &[_]type{[]const u8} ++ types ++ &[_]type{[]const u8}; + + const ArgsType = std.meta.Tuple(types); + comptime var comptimeTuple: ArgsType = undefined; + comptime var len: usize = 0; + i_1 = 0; + i_2 = 0; + inline while(len != sections) : (len += 1) { + if(i_2 < sectionResults.len) { + if(len == sectionId[i_2]) { + comptimeTuple[len + 1] = sectionResults[i_2]; + i_2 += 1; + continue; + } + } + i_1 += 1; + } + comptimeTuple[0] = filePrefix; + comptimeTuple[comptimeTuple.len - 1] = fileSuffix; + var resultArgs: ArgsType = comptimeTuple; + len = 0; + i_1 = 0; + i_2 = 0; + inline while(len != sections) : (len += 1) { + if(i_2 < sectionResults.len) { + if(len == sectionId[i_2]) { + i_2 += 1; + continue; + } + } + resultArgs[len + 1] = args[i_1]; + i_1 += 1; + } + + logToFile(formatString, resultArgs); + + if(supportsANSIColors) { + resultArgs[0] = color; + resultArgs[resultArgs.len - 1] = colorReset; + } + logToStdErr(formatString, resultArgs); + if(level == .err and !openingErrorWindow and !settings.launchConfig.headlessServer) { + openingErrorWindow = true; + gui.openWindow("error_prompt"); + openingErrorWindow = false; + } + } + }.logFn, +}; + +fn initLogging() void { + logFile = null; + files.cwd().makePath("logs") catch |err| { + std.log.err("Couldn't create logs folder: {s}", .{@errorName(err)}); + return; + }; + logFile = std.fs.cwd().createFile("logs/latest.log", .{}) catch |err| { + std.log.err("Couldn't create logs/latest.log: {s}", .{@errorName(err)}); + return; + }; + + const _timestamp = (std.Io.Clock.Timestamp.now(io, .real) catch unreachable).raw; + + const _path_str = std.fmt.allocPrint(stackAllocator.allocator, "logs/ts_{}.log", .{_timestamp.nanoseconds}) catch unreachable; + defer stackAllocator.free(_path_str); + + logFileTs = std.fs.cwd().createFile(_path_str, .{}) catch |err| { + std.log.err("Couldn't create {s}: {s}", .{_path_str, @errorName(err)}); + return; + }; + + supportsANSIColors = std.fs.File.stdout().supportsAnsiEscapeCodes(); +} + +fn deinitLogging() void { + if(logFile) |_logFile| { + _logFile.close(); + logFile = null; + } + + if(logFileTs) |_logFileTs| { + _logFileTs.close(); + logFileTs = null; + } +} + +fn logToFile(comptime format: []const u8, args: anytype) void { + var buf: [65536]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&buf); + const allocator = fba.allocator(); + + const string = std.fmt.allocPrint(allocator, format, args) catch format; + (logFile orelse return).writeAll(string) catch {}; + (logFileTs orelse return).writeAll(string) catch {}; +} + +fn logToStdErr(comptime format: []const u8, args: anytype) void { + var buf: [65536]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&buf); + const allocator = fba.allocator(); + + const string = std.fmt.allocPrint(allocator, format, args) catch format; + const writer = std.debug.lockStderrWriter(&.{}); + defer std.debug.unlockStderrWriter(); + nosuspend writer[0].writeAll(string) catch {}; +} + +// MARK: Callbacks +fn escape(mods: Window.Key.Modifiers) void { + if(gui.selectedTextInput != null) gui.setSelectedTextInput(null); + inventory(mods); +} +fn inventory(_: Window.Key.Modifiers) void { + if(game.world == null) return; + gui.toggleGameMenu(); +} +fn ungrabMouse(_: Window.Key.Modifiers) void { + if(Window.grabbed) { + gui.toggleGameMenu(); + } +} +fn openCreativeInventory(mods: Window.Key.Modifiers) void { + if(game.world == null) return; + if(!game.Player.isCreative()) return; + ungrabMouse(mods); + gui.openWindow("creative_inventory"); +} +fn openChat(mods: Window.Key.Modifiers) void { + if(game.world == null) return; + ungrabMouse(mods); + gui.openWindow("chat"); + gui.windowlist.chat.input.select(); +} +fn openCommand(mods: Window.Key.Modifiers) void { + if(game.world == null) return; + openChat(mods); + gui.windowlist.chat.input.clear(); + gui.windowlist.chat.input.inputCharacter('/'); +} +fn takeBackgroundImageFn(_: Window.Key.Modifiers) void { + if(game.world == null) return; + + const oldHideGui = gui.hideGui; + gui.hideGui = true; + const oldShowItem = itemdrop.ItemDisplayManager.showItem; + itemdrop.ItemDisplayManager.showItem = false; + + renderer.MenuBackGround.takeBackgroundImage(); + + gui.hideGui = oldHideGui; + itemdrop.ItemDisplayManager.showItem = oldShowItem; +} +fn toggleHideGui(_: Window.Key.Modifiers) void { + gui.hideGui = !gui.hideGui; +} +fn toggleHideDisplayItem(_: Window.Key.Modifiers) void { + itemdrop.ItemDisplayManager.showItem = !itemdrop.ItemDisplayManager.showItem; +} +fn toggleDebugOverlay(_: Window.Key.Modifiers) void { + gui.toggleWindow("debug"); +} +fn togglePerformanceOverlay(_: Window.Key.Modifiers) void { + gui.toggleWindow("performance_graph"); +} +fn toggleGPUPerformanceOverlay(_: Window.Key.Modifiers) void { + gui.toggleWindow("gpu_performance_measuring"); +} +fn toggleNetworkDebugOverlay(_: Window.Key.Modifiers) void { + gui.toggleWindow("debug_network"); +} +fn toggleAdvancedNetworkDebugOverlay(_: Window.Key.Modifiers) void { + gui.toggleWindow("debug_network_advanced"); +} +fn cycleHotbarSlot(i: comptime_int) *const fn(Window.Key.Modifiers) void { + return &struct { + fn set(_: Window.Key.Modifiers) void { + game.Player.selectedSlot = @intCast(@mod(@as(i33, game.Player.selectedSlot) + i, 12)); + } + }.set; +} +fn setHotbarSlot(i: comptime_int) *const fn(Window.Key.Modifiers) void { + return &struct { + fn set(_: Window.Key.Modifiers) void { + game.Player.selectedSlot = i - 1; + } + }.set; +} + +pub const KeyBoard = struct { // MARK: KeyBoard + const c = Window.c; + pub var keys = [_]Window.Key{ + // Gameplay: + .{.name = "forward", .key = c.GLFW_KEY_W, .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_Y, .positive = false}}, + .{.name = "left", .key = c.GLFW_KEY_A, .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_X, .positive = false}}, + .{.name = "backward", .key = c.GLFW_KEY_S, .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_Y, .positive = true}}, + .{.name = "right", .key = c.GLFW_KEY_D, .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_X, .positive = true}}, + .{.name = "sprint", .key = c.GLFW_KEY_LEFT_CONTROL, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_LEFT_THUMB, .isToggling = .no}, + .{.name = "jump", .key = c.GLFW_KEY_SPACE, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_A}, + .{.name = "crouch", .key = c.GLFW_KEY_LEFT_SHIFT, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_RIGHT_THUMB}, + .{.name = "fly", .key = c.GLFW_KEY_F, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_DPAD_DOWN, .pressAction = &game.flyToggle}, + .{.name = "ghost", .key = c.GLFW_KEY_G, .pressAction = &game.ghostToggle}, + .{.name = "hyperSpeed", .key = c.GLFW_KEY_H, .pressAction = &game.hyperSpeedToggle}, + .{.name = "fall", .key = c.GLFW_KEY_LEFT_SHIFT, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_RIGHT_THUMB}, + .{.name = "placeBlock", .mouseButton = c.GLFW_MOUSE_BUTTON_RIGHT, .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_TRIGGER}, .pressAction = &game.pressPlace, .releaseAction = &game.releasePlace, .notifyRequirement = .inGame}, + .{.name = "breakBlock", .mouseButton = c.GLFW_MOUSE_BUTTON_LEFT, .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER}, .pressAction = &game.pressBreak, .releaseAction = &game.releaseBreak, .notifyRequirement = .inGame}, + .{.name = "acquireSelectedBlock", .mouseButton = c.GLFW_MOUSE_BUTTON_MIDDLE, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_DPAD_LEFT, .pressAction = &game.pressAcquireSelectedBlock, .notifyRequirement = .inGame}, + .{.name = "drop", .key = c.GLFW_KEY_Q, .repeatAction = &game.Player.dropFromHand, .notifyRequirement = .inGame}, + + .{.name = "takeBackgroundImage", .key = c.GLFW_KEY_PRINT_SCREEN, .pressAction = &takeBackgroundImageFn}, + .{.name = "fullscreen", .key = c.GLFW_KEY_F11, .pressAction = &Window.toggleFullscreen}, + + // Gui: + .{.name = "escape", .key = c.GLFW_KEY_ESCAPE, .pressAction = &escape, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_B}, + .{.name = "openInventory", .key = c.GLFW_KEY_E, .pressAction = &escape, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_X}, + .{.name = "openCreativeInventory(aka cheat inventory)", .key = c.GLFW_KEY_C, .pressAction = &openCreativeInventory, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_Y}, + .{.name = "openChat", .key = c.GLFW_KEY_T, .releaseAction = &openChat}, + .{.name = "openCommand", .key = c.GLFW_KEY_SLASH, .releaseAction = &openCommand}, + .{.name = "mainGuiButton", .mouseButton = c.GLFW_MOUSE_BUTTON_LEFT, .pressAction = &gui.mainButtonPressed, .releaseAction = &gui.mainButtonReleased, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_A, .notifyRequirement = .inMenu}, + .{.name = "secondaryGuiButton", .mouseButton = c.GLFW_MOUSE_BUTTON_RIGHT, .pressAction = &gui.secondaryButtonPressed, .releaseAction = &gui.secondaryButtonReleased, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_Y, .notifyRequirement = .inMenu}, + // gamepad gui. + .{.name = "scrollUp", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_RIGHT_Y, .positive = false}}, + .{.name = "scrollDown", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_RIGHT_Y, .positive = true}}, + .{.name = "uiUp", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_Y, .positive = false}}, + .{.name = "uiLeft", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_X, .positive = false}}, + .{.name = "uiDown", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_Y, .positive = true}}, + .{.name = "uiRight", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_X, .positive = true}}, + // text: + .{.name = "textCursorLeft", .key = c.GLFW_KEY_LEFT, .repeatAction = &gui.textCallbacks.left}, + .{.name = "textCursorRight", .key = c.GLFW_KEY_RIGHT, .repeatAction = &gui.textCallbacks.right}, + .{.name = "textCursorDown", .key = c.GLFW_KEY_DOWN, .repeatAction = &gui.textCallbacks.down}, + .{.name = "textCursorUp", .key = c.GLFW_KEY_UP, .repeatAction = &gui.textCallbacks.up}, + .{.name = "textGotoStart", .key = c.GLFW_KEY_HOME, .repeatAction = &gui.textCallbacks.gotoStart}, + .{.name = "textGotoEnd", .key = c.GLFW_KEY_END, .repeatAction = &gui.textCallbacks.gotoEnd}, + .{.name = "textDeleteLeft", .key = c.GLFW_KEY_BACKSPACE, .repeatAction = &gui.textCallbacks.deleteLeft}, + .{.name = "textDeleteRight", .key = c.GLFW_KEY_DELETE, .repeatAction = &gui.textCallbacks.deleteRight}, + .{.name = "textSelectAll", .key = c.GLFW_KEY_A, .repeatAction = &gui.textCallbacks.selectAll, .requiredModifiers = .{.control = true}}, + .{.name = "textCopy", .key = c.GLFW_KEY_C, .repeatAction = &gui.textCallbacks.copy, .requiredModifiers = .{.control = true}}, + .{.name = "textPaste", .key = c.GLFW_KEY_V, .repeatAction = &gui.textCallbacks.paste, .requiredModifiers = .{.control = true}}, + .{.name = "textCut", .key = c.GLFW_KEY_X, .repeatAction = &gui.textCallbacks.cut, .requiredModifiers = .{.control = true}}, + .{.name = "textNewline", .key = c.GLFW_KEY_ENTER, .repeatAction = &gui.textCallbacks.newline}, + + // Hotbar shortcuts: + .{.name = "Hotbar 1", .key = c.GLFW_KEY_1, .pressAction = setHotbarSlot(1)}, + .{.name = "Hotbar 2", .key = c.GLFW_KEY_2, .pressAction = setHotbarSlot(2)}, + .{.name = "Hotbar 3", .key = c.GLFW_KEY_3, .pressAction = setHotbarSlot(3)}, + .{.name = "Hotbar 4", .key = c.GLFW_KEY_4, .pressAction = setHotbarSlot(4)}, + .{.name = "Hotbar 5", .key = c.GLFW_KEY_5, .pressAction = setHotbarSlot(5)}, + .{.name = "Hotbar 6", .key = c.GLFW_KEY_6, .pressAction = setHotbarSlot(6)}, + .{.name = "Hotbar 7", .key = c.GLFW_KEY_7, .pressAction = setHotbarSlot(7)}, + .{.name = "Hotbar 8", .key = c.GLFW_KEY_8, .pressAction = setHotbarSlot(8)}, + .{.name = "Hotbar 9", .key = c.GLFW_KEY_9, .pressAction = setHotbarSlot(9)}, + .{.name = "Hotbar 10", .key = c.GLFW_KEY_0, .pressAction = setHotbarSlot(10)}, + .{.name = "Hotbar 11", .key = c.GLFW_KEY_MINUS, .pressAction = setHotbarSlot(11)}, + .{.name = "Hotbar 12", .key = c.GLFW_KEY_EQUAL, .pressAction = setHotbarSlot(12)}, + .{.name = "Hotbar left", .gamepadButton = c.GLFW_GAMEPAD_BUTTON_LEFT_BUMPER, .pressAction = cycleHotbarSlot(-1)}, + .{.name = "Hotbar right", .gamepadButton = c.GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER, .pressAction = cycleHotbarSlot(1)}, + .{.name = "cameraLeft", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_RIGHT_X, .positive = false}}, + .{.name = "cameraRight", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_RIGHT_X, .positive = true}}, + .{.name = "cameraUp", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_RIGHT_Y, .positive = false}}, + .{.name = "cameraDown", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_RIGHT_Y, .positive = true}}, + // debug: + .{.name = "hideMenu", .key = c.GLFW_KEY_F1, .pressAction = &toggleHideGui}, + .{.name = "hideDisplayItem", .key = c.GLFW_KEY_F2, .pressAction = &toggleHideDisplayItem}, + .{.name = "debugOverlay", .key = c.GLFW_KEY_F3, .pressAction = &toggleDebugOverlay}, + .{.name = "performanceOverlay", .key = c.GLFW_KEY_F4, .pressAction = &togglePerformanceOverlay}, + .{.name = "gpuPerformanceOverlay", .key = c.GLFW_KEY_F5, .pressAction = &toggleGPUPerformanceOverlay}, + .{.name = "networkDebugOverlay", .key = c.GLFW_KEY_F6, .pressAction = &toggleNetworkDebugOverlay}, + .{.name = "advancedNetworkDebugOverlay", .key = c.GLFW_KEY_F7, .pressAction = &toggleAdvancedNetworkDebugOverlay}, + }; + + fn findKey(name: []const u8) ?*Window.Key { // TODO: Maybe I should use a hashmap here? + for(&keys) |*_key| { + if(std.mem.eql(u8, name, _key.name)) { + return _key; + } + } + return null; + } + pub fn key(name: []const u8) *const Window.Key { + return findKey(name) orelse { + std.log.err("Couldn't find keyboard key with name {s}", .{name}); + return &.{.name = ""}; + }; + } + pub fn setIsToggling(name: []const u8, value: bool) void { + if(findKey(name)) |theKey| { + if(theKey.isToggling == .never) { + std.log.err("Tried setting toggling on non-toggling key with name {s}", .{name}); + return; + } + theKey.isToggling = if(value) .yes else .no; + if(!value) { + theKey.pressed = false; + } + } else { + std.log.err("Couldn't find keyboard key to toggle with name {s}", .{name}); + } + } +}; + +/// Records gpu time per frame. +pub var lastFrameTime = std.atomic.Value(f64).init(0); +/// Measures time between different frames' beginnings. +pub var lastDeltaTime = std.atomic.Value(f64).init(0); + +var shouldExitToMenu = std.atomic.Value(bool).init(false); +pub fn exitToMenu(_: usize) void { + shouldExitToMenu.store(true, .monotonic); +} + +fn isHiddenOrParentHiddenPosix(path: []const u8) bool { + var iter = std.fs.path.componentIterator(path) catch |err| { + std.log.err("Cannot iterate on path {s}: {s}!", .{path, @errorName(err)}); + return false; + }; + while(iter.next()) |component| { + if(std.mem.eql(u8, component.name, ".") or std.mem.eql(u8, component.name, "..")) { + continue; + } + if(component.name.len > 0 and component.name[0] == '.') { + return true; + } + } + return false; +} + +pub fn main() void { // MARK: main() + defer heap.allocators.deinit(); + defer heap.GarbageCollection.assertAllThreadsStopped(); + initThreadLocals(); + defer deinitThreadLocals(); + threadedIo = .init(globalAllocator.allocator); + defer threadedIo.deinit(); + + initLogging(); + defer deinitLogging(); + + std.log.info("Starting game with version {s}", .{settings.version.version}); + + settings.launchConfig.init(); + + const headless = settings.launchConfig.headlessServer; + + if(!headless) gui.initWindowList(); + defer if(!headless) gui.deinitWindowList(); + + files.init(); + defer files.deinit(); + + settings.init(); + defer settings.deinit(); + + threadPool = utils.ThreadPool.init(globalAllocator, settings.cpuThreads orelse @max(1, (std.Thread.getCpuCount() catch 4) -| 1)); + defer threadPool.deinit(); + + file_monitor.init(); + defer file_monitor.deinit(); + + if(!headless) Window.init(); + defer if(!headless) Window.deinit(); + + if(!headless) graphics.init(); + defer if(!headless) graphics.deinit(); + + if(!headless) audio.init() catch std.log.err("Failed to initialize audio. Continuing the game without sounds.", .{}); + defer if(!headless) audio.deinit(); + + utils.initDynamicIntArrayStorage(); + defer utils.deinitDynamicIntArrayStorage(); + + chunk.init(); + defer chunk.deinit(); + + rotation.init(); + defer rotation.deinit(); + + callbacks.init(); + + block_entity.init(); + defer block_entity.deinit(); + + models.init(); + defer models.deinit(); + + items.globalInit(); + defer items.deinit(); + + if(!headless) itemdrop.ItemDropRenderer.init(); + defer if(!headless) itemdrop.ItemDropRenderer.deinit(); + + assets.init(); + + if(!headless) blocks.meshes.init(); + defer if(!headless) blocks.meshes.deinit(); + + if(!headless) renderer.init(); + defer if(!headless) renderer.deinit(); + + network.init(); + + if(!headless) entity.ClientEntityManager.init(); + defer if(!headless) entity.ClientEntityManager.deinit(); + + if(!headless) gui.init(); + defer if(!headless) gui.deinit(); + + if(!headless) particles.ParticleManager.init(); + defer if(!headless) particles.ParticleManager.deinit(); + + server.terrain.globalInit(); + defer server.terrain.globalDeinit(); + + if(headless) { + server.startFromExistingThread(settings.launchConfig.autoEnterWorld, null); + } else { + clientMain(); + } +} + +pub fn clientMain() void { // MARK: clientMain() + if(settings.playerName.len == 0) { + gui.openWindow("change_name"); + } else { + gui.openWindow("main"); + } + + const c = Window.c; + Window.GLFWCallbacks.framebufferSize(undefined, Window.width, Window.height); + var lastBeginRendering = timestamp(); + + if(settings.launchConfig.autoEnterWorld.len != 0) { + // Speed up the dev process by entering the world directly. + gui.windowlist.save_selection.openWorld(settings.launchConfig.autoEnterWorld); + } + + audio.setMusic("cubyz:TotalDemented/Cubyz"); + + while(c.glfwWindowShouldClose(Window.window) == 0) { + heap.GarbageCollection.syncPoint(); + const isHidden = c.glfwGetWindowAttrib(Window.window, c.GLFW_ICONIFIED) == c.GLFW_TRUE; + if(!isHidden) { + c.glfwSwapBuffers(Window.window); + // Clear may also wait on vsync, so it's done before handling events: + gui.windowlist.gpu_performance_measuring.startQuery(.screenbuffer_clear); + c.glDepthFunc(c.GL_LESS); + c.glDepthMask(c.GL_TRUE); + c.glDisable(c.GL_SCISSOR_TEST); + c.glClearColor(0.5, 1, 1, 1); + c.glClear(c.GL_DEPTH_BUFFER_BIT | c.GL_STENCIL_BUFFER_BIT | c.GL_COLOR_BUFFER_BIT); + gui.windowlist.gpu_performance_measuring.stopQuery(); + } else { + io.sleep(.fromMilliseconds(16), .awake) catch {}; + } + + const endRendering = timestamp(); + const frameTime = @as(f64, @floatFromInt(endRendering.nanoseconds -% lastBeginRendering.nanoseconds))/1.0e9; + if(settings.developerGPUInfiniteLoopDetection and frameTime > 5) { // On linux a process that runs 10 seconds or longer on the GPU will get stopped. This allows detecting an infinite loop on the GPU. + std.log.err("Frame got too long with {} seconds. Infinite loop on GPU?", .{frameTime}); + std.posix.exit(1); + } + lastFrameTime.store(frameTime, .monotonic); + + if(settings.fpsCap) |fpsCap| { + const minFrameTime = @divFloor(1000*1000*1000, fpsCap); + const sleep = @min(minFrameTime, @max(0, minFrameTime - (endRendering.nanoseconds -% lastBeginRendering.nanoseconds))); + io.sleep(.fromNanoseconds(sleep), .awake) catch {}; + } + const begin = timestamp(); + const deltaTime = @as(f64, @floatFromInt(begin.nanoseconds -% lastBeginRendering.nanoseconds))/1.0e9; + lastDeltaTime.store(deltaTime, .monotonic); + lastBeginRendering = begin; + + Window.handleEvents(deltaTime); + + file_monitor.handleEvents(); + + if(game.world != null) { // Update the game + game.update(deltaTime); + } + + if(!isHidden) { + if(game.world != null) { + renderer.updateFov(settings.fov); + renderer.render(game.Player.getEyePosBlocking(), deltaTime); + } else { + renderer.updateFov(70.0); + renderer.MenuBackGround.render(deltaTime); + } + // Render the GUI + gui.windowlist.gpu_performance_measuring.startQuery(.gui); + gui.updateAndRenderGui(); + gui.windowlist.gpu_performance_measuring.stopQuery(); + } + + if(shouldExitToMenu.load(.monotonic)) { + shouldExitToMenu.store(false, .monotonic); + Window.setMouseGrabbed(false); + if(game.world) |world| { + world.deinit(); + game.world = null; + } + gui.openWindow("main"); + audio.setMusic("cubyz:TotalDemented/Cubyz"); + } + } + + if(game.world) |world| { + world.deinit(); + game.world = null; + } +} + +/// std.testing.refAllDeclsRecursive, but ignores C imports (by name) +pub fn refAllDeclsRecursiveExceptCImports(comptime T: type) void { + if(!@import("builtin").is_test) return; + inline for(comptime std.meta.declarations(T)) |decl| blk: { + if(comptime std.mem.eql(u8, decl.name, "c")) continue; + if(comptime std.mem.eql(u8, decl.name, "hbft")) break :blk; + if(comptime std.mem.eql(u8, decl.name, "stb_image")) break :blk; + // TODO: Remove this after Zig removes Managed hashmap PixelGuys/Cubyz#308 + if(comptime std.mem.eql(u8, decl.name, "Managed")) continue; + if(@TypeOf(@field(T, decl.name)) == type) { + switch(@typeInfo(@field(T, decl.name))) { + .@"struct", .@"enum", .@"union", .@"opaque" => refAllDeclsRecursiveExceptCImports(@field(T, decl.name)), + else => {}, + } + } + _ = &@field(T, decl.name); + } +} + +test "abc" { + @setEvalBranchQuota(1000000); + refAllDeclsRecursiveExceptCImports(@This()); + _ = @import("zon.zig"); +} diff --git a/meta.zig b/meta.zig new file mode 100644 index 0000000000..1535788981 --- /dev/null +++ b/meta.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +// MARK: functionPtrCast() +fn CastFunctionSelfToAnyopaqueType(Fn: type) type { + var typeInfo = @typeInfo(Fn); + var params = typeInfo.@"fn".params[0..typeInfo.@"fn".params.len].*; + if(@sizeOf(params[0].type.?) != @sizeOf(*anyopaque) or @alignOf(params[0].type.?) != @alignOf(*anyopaque)) { + @compileError(std.fmt.comptimePrint("Cannot convert {} to *anyopaque", .{params[0].type.?})); + } + params[0].type = *anyopaque; + typeInfo.@"fn".params = params[0..]; + return @Type(typeInfo); +} +/// Turns the first parameter into a anyopaque* +pub fn castFunctionSelfToAnyopaque(function: anytype) *const CastFunctionSelfToAnyopaqueType(@TypeOf(function)) { + return @ptrCast(&function); +} + +fn CastFunctionReturnToAnyopaqueType(Fn: type) type { + var typeInfo = @typeInfo(Fn); + if(@sizeOf(typeInfo.@"fn".return_type.?) != @sizeOf(*anyopaque) or @alignOf(typeInfo.@"fn".return_type.?) != @alignOf(*anyopaque) or @typeInfo(typeInfo.@"fn".return_type.?) == .optional) { + @compileError(std.fmt.comptimePrint("Cannot convert {} to *anyopaque", .{typeInfo.@"fn".return_type.?})); + } + typeInfo.@"fn".return_type = *anyopaque; + return @Type(typeInfo); +} + +fn CastFunctionReturnToOptionalAnyopaqueType(Fn: type) type { + var typeInfo = @typeInfo(Fn); + if(@sizeOf(typeInfo.@"fn".return_type.?) != @sizeOf(?*anyopaque) or @alignOf(typeInfo.@"fn".return_type.?) != @alignOf(?*anyopaque) or @typeInfo(typeInfo.@"fn".return_type.?) != .optional) { + @compileError(std.fmt.comptimePrint("Cannot convert {} to ?*anyopaque", .{typeInfo.@"fn".return_type.?})); + } + typeInfo.@"fn".return_type = ?*anyopaque; + return @Type(typeInfo); +} +/// Turns the return parameter into a anyopaque* +pub fn castFunctionReturnToAnyopaque(function: anytype) *const CastFunctionReturnToAnyopaqueType(@TypeOf(function)) { + return @ptrCast(&function); +} +pub fn castFunctionReturnToOptionalAnyopaque(function: anytype) *const CastFunctionReturnToOptionalAnyopaqueType(@TypeOf(function)) { + return @ptrCast(&function); +} diff --git a/migrations.zig b/migrations.zig new file mode 100644 index 0000000000..97cd9f1b2a --- /dev/null +++ b/migrations.zig @@ -0,0 +1,117 @@ +const std = @import("std"); + +const main = @import("main"); +const ZonElement = @import("zon.zig").ZonElement; +const Palette = @import("assets.zig").Palette; +const Assets = main.assets.Assets; + +var blockMigrations: std.StringHashMapUnmanaged([]const u8) = .{}; +var itemMigrations: std.StringHashMapUnmanaged([]const u8) = .{}; +var biomeMigrations: std.StringHashMapUnmanaged([]const u8) = .{}; + +const MigrationType = enum { + block, + item, + biome, +}; + +pub fn registerAll(comptime typ: MigrationType, migrations: *Assets.AddonNameToZonMap) void { + std.log.info("Registering {s} migrations for {} addons", .{@tagName(typ), migrations.count()}); + const collection = switch(typ) { + .block => &blockMigrations, + .item => &itemMigrations, + .biome => &biomeMigrations, + }; + var migrationIterator = migrations.iterator(); + while(migrationIterator.next()) |migration| { + register(typ, collection, migration.key_ptr.*, migration.value_ptr.*); + } +} + +fn register( + comptime typ: MigrationType, + collection: *std.StringHashMapUnmanaged([]const u8), + addonName: []const u8, + migrationZon: ZonElement, +) void { + if(migrationZon != .array) { + if(migrationZon == .object and migrationZon.object.count() == 0) { + std.log.warn("Skipping empty {s} migration data structure from addon {s}", .{@tagName(typ), addonName}); + return; + } + std.log.err("Skipping incorrect {s} migration data structure from addon {s}", .{@tagName(typ), addonName}); + return; + } + if(migrationZon.array.items.len == 0) { + std.log.warn("Skipping empty {s} migration data structure from addon {s}", .{@tagName(typ), addonName}); + return; + } + + for(migrationZon.array.items) |migration| { + const oldZonOpt = migration.get(?[]const u8, "old", null); + const newZonOpt = migration.get(?[]const u8, "new", null); + + if(oldZonOpt == null or newZonOpt == null) { + std.log.err("Skipping incomplete migration in {s} migrations: '{s}:{s}' -> '{s}:{s}'", .{@tagName(typ), addonName, oldZonOpt orelse "", addonName, newZonOpt orelse ""}); + continue; + } + + const oldZon = oldZonOpt orelse unreachable; + const newZon = newZonOpt orelse unreachable; + + if(std.mem.eql(u8, oldZon, newZon)) { + std.log.err("Skipping identity migration in {s} migrations: '{s}:{s}' -> '{s}:{s}'", .{@tagName(typ), addonName, oldZon, addonName, newZon}); + continue; + } + + const oldAssetId = std.fmt.allocPrint(main.worldArena.allocator, "{s}:{s}", .{addonName, oldZon}) catch unreachable; + const result = collection.getOrPut(main.worldArena.allocator, oldAssetId) catch unreachable; + + if(result.found_existing) { + std.log.err("Skipping name collision in {s} migration: '{s}' -> '{s}:{s}'", .{@tagName(typ), oldAssetId, addonName, newZon}); + const existingMigration = collection.get(oldAssetId) orelse unreachable; + std.log.err("Already mapped to '{s}'", .{existingMigration}); + + main.worldArena.free(oldAssetId); + } else { + const newAssetId = std.fmt.allocPrint(main.worldArena.allocator, "{s}:{s}", .{addonName, newZon}) catch unreachable; + + result.key_ptr.* = oldAssetId; + result.value_ptr.* = newAssetId; + std.log.info("Registered {s} migration: '{s}' -> '{s}'", .{@tagName(typ), oldAssetId, newAssetId}); + } + } +} + +pub fn applySingle(comptime typ: MigrationType, assetName: []const u8) []const u8 { + const migrations = switch(typ) { + .block => blockMigrations, + .item => itemMigrations, + .biome => biomeMigrations, + }; + + const newAssetName = migrations.get(assetName) orelse return assetName; + std.log.info("Migrating {s} {s} -> {s}", .{@tagName(typ), assetName, newAssetName}); + return newAssetName; +} + +pub fn apply(comptime typ: MigrationType, palette: *Palette) void { + const migrations = switch(typ) { + .block => blockMigrations, + .item => itemMigrations, + .biome => biomeMigrations, + }; + std.log.info("Applying {} migrations to {s} palette", .{migrations.count(), @tagName(typ)}); + + for(palette.palette.items, 0..) |assetName, i| { + const newAssetName = migrations.get(assetName) orelse continue; + std.log.info("Migrating {s} {s} -> {s}", .{@tagName(typ), assetName, newAssetName}); + palette.replaceEntry(i, newAssetName); + } +} + +pub fn reset() void { + biomeMigrations = .{}; + blockMigrations = .{}; + itemMigrations = .{}; +} diff --git a/models.zig b/models.zig new file mode 100644 index 0000000000..31b6b96f38 --- /dev/null +++ b/models.zig @@ -0,0 +1,824 @@ +const std = @import("std"); + +const chunk = @import("chunk.zig"); +const Neighbor = chunk.Neighbor; +const graphics = @import("graphics.zig"); +const main = @import("main"); +const vec = @import("vec.zig"); +const Vec3i = vec.Vec3i; +const Vec3f = vec.Vec3f; +const Vec3d = vec.Vec3d; +const Vec2f = vec.Vec2f; +const Mat4f = vec.Mat4f; + +const FaceData = main.renderer.chunk_meshing.FaceData; +const NeverFailingAllocator = main.heap.NeverFailingAllocator; +const Box = main.game.collision.Box; + +var quadSSBO: graphics.SSBO = undefined; + +pub const QuadInfo = extern struct { + normal: [3]f32 align(16), + corners: [4][3]f32, + cornerUV: [4][2]f32 align(8), + textureSlot: u32, + opaqueInLod: u32 = 0, + + pub fn normalVec(self: QuadInfo) Vec3f { + return self.normal; + } + pub fn cornerVec(self: QuadInfo, i: usize) Vec3f { + return self.corners[i]; + } + pub fn cornerUvVec(self: QuadInfo, i: usize) Vec2f { + return self.cornerUV[i]; + } +}; + +const ExtraQuadInfo = struct { + faceNeighbor: ?Neighbor, + isFullQuad: bool, + hasOnlyCornerVertices: bool, + alignedNormalDirection: ?Neighbor, +}; + +const gridSize = 4096; +const collisionGridSize = 16; +const CollisionGridInteger = std.meta.Int(.unsigned, collisionGridSize); + +fn snapToGrid(x: anytype) @TypeOf(x) { + const T = @TypeOf(x); + const Vec = @Vector(x.len, std.meta.Child(T)); + const int = @as(@Vector(x.len, i32), @intFromFloat(std.math.round(@as(Vec, x)*@as(Vec, @splat(gridSize))))); + return @as(Vec, @floatFromInt(int))/@as(Vec, @splat(gridSize)); +} + +const Triangle = struct { + vertex: [3]usize, + normal: usize, + uvs: [3]usize, +}; + +const Quad = struct { + vertex: [4]usize, + normal: usize, + uvs: [4]usize, +}; + +pub const ModelIndex = enum(u32) { + _, + + pub fn model(self: ModelIndex) *const Model { + return &models.items()[@intFromEnum(self)]; + } + pub fn add(self: ModelIndex, offset: u32) ModelIndex { + return @enumFromInt(@intFromEnum(self) + offset); + } +}; + +pub const QuadIndex = enum(u16) { + _, + + pub fn quadInfo(self: QuadIndex) *const QuadInfo { + return &quads.items[@intFromEnum(self)]; + } + + pub fn extraQuadInfo(self: QuadIndex) *const ExtraQuadInfo { + return &extraQuadInfos.items[@intFromEnum(self)]; + } +}; + +pub const Model = struct { + min: Vec3f, + max: Vec3f, + internalQuads: []QuadIndex, + neighborFacingQuads: [6][]QuadIndex, + isNeighborOccluded: [6]bool, + allNeighborsOccluded: bool, + noNeighborsOccluded: bool, + hasNeighborFacingQuads: bool, + collision: []Box, + + fn getFaceNeighbor(quad: *const QuadInfo) ?chunk.Neighbor { + var allZero: @Vector(3, bool) = .{true, true, true}; + var allOne: @Vector(3, bool) = .{true, true, true}; + for(quad.corners) |corner| { + allZero = allZero & (corner == @as(Vec3f, @splat(0))); + allOne = allOne & (corner == @as(Vec3f, @splat(1))); + } + if(allZero[0]) return .dirNegX; + if(allZero[1]) return .dirNegY; + if(allZero[2]) return .dirDown; + if(allOne[0]) return .dirPosX; + if(allOne[1]) return .dirPosY; + if(allOne[2]) return .dirUp; + return null; + } + + fn fullyOccludesNeighbor(quad: *const QuadInfo) bool { + var zeroes: @Vector(3, u32) = .{0, 0, 0}; + var ones: @Vector(3, u32) = .{0, 0, 0}; + for(quad.corners) |corner| { + zeroes += @select(u32, corner == @as(Vec3f, @splat(0)), .{1, 1, 1}, .{0, 0, 0}); + ones += @select(u32, corner == @as(Vec3f, @splat(1)), .{1, 1, 1}, .{0, 0, 0}); + } + // For full coverage there will 2 ones and 2 zeroes for two components, while the other one is constant. + const hasTwoZeroes = zeroes == @Vector(3, u32){2, 2, 2}; + const hasTwoOnes = ones == @Vector(3, u32){2, 2, 2}; + return @popCount(@as(u3, @bitCast(hasTwoOnes))) == 2 and @popCount(@as(u3, @bitCast(hasTwoZeroes))) == 2; + } + + pub fn init(quadInfos: []const QuadInfo) ModelIndex { + const adjustedQuads = main.stackAllocator.alloc(QuadInfo, quadInfos.len); + defer main.stackAllocator.free(adjustedQuads); + for(adjustedQuads, quadInfos) |*dest, *src| { + dest.* = src.*; + // Snap all values to a fixed point grid to make comparisons more accurate. + for(&dest.corners) |*corner| { + corner.* = snapToGrid(corner.*); + } + for(&dest.cornerUV) |*uv| { + uv.* = snapToGrid(uv.*); + } + // Snap the normals as well: + dest.normal = snapToGrid(dest.normal); + } + const modelIndex: ModelIndex = @enumFromInt(models.len); + const self = models.addOne(); + var amounts: [6]usize = .{0, 0, 0, 0, 0, 0}; + var internalAmount: usize = 0; + self.min = .{1, 1, 1}; + self.max = .{0, 0, 0}; + self.isNeighborOccluded = @splat(false); + for(adjustedQuads) |*quad| { + for(quad.corners) |corner| { + self.min = @min(self.min, @as(Vec3f, corner)); + self.max = @max(self.max, @as(Vec3f, corner)); + } + if(getFaceNeighbor(quad)) |neighbor| { + amounts[neighbor.toInt()] += 1; + } else { + internalAmount += 1; + } + } + + for(0..6) |i| { + self.neighborFacingQuads[i] = main.globalAllocator.alloc(QuadIndex, amounts[i]); + } + self.internalQuads = main.globalAllocator.alloc(QuadIndex, internalAmount); + + var indices: [6]usize = .{0, 0, 0, 0, 0, 0}; + var internalIndex: usize = 0; + for(adjustedQuads) |_quad| { + var quad = _quad; + if(getFaceNeighbor(&quad)) |neighbor| { + for(&quad.corners) |*corner| { + corner.* = @as(Vec3f, corner.*) - @as(Vec3f, quad.normal); + } + const quadIndex = addQuad(quad) catch continue; + self.neighborFacingQuads[neighbor.toInt()][indices[neighbor.toInt()]] = quadIndex; + indices[neighbor.toInt()] += 1; + } else { + const quadIndex = addQuad(quad) catch continue; + self.internalQuads[internalIndex] = quadIndex; + internalIndex += 1; + } + } + for(0..6) |i| { + self.neighborFacingQuads[i] = main.globalAllocator.realloc(self.neighborFacingQuads[i], indices[i]); + } + self.internalQuads = main.globalAllocator.realloc(self.internalQuads, internalIndex); + self.hasNeighborFacingQuads = false; + self.allNeighborsOccluded = true; + self.noNeighborsOccluded = true; + for(0..6) |neighbor| { + for(self.neighborFacingQuads[neighbor]) |quad| { + if(fullyOccludesNeighbor(quad.quadInfo())) { + self.isNeighborOccluded[neighbor] = true; + } + } + self.hasNeighborFacingQuads = self.hasNeighborFacingQuads or self.neighborFacingQuads[neighbor].len != 0; + self.allNeighborsOccluded = self.allNeighborsOccluded and self.isNeighborOccluded[neighbor]; + self.noNeighborsOccluded = self.noNeighborsOccluded and !self.isNeighborOccluded[neighbor]; + } + generateCollision(self, adjustedQuads); + return modelIndex; + } + + fn edgeInterp(y: f32, x0: f32, y0: f32, x1: f32, y1: f32) f32 { + if(y1 == y0) return x0; + return x0 + (x1 - x0)*(y - y0)/(y1 - y0); + } + + fn solveDepth(normal: Vec3f, v0: Vec3f, xIndex: usize, yIndex: usize, zIndex: usize, u: f32, v: f32) f32 { + const nX = @as([3]f32, normal)[xIndex]; + const nY = @as([3]f32, normal)[yIndex]; + const nZ = @as([3]f32, normal)[zIndex]; + + const planeOffset = -vec.dot(v0, normal); + + return (-(nX*u + nY*v + planeOffset))/nZ; + } + + fn rasterize(triangle: [3]Vec3f, grid: *[collisionGridSize][collisionGridSize]CollisionGridInteger, normal: Vec3f) void { + var xIndex: usize = undefined; + var yIndex: usize = undefined; + var zIndex: usize = undefined; + + const v0 = triangle[0]*@as(Vec3f, @splat(@floatFromInt(collisionGridSize))); + const v1 = triangle[1]*@as(Vec3f, @splat(@floatFromInt(collisionGridSize))); + const v2 = triangle[2]*@as(Vec3f, @splat(@floatFromInt(collisionGridSize))); + + const absNormal = @abs(normal); + if(absNormal[0] >= absNormal[1] and absNormal[0] >= absNormal[2]) { + xIndex = 1; + yIndex = 2; + zIndex = 0; + } else if(absNormal[1] >= absNormal[0] and absNormal[1] >= absNormal[2]) { + xIndex = 0; + yIndex = 2; + zIndex = 1; + } else { + xIndex = 0; + yIndex = 1; + zIndex = 2; + } + + const min: Vec3f = @min(v0, v1, v2); + const max: Vec3f = @max(v0, v1, v2); + + const voxelMin: Vec3i = @max(@as(Vec3i, @intFromFloat(@floor(min))), @as(Vec3i, @splat(0))); + const voxelMax: Vec3i = @max(@as(Vec3i, @intFromFloat(@ceil(max))), @as(Vec3i, @splat(0))); + + var p0 = Vec2f{@as([3]f32, v0)[xIndex], @as([3]f32, v0)[yIndex]}; + var p1 = Vec2f{@as([3]f32, v1)[xIndex], @as([3]f32, v1)[yIndex]}; + var p2 = Vec2f{@as([3]f32, v2)[xIndex], @as([3]f32, v2)[yIndex]}; + + if(p0[1] > p1[1]) { + std.mem.swap(Vec2f, &p0, &p1); + } + if(p0[1] > p2[1]) { + std.mem.swap(Vec2f, &p0, &p2); + } + if(p1[1] > p2[1]) { + std.mem.swap(Vec2f, &p1, &p2); + } + + for(@intCast(@as([3]i32, voxelMin)[yIndex])..@intCast(@as([3]i32, voxelMax)[yIndex])) |y| { + if(y >= collisionGridSize) continue; + const yf = std.math.clamp(@as(f32, @floatFromInt(y)) + 0.5, @as([3]f32, min)[yIndex], @as([3]f32, max)[yIndex]); + var xa: f32 = undefined; + var xb: f32 = undefined; + if(yf < p1[1]) { + xa = edgeInterp(yf, p0[0], p0[1], p1[0], p1[1]); + xb = edgeInterp(yf, p0[0], p0[1], p2[0], p2[1]); + } else { + xa = edgeInterp(yf, p1[0], p1[1], p2[0], p2[1]); + xb = edgeInterp(yf, p0[0], p0[1], p2[0], p2[1]); + } + + const xStart: f32 = @min(xa, xb); + const xEnd: f32 = @max(xa, xb); + + const voxelXStart: usize = @intFromFloat(@max(@floor(xStart), 0.0)); + const voxelXEnd: usize = @intFromFloat(@max(@ceil(xEnd), 0.0)); + + for(voxelXStart..voxelXEnd) |x| { + if(x < 0 or x >= collisionGridSize) continue; + const xf = std.math.clamp(@as(f32, @floatFromInt(x)) + 0.5, xStart, xEnd); + + const zf = solveDepth(normal, v0, xIndex, yIndex, zIndex, xf, yf); + if(zf < 0.0) continue; + const z: usize = @intFromFloat(zf); + + if(z >= collisionGridSize) continue; + + const pos: [3]usize = .{x, y, z}; + var realPos: [3]usize = undefined; + realPos[xIndex] = pos[0]; + realPos[yIndex] = pos[1]; + realPos[zIndex] = pos[2]; + grid[realPos[0]][realPos[1]] |= @as(CollisionGridInteger, 1) << @intCast(realPos[2]); + } + } + } + + fn generateCollision(self: *Model, modelQuads: []QuadInfo) void { + var hollowGrid: [collisionGridSize][collisionGridSize]CollisionGridInteger = @splat(@splat(0)); + const voxelSize: Vec3f = @splat(1.0/@as(f32, collisionGridSize)); + + for(modelQuads) |quad| { + var shift = Vec3f{0, 0, 0}; + inline for(0..3) |i| { + if(@abs(quad.normalVec()[i]) == 1.0 and @floor(quad.corners[0][i]*collisionGridSize) == quad.corners[0][i]*collisionGridSize) { + shift = quad.normalVec()*voxelSize*@as(Vec3f, @splat(0.5)); + } + } + const triangle1: [3]Vec3f = .{ + quad.cornerVec(0) - shift, + quad.cornerVec(1) - shift, + quad.cornerVec(2) - shift, + }; + const triangle2: [3]Vec3f = .{ + quad.cornerVec(1) - shift, + quad.cornerVec(2) - shift, + quad.cornerVec(3) - shift, + }; + + rasterize(triangle1, &hollowGrid, quad.normalVec()); + rasterize(triangle2, &hollowGrid, quad.normalVec()); + } + + const allOnes = ~@as(CollisionGridInteger, 0); + var grid: [collisionGridSize][collisionGridSize]CollisionGridInteger = @splat(@splat(allOnes)); + + var floodfillQueue = main.utils.CircularBufferQueue(struct {x: usize, y: usize, val: CollisionGridInteger}).init(main.stackAllocator, 1024); + defer floodfillQueue.deinit(); + + for(0..collisionGridSize) |x| { + for(0..collisionGridSize) |y| { + var val = 1 | @as(CollisionGridInteger, 1) << (@bitSizeOf(CollisionGridInteger) - 1); + if(x == 0 or x == collisionGridSize - 1 or y == 0 or y == collisionGridSize - 1) val = allOnes; + + floodfillQueue.pushBack(.{.x = x, .y = y, .val = val}); + } + } + + while(floodfillQueue.popFront()) |elem| { + const oldValue = grid[elem.x][elem.y]; + const newValue = oldValue & ~(~hollowGrid[elem.x][elem.y] & elem.val); + if(oldValue == newValue) continue; + grid[elem.x][elem.y] = newValue; + + if(elem.x != 0) floodfillQueue.pushBack(.{.x = elem.x - 1, .y = elem.y, .val = ~newValue}); + if(elem.x != collisionGridSize - 1) floodfillQueue.pushBack(.{.x = elem.x + 1, .y = elem.y, .val = ~newValue}); + if(elem.y != 0) floodfillQueue.pushBack(.{.x = elem.x, .y = elem.y - 1, .val = ~newValue}); + if(elem.y != collisionGridSize - 1) floodfillQueue.pushBack(.{.x = elem.x, .y = elem.y + 1, .val = ~newValue}); + floodfillQueue.pushBack(.{.x = elem.x, .y = elem.y, .val = ~newValue << 1 | ~newValue >> 1}); + } + + var collision: main.List(Box) = .init(main.globalAllocator); + + for(0..collisionGridSize) |x| { + for(0..collisionGridSize) |y| { + while(grid[x][y] != 0) { + const startZ = @ctz(grid[x][y]); + const height = @min(@bitSizeOf(CollisionGridInteger) - startZ, @ctz(~grid[x][y] >> @intCast(startZ))); + const mask = allOnes << @intCast(startZ) & ~((allOnes << 1) << @intCast(height + startZ - 1)); + + const boxMin = Vec3i{@intCast(x), @intCast(y), startZ}; + var boxMax = Vec3i{@intCast(x + 1), @intCast(y + 1), startZ + height}; + + while(canExpand(&grid, boxMin, boxMax, .x, mask)) boxMax[0] += 1; + while(canExpand(&grid, boxMin, boxMax, .y, mask)) boxMax[1] += 1; + disableAll(&grid, boxMin, boxMax, mask); + + const min = @as(Vec3f, @floatFromInt(boxMin))/@as(Vec3f, @splat(collisionGridSize)); + const max = @as(Vec3f, @floatFromInt(boxMax))/@as(Vec3f, @splat(collisionGridSize)); + + collision.append(Box{.min = min, .max = max}); + } + } + } + + self.collision = collision.toOwnedSlice(); + } + + fn allTrue(grid: *const [collisionGridSize][collisionGridSize]CollisionGridInteger, min: Vec3i, max: Vec3i, mask: CollisionGridInteger) bool { + if(max[0] > collisionGridSize or max[1] > collisionGridSize) { + return false; + } + for(@intCast(min[0])..@intCast(max[0])) |x| { + for(@intCast(min[1])..@intCast(max[1])) |y| { + if((grid[x][y] & mask) != mask) { + return false; + } + } + } + return true; + } + + fn disableAll(grid: *[collisionGridSize][collisionGridSize]CollisionGridInteger, min: Vec3i, max: Vec3i, mask: CollisionGridInteger) void { + for(@intCast(min[0])..@intCast(max[0])) |x| { + for(@intCast(min[1])..@intCast(max[1])) |y| { + grid[x][y] &= ~mask; + } + } + } + + fn canExpand(grid: *const [collisionGridSize][collisionGridSize]CollisionGridInteger, min: Vec3i, max: Vec3i, dir: enum {x, y}, mask: CollisionGridInteger) bool { + return switch(dir) { + .x => allTrue(grid, Vec3i{max[0], min[1], min[2]}, Vec3i{max[0] + 1, max[1], max[2]}, mask), + .y => allTrue(grid, Vec3i{min[0], max[1], min[2]}, Vec3i{max[0], max[1] + 1, max[2]}, mask), + }; + } + + fn addVert(vert: Vec3f, vertList: *main.List(Vec3f)) usize { + const ind = for(vertList.*.items, 0..) |vertex, index| { + if(vertex == vert) break index; + } else vertList.*.items.len; + + if(ind == vertList.*.items.len) { + vertList.*.append(vert); + } + + return ind; + } + + pub fn loadModel(data: []const u8) ModelIndex { + const quadInfos = loadRawModelDataFromObj(main.stackAllocator, data); + defer main.stackAllocator.free(quadInfos); + for(quadInfos) |*quad| { + var minUv: Vec2f = @splat(std.math.inf(f32)); + for(0..4) |i| { + quad.cornerUV[i] = @as(Vec2f, quad.cornerUV[i])*@as(Vec2f, @splat(4)); + minUv = @min(minUv, @as(Vec2f, quad.cornerUV[i])); + } + minUv = @floor(minUv); + quad.textureSlot = @as(u32, @intFromFloat(minUv[1]))*4 + @as(u32, @intFromFloat(minUv[0])); + + if(minUv[0] < 0 or minUv[0] > 4 or minUv[1] < 0 or minUv[1] > 4) { + std.log.err("Uv value for model is outside of 0-1 range", .{}); + } + + for(0..4) |i| { + quad.cornerUV[i] = @as(Vec2f, quad.cornerUV[i]) - minUv; + } + } + return Model.init(quadInfos); + } + + pub fn loadRawModelDataFromObj(allocator: main.heap.NeverFailingAllocator, data: []const u8) []QuadInfo { + var vertices = main.List(Vec3f).init(main.stackAllocator); + defer vertices.deinit(); + + var normals = main.List(Vec3f).init(main.stackAllocator); + defer normals.deinit(); + + var uvs = main.List(Vec2f).init(main.stackAllocator); + defer uvs.deinit(); + + var tris = main.List(Triangle).init(main.stackAllocator); + defer tris.deinit(); + + var quadFaces = main.List(Quad).init(main.stackAllocator); + defer quadFaces.deinit(); + + var splitIterator = std.mem.splitScalar(u8, data, '\n'); + while(splitIterator.next()) |lineUntrimmed| { + if(lineUntrimmed.len < 3) + continue; + + var line = lineUntrimmed; + if(line[line.len - 1] == '\r') { + line = line[0 .. line.len - 1]; + } + + if(line[0] == '#') + continue; + + if(std.mem.eql(u8, line[0..2], "v ")) { + var coordsIter = std.mem.splitScalar(u8, line[2..], ' '); + var coords: [3]f32 = undefined; + var i: usize = 0; + while(coordsIter.next()) |coord| : (i += 1) { + coords[i] = std.fmt.parseFloat(f32, coord) catch |e| blk: { + std.log.err("Failed parsing {s} into float: {any}", .{coord, e}); + break :blk 0; + }; + } + vertices.append(coords); + } else if(std.mem.eql(u8, line[0..3], "vn ")) { + var coordsIter = std.mem.splitScalar(u8, line[3..], ' '); + var norm: [3]f32 = undefined; + var i: usize = 0; + while(coordsIter.next()) |coord| : (i += 1) { + norm[i] = std.fmt.parseFloat(f32, coord) catch |e| blk: { + std.log.err("Failed parsing {s} into float: {any}", .{coord, e}); + break :blk 0; + }; + } + normals.append(norm); + } else if(std.mem.eql(u8, line[0..3], "vt ")) { + var coordsIter = std.mem.splitScalar(u8, line[3..], ' '); + var uv: [2]f32 = undefined; + var i: usize = 0; + while(coordsIter.next()) |coord| : (i += 1) { + uv[i] = std.fmt.parseFloat(f32, coord) catch |e| blk: { + std.log.err("Failed parsing {s} into float: {any}", .{coord, e}); + break :blk 0; + }; + } + uvs.append(uv); + } else if(std.mem.eql(u8, line[0..2], "f ")) { + var coordsIter = std.mem.splitScalar(u8, line[2..], ' '); + var faceData: [3][4]usize = undefined; + var i: usize = 0; + var failed = false; + while(coordsIter.next()) |vertex| : (i += 1) { + if(i >= 4) { + failed = true; + std.log.err("More than 4 verticies in a face", .{}); + break; + } + var d = std.mem.splitScalar(u8, vertex, '/'); + var j: usize = 0; + if(std.mem.count(u8, vertex, "/") != 2 or std.mem.count(u8, vertex, "//") != 0) { + failed = true; + std.log.err("Failed loading face {s}. Each vertex must use vertex/uv/normal", .{line}); + break; + } + while(d.next()) |value| : (j += 1) { + faceData[j][i] = std.fmt.parseUnsigned(usize, value, 10) catch |e| blk: { + std.log.err("Failed parsing {s} into uint: {any}", .{value, e}); + break :blk 1; + }; + faceData[j][i] -= 1; + } + } + if(!failed) { + switch(i) { + 3 => { + tris.append(.{.vertex = faceData[0][0..3].*, .uvs = faceData[1][0..3].*, .normal = faceData[2][0]}); + }, + 4 => { + quadFaces.append(.{.vertex = faceData[0], .uvs = faceData[1], .normal = faceData[2][0]}); + }, + else => std.log.err("Failed loading face {s} with {d} vertices", .{line, i}), + } + } + } + } + + var quadInfos = main.List(QuadInfo).initCapacity(allocator, tris.items.len + quads.items.len); + defer quadInfos.deinit(); + + for(tris.items) |face| { + const normal: Vec3f = normals.items[face.normal]; + + const uvA: Vec2f = uvs.items[face.uvs[0]]; + const uvB: Vec2f = uvs.items[face.uvs[2]]; + const uvC: Vec2f = uvs.items[face.uvs[1]]; + + const cornerA: Vec3f = vertices.items[face.vertex[0]]; + const cornerB: Vec3f = vertices.items[face.vertex[2]]; + const cornerC: Vec3f = vertices.items[face.vertex[1]]; + + quadInfos.append(.{ + .normal = normal, + .corners = .{cornerA, cornerB, cornerC, cornerB}, + .cornerUV = .{uvA, uvB, uvC, uvB}, + .textureSlot = 0, + }); + } + + for(quadFaces.items) |face| { + const normal: Vec3f = normals.items[face.normal]; + + const uvA: Vec2f = uvs.items[face.uvs[1]]; + const uvB: Vec2f = uvs.items[face.uvs[0]]; + const uvC: Vec2f = uvs.items[face.uvs[2]]; + const uvD: Vec2f = uvs.items[face.uvs[3]]; + + const cornerA: Vec3f = vertices.items[face.vertex[1]]; + const cornerB: Vec3f = vertices.items[face.vertex[0]]; + const cornerC: Vec3f = vertices.items[face.vertex[2]]; + const cornerD: Vec3f = vertices.items[face.vertex[3]]; + + quadInfos.append(.{ + .normal = normal, + .corners = .{cornerA, cornerB, cornerC, cornerD}, + .cornerUV = .{uvA, uvB, uvC, uvD}, + .textureSlot = 0, + }); + } + + return quadInfos.toOwnedSlice(); + } + + fn deinit(self: *const Model) void { + for(0..6) |i| { + main.globalAllocator.free(self.neighborFacingQuads[i]); + } + main.globalAllocator.free(self.internalQuads); + main.globalAllocator.free(self.collision); + } + + pub fn getRawFaces(model: Model, quadList: *main.List(QuadInfo)) void { + for(model.internalQuads) |quadIndex| { + quadList.append(quadIndex.quadInfo().*); + } + for(0..6) |neighbor| { + for(model.neighborFacingQuads[neighbor]) |quadIndex| { + var quad = quadIndex.quadInfo().*; + for(&quad.corners) |*corner| { + corner.* = @as(Vec3f, corner.*) + @as(Vec3f, quad.normal); + } + quadList.append(quad); + } + } + } + + pub fn mergeModels(modelList: []ModelIndex) ModelIndex { + var quadList = main.List(QuadInfo).init(main.stackAllocator); + defer quadList.deinit(); + for(modelList) |model| { + model.model().getRawFaces(&quadList); + } + return Model.init(quadList.items); + } + + pub fn transformModel(model: Model, transformFunction: anytype, transformFunctionParameters: anytype) ModelIndex { + var quadList = main.List(QuadInfo).init(main.stackAllocator); + defer quadList.deinit(); + model.getRawFaces(&quadList); + for(quadList.items) |*quad| { + @call(.auto, transformFunction, .{quad} ++ transformFunctionParameters); + } + return Model.init(quadList.items); + } + + fn appendQuadsToList(quadList: []const QuadIndex, list: *main.ListUnmanaged(FaceData), allocator: NeverFailingAllocator, block: main.blocks.Block, pos: main.chunk.BlockPos, comptime backFace: bool) void { + for(quadList) |quadIndex| { + const texture = main.blocks.meshes.textureIndex(block, quadIndex.quadInfo().textureSlot); + list.append(allocator, FaceData.init(texture, quadIndex, pos, backFace)); + } + } + + pub fn appendInternalQuadsToList(self: *const Model, list: *main.ListUnmanaged(FaceData), allocator: NeverFailingAllocator, block: main.blocks.Block, pos: main.chunk.BlockPos, comptime backFace: bool) void { + appendQuadsToList(self.internalQuads, list, allocator, block, pos, backFace); + } + + pub fn appendNeighborFacingQuadsToList(self: *const Model, list: *main.ListUnmanaged(FaceData), allocator: NeverFailingAllocator, block: main.blocks.Block, neighbor: Neighbor, pos: main.chunk.BlockPos, comptime backFace: bool) void { + appendQuadsToList(self.neighborFacingQuads[neighbor.toInt()], list, allocator, block, pos, backFace); + } +}; + +var nameToIndex: std.StringHashMap(ModelIndex) = undefined; + +pub fn getModelIndex(string: []const u8) ModelIndex { + return nameToIndex.get(string) orelse { + std.log.err("Couldn't find voxelModel with name: {s}.", .{string}); + return @enumFromInt(0); + }; +} + +var quads: main.List(QuadInfo) = undefined; +var extraQuadInfos: main.List(ExtraQuadInfo) = undefined; +var models: main.utils.VirtualList(Model, 1 << 20) = undefined; + +var quadDeduplication: std.AutoHashMap([@sizeOf(QuadInfo)]u8, QuadIndex) = undefined; + +fn addQuad(info_: QuadInfo) error{Degenerate}!QuadIndex { + var info = info_; + if(quadDeduplication.get(std.mem.toBytes(info))) |id| { + return id; + } + // Check if it's degenerate: + var cornerEqualities: u32 = 0; + for(0..4) |i| { + for(i + 1..4) |j| { + if(@reduce(.And, @as(Vec3f, info.corners[i]) == @as(Vec3f, info.corners[j]))) cornerEqualities += 1; + } + } + if(cornerEqualities >= 2) return error.Degenerate; // One corner equality is fine, since then the quad degenerates to a triangle, which has a non-zero area. + const index: QuadIndex = @enumFromInt(quads.items.len); + if(info.opaqueInLod == 2) { + info.opaqueInLod = 0; + } else { + info.opaqueInLod = @intFromBool(Model.getFaceNeighbor(&info) != null); + } + quads.append(info); + quadDeduplication.put(std.mem.toBytes(info), index) catch unreachable; + + var extraQuadInfo: ExtraQuadInfo = undefined; + extraQuadInfo.faceNeighbor = Model.getFaceNeighbor(&info); + extraQuadInfo.isFullQuad = Model.fullyOccludesNeighbor(&info); + { + var zeroes: @Vector(3, u32) = .{0, 0, 0}; + var ones: @Vector(3, u32) = .{0, 0, 0}; + for(info.corners) |corner| { + zeroes += @select(u32, corner == @as(Vec3f, @splat(0)), .{1, 1, 1}, .{0, 0, 0}); + ones += @select(u32, corner == @as(Vec3f, @splat(1)), .{1, 1, 1}, .{0, 0, 0}); + } + const cornerValues = @reduce(.Add, zeroes) + @reduce(.Add, ones); + extraQuadInfo.hasOnlyCornerVertices = cornerValues == 4*3; + } + { + extraQuadInfo.alignedNormalDirection = null; + if(@reduce(.And, info.normal == Vec3f{-1, 0, 0})) extraQuadInfo.alignedNormalDirection = .dirNegX; + if(@reduce(.And, info.normal == Vec3f{1, 0, 0})) extraQuadInfo.alignedNormalDirection = .dirPosX; + if(@reduce(.And, info.normal == Vec3f{0, -1, 0})) extraQuadInfo.alignedNormalDirection = .dirNegY; + if(@reduce(.And, info.normal == Vec3f{0, 1, 0})) extraQuadInfo.alignedNormalDirection = .dirPosY; + if(@reduce(.And, info.normal == Vec3f{0, 0, -1})) extraQuadInfo.alignedNormalDirection = .dirDown; + if(@reduce(.And, info.normal == Vec3f{0, 0, 1})) extraQuadInfo.alignedNormalDirection = .dirUp; + } + extraQuadInfos.append(extraQuadInfo); + + return index; +} + +fn box(min: Vec3f, max: Vec3f, uvOffset: Vec2f) [6]QuadInfo { + const corner000: Vec3f = .{min[0], min[1], min[2]}; + const corner001: Vec3f = .{min[0], min[1], max[2]}; + const corner010: Vec3f = .{min[0], max[1], min[2]}; + const corner011: Vec3f = .{min[0], max[1], max[2]}; + const corner100: Vec3f = .{max[0], min[1], min[2]}; + const corner101: Vec3f = .{max[0], min[1], max[2]}; + const corner110: Vec3f = .{max[0], max[1], min[2]}; + const corner111: Vec3f = .{max[0], max[1], max[2]}; + return .{ + .{ + .normal = .{-1, 0, 0}, + .corners = .{corner010, corner011, corner000, corner001}, + .cornerUV = .{uvOffset + Vec2f{1 - max[1], min[2]}, uvOffset + Vec2f{1 - max[1], max[2]}, uvOffset + Vec2f{1 - min[1], min[2]}, uvOffset + Vec2f{1 - min[1], max[2]}}, + .textureSlot = Neighbor.dirNegX.toInt(), + }, + .{ + .normal = .{1, 0, 0}, + .corners = .{corner100, corner101, corner110, corner111}, + .cornerUV = .{uvOffset + Vec2f{min[1], min[2]}, uvOffset + Vec2f{min[1], max[2]}, uvOffset + Vec2f{max[1], min[2]}, uvOffset + Vec2f{max[1], max[2]}}, + .textureSlot = Neighbor.dirPosX.toInt(), + }, + .{ + .normal = .{0, -1, 0}, + .corners = .{corner000, corner001, corner100, corner101}, + .cornerUV = .{uvOffset + Vec2f{min[0], min[2]}, uvOffset + Vec2f{min[0], max[2]}, uvOffset + Vec2f{max[0], min[2]}, uvOffset + Vec2f{max[0], max[2]}}, + .textureSlot = Neighbor.dirNegY.toInt(), + }, + .{ + .normal = .{0, 1, 0}, + .corners = .{corner110, corner111, corner010, corner011}, + .cornerUV = .{uvOffset + Vec2f{1 - max[0], min[2]}, uvOffset + Vec2f{1 - max[0], max[2]}, uvOffset + Vec2f{1 - min[0], min[2]}, uvOffset + Vec2f{1 - min[0], max[2]}}, + .textureSlot = Neighbor.dirPosY.toInt(), + }, + .{ + .normal = .{0, 0, -1}, + .corners = .{corner010, corner000, corner110, corner100}, + .cornerUV = .{uvOffset + Vec2f{min[0], 1 - max[1]}, uvOffset + Vec2f{min[0], 1 - min[1]}, uvOffset + Vec2f{max[0], 1 - max[1]}, uvOffset + Vec2f{max[0], 1 - min[1]}}, + .textureSlot = Neighbor.dirDown.toInt(), + }, + .{ + .normal = .{0, 0, 1}, + .corners = .{corner111, corner101, corner011, corner001}, + .cornerUV = .{uvOffset + Vec2f{1 - max[0], 1 - max[1]}, uvOffset + Vec2f{1 - max[0], 1 - min[1]}, uvOffset + Vec2f{1 - min[0], 1 - max[1]}, uvOffset + Vec2f{1 - min[0], 1 - min[1]}}, + .textureSlot = Neighbor.dirUp.toInt(), + }, + }; +} + +fn openBox(min: Vec3f, max: Vec3f, uvOffset: Vec2f, openSide: enum {x, y, z}) [4]QuadInfo { + const fullBox = box(min, max, uvOffset); + switch(openSide) { + .x => return fullBox[2..6].*, + .y => return fullBox[0..2].* ++ fullBox[4..6].*, + .z => return fullBox[0..4].*, + } +} + +pub fn registerModel(id: []const u8, data: []const u8) ModelIndex { + const model = Model.loadModel(data); + nameToIndex.put(id, model) catch unreachable; + return model; +} + +// TODO: Entity models. +pub fn init() void { + models = .init(); + quads = .init(main.globalAllocator); + extraQuadInfos = .init(main.globalAllocator); + quadDeduplication = .init(main.globalAllocator.allocator); + + nameToIndex = .init(main.globalAllocator.allocator); + + nameToIndex.put("none", Model.init(&.{})) catch unreachable; +} + +pub fn reset() void { + for(models.items()) |model| { + model.deinit(); + } + models.clearRetainingCapacity(); + quads.clearRetainingCapacity(); + extraQuadInfos.clearRetainingCapacity(); + quadDeduplication.clearRetainingCapacity(); + nameToIndex.clearRetainingCapacity(); + nameToIndex.put("none", Model.init(&.{})) catch unreachable; +} + +pub fn deinit() void { + quadSSBO.deinit(); + nameToIndex.deinit(); + for(models.items()) |model| { + model.deinit(); + } + models.deinit(); + quads.deinit(); + extraQuadInfos.deinit(); + quadDeduplication.deinit(); +} + +pub fn uploadModels() void { + quadSSBO = graphics.SSBO.initStatic(QuadInfo, quads.items); + quadSSBO.bind(4); +} diff --git a/network.zig b/network.zig new file mode 100644 index 0000000000..f1d992323d --- /dev/null +++ b/network.zig @@ -0,0 +1,1578 @@ +const builtin = @import("builtin"); +const std = @import("std"); +const Atomic = std.atomic.Value; + +const main = @import("main"); +const game = main.game; +const settings = main.settings; +const utils = main.utils; +const NeverFailingAllocator = main.heap.NeverFailingAllocator; + +pub const protocols = @import("network/protocols.zig"); + +// TODO: Might want to use SSL or something similar to encode the message + +const ms = 1_000; +inline fn networkTimestamp() i64 { + return @truncate(@divTrunc(main.timestamp().toNanoseconds(), 1000)); +} + +const Socket = struct { + const ws2 = @cImport({ + @cInclude("winsock2.h"); + }); + const posix = std.posix; + socketID: if(builtin.os.tag == .windows) ws2.SOCKET else posix.socket_t, + + fn windowsError(err: c_int) !void { + if(err == 0) return; + switch(err) { + ws2.WSASYSNOTREADY => return error.WSASYSNOTREADY, + ws2.WSAVERNOTSUPPORTED => return error.WSAVERNOTSUPPORTED, + ws2.WSAEINPROGRESS => return error.WSAEINPROGRESS, + ws2.WSAEPROCLIM => return error.WSAEPROCLIM, + ws2.WSAEFAULT => return error.WSAEFAULT, + ws2.WSANOTINITIALISED => return error.WSANOTINITIALISED, + ws2.WSAENETDOWN => return error.WSAENETDOWN, + ws2.WSAEACCES => return error.WSAEACCES, + ws2.WSAEADDRINUSE => return error.WSAEADDRINUSE, + ws2.WSAEADDRNOTAVAIL => return error.WSAEADDRNOTAVAIL, + ws2.WSAEINVAL => return error.WSAEINVAL, + ws2.WSAENOBUFS => return error.WSAENOBUFS, + ws2.WSAENOTSOCK => return error.WSAENOTSOCK, + else => return error.UNKNOWN, + } + } + + fn startup() void { + if(builtin.os.tag == .windows) { + var data: ws2.WSADATA = undefined; + windowsError(ws2.WSAStartup(0x0202, &data)) catch |err| { + std.log.err("Could not initialize the Windows Socket API: {s}", .{@errorName(err)}); + @panic("Could not init networking."); + }; + } + } + + fn init(localPort: u16) !Socket { + const self = Socket{ + .socketID = blk: { + if(builtin.os.tag == .windows) { + const socket = ws2.socket(ws2.AF_INET, ws2.SOCK_DGRAM, ws2.IPPROTO_UDP); + if(socket == ws2.INVALID_SOCKET) { + try windowsError(ws2.WSAGetLastError()); + return error.UNKNOWN; + } + break :blk socket; + } else { + break :blk try posix.socket(posix.AF.INET, posix.SOCK.DGRAM, posix.IPPROTO.UDP); + } + }, + }; + errdefer self.deinit(); + const bindingAddr = posix.sockaddr.in{ + .port = @byteSwap(localPort), + .addr = 0, + }; + if(builtin.os.tag == .windows) { + if(ws2.bind(self.socketID, @ptrCast(&bindingAddr), @sizeOf(posix.sockaddr.in)) == ws2.SOCKET_ERROR) { + try windowsError(ws2.WSAGetLastError()); + } + } else { + try posix.bind(self.socketID, @ptrCast(&bindingAddr), @sizeOf(posix.sockaddr.in)); + } + return self; + } + + fn deinit(self: Socket) void { + if(builtin.os.tag == .windows) { + _ = ws2.closesocket(self.socketID); + } else { + posix.close(self.socketID); + } + } + + fn send(self: Socket, data: []const u8, destination: Address) void { + const addr = posix.sockaddr.in{ + .port = @byteSwap(destination.port), + .addr = destination.ip, + }; + if(builtin.os.tag == .windows) { + const result = ws2.sendto(self.socketID, data.ptr, @intCast(data.len), 0, @ptrCast(&addr), @sizeOf(posix.sockaddr.in)); + if(result == ws2.SOCKET_ERROR) { + const err: anyerror = if(windowsError(ws2.WSAGetLastError())) error.Unknown else |err| err; + std.log.warn("Got error while sending to {f}: {s}", .{destination, @errorName(err)}); + } else { + std.debug.assert(@as(usize, @intCast(result)) == data.len); + } + } else { + std.debug.assert(data.len == posix.sendto(self.socketID, data, 0, @ptrCast(&addr), @sizeOf(posix.sockaddr.in)) catch |err| { + std.log.warn("Got error while sending to {f}: {s}", .{destination, @errorName(err)}); + return; + }); + } + } + + fn receive(self: Socket, buffer: []u8, timeout: i32, resultAddress: *Address) ![]u8 { + if(builtin.os.tag == .windows) { // Of course Windows always has it's own special thing. + var pfd = [1]ws2.pollfd{ + .{.fd = self.socketID, .events = std.c.POLL.RDNORM | std.c.POLL.RDBAND, .revents = undefined}, + }; + const length = ws2.WSAPoll(&pfd, pfd.len, 0); // The timeout is set to zero. Otherwise sendto operations from other threads will block on this. + if(length == ws2.SOCKET_ERROR) { + try windowsError(ws2.WSAGetLastError()); + } else if(length == 0) { + main.io.sleep(.fromMilliseconds(1), .awake) catch {}; // Manually sleep, since WSAPoll is blocking. + return error.Timeout; + } + } else { + var pfd = [1]posix.pollfd{ + .{.fd = self.socketID, .events = posix.POLL.IN, .revents = undefined}, + }; + const length = try posix.poll(&pfd, timeout); + if(length == 0) return error.Timeout; + } + var addr: posix.sockaddr.in = undefined; + const length: usize = blk: { + if(builtin.os.tag == .windows) { + var addrLen: c_int = @sizeOf(posix.sockaddr.in); + const result = ws2.recvfrom(self.socketID, buffer.ptr, @intCast(buffer.len), 0, @ptrCast(&addr), &addrLen); + if(result == ws2.SOCKET_ERROR) { + try windowsError(ws2.WSAGetLastError()); + } + break :blk @intCast(result); + } else { + var addrLen: posix.socklen_t = @sizeOf(posix.sockaddr.in); + break :blk try posix.recvfrom(self.socketID, buffer, 0, @ptrCast(&addr), &addrLen); + } + }; + resultAddress.ip = addr.addr; + resultAddress.port = @byteSwap(addr.port); + return buffer[0..length]; + } + + fn resolveIP(name: []const u8) !u32 { + var nameBuf: [255]u8 = undefined; + var buf: [16]std.Io.net.HostName.LookupResult = undefined; + var resultQueue = std.Io.Queue(std.Io.net.HostName.LookupResult).init(&buf); + std.Io.net.HostName.lookup(.{.bytes = name}, main.io, &resultQueue, .{.canonical_name_buffer = &nameBuf, .port = 0}); + while(true) { + const entry = resultQueue.getOneUncancelable(main.io); + switch(entry) { + .address => |addr| { + if(addr != .ip4) continue; + return std.mem.bytesToValue(u32, addr.ip4.bytes[0..4]); + }, + .canonical_name => {}, + .end => |err| { + try err; + break; + }, + } + } + return error.ReachedEndWithoutFindingAnything; + } + + fn getPort(self: Socket) !u16 { + var addr: posix.sockaddr.in = undefined; + if(builtin.os.tag == .windows) { + var addrLen: c_int = @sizeOf(posix.sockaddr.in); + if(ws2.getsockname(self.socketID, @ptrCast(&addr), &addrLen) == ws2.SOCKET_ERROR) { + try windowsError(ws2.WSAGetLastError()); + } + } else { + var addrLen: posix.socklen_t = @sizeOf(posix.sockaddr.in); + try posix.getsockname(self.socketID, @ptrCast(&addr), &addrLen); + } + return @byteSwap(addr.port); + } +}; + +pub fn init() void { + Socket.startup(); + protocols.init(); +} + +pub const Address = struct { + ip: u32, + port: u16, + isSymmetricNAT: bool = false, + + pub const localHost = 0x0100007f; + + pub fn format(self: Address, writer: anytype) !void { + if(self.isSymmetricNAT) { + try writer.print("{}.{}.{}.{}:?{}", .{self.ip & 255, self.ip >> 8 & 255, self.ip >> 16 & 255, self.ip >> 24, self.port}); + } else { + try writer.print("{}.{}.{}.{}:{}", .{self.ip & 255, self.ip >> 8 & 255, self.ip >> 16 & 255, self.ip >> 24, self.port}); + } + } +}; + +const Request = struct { + address: Address, + data: []const u8, + requestNotifier: std.Thread.Condition = std.Thread.Condition{}, +}; + +/// Implements parts of the STUN(Session Traversal Utilities for NAT) protocol to discover public IP+Port +/// Reference: https://datatracker.ietf.org/doc/html/rfc5389 +const stun = struct { // MARK: stun + const ipServerList = [_][]const u8{ + "stun.12voip.com:3478", + "stun.1und1.de:3478", + "stun.acrobits.cz:3478", + "stun.actionvoip.com:3478", + "stun.antisip.com:3478", + "stun.avigora.fr:3478", + "stun.bluesip.net:3478", + "stun.cablenet-as.net:3478", + "stun.callromania.ro:3478", + "stun.cheapvoip.com:3478", + "stun.cope.es:3478", + "stun.counterpath.com:3478", + "stun.counterpath.net:3478", + "stun.dcalling.de:3478", + "stun.dus.net:3478", + "stun.ekiga.net:3478", + "stun.epygi.com:3478", + "stun.freeswitch.org:3478", + "stun.freevoipdeal.com:3478", + "stun.gmx.de:3478", + "stun.gmx.net:3478", + "stun.halonet.pl:3478", + "stun.hoiio.com:3478", + "stun.internetcalls.com:3478", + "stun.intervoip.com:3478", + "stun.ipfire.org:3478", + "stun.ippi.fr:3478", + "stun.ipshka.com:3478", + "stun.it1.hr:3478", + "stun.jumblo.com:3478", + "stun.justvoip.com:3478", + "stun.l.google.com:19302", + "stun.linphone.org:3478", + "stun.liveo.fr:3478", + "stun.lowratevoip.com:3478", + "stun.myvoiptraffic.com:3478", + "stun.netappel.com:3478", + "stun.netgsm.com.tr:3478", + "stun.nfon.net:3478", + "stun.nonoh.net:3478", + "stun.ozekiphone.com:3478", + "stun.pjsip.org:3478", + "stun.powervoip.com:3478", + "stun.ppdi.com:3478", + "stun.rockenstein.de:3478", + "stun.rolmail.net:3478", + "stun.rynga.com:3478", + "stun.schlund.de:3478", + "stun.sigmavoip.com:3478", + "stun.sip.us:3478", + "stun.sipdiscount.com:3478", + "stun.sipgate.net:10000", + "stun.sipgate.net:3478", + "stun.siplogin.de:3478", + "stun.siptraffic.com:3478", + "stun.smartvoip.com:3478", + "stun.smsdiscount.com:3478", + "stun.solcon.nl:3478", + "stun.solnet.ch:3478", + "stun.sonetel.com:3478", + "stun.sonetel.net:3478", + "stun.srce.hr:3478", + "stun.t-online.de:3478", + "stun.tel.lu:3478", + "stun.telbo.com:3478", + "stun.tng.de:3478", + "stun.twt.it:3478", + "stun.vo.lu:3478", + "stun.voicetrading.com:3478", + "stun.voip.aebc.com:3478", + "stun.voip.blackberry.com:3478", + "stun.voip.eutelia.it:3478", + "stun.voipblast.com:3478", + "stun.voipbuster.com:3478", + "stun.voipbusterpro.com:3478", + "stun.voipcheap.co.uk:3478", + "stun.voipcheap.com:3478", + "stun.voipgain.com:3478", + "stun.voipgate.com:3478", + "stun.voipinfocenter.com:3478", + "stun.voipplanet.nl:3478", + "stun.voippro.com:3478", + "stun.voipraider.com:3478", + "stun.voipstunt.com:3478", + "stun.voipwise.com:3478", + "stun.voipzoom.com:3478", + "stun.voys.nl:3478", + "stun.voztele.com:3478", + "stun.webcalldirect.com:3478", + "stun.zadarma.com:3478", + "stun1.l.google.com:19302", + "stun2.l.google.com:19302", + "stun3.l.google.com:19302", + "stun4.l.google.com:19302", + "stun.nextcloud.com:443", + "relay.webwormhole.io:3478", + }; + const MAPPED_ADDRESS: u16 = 0x0001; + const XOR_MAPPED_ADDRESS: u16 = 0x0020; + const MAGIC_COOKIE = [_]u8{0x21, 0x12, 0xA4, 0x42}; + + fn requestAddress(connection: *ConnectionManager) Address { + var oldAddress: ?Address = null; + var seed: [std.Random.DefaultCsprng.secret_seed_length]u8 = @splat(0); + std.mem.writeInt(i128, seed[0..16], main.timestamp().toMilliseconds(), builtin.cpu.arch.endian()); // Not the best seed, but it's not that important. + var random = std.Random.DefaultCsprng.init(seed); + for(0..16) |_| { + // Choose a somewhat random server, so we faster notice if any one of them stopped working. + const server = ipServerList[random.random().intRangeAtMost(usize, 0, ipServerList.len - 1)]; + var data = [_]u8{ + 0x00, 0x01, // message type + 0x00, 0x00, // message length + MAGIC_COOKIE[0], MAGIC_COOKIE[1], MAGIC_COOKIE[2], MAGIC_COOKIE[3], // "Magic cookie" + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // transaction ID + }; + random.fill(data[8..]); // Fill the transaction ID. + + var splitter = std.mem.splitScalar(u8, server, ':'); + const ip = splitter.first(); + const serverAddress = Address{ + .ip = Socket.resolveIP(ip) catch |err| { + std.log.warn("Cannot resolve STUN server address: {s}, error: {s}", .{ip, @errorName(err)}); + continue; + }, + .port = std.fmt.parseUnsigned(u16, splitter.rest(), 10) catch 3478, + }; + if(connection.sendRequest(main.globalAllocator, &data, serverAddress, 500*1000000)) |answer| { + defer main.globalAllocator.free(answer); + verifyHeader(answer, data[8..20]) catch |err| { + std.log.err("Header verification failed with {s} for STUN server: {s} data: {any}", .{@errorName(err), server, answer}); + continue; + }; + var result = findIPPort(answer) catch |err| { + std.log.err("Could not parse IP+Port: {s} for STUN server: {s} data: {any}", .{@errorName(err), server, answer}); + continue; + }; + if(oldAddress) |other| { + std.log.info("{f}", .{result}); + if(other.ip == result.ip and other.port == result.port) { + return result; + } else { + result.isSymmetricNAT = true; + return result; + } + } else { + oldAddress = result; + } + } else { + std.log.warn("Couldn't reach STUN server: {s}", .{server}); + } + } + return Address{.ip = Socket.resolveIP("127.0.0.1") catch unreachable, .port = settings.defaultPort}; // TODO: Return ip address in LAN. + } + + fn findIPPort(_data: []const u8) !Address { + var data = _data[20..]; // Skip the header. + while(data.len > 0) { + const typ = std.mem.readInt(u16, data[0..2], .big); + const len = std.mem.readInt(u16, data[2..4], .big); + data = data[4..]; + switch(typ) { + XOR_MAPPED_ADDRESS, MAPPED_ADDRESS => { + const xor = data[0]; + if(typ == MAPPED_ADDRESS and xor != 0) return error.NonZeroXORForMappedAddress; + if(data[1] == 0x01) { + var addressData: [6]u8 = data[2..8].*; + if(typ == XOR_MAPPED_ADDRESS) { + addressData[0] ^= MAGIC_COOKIE[0]; + addressData[1] ^= MAGIC_COOKIE[1]; + addressData[2] ^= MAGIC_COOKIE[0]; + addressData[3] ^= MAGIC_COOKIE[1]; + addressData[4] ^= MAGIC_COOKIE[2]; + addressData[5] ^= MAGIC_COOKIE[3]; + } + return Address{ + .port = std.mem.readInt(u16, addressData[0..2], .big), + .ip = std.mem.readInt(u32, addressData[2..6], builtin.cpu.arch.endian()), // Needs to stay in big endian → native. + }; + } else if(data[1] == 0x02) { + data = data[(len + 3) & ~@as(usize, 3) ..]; // Pad to 32 Bit. + continue; // I don't care about IPv6. + } else { + return error.UnknownAddressFamily; + } + }, + else => { + data = data[(len + 3) & ~@as(usize, 3) ..]; // Pad to 32 Bit. + }, + } + } + return error.IpPortNotFound; + } + + fn verifyHeader(data: []const u8, transactionID: []const u8) !void { + if(data[0] != 0x01 or data[1] != 0x01) return error.NotABinding; + if(@as(u16, @intCast(data[2] & 0xff))*256 + (data[3] & 0xff) != data.len - 20) return error.BadSize; + for(MAGIC_COOKIE, 0..) |cookie, i| { + if(data[i + 4] != cookie) return error.WrongCookie; + } + for(transactionID, 0..) |_, i| { + if(data[i + 8] != transactionID[i]) return error.WrongTransaction; + } + } +}; + +pub const ConnectionManager = struct { // MARK: ConnectionManager + socket: Socket = undefined, + thread: std.Thread = undefined, + threadId: std.Thread.Id = undefined, + externalAddress: Address = undefined, + online: Atomic(bool) = .init(false), + running: Atomic(bool) = .init(true), + + connections: main.List(*Connection) = undefined, + requests: main.List(*Request) = undefined, + + mutex: std.Thread.Mutex = .{}, + waitingToFinishReceive: std.Thread.Condition = std.Thread.Condition{}, + allowNewConnections: Atomic(bool) = .init(false), + + receiveBuffer: [Connection.maxMtu]u8 = undefined, + + world: ?*game.World = null, + + localPort: u16 = undefined, + + packetSendRequests: std.PriorityQueue(PacketSendRequest, void, PacketSendRequest.compare) = undefined, + + const PacketSendRequest = struct { + data: []const u8, + target: Address, + time: i64, + + fn compare(_: void, a: PacketSendRequest, b: PacketSendRequest) std.math.Order { + return std.math.order(a.time, b.time); + } + }; + + pub fn init(localPort: u16, online: bool) !*ConnectionManager { + const result: *ConnectionManager = main.globalAllocator.create(ConnectionManager); + errdefer main.globalAllocator.destroy(result); + result.* = .{}; + result.connections = .init(main.globalAllocator); + result.requests = .init(main.globalAllocator); + result.packetSendRequests = .init(main.globalAllocator.allocator, {}); + + result.localPort = localPort; + result.socket = Socket.init(localPort) catch |err| blk: { + if(err == error.AddressInUse) { + const socket = try Socket.init(0); // Use any port. + result.localPort = try socket.getPort(); + break :blk socket; + } else return err; + }; + errdefer result.socket.deinit(); + if(localPort == 0) result.localPort = try result.socket.getPort(); + + result.thread = try std.Thread.spawn(.{}, run, .{result}); + result.thread.setName("Network Thread") catch |err| std.log.err("Couldn't rename thread: {s}", .{@errorName(err)}); + if(online) { + result.makeOnline(); + } + if(main.settings.launchConfig.headlessServer) { + result.allowNewConnections.store(true, .monotonic); + } + return result; + } + + pub fn deinit(self: *ConnectionManager) void { + for(self.connections.items) |conn| { + conn.disconnect(); + } + + self.running.store(false, .monotonic); + self.thread.join(); + self.socket.deinit(); + self.connections.deinit(); + for(self.requests.items) |request| { + request.requestNotifier.signal(); + } + self.requests.deinit(); + while(self.packetSendRequests.removeOrNull()) |packet| { + main.globalAllocator.free(packet.data); + } + self.packetSendRequests.deinit(); + + main.globalAllocator.destroy(self); + } + + pub fn makeOnline(self: *ConnectionManager) void { + if(!self.online.load(.acquire)) { + self.externalAddress = stun.requestAddress(self); + self.online.store(true, .release); + } + } + + pub fn send(self: *ConnectionManager, data: []const u8, target: Address, nanoTime: ?i64) void { + if(nanoTime) |time| { + self.mutex.lock(); + defer self.mutex.unlock(); + self.packetSendRequests.add(.{ + .data = main.globalAllocator.dupe(u8, data), + .target = target, + .time = time, + }) catch unreachable; + } else { + self.socket.send(data, target); + } + } + + pub fn sendRequest(self: *ConnectionManager, allocator: NeverFailingAllocator, data: []const u8, target: Address, timeout_ns: u64) ?[]const u8 { + self.socket.send(data, target); + var request = Request{.address = target, .data = data}; + { + self.mutex.lock(); + defer self.mutex.unlock(); + self.requests.append(&request); + + request.requestNotifier.timedWait(&self.mutex, timeout_ns) catch {}; + + for(self.requests.items, 0..) |req, i| { + if(req == &request) { + _ = self.requests.swapRemove(i); + break; + } + } + } + + // The request data gets modified when a result was received. + if(request.data.ptr == data.ptr) { + return null; + } else { + if(allocator.allocator.ptr == main.globalAllocator.allocator.ptr) { + return request.data; + } else { + const result = allocator.dupe(u8, request.data); + main.globalAllocator.free(request.data); + return result; + } + } + } + + pub fn addConnection(self: *ConnectionManager, conn: *Connection) error{AlreadyConnected}!void { + self.mutex.lock(); + defer self.mutex.unlock(); + for(self.connections.items) |other| { + if(other.remoteAddress.ip == conn.remoteAddress.ip and other.remoteAddress.port == conn.remoteAddress.port) return error.AlreadyConnected; + } + self.connections.append(conn); + } + + pub fn finishCurrentReceive(self: *ConnectionManager) void { + std.debug.assert(self.threadId != std.Thread.getCurrentId()); // WOuld cause deadlock, since we are in a receive. + self.mutex.lock(); + defer self.mutex.unlock(); + self.waitingToFinishReceive.wait(&self.mutex); + } + + pub fn removeConnection(self: *ConnectionManager, conn: *Connection) void { + self.mutex.lock(); + defer self.mutex.unlock(); + + for(self.connections.items, 0..) |other, i| { + if(other == conn) { + _ = self.connections.swapRemove(i); + break; + } + } + } + + fn onReceive(self: *ConnectionManager, data: []const u8, source: Address) void { + std.debug.assert(self.threadId == std.Thread.getCurrentId()); + self.mutex.lock(); + + for(self.connections.items) |conn| { + if(conn.remoteAddress.ip == source.ip) { + if(conn.bruteforcingPort) { + conn.remoteAddress.port = source.port; + conn.bruteforcingPort = false; + } + if(conn.remoteAddress.port == source.port) { + self.mutex.unlock(); + conn.receive(data); + return; + } + } + } + { + defer self.mutex.unlock(); + // Check if it's part of an active request: + for(self.requests.items) |request| { + if(request.address.ip == source.ip and request.address.port == source.port) { + request.data = main.globalAllocator.dupe(u8, data); + request.requestNotifier.signal(); + return; + } + } + if(self.online.load(.acquire) and source.ip == self.externalAddress.ip and source.port == self.externalAddress.port) return; + } + if(self.allowNewConnections.load(.monotonic) or source.ip == Address.localHost) { + if(data.len != 0 and data[0] == @intFromEnum(Connection.ChannelId.init)) { + const ip = std.fmt.allocPrint(main.stackAllocator.allocator, "{f}", .{source}) catch unreachable; + defer main.stackAllocator.free(ip); + const user = main.server.User.initAndIncreaseRefCount(main.server.connectionManager, ip) catch |err| { + std.log.err("Cannot connect user from external IP {f}: {s}", .{source, @errorName(err)}); + return; + }; + user.decreaseRefCount(); + } + } else { + // TODO: Reduce the number of false alarms in the short period after a disconnect. + std.log.warn("Unknown connection from address: {f}", .{source}); + std.log.debug("Message: {any}", .{data}); + } + } + + pub fn run(self: *ConnectionManager) void { + self.threadId = std.Thread.getCurrentId(); + main.initThreadLocals(); + defer main.deinitThreadLocals(); + + var lastTime: i64 = networkTimestamp(); + while(self.running.load(.monotonic)) { + main.heap.GarbageCollection.syncPoint(); + self.waitingToFinishReceive.broadcast(); + var source: Address = undefined; + if(self.socket.receive(&self.receiveBuffer, 1, &source)) |data| { + self.onReceive(data, source); + } else |err| { + if(err == error.Timeout) { + // No message within the last ~100 ms. + } else if(err == error.ConnectionResetByPeer) { + std.log.err("Got error.ConnectionResetByPeer on receive. This indicates that a previous message did not find a valid destination.", .{}); + } else { + std.log.err("Got error on receive: {s}", .{@errorName(err)}); + @panic("Network failed."); + } + } + const curTime: i64 = networkTimestamp(); + { + self.mutex.lock(); + defer self.mutex.unlock(); + while(self.packetSendRequests.peek() != null and self.packetSendRequests.peek().?.time -% curTime <= 0) { + const packet = self.packetSendRequests.remove(); + self.socket.send(packet.data, packet.target); + main.globalAllocator.free(packet.data); + } + } + + // Send packets roughly every 1 ms: + if(curTime -% lastTime > 1*ms) { + lastTime = curTime; + var i: u32 = 0; + self.mutex.lock(); + defer self.mutex.unlock(); + while(i < self.connections.items.len) { + var conn = self.connections.items[i]; + self.mutex.unlock(); + conn.processNextPackets(); + self.mutex.lock(); + i += 1; + } + if(self.connections.items.len == 0 and self.online.load(.acquire)) { + // Send a message to external ip, to keep the port open: + const data = [1]u8{0}; + self.socket.send(&data, self.externalAddress); + } + } + } + } +}; + +const UnconfirmedPacket = struct { + data: []const u8, + lastKeepAliveSentBefore: u32, + id: u32, +}; + +pub const Connection = struct { // MARK: Connection + const maxMtu: u32 = 65507; // max udp packet size + const importantHeaderSize: u32 = 5; + const minMtu: u32 = 576 - 20 - 8; // IPv4 MTU minus IP header minus udp header + const headerOverhead = 20 + 8 + 42; // IP Header + UDP Header + Ethernet header/footer + const congestionControl_historySize = 16; + const congestionControl_historyMask = congestionControl_historySize - 1; + const minimumBandWidth = 10_000; + + const receiveBufferSize = 8 << 20; + + // Statistics: + pub var packetsSent: Atomic(u32) = .init(0); + pub var packetsResent: Atomic(u32) = .init(0); + pub var internalMessageOverhead: Atomic(usize) = .init(0); + pub var internalHeaderOverhead: Atomic(usize) = .init(0); + pub var externalHeaderOverhead: Atomic(usize) = .init(0); + + const SequenceIndex = i32; + + const LossStatus = enum { + noLoss, + singleLoss, + doubleLoss, + }; + + const RangeBuffer = struct { // MARK: RangeBuffer + const Range = struct { + start: SequenceIndex, + len: SequenceIndex, + + fn end(self: Range) SequenceIndex { + return self.start +% self.len; + } + }; + ranges: main.ListUnmanaged(Range), + + pub fn init() RangeBuffer { + return .{ + .ranges = .{}, + }; + } + + pub fn clear(self: *RangeBuffer) void { + self.ranges.clearRetainingCapacity(); + } + + pub fn deinit(self: RangeBuffer, allocator: NeverFailingAllocator) void { + self.ranges.deinit(allocator); + } + + pub fn addRange(self: *RangeBuffer, allocator: NeverFailingAllocator, range: Range) void { + if(self.hasRange(range)) return; + var startRange: ?Range = null; + var endRange: ?Range = null; + var i: usize = 0; + while(i < self.ranges.items.len) { + const other = self.ranges.items[i]; + if(range.start -% other.start <= 0 and range.end() -% other.end() >= 0) { + _ = self.ranges.swapRemove(i); + continue; + } + if(range.start -% other.end() <= 0 and range.start -% other.start >= 0) { + _ = self.ranges.swapRemove(i); + startRange = other; + continue; + } + if(range.end() -% other.start >= 0 and range.end() -% other.end() <= 0) { + _ = self.ranges.swapRemove(i); + endRange = other; + continue; + } + i += 1; + } + var mergedRange = range; + if(startRange) |start| { + mergedRange.start = start.start; + mergedRange.len = range.end() -% mergedRange.start; + } + if(endRange) |end| { + mergedRange.len = end.end() -% mergedRange.start; + } + self.ranges.append(allocator, mergedRange); + } + + pub fn hasRange(self: *RangeBuffer, range: Range) bool { + for(self.ranges.items) |other| { + if(range.start -% other.start >= 0 and range.end() -% other.end() <= 0) { + return true; + } + } + return false; + } + + pub fn extractFirstRange(self: *RangeBuffer) ?Range { + if(self.ranges.items.len == 0) return null; + var firstRange = self.ranges.items[0]; + var index: usize = 0; + for(self.ranges.items[1..], 1..) |range, i| { + if(range.start -% firstRange.start < 0) { + firstRange = range; + index = i; + } + } + _ = self.ranges.swapRemove(index); + return firstRange; + } + }; + + const ReceiveBuffer = struct { // MARK: ReceiveBuffer + const Range = struct { + start: SequenceIndex, + len: SequenceIndex, + }; + const Header = struct { + protocolIndex: u8, + size: u32, + }; + ranges: RangeBuffer, + availablePosition: SequenceIndex = undefined, + currentReadPosition: SequenceIndex = undefined, + buffer: main.utils.FixedSizeCircularBuffer(u8, receiveBufferSize), + header: ?Header = null, + protocolBuffer: main.ListUnmanaged(u8) = .{}, + + pub fn init() ReceiveBuffer { + return .{ + .ranges = .init(), + .buffer = .init(main.globalAllocator), + }; + } + + pub fn deinit(self: ReceiveBuffer) void { + self.ranges.deinit(main.globalAllocator); + self.protocolBuffer.deinit(main.globalAllocator); + self.buffer.deinit(main.globalAllocator); + } + + fn applyRanges(self: *ReceiveBuffer) void { + const range = self.ranges.extractFirstRange() orelse unreachable; + std.debug.assert(range.start == self.availablePosition); + self.availablePosition = range.end(); + } + + fn getHeaderInformation(self: *ReceiveBuffer) !?Header { + if(self.currentReadPosition == self.availablePosition) return null; + var header: Header = .{ + .protocolIndex = self.buffer.getAtOffset(0) orelse unreachable, + .size = 0, + }; + var i: u8 = 1; + while(true) : (i += 1) { + if(self.currentReadPosition +% i == self.availablePosition) return null; + const nextByte = self.buffer.getAtOffset(i) orelse unreachable; + header.size = header.size << 7 | (nextByte & 0x7f); + if(nextByte & 0x80 == 0) break; + if(header.size > std.math.maxInt(@TypeOf(header.size)) >> 7) return error.Invalid; + } + self.buffer.discardElementsFront(i + 1); + self.currentReadPosition +%= @intCast(i + 1); + return header; + } + + fn collectRangesAndExecuteProtocols(self: *ReceiveBuffer, conn: *Connection) !void { + self.applyRanges(); + while(true) { + if(self.header == null) { + self.header = try self.getHeaderInformation() orelse return; + self.protocolBuffer.ensureCapacity(main.globalAllocator, self.header.?.size); + } + const amount = @min(@as(usize, @intCast(self.availablePosition -% self.currentReadPosition)), self.header.?.size - self.protocolBuffer.items.len); + if(self.availablePosition -% self.currentReadPosition == 0) return; + + self.buffer.popSliceFront(self.protocolBuffer.addManyAssumeCapacity(amount)) catch unreachable; + self.currentReadPosition +%= @intCast(amount); + if(self.protocolBuffer.items.len != self.header.?.size) return; + + const protocolIndex = self.header.?.protocolIndex; + self.header = null; + try protocols.onReceive(conn, protocolIndex, self.protocolBuffer.items); + self.protocolBuffer.clearRetainingCapacity(); + if(self.protocolBuffer.items.len > 1 << 24) { + self.protocolBuffer.shrinkAndFree(main.globalAllocator, 1 << 24); + } + } + } + + const ReceiveStatus = enum { + accepted, + rejected, + }; + + pub fn receive(self: *ReceiveBuffer, conn: *Connection, start: SequenceIndex, data: []const u8) !ReceiveStatus { + const len: SequenceIndex = @intCast(data.len); + if(start -% self.availablePosition < 0) return .accepted; // We accepted it in the past. + const offset: usize = @intCast(start -% self.currentReadPosition); + self.buffer.insertSliceAtOffset(data, offset) catch return .rejected; + self.ranges.addRange(main.globalAllocator, .{.start = start, .len = len}); + if(start == self.availablePosition) { + try self.collectRangesAndExecuteProtocols(conn); + } + return .accepted; + } + }; + + const SendBuffer = struct { // MARK: SendBuffer + const Range = struct { + start: SequenceIndex, + len: SequenceIndex, + timestamp: i64, + wasResent: bool = false, + wasResentAsFirstPacket: bool = false, + considerForCongestionControl: bool, + + fn compareTime(_: void, a: Range, b: Range) std.math.Order { + if(a.timestamp == b.timestamp) return .eq; + if(a.timestamp -% b.timestamp > 0) return .gt; + return .lt; + } + }; + unconfirmedRanges: std.PriorityQueue(Range, void, Range.compareTime), + lostRanges: main.utils.CircularBufferQueue(Range), + buffer: main.utils.CircularBufferQueue(u8), + fullyConfirmedIndex: SequenceIndex, + highestSentIndex: SequenceIndex, + nextIndex: SequenceIndex, + lastUnsentTime: i64, + + pub fn init(index: SequenceIndex) SendBuffer { + return .{ + .unconfirmedRanges = .init(main.globalAllocator.allocator, {}), + .lostRanges = .init(main.globalAllocator, 1 << 10), + .buffer = .init(main.globalAllocator, 1 << 20), + .fullyConfirmedIndex = index, + .highestSentIndex = index, + .nextIndex = index, + .lastUnsentTime = networkTimestamp(), + }; + } + + pub fn deinit(self: SendBuffer) void { + self.unconfirmedRanges.deinit(); + self.lostRanges.deinit(); + self.buffer.deinit(); + } + + pub fn insertMessage(self: *SendBuffer, protocolIndex: u8, data: []const u8, time: i64) !void { + if(self.highestSentIndex == self.fullyConfirmedIndex) { + self.lastUnsentTime = time; + } + if(data.len + self.buffer.len > std.math.maxInt(SequenceIndex)) return error.OutOfMemory; + self.buffer.pushBack(protocolIndex); + self.nextIndex +%= 1; + _ = internalHeaderOverhead.fetchAdd(1, .monotonic); + const bits = 1 + if(data.len == 0) 0 else std.math.log2_int(usize, data.len); + const bytes = std.math.divCeil(usize, bits, 7) catch unreachable; + for(0..bytes) |i| { + const shift = 7*(bytes - i - 1); + const byte = (data.len >> @intCast(shift) & 0x7f) | if(i == bytes - 1) @as(u8, 0) else 0x80; + self.buffer.pushBack(@intCast(byte)); + self.nextIndex +%= 1; + _ = internalHeaderOverhead.fetchAdd(1, .monotonic); + } + self.buffer.pushBackSlice(data); + self.nextIndex +%= @intCast(data.len); + } + + const ReceiveConfirmationResult = struct { + timestamp: i64, + packetLen: SequenceIndex, + considerForCongestionControl: bool, + }; + + pub fn receiveConfirmationAndGetTimestamp(self: *SendBuffer, start: SequenceIndex) ?ReceiveConfirmationResult { + var result: ?ReceiveConfirmationResult = null; + for(self.unconfirmedRanges.items, 0..) |range, i| { + if(range.start == start) { + result = .{ + .timestamp = range.timestamp, + .considerForCongestionControl = range.considerForCongestionControl, + .packetLen = range.len, + }; + _ = self.unconfirmedRanges.removeIndex(i); + break; + } + } + var smallestUnconfirmed = self.highestSentIndex; + for(self.unconfirmedRanges.items) |range| { + if(smallestUnconfirmed -% range.start > 0) { + smallestUnconfirmed = range.start; + } + } + for(0..self.lostRanges.len) |i| { + const range = self.lostRanges.getAtOffset(i) catch unreachable; + if(smallestUnconfirmed -% range.start > 0) { + smallestUnconfirmed = range.start; + } + } + self.buffer.discardFront(@intCast(smallestUnconfirmed -% self.fullyConfirmedIndex)) catch unreachable; + self.fullyConfirmedIndex = smallestUnconfirmed; + return result; + } + + pub fn checkForLosses(self: *SendBuffer, time: i64, retransmissionTimeout: i64) LossStatus { + var hadLoss: bool = false; + var hadDoubleLoss: bool = false; + while(true) { + var range = self.unconfirmedRanges.peek() orelse break; + if(range.timestamp +% retransmissionTimeout -% time >= 0) break; + _ = self.unconfirmedRanges.remove(); + if(self.fullyConfirmedIndex == range.start) { + // In TCP effectively only the second loss of the lowest unconfirmed packet is counted for congestion control + // This decreases the chance of triggering congestion control from random packet loss + if(range.wasResentAsFirstPacket) hadDoubleLoss = true; + hadLoss = true; + range.wasResentAsFirstPacket = true; + } + range.wasResent = true; + self.lostRanges.pushBack(range); + _ = packetsResent.fetchAdd(1, .monotonic); + } + if(hadDoubleLoss) return .doubleLoss; + if(hadLoss) return .singleLoss; + return .noLoss; + } + + pub fn getNextPacketToSend(self: *SendBuffer, byteIndex: *SequenceIndex, buf: []u8, time: i64, considerForCongestionControl: bool, allowedDelay: i64) ?usize { + self.unconfirmedRanges.ensureUnusedCapacity(1) catch unreachable; + // Resend old packet: + if(self.lostRanges.popFront()) |_range| { + var range = _range; + if(range.len > buf.len) { // MTU changed → split the data + self.lostRanges.pushFront(.{ + .start = range.start +% @as(SequenceIndex, @intCast(buf.len)), + .len = range.len - @as(SequenceIndex, @intCast(buf.len)), + .timestamp = range.timestamp, + .considerForCongestionControl = range.considerForCongestionControl, + }); + range.len = @intCast(buf.len); + } + + self.buffer.getSliceAtOffset(@intCast(range.start -% self.fullyConfirmedIndex), buf[0..@intCast(range.len)]) catch unreachable; + range.timestamp = time; + byteIndex.* = range.start; + self.unconfirmedRanges.add(range) catch unreachable; + return @intCast(range.len); + } + + if(self.highestSentIndex == self.nextIndex) return null; + if(self.highestSentIndex +% @as(i32, @intCast(buf.len)) -% self.fullyConfirmedIndex > receiveBufferSize) return null; + // Send new packet: + const len: SequenceIndex = @min(self.nextIndex -% self.highestSentIndex, @as(i32, @intCast(buf.len))); + if(len < buf.len and time -% self.lastUnsentTime < allowedDelay) return null; + + self.buffer.getSliceAtOffset(@intCast(self.highestSentIndex -% self.fullyConfirmedIndex), buf[0..@intCast(len)]) catch unreachable; + byteIndex.* = self.highestSentIndex; + self.unconfirmedRanges.add(.{ + .start = self.highestSentIndex, + .len = len, + .timestamp = time, + .considerForCongestionControl = considerForCongestionControl, + }) catch unreachable; + self.highestSentIndex +%= len; + return @intCast(len); + } + }; + + const Channel = struct { // MARK: Channel + receiveBuffer: ReceiveBuffer, + sendBuffer: SendBuffer, + allowedDelay: i64, + channelId: ChannelId, + + pub fn init(sequenceIndex: SequenceIndex, delay: i64, id: ChannelId) Channel { + return .{ + .receiveBuffer = .init(), + .sendBuffer = .init(sequenceIndex), + .allowedDelay = delay, + .channelId = id, + }; + } + + pub fn deinit(self: *Channel) void { + self.receiveBuffer.deinit(); + self.sendBuffer.deinit(); + } + + pub fn connect(self: *Channel, remoteStart: SequenceIndex) void { + std.debug.assert(self.receiveBuffer.buffer.len == 0); + self.receiveBuffer.availablePosition = remoteStart; + self.receiveBuffer.currentReadPosition = remoteStart; + } + + pub fn receive(self: *Channel, conn: *Connection, start: SequenceIndex, data: []const u8) !ReceiveBuffer.ReceiveStatus { + return self.receiveBuffer.receive(conn, start, data); + } + + pub fn send(self: *Channel, protocolIndex: u8, data: []const u8, time: i64) !void { + return self.sendBuffer.insertMessage(protocolIndex, data, time); + } + + pub fn receiveConfirmationAndGetTimestamp(self: *Channel, start: SequenceIndex) ?SendBuffer.ReceiveConfirmationResult { + return self.sendBuffer.receiveConfirmationAndGetTimestamp(start); + } + + pub fn checkForLosses(self: *Channel, conn: *Connection, time: i64) LossStatus { + const retransmissionTimeout: i64 = @intFromFloat(conn.rttEstimate + 3*conn.rttUncertainty + @as(f32, @floatFromInt(self.allowedDelay))); + return self.sendBuffer.checkForLosses(time, retransmissionTimeout); + } + + pub fn sendNextPacketAndGetSize(self: *Channel, conn: *Connection, time: i64, considerForCongestionControl: bool) ?usize { + var writer = utils.BinaryWriter.initCapacity(main.stackAllocator, conn.mtuEstimate); + defer writer.deinit(); + + writer.writeEnum(ChannelId, self.channelId); + + var byteIndex: SequenceIndex = undefined; + const packetLen = self.sendBuffer.getNextPacketToSend(&byteIndex, writer.data.items.ptr[5..writer.data.capacity], time, considerForCongestionControl, self.allowedDelay) orelse return null; + writer.writeInt(SequenceIndex, byteIndex); + _ = internalHeaderOverhead.fetchAdd(5, .monotonic); + _ = externalHeaderOverhead.fetchAdd(headerOverhead, .monotonic); + writer.data.items.len += packetLen; + + _ = packetsSent.fetchAdd(1, .monotonic); + conn.manager.send(writer.data.items, conn.remoteAddress, null); + return writer.data.items.len; + } + + pub fn getStatistics(self: *Channel, unconfirmed: *usize, queued: *usize) void { + for(self.sendBuffer.unconfirmedRanges.items) |range| { + unconfirmed.* += @intCast(range.len); + } + queued.* = @intCast(self.sendBuffer.nextIndex -% self.sendBuffer.highestSentIndex); + } + }; + + const ChannelId = enum(u8) { // MARK: ChannelId + lossy = 0, + fast = 1, + slow = 2, + confirmation = 3, + init = 4, + keepalive = 5, + disconnect = 6, + }; + + const ConfirmationData = struct { + channel: ChannelId, + start: SequenceIndex, + receiveTimeStamp: i64, + }; + + const ConnectionState = enum(u8) { + awaitingClientConnection, + awaitingServerResponse, + awaitingClientAcknowledgement, + connected, + disconnectDesired, + }; + + pub const HandShakeState = enum(u8) { + start = 0, + userData = 1, + assets = 2, + serverData = 3, + complete = 255, + }; + + // MARK: fields + + manager: *ConnectionManager, + user: ?*main.server.User, + + remoteAddress: Address, + bruteforcingPort: bool = false, + bruteForcedPortRange: u16 = 0, + + lossyChannel: Channel, // TODO: Actually allow it to be lossy + fastChannel: Channel, + slowChannel: Channel, + + hasRttEstimate: bool = false, + rttEstimate: f32 = 1000*ms, + rttUncertainty: f32 = 0.0, + lastRttSampleTime: i64, + nextPacketTimestamp: i64, + nextConfirmationTimestamp: i64, + queuedConfirmations: main.utils.CircularBufferQueue(ConfirmationData), + mtuEstimate: u16 = minMtu, + + bandwidthEstimateInBytesPerRtt: f32 = minMtu, + slowStart: bool = true, + relativeSendTime: i64 = 0, + relativeIdleTime: i64 = 0, + + connectionState: Atomic(ConnectionState), + handShakeState: Atomic(HandShakeState) = .init(.start), + handShakeWaiting: std.Thread.Condition = std.Thread.Condition{}, + lastConnection: i64, + + // To distinguish different connections from the same computer to avoid multiple reconnects + connectionIdentifier: i64, + remoteConnectionIdentifier: i64, + + mutex: std.Thread.Mutex = .{}, + + pub fn init(manager: *ConnectionManager, ipPort: []const u8, user: ?*main.server.User) !*Connection { + const result: *Connection = main.globalAllocator.create(Connection); + errdefer main.globalAllocator.destroy(result); + result.* = Connection{ + .manager = manager, + .user = user, + .remoteAddress = undefined, + .connectionState = .init(if(user != null) .awaitingClientConnection else .awaitingServerResponse), + .lastConnection = networkTimestamp(), + .nextPacketTimestamp = networkTimestamp(), + .nextConfirmationTimestamp = networkTimestamp(), + .lastRttSampleTime = networkTimestamp() -% 10_000*ms, + .queuedConfirmations = .init(main.globalAllocator, 1024), + .lossyChannel = .init(main.random.nextInt(SequenceIndex, &main.seed), 1*ms, .lossy), + .fastChannel = .init(main.random.nextInt(SequenceIndex, &main.seed), 10*ms, .fast), + .slowChannel = .init(main.random.nextInt(SequenceIndex, &main.seed), 100*ms, .slow), + .connectionIdentifier = networkTimestamp(), + .remoteConnectionIdentifier = 0, + }; + errdefer { + result.lossyChannel.deinit(); + result.fastChannel.deinit(); + result.slowChannel.deinit(); + result.queuedConfirmations.deinit(); + } + if(result.connectionIdentifier == 0) result.connectionIdentifier = 1; + + var splitter = std.mem.splitScalar(u8, ipPort, ':'); + const ip = splitter.first(); + result.remoteAddress.ip = try Socket.resolveIP(ip); + var port = splitter.rest(); + if(port.len != 0 and port[0] == '?') { + result.remoteAddress.isSymmetricNAT = true; + result.bruteforcingPort = true; + port = port[1..]; + } + result.remoteAddress.port = std.fmt.parseUnsigned(u16, port, 10) catch blk: { + if(ip.len != ipPort.len) std.log.err("Could not parse port \"{s}\". Using default port instead.", .{port}); + break :blk settings.defaultPort; + }; + + try result.manager.addConnection(result); + return result; + } + + pub fn deinit(self: *Connection) void { + self.disconnect(); + self.manager.finishCurrentReceive(); // Wait until all currently received packets are done. + self.lossyChannel.deinit(); + self.fastChannel.deinit(); + self.slowChannel.deinit(); + self.queuedConfirmations.deinit(); + main.globalAllocator.destroy(self); + } + + pub fn send(self: *Connection, comptime channel: ChannelId, protocolIndex: u8, data: []const u8) void { + _ = protocols.bytesSent[protocolIndex].fetchAdd(data.len, .monotonic); + self.mutex.lock(); + defer self.mutex.unlock(); + + _ = switch(channel) { + .lossy => self.lossyChannel.send(protocolIndex, data, networkTimestamp()), + .fast => self.fastChannel.send(protocolIndex, data, networkTimestamp()), + .slow => self.slowChannel.send(protocolIndex, data, networkTimestamp()), + else => comptime unreachable, + } catch { + std.log.err("Cannot send any more packets. Disconnecting", .{}); + self.disconnect(); + }; + } + + pub fn isConnected(self: *Connection) bool { + self.mutex.lock(); + defer self.mutex.unlock(); + + return self.connectionState.load(.unordered) == .connected; + } + + pub fn isServerSide(conn: *Connection) bool { + return conn.user != null; + } + + fn handlePacketLoss(self: *Connection, loss: LossStatus) void { + if(loss == .noLoss) return; + self.slowStart = false; + if(loss == .doubleLoss) { + self.rttEstimate *= 1.5; + self.bandwidthEstimateInBytesPerRtt /= 2; + self.bandwidthEstimateInBytesPerRtt = @max(self.bandwidthEstimateInBytesPerRtt, minMtu); + } + } + + fn increaseCongestionBandwidth(self: *Connection, packetLen: SequenceIndex) void { + const fullPacketLen: f32 = @floatFromInt(packetLen + headerOverhead); + if(self.slowStart) { + self.bandwidthEstimateInBytesPerRtt += fullPacketLen; + } else { + self.bandwidthEstimateInBytesPerRtt += fullPacketLen/self.bandwidthEstimateInBytesPerRtt*@as(f32, @floatFromInt(self.mtuEstimate)) + fullPacketLen/100.0; + } + } + + fn receiveConfirmationPacket(self: *Connection, reader: *utils.BinaryReader, timestamp: i64) !void { + self.mutex.lock(); + defer self.mutex.unlock(); + + var minRtt: f32 = std.math.floatMax(f32); + var maxRtt: f32 = 1000; + var sumRtt: f32 = 0; + var numRtt: f32 = 0; + while(reader.remaining.len != 0) { + const channel = try reader.readEnum(ChannelId); + const timeOffset = 2*@as(i64, try reader.readInt(u16)); + const start = try reader.readInt(SequenceIndex); + const confirmationResult = switch(channel) { + .lossy => self.lossyChannel.receiveConfirmationAndGetTimestamp(start) orelse continue, + .fast => self.fastChannel.receiveConfirmationAndGetTimestamp(start) orelse continue, + .slow => self.slowChannel.receiveConfirmationAndGetTimestamp(start) orelse continue, + else => return error.Invalid, + }; + const rtt: f32 = @floatFromInt(@max(1, timestamp -% confirmationResult.timestamp -% timeOffset)); + numRtt += 1; + sumRtt += rtt; + minRtt = @min(minRtt, rtt); + maxRtt = @max(maxRtt, rtt); + if(confirmationResult.considerForCongestionControl) { + self.increaseCongestionBandwidth(confirmationResult.packetLen); + } + } + if(numRtt > 0) { + // Taken mostly from RFC 6298 with some minor changes + const averageRtt = sumRtt/numRtt; + const largestDifference = @max(maxRtt - averageRtt, averageRtt - minRtt, @abs(maxRtt - self.rttEstimate), @abs(self.rttEstimate - minRtt)); + const timeDifference: f32 = @floatFromInt(timestamp -% self.lastRttSampleTime); + const alpha = 1.0 - std.math.pow(f32, 7.0/8.0, timeDifference/self.rttEstimate); + const beta = 1.0 - std.math.pow(f32, 3.0/4.0, timeDifference/self.rttEstimate); + self.rttEstimate = (1 - alpha)*self.rttEstimate + alpha*averageRtt; + self.rttUncertainty = (1 - beta)*self.rttUncertainty + beta*largestDifference; + self.lastRttSampleTime = timestamp; + if(!self.hasRttEstimate) { // Kill the 1 second delay caused by the first packet + self.nextPacketTimestamp = timestamp; + self.hasRttEstimate = true; + } + } + } + + fn sendConfirmationPacket(self: *Connection, timestamp: i64) void { + std.debug.assert(self.manager.threadId == std.Thread.getCurrentId()); + var writer = utils.BinaryWriter.initCapacity(main.stackAllocator, self.mtuEstimate); + defer writer.deinit(); + + writer.writeEnum(ChannelId, .confirmation); + + while(self.queuedConfirmations.popFront()) |confirmation| { + writer.writeEnum(ChannelId, confirmation.channel); + writer.writeInt(u16, std.math.lossyCast(u16, @divTrunc(timestamp -% confirmation.receiveTimeStamp, 2))); + writer.writeInt(SequenceIndex, confirmation.start); + if(writer.data.capacity - writer.data.items.len < @sizeOf(ChannelId) + @sizeOf(u16) + @sizeOf(SequenceIndex)) break; + } + + _ = internalMessageOverhead.fetchAdd(writer.data.items.len + headerOverhead, .monotonic); + self.manager.send(writer.data.items, self.remoteAddress, null); + } + + pub fn receive(self: *Connection, data: []const u8) void { + self.tryReceive(data) catch |err| { + std.log.err("Got error while processing received network data: {s}", .{@errorName(err)}); + if(@errorReturnTrace()) |trace| { + std.log.info("{f}", .{std.debug.FormatStackTrace{.stack_trace = trace.*, .tty_config = .no_color}}); + } + std.log.debug("Packet data: {any}", .{data}); + self.disconnect(); + }; + } + + fn tryReceive(self: *Connection, data: []const u8) !void { + std.debug.assert(self.manager.threadId == std.Thread.getCurrentId()); + var reader = utils.BinaryReader.init(data); + const channel = try reader.readEnum(ChannelId); + if(channel == .init) { + const remoteConnectionIdentifier = try reader.readInt(i64); + const isAcknowledgement = reader.remaining.len == 0; + if(isAcknowledgement) { + switch(self.connectionState.load(.monotonic)) { + .awaitingClientAcknowledgement => { + if(self.remoteConnectionIdentifier == remoteConnectionIdentifier) { + _ = self.connectionState.cmpxchgStrong(.awaitingClientAcknowledgement, .connected, .monotonic, .monotonic); + } + }, + else => {}, + } + return; + } + const lossyStart = try reader.readInt(SequenceIndex); + const fastStart = try reader.readInt(SequenceIndex); + const slowStart = try reader.readInt(SequenceIndex); + switch(self.connectionState.load(.monotonic)) { + .awaitingClientConnection => { + self.lossyChannel.connect(lossyStart); + self.fastChannel.connect(fastStart); + self.slowChannel.connect(slowStart); + _ = self.connectionState.cmpxchgStrong(.awaitingClientConnection, .awaitingClientAcknowledgement, .monotonic, .monotonic); + self.remoteConnectionIdentifier = remoteConnectionIdentifier; + }, + .awaitingServerResponse => { + self.lossyChannel.connect(lossyStart); + self.fastChannel.connect(fastStart); + self.slowChannel.connect(slowStart); + _ = self.connectionState.cmpxchgStrong(.awaitingServerResponse, .connected, .monotonic, .monotonic); + self.remoteConnectionIdentifier = remoteConnectionIdentifier; + }, + .awaitingClientAcknowledgement => {}, + .connected => { + if(self.remoteConnectionIdentifier != remoteConnectionIdentifier) { // Reconnection attempt + if(self.user) |user| { + self.manager.removeConnection(self); + main.server.disconnect(user); + } else { + std.log.err("Server reconnected?", .{}); + self.disconnect(); + } + return; + } + }, + .disconnectDesired => {}, + } + // Acknowledge the packet on the client: + if(self.user == null) { + var writer = utils.BinaryWriter.initCapacity(main.stackAllocator, 1 + @sizeOf(i64)); + defer writer.deinit(); + + writer.writeEnum(ChannelId, .init); + writer.writeInt(i64, self.connectionIdentifier); + + _ = internalMessageOverhead.fetchAdd(writer.data.items.len + headerOverhead, .monotonic); + self.manager.send(writer.data.items, self.remoteAddress, null); + } + return; + } + if(self.connectionState.load(.monotonic) != .connected) return; // Reject all non-handshake packets until the handshake is done. + switch(channel) { + .lossy => { + const start = try reader.readInt(SequenceIndex); + if(try self.lossyChannel.receive(self, start, reader.remaining) == .accepted) { + self.queuedConfirmations.pushBack(.{ + .channel = channel, + .start = start, + .receiveTimeStamp = networkTimestamp(), + }); + } + }, + .fast => { + const start = try reader.readInt(SequenceIndex); + if(try self.fastChannel.receive(self, start, reader.remaining) == .accepted) { + self.queuedConfirmations.pushBack(.{ + .channel = channel, + .start = start, + .receiveTimeStamp = networkTimestamp(), + }); + } + }, + .slow => { + const start = try reader.readInt(SequenceIndex); + if(try self.slowChannel.receive(self, start, reader.remaining) == .accepted) { + self.queuedConfirmations.pushBack(.{ + .channel = channel, + .start = start, + .receiveTimeStamp = networkTimestamp(), + }); + } + }, + .confirmation => { + try self.receiveConfirmationPacket(&reader, networkTimestamp()); + }, + .init => unreachable, + .keepalive => {}, + .disconnect => { + self.disconnect(); + }, + } + self.lastConnection = networkTimestamp(); + + // TODO: Packet statistics + } + + pub fn processNextPackets(self: *Connection) void { + const timestamp = networkTimestamp(); + + switch(self.connectionState.load(.monotonic)) { + .awaitingClientConnection => { + if(timestamp -% self.nextPacketTimestamp < 0) return; + self.nextPacketTimestamp = timestamp +% 100*ms; + self.manager.send(&.{@intFromEnum(ChannelId.keepalive)}, self.remoteAddress, null); + }, + .awaitingServerResponse, .awaitingClientAcknowledgement => { + // Send the initial packet once every 100 ms. + if(timestamp -% self.nextPacketTimestamp < 0) return; + self.nextPacketTimestamp = timestamp +% 100*ms; + var writer = utils.BinaryWriter.initCapacity(main.stackAllocator, 1 + @sizeOf(i64) + 3*@sizeOf(SequenceIndex)); + defer writer.deinit(); + + writer.writeEnum(ChannelId, .init); + writer.writeInt(i64, self.connectionIdentifier); + writer.writeInt(SequenceIndex, self.lossyChannel.sendBuffer.fullyConfirmedIndex); + writer.writeInt(SequenceIndex, self.fastChannel.sendBuffer.fullyConfirmedIndex); + writer.writeInt(SequenceIndex, self.slowChannel.sendBuffer.fullyConfirmedIndex); + _ = internalMessageOverhead.fetchAdd(writer.data.items.len + headerOverhead, .monotonic); + self.manager.send(writer.data.items, self.remoteAddress, null); + return; + }, + .connected => { + if(timestamp -% self.lastConnection -% settings.connectionTimeout > 0) { + std.log.info("timeout", .{}); + self.disconnect(); + return; + } + }, + .disconnectDesired => return, + } + + self.handlePacketLoss(self.lossyChannel.checkForLosses(self, timestamp)); + self.handlePacketLoss(self.fastChannel.checkForLosses(self, timestamp)); + self.handlePacketLoss(self.slowChannel.checkForLosses(self, timestamp)); + + // We don't want to send too many packets at once if there was a period of no traffic. + if(timestamp -% 10*ms -% self.nextPacketTimestamp > 0) { + self.relativeIdleTime += timestamp -% 10*ms -% self.nextPacketTimestamp; + self.nextPacketTimestamp = timestamp -% 10*ms; + } + + if(self.relativeIdleTime + self.relativeSendTime > @as(i64, @intFromFloat(self.rttEstimate))) { + self.relativeIdleTime >>= 1; + self.relativeSendTime >>= 1; + } + + while(timestamp -% self.nextConfirmationTimestamp > 0 and !self.queuedConfirmations.isEmpty()) { + self.sendConfirmationPacket(timestamp); + } + + while(timestamp -% self.nextPacketTimestamp > 0) { + // Only attempt to increase the congestion bandwidth if we actual use the bandwidth, to prevent unbounded growth + const considerForCongestionControl = @divFloor(self.relativeSendTime, 2) > self.relativeIdleTime; + const dataLen = blk: { + self.mutex.lock(); + defer self.mutex.unlock(); + if(self.lossyChannel.sendNextPacketAndGetSize(self, timestamp, considerForCongestionControl)) |dataLen| break :blk dataLen; + if(self.fastChannel.sendNextPacketAndGetSize(self, timestamp, considerForCongestionControl)) |dataLen| break :blk dataLen; + if(self.slowChannel.sendNextPacketAndGetSize(self, timestamp, considerForCongestionControl)) |dataLen| break :blk dataLen; + + break; + }; + const networkLen: f32 = @floatFromInt(dataLen + headerOverhead); + const packetTime: i64 = @intFromFloat(@max(1, networkLen/self.bandwidthEstimateInBytesPerRtt*self.rttEstimate)); + self.nextPacketTimestamp +%= packetTime; + self.relativeSendTime += packetTime; + } + } + + pub fn disconnect(self: *Connection) void { + self.manager.send(&.{@intFromEnum(ChannelId.disconnect)}, self.remoteAddress, null); + self.connectionState.store(.disconnectDesired, .unordered); + if(builtin.os.tag == .windows and !self.isServerSide() and main.server.world != null) { + main.io.sleep(.fromMilliseconds(10), .awake) catch {}; // Windows is too eager to close the socket, without waiting here we get a ConnectionResetByPeer on the other side. + } + self.manager.removeConnection(self); + if(self.user) |user| { + main.server.disconnect(user); + } else { + self.handShakeWaiting.broadcast(); + main.exitToMenu(undefined); + } + std.log.info("Disconnected", .{}); + } +}; diff --git a/particles.zig b/particles.zig new file mode 100644 index 0000000000..0589f1bbf6 --- /dev/null +++ b/particles.zig @@ -0,0 +1,476 @@ +const std = @import("std"); + +const main = @import("main"); +const chunk_meshing = @import("renderer/chunk_meshing.zig"); +const graphics = @import("graphics.zig"); +const SSBO = graphics.SSBO; +const TextureArray = graphics.TextureArray; +const Shader = graphics.Shader; +const Image = graphics.Image; +const c = graphics.c; +const game = @import("game.zig"); +const ZonElement = @import("zon.zig").ZonElement; +const random = @import("random.zig"); +const vec = @import("vec.zig"); +const Mat4f = vec.Mat4f; +const Vec3d = vec.Vec3d; +const Vec4d = vec.Vec4d; +const Vec3f = vec.Vec3f; +const Vec4f = vec.Vec4f; +const Vec3i = vec.Vec3i; + +pub const ParticleManager = struct { + var particleTypesSSBO: SSBO = undefined; + var types: main.ListUnmanaged(ParticleType) = .{}; + var textures: main.ListUnmanaged(Image) = .{}; + var emissionTextures: main.ListUnmanaged(Image) = .{}; + + var textureArray: TextureArray = undefined; + var emissionTextureArray: TextureArray = undefined; + + const ParticleIndex = u16; + var particleTypeHashmap: std.StringHashMapUnmanaged(ParticleIndex) = .{}; + + pub fn init() void { + textureArray = .init(); + emissionTextureArray = .init(); + particleTypesSSBO = SSBO.init(); + ParticleSystem.init(); + } + + pub fn deinit() void { + textureArray.deinit(); + emissionTextureArray.deinit(); + ParticleSystem.deinit(); + particleTypesSSBO.deinit(); + } + + pub fn reset() void { + types = .{}; + textures = .{}; + emissionTextures = .{}; + particleTypeHashmap = .{}; + ParticleSystem.reset(); + } + + pub fn register(assetsFolder: []const u8, id: []const u8, zon: ZonElement) void { + const textureId = zon.get(?[]const u8, "texture", null) orelse { + std.log.err("Particle texture id was not specified for {s} ({s})", .{id, assetsFolder}); + return; + }; + + const particleType = readTextureDataAndParticleType(assetsFolder, textureId); + + particleTypeHashmap.put(main.worldArena.allocator, id, @intCast(types.items.len)) catch unreachable; + types.append(main.worldArena, particleType); + + std.log.debug("Registered particle type: {s}", .{id}); + } + fn readTextureDataAndParticleType(assetsFolder: []const u8, textureId: []const u8) ParticleType { + var typ: ParticleType = undefined; + + const base = readTexture(assetsFolder, textureId, ".png", Image.defaultImage, .isMandatory); + const emission = readTexture(assetsFolder, textureId, "_emission.png", Image.emptyImage, .isOptional); + const hasEmission = (emission.imageData.ptr != Image.emptyImage.imageData.ptr); + const baseAnimationFrameCount = base.height/base.width; + const emissionAnimationFrameCount = emission.height/emission.width; + + typ.frameCount = @floatFromInt(baseAnimationFrameCount); + typ.startFrame = @floatFromInt(textures.items.len); + typ.size = @as(f32, @floatFromInt(base.width))/16; + + var isBaseBroken = false; + var isEmissionBroken = false; + + if(base.height%base.width != 0) { + std.log.err("Particle base texture has incorrect dimensions ({}x{}) expected height to be multiple of width for {s} ({s})", .{base.width, base.height, textureId, assetsFolder}); + isBaseBroken = true; + } + if(hasEmission and emission.height%emission.width != 0) { + std.log.err("Particle emission texture has incorrect dimensions ({}x{}) expected height to be multiple of width for {s} ({s})", .{base.width, base.height, textureId, assetsFolder}); + isEmissionBroken = true; + } + if(hasEmission and baseAnimationFrameCount != emissionAnimationFrameCount) { + std.log.err("Particle base texture and emission texture frame count mismatch ({} vs {}) for {s} ({s})", .{baseAnimationFrameCount, emissionAnimationFrameCount, textureId, assetsFolder}); + isEmissionBroken = true; + } + + createAnimationFrames(&textures, baseAnimationFrameCount, base, isBaseBroken); + createAnimationFrames(&emissionTextures, baseAnimationFrameCount, emission, isBaseBroken or isEmissionBroken or !hasEmission); + + return typ; + } + + fn readTexture(assetsFolder: []const u8, textureId: []const u8, suffix: []const u8, default: graphics.Image, status: enum {isOptional, isMandatory}) graphics.Image { + var splitter = std.mem.splitScalar(u8, textureId, ':'); + const mod = splitter.first(); + const id = splitter.rest(); + + const gameAssetsPath = std.fmt.allocPrint(main.stackAllocator.allocator, "assets/{s}/particles/textures/{s}{s}", .{mod, id, suffix}) catch unreachable; + defer main.stackAllocator.free(gameAssetsPath); + + const worldAssetsPath = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{s}/particles/textures/{s}{s}", .{assetsFolder, mod, id, suffix}) catch unreachable; + defer main.stackAllocator.free(worldAssetsPath); + + return graphics.Image.readFromFile(main.worldArena, worldAssetsPath) catch graphics.Image.readFromFile(main.worldArena, gameAssetsPath) catch { + if(status == .isMandatory) std.log.err("Particle texture not found in {s} and {s}.", .{worldAssetsPath, gameAssetsPath}); + return default; + }; + } + + fn createAnimationFrames(container: *main.ListUnmanaged(Image), frameCount: usize, image: Image, isBroken: bool) void { + for(0..frameCount) |i| { + container.append(main.worldArena, if(isBroken) image else extractAnimationSlice(image, i)); + } + } + + fn extractAnimationSlice(image: Image, frameIndex: usize) Image { + const frameCount = image.height/image.width; + const frameHeight = image.height/frameCount; + const startHeight = frameHeight*frameIndex; + const endHeight = frameHeight*(frameIndex + 1); + var result = image; + result.height = @intCast(frameHeight); + result.imageData = result.imageData[startHeight*image.width .. endHeight*image.width]; + return result; + } + + pub fn generateTextureArray() void { + textureArray.generate(textures.items, true, true); + emissionTextureArray.generate(emissionTextures.items, true, false); + + particleTypesSSBO.bufferData(ParticleType, ParticleManager.types.items); + particleTypesSSBO.bind(14); + } +}; + +pub const ParticleSystem = struct { + pub const maxCapacity: u32 = 524288; + var particleCount: u32 = 0; + var particles: [maxCapacity]Particle = undefined; + var particlesLocal: [maxCapacity]ParticleLocal = undefined; + var properties: EmitterProperties = undefined; + var previousPlayerPos: Vec3d = undefined; + + var mutex: std.Thread.Mutex = .{}; + var networkCreationQueue: main.ListUnmanaged(struct {emitter: Emitter, pos: Vec3d, count: u32}) = .{}; + + var particlesSSBO: SSBO = undefined; + + var pipeline: graphics.Pipeline = undefined; + const UniformStruct = struct { + projectionAndViewMatrix: c_int, + billboardMatrix: c_int, + ambientLight: c_int, + }; + var uniforms: UniformStruct = undefined; + + fn init() void { + pipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/particles/particles.vert", + "assets/cubyz/shaders/particles/particles.frag", + "", + &uniforms, + .{}, + .{.depthTest = true, .depthWrite = true}, + .{.attachments = &.{.noBlending}}, + ); + + properties = EmitterProperties{ + .gravity = .{0, 0, -2}, + .drag = 0.2, + .lifeTimeMin = 10, + .lifeTimeMax = 10, + .velMin = 0.1, + .velMax = 0.3, + .rotVelMin = std.math.pi*0.2, + .rotVelMax = std.math.pi*0.6, + .randomizeRotationOnSpawn = true, + }; + particlesSSBO = SSBO.init(); + particlesSSBO.createDynamicBuffer(Particle, maxCapacity); + particlesSSBO.bind(13); + } + + fn deinit() void { + pipeline.deinit(); + particlesSSBO.deinit(); + } + + fn reset() void { + networkCreationQueue = .{}; + } + + pub fn update(deltaTime: f32) void { + mutex.lock(); + if(networkCreationQueue.items.len != 0) { + for(networkCreationQueue.items) |creation| { + creation.emitter.spawnParticles(creation.count, Emitter.SpawnPoint, .{ + .mode = .spread, + .position = creation.pos, + }); + } + networkCreationQueue.clearRetainingCapacity(); + } + mutex.unlock(); + + const vecDeltaTime: Vec4f = @as(Vec4f, @splat(deltaTime)); + const playerPos = game.Player.getEyePosBlocking(); + const prevPlayerPosDifference: Vec3f = @floatCast(previousPlayerPos - playerPos); + + var i: u32 = 0; + while(i < particleCount) { + const particle = &particles[i]; + const particleLocal = &particlesLocal[i]; + particle.lifeRatio -= particleLocal.lifeVelocity*deltaTime; + if(particle.lifeRatio < 0) { + particleCount -= 1; + particles[i] = particles[particleCount]; + particlesLocal[i] = particlesLocal[particleCount]; + continue; + } + + var pos: Vec3f = particle.pos; + var rot = particle.rot; + const rotVel = particleLocal.velAndRotationVel[3]; + rot += rotVel*deltaTime; + + particleLocal.velAndRotationVel += vec.combine(properties.gravity, 0)*vecDeltaTime; + particleLocal.velAndRotationVel *= @splat(@exp(-properties.drag*deltaTime)); + const posDelta = particleLocal.velAndRotationVel*vecDeltaTime; + + if(particleLocal.collides) { + const size = ParticleManager.types.items[particle.typ].size; + const hitBox: game.collision.Box = .{.min = @splat(size*-0.5), .max = @splat(size*0.5)}; + var v3Pos = playerPos + @as(Vec3d, @floatCast(pos + prevPlayerPosDifference)); + v3Pos[0] += posDelta[0]; + if(game.collision.collides(.client, .x, -posDelta[0], v3Pos, hitBox)) |box| { + v3Pos[0] = if(posDelta[0] < 0) + box.max[0] - hitBox.min[0] + else + box.min[0] - hitBox.max[0]; + } + v3Pos[1] += posDelta[1]; + if(game.collision.collides(.client, .y, -posDelta[1], v3Pos, hitBox)) |box| { + v3Pos[1] = if(posDelta[1] < 0) + box.max[1] - hitBox.min[1] + else + box.min[1] - hitBox.max[1]; + } + v3Pos[2] += posDelta[2]; + if(game.collision.collides(.client, .z, -posDelta[2], v3Pos, hitBox)) |box| { + v3Pos[2] = if(posDelta[2] < 0) + box.max[2] - hitBox.min[2] + else + box.min[2] - hitBox.max[2]; + } + pos = @as(Vec3f, @floatCast(v3Pos - playerPos)); + } else { + pos += Vec3f{posDelta[0], posDelta[1], posDelta[2]} + prevPlayerPosDifference; + } + + particle.pos = pos; + particle.rot = rot; + particleLocal.velAndRotationVel[3] = rotVel; + + const positionf64 = @as(Vec3d, @floatCast(pos)) + playerPos; + const intPos: vec.Vec3i = @intFromFloat(@floor(positionf64)); + const light: [6]u8 = main.renderer.mesh_storage.getLight(intPos[0], intPos[1], intPos[2]) orelse @splat(0); + const compressedLight = + @as(u32, light[0] >> 3) << 25 | + @as(u32, light[1] >> 3) << 20 | + @as(u32, light[2] >> 3) << 15 | + @as(u32, light[3] >> 3) << 10 | + @as(u32, light[4] >> 3) << 5 | + @as(u32, light[5] >> 3); + particle.light = compressedLight; + + i += 1; + } + previousPlayerPos = playerPos; + } + + fn addParticle(typ: u32, pos: Vec3d, vel: Vec3f, collides: bool) void { + const lifeTime = properties.lifeTimeMin + random.nextFloat(&main.seed)*properties.lifeTimeMax; + const rot = if(properties.randomizeRotationOnSpawn) random.nextFloat(&main.seed)*std.math.pi*2 else 0; + + particles[particleCount] = Particle{ + .pos = @as(Vec3f, @floatCast(pos - previousPlayerPos)), + .rot = rot, + .typ = typ, + }; + particlesLocal[particleCount] = ParticleLocal{ + .velAndRotationVel = vec.combine(vel, properties.rotVelMin + random.nextFloatSigned(&main.seed)*properties.rotVelMax), + .lifeVelocity = 1/lifeTime, + .collides = collides, + }; + particleCount += 1; + } + + pub fn render(projectionMatrix: Mat4f, viewMatrix: Mat4f, ambientLight: Vec3f) void { + particlesSSBO.bufferSubData(Particle, &particles, particleCount); + + pipeline.bind(null); + + const projectionAndViewMatrix = Mat4f.mul(projectionMatrix, viewMatrix); + c.glUniformMatrix4fv(uniforms.projectionAndViewMatrix, 1, c.GL_TRUE, @ptrCast(&projectionAndViewMatrix)); + c.glUniform3fv(uniforms.ambientLight, 1, @ptrCast(&ambientLight)); + + const billboardMatrix = Mat4f.rotationZ(-game.camera.rotation[2] + std.math.pi*0.5) + .mul(Mat4f.rotationY(game.camera.rotation[0] - std.math.pi*0.5)); + c.glUniformMatrix4fv(uniforms.billboardMatrix, 1, c.GL_TRUE, @ptrCast(&billboardMatrix)); + + c.glActiveTexture(c.GL_TEXTURE0); + ParticleManager.textureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE1); + ParticleManager.emissionTextureArray.bind(); + + c.glBindVertexArray(chunk_meshing.vao); + + const maxQuads = chunk_meshing.maxQuadsInIndexBuffer; + const count = std.math.divCeil(u32, particleCount, maxQuads) catch unreachable; + for(0..count) |i| { + const particleOffset = (maxQuads*4)*i; + const particleCurrentCount: u32 = @min(maxQuads, particleCount - maxQuads*i); + c.glDrawElementsBaseVertex(c.GL_TRIANGLES, @intCast(particleCurrentCount*6), c.GL_UNSIGNED_INT, null, @intCast(particleOffset)); + } + } + + pub fn getParticleCount() u32 { + return particleCount; + } + + pub fn addParticlesFromNetwork(emitter: Emitter, pos: Vec3d, count: u32) void { + mutex.lock(); + defer mutex.unlock(); + networkCreationQueue.append(main.worldArena, .{.emitter = emitter, .pos = pos, .count = count}); + } +}; + +pub const EmitterProperties = struct { + gravity: Vec3f = @splat(0), + drag: f32 = 0, + velMin: f32 = 0, + velMax: f32 = 0, + rotVelMin: f32 = 0, + rotVelMax: f32 = 0, + lifeTimeMin: f32 = 0, + lifeTimeMax: f32 = 0, + randomizeRotationOnSpawn: bool = false, +}; + +pub const DirectionMode = union(enum(u8)) { + // The particle goes in the direction away from the center + spread: void, + // The particle goes in a random direction + scatter: void, + // The particle goes in the specified direction + direction: Vec3f, +}; + +pub const Emitter = struct { + typ: u16 = 0, + collides: bool, + + pub const SpawnPoint = struct { + mode: DirectionMode, + position: Vec3d, + + pub fn spawn(self: SpawnPoint) struct {Vec3d, Vec3f} { + const particlePos = self.position; + const speed: Vec3f = @splat(ParticleSystem.properties.velMin + random.nextFloat(&main.seed)*ParticleSystem.properties.velMax); + const dir: Vec3f = switch(self.mode) { + .direction => |dir| dir, + .scatter, .spread => vec.normalize(random.nextFloatVectorSigned(3, &main.seed)), + }; + const particleVel = dir*speed; + + return .{particlePos, particleVel}; + } + }; + + pub const SpawnSphere = struct { + radius: f32, + mode: DirectionMode, + position: Vec3d, + + pub fn spawn(self: SpawnSphere) struct {Vec3d, Vec3f} { + const spawnPos: Vec3f = @splat(self.radius); + var offsetPos: Vec3f = undefined; + while(true) { + offsetPos = random.nextFloatVectorSigned(3, &main.seed); + if(vec.lengthSquare(offsetPos) <= 1) break; + } + const particlePos = self.position + @as(Vec3d, @floatCast(offsetPos*spawnPos)); + const speed: Vec3f = @splat(ParticleSystem.properties.velMin + random.nextFloat(&main.seed)*ParticleSystem.properties.velMax); + const dir: Vec3f = switch(self.mode) { + .direction => |dir| dir, + .scatter => vec.normalize(random.nextFloatVectorSigned(3, &main.seed)), + .spread => @floatCast(offsetPos), + }; + const particleVel = dir*speed; + + return .{particlePos, particleVel}; + } + }; + + pub const SpawnCube = struct { + size: Vec3f, + mode: DirectionMode, + position: Vec3d, + + pub fn spawn(self: SpawnCube) struct {Vec3d, Vec3f} { + const spawnPos: Vec3f = self.size; + const offsetPos: Vec3f = random.nextFloatVectorSigned(3, &main.seed); + const particlePos = self.position + @as(Vec3d, @floatCast(offsetPos*spawnPos)); + const speed: Vec3f = @splat(ParticleSystem.properties.velMin + random.nextFloat(&main.seed)*ParticleSystem.properties.velMax); + const dir: Vec3f = switch(self.mode) { + .direction => |dir| dir, + .scatter => vec.normalize(random.nextFloatVectorSigned(3, &main.seed)), + .spread => vec.normalize(@as(Vec3f, @floatCast(offsetPos))), + }; + const particleVel = dir*speed; + + return .{particlePos, particleVel}; + } + }; + + pub fn init(id: []const u8, collides: bool) Emitter { + const emitter = Emitter{ + .typ = ParticleManager.particleTypeHashmap.get(id) orelse 0, + .collides = collides, + }; + + return emitter; + } + + pub fn spawnParticles(self: Emitter, spawnCount: u32, comptime T: type, spawnRules: T) void { + const count = @min(spawnCount, ParticleSystem.maxCapacity - ParticleSystem.particleCount); + for(0..count) |_| { + const particlePos, const particleVel = spawnRules.spawn(); + + ParticleSystem.addParticle(self.typ, particlePos, particleVel, self.collides); + } + } +}; + +pub const ParticleType = struct { + frameCount: f32, + startFrame: f32, + size: f32, +}; + +pub const Particle = extern struct { + pos: [3]f32 align(16), + rot: f32 = 0, + lifeRatio: f32 = 1, + light: u32 = 0, + typ: u32, + // 4 bytes left for use +}; + +pub const ParticleLocal = struct { + velAndRotationVel: Vec4f, + lifeVelocity: f32, + collides: bool, +}; diff --git a/physics.zig b/physics.zig new file mode 100644 index 0000000000..b9aa7db27e --- /dev/null +++ b/physics.zig @@ -0,0 +1,262 @@ +const std = @import("std"); + +const items = @import("items.zig"); +const Inventory = items.Inventory; +const main = @import("main"); +const vec = @import("vec.zig"); +const Vec2f = vec.Vec2f; +const Vec3f = vec.Vec3f; +const Vec3d = vec.Vec3d; +const settings = @import("settings.zig"); +const Player = main.game.Player; +const collision = main.game.collision; +const camera = main.game.camera; + +const gravity = 30.0; +const airTerminalVelocity = 90.0; +const playerDensity = 1.2; + +pub fn calculateProperties() void { + if(main.renderer.mesh_storage.getBlockFromRenderThread(@intFromFloat(@floor(Player.super.pos[0])), @intFromFloat(@floor(Player.super.pos[1])), @intFromFloat(@floor(Player.super.pos[2]))) != null) { + Player.volumeProperties = collision.calculateVolumeProperties(.client, Player.super.pos, Player.outerBoundingBox, .{.density = 0.001, .terminalVelocity = airTerminalVelocity, .maxDensity = 0.001, .mobility = 1.0}); + + const groundFriction = if(!Player.onGround and !Player.isFlying.load(.monotonic)) 0 else collision.calculateSurfaceProperties(.client, Player.super.pos, Player.outerBoundingBox, 20).friction; + const volumeFrictionCoeffecient: f32 = @floatCast(gravity/Player.volumeProperties.terminalVelocity); + Player.currentFriction = if(Player.isFlying.load(.monotonic)) 20 else groundFriction + volumeFrictionCoeffecient; + } +} + +pub fn update(deltaTime: f64, inputAcc: Vec3d, jumping: bool) void { // MARK: update() + var move: Vec3d = .{0, 0, 0}; + if(main.renderer.mesh_storage.getBlockFromRenderThread(@intFromFloat(@floor(Player.super.pos[0])), @intFromFloat(@floor(Player.super.pos[1])), @intFromFloat(@floor(Player.super.pos[2]))) != null) { + const effectiveGravity = gravity*(playerDensity - Player.volumeProperties.density)/playerDensity; + const volumeFrictionCoeffecient: f32 = @floatCast(gravity/Player.volumeProperties.terminalVelocity); + var acc = inputAcc; + if(!Player.isFlying.load(.monotonic)) { + acc[2] -= effectiveGravity; + } + + const baseFrictionCoefficient: f32 = Player.currentFriction; + var directionalFrictionCoefficients: Vec3f = @splat(0); + + // This our model for movement on a single frame: + // dv/dt = a - λ·v + // dx/dt = v + // Where a is the acceleration and λ is the friction coefficient + inline for(0..3) |i| { + var frictionCoefficient = baseFrictionCoefficient + directionalFrictionCoefficients[i]; + if(i == 2 and jumping) { // No friction while jumping + // Here we want to ensure a specified jump height under air friction. + const jumpVelocity = @sqrt(Player.jumpHeight*gravity*2); + Player.super.vel[i] = @max(jumpVelocity, Player.super.vel[i] + jumpVelocity); + frictionCoefficient = volumeFrictionCoeffecient; + } + const v_0 = Player.super.vel[i]; + const a = acc[i]; + // Here the solution can be easily derived: + // dv/dt = a - λ·v + // (1 - a)/v dv = -λ dt + // (1 - a)ln(v) + C = -λt + // v(t) = a/λ + c_1 e^(λ (-t)) + // v(0) = a/λ + c_1 = v₀ + // c_1 = v₀ - a/λ + // x(t) = ∫v(t) dt + // x(t) = ∫a/λ + c_1 e^(λ (-t)) dt + // x(t) = a/λt - c_1/λ e^(λ (-t)) + C + // With x(0) = 0 we get C = c_1/λ + // x(t) = a/λt - c_1/λ e^(λ (-t)) + c_1/λ + const c_1 = v_0 - a/frictionCoefficient; + Player.super.vel[i] = a/frictionCoefficient + c_1*@exp(-frictionCoefficient*deltaTime); + move[i] = a/frictionCoefficient*deltaTime - c_1/frictionCoefficient*@exp(-frictionCoefficient*deltaTime) + c_1/frictionCoefficient; + } + + acc = @splat(0); + // Apply springs to the eye position: + var springConstants = Vec3d{0, 0, 0}; + { + const forceMultipliers = Vec3d{ + 400, + 400, + 400, + }; + const frictionMultipliers = Vec3d{ + 30, + 30, + 30, + }; + const strength = (-Player.eye.pos)/(Player.eye.box.max - Player.eye.box.min); + const force = strength*forceMultipliers; + const friction = frictionMultipliers; + springConstants += forceMultipliers/(Player.eye.box.max - Player.eye.box.min); + directionalFrictionCoefficients += @floatCast(friction); + acc += force; + } + + // This our model for movement of the eye position on a single frame: + // dv/dt = a - k*x - λ·v + // dx/dt = v + // Where a is the acceleration, k is the spring constant and λ is the friction coefficient + inline for(0..3) |i| blk: { + if(Player.eye.step[i]) { + const oldPos = Player.eye.pos[i]; + const newPos = oldPos + Player.eye.vel[i]*deltaTime; + if(newPos*std.math.sign(Player.eye.vel[i]) <= -0.1) { + Player.eye.pos[i] = newPos; + break :blk; + } else { + Player.eye.step[i] = false; + } + } + if(i == 2 and Player.eye.coyote > 0) { + break :blk; + } + const frictionCoefficient = directionalFrictionCoefficients[i]; + const v_0 = Player.eye.vel[i]; + const k = springConstants[i]; + const a = acc[i]; + // here we need to solve the full equation: + // The solution of this differential equation is given by + // x(t) = a/k + c_1 e^(1/2 t (-c_3 - λ)) + c_2 e^(1/2 t (c_3 - λ)) + // With c_3 = sqrt(λ^2 - 4 k) which can be imaginary + // v(t) is just the derivative, given by + // v(t) = 1/2 (-c_3 - λ) c_1 e^(1/2 t (-c_3 - λ)) + (1/2 (c_3 - λ)) c_2 e^(1/2 t (c_3 - λ)) + // Now for simplicity we set x(0) = 0 and v(0) = v₀ + // a/k + c_1 + c_2 = 0 → c_1 = -a/k - c_2 + // (-c_3 - λ) c_1 + (c_3 - λ) c_2 = 2v₀ + // → (-c_3 - λ) (-a/k - c_2) + (c_3 - λ) c_2 = 2v₀ + // → (-c_3 - λ) (-a/k) - (-c_3 - λ)c_2 + (c_3 - λ) c_2 = 2v₀ + // → ((c_3 - λ) - (-c_3 - λ))c_2 = 2v₀ - (c_3 + λ) (a/k) + // → (c_3 - λ + c_3 + λ)c_2 = 2v₀ - (c_3 + λ) (a/k) + // → 2 c_3 c_2 = 2v₀ - (c_3 + λ) (a/k) + // → c_2 = (2v₀ - (c_3 + λ) (a/k))/(2 c_3) + // → c_2 = v₀/c_3 - (1 + λ/c_3)/2 (a/k) + // In total we get: + // c_3 = sqrt(λ^2 - 4 k) + // c_2 = (2v₀ - (c_3 + λ) (a/k))/(2 c_3) + // c_1 = -a/k - c_2 + const c_3 = vec.Complex.fromSqrt(frictionCoefficient*frictionCoefficient - 4*k); + const c_2 = (((c_3.addScalar(frictionCoefficient).mulScalar(-a/k)).addScalar(2*v_0)).div(c_3.mulScalar(2))); + const c_1 = c_2.addScalar(a/k).negate(); + // v(t) = 1/2 (-c_3 - λ) c_1 e^(1/2 t (-c_3 - λ)) + (1/2 (c_3 - λ)) c_2 e^(1/2 t (c_3 - λ)) + // x(t) = a/k + c_1 e^(1/2 t (-c_3 - λ)) + c_2 e^(1/2 t (c_3 - λ)) + const firstTerm = c_1.mul((c_3.negate().subScalar(frictionCoefficient)).mulScalar(deltaTime/2).exp()); + const secondTerm = c_2.mul((c_3.subScalar(frictionCoefficient)).mulScalar(deltaTime/2).exp()); + Player.eye.vel[i] = firstTerm.mul(c_3.negate().subScalar(frictionCoefficient).mulScalar(0.5)).add(secondTerm.mul((c_3.subScalar(frictionCoefficient)).mulScalar(0.5))).val[0]; + Player.eye.pos[i] += firstTerm.add(secondTerm).addScalar(a/k).val[0]; + } + } + + if(!Player.isGhost.load(.monotonic)) { + Player.mutex.lock(); + defer Player.mutex.unlock(); + + const hitBox = Player.outerBoundingBox; + var steppingHeight = Player.steppingHeight()[2]; + if(Player.super.vel[2] > 0) { + steppingHeight = Player.super.vel[2]*Player.super.vel[2]/gravity/2; + } + steppingHeight = @min(steppingHeight, Player.eye.pos[2] - Player.eye.box.min[2]); + + const slipLimit = 0.25*Player.currentFriction; + + const xMovement = collision.collideOrStep(.client, .x, move[0], Player.super.pos, hitBox, steppingHeight); + Player.super.pos += xMovement; + if(Player.crouching and Player.onGround and @abs(Player.super.vel[0]) < slipLimit) { + if(collision.collides(.client, .x, 0, Player.super.pos - Vec3d{0, 0, 1}, hitBox) == null) { + Player.super.pos -= xMovement; + Player.super.vel[0] = 0; + } + } + + const yMovement = collision.collideOrStep(.client, .y, move[1], Player.super.pos, hitBox, steppingHeight); + Player.super.pos += yMovement; + if(Player.crouching and Player.onGround and @abs(Player.super.vel[1]) < slipLimit) { + if(collision.collides(.client, .y, 0, Player.super.pos - Vec3d{0, 0, 1}, hitBox) == null) { + Player.super.pos -= yMovement; + Player.super.vel[1] = 0; + } + } + + if(xMovement[0] != move[0]) { + Player.super.vel[0] = 0; + } + if(yMovement[1] != move[1]) { + Player.super.vel[1] = 0; + } + + const stepAmount = xMovement[2] + yMovement[2]; + if(stepAmount > 0) { + if(Player.eye.coyote <= 0) { + Player.eye.vel[2] = @max(1.5*vec.length(Player.super.vel), Player.eye.vel[2], 4); + Player.eye.step[2] = true; + if(Player.super.vel[2] > 0) { + Player.eye.vel[2] = Player.super.vel[2]; + Player.eye.step[2] = false; + } + } else { + Player.eye.coyote = 0; + } + Player.eye.pos[2] -= stepAmount; + move[2] = -0.01; + Player.onGround = true; + } + + const wasOnGround = Player.onGround; + Player.onGround = false; + Player.super.pos[2] += move[2]; + if(collision.collides(.client, .z, -move[2], Player.super.pos, hitBox)) |box| { + if(move[2] < 0) { + if(!wasOnGround) { + Player.eye.vel[2] = Player.super.vel[2]; + Player.eye.pos[2] -= (box.max[2] - hitBox.min[2] - Player.super.pos[2]); + } + Player.onGround = true; + Player.super.pos[2] = box.max[2] - hitBox.min[2]; + Player.eye.coyote = 0; + } else { + Player.super.pos[2] = box.min[2] - hitBox.max[2]; + } + var bounciness = if(Player.isFlying.load(.monotonic)) 0 else collision.calculateSurfaceProperties(.client, Player.super.pos, Player.outerBoundingBox, 0.0).bounciness; + if(Player.crouching) { + bounciness *= 0.5; + } + var velocityChange: f64 = undefined; + + if(bounciness != 0.0 and Player.super.vel[2] < -3.0) { + velocityChange = Player.super.vel[2]*@as(f64, @floatCast(1 - bounciness)); + Player.super.vel[2] = -Player.super.vel[2]*bounciness; + Player.jumpCoyote = Player.jumpCoyoteTimeConstant + deltaTime; + Player.eye.vel[2] *= 2; + } else { + velocityChange = Player.super.vel[2]; + Player.super.vel[2] = 0; + } + const damage: f32 = @floatCast(@round(@max((velocityChange*velocityChange)/(2*gravity) - 7, 0))/2); + if(damage > 0.01) { + Inventory.Sync.addHealth(-damage, .fall, .client, Player.id); + } + + // Always unstuck upwards for now + while(collision.collides(.client, .z, 0, Player.super.pos, hitBox)) |_| { + Player.super.pos[2] += 1; + } + } else if(wasOnGround and move[2] < 0) { + // If the player drops off a ledge, they might just be walking over a small gap, so lock the y position of the eyes that long. + // This calculates how long the player has to fall until we know they're not walking over a small gap. + // We add deltaTime because we subtract deltaTime at the bottom of update + Player.eye.coyote = @sqrt(2*Player.steppingHeight()[2]/gravity) + deltaTime; + Player.jumpCoyote = Player.jumpCoyoteTimeConstant + deltaTime; + Player.eye.pos[2] -= move[2]; + } else if(Player.eye.coyote > 0) { + Player.eye.pos[2] -= move[2]; + } + collision.touchBlocks(&Player.super, hitBox, .client, deltaTime); + } else { + Player.super.pos += move; + } + + // Clamp the eye.position and subtract eye coyote time. + Player.eye.pos = @max(Player.eye.box.min, @min(Player.eye.pos, Player.eye.box.max)); + Player.eye.coyote -= deltaTime; + Player.jumpCoyote -= deltaTime; +} diff --git a/random.zig b/random.zig new file mode 100644 index 0000000000..498be4cd6b --- /dev/null +++ b/random.zig @@ -0,0 +1,131 @@ +const std = @import("std"); + +const main = @import("main"); +const Vec2f = main.vec.Vec2f; +const Vec2i = main.vec.Vec2i; +const Vec3i = main.vec.Vec3i; + +const multiplier: u64 = 0x5deece66d; +const addend: u64 = 0xb; +const mask: u64 = (1 << 48) - 1; + +pub fn scrambleSeed(seed: *u64) void { + seed.* = (seed.* ^ multiplier) & mask; +} + +fn nextWithBitSize(comptime T: type, seed: *u64, bitSize: u6) T { + seed.* = ((seed.*)*%multiplier +% addend) & mask; + return @intCast((seed.* >> (48 - bitSize)) & std.math.maxInt(T)); +} + +fn next(comptime T: type, seed: *u64) T { + return nextWithBitSize(T, seed, @bitSizeOf(T)); +} + +pub fn nextInt(comptime T: type, seed: *u64) T { + if(@bitSizeOf(T) > 32) { + var result: T = 0; + for(0..(@bitSizeOf(T) + 31)/32) |_| { + result = result << 5 | next(u32, seed); + } + return result; + } else { + return next(T, seed); + } +} + +pub fn nextIntBounded(comptime T: type, seed: *u64, bound: T) T { + if(@typeInfo(T) != .int) @compileError("Type must be integer."); + if(@typeInfo(T).int.signedness == .signed) return nextIntBounded(std.meta.Int(.unsigned, @bitSizeOf(T) - 1), seed, @intCast(bound)); + const bitSize = std.math.log2_int_ceil(T, bound); + var result = nextWithBitSize(T, seed, bitSize); + while(result >= bound) { + result = nextWithBitSize(T, seed, bitSize); + } + return result; +} + +pub fn nextFloat(seed: *u64) f32 { + return @as(f32, @floatFromInt(nextInt(u24, seed)))/(1 << 24); +} + +pub fn nextFloatSigned(seed: *u64) f32 { + return @as(f32, @floatFromInt(@as(i24, @bitCast(nextInt(u24, seed)))))/(1 << 23); +} + +pub fn nextFloatExp(seed: *u64) f32 { + return -@log(nextFloat(seed)); +} + +pub fn nextFloatGauss(seed: *u64) f32 { + const a = nextFloat(seed); + const b = nextFloat(seed); + + return @sqrt(-2.0*@log(a))*@cos(2.0*std.math.pi*b); +} + +pub fn nextFloatVector(len: comptime_int, seed: *u64) @Vector(len, f32) { + var result: @Vector(len, f32) = undefined; + inline for(0..len) |i| { + result[i] = nextFloat(seed); + } + return result; +} + +pub fn nextFloatVectorSigned(len: comptime_int, seed: *u64) @Vector(len, f32) { + var result: @Vector(len, f32) = undefined; + inline for(0..len) |i| { + result[i] = nextFloatSigned(seed); + } + return result; +} + +pub fn nextDouble(seed: *u64) f64 { + const lower: u52 = nextInt(u32, seed); + const upper: u52 = nextInt(u20, seed); + return @as(f64, @floatFromInt(upper << 32 | lower))/(1 << 52); +} + +pub fn nextDoubleSigned(seed: *u64) f64 { + const lower: i52 = nextInt(u32, seed); + const upper: i52 = nextInt(u20, seed); + return @as(f64, @floatFromInt(upper << 32 | lower))/(1 << 51); +} + +pub fn nextDoubleVector(len: comptime_int, seed: *u64) @Vector(len, f64) { + var result: @Vector(len, f64) = undefined; + inline for(0..len) |i| { + result[i] = nextDouble(seed); + } + return result; +} + +pub fn nextDoubleVectorSigned(len: comptime_int, seed: *u64) @Vector(len, f64) { + var result: @Vector(len, f64) = undefined; + inline for(0..len) |i| { + result[i] = nextDoubleSigned(seed); + } + return result; +} + +pub fn nextPointInUnitCircle(seed: *u64) Vec2f { + while(true) { + const x: f32 = nextFloatSigned(seed); + const y: f32 = nextFloatSigned(seed); + if(x*x + y*y < 1) { + return Vec2f{x, y}; + } + } +} + +pub fn initSeed3D(worldSeed: u64, pos: Vec3i) u64 { + const fac = Vec3i{11248723, 105436839, 45399083}; + const seed = @reduce(.Xor, fac*%pos); + return @as(u32, @bitCast(seed)) ^ worldSeed; +} + +pub fn initSeed2D(worldSeed: u64, pos: Vec2i) u64 { + const fac = Vec2i{11248723, 105436839}; + const seed = @reduce(.Xor, fac*%pos); + return @as(u32, @bitCast(seed)) ^ worldSeed; +} diff --git a/renderer.zig b/renderer.zig new file mode 100644 index 0000000000..b4162a8533 --- /dev/null +++ b/renderer.zig @@ -0,0 +1,1160 @@ +const std = @import("std"); +const Atomic = std.atomic.Value; + +const blocks = @import("blocks.zig"); +const chunk = @import("chunk.zig"); +const entity = @import("entity.zig"); +const graphics = @import("graphics.zig"); +const particles = @import("particles.zig"); +const c = graphics.c; +const game = @import("game.zig"); +const World = game.World; +const itemdrop = @import("itemdrop.zig"); +const main = @import("main"); +const Window = main.Window; +const models = @import("models.zig"); +const network = @import("network.zig"); +const settings = @import("settings.zig"); +const vec = @import("vec.zig"); +const gpu_performance_measuring = main.gui.windowlist.gpu_performance_measuring; +const crosshair = main.gui.windowlist.crosshair; +const Vec2f = vec.Vec2f; +const Vec3i = vec.Vec3i; +const Vec3f = vec.Vec3f; +const Vec3d = vec.Vec3d; +const Vec4f = vec.Vec4f; +const Mat4f = vec.Mat4f; + +pub const chunk_meshing = @import("renderer/chunk_meshing.zig"); +pub const mesh_storage = @import("renderer/mesh_storage.zig"); + +/// Time after which no more chunk meshes are created. This allows the game to run smoother on movement. +const maximumMeshTime: std.Io.Duration = .fromMilliseconds(12); +pub const zNear = 0.1; +pub const zFar = 65536.0; // TODO: Fix z-fighting problems. + +var deferredRenderPassPipeline: graphics.Pipeline = undefined; +var deferredUniforms: struct { + @"fog.color": c_int, + @"fog.density": c_int, + @"fog.fogLower": c_int, + @"fog.fogHigher": c_int, + tanXY: c_int, + zNear: c_int, + zFar: c_int, + invViewMatrix: c_int, + playerPositionInteger: c_int, + playerPositionFraction: c_int, +} = undefined; +var fakeReflectionPipeline: graphics.Pipeline = undefined; +var fakeReflectionUniforms: struct { + normalVector: c_int, + upVector: c_int, + rightVector: c_int, + frequency: c_int, + reflectionMapSize: c_int, +} = undefined; + +pub var activeFrameBuffer: c_uint = 0; + +pub const reflectionCubeMapSize = 64; +var reflectionCubeMap: graphics.CubeMapTexture = undefined; + +pub fn init() void { + deferredRenderPassPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/deferred_render_pass.vert", + "assets/cubyz/shaders/deferred_render_pass.frag", + "", + &deferredUniforms, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.noBlending}}, + ); + fakeReflectionPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/fake_reflection.vert", + "assets/cubyz/shaders/fake_reflection.frag", + "", + &fakeReflectionUniforms, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.noBlending}}, + ); + worldFrameBuffer.init(true, c.GL_NEAREST, c.GL_CLAMP_TO_EDGE); + worldFrameBuffer.updateSize(Window.width, Window.height, c.GL_RGB16F); + Bloom.init(); + MeshSelection.init(); + MenuBackGround.init(); + Skybox.init(); + chunk_meshing.init(); + mesh_storage.init(); + reflectionCubeMap = .init(); + reflectionCubeMap.generate(reflectionCubeMapSize, reflectionCubeMapSize); + initReflectionCubeMap(); +} + +pub fn deinit() void { + deferredRenderPassPipeline.deinit(); + fakeReflectionPipeline.deinit(); + worldFrameBuffer.deinit(); + Bloom.deinit(); + MeshSelection.deinit(); + MenuBackGround.deinit(); + Skybox.deinit(); + mesh_storage.deinit(); + chunk_meshing.deinit(); + reflectionCubeMap.deinit(); +} + +fn initReflectionCubeMap() void { + c.glViewport(0, 0, reflectionCubeMapSize, reflectionCubeMapSize); + var framebuffer: graphics.FrameBuffer = undefined; + framebuffer.init(false, c.GL_LINEAR, c.GL_CLAMP_TO_EDGE); + defer framebuffer.deinit(); + framebuffer.bind(); + fakeReflectionPipeline.bind(null); + c.glUniform1f(fakeReflectionUniforms.frequency, 1); + c.glUniform1f(fakeReflectionUniforms.reflectionMapSize, reflectionCubeMapSize); + for(0..6) |face| { + c.glUniform3fv(fakeReflectionUniforms.normalVector, 1, @ptrCast(&graphics.CubeMapTexture.faceNormal(face))); + c.glUniform3fv(fakeReflectionUniforms.upVector, 1, @ptrCast(&graphics.CubeMapTexture.faceUp(face))); + c.glUniform3fv(fakeReflectionUniforms.rightVector, 1, @ptrCast(&graphics.CubeMapTexture.faceRight(face))); + reflectionCubeMap.bindToFramebuffer(framebuffer, @intCast(face)); + c.glBindVertexArray(graphics.draw.rectVAO); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } +} + +var worldFrameBuffer: graphics.FrameBuffer = undefined; + +pub var lastWidth: u31 = 0; +pub var lastHeight: u31 = 0; +var lastFov: f32 = 0; +pub fn updateFov(fov: f32) void { + if(lastFov != fov) { + lastFov = fov; + game.projectionMatrix = Mat4f.perspective(std.math.degreesToRadians(fov), @as(f32, @floatFromInt(lastWidth))/@as(f32, @floatFromInt(lastHeight)), zNear, zFar); + } +} +pub fn updateViewport(width: u31, height: u31) void { + lastWidth = @intFromFloat(@as(f32, @floatFromInt(width))*main.settings.resolutionScale); + lastHeight = @intFromFloat(@as(f32, @floatFromInt(height))*main.settings.resolutionScale); + game.projectionMatrix = Mat4f.perspective(std.math.degreesToRadians(lastFov), @as(f32, @floatFromInt(lastWidth))/@as(f32, @floatFromInt(lastHeight)), zNear, zFar); + worldFrameBuffer.updateSize(lastWidth, lastHeight, c.GL_RGB16F); + worldFrameBuffer.unbind(); +} + +pub fn render(playerPosition: Vec3d, deltaTime: f64) void { + // TODO: player bobbing + // TODO: Handle colors and sun position in the world. + std.debug.assert(game.world != null); + var ambient: Vec3f = undefined; + ambient[0] = @max(0.1, game.world.?.ambientLight); + ambient[1] = @max(0.1, game.world.?.ambientLight); + ambient[2] = @max(0.1, game.world.?.ambientLight); + + itemdrop.ItemDisplayManager.update(deltaTime); + renderWorld(game.world.?, ambient, game.fog.skyColor, playerPosition); + const startTime = main.timestamp(); + mesh_storage.updateMeshes(startTime.addDuration(maximumMeshTime)); +} + +pub fn crosshairDirection(rotationMatrix: Mat4f, fovY: f32, width: u31, height: u31) Vec3f { + // stolen code from Frustum.init + const invRotationMatrix = rotationMatrix.transpose(); + const cameraDir = vec.xyz(invRotationMatrix.mulVec(Vec4f{0, 1, 0, 1})); + const cameraUp = vec.xyz(invRotationMatrix.mulVec(Vec4f{0, 0, 1, 1})); + const cameraRight = vec.xyz(invRotationMatrix.mulVec(Vec4f{1, 0, 0, 1})); + + const screenSize = Vec2f{@floatFromInt(width), @floatFromInt(height)}; + const screenCoord = (crosshair.window.pos + crosshair.window.contentSize*Vec2f{0.5, 0.5}*@as(Vec2f, @splat(crosshair.window.scale)))*@as(Vec2f, @splat(main.gui.scale*main.settings.resolutionScale)); + + const halfVSide = std.math.tan(std.math.degreesToRadians(fovY)*0.5); + const halfHSide = halfVSide*screenSize[0]/screenSize[1]; + const sides = Vec2f{halfHSide, halfVSide}; + + const scale = (Vec2f{-1, 1} + Vec2f{2, -2}*screenCoord/screenSize)*sides; + const forwards = cameraDir; + const horizontal = cameraRight*@as(Vec3f, @splat(scale[0])); + const vertical = cameraUp*@as(Vec3f, @splat(scale[1])); // adjust for y coordinate + + const adjusted = forwards + horizontal + vertical; + return adjusted; +} + +pub fn renderWorld(world: *World, ambientLight: Vec3f, skyColor: Vec3f, playerPos: Vec3d) void { // MARK: renderWorld() + worldFrameBuffer.bind(); + c.glViewport(0, 0, lastWidth, lastHeight); + gpu_performance_measuring.startQuery(.clear); + worldFrameBuffer.clear(Vec4f{skyColor[0], skyColor[1], skyColor[2], 1}); + gpu_performance_measuring.stopQuery(); + game.camera.updateViewMatrix(); + + // Uses FrustumCulling on the chunks. + const frustum = Frustum.init(Vec3f{0, 0, 0}, game.camera.viewMatrix, lastFov, lastWidth, lastHeight); + + const time: u32 = @intCast(main.timestamp().toMilliseconds() & std.math.maxInt(u32)); + + gpu_performance_measuring.startQuery(.skybox); + Skybox.render(); + gpu_performance_measuring.stopQuery(); + + gpu_performance_measuring.startQuery(.animation); + blocks.meshes.preProcessAnimationData(time); + gpu_performance_measuring.stopQuery(); + + // Update the uniforms. The uniforms are needed to render the replacement meshes. + chunk_meshing.bindShaderAndUniforms(game.projectionMatrix, ambientLight, playerPos); + + c.glActiveTexture(c.GL_TEXTURE0); + blocks.meshes.blockTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE1); + blocks.meshes.emissionTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE2); + blocks.meshes.reflectivityAndAbsorptionTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE5); + blocks.meshes.ditherTexture.bind(); + reflectionCubeMap.bindTo(4); + + chunk_meshing.quadsDrawn = 0; + chunk_meshing.transparentQuadsDrawn = 0; + const meshes = mesh_storage.updateAndGetRenderChunks(world.conn, &frustum, playerPos, settings.renderDistance); + + gpu_performance_measuring.startQuery(.chunk_rendering_preparation); + const direction = crosshairDirection(game.camera.viewMatrix, lastFov, lastWidth, lastHeight); + MeshSelection.select(playerPos, direction, game.Player.inventory.getItem(game.Player.selectedSlot)); + + chunk_meshing.beginRender(); + + var chunkLists: [main.settings.highestSupportedLod + 1]main.List(u32) = @splat(main.List(u32).init(main.stackAllocator)); + defer for(chunkLists) |list| list.deinit(); + for(meshes) |mesh| { + mesh.prepareRendering(&chunkLists); + } + gpu_performance_measuring.stopQuery(); + gpu_performance_measuring.startQuery(.chunk_rendering); + chunk_meshing.drawChunksIndirect(&chunkLists, game.projectionMatrix, ambientLight, playerPos, false); + gpu_performance_measuring.stopQuery(); + + gpu_performance_measuring.startQuery(.entity_rendering); + entity.ClientEntityManager.render(game.projectionMatrix, ambientLight, playerPos); + + itemdrop.ItemDropRenderer.renderItemDrops(game.projectionMatrix, ambientLight, playerPos); + gpu_performance_measuring.stopQuery(); + + gpu_performance_measuring.startQuery(.block_entity_rendering); + main.block_entity.renderAll(game.projectionMatrix, ambientLight, playerPos); + gpu_performance_measuring.stopQuery(); + + gpu_performance_measuring.startQuery(.particle_rendering); + particles.ParticleSystem.render(game.projectionMatrix, game.camera.viewMatrix, ambientLight); + gpu_performance_measuring.stopQuery(); + + // Rebind block textures back to their original slots + c.glActiveTexture(c.GL_TEXTURE0); + blocks.meshes.blockTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE1); + blocks.meshes.emissionTextureArray.bind(); + + MeshSelection.render(game.projectionMatrix, game.camera.viewMatrix, playerPos); + + // Render transparent chunk meshes: + worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE5); + + gpu_performance_measuring.startQuery(.transparent_rendering_preparation); + c.glTextureBarrier(); + + { + for(&chunkLists) |*list| list.clearRetainingCapacity(); + var i: usize = meshes.len; + while(true) { + if(i == 0) break; + i -= 1; + meshes[i].prepareTransparentRendering(playerPos, &chunkLists); + } + gpu_performance_measuring.stopQuery(); + gpu_performance_measuring.startQuery(.transparent_rendering); + chunk_meshing.drawChunksIndirect(&chunkLists, game.projectionMatrix, ambientLight, playerPos, true); + gpu_performance_measuring.stopQuery(); + } + + c.glDepthRange(0, 0.001); + itemdrop.ItemDropRenderer.renderDisplayItems(ambientLight, playerPos); + c.glDepthRange(0.001, 1); + + chunk_meshing.endRender(); + + worldFrameBuffer.bindTexture(c.GL_TEXTURE3); + + const playerBlock = mesh_storage.getBlockFromAnyLodFromRenderThread(@intFromFloat(@floor(playerPos[0])), @intFromFloat(@floor(playerPos[1])), @intFromFloat(@floor(playerPos[2]))); + + if(settings.bloom) { + Bloom.render(lastWidth, lastHeight, playerBlock, playerPos, game.camera.viewMatrix); + } else { + Bloom.bindReplacementImage(); + } + gpu_performance_measuring.startQuery(.final_copy); + if(activeFrameBuffer == 0) c.glViewport(0, 0, main.Window.width, main.Window.height); + worldFrameBuffer.bindTexture(c.GL_TEXTURE3); + worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE4); + worldFrameBuffer.unbind(); + deferredRenderPassPipeline.bind(null); + if(!blocks.meshes.hasFog(playerBlock)) { + c.glUniform3fv(deferredUniforms.@"fog.color", 1, @ptrCast(&game.fog.fogColor)); + c.glUniform1f(deferredUniforms.@"fog.density", game.fog.density); + c.glUniform1f(deferredUniforms.@"fog.fogLower", game.fog.fogLower); + c.glUniform1f(deferredUniforms.@"fog.fogHigher", game.fog.fogHigher); + } else { + const fogColor = blocks.meshes.fogColor(playerBlock); + c.glUniform3f(deferredUniforms.@"fog.color", @as(f32, @floatFromInt(fogColor >> 16 & 255))/255.0, @as(f32, @floatFromInt(fogColor >> 8 & 255))/255.0, @as(f32, @floatFromInt(fogColor >> 0 & 255))/255.0); + c.glUniform1f(deferredUniforms.@"fog.density", blocks.meshes.fogDensity(playerBlock)); + c.glUniform1f(deferredUniforms.@"fog.fogLower", 1e10); + c.glUniform1f(deferredUniforms.@"fog.fogHigher", 1e10); + } + c.glUniformMatrix4fv(deferredUniforms.invViewMatrix, 1, c.GL_TRUE, @ptrCast(&game.camera.viewMatrix.transpose())); + c.glUniform3i(deferredUniforms.playerPositionInteger, @intFromFloat(@floor(playerPos[0])), @intFromFloat(@floor(playerPos[1])), @intFromFloat(@floor(playerPos[2]))); + c.glUniform3f(deferredUniforms.playerPositionFraction, @floatCast(@mod(playerPos[0], 1)), @floatCast(@mod(playerPos[1], 1)), @floatCast(@mod(playerPos[2], 1))); + c.glUniform1f(deferredUniforms.zNear, zNear); + c.glUniform1f(deferredUniforms.zFar, zFar); + c.glUniform2f(deferredUniforms.tanXY, 1.0/game.projectionMatrix.rows[0][0], 1.0/game.projectionMatrix.rows[1][2]); + + c.glBindFramebuffer(c.GL_FRAMEBUFFER, activeFrameBuffer); + + c.glBindVertexArray(graphics.draw.rectVAO); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + + c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + + if(!main.gui.hideGui) entity.ClientEntityManager.renderNames(game.projectionMatrix, playerPos); + gpu_performance_measuring.stopQuery(); +} + +const Bloom = struct { // MARK: Bloom + var buffer1: graphics.FrameBuffer = undefined; + var buffer2: graphics.FrameBuffer = undefined; + var emptyBuffer: graphics.Texture = undefined; + var width: u31 = std.math.maxInt(u31); + var height: u31 = std.math.maxInt(u31); + var firstPassPipeline: graphics.Pipeline = undefined; + var secondPassPipeline: graphics.Pipeline = undefined; + var colorExtractAndDownsamplePipeline: graphics.Pipeline = undefined; + var colorExtractUniforms: struct { + zNear: c_int, + zFar: c_int, + tanXY: c_int, + @"fog.color": c_int, + @"fog.density": c_int, + @"fog.fogLower": c_int, + @"fog.fogHigher": c_int, + invViewMatrix: c_int, + playerPositionInteger: c_int, + playerPositionFraction: c_int, + } = undefined; + + pub fn init() void { + buffer1.init(false, c.GL_LINEAR, c.GL_CLAMP_TO_EDGE); + buffer2.init(false, c.GL_LINEAR, c.GL_CLAMP_TO_EDGE); + emptyBuffer = .init(); + emptyBuffer.generate(graphics.Image.emptyImage); + firstPassPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/bloom/first_pass.vert", + "assets/cubyz/shaders/bloom/first_pass.frag", + "", + null, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.noBlending}}, + ); + secondPassPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/bloom/second_pass.vert", + "assets/cubyz/shaders/bloom/second_pass.frag", + "", + null, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.noBlending}}, + ); + colorExtractAndDownsamplePipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/bloom/color_extractor_downsample.vert", + "assets/cubyz/shaders/bloom/color_extractor_downsample.frag", + "", + &colorExtractUniforms, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.noBlending}}, + ); + } + + pub fn deinit() void { + buffer1.deinit(); + buffer2.deinit(); + firstPassPipeline.deinit(); + secondPassPipeline.deinit(); + colorExtractAndDownsamplePipeline.deinit(); + } + + fn extractImageDataAndDownsample(playerBlock: blocks.Block, playerPos: Vec3d, viewMatrix: Mat4f) void { + colorExtractAndDownsamplePipeline.bind(null); + worldFrameBuffer.bindTexture(c.GL_TEXTURE3); + worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE4); + buffer1.bind(); + if(!blocks.meshes.hasFog(playerBlock)) { + c.glUniform3fv(colorExtractUniforms.@"fog.color", 1, @ptrCast(&game.fog.fogColor)); + c.glUniform1f(colorExtractUniforms.@"fog.density", game.fog.density); + c.glUniform1f(colorExtractUniforms.@"fog.fogLower", game.fog.fogLower); + c.glUniform1f(colorExtractUniforms.@"fog.fogHigher", game.fog.fogHigher); + } else { + const fogColor = blocks.meshes.fogColor(playerBlock); + c.glUniform3f(colorExtractUniforms.@"fog.color", @as(f32, @floatFromInt(fogColor >> 16 & 255))/255.0, @as(f32, @floatFromInt(fogColor >> 8 & 255))/255.0, @as(f32, @floatFromInt(fogColor >> 0 & 255))/255.0); + c.glUniform1f(colorExtractUniforms.@"fog.density", blocks.meshes.fogDensity(playerBlock)); + c.glUniform1f(colorExtractUniforms.@"fog.fogLower", 1e10); + c.glUniform1f(colorExtractUniforms.@"fog.fogHigher", 1e10); + } + + c.glUniformMatrix4fv(colorExtractUniforms.invViewMatrix, 1, c.GL_TRUE, @ptrCast(&viewMatrix.transpose())); + c.glUniform3i(colorExtractUniforms.playerPositionInteger, @intFromFloat(@floor(playerPos[0])), @intFromFloat(@floor(playerPos[1])), @intFromFloat(@floor(playerPos[2]))); + c.glUniform3f(colorExtractUniforms.playerPositionFraction, @floatCast(@mod(playerPos[0], 1)), @floatCast(@mod(playerPos[1], 1)), @floatCast(@mod(playerPos[2], 1))); + c.glUniform1f(colorExtractUniforms.zNear, zNear); + c.glUniform1f(colorExtractUniforms.zFar, zFar); + c.glUniform2f(colorExtractUniforms.tanXY, 1.0/game.projectionMatrix.rows[0][0], 1.0/game.projectionMatrix.rows[1][2]); + c.glBindVertexArray(graphics.draw.rectVAO); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + fn firstPass() void { + firstPassPipeline.bind(null); + buffer1.bindTexture(c.GL_TEXTURE3); + buffer2.bind(); + c.glBindVertexArray(graphics.draw.rectVAO); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + fn secondPass() void { + secondPassPipeline.bind(null); + buffer2.bindTexture(c.GL_TEXTURE3); + buffer1.bind(); + c.glBindVertexArray(graphics.draw.rectVAO); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + fn render(currentWidth: u31, currentHeight: u31, playerBlock: blocks.Block, playerPos: Vec3d, viewMatrix: Mat4f) void { + if(width != currentWidth or height != currentHeight) { + width = currentWidth; + height = currentHeight; + buffer1.updateSize(width/4, height/4, c.GL_R11F_G11F_B10F); + std.debug.assert(buffer1.validate()); + buffer2.updateSize(width/4, height/4, c.GL_R11F_G11F_B10F); + std.debug.assert(buffer2.validate()); + } + gpu_performance_measuring.startQuery(.bloom_extract_downsample); + + c.glViewport(0, 0, width/4, height/4); + extractImageDataAndDownsample(playerBlock, playerPos, viewMatrix); + gpu_performance_measuring.stopQuery(); + gpu_performance_measuring.startQuery(.bloom_first_pass); + firstPass(); + gpu_performance_measuring.stopQuery(); + gpu_performance_measuring.startQuery(.bloom_second_pass); + secondPass(); + + c.glViewport(0, 0, width, height); + buffer1.bindTexture(c.GL_TEXTURE5); + + gpu_performance_measuring.stopQuery(); + } + + fn bindReplacementImage() void { + emptyBuffer.bindTo(5); + } +}; + +pub const MenuBackGround = struct { + var pipeline: graphics.Pipeline = undefined; + var uniforms: struct { + viewMatrix: c_int, + projectionMatrix: c_int, + } = undefined; + + var vao: c_uint = undefined; + var vbos: [2]c_uint = undefined; + var texture: graphics.Texture = undefined; + + var angle: f32 = 0; + + fn init() void { + pipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/background/vertex.vert", + "assets/cubyz/shaders/background/fragment.frag", + "", + &uniforms, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.noBlending}}, + ); + // 4 sides of a simple cube with some panorama texture on it. + const rawData = [_]f32{ + -1, 1, -1, 1, 1, + -1, 1, 1, 1, 0, + -1, -1, -1, 0.75, 1, + -1, -1, 1, 0.75, 0, + 1, -1, -1, 0.5, 1, + 1, -1, 1, 0.5, 0, + 1, 1, -1, 0.25, 1, + 1, 1, 1, 0.25, 0, + -1, 1, -1, 0, 1, + -1, 1, 1, 0, 0, + }; + + const indices = [_]c_int{ + 0, 1, 2, + 2, 3, 1, + 2, 3, 4, + 4, 5, 3, + 4, 5, 6, + 6, 7, 5, + 6, 7, 8, + 8, 9, 7, + }; + + c.glGenVertexArrays(1, &vao); + c.glBindVertexArray(vao); + c.glGenBuffers(2, &vbos); + c.glBindBuffer(c.GL_ARRAY_BUFFER, vbos[0]); + c.glBufferData(c.GL_ARRAY_BUFFER, @intCast(rawData.len*@sizeOf(f32)), &rawData, c.GL_STATIC_DRAW); + c.glVertexAttribPointer(0, 3, c.GL_FLOAT, c.GL_FALSE, 5*@sizeOf(f32), null); + c.glVertexAttribPointer(1, 2, c.GL_FLOAT, c.GL_FALSE, 5*@sizeOf(f32), @ptrFromInt(3*@sizeOf(f32))); + c.glEnableVertexAttribArray(0); + c.glEnableVertexAttribArray(1); + c.glBindBuffer(c.GL_ELEMENT_ARRAY_BUFFER, vbos[1]); + c.glBufferData(c.GL_ELEMENT_ARRAY_BUFFER, @intCast(indices.len*@sizeOf(c_int)), &indices, c.GL_STATIC_DRAW); + + const backgroundPath = chooseBackgroundImagePath(main.stackAllocator) catch |err| { + std.log.err("Couldn't open background path: {s}", .{@errorName(err)}); + texture = .{.textureID = 0}; + return; + }; + defer main.stackAllocator.free(backgroundPath); + texture = graphics.Texture.initFromFile(backgroundPath); + } + + fn chooseBackgroundImagePath(allocator: main.heap.NeverFailingAllocator) ![]const u8 { + var dir = try main.files.cubyzDir().openIterableDir("backgrounds"); + defer dir.close(); + + // Whenever the version changes copy over the new background image and display it. + if(!std.mem.eql(u8, settings.lastVersionString, settings.version.version)) { + const defaultImageData = try main.files.cwd().read(main.stackAllocator, "assets/cubyz/default_background.png"); + defer main.stackAllocator.free(defaultImageData); + try dir.write("default_background.png", defaultImageData); + + return std.fmt.allocPrint(allocator.allocator, "{s}/backgrounds/default_background.png", .{main.files.cubyzDirStr()}) catch unreachable; + } + + // Otherwise load a random texture from the backgrounds folder. The player may make their own pictures which can be chosen as well. + var walker = dir.walk(main.stackAllocator); + defer walker.deinit(); + var fileList = main.List([]const u8).init(main.stackAllocator); + defer { + for(fileList.items) |fileName| { + main.stackAllocator.free(fileName); + } + fileList.deinit(); + } + + while(try walker.next()) |entry| { + if(entry.kind == .file and std.ascii.endsWithIgnoreCase(entry.basename, ".png")) { + fileList.append(main.stackAllocator.dupe(u8, entry.path)); + } + } + if(fileList.items.len == 0) { + return error.NoBackgroundImagesFound; + } + const theChosenOne = main.random.nextIntBounded(u32, &main.seed, @as(u32, @intCast(fileList.items.len))); + return std.fmt.allocPrint(allocator.allocator, "{s}/backgrounds/{s}", .{main.files.cubyzDirStr(), fileList.items[theChosenOne]}) catch unreachable; + } + + pub fn deinit() void { + pipeline.deinit(); + c.glDeleteVertexArrays(1, &vao); + c.glDeleteBuffers(2, &vbos); + } + + pub fn hasImage() bool { + return texture.textureID != 0; + } + + pub fn render(deltaTime: f64) void { + c.glViewport(0, 0, main.Window.width, main.Window.height); + if(texture.textureID == 0) return; + + // Use a simple rotation around the z axis, with a steadily increasing angle. + angle += @as(f32, @floatCast(deltaTime))/20.0; + const viewMatrix = Mat4f.rotationZ(angle); + pipeline.bind(null); + c.glUniformMatrix4fv(uniforms.viewMatrix, 1, c.GL_TRUE, @ptrCast(&viewMatrix)); + c.glUniformMatrix4fv(uniforms.projectionMatrix, 1, c.GL_TRUE, @ptrCast(&game.projectionMatrix)); + + texture.bindTo(0); + + c.glBindVertexArray(vao); + c.glDrawElements(c.GL_TRIANGLES, 24, c.GL_UNSIGNED_INT, null); + } + + pub fn takeBackgroundImage() void { + const size: usize = 1024; // Use a power of 2 here, to reduce video memory waste. + const pixels: []u32 = main.stackAllocator.alloc(u32, size*size); + defer main.stackAllocator.free(pixels); + + // Change the viewport and the matrices to render 4 cube faces: + + const oldResolutionScale = main.settings.resolutionScale; + main.settings.resolutionScale = 1; + updateViewport(size, size); + updateFov(90.0); + defer updateFov(main.settings.fov); + main.settings.resolutionScale = oldResolutionScale; + defer updateViewport(Window.width, Window.height); + + var buffer: graphics.FrameBuffer = undefined; + buffer.init(true, c.GL_NEAREST, c.GL_REPEAT); + defer buffer.deinit(); + buffer.updateSize(size, size, c.GL_RGBA8); + + activeFrameBuffer = buffer.frameBuffer; + defer activeFrameBuffer = 0; + + const oldRotation = game.camera.rotation; + defer game.camera.rotation = oldRotation; + + const angles = [_]f32{std.math.pi/2.0, std.math.pi, std.math.pi*3/2.0, std.math.pi*2}; + + // All 4 sides are stored in a single image. + const image = graphics.Image.init(main.stackAllocator, 4*size, size); + defer image.deinit(main.stackAllocator); + + for(0..4) |i| { + c.glDepthFunc(c.GL_LESS); + c.glDepthMask(c.GL_TRUE); + c.glDisable(c.GL_SCISSOR_TEST); + game.camera.rotation = .{0, 0, angles[i]}; + // Draw to frame buffer. + buffer.bind(); + c.glClear(c.GL_DEPTH_BUFFER_BIT | c.GL_STENCIL_BUFFER_BIT | c.GL_COLOR_BUFFER_BIT); + main.renderer.render(game.Player.getEyePosBlocking(), 0); + // Copy the pixels directly from OpenGL + buffer.bind(); + c.glReadPixels(0, 0, size, size, c.GL_RGBA, c.GL_UNSIGNED_BYTE, pixels.ptr); + + for(0..size) |y| { + for(0..size) |x| { + const index = x + y*size; + // Needs to flip the image in y-direction. + image.setRGB(x + size*i, size - 1 - y, @bitCast(pixels[index])); + } + } + } + c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + + const fileName = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/backgrounds/{s}_{}.png", .{main.files.cubyzDirStr(), game.world.?.name, game.world.?.gameTime.load(.monotonic)}) catch unreachable; + defer main.stackAllocator.free(fileName); + image.exportToFile(fileName) catch |err| { + std.log.err("Cannot write file {s} due to {s}", .{fileName, @errorName(err)}); + }; + // TODO: Performance is terrible even with -O3. Consider using qoi instead. + } +}; + +pub const Skybox = struct { + var starPipeline: graphics.Pipeline = undefined; + var starUniforms: struct { + mvp: c_int, + starOpacity: c_int, + } = undefined; + + var starVao: c_uint = undefined; + + var starSsbo: graphics.SSBO = undefined; + + const numStars = 10000; + + fn getStarPos(seed: *u64) Vec3f { + const x: f32 = @floatCast(main.random.nextFloatGauss(seed)); + const y: f32 = @floatCast(main.random.nextFloatGauss(seed)); + const z: f32 = @floatCast(main.random.nextFloatGauss(seed)); + + const r = std.math.cbrt(main.random.nextFloat(seed))*5000.0; + + return vec.normalize(Vec3f{x, y, z})*@as(Vec3f, @splat(r)); + } + + fn getStarColor(temperature: f32, light: f32, image: graphics.Image) Vec3f { + const rgbCol = image.getRGB(@intFromFloat(std.math.clamp(temperature/15000.0*@as(f32, @floatFromInt(image.width)), 0.0, @as(f32, @floatFromInt(image.width - 1)))), 0); + var rgb: Vec3f = @floatFromInt(Vec3i{rgbCol.r, rgbCol.g, rgbCol.b}); + rgb /= @splat(255.0); + + rgb *= @as(Vec3f, @splat(light)); + + const m = @reduce(.Max, rgb); + if(m > 1.0) { + rgb /= @as(Vec3f, @splat(m)); + } + + return rgb; + } + + fn init() void { + const starColorImage = graphics.Image.readFromFile(main.stackAllocator, "assets/cubyz/star.png") catch |err| { + std.log.err("Failed to load star image: {s}", .{@errorName(err)}); + return; + }; + defer starColorImage.deinit(main.stackAllocator); + + starPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/skybox/star.vert", + "assets/cubyz/shaders/skybox/star.frag", + "", + &starUniforms, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.{ + .srcColorBlendFactor = .one, + .dstColorBlendFactor = .one, + .colorBlendOp = .add, + .srcAlphaBlendFactor = .one, + .dstAlphaBlendFactor = .one, + .alphaBlendOp = .add, + }}}, + ); + + var starData: [numStars*20]f32 = undefined; + + const starDist = 200.0; + + const off: f32 = @sqrt(3.0)/6.0; + + const triVertA = Vec3f{0.5, starDist, -off}; + const triVertB = Vec3f{-0.5, starDist, -off}; + const triVertC = Vec3f{0.0, starDist, @sqrt(3.0)/2.0 - off}; + + var seed: u64 = 0; + + for(0..numStars) |i| { + var pos: Vec3f = undefined; + + var radius: f32 = undefined; + + var temperature: f32 = undefined; + + var light: f32 = 0; + + while(light < 0.1) { + pos = getStarPos(&seed); + + radius = @floatCast(main.random.nextFloatExp(&seed)*4 + 0.2); + + temperature = @floatCast(@abs(main.random.nextFloatGauss(&seed)*3000.0 + 5000.0) + 1000.0); + + // 3.6e-12 can be modified to change the brightness of the stars + light = (3.6e-12*radius*radius*temperature*temperature*temperature*temperature)/(vec.dot(pos, pos)); + } + + pos = vec.normalize(pos)*@as(Vec3f, @splat(starDist)); + + const normPos = vec.normalize(pos); + + const color = getStarColor(temperature, light, starColorImage); + + const latitude: f32 = @floatCast(std.math.asin(normPos[2])); + const longitude: f32 = @floatCast(std.math.atan2(-normPos[0], normPos[1])); + + const mat = Mat4f.rotationZ(longitude).mul(Mat4f.rotationX(latitude)); + + const posA = vec.xyz(mat.mulVec(.{triVertA[0], triVertA[1], triVertA[2], 1.0})); + const posB = vec.xyz(mat.mulVec(.{triVertB[0], triVertB[1], triVertB[2], 1.0})); + const posC = vec.xyz(mat.mulVec(.{triVertC[0], triVertC[1], triVertC[2], 1.0})); + + starData[i*20 ..][0..3].* = posA; + starData[i*20 + 4 ..][0..3].* = posB; + starData[i*20 + 8 ..][0..3].* = posC; + + starData[i*20 + 12 ..][0..3].* = pos; + starData[i*20 + 16 ..][0..3].* = color; + } + + starSsbo = graphics.SSBO.initStatic(f32, &starData); + + c.glGenVertexArrays(1, &starVao); + c.glBindVertexArray(starVao); + c.glEnableVertexAttribArray(0); + } + + pub fn deinit() void { + starPipeline.deinit(); + starSsbo.deinit(); + c.glDeleteVertexArrays(1, &starVao); + } + + pub fn render() void { + const viewMatrix = game.camera.viewMatrix; + + const time = game.world.?.gameTime.load(.monotonic); + + var starOpacity: f32 = 0; + const dayTime = @abs(@mod(time, game.World.dayCycle) -% game.World.dayCycle/2); + if(dayTime < game.World.dayCycle/4 - game.World.dayCycle/16) { + starOpacity = 1; + } else if(dayTime > game.World.dayCycle/4 + game.World.dayCycle/16) { + starOpacity = 0; + } else { + starOpacity = 1 - @as(f32, @floatFromInt(dayTime - (game.World.dayCycle/4 - game.World.dayCycle/16)))/@as(f32, @floatFromInt(game.World.dayCycle/8)); + } + + if(starOpacity != 0) { + starPipeline.bind(null); + + const starMatrix = game.projectionMatrix.mul(viewMatrix.mul(Mat4f.rotationX(@as(f32, @floatFromInt(time))/@as(f32, @floatFromInt(main.game.World.dayCycle))))); + + starSsbo.bind(12); + + c.glUniform1f(starUniforms.starOpacity, starOpacity); + c.glUniformMatrix4fv(starUniforms.mvp, 1, c.GL_TRUE, @ptrCast(&starMatrix)); + + c.glBindVertexArray(starVao); + c.glDrawArrays(c.GL_TRIANGLES, 0, numStars*3); + + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); + } + } +}; + +pub const Frustum = struct { // MARK: Frustum + const Plane = struct { + pos: Vec3f, + norm: Vec3f, + }; + planes: [4]Plane, // Who cares about the near/far plane anyways? + + pub fn init(cameraPos: Vec3f, rotationMatrix: Mat4f, fovY: f32, width: u31, height: u31) Frustum { + const invRotationMatrix = rotationMatrix.transpose(); + const cameraDir = vec.xyz(invRotationMatrix.mulVec(Vec4f{0, 1, 0, 1})); + const cameraUp = vec.xyz(invRotationMatrix.mulVec(Vec4f{0, 0, 1, 1})); + const cameraRight = vec.xyz(invRotationMatrix.mulVec(Vec4f{1, 0, 0, 1})); + + const halfVSide = std.math.tan(std.math.degreesToRadians(fovY)*0.5); + const halfHSide = halfVSide*@as(f32, @floatFromInt(width))/@as(f32, @floatFromInt(height)); + + var self: Frustum = undefined; + self.planes[0] = Plane{.pos = cameraPos, .norm = vec.cross(cameraUp, cameraDir + cameraRight*@as(Vec3f, @splat(halfHSide)))}; // right + self.planes[1] = Plane{.pos = cameraPos, .norm = vec.cross(cameraDir - cameraRight*@as(Vec3f, @splat(halfHSide)), cameraUp)}; // left + self.planes[2] = Plane{.pos = cameraPos, .norm = vec.cross(cameraRight, cameraDir - cameraUp*@as(Vec3f, @splat(halfVSide)))}; // top + self.planes[3] = Plane{.pos = cameraPos, .norm = vec.cross(cameraDir + cameraUp*@as(Vec3f, @splat(halfVSide)), cameraRight)}; // bottom + return self; + } + + pub fn testAAB(self: Frustum, pos: Vec3f, dim: Vec3f) bool { + inline for(self.planes) |plane| { + var dist: f32 = vec.dot(pos - plane.pos, plane.norm); + // Find the most positive corner: + dist += @reduce(.Add, @max(Vec3f{0, 0, 0}, dim*plane.norm)); + if(dist < 0) return false; + } + return true; + } +}; + +pub const MeshSelection = struct { // MARK: MeshSelection + var pipeline: graphics.Pipeline = undefined; + var uniforms: struct { + projectionMatrix: c_int, + viewMatrix: c_int, + modelPosition: c_int, + lowerBounds: c_int, + upperBounds: c_int, + lineSize: c_int, + } = undefined; + + pub fn init() void { + pipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/block_selection_vertex.vert", + "assets/cubyz/shaders/block_selection_fragment.frag", + "", + &uniforms, + .{.cullMode = .none}, + .{.depthTest = true, .depthWrite = true}, + .{.attachments = &.{.alphaBlending}}, + ); + } + + pub fn deinit() void { + pipeline.deinit(); + } + + var posBeforeBlock: Vec3i = undefined; + var neighborOfSelection: chunk.Neighbor = undefined; + pub var selectedBlockPos: ?Vec3i = null; + var lastSelectedBlockPos: Vec3i = undefined; + var currentBlockProgress: f32 = 0; + var currentSwingProgress: f32 = 0; + var currentSwingTime: f32 = 0; + var selectionMin: Vec3f = undefined; + var selectionMax: Vec3f = undefined; + var selectionFace: chunk.Neighbor = undefined; + var lastPos: Vec3d = undefined; + var lastDir: Vec3f = undefined; + pub fn select(pos: Vec3d, _dir: Vec3f, item: main.items.Item) void { + lastPos = pos; + const dir: Vec3d = @floatCast(_dir); + lastDir = _dir; + + // Test blocks: + const closestDistance: f64 = 6.0; // selection now limited + // Implementation of "A Fast Voxel Traversal Algorithm for Ray Tracing" http://www.cse.yorku.ca/~amana/research/grid.pdf + const step: Vec3i = @intFromFloat(std.math.sign(dir)); + const invDir = @as(Vec3d, @splat(1))/dir; + const tDelta = @abs(invDir); + var tMax = (@floor(pos) - pos)*invDir; + tMax = @max(tMax, tMax + tDelta*@as(Vec3f, @floatFromInt(step))); + tMax = @select(f64, dir == @as(Vec3d, @splat(0)), @as(Vec3d, @splat(std.math.inf(f64))), tMax); + var voxelPos: Vec3i = @intFromFloat(@floor(pos)); + + var total_tMax: f64 = 0; + + selectedBlockPos = null; + + while(total_tMax < closestDistance) { + const block = mesh_storage.getBlockFromRenderThread(voxelPos[0], voxelPos[1], voxelPos[2]) orelse break; + if(block.typ != 0) blk: { + const fluidPlaceable = item == .baseItem and item.baseItem.hasTag(.fluidPlaceable); + const holdingTargetedBlock = item == .baseItem and item.baseItem.block() == block.typ; + if(block.hasTag(.air) and !holdingTargetedBlock) break :blk; + if(block.hasTag(.fluid) and !fluidPlaceable and !holdingTargetedBlock) break :blk; // TODO: Buckets could select fluids + const relativePlayerPos: Vec3f = @floatCast(pos - @as(Vec3d, @floatFromInt(voxelPos))); + if(block.mode().rayIntersection(block, item, relativePlayerPos, _dir)) |intersection| { + if(intersection.distance <= closestDistance) { + selectedBlockPos = voxelPos; + selectionMin = intersection.min; + selectionMax = intersection.max; + selectionFace = intersection.face; + break; + } + } + } + posBeforeBlock = voxelPos; + if(tMax[0] < tMax[1]) { + if(tMax[0] < tMax[2]) { + voxelPos[0] +%= step[0]; + total_tMax = tMax[0]; + tMax[0] += tDelta[0]; + neighborOfSelection = if(step[0] == 1) .dirPosX else .dirNegX; + } else { + voxelPos[2] +%= step[2]; + total_tMax = tMax[2]; + tMax[2] += tDelta[2]; + neighborOfSelection = if(step[2] == 1) .dirUp else .dirDown; + } + } else { + if(tMax[1] < tMax[2]) { + voxelPos[1] +%= step[1]; + total_tMax = tMax[1]; + tMax[1] += tDelta[1]; + neighborOfSelection = if(step[1] == 1) .dirPosY else .dirNegY; + } else { + voxelPos[2] +%= step[2]; + total_tMax = tMax[2]; + tMax[2] += tDelta[2]; + neighborOfSelection = if(step[2] == 1) .dirUp else .dirDown; + } + } + } + // TODO: Test entities + } + + fn canPlaceBlock(pos: Vec3i, block: main.blocks.Block) bool { + if(main.game.collision.collideWithBlock(block, pos[0], pos[1], pos[2], main.game.Player.getPosBlocking() + main.game.Player.outerBoundingBox.center(), main.game.Player.outerBoundingBox.extent(), .{0, 0, 0}) != null) { + return false; + } + return true; // TODO: Check other entities + } + + pub fn placeBlock(inventory: main.items.Inventory, slot: u32) void { + if(selectedBlockPos) |selectedPos| { + var oldBlock = mesh_storage.getBlockFromRenderThread(selectedPos[0], selectedPos[1], selectedPos[2]) orelse return; + var block = oldBlock; + switch(inventory.getItem(slot)) { + .baseItem => |baseItem| { + if(baseItem.block()) |itemBlock| { + const rotationMode = blocks.Block.mode(.{.typ = itemBlock, .data = 0}); + var neighborDir = Vec3i{0, 0, 0}; + // Check if stuff can be added to the block itself: + if(itemBlock == block.typ) { + const relPos: Vec3f = @floatCast(lastPos - @as(Vec3d, @floatFromInt(selectedPos))); + if(rotationMode.generateData(main.game.world.?, selectedPos, relPos, lastDir, neighborDir, null, &block, .{.typ = 0, .data = 0}, false)) { + if(!canPlaceBlock(selectedPos, block)) return; + updateBlockAndSendUpdate(inventory, slot, selectedPos[0], selectedPos[1], selectedPos[2], oldBlock, block); + return; + } + } else { + if(rotationMode.modifyBlock(&block, itemBlock)) { + if(!canPlaceBlock(selectedPos, block)) return; + updateBlockAndSendUpdate(inventory, slot, selectedPos[0], selectedPos[1], selectedPos[2], oldBlock, block); + return; + } + } + // Check the block in front of it: + const neighborPos = posBeforeBlock; + neighborDir = selectedPos - posBeforeBlock; + const relPos: Vec3f = @floatCast(lastPos - @as(Vec3d, @floatFromInt(neighborPos))); + const neighborBlock = block; + oldBlock = mesh_storage.getBlockFromRenderThread(neighborPos[0], neighborPos[1], neighborPos[2]) orelse return; + block = oldBlock; + if(block.typ == itemBlock) { + if(rotationMode.generateData(main.game.world.?, neighborPos, relPos, lastDir, neighborDir, neighborOfSelection, &block, neighborBlock, false)) { + if(!canPlaceBlock(neighborPos, block)) return; + updateBlockAndSendUpdate(inventory, slot, neighborPos[0], neighborPos[1], neighborPos[2], oldBlock, block); + return; + } + } else { + if(!block.replacable()) return; + block.typ = itemBlock; + block.data = 0; + if(rotationMode.generateData(main.game.world.?, neighborPos, relPos, lastDir, neighborDir, neighborOfSelection, &block, neighborBlock, true)) { + if(!canPlaceBlock(neighborPos, block)) return; + updateBlockAndSendUpdate(inventory, slot, neighborPos[0], neighborPos[1], neighborPos[2], oldBlock, block); + return; + } + } + } + if(std.mem.eql(u8, baseItem.id(), "cubyz:selection_wand")) { + game.Player.selectionPosition2 = selectedPos; + main.network.protocols.genericUpdate.sendWorldEditPos(main.game.world.?.conn, .selectedPos2, selectedPos); + return; + } + }, + .tool => |tool| { + _ = tool; // TODO: Tools might change existing blocks. + }, + .null => {}, + } + } + } + + pub fn breakBlock(inventory: main.items.Inventory, slot: u32, deltaTime: f64) void { + if(selectedBlockPos) |selectedPos| { + const stack = inventory.getStack(slot); + const isSelectionWand = stack.item == .baseItem and std.mem.eql(u8, stack.item.baseItem.id(), "cubyz:selection_wand"); + if(isSelectionWand) { + game.Player.selectionPosition1 = selectedPos; + main.network.protocols.genericUpdate.sendWorldEditPos(main.game.world.?.conn, .selectedPos1, selectedPos); + return; + } + + if(@reduce(.Or, lastSelectedBlockPos != selectedPos)) { + mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); + lastSelectedBlockPos = selectedPos; + currentBlockProgress = 0; + } + const block = mesh_storage.getBlockFromRenderThread(selectedPos[0], selectedPos[1], selectedPos[2]) orelse return; + const holdingTargetedBlock = stack.item == .baseItem and stack.item.baseItem.block() == block.typ; + if((block.hasTag(.fluid) or block.hasTag(.air)) and !holdingTargetedBlock) return; + + const relPos: Vec3f = @floatCast(lastPos - @as(Vec3d, @floatFromInt(selectedPos))); + + main.items.Inventory.Sync.ClientSide.mutex.lock(); + if(!game.Player.isCreative()) { + var damage: f32 = main.game.Player.defaultBlockDamage; + const isTool = stack.item == .tool; + if(isTool) { + damage = stack.item.tool.getBlockDamage(block); + } + damage -= block.blockResistance(); + if(damage > 0) { + const swingTime = if(isTool and stack.item.tool.isEffectiveOn(block)) 1.0/stack.item.tool.swingSpeed else 0.5; + if(currentSwingTime != swingTime) { + currentSwingProgress = 0; + currentSwingTime = swingTime; + } + currentSwingProgress += @floatCast(deltaTime); + while(currentSwingProgress > currentSwingTime) { + currentSwingProgress -= currentSwingTime; + currentBlockProgress += damage/block.blockHealth(); + if(currentBlockProgress > 1) break; + } + if(currentBlockProgress < 1) { + mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); + if(currentBlockProgress != 0) { + mesh_storage.addBreakingAnimation(lastSelectedBlockPos, currentBlockProgress); + } + main.items.Inventory.Sync.ClientSide.mutex.unlock(); + + return; + } else { + currentSwingProgress += (currentBlockProgress - 1)*block.blockHealth()/damage*currentSwingTime; + mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); + currentBlockProgress = 0; + } + } else { + main.items.Inventory.Sync.ClientSide.mutex.unlock(); + return; + } + } + + var newBlock = block; + block.mode().onBlockBreaking(inventory.getStack(slot).item, relPos, lastDir, &newBlock); + main.items.Inventory.Sync.ClientSide.mutex.unlock(); + + if(newBlock != block) { + updateBlockAndSendUpdate(inventory, slot, selectedPos[0], selectedPos[1], selectedPos[2], block, newBlock); + } + } + } + + fn updateBlockAndSendUpdate(source: main.items.Inventory, slot: u32, x: i32, y: i32, z: i32, oldBlock: blocks.Block, newBlock: blocks.Block) void { + main.items.Inventory.Sync.ClientSide.executeCommand(.{ + .updateBlock = .{ + .source = .{.inv = source, .slot = slot}, + .pos = .{x, y, z}, + .dropLocation = .{ + .dir = selectionFace, + .min = selectionMin, + .max = selectionMax, + }, + .oldBlock = oldBlock, + .newBlock = newBlock, + }, + }); + mesh_storage.updateBlock(.{.x = x, .y = y, .z = z, .newBlock = newBlock, .blockEntityData = &.{}}); + } + + pub fn drawCube(projectionMatrix: Mat4f, viewMatrix: Mat4f, relativePositionToPlayer: Vec3d, min: Vec3f, max: Vec3f) void { + pipeline.bind(null); + + c.glUniformMatrix4fv(uniforms.projectionMatrix, 1, c.GL_TRUE, @ptrCast(&projectionMatrix)); + c.glUniformMatrix4fv(uniforms.viewMatrix, 1, c.GL_TRUE, @ptrCast(&viewMatrix)); + + c.glUniform3f( + uniforms.modelPosition, + @floatCast(relativePositionToPlayer[0]), + @floatCast(relativePositionToPlayer[1]), + @floatCast(relativePositionToPlayer[2]), + ); + c.glUniform3f(uniforms.lowerBounds, min[0], min[1], min[2]); + c.glUniform3f(uniforms.upperBounds, max[0], max[1], max[2]); + c.glUniform1f(uniforms.lineSize, 1.0/128.0); + + c.glBindVertexArray(main.renderer.chunk_meshing.vao); + c.glDrawElements(c.GL_TRIANGLES, 12*6*6, c.GL_UNSIGNED_INT, null); + } + + pub fn render(projectionMatrix: Mat4f, viewMatrix: Mat4f, playerPos: Vec3d) void { + if(main.gui.hideGui) return; + if(selectedBlockPos) |_selectedBlockPos| { + drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(_selectedBlockPos)) - playerPos, selectionMin, selectionMax); + } + if(game.Player.selectionPosition1) |pos1| { + if(game.Player.selectionPosition2) |pos2| { + const bottomLeft: Vec3i = @min(pos1, pos2); + const topRight: Vec3i = @max(pos1, pos2); + drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(bottomLeft)) - playerPos, .{0, 0, 0}, @floatFromInt(topRight - bottomLeft + Vec3i{1, 1, 1})); + } + } + } +}; diff --git a/rotation.zig b/rotation.zig new file mode 100644 index 0000000000..bc084d2a09 --- /dev/null +++ b/rotation.zig @@ -0,0 +1,220 @@ +const std = @import("std"); + +const blocks = @import("blocks.zig"); +const Block = blocks.Block; +const chunk = @import("chunk.zig"); +const Neighbor = chunk.Neighbor; +const main = @import("main"); +const ModelIndex = main.models.ModelIndex; +const Tag = main.Tag; +const vec = main.vec; +const Vec3i = vec.Vec3i; +const Vec3f = vec.Vec3f; +const Mat4f = vec.Mat4f; +const ZonElement = main.ZonElement; + +const list = @import("rotation"); + +pub const RayIntersectionResult = struct { + distance: f64, + min: Vec3f, + max: Vec3f, + face: Neighbor, +}; + +pub const Degrees = enum(u2) { + @"0" = 0, + @"90" = 1, + @"180" = 2, + @"270" = 3, +}; + +// TODO: Why not just use a tagged union? +/// Each block gets 16 bit of additional storage(apart from the reference to the block type). +/// These 16 bits are accessed and interpreted by the `RotationMode`. +/// With the `RotationMode` interface there is almost no limit to what can be done with those 16 bit. +pub const RotationMode = struct { // MARK: RotationMode + pub const DefaultFunctions = struct { + pub fn model(block: Block) ModelIndex { + return blocks.meshes.modelIndexStart(block); + } + pub fn rotateZ(data: u16, _: Degrees) u16 { + return data; + } + pub fn generateData(_: *main.game.World, _: Vec3i, _: Vec3f, _: Vec3f, _: Vec3i, _: ?Neighbor, _: *Block, _: Block, blockPlacing: bool) bool { + return blockPlacing; + } + pub fn createBlockModel(_: Block, _: *u16, zon: ZonElement) ModelIndex { + return main.models.getModelIndex(zon.as([]const u8, "cubyz:cube")); + } + pub fn updateData(_: *Block, _: Neighbor, _: Block) bool { + return false; + } + pub fn modifyBlock(_: *Block, _: u16) bool { + return false; + } + pub fn rayIntersection(block: Block, _: main.items.Item, relativePlayerPos: Vec3f, playerDir: Vec3f) ?RayIntersectionResult { + return rayModelIntersection(blocks.meshes.model(block), relativePlayerPos, playerDir); + } + pub fn rayModelIntersection(modelIndex: ModelIndex, relativePlayerPos: Vec3f, playerDir: Vec3f) ?RayIntersectionResult { + // Check the true bounding box (using this algorithm here: https://tavianator.com/2011/ray_box.html): + const invDir = @as(Vec3f, @splat(1))/playerDir; + const modelData = modelIndex.model(); + const min: Vec3f = modelData.min; + const max: Vec3f = modelData.max; + const t1 = (min - relativePlayerPos)*invDir; + const t2 = (max - relativePlayerPos)*invDir; + const boxTMin = @reduce(.Max, @min(t1, t2)); + const boxTMax = @reduce(.Min, @max(t1, t2)); + if(boxTMin <= boxTMax and boxTMax > 0) { + var face: Neighbor = undefined; + if(boxTMin == t1[0]) { + face = Neighbor.dirNegX; + } else if(boxTMin == t1[1]) { + face = Neighbor.dirNegY; + } else if(boxTMin == t1[2]) { + face = Neighbor.dirDown; + } else if(boxTMin == t2[0]) { + face = Neighbor.dirPosX; + } else if(boxTMin == t2[1]) { + face = Neighbor.dirPosY; + } else if(boxTMin == t2[2]) { + face = Neighbor.dirUp; + } else { + unreachable; + } + return .{ + .distance = boxTMin, + .min = min, + .max = max, + .face = face, + }; + } + return null; + } + pub fn onBlockBreaking(_: main.items.Item, _: Vec3f, _: Vec3f, currentData: *Block) void { + currentData.* = .{.typ = 0, .data = 0}; + } + pub fn canBeChangedInto(oldBlock: Block, newBlock: Block, item: main.items.ItemStack, shouldDropSourceBlockOnSuccess: *bool) CanBeChangedInto { + shouldDropSourceBlockOnSuccess.* = true; + if(oldBlock == newBlock) return .no; + if(oldBlock.typ == newBlock.typ) return .yes; + if(!oldBlock.replacable()) { + var damage: f32 = main.game.Player.defaultBlockDamage; + const isTool = item.item == .tool; + if(isTool) { + damage = item.item.tool.getBlockDamage(oldBlock); + } + damage -= oldBlock.blockResistance(); + if(damage > 0) { + if(isTool and item.item.tool.isEffectiveOn(oldBlock)) { + return .{.yes_costsDurability = 1}; + } else return .yes; + } + } else { + if(item.item == .baseItem) { + if(item.item.baseItem.block() != null and item.item.baseItem.block().? == newBlock.typ) { + return .{.yes_costsItems = 1}; + } + } + if(newBlock.typ == 0) { + return .yes; + } + } + return .no; + } + pub fn getBlockTags() []const Tag { + return &.{}; + } + }; + + pub const CanBeChangedInto = union(enum) { + no: void, + yes: void, + yes_costsDurability: u16, + yes_costsItems: u16, + yes_dropsItems: u16, + }; + + /// if the block should be destroyed or changed when a certain neighbor is removed. + dependsOnNeighbors: bool = false, + + /// The default rotation data intended for generation algorithms + naturalStandard: u16 = 0, + + model: *const fn(block: Block) ModelIndex = &DefaultFunctions.model, + + // Rotates block data counterclockwise around the Z axis. + rotateZ: *const fn(data: u16, angle: Degrees) u16 = DefaultFunctions.rotateZ, + + createBlockModel: *const fn(block: Block, modeData: *u16, zon: ZonElement) ModelIndex = &DefaultFunctions.createBlockModel, + + /// Updates the block data of a block in the world or places a block in the world. + /// return true if the placing was successful, false otherwise. + generateData: *const fn(world: *main.game.World, pos: Vec3i, relativePlayerPos: Vec3f, playerDir: Vec3f, relativeDir: Vec3i, neighbor: ?Neighbor, currentData: *Block, neighborBlock: Block, blockPlacing: bool) bool = DefaultFunctions.generateData, + + /// Updates data of a placed block if the RotationMode dependsOnNeighbors. + updateData: *const fn(block: *Block, neighbor: Neighbor, neighborBlock: Block) bool = &DefaultFunctions.updateData, + + modifyBlock: *const fn(block: *Block, newType: u16) bool = DefaultFunctions.modifyBlock, + + rayIntersection: *const fn(block: Block, item: main.items.Item, relativePlayerPos: Vec3f, playerDir: Vec3f) ?RayIntersectionResult = &DefaultFunctions.rayIntersection, + + onBlockBreaking: *const fn(item: main.items.Item, relativePlayerPos: Vec3f, playerDir: Vec3f, currentData: *Block) void = &DefaultFunctions.onBlockBreaking, + + canBeChangedInto: *const fn(oldBlock: Block, newBlock: Block, item: main.items.ItemStack, shouldDropSourceBlockOnSuccess: *bool) CanBeChangedInto = DefaultFunctions.canBeChangedInto, + + getBlockTags: *const fn() []const Tag = DefaultFunctions.getBlockTags, +}; + +var rotationModes: std.StringHashMap(RotationMode) = undefined; + +pub fn rotationMatrixTransform(quad: *main.models.QuadInfo, transformMatrix: Mat4f) void { + quad.normal = vec.xyz(Mat4f.mulVec(transformMatrix, vec.combine(quad.normal, 0))); + for(&quad.corners) |*corner| { + corner.* = vec.xyz(Mat4f.mulVec(transformMatrix, vec.combine(corner.* - Vec3f{0.5, 0.5, 0.5}, 1))) + Vec3f{0.5, 0.5, 0.5}; + } +} + +// MARK: init/register + +pub fn init() void { + rotationModes = .init(main.globalAllocator.allocator); + inline for(@typeInfo(list).@"struct".decls) |declaration| { + register(declaration.name, @field(list, declaration.name)); + } +} + +pub fn reset() void { + inline for(@typeInfo(list).@"struct".decls) |declaration| { + @field(list, declaration.name).reset(); + } +} + +pub fn deinit() void { + rotationModes.deinit(); + inline for(@typeInfo(list).@"struct".decls) |declaration| { + @field(list, declaration.name).deinit(); + } +} + +pub fn getByID(id: []const u8) *const RotationMode { + if(rotationModes.getPtr(id)) |mode| return mode; + std.log.err("Could not find rotation mode {s}. Using cubyz:no_rotation instead.", .{id}); + return rotationModes.getPtr("cubyz:no_rotation").?; +} + +pub fn register(comptime id: []const u8, comptime Mode: type) void { + Mode.init(); + var result: RotationMode = RotationMode{}; + inline for(@typeInfo(RotationMode).@"struct".fields) |field| { + if(@hasDecl(Mode, field.name)) { + if(field.type == @TypeOf(@field(Mode, field.name))) { + @field(result, field.name) = @field(Mode, field.name); + } else { + @field(result, field.name) = &@field(Mode, field.name); + } + } + } + rotationModes.putNoClobber(id, result) catch unreachable; +} diff --git a/server/BlockUpdateSystem.zig b/server/BlockUpdateSystem.zig new file mode 100644 index 0000000000..b53b0348a4 --- /dev/null +++ b/server/BlockUpdateSystem.zig @@ -0,0 +1,45 @@ +const std = @import("std"); + +const main = @import("main"); +const BlockPos = main.chunk.BlockPos; +const ZonElement = main.ZonElement; +const vec = main.vec; +const Vec3i = vec.Vec3i; +const NeverFailingAllocator = main.heap.NeverFailingAllocator; + +list: main.ListUnmanaged(BlockPos) = .{}, +mutex: std.Thread.Mutex = .{}, + +pub fn init() @This() { + return .{}; +} +pub fn deinit(self: *@This()) void { + self.mutex = undefined; + self.list.deinit(main.globalAllocator); +} +pub fn add(self: *@This(), position: BlockPos) void { + self.mutex.lock(); + defer self.mutex.unlock(); + self.list.append(main.globalAllocator, position); +} +pub fn update(self: *@This(), ch: *main.chunk.ServerChunk) void { + // swap + self.mutex.lock(); + const list = self.list; + defer list.deinit(main.globalAllocator); + self.list = .{}; + self.mutex.unlock(); + + // handle events + for(list.items) |event| { + ch.mutex.lock(); + const block = ch.getBlock(event.x, event.y, event.z); + ch.mutex.unlock(); + + _ = block.onUpdate().run(.{ + .block = block, + .chunk = ch, + .blockPos = event, + }); + } +} diff --git a/server/Entity.zig b/server/Entity.zig new file mode 100644 index 0000000000..42226f8e28 --- /dev/null +++ b/server/Entity.zig @@ -0,0 +1,36 @@ +const std = @import("std"); + +const main = @import("main"); +const ZonElement = main.ZonElement; +const vec = main.vec; +const Vec3f = vec.Vec3f; +const Vec3d = vec.Vec3d; +const NeverFailingAllocator = main.heap.NeverFailingAllocator; + +pos: Vec3d = .{0, 0, 0}, +vel: Vec3d = .{0, 0, 0}, +rot: Vec3f = .{0, 0, 0}, + +health: f32 = 8, +maxHealth: f32 = 8, +energy: f32 = 8, +maxEnergy: f32 = 8, +// TODO: Name + +pub fn loadFrom(self: *@This(), zon: ZonElement) void { + self.pos = zon.get(Vec3d, "position", .{0, 0, 0}); + self.vel = zon.get(Vec3d, "velocity", .{0, 0, 0}); + self.rot = zon.get(Vec3f, "rotation", .{0, 0, 0}); + self.health = zon.get(f32, "health", self.maxHealth); + self.energy = zon.get(f32, "energy", self.maxEnergy); +} + +pub fn save(self: *@This(), allocator: NeverFailingAllocator) ZonElement { + const zon = ZonElement.initObject(allocator); + zon.put("position", self.pos); + zon.put("velocity", self.vel); + zon.put("rotation", self.rot); + zon.put("health", self.health); + zon.put("energy", self.energy); + return zon; +} diff --git a/server/SimulationChunk.zig b/server/SimulationChunk.zig new file mode 100644 index 0000000000..6ab214b94e --- /dev/null +++ b/server/SimulationChunk.zig @@ -0,0 +1,72 @@ +const std = @import("std"); + +const main = @import("main"); +const ChunkPosition = main.chunk.ChunkPosition; +const ServerChunk = main.chunk.ServerChunk; +const BlockUpdateSystem = main.server.BlockUpdateSystem; + +const SimulationChunk = @This(); + +chunk: std.atomic.Value(?*ServerChunk) = .init(null), +refCount: std.atomic.Value(u32), +pos: ChunkPosition, +blockUpdateSystem: BlockUpdateSystem, + +pub fn initAndIncreaseRefCount(pos: ChunkPosition) *SimulationChunk { + const self = main.globalAllocator.create(SimulationChunk); + self.* = .{ + .refCount = .init(1), + .pos = pos, + .blockUpdateSystem = .init(), + }; + return self; +} + +fn deinit(self: *SimulationChunk) void { + std.debug.assert(self.refCount.load(.monotonic) == 0); + self.blockUpdateSystem.deinit(); + if(self.chunk.raw) |ch| ch.decreaseRefCount(); + main.globalAllocator.destroy(self); +} + +pub fn increaseRefCount(self: *SimulationChunk) void { + const prevVal = self.refCount.fetchAdd(1, .monotonic); + std.debug.assert(prevVal != 0); +} + +pub fn decreaseRefCount(self: *SimulationChunk) void { + const prevVal = self.refCount.fetchSub(1, .monotonic); + std.debug.assert(prevVal != 0); + if(prevVal == 2) { + main.server.world_zig.ChunkManager.tryRemoveSimulationChunk(self); + } + if(prevVal == 1) { + self.deinit(); + } +} + +pub fn getChunk(self: *SimulationChunk) ?*ServerChunk { + return self.chunk.load(.acquire); +} + +pub fn setChunkAndDecreaseRefCount(self: *SimulationChunk, ch: *ServerChunk) void { + std.debug.assert(self.chunk.swap(ch, .release) == null); +} + +pub fn update(self: *SimulationChunk, randomTickSpeed: u32) void { + const serverChunk = self.getChunk() orelse return; + tickBlocksInChunk(serverChunk, randomTickSpeed); + self.blockUpdateSystem.update(serverChunk); +} + +fn tickBlocksInChunk(_chunk: *ServerChunk, randomTickSpeed: u32) void { + for(0..randomTickSpeed) |_| { + const blockIndex = main.random.nextInt(u15, &main.seed); + const pos = main.chunk.BlockPos.fromIndex(blockIndex); + + _chunk.mutex.lock(); + const block = _chunk.getBlock(pos.x, pos.y, pos.z); + _chunk.mutex.unlock(); + _ = block.onTick().run(.{.block = block, .chunk = _chunk, .blockPos = pos}); + } +} diff --git a/server/command/_command.zig b/server/command/_command.zig new file mode 100644 index 0000000000..7ed24e5137 --- /dev/null +++ b/server/command/_command.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +const main = @import("main"); +const User = main.server.User; + +pub const Command = struct { + exec: *const fn(args: []const u8, source: *User) void, + name: []const u8, + description: []const u8, + usage: []const u8, +}; + +pub var commands: std.StringHashMap(Command) = undefined; + +pub fn init() void { + commands = .init(main.globalAllocator.allocator); + const commandList = @import("_list.zig"); + inline for(@typeInfo(commandList).@"struct".decls) |decl| { + commands.put(decl.name, .{ + .name = decl.name, + .description = @field(commandList, decl.name).description, + .usage = @field(commandList, decl.name).usage, + .exec = &@field(commandList, decl.name).execute, + }) catch unreachable; + std.log.debug("Registered command: '/{s}'", .{decl.name}); + } +} + +pub fn deinit() void { + commands.deinit(); +} + +pub fn execute(msg: []const u8, source: *User) void { + const end = std.mem.indexOfScalar(u8, msg, ' ') orelse msg.len; + const command = msg[0..end]; + if(commands.get(command)) |cmd| { + source.sendMessage("#00ff00Executing Command /{s}", .{msg}); + cmd.exec(msg[@min(end + 1, msg.len)..], source); + } else { + source.sendMessage("#ff0000Unrecognized Command \"{s}\"", .{command}); + } +} diff --git a/server/command/_list.zig b/server/command/_list.zig new file mode 100644 index 0000000000..278ecc0e45 --- /dev/null +++ b/server/command/_list.zig @@ -0,0 +1,23 @@ +pub const clear = @import("clear.zig"); +pub const gamemode = @import("gamemode.zig"); +pub const help = @import("help.zig"); +pub const invite = @import("invite.zig"); +pub const kill = @import("kill.zig"); +pub const setSpawn = @import("setSpawn.zig"); +pub const particles = @import("particles.zig"); +pub const tickspeed = @import("tickspeed.zig"); +pub const time = @import("time.zig"); +pub const tp = @import("tp.zig"); + +pub const undo = @import("worldedit/undo.zig"); +pub const redo = @import("worldedit/redo.zig"); +pub const pos1 = @import("worldedit/pos1.zig"); +pub const pos2 = @import("worldedit/pos2.zig"); +pub const deselect = @import("worldedit/deselect.zig"); +pub const copy = @import("worldedit/copy.zig"); +pub const paste = @import("worldedit/paste.zig"); +pub const blueprint = @import("worldedit/blueprint.zig"); +pub const rotate = @import("worldedit/rotate.zig"); +pub const set = @import("worldedit/set.zig"); +pub const mask = @import("worldedit/mask.zig"); +pub const replace = @import("worldedit/replace.zig"); diff --git a/server/command/clear.zig b/server/command/clear.zig new file mode 100644 index 0000000000..d2d2c09b58 --- /dev/null +++ b/server/command/clear.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +const main = @import("main"); +const User = main.server.User; + +pub const description = "Clears your inventory"; +pub const usage = "/clear"; + +pub fn execute(args: []const u8, source: *User) void { + if(args.len != 0) { + source.sendMessage("#ff0000Too many arguments for command /clear. Expected no arguments.", .{}); + return; + } + main.items.Inventory.Sync.ServerSide.clearPlayerInventory(source); +} diff --git a/server/command/gamemode.zig b/server/command/gamemode.zig new file mode 100644 index 0000000000..851d218f0f --- /dev/null +++ b/server/command/gamemode.zig @@ -0,0 +1,22 @@ +const std = @import("std"); + +const main = @import("main"); +const User = main.server.User; + +pub const description = "Get or set your gamemode."; +pub const usage = "/gamemode\n/gamemode "; + +pub fn execute(args: []const u8, source: *User) void { + if(args.len == 0) { + source.sendMessage("#ffff00{s}", .{@tagName(source.gamemode.load(.monotonic))}); + return; + } + if(std.ascii.eqlIgnoreCase(args, "survival")) { + main.items.Inventory.Sync.setGamemode(source, .survival); + } else if(std.ascii.eqlIgnoreCase(args, "creative")) { + main.items.Inventory.Sync.setGamemode(source, .creative); + } else { + source.sendMessage("#ff0000Invalid argument for command /gamemode. Must be 'survival' or 'creative'.", .{}); + return; + } +} diff --git a/server/command/help.zig b/server/command/help.zig new file mode 100644 index 0000000000..d5a17d4320 --- /dev/null +++ b/server/command/help.zig @@ -0,0 +1,45 @@ +const std = @import("std"); + +const main = @import("main"); +const User = main.server.User; + +const command = @import("_command.zig"); + +pub const description = "Shows info about all the commands."; +pub const usage = "/help\n/help "; + +pub fn execute(args: []const u8, source: *User) void { + var msg = main.List(u8).init(main.stackAllocator); + defer msg.deinit(); + msg.appendSlice("#ffff00"); + if(args.len == 0) { + var iterator = command.commands.valueIterator(); + while(iterator.next()) |cmd| { + msg.append('/'); + msg.appendSlice(cmd.name); + msg.appendSlice(": "); + msg.appendSlice(cmd.description); + msg.append('\n'); + } + msg.appendSlice("\nUse /help for usage of a specific command.\n"); + } else { + var split = std.mem.splitScalar(u8, args, ' '); + while(split.next()) |arg| { + if(command.commands.get(arg)) |cmd| { + msg.append('/'); + msg.appendSlice(cmd.name); + msg.appendSlice(": "); + msg.appendSlice(cmd.description); + msg.append('\n'); + msg.appendSlice(cmd.usage); + msg.append('\n'); + } else { + msg.appendSlice("#ff0000Unrecognized Command \""); + msg.appendSlice(arg); + msg.appendSlice("\"#ffff00\n"); + } + } + } + if(msg.items[msg.items.len - 1] == '\n') _ = msg.pop(); + source.sendMessage("{s}", .{msg.items}); +} diff --git a/server/command/invite.zig b/server/command/invite.zig new file mode 100644 index 0000000000..7a00127580 --- /dev/null +++ b/server/command/invite.zig @@ -0,0 +1,25 @@ +const std = @import("std"); + +const main = @import("main"); +const User = main.server.User; + +pub const description = "Invite a player"; +pub const usage = "/invite "; + +pub fn execute(args: []const u8, source: *User) void { + var split = std.mem.splitScalar(u8, args, ' '); + if(split.next()) |arg| blk: { + if(arg.len == 0) break :blk; + if(split.next() != null) { + source.sendMessage("#ff0000Too many arguments for command /invite", .{}); + } + const user = main.server.User.initAndIncreaseRefCount(main.server.connectionManager, arg) catch |err| { + std.log.err("Error while trying to connect: {s}", .{@errorName(err)}); + source.sendMessage("#ff0000Error while trying to connect: {s}", .{@errorName(err)}); + return; + }; + user.decreaseRefCount(); + return; + } + source.sendMessage("#ff0000Too few arguments for command /invite", .{}); +} diff --git a/server/command/kill.zig b/server/command/kill.zig new file mode 100644 index 0000000000..23f08c4f41 --- /dev/null +++ b/server/command/kill.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +const main = @import("main"); +const User = main.server.User; + +pub const description = "Kills the player"; +pub const usage = "/kill"; + +pub fn execute(args: []const u8, source: *User) void { + if(args.len != 0) { + source.sendMessage("#ff0000Too many arguments for command /kill. Expected no arguments.", .{}); + return; + } + main.items.Inventory.Sync.addHealth(-std.math.floatMax(f32), .kill, .server, source.id); +} diff --git a/server/command/particles.zig b/server/command/particles.zig new file mode 100644 index 0000000000..50d0a41f2e --- /dev/null +++ b/server/command/particles.zig @@ -0,0 +1,86 @@ +const std = @import("std"); + +const main = @import("main"); +const particles = main.particles; +const User = main.server.User; + +pub const description = "Spawns particles."; +pub const usage = + \\/particles + \\/particles + \\/particles + \\ + \\tip: use "~" to apply current player position coordinate in fields. +; + +pub fn execute(args: []const u8, source: *User) void { + parseArguments(source, args) catch |err| { + switch(err) { + error.TooFewArguments => source.sendMessage("#ff0000Too few arguments for command /particles", .{}), + error.TooManyArguments => source.sendMessage("#ff0000Too many arguments for command /particles", .{}), + error.InvalidParticleId => source.sendMessage("#ff0000Invalid particle id", .{}), + error.InvalidBoolean => source.sendMessage("#ff0000Invalid argument. Expected \"true\" or \"false\"", .{}), + error.InvalidNumber => return, + else => source.sendMessage("#ff0000Error: {s}", .{@errorName(err)}), + } + return; + }; +} + +fn parseArguments(source: *User, args: []const u8) anyerror!void { + var split = std.mem.splitScalar(u8, args, ' '); + const particleId = split.next() orelse return error.TooFewArguments; + + const x = try parsePosition(split.next() orelse return error.TooFewArguments, source.player.pos[0], source); + const y = try parsePosition(split.next() orelse return error.TooFewArguments, source.player.pos[1], source); + const z = try parsePosition(split.next() orelse return error.TooFewArguments, source.player.pos[2], source); + const collides = try parseBool(split.next() orelse "true"); + const particleCount = try parseNumber(split.next() orelse "1", source); + if(split.next() != null) return error.TooManyArguments; + + const users = main.server.getUserListAndIncreaseRefCount(main.stackAllocator); + defer main.server.freeUserListAndDecreaseRefCount(main.stackAllocator, users); + for(users) |user| { + main.network.protocols.genericUpdate.sendParticles(user.conn, particleId, .{x, y, z}, collides, particleCount); + } +} + +fn parsePosition(arg: []const u8, playerPos: f64, source: *User) anyerror!f64 { + const hasTilde = if(arg.len == 0) false else arg[0] == '~'; + const numberSlice = if(hasTilde) arg[1..] else arg; + const num: f64 = std.fmt.parseFloat(f64, numberSlice) catch ret: { + if(arg.len > 1 or arg.len == 0) { + source.sendMessage("#ff0000Expected number or \"~\", found \"{s}\"", .{arg}); + return error.InvalidNumber; + } + break :ret 0; + }; + + return if(hasTilde) playerPos + num else num; +} + +fn parseBool(arg: []const u8) anyerror!bool { + if(std.mem.eql(u8, arg, "true")) { + return true; + } else if(std.mem.eql(u8, arg, "false")) { + return false; + } + + return error.InvalidBoolean; +} + +fn parseNumber(arg: []const u8, source: *User) anyerror!u32 { + return std.fmt.parseUnsigned(u32, arg, 0) catch |err| { + switch(err) { + error.Overflow => { + const maxParticleCount = particles.ParticleSystem.maxCapacity; + source.sendMessage("#ff0000Too many particles spawned \"{s}\", maximum: \"{d}\"", .{arg, maxParticleCount}); + return maxParticleCount; + }, + error.InvalidCharacter => { + source.sendMessage("#ff0000Expected number, found \"{s}\"", .{arg}); + return error.InvalidNumber; + }, + } + }; +} diff --git a/server/command/setSpawn.zig b/server/command/setSpawn.zig new file mode 100644 index 0000000000..1b403d80de --- /dev/null +++ b/server/command/setSpawn.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +const main = @import("main"); +const User = main.server.User; + +pub const description = "Sets the spawn point for the player"; +pub const usage = "/setSpawn"; + +pub fn execute(args: []const u8, source: *User) void { + var x: ?f64 = null; + var y: ?f64 = null; + var z: ?f64 = null; + var split = std.mem.splitScalar(u8, args, ' '); + while(split.next()) |arg| { + const num: f64 = std.fmt.parseFloat(f64, arg) catch { + source.sendMessage("#ff0000Expected number, found \"{s}\"", .{arg}); + return; + }; + if(x == null) { + x = num; + } else if(y == null) { + y = num; + } else if(z == null) { + z = num; + } else { + source.sendMessage("#ff0000Too many arguments for command /setspawn", .{}); + return; + } + } + if(x == null or y == null) { + source.sendMessage("#ff0000Too few arguments for command /setspawn", .{}); + return; + } + if(z == null) { + z = source.player.pos[2]; + } + x = std.math.clamp(x.?, -1e9, 1e9); // TODO: Remove after #310 is implemented + y = std.math.clamp(y.?, -1e9, 1e9); + z = std.math.clamp(z.?, -1e9, 1e9); + + main.items.Inventory.Sync.setSpawn(source, .{x.?, y.?, z.?}); +} diff --git a/server/command/tickspeed.zig b/server/command/tickspeed.zig new file mode 100644 index 0000000000..6b5a053b06 --- /dev/null +++ b/server/command/tickspeed.zig @@ -0,0 +1,25 @@ +const std = @import("std"); + +const main = @import("main"); +const User = main.server.User; + +pub const description = "Get or set the server's random tickrate, measured in blocks per chunk per tick."; +pub const usage = "/tickspeed\n/tickspeed "; + +pub fn execute(args: []const u8, source: *User) void { + var split = std.mem.splitScalar(u8, args, ' '); + if(split.next()) |arg| blk: { + if(arg.len == 0) break :blk; + if(split.next() != null) { + source.sendMessage("#ff0000Too many arguments for command /tickspeed", .{}); + return; + } + const tickSpeed = std.fmt.parseInt(u32, arg, 0) catch { + source.sendMessage("#ff0000Expected u32 number, found \"{s}\"", .{arg}); + return; + }; + main.server.world.?.tickSpeed.store(tickSpeed, .monotonic); + return; + } + source.sendMessage("#ffff00{}", .{main.server.world.?.tickSpeed.load(.monotonic)}); +} diff --git a/server/command/time.zig b/server/command/time.zig new file mode 100644 index 0000000000..94da32f023 --- /dev/null +++ b/server/command/time.zig @@ -0,0 +1,38 @@ +const std = @import("std"); + +const main = @import("main"); +const User = main.server.User; + +pub const description = "Get or set the server time."; +pub const usage = "/time\n/time \n/time