diff --git a/src/graphics.zig b/src/graphics.zig index 8aab3219c7..97f9796468 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -647,7 +647,7 @@ pub const TextBuffer = struct { // MARK: TextBuffer return next[0]; } - fn parse(self: *Parser) void { + pub fn parse(self: *Parser) void { self.curIndex = @intCast(self.unicodeIterator.i); self.curChar = self.unicodeIterator.nextCodepoint() orelse return; while (true) { diff --git a/src/log.zig b/src/log.zig new file mode 100644 index 0000000000..77beb1f3fa --- /dev/null +++ b/src/log.zig @@ -0,0 +1,250 @@ +const std = @import("std"); +const main = @import("main"); +const files = main.files; +const fmt = main.fmt; +const graphics = main.graphics; +const gui = main.gui; +const List = main.List; +const settings = main.settings; + +pub const Level = enum { + /// Error: something has gone wrong. This might be recoverable or might + /// be followed by the program exiting. + err, + /// Warning: it is uncertain if something has gone wrong or not, but the + /// circumstances would be worth investigating. + warn, + /// Info: general messages about the state of the program. + info, + /// Debug: messages only useful for debugging. + debug, + /// server messages + server, + /// chat messages + chat, + + fn isColorCoded(self: Level) bool { + return self == .chat or self == .server; + } + + fn fromStdLevel(level: std.log.Level) Level { + return switch (level) { + .err => .err, + .warn => .warn, + .info => .info, + .debug => .debug, + }; + } +}; + +var logFile: ?std.Io.File = undefined; +var logFileTs: ?std.Io.File = undefined; +var supportsANSIColors: bool = undefined; +var openingErrorWindow: bool = false; + +pub fn logFn( + comptime level: std.log.Level, + comptime _: @EnumLiteral(), + comptime format: []const u8, + args: anytype, +) void { + var runtimeArgs: [args.len]fmt.FormatArg = undefined; + inline for (0..args.len) |i| { + runtimeArgs[i] = .fromAnytype(@TypeOf(args[i]), &args[i]); + } + + runtimeLogFn(.fromStdLevel(level), format, &runtimeArgs); +} + +noinline fn runtimeLogFn(level: Level, format: []const u8, args: []const fmt.FormatArg) void { + var buf: [65536]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + fmt.format(&writer, format, args) catch { + std.log.err("Truncated long log message.", .{}); + }; + + const color: []const u8 = switch (level) { + .err => "\x1b[31m", + .info => "", + .warn => "\x1b[33m", + .debug => "\x1b[37;44m", + .server => "\x1b[34mserver\x1b[0m: ", + .chat => "\x1b[36mchat\x1b[0m: ", + }; + const colorReset = "\x1b[0m\n"; + const filePrefix = switch (level) { + .err => "error", + .warn => "warning", + .info => "info", + .debug => "debug", + .server => "server", + .chat => "chat", + }; + const fileSuffix = "\n"; + + logToFile("[{s}]: {s}{s}", .{filePrefix, writer.buffered(), fileSuffix}); + if (supportsANSIColors) { + logToStdErr(level, "{s}{s}{s}", .{color, writer.buffered(), colorReset}); + } else { + logToStdErr(level, "[{s}]: {s}{s}", .{filePrefix, writer.buffered(), fileSuffix}); + } + + if (level == .err and !openingErrorWindow and !settings.launchConfig.headlessServer) { + openingErrorWindow = true; + gui.openWindow("error_prompt"); + openingErrorWindow = false; + } +} + +pub fn init() void { + logFile = null; + files.cwd().makePath("logs") catch |err| { + std.log.err("Couldn't create logs folder: {s}", .{@errorName(err)}); + return; + }; + logFile = std.Io.Dir.cwd().createFile(main.io, "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(main.io, .real).raw; + + const _path_str = std.fmt.allocPrint(main.stackAllocator.allocator, "logs/ts_{}.log", .{_timestamp.nanoseconds}) catch unreachable; + defer main.stackAllocator.free(_path_str); + + logFileTs = std.Io.Dir.cwd().createFile(main.io, _path_str, .{}) catch |err| { + std.log.err("Couldn't create {s}: {s}", .{_path_str, @errorName(err)}); + return; + }; + + supportsANSIColors = std.Io.File.stdout().supportsAnsiEscapeCodes(main.io) catch unreachable; +} + +pub fn deinit() void { + if (logFile) |_logFile| { + _logFile.close(main.io); + logFile = null; + } + + if (logFileTs) |_logFileTs| { + _logFileTs.close(main.io); + 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).writeStreamingAll(main.io, string) catch {}; + (logFileTs orelse return).writeStreamingAll(main.io, string) catch {}; +} + +fn logToStdErr(level: Level, 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 string = if (level.isColorCoded() and supportsANSIColors) convertColorToANSI(main.stackAllocator, _string) else _string; + defer if (level.isColorCoded() and supportsANSIColors) main.stackAllocator.free(string); + + const writer = std.debug.lockStderr(&.{}); + defer std.debug.unlockStderr(); + nosuspend writer.file_writer.interface.writeAll(string) catch {}; +} + +fn convertColorToANSI(allocator: main.heap.NeverFailingAllocator, text: []const u8) []const u8 { + var list: List(u8) = .empty; + + var parser = graphics.TextBuffer.Parser{ + .unicodeIterator = std.unicode.Utf8Iterator{.bytes = text, .i = 0}, + .currentFontEffect = .{}, + .parsedText = .init(allocator), + .fontEffects = .init(allocator), + .characterIndex = .init(allocator), + .showControlCharacters = false, + }; + defer parser.fontEffects.deinit(); + defer parser.parsedText.deinit(); + defer parser.characterIndex.deinit(); + parser.parse(); + + var currentFontEffect: graphics.TextBuffer.FontEffect = .{}; + for (0..parser.parsedText.items.len) |i| { + // add actual text at the end + defer blk: { + var testBuff: [3]u8 = undefined; + const len = std.unicode.utf8Encode(@intCast(parser.parsedText.items[i]), &testBuff) catch break :blk; + list.appendSlice(allocator, testBuff[0..len]); + } + if (parser.fontEffects.items[i] == currentFontEffect) continue; + + list.appendSlice(allocator, "\x1b["); + defer list.append(allocator, 'm'); + + var addedEffect: bool = false; + + if (parser.fontEffects.items[i].color != currentFontEffect.color) { + list.appendSlice(allocator, "38;2"); + var shift: u5 = 16; + while (true) : (shift -= 8) { + list.print(allocator, ";{d}", .{@as(u8, @truncate(parser.fontEffects.items[i].color >> shift))}); + if (shift == 0) break; + } + addedEffect = true; + } + if (parser.fontEffects.items[i].bold != currentFontEffect.bold) { + if (addedEffect) list.append(allocator, ';'); + if (!parser.currentFontEffect.bold) { + list.append(allocator, '1'); + } else { + list.appendSlice(allocator, "22"); + } + addedEffect = true; + } + if (parser.fontEffects.items[i].italic != currentFontEffect.italic) { + if (addedEffect) list.append(allocator, ';'); + if (parser.currentFontEffect.italic) { + list.append(allocator, '2'); + } + list.append(allocator, '3'); + addedEffect = true; + } + if (parser.fontEffects.items[i].strikethrough != currentFontEffect.strikethrough) { + if (addedEffect) list.append(allocator, ';'); + if (parser.currentFontEffect.strikethrough) { + list.append(allocator, '2'); + } + list.append(allocator, '9'); + addedEffect = true; + } + if (parser.fontEffects.items[i].underline != currentFontEffect.underline) { + if (addedEffect) list.append(allocator, ';'); + if (parser.currentFontEffect.underline) { + list.append(allocator, '2'); + } + list.append(allocator, '4'); + } + currentFontEffect = parser.fontEffects.items[i]; + } + return list.toOwnedSlice(allocator); +} + +pub fn server(comptime format: []const u8, args: anytype) void { + var runtimeArgs: [args.len]fmt.FormatArg = undefined; + inline for (0..args.len) |i| { + runtimeArgs[i] = .fromAnytype(@TypeOf(args[i]), &args[i]); + } + runtimeLogFn(.server, format, &runtimeArgs); +} + +pub fn chat(comptime format: []const u8, args: anytype) void { + var runtimeArgs: [args.len]fmt.FormatArg = undefined; + inline for (0..args.len) |i| { + runtimeArgs[i] = .fromAnytype(@TypeOf(args[i]), &args[i]); + } + runtimeLogFn(.chat, format, &runtimeArgs); +} diff --git a/src/main.zig b/src/main.zig index 36d5af3ee8..0dde11668d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -22,6 +22,7 @@ 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 log = @import("log.zig"); pub const meta = @import("meta.zig"); pub const migrations = @import("migrations.zig"); pub const models = @import("models.zig"); @@ -86,122 +87,13 @@ fn cacheStringImpl(comptime len: usize, comptime str: [len]u8) []const u8 { fn cacheString(comptime str: []const u8) []const u8 { return cacheStringImpl(str.len, str[0..].*); } -var logFile: ?std.Io.File = undefined; -var logFileTs: ?std.Io.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 _: @EnumLiteral(), - comptime format: []const u8, - args: anytype, - ) void { - var runtimeArgs: [args.len]fmt.FormatArg = undefined; - inline for (0..args.len) |i| { - runtimeArgs[i] = .fromAnytype(@TypeOf(args[i]), &args[i]); - } - runtimeLogFn(level, format, &runtimeArgs); - } - }.logFn, + .logFn = log.logFn, }; -noinline fn runtimeLogFn(level: std.log.Level, format: []const u8, args: []const fmt.FormatArg) void { - var buf: [65536]u8 = undefined; - var writer: std.Io.Writer = .fixed(&buf); - fmt.format(&writer, format, args) catch { - std.log.err("Truncated long log message.", .{}); - }; - - const color: []const u8 = 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 = switch (level) { - .err => "error", - .warn => "warning", - .info => "info", - .debug => "debug", - }; - const fileSuffix = "\n"; - - logToFile("[{s}]: {s}{s}", .{filePrefix, writer.buffered(), fileSuffix}); - if (supportsANSIColors) { - logToStdErr("{s}{s}{s}", .{color, writer.buffered(), colorReset}); - } else { - logToStdErr("[{s}]: {s}{s}", .{filePrefix, writer.buffered(), fileSuffix}); - } - - if (level == .err and !openingErrorWindow and !settings.launchConfig.headlessServer) { - openingErrorWindow = true; - gui.openWindow("error_prompt"); - openingErrorWindow = false; - } -} - -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.Io.Dir.cwd().createFile(io, "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).raw; - - const _path_str = std.fmt.allocPrint(stackAllocator.allocator, "logs/ts_{}.log", .{_timestamp.nanoseconds}) catch unreachable; - defer stackAllocator.free(_path_str); - - logFileTs = std.Io.Dir.cwd().createFile(io, _path_str, .{}) catch |err| { - std.log.err("Couldn't create {s}: {s}", .{_path_str, @errorName(err)}); - return; - }; - - supportsANSIColors = std.Io.File.stdout().supportsAnsiEscapeCodes(io) catch unreachable; -} - -fn deinitLogging() void { - if (logFile) |_logFile| { - _logFile.close(io); - logFile = null; - } - - if (logFileTs) |_logFileTs| { - _logFileTs.close(io); - 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).writeStreamingAll(io, string) catch {}; - (logFileTs orelse return).writeStreamingAll(io, 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.lockStderr(&.{}); - defer std.debug.unlockStderr(); - nosuspend writer.file_writer.interface.writeAll(string) catch {}; -} - // MARK: Callbacks fn escape(mods: Window.Key.Modifiers) void { if (gui.selectedTextInput != null) gui.setSelectedTextInput(null); @@ -434,8 +326,8 @@ pub fn main(args: std.process.Init.Minimal) void { // MARK: main() threadedIo = .init(globalAllocator.allocator, .{}); defer threadedIo.deinit(); - initLogging(); - defer deinitLogging(); + log.init(); + defer log.deinit(); std.log.info("Starting game with version {s}", .{settings.version.version}); diff --git a/src/server/server.zig b/src/server/server.zig index f1e1d229b2..0fb934ec41 100644 --- a/src/server/server.zig +++ b/src/server/server.zig @@ -824,7 +824,7 @@ pub fn messageFrom(msg: []const u8, source: *User) void { // MARK: message fn sendRawMessage(msg: []const u8) void { chatMutex.lock(); defer chatMutex.unlock(); - std.log.info("Chat: {s}", .{msg}); // TODO use color \033[0;32m + main.log.chat("{s}", .{msg}); const userList = getUserListAndIncreaseRefCount(main.stackAllocator); defer freeUserListAndDecreaseRefCount(main.stackAllocator, userList); for (userList) |user| { diff --git a/src/sync.zig b/src/sync.zig index 0dee239a7c..2c994dc557 100644 --- a/src/sync.zig +++ b/src/sync.zig @@ -1751,7 +1751,7 @@ pub const Command = struct { // MARK: Command if (ctx.side == .server) { const user = ctx.user orelse return; if (main.server.world.?.settings.allowCheats) { - std.log.info("User \"{f}\" executed command \"{s}\"", .{user, self.message}); // TODO use color \033[0;32m + main.log.server("User \"{f}ยง#ffffff\" executed command \"{s}\"", .{user, self.message}); main.server.command.execute(self.message, user); } else { user.sendRawMessage("Commands are not allowed because cheats are disabled");