const std = @import("std"); const main = @import("main"); const blocks = main.blocks; const Block = blocks.Block; const Neighbor = main.chunk.Neighbor; const ModelIndex = main.models.ModelIndex; const rotation = main.rotation; const Degrees = rotation.Degrees; const RotationMode = rotation.RotationMode; const vec = main.vec; const Mat4f = vec.Mat4f; const Vec2f = vec.Vec2f; const Vec3f = vec.Vec3f; const Vec3i = vec.Vec3i; const ZonElement = main.ZonElement; pub const dependsOnNeighbors = true; var branchModels: std.HashMap(HashMapKey, ModelIndex, HashMapKey, std.hash_map.default_max_load_percentage) = undefined; const HashMapKey = struct { radius: u16, shellModelId: []const u8, textureSlotOffset: u32, pub fn hash(_: HashMapKey, val: HashMapKey) u64 { var hasher = std.hash.Wyhash.init(0); std.hash.autoHashStrat(&hasher, val, .DeepRecursive); return hasher.final(); } pub fn eql(_: HashMapKey, val1: HashMapKey, val2: HashMapKey) bool { if(val1.radius != val2.radius) return false; if(val1.textureSlotOffset != val2.textureSlotOffset) return false; return std.mem.eql(u8, val1.shellModelId, val2.shellModelId); } }; pub const BranchData = packed struct(u6) { enabledConnections: u6, pub inline fn init(blockData: u16) BranchData { return .{.enabledConnections = @truncate(blockData)}; } pub inline fn isConnected(self: @This(), neighbor: Neighbor) bool { return (self.enabledConnections & Neighbor.bitMask(neighbor)) != 0; } pub inline fn setConnection(self: *@This(), neighbor: Neighbor, value: bool) void { if(value) { self.enabledConnections |= Neighbor.bitMask(neighbor); } else { self.enabledConnections &= ~Neighbor.bitMask(neighbor); } } }; pub fn init() void { branchModels = .initContext(main.globalAllocator.allocator, undefined); } pub fn deinit() void { branchModels.deinit(); } pub fn reset() void { branchModels.clearRetainingCapacity(); } pub const Direction = enum(u2) { negYDir = 0, posXDir = 1, posYDir = 2, negXDir = 3, }; const Pattern = union(enum) { dot: void, halfLine: Direction, line: Direction, bend: Direction, intersection: Direction, cross: void, }; fn rotateQuad(originalCorners: [4]Vec2f, pattern: Pattern, min: f32, max: f32, side: Neighbor, textureSlotOffset: u32) main.models.QuadInfo { var corners: [4]Vec2f = originalCorners; switch(pattern) { .dot, .cross => {}, inline else => |typ| { const angle: f32 = @as(f32, @floatFromInt(@intFromEnum(typ)))*std.math.pi/2.0; corners = .{ vec.rotate2d(originalCorners[0], angle, @splat(0.5)), vec.rotate2d(originalCorners[1], angle, @splat(0.5)), vec.rotate2d(originalCorners[2], angle, @splat(0.5)), vec.rotate2d(originalCorners[3], angle, @splat(0.5)), }; }, } const offX: f32 = @floatFromInt(@intFromBool(@reduce(.Add, side.textureX()) < 0)); const offY: f32 = @floatFromInt(@intFromBool(@reduce(.Add, side.textureY()) < 0)); const corners3d = .{ @as(Vec3f, @floatFromInt(side.textureX()))*@as(Vec3f, @splat(corners[0][0] - offX)) + @as(Vec3f, @floatFromInt(side.textureY()))*@as(Vec3f, @splat(corners[0][1] - offY)), @as(Vec3f, @floatFromInt(side.textureX()))*@as(Vec3f, @splat(corners[1][0] - offX)) + @as(Vec3f, @floatFromInt(side.textureY()))*@as(Vec3f, @splat(corners[1][1] - offY)), @as(Vec3f, @floatFromInt(side.textureX()))*@as(Vec3f, @splat(corners[2][0] - offX)) + @as(Vec3f, @floatFromInt(side.textureY()))*@as(Vec3f, @splat(corners[2][1] - offY)), @as(Vec3f, @floatFromInt(side.textureX()))*@as(Vec3f, @splat(corners[3][0] - offX)) + @as(Vec3f, @floatFromInt(side.textureY()))*@as(Vec3f, @splat(corners[3][1] - offY)), }; var offset: Vec3f = .{0.0, 0.0, 0.0}; offset[@intFromEnum(side.vectorComponent())] = if(side.isPositive()) max else min; const res: main.models.QuadInfo = .{ .corners = .{ corners3d[0] + offset, corners3d[1] + offset, corners3d[2] + offset, corners3d[3] + offset, }, .cornerUV = originalCorners, .normal = @as(Vec3f, @floatFromInt(side.relPos())), .textureSlot = textureSlotOffset + @intFromEnum(pattern), }; return res; } fn addQuads(pattern: Pattern, side: Neighbor, radius: f32, out: *main.List(main.models.QuadInfo), textureSlotOffset: u32) void { const min: f32 = (8.0 - radius)/16.0; const max: f32 = (8.0 + radius)/16.0; switch(pattern) { .dot => { out.append(rotateQuad(.{ .{min, min}, .{min, max}, .{max, min}, .{max, max}, }, pattern, min, max, side, textureSlotOffset)); }, .halfLine => { out.append(rotateQuad(.{ .{min, 0.0}, .{min, max}, .{max, 0.0}, .{max, max}, }, pattern, min, max, side, textureSlotOffset)); }, .line => { out.append(rotateQuad(.{ .{min, 0.0}, .{min, 1.0}, .{max, 0.0}, .{max, 1.0}, }, pattern, min, max, side, textureSlotOffset)); }, .bend => { out.append(rotateQuad(.{ .{0.0, 0.0}, .{0.0, max}, .{max, 0.0}, .{max, max}, }, pattern, min, max, side, textureSlotOffset)); }, .intersection => { out.append(rotateQuad(.{ .{0.0, 0.0}, .{0.0, max}, .{1.0, 0.0}, .{1.0, max}, }, pattern, min, max, side, textureSlotOffset)); }, .cross => { out.append(rotateQuad(.{ .{0.0, 0.0}, .{0.0, 1.0}, .{1.0, 0.0}, .{1.0, 1.0}, }, pattern, min, max, side, textureSlotOffset)); }, } } pub fn getPattern(data: BranchData, side: Neighbor) ?Pattern { const posX = Neighbor.fromRelPos(side.textureX()).?; const negX = Neighbor.fromRelPos(side.textureX()).?.reverse(); const posY = Neighbor.fromRelPos(side.textureY()).?; const negY = Neighbor.fromRelPos(side.textureY()).?.reverse(); const connectedPosX = data.isConnected(posX); const connectedNegX = data.isConnected(negX); const connectedPosY = data.isConnected(posY); const connectedNegY = data.isConnected(negY); const count: u6 = @as(u6, @intFromBool(connectedPosX)) + @as(u6, @intFromBool(connectedNegX)) + @as(u6, @intFromBool(connectedPosY)) + @as(u6, @intFromBool(connectedNegY)); return switch(count) { 0 => { if(data.isConnected(side)) { return null; } return .dot; }, 1 => { var dir: Direction = .negXDir; if(connectedNegY) { dir = .negYDir; } else if(connectedPosX) { dir = .posXDir; } else if(connectedPosY) { dir = .posYDir; } return .{.halfLine = dir}; }, 2 => { if((connectedPosX and connectedNegX) or (connectedPosY and connectedNegY)) { var dir: Direction = .negYDir; if(connectedPosX and connectedNegX) { dir = .posXDir; } return .{.line = dir}; } var dir: Direction = .negXDir; if(connectedNegY) { dir = .negYDir; if(connectedPosX) { dir = .posXDir; } } else if(connectedPosX) { dir = .posXDir; if(connectedPosY) { dir = .posYDir; } } else if(connectedPosY) { dir = .posYDir; if(connectedNegX) { dir = .negXDir; } } return .{.bend = dir}; }, 3 => { var dir: Direction = undefined; if(!connectedPosY) dir = .negYDir; if(!connectedNegX) dir = .posXDir; if(!connectedNegY) dir = .posYDir; if(!connectedPosX) dir = .negXDir; return .{.intersection = dir}; }, 4 => { return .cross; }, else => undefined, }; } pub fn createBlockModel(_: Block, modeData: *u16, zon: ZonElement) ModelIndex { var radius = zon.get(f32, "radius", 4); const radiusForComparisons = std.math.lossyCast(u16, @round(radius*65536.0/16.0)); radius = @as(f32, @floatFromInt(radiusForComparisons))*16.0/65536.0; modeData.* = radiusForComparisons; const shellModelId = zon.get([]const u8, "shellModel", ""); const textureSlotOffset = zon.get(u32, "textureSlotOffset", 0); if(branchModels.get(.{.radius = radiusForComparisons, .shellModelId = shellModelId, .textureSlotOffset = textureSlotOffset})) |modelIndex| return modelIndex; var shellQuads = main.List(main.models.QuadInfo).init(main.stackAllocator); defer shellQuads.deinit(); if(shellModelId.len != 0) { const shellModel = main.models.getModelIndex(shellModelId).model(); shellModel.getRawFaces(&shellQuads); } var modelIndex: ModelIndex = undefined; for(0..64) |i| { var quads = main.List(main.models.QuadInfo).init(main.stackAllocator); defer quads.deinit(); quads.appendSlice(shellQuads.items); for(Neighbor.iterable) |neighbor| { const pattern = getPattern(BranchData.init(@intCast(i)), neighbor); if(pattern) |pat| { addQuads(pat, neighbor, radius, &quads, textureSlotOffset); } } const index = main.models.Model.init(quads.items); if(i == 0) { modelIndex = index; } } branchModels.put(.{.radius = radiusForComparisons, .shellModelId = shellModelId, .textureSlotOffset = textureSlotOffset}, modelIndex) catch unreachable; return modelIndex; } pub fn model(block: Block) ModelIndex { return .{.index = blocks.meshes.modelIndexStart(block).index + (block.data & 63)}; } pub fn rotateZ(data: u16, angle: Degrees) u16 { @setEvalBranchQuota(65_536); comptime var rotationTable: [4][16]u8 = undefined; comptime for(0..16) |i| { rotationTable[0][i] = @intCast(i << 2); }; comptime for(1..4) |a| { for(0..16) |i| { const old: BranchData = .init(rotationTable[a - 1][i]); var new: BranchData = .init(0); new.setConnection(Neighbor.dirPosX.rotateZ(), old.isConnected(Neighbor.dirPosX)); new.setConnection(Neighbor.dirNegX.rotateZ(), old.isConnected(Neighbor.dirNegX)); new.setConnection(Neighbor.dirPosY.rotateZ(), old.isConnected(Neighbor.dirPosY)); new.setConnection(Neighbor.dirNegY.rotateZ(), old.isConnected(Neighbor.dirNegY)); rotationTable[a][i] = new.enabledConnections; } }; if(data >= 0b111111) return 0; const rotationIndex = (data & 0b111100) >> 2; const upDownFlags = data & 0b000011; return rotationTable[@intFromEnum(angle)][rotationIndex] | upDownFlags; } pub fn generateData( _: *main.game.World, _: Vec3i, _: Vec3f, _: Vec3f, _: Vec3i, neighbor: ?Neighbor, currentBlock: *Block, neighborBlock: Block, blockPlacing: bool, ) bool { const canConnectToNeighbor = currentBlock.mode() == neighborBlock.mode() and currentBlock.modeData() == neighborBlock.modeData(); if(blockPlacing or canConnectToNeighbor or !neighborBlock.replacable()) { const neighborModel = blocks.meshes.model(neighborBlock).model(); var currentData = BranchData.init(currentBlock.data); // Branch block upon placement should extend towards a block it was placed // on if the block is solid or also uses branch model. const targetVal = ((!neighborBlock.replacable() and (!neighborBlock.viewThrough() or canConnectToNeighbor)) and (canConnectToNeighbor or neighborModel.isNeighborOccluded[neighbor.?.reverse().toInt()])); currentData.setConnection(neighbor.?, targetVal); const result: u16 = currentData.enabledConnections; if(result == currentBlock.data) return false; currentBlock.data = result; return true; } return false; } pub fn updateData(block: *Block, neighbor: Neighbor, neighborBlock: Block) bool { const canConnectToNeighbor = block.mode() == neighborBlock.mode() and block.modeData() == neighborBlock.modeData(); var currentData = BranchData.init(block.data); // Handle joining with other branches. While placed, branches extend in a // opposite direction than they were placed from, effectively connecting // to the block they were placed at. if(canConnectToNeighbor) { const neighborData = BranchData.init(neighborBlock.data); currentData.setConnection(neighbor, neighborData.isConnected(neighbor.reverse())); } else if(neighborBlock.replacable()) { currentData.setConnection(neighbor, false); } const result: u16 = currentData.enabledConnections; if(result == block.data) return false; block.data = result; return true; } fn closestRay(block: Block, relativePlayerPos: Vec3f, playerDir: Vec3f) ?u16 { var closestIntersectionDistance: f64 = std.math.inf(f64); var resultBitMask: ?u16 = null; { const modelIndex = blocks.meshes.modelIndexStart(block); if(RotationMode.DefaultFunctions.rayModelIntersection(modelIndex, relativePlayerPos, playerDir)) |intersection| { closestIntersectionDistance = intersection.distance; resultBitMask = 0; } } for(Neighbor.iterable) |direction| { const directionBitMask = Neighbor.bitMask(direction); if((block.data & directionBitMask) != 0) { const modelIndex = ModelIndex{.index = blocks.meshes.modelIndexStart(block).index + directionBitMask}; if(RotationMode.DefaultFunctions.rayModelIntersection(modelIndex, relativePlayerPos, playerDir)) |intersection| { if(@abs(closestIntersectionDistance) > @abs(intersection.distance)) { closestIntersectionDistance = intersection.distance; resultBitMask = direction.bitMask(); } } } } return resultBitMask; } pub fn onBlockBreaking(_: ?main.items.Item, relativePlayerPos: Vec3f, playerDir: Vec3f, currentData: *Block) void { if(closestRay(currentData.*, relativePlayerPos, playerDir)) |directionBitMask| { // If player destroys a central part of branch block, branch block is completely destroyed. if(directionBitMask == 0) { currentData.typ = 0; currentData.data = 0; return; } // Otherwise only the connection player aimed at is destroyed. currentData.data &= ~directionBitMask; } }