Files
VRyHungry1/addons/sweet-logger/sweet_logger.gd
T
2026-08-11 13:24:51 +02:00

334 lines
13 KiB
GDScript

extends Node
const enable_debug = true
# Inside SweetLogger or a static helper class:
static func get_current_function_name() -> String:
var stack = get_stack()
# Index 0 is 'get_current_function_name'
# Index 1 is the function that called 'get_current_function_name'
if stack.size() > 2:
return stack[2].function
return "unknown_func"
static func get_caller_file_name() -> String:
var stack = get_stack()
if stack.size() > 2:
return stack[2].source.get_file() # e.g., "station.gd"
return "unknown_file"
## Global Logger singleton for consistent logging across the game.
## All logs are prefixed with [peer_id]: to identify which instance is logging.
## Uses print_rich with colorized backgrounds for log type, local time, peer IDs, and script context.
# COLORS
#===================================================================================#
## Log type configuration with background color, message background color, and contrast text color
const LOG_TYPES = {
"log": {
"bg_color": "#2d2d2d",
"message_bg_color": "#3d3d3d",
"text_color": "#ffffff"
},
"info": {
"bg_color": "#2563eb",
"message_bg_color": "#3b82f6",
"text_color": "#ffffff"
},
"warning": {
"bg_color": "#fbbf24",
"message_bg_color": "#fcd34d",
"text_color": "#ffffff"
},
"error": {
"bg_color": "#dc2626",
"message_bg_color": "#ef4444",
"text_color": "#ffffff"
},
"debug": {
"bg_color": "#7c3aed",
"message_bg_color": "#a78bfa",
"text_color": "#ffffff"
}
}
## Contrast text color that works on all backgrounds
const CONTRAST_TEXT = "#ffffff"
## Predefined colors for special peer IDs
const SPECIAL_PEER_COLORS = {
"SERVER": {
"bg_color": "#7c3aed",
"text_color": "#ffffff"
},
"DISCONNECTED": {
"bg_color": "#6b7280",
"text_color": "#ffffff"
}
}
const PEER_COLOR_PALETTE = [
{"bg_color": "#10b981", "text_color": "#ffffff"}, # emerald green
{"bg_color": "#06b6d4", "text_color": "#ffffff"}, # cyan
{"bg_color": "#ec4899", "text_color": "#ffffff"}, # fuchsia pink
{"bg_color": "#f97316", "text_color": "#ffffff"}, # orange
{"bg_color": "#14b8a6", "text_color": "#ffffff"}, # teal
{"bg_color": "#84cc16", "text_color": "#000000"}, # lime green
{"bg_color": "#0ea5e9", "text_color": "#ffffff"}, # sky blue
{"bg_color": "#22c55e", "text_color": "#ffffff"}, # green
{"bg_color": "#e11d48", "text_color": "#ffffff"}, # rose red
{"bg_color": "#d946ef", "text_color": "#ffffff"}, # magenta
]
## Background color for the entire log line
const LOG_LINE_BG_COLOR = "#1a1a1a"
## Background color for the script/function name column
const SCRIPT_FUNCTION_BG_COLOR = "#4a5a6a"
## Background color for the local time column (between log type and peer id)
const TIMESTAMP_BG_COLOR = "#1e3a5f"
#===================================================================================#
# WIDTHS
#===================================================================================#
## Column widths for alignment (in characters)
const PEER_ID_COLUMN_WIDTH = 6
const LOG_TYPE_COLUMN_WIDTH = 7
## Default mm:ss:ms; with SHOW_TIMESTAMP_HOURS, hh:mm:ss:ms
const TIMESTAMP_COLUMN_WIDTH_MMSSMS = 9
const TIMESTAMP_COLUMN_WIDTH_HHMMSSMS = 12
const SCRIPT_FUNCTION_COLUMN_WIDTH = 50
const TRUNCATE_PEER_NAME = 6
const TRUNCATE_SCRIPT_NAME = 20
const TRUNCATE_FUNCTION_NAME = 30
#===================================================================================#
# CONFIGURATION
#===================================================================================#
## Cache for peer ID colors to ensure consistency
var _peer_color_cache: Dictionary = {}
## Enable/disable showing script name and function name in logs
@export var SHOW_SCRIPT_NAME = true
@export var SHOW_FUNCTION_NAME = true
## When false (default), timestamp is local mm:ss:ms. When true, local hh:mm:ss:ms (wider column).
@export var SHOW_TIMESTAMP_HOURS = true
#===================================================================================#
# GETTERS
#===================================================================================#
func _get_peer_id() -> String:
"""Get the current peer ID, or return a default identifier if not connected."""
if multiplayer == null:
return "DISCONNECTED"
# Check if we're the server (server always has ID 1, but we check is_server() for clarity)
if multiplayer.is_server():
return "SERVER"
# Get the unique ID (will be 1 for server, or random positive int > 1 for clients)
var peer_id = multiplayer.get_unique_id()
return str(peer_id)
func _get_peer_color_config(peer_id_str: String) -> Dictionary:
"""Get color configuration for a peer ID string."""
# Check special peer IDs first
if SPECIAL_PEER_COLORS.has(peer_id_str):
return SPECIAL_PEER_COLORS[peer_id_str]
# Check cache
if _peer_color_cache.has(peer_id_str):
return _peer_color_cache[peer_id_str]
# Generate consistent color based on peer ID
# Try to parse the peer_id_str as a number
var peer_id_num = peer_id_str.to_int()
var color_index: int
if peer_id_num > 0:
color_index = peer_id_num % PEER_COLOR_PALETTE.size()
else:
# If parsing fails, use hash of the string for consistency
var hash_value = peer_id_str.hash()
color_index = abs(hash_value) % PEER_COLOR_PALETTE.size()
var color_config = PEER_COLOR_PALETTE[color_index].duplicate()
_peer_color_cache[peer_id_str] = color_config
return color_config
#===================================================================================#
# FORMATTERS
#===================================================================================#
func _format_message(message: String, args: Array = []) -> String:
"""Format message with optional arguments."""
if args.is_empty():
return message
var formatted = message
for i in range(args.size()):
formatted = formatted.replace("{" + str(i) + "}", str(args[i]))
return formatted
func _format_rich_text(text: String, bg_color: String, text_color: String) -> String:
"""Format text with background and text colors using BBCode."""
return "[bgcolor=%s][color=%s]%s[/color][/bgcolor]" % [bg_color, text_color, text]
func _format_space_with_bg(bg_color: String) -> String:
"""Format a space character with a background color."""
return "[bgcolor=%s] [/bgcolor]" % bg_color
func _get_padding(text_length: int, width: int) -> String:
"""Get padding string for a given text length and target width."""
if text_length >= width:
return ""
var padding = ""
for i in range(width - text_length):
padding += " "
return padding
func _pad_text(text: String, width: int) -> String:
"""Pad text to a specific width for column alignment."""
return text + _get_padding(text.length(), width)
func _truncate_text(text: String, max_length: int) -> String:
"""Truncate text to a maximum length."""
if text.length() <= max_length:
return text
return text.substr(0, max_length)
func _get_local_timestamp_string() -> String:
"""Local time: mm:ss:ms by default, or hh:mm:ss:ms when SHOW_TIMESTAMP_HOURS is true."""
var unix = Time.get_unix_time_from_system()
var ms = clampi(int(floor(fmod(unix, 1.0) * 1000.0)), 0, 999)
var t = Time.get_time_dict_from_system()
if SHOW_TIMESTAMP_HOURS:
return "%02d:%02d:%02d:%03d" % [t.hour, t.minute, t.second, ms]
return "%02d:%02d:%03d" % [t.minute, t.second, ms]
#===================================================================================#
# PRINT
#===================================================================================#
func _print_rich_log(peer_id_str: String, log_type: String, message: String, script_name: String = "", function_name: String = "") -> void:
"""Print a rich formatted log with peer ID and log type colors."""
var peer_color = _get_peer_color_config(peer_id_str)
var log_config = LOG_TYPES.get(log_type, LOG_TYPES["log"])
# Use peer ID directly without "Peer" prefix
var peer_label = peer_id_str
var log_type_label = log_type.to_upper()
# Pad peer ID column for alignment
var peer_padded = _truncate_text(_pad_text(peer_label, PEER_ID_COLUMN_WIDTH), TRUNCATE_PEER_NAME)
# Format peer ID with its background color
var peer_formatted = _format_rich_text(peer_padded, peer_color.bg_color, peer_color.text_color)
# Format log type with its background color
var log_type_padded = _pad_text(log_type_label, LOG_TYPE_COLUMN_WIDTH)
var log_type_formatted = _format_rich_text(log_type_padded, log_config.bg_color, log_config.text_color)
# Local time column (between log type and peer id)
var ts_width = TIMESTAMP_COLUMN_WIDTH_HHMMSSMS if SHOW_TIMESTAMP_HOURS else TIMESTAMP_COLUMN_WIDTH_MMSSMS
var time_padded = _pad_text(_get_local_timestamp_string(), ts_width)
var time_formatted = _format_rich_text(time_padded, TIMESTAMP_BG_COLOR, CONTRAST_TEXT)
# Build the context string (script name and function name) combined in one column
var context_string = ""
var script_function_parts: Array[String] = []
if SHOW_SCRIPT_NAME and script_name != "":
var script_truncated = _truncate_text(script_name, TRUNCATE_SCRIPT_NAME)
script_function_parts.append(script_truncated)
if SHOW_FUNCTION_NAME and function_name != "":
var function_truncated = _truncate_text(function_name, TRUNCATE_FUNCTION_NAME)
script_function_parts.append(function_truncated)
if not script_function_parts.is_empty():
# Combine script and function names with a separator
var combined = "::".join(script_function_parts)
# Pad, then format with custom background color (so whitespace has background too)
var combined_padded = _pad_text(combined, SCRIPT_FUNCTION_COLUMN_WIDTH)
var combined_formatted = _format_rich_text(combined_padded, SCRIPT_FUNCTION_BG_COLOR, log_config.text_color)
context_string = combined_formatted
# Format message with text color only (no background)
var message_formatted = "[color=%s]%s[/color]" % [log_config.text_color, message]
# Format spaces with peer ID background color
var peer_space = _format_space_with_bg(peer_color.bg_color)
# Combine log type, context, and message with peer-colored spaces
var peer_and_message = peer_formatted
if context_string != "":
peer_and_message += peer_space + context_string
peer_and_message += peer_space + message_formatted
# Create the full log line: log type, timestamp, then peer id and message
var log_line = log_type_formatted + time_formatted + peer_and_message
var wrapped_line = "[bgcolor=%s]%s[/bgcolor]" % [LOG_LINE_BG_COLOR, log_line]
print_rich(wrapped_line)
#===================================================================================#
# CALLERS
#===================================================================================#
func log(message: String, args: Array = [], script_name: String = "", function_name: String = "") -> void:
"""Basic log function with peer_id prefix."""
if script_name.is_empty():
script_name = get_caller_file_name()
if function_name.is_empty():
function_name = get_current_function_name()
var formatted = _format_message(message, args)
var peer_id_str = _get_peer_id()
_print_rich_log(peer_id_str, "log", formatted, script_name, function_name)
func info(message: String, args: Array = [], script_name: String = "", function_name: String = "") -> void:
"""Log an informational message."""
if script_name.is_empty():
script_name = get_caller_file_name()
if function_name.is_empty():
function_name = get_current_function_name()
var formatted = _format_message(message, args)
var peer_id_str = _get_peer_id()
_print_rich_log(peer_id_str, "info", formatted, script_name, function_name)
func warning(message: String, args: Array = [], script_name: String = "", function_name: String = "") -> void:
"""Log a warning message."""
if script_name.is_empty():
script_name = get_caller_file_name()
if function_name.is_empty():
function_name = get_current_function_name()
var formatted = _format_message(message, args)
var peer_id_str = _get_peer_id()
_print_rich_log(peer_id_str, "warning", formatted, script_name, function_name)
func error(message: String, args: Array = [], script_name: String = "", function_name: String = "") -> void:
"""Log an error message."""
if script_name.is_empty():
script_name = get_caller_file_name()
if function_name.is_empty():
function_name = get_current_function_name()
var formatted = _format_message(message, args)
var peer_id_str = _get_peer_id()
_print_rich_log(peer_id_str, "error", formatted, script_name, function_name)
func debug(message: String, args: Array = [], script_name: String = "", function_name: String = "") -> void:
"""Log a debug message."""
if not enable_debug:
return
if script_name.is_empty():
script_name = get_caller_file_name()
if function_name.is_empty():
function_name = get_current_function_name()
var formatted = _format_message(message, args)
var peer_id_str = _get_peer_id()
_print_rich_log(peer_id_str, "debug", formatted, script_name, function_name)
#===================================================================================#