[
  {
    "path": ".github/workflows/.gdlintrc",
    "content": "class-definitions-order:\n- tools\n- classnames\n- extends\n- signals\n- enums\n- consts\n- exports\n- pubvars\n- prvvars\n- onreadypubvars\n- onreadyprvvars\n- others\nclass-load-variable-name: (([A-Z][a-z0-9]*)+|_?[a-z][a-z0-9]*(_[a-z0-9]+)*)\nclass-name: ([A-Z][a-z0-9]*)+\nclass-variable-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)*\ncomparison-with-itself: null\nconstant-name: '[A-Z][A-Z0-9]*(_[A-Z0-9]+)*'\nduplicated-load: null\nenum-element-name: '[A-Z][A-Z0-9]*(_[A-Z0-9]+)*'\nenum-name: ([A-Z][a-z0-9]*)+\nexpression-not-assigned: null\nfunction-argument-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)*\nfunction-arguments-number: 10\nfunction-load-variable-name: ([A-Z][a-z0-9]*)+\nfunction-name: (_on_([A-Z][a-z0-9]*)+(_[a-z0-9]+)*|_?[a-z][a-z0-9]*(_[a-z0-9]+)*)\nfunction-variable-name: '[a-z][a-z0-9]*(_[a-z0-9]+)*'\nload-constant-name: (([A-Z][a-z0-9]*)+|[A-Z][A-Z0-9]*(_[A-Z0-9]+)*)\nloop-variable-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)*\nmax-file-lines: 1000\nmax-line-length: 150\nmax-public-methods: 50\nmixed-tabs-and-spaces: null\nprivate-method-call: null\nsignal-name: '[a-z][a-z0-9]*(_[a-z0-9]+)*'\nsub-class-name: _?([A-Z][a-z0-9]*)+\ntrailing-whitespace: null\nunnecessary-pass: null\nunused-argument: null\ndisable: [class-definitions-order, max-file-lines, max-line-length, max-public-methods]\n"
  },
  {
    "path": ".github/workflows/CodeStyleWorkflow.yml",
    "content": "name: Code style workflow\n\n# Events but only for the master branch\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  format:\n    # The type of runner that the job will run on\n    runs-on: ubuntu-latest\n\n    # Steps represent a sequence of tasks that will be executed as part of the job\n    steps:\n      - uses: actions/checkout@v2\n        with:\n          ref: ${{ github.head_ref }}\n      - name: Set up Python\n        uses: actions/setup-python@v2\n        with:\n          python-version: '3.x'\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install gdtoolkit\n      - name: Format with gdformat\n        run: |\n          gdformat --line-length=150 logger.gd\n      - name: Commit changes from formatting\n        uses: stefanzweifel/git-auto-commit-action@v4.1.2\n        with:\n          commit_message: Apply formatting changes\n          branch: ${{ github.head_ref }}\n  \n  lint:\n    needs: [format]\n\n    # The type of runner that the job will run on\n    runs-on: ubuntu-latest\n\n    # Steps represent a sequence of tasks that will be executed as part of the job\n    steps:\n      - uses: actions/checkout@v2\n        with:\n          ref: ${{ github.head_ref }}\n      - name: Set up Python\n        uses: actions/setup-python@v2\n        with:\n          python-version: '3.x'\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install gdtoolkit\n      - name: Lint the code with gdlint\n        run: |\n          cp './.github/workflows/.gdlintrc' '.'\n          gdlint logger.gd\n          echo \"Done!\"\n"
  },
  {
    "path": "LICENSE.md",
    "content": "Copyright (c) 2016 KOBUGE Games\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "Godot Logger - Logging Addon for Godot Engine\n===========================================\n\nThe *Godot Logger* is a plugin that provides a logging API for\nprojects developed with [Godot Engine](https://godotengine.org).\n\n# Usage\n\n1. Clone or extract this repository as the `res://addons/godot-logger` folder in your project.\n2. Enable the addon from  Project -> Project Settings -> Plugins -> Godot Logger.\n3. An autoloaded script will be added to your project singletons list as `KLogger`.\n\nThe methods of the API can then be accessed from any other script via the *KLogger*\nsingleton:\n```\n  KLogger.warn(\"Alpaca mismatch!\")\n\n  KLogger.add_module(\"mymodule\")\n  KLogger.info(\"Reticulating splines...\", \"mymodule\")\n```\n\nRead the code for details about the API, it's extensively documented.\n\n## Output format configuration\n\nThe `output_format` property can be customized using format fields from the\n`FORMAT_IDS` dictionary. They are placeholders which will be replaced by the\nrelevant content.\n\n**Log format fields**:\n\n* `{LVL}`     = Level of the log\n* `{MOD}`     = Module that does the logging\n* `{MSG}`     = Message from the user\n* `{TIME}`    = Timestamp when the logging occurred\n* `{ERR_MSG}` = Error message corresponding to the error code, if provided.\n                It is automatically prepended with a space.\n\nThe timestamp format can be configured for each module using the `time_format`\nproperty, with the placeholders described below.\n\n**Timestamp fields**:\n\n* `YYYY` = Year\n* `MM` = Month\n* `DD` = Day\n* `hh` = Hour\n* `mm` = Minute\n* `ss` = Second\n* `SSS` = Millisecond\n\n**Error codes:**\n\nAll logging levels can also optionally include an\n[`Error` code](https://docs.godotengine.org/en/stable/classes/class_@globalscope.html?#enum-globalscope-error),\nwhich will be mapped to a human-readable error message.\n\nExample:\n```\nKLogger.error(\"Failed to rotate the albatross\", \"main\", ERR_INVALID_DATA)\n```\n\n### Example\n\n```gdscript\nvar msg = \"Error occurred!\"\n\nKLogger.output_format = \"[{TIME}] [{LVL}] [{MOD}] {MSG}\"\nKLogger.time_format = \"YYYY.MM.DD hh:mm:ss.SSS\"\nKLogger.error(msg)\n\nKLogger.time_format = \"hh:mm:ss\"\nKLogger.error(msg)\n\nKLogger.output_format = \"[{LVL}] {MSG} at {TIME}\"\nKLogger.error(msg)\n```\n\nResults in:\n```\n[2020.10.09 12:10:47.034] [ERROR] [main] Error occurred!\n\n[12:10:47] [ERROR] [main] Error occurred!\n\n[ERROR] Error occurred! at 12:10:47\n```\n\n## Licensing\n\nThe KLogger class and all other files of this repository are distributed under the\nMIT license (see the LICENSE.md file).\n"
  },
  {
    "path": "logger.gd",
    "content": "# Copyright (c) 2016 KOBUGE Games\n# Distributed under the terms of the MIT license.\n# https://github.com/KOBUGE-Games/godot-logger/blob/master/LICENSE.md\n#\n# Upstream repo: https://github.com/KOBUGE-Games/godot-logger\n\nextends Node  # Needed to work as a singleton\n\n##================##\n## Inner classes  ##\n##================##\n\n\nclass ExternalSink:\n\t# Queue modes\n\tenum QUEUE_MODES {\n\t\tNONE = 0,\n\t\tALL = 1,\n\t\tSMART = 2,\n\t}\n\n\tvar name\n\tvar queue_mode\n\tvar buffer = PackedStringArray()\n\tvar buffer_idx = 0\n\n\tfunc _init(_name, _queue_mode = QUEUE_MODES.NONE) -> void:\n\t\tname = _name\n\t\tqueue_mode = _queue_mode\n\n\tfunc flush_buffer():\n\t\t\"\"\"Flush the buffer, i.e. write its contents to the target external sink.\"\"\"\n\t\tprint(\"[ERROR] [logger] Using method which has to be overriden in your custom sink\")\n\n\tfunc write(output, level):\n\t\t\"\"\"Write the string at the end of the sink (append mode), following\n\t\tthe queue mode.\"\"\"\n\t\tprint(\"[ERROR] [logger] Using method which has to be overriden in your custom sink\")\n\n\tfunc set_queue_mode(new_mode):\n\t\tqueue_mode = new_mode\n\n\tfunc get_queue_mode():\n\t\treturn queue_mode\n\n\tfunc get_name():\n\t\treturn name\n\n\tfunc get_config():\n\t\treturn {\n\t\t\t\"queue_mode\": get_queue_mode(),\n\t\t}\n\n\nclass Logfile:\n\textends ExternalSink\n\t# TODO: Godot doesn't support docstrings for inner classes, GoDoIt (GH-1320)\n\t# \"\"\"Class for log files that can be shared between various modules.\"\"\"\n\tconst FILE_BUFFER_SIZE = 30\n\tvar path = \"\"\n\n\tfunc _init(_path, _queue_mode = QUEUE_MODES.NONE):\n\t\tsuper(_path, _queue_mode)\n\t\tif validate_path(_path):\n\t\t\tpath = _path\n\t\tbuffer.resize(FILE_BUFFER_SIZE)\n\n\tfunc get_path():\n\t\treturn path\n\n\tfunc get_write_mode():\n\t\tif not FileAccess.file_exists(path):\n\t\t\treturn FileAccess.WRITE  # create\n\t\telse:\n\t\t\treturn FileAccess.READ_WRITE  # append\n\n\tfunc validate_path(path):\n\t\t\"\"\"Validate the path given as argument, making it possible to write to\n\t\tthe designated file or folder. Returns whether the path is valid.\"\"\"\n\t\tif not (path.is_absolute_path() or path.is_rel_path()):\n\t\t\tprint(\"[ERROR] [logger] The given path '%s' is not valid.\" % path)\n\t\t\treturn false\n\t\tvar base_dir = path.get_base_dir()\n\t\tvar dir = DirAccess.open(base_dir)\n\t\tif not dir:\n\t\t\tvar err = DirAccess.get_open_error()\n\t\t\tif err:\n\t\t\t\tprint(\"[ERROR] [logger] Could not create the '%s' directory; exited with error %d.\" % [base_dir, err])\n\t\t\t\treturn false\n\t\t\telse:\n\t\t\t\t# TODO: Move directory creation to the function that will actually *write*\n\t\t\t\tdir.make_dir_recursive(base_dir)\n\t\t\t\tvar err2 = DirAccess.get_open_error()\n\t\t\t\tif err2:\n\t\t\t\t\tprint(\"[ERROR] [logger] Could not create the '%s' directory; exited with error %d.\" % [base_dir, err2])\n\t\t\t\t\treturn false\n\n\t\t\t\tprint(\"[INFO] [logger] Successfully created the '%s' directory.\" % base_dir)\n\t\treturn true\n\n\tfunc flush_buffer():\n\t\t\"\"\"Flush the buffer, i.e. write its contents to the target file.\"\"\"\n\t\tif buffer_idx == 0:\n\t\t\treturn  # Nothing to write\n\t\tvar temp_file = _open_file(path)\n\t\tif temp_file:\n\t\t\ttemp_file.seek_end()\n\t\t\tfor i in range(buffer_idx):\n\t\t\t\ttemp_file.store_line(buffer[i])\n\t\t\ttemp_file.close()\n\t\t\tbuffer_idx = 0  # We don't clear the memory, we'll just overwrite it\n\n\tfunc write(output, level):\n\t\t\"\"\"Write the string at the end of the file (append mode), following\n\t\tthe queue mode.\"\"\"\n\t\tvar queue_action = queue_mode\n\t\tif queue_action == QUEUE_MODES.SMART:\n\t\t\tif level >= WARN:  # Don't queue warnings and errors\n\t\t\t\tqueue_action = QUEUE_MODES.NONE\n\t\t\t\tflush_buffer()\n\t\t\telse:  # Queue the log, not important enough for \"smart\"\n\t\t\t\tqueue_action = QUEUE_MODES.ALL\n\n\t\tif queue_action == QUEUE_MODES.NONE:\n\t\t\tvar temp_file = _open_file(path)\n\t\t\tif temp_file == null:\n\t\t\t\treturn\n\t\t\telse:\n\t\t\t\ttemp_file.seek_end()\n\t\t\t\ttemp_file.store_line(output)\n\t\t\t\ttemp_file.close()\n\n\t\tif queue_action == QUEUE_MODES.ALL:\n\t\t\tbuffer[buffer_idx] = output\n\t\t\tbuffer_idx += 1\n\t\t\tif buffer_idx >= FILE_BUFFER_SIZE:\n\t\t\t\tflush_buffer()\n\n\tfunc get_config():\n\t\treturn {\n\t\t\t\"path\": get_path(),\n\t\t\t\"queue_mode\": get_queue_mode(),\n\t\t}\n\n\tfunc _open_file(path):\n\t\tvar result = FileAccess.open(path, get_write_mode())\n\n\t\tif result == null:\n\t\t\tvar err = FileAccess.get_open_error()\n\t\t\tprint(\"[ERROR] [logger] Could not open the '%s' log file; exited with error %d.\" % [path, err])\n\t\t\treturn null\n\t\telse:\n\t\t\treturn result\n\n\nclass Module:\n\t# \"\"\"Class for customizable logging modules.\"\"\"\n\tvar name = \"\"\n\tvar output_level = 0\n\tvar output_strategies = []\n\tvar external_sink = null\n\n\tfunc _init(_name, _output_level, _output_strategies, _external_sink):\n\t\tname = _name\n\t\tset_output_level(_output_level)\n\n\t\tif typeof(_output_strategies) == TYPE_INT:  # Only one strategy, use it for all\n\t\t\tfor i in range(0, LEVELS.size()):\n\t\t\t\toutput_strategies.append(_output_strategies)\n\t\telse:\n\t\t\tfor strategy in _output_strategies:  # Need to force deep copy\n\t\t\t\toutput_strategies.append(strategy)\n\n\t\tset_external_sink(_external_sink)\n\n\tfunc get_name():\n\t\treturn name\n\n\tfunc set_output_level(level):\n\t\t\"\"\"Set the custom minimal level for the output of the module.\n\t\tAll levels greater or equal to the given once will be output based\n\t\ton their respective strategies, while levels lower than the given one\n\t\twill be discarded.\"\"\"\n\t\tif not level in range(0, LEVELS.size()):\n\t\t\tprint(\"[ERROR] [%s] The level must be comprised between 0 and %d.\" % [PLUGIN_NAME, LEVELS.size() - 1])\n\t\t\treturn\n\t\toutput_level = level\n\n\tfunc get_output_level():\n\t\treturn output_level\n\n\tfunc set_common_output_strategy(output_strategy_mask):\n\t\t\"\"\"Set the common output strategy mask for all levels of the module.\"\"\"\n\t\tif not output_strategy_mask in range(0, MAX_STRATEGY + 1):\n\t\t\tprint(\"[ERROR] [%s] The output strategy mask must be comprised between 0 and %d.\" % [PLUGIN_NAME, MAX_STRATEGY])\n\t\t\treturn\n\t\tfor i in range(0, LEVELS.size()):\n\t\t\toutput_strategies[i] = output_strategy_mask\n\n\tfunc set_output_strategy(output_strategy_mask, level = -1):\n\t\t\"\"\"Set the output strategy for the given level or (by default) all\n\t\tlevels of the module.\"\"\"\n\t\tif not output_strategy_mask in range(0, MAX_STRATEGY + 1):\n\t\t\tprint(\"[ERROR] [%s] The output strategy mask must be comprised between 0 and %d.\" % [PLUGIN_NAME, MAX_STRATEGY])\n\t\t\treturn\n\t\tif level == -1:  # Set for all levels\n\t\t\tfor i in range(0, LEVELS.size()):\n\t\t\t\toutput_strategies[i] = output_strategy_mask\n\t\telse:\n\t\t\tif not level in range(0, LEVELS.size()):\n\t\t\t\tprint(\"[ERROR] [%s] The level must be comprised between 0 and %d.\" % [PLUGIN_NAME, LEVELS.size() - 1])\n\t\t\t\treturn\n\t\t\toutput_strategies[level] = output_strategy_mask\n\n\tfunc get_output_strategy(level = -1):\n\t\tif level == -1:\n\t\t\treturn output_strategies\n\t\telse:\n\t\t\treturn output_strategies[level]\n\n\tfunc set_external_sink(new_external_sink):\n\t\t\"\"\"Set the external sink instance for the module.\"\"\"\n\t\texternal_sink = new_external_sink\n\n\tfunc get_external_sink():\n\t\treturn external_sink\n\n\tfunc get_config():\n\t\treturn {\n\t\t\t\"name\": get_name(),\n\t\t\t\"output_level\": get_output_level(),\n\t\t\t\"output_strategies\": get_output_strategy(),\n\t\t\t\"external_sink\": get_external_sink().get_config(),\n\t\t}\n\n\n##=============##\n##  Constants  ##\n##=============##\n\nconst PLUGIN_NAME = \"logger\"\n\n# Logging levels - the array and the integers should be matching\nconst LEVELS = [\"VERBOSE\", \"DEBUG\", \"INFO\", \"WARN\", \"ERROR\"]\nconst VERBOSE = 0\nconst DEBUG = 1\nconst INFO = 2\nconst WARN = 3\nconst ERROR = 4\n\n# Output strategies\nconst STRATEGY_MUTE = 0\nconst STRATEGY_PRINT = 1\nconst STRATEGY_EXTERNAL_SINK = 2\nconst STRATEGY_PRINT_AND_EXTERNAL_SINK = STRATEGY_PRINT | STRATEGY_EXTERNAL_SINK\nconst STRATEGY_MEMORY = 4\nconst MAX_STRATEGY = STRATEGY_MEMORY * 2 - 1\n\n# Output format identifiers\nconst FORMAT_IDS = {\n\t\"level\": \"{LVL}\",\n\t\"module\": \"{MOD}\",\n\t\"message\": \"{MSG}\",\n\t\"time\": \"{TIME}\",\n\t\"error_message\": \"{ERR_MSG}\",\n}\n\n# Maps Error code to strings.\n# This might eventually be supported out of the box in Godot,\n# so we'll be able to drop this.\nconst ERROR_MESSAGES = {\n\tOK: \"OK.\",\n\tFAILED: \"Generic error.\",\n\tERR_UNAVAILABLE: \"Unavailable error.\",\n\tERR_UNCONFIGURED: \"Unconfigured error.\",\n\tERR_UNAUTHORIZED: \"Unauthorized error.\",\n\tERR_PARAMETER_RANGE_ERROR: \"Parameter range error.\",\n\tERR_OUT_OF_MEMORY: \"Out of memory (OOM) error.\",\n\tERR_FILE_NOT_FOUND: \"File: Not found error.\",\n\tERR_FILE_BAD_DRIVE: \"File: Bad drive error.\",\n\tERR_FILE_BAD_PATH: \"File: Bad path error.\",\n\tERR_FILE_NO_PERMISSION: \"File: No permission error.\",\n\tERR_FILE_ALREADY_IN_USE: \"File: Already in use error.\",\n\tERR_FILE_CANT_OPEN: \"File: Can't open error.\",\n\tERR_FILE_CANT_WRITE: \"File: Can't write error.\",\n\tERR_FILE_CANT_READ: \"File: Can't read error.\",\n\tERR_FILE_UNRECOGNIZED: \"File: Unrecognized error.\",\n\tERR_FILE_CORRUPT: \"File: Corrupt error.\",\n\tERR_FILE_MISSING_DEPENDENCIES: \"File: Missing dependencies error.\",\n\tERR_FILE_EOF: \"File: End of file (EOF) error.\",\n\tERR_CANT_OPEN: \"Can't open error.\",\n\tERR_CANT_CREATE: \"Can't create error.\",\n\tERR_QUERY_FAILED: \"Query failed error.\",\n\tERR_ALREADY_IN_USE: \"Already in use error.\",\n\tERR_LOCKED: \"Locked error.\",\n\tERR_TIMEOUT: \"Timeout error.\",\n\tERR_CANT_CONNECT: \"Can't connect error.\",\n\tERR_CANT_RESOLVE: \"Can't resolve error.\",\n\tERR_CONNECTION_ERROR: \"Connection error.\",\n\tERR_CANT_ACQUIRE_RESOURCE: \"Can't acquire resource error.\",\n\tERR_CANT_FORK: \"Can't fork process error.\",\n\tERR_INVALID_DATA: \"Invalid data error.\",\n\tERR_INVALID_PARAMETER: \"Invalid parameter error.\",\n\tERR_ALREADY_EXISTS: \"Already exists error.\",\n\tERR_DOES_NOT_EXIST: \"Does not exist error.\",\n\tERR_DATABASE_CANT_READ: \"Database: Read error.\",\n\tERR_DATABASE_CANT_WRITE: \"Database: Write error.\",\n\tERR_COMPILATION_FAILED: \"Compilation failed error.\",\n\tERR_METHOD_NOT_FOUND: \"Method not found error.\",\n\tERR_LINK_FAILED: \"Linking failed error.\",\n\tERR_SCRIPT_FAILED: \"Script failed error.\",\n\tERR_CYCLIC_LINK: \"Cycling link (import cycle) error.\",\n\tERR_INVALID_DECLARATION: \"Invalid declaration error.\",\n\tERR_DUPLICATE_SYMBOL: \"Duplicate symbol error.\",\n\tERR_PARSE_ERROR: \"Parse error.\",\n\tERR_BUSY: \"Busy error.\",\n\tERR_SKIP: \"Skip error.\",\n\tERR_HELP: \"Help error.\",\n\tERR_BUG: \"Bug error.\",\n\tERR_PRINTER_ON_FIRE: \"Printer on fire error.\",\n}\n\n##=============##\n##  Variables  ##\n##=============##\n\n# Configuration\nvar default_output_level = INFO\n# TODO: Find (or implement in Godot) a more clever way to achieve that\n\nvar default_output_strategies = [STRATEGY_PRINT, STRATEGY_PRINT, STRATEGY_PRINT, STRATEGY_PRINT, STRATEGY_PRINT]\nvar default_logfile_path = \"user://%s.log\" % ProjectSettings.get_setting(\"application/config/name\")  # TODO @File\nvar default_configfile_path = \"user://%s.cfg\" % PLUGIN_NAME\n\n# e.g. \"[INFO] [main] The young alpaca started growing a goatie.\"\nvar output_format = \"[{TIME}] [{LVL}] [{MOD}]{ERR_MSG} {MSG}\"\n# Example with all supported placeholders: \"YYYY.MM.DD hh.mm.ss.SSS\"\n# would output e.g.: \"2020.10.09 12:10:47.034\".\nvar time_format = \"hh:mm:ss\"\n\n# Holds the name of the debug module for easy usage across all logging functions.\nvar default_module_name = \"main\"\n\n# Specific to STRATEGY_MEMORY\nvar max_memory_size = 30\nvar memory_buffer = []\nvar memory_idx = 0\nvar memory_first_loop = true\nvar memory_cache = []\nvar invalid_memory_cache = false\n\n# Holds default and custom modules and external sinks defined by the user\n# Default modules are initialized in _init via add_module\nvar external_sinks = {}\nvar modules = {}\n\n##=============##\n##  Functions  ##\n##=============##\n\n\nfunc put(level, message, module = default_module_name, error_code = -1):\n\t\"\"\"Log a message in the given module with the given logging level.\"\"\"\n\tvar module_ref = get_module(module)\n\tvar output_strategy = module_ref.get_output_strategy(level)\n\tif output_strategy == STRATEGY_MUTE or module_ref.get_output_level() > level:\n\t\treturn  # Out of scope\n\n\tvar output = format(output_format, level, module, message, error_code)\n\n\tif output_strategy & STRATEGY_PRINT:\n\t\tprint(output)\n\n\tif output_strategy & STRATEGY_EXTERNAL_SINK:\n\t\tmodule_ref.get_external_sink().write(output, level)\n\n\tif output_strategy & STRATEGY_MEMORY:\n\t\tmemory_buffer[memory_idx] = output\n\t\tmemory_idx += 1\n\t\tinvalid_memory_cache = true\n\t\tif memory_idx >= max_memory_size:\n\t\t\tmemory_idx = 0\n\t\t\tmemory_first_loop = false\n\n\n# Helper functions for each level\n# -------------------------------\n\n\nfunc verbose(message, module = default_module_name, error_code = -1):\n\t\"\"\"Log a message in the given module with level VERBOSE.\"\"\"\n\tput(VERBOSE, message, module, error_code)\n\n\nfunc debug(message, module = default_module_name, error_code = -1):\n\t\"\"\"Log a message in the given module with level DEBUG.\"\"\"\n\tput(DEBUG, message, module, error_code)\n\n\nfunc info(message, module = default_module_name, error_code = -1):\n\t\"\"\"Log a message in the given module with level INFO.\"\"\"\n\tput(INFO, message, module, error_code)\n\n\nfunc warn(message, module = default_module_name, error_code = -1):\n\t\"\"\"Log a message in the given module with level WARN.\"\"\"\n\tput(WARN, message, module, error_code)\n\n\nfunc error(message, module = default_module_name, error_code = -1):\n\t\"\"\"Log a message in the given module with level ERROR.\"\"\"\n\tput(ERROR, message, module, error_code)\n\n\n# Module management\n# -----------------\n\n\nfunc add_module(name, output_level = default_output_level, output_strategies = default_output_strategies, logfile = null):\n\t\"\"\"Add a new module with the given parameter or (by default) the\n\tdefault ones.\n\tReturns a reference to the instanced module.\"\"\"\n\tif modules.has(name):\n\t\tinfo(\"The module '%s' already exists; discarding the call to add it anew.\" % name, PLUGIN_NAME)\n\telse:\n\t\tif logfile == null:\n\t\t\tlogfile = get_external_sink(default_logfile_path)\n\t\tmodules[name] = Module.new(name, output_level, output_strategies, logfile)\n\treturn modules[name]\n\n\nfunc get_module(module = default_module_name):\n\t\"\"\"Retrieve the given module if it exists; if not, it will be created.\"\"\"\n\tif not modules.has(module):\n\t\tinfo(\"The requested module '%s' does not exist. It will be created with default values.\" % module, PLUGIN_NAME)\n\t\tadd_module(module)\n\treturn modules[module]\n\n\nfunc get_modules():\n\t\"\"\"Retrieve the dictionary containing all modules.\"\"\"\n\treturn modules\n\n\n# Logfiles management\n# -------------------\n\n\nfunc set_default_logfile_path(new_logfile_path, keep_old = false):\n\t\"\"\"Sets the new default logfile path. Unless configured otherwise with\n\tthe optional keep_old argument, it will replace the logfile for all\n\tmodules which were configured for the previous logfile path.\"\"\"\n\tif new_logfile_path == default_logfile_path:\n\t\treturn  # Nothing to do\n\n\tvar old_logfile = get_external_sink(default_logfile_path)\n\tvar new_logfile = null\n\tif external_sinks.has(new_logfile_path):  # Already exists\n\t\tnew_logfile = external_sinks[new_logfile_path]\n\telse:  # Create a new logfile\n\t\tnew_logfile = add_logfile(new_logfile_path)\n\t\texternal_sinks[new_logfile_path] = new_logfile\n\n\tif not keep_old:  # Replace the old defaut logfile in all modules that used it\n\t\tfor module in modules.values():\n\t\t\tif module.get_external_sink() == old_logfile:\n\t\t\t\tmodule.set_external_sink(new_logfile)\n\t\texternal_sinks.erase(default_logfile_path)\n\tdefault_logfile_path = new_logfile_path\n\n\nfunc get_default_logfile_path():\n\t\"\"\"Return the default logfile path.\"\"\"\n\treturn default_logfile_path\n\n\nfunc add_logfile(logfile_path = default_logfile_path):\n\t\"\"\"Add a new logfile that can then be attached to one or more modules.\n\tReturns a reference to the instanced logfile.\"\"\"\n\tif external_sinks.has(logfile_path):\n\t\tinfo(\"A logfile pointing to '%s' already exists; discarding the call to add it anew.\" % logfile_path, PLUGIN_NAME)\n\telse:\n\t\texternal_sinks[logfile_path] = Logfile.new(logfile_path)\n\treturn external_sinks[logfile_path]\n\n\nfunc get_external_sink(_external_sink_name):\n\t\"\"\"Retrieve the first given external sink if it exists, otherwise returns null.\"\"\"\n\tif not external_sinks.has(_external_sink_name):\n\t\twarn(\"The requested external sink pointing to '%s' does not exist.\" % _external_sink_name, PLUGIN_NAME)\n\t\treturn null\n\telse:\n\t\treturn external_sinks[_external_sink_name]\n\n\nfunc get_external_sinks():\n\t\"\"\"Retrieve the dictionary containing all external sinks.\"\"\"\n\treturn external_sinks\n\n\nfunc flush_buffers():\n\t\"\"\"Flush non-empty buffers.\"\"\"\n\tvar processed_external_sinks = []\n\tvar external_sink = null\n\tfor module in modules:\n\t\texternal_sink = modules[module].get_external_sink()\n\t\tif external_sink in processed_external_sinks:\n\t\t\tcontinue\n\t\texternal_sink.flush_buffer()\n\t\tprocessed_external_sinks.append(external_sink)\n\n\n# Default output configuration\n# ----------------------------\n\n\nfunc set_default_output_strategy(output_strategy_mask, level = -1):\n\t\"\"\"Set the default output strategy mask of the given level or (by\n\tdefault) all levels for all modules without a custom strategy.\"\"\"\n\tif not output_strategy_mask in range(0, MAX_STRATEGY + 1):\n\t\terror(\"The output strategy mask must be comprised between 0 and %d.\" % MAX_STRATEGY, PLUGIN_NAME)\n\t\treturn\n\tif level == -1:  # Set for all levels\n\t\tfor i in range(0, LEVELS.size()):\n\t\t\tdefault_output_strategies[i] = output_strategy_mask\n\t\tinfo(\"The default output strategy mask was set to '%d' for all levels.\" % [output_strategy_mask], PLUGIN_NAME)\n\telse:\n\t\tif not level in range(0, LEVELS.size()):\n\t\t\terror(\"The level must be comprised between 0 and %d.\" % (LEVELS.size() - 1), PLUGIN_NAME)\n\t\t\treturn\n\t\tdefault_output_strategies[level] = output_strategy_mask\n\t\tinfo(\"The default output strategy mask was set to '%d' for the '%s' level.\" % [output_strategy_mask, LEVELS[level]], PLUGIN_NAME)\n\n\nfunc get_default_output_strategy(level):\n\t\"\"\"Get the default output strategy mask of the given level or (by\n\tdefault) all levels for all modules without a custom strategy.\"\"\"\n\treturn default_output_strategies[level]\n\n\nfunc set_default_output_level(level):\n\t\"\"\"Set the default minimal level for the output of all modules without\n\ta custom output level.\n\tAll levels greater or equal to the given once will be output based on\n\ttheir respective strategies, while levels lower than the given one will\n\tbe discarded.\n\t\"\"\"\n\tif not level in range(0, LEVELS.size()):\n\t\terror(\"The level must be comprised between 0 and %d.\" % (LEVELS.size() - 1), PLUGIN_NAME)\n\t\treturn\n\tdefault_output_level = level\n\tinfo(\"The default output level was set to '%s'.\" % LEVELS[level], PLUGIN_NAME)\n\n\nfunc get_default_output_level():\n\t\"\"\"Get the default minimal level for the output of all modules without\n\ta custom output level.\"\"\"\n\treturn default_output_level\n\n\n# Output formatting\n# -----------------\n\n\n# Format the fields:\n# * YYYY = Year\n# * MM = Month\n# * DD = Day\n# * hh = Hour\n# * mm = Minutes\n# * ss = Seconds\n# * SSS = Milliseconds\nfunc get_formatted_datetime():\n\tvar unix_time: float = Time.get_unix_time_from_system()\n\tvar time_zone: Dictionary = Time.get_time_zone_from_system()\n\tunix_time += time_zone.bias * 60\n\tvar datetime: Dictionary = Time.get_datetime_dict_from_unix_time(int(unix_time))\n\tdatetime.millisecond = int(unix_time * 1000) % 1000\n\tvar result = time_format\n\tresult = result.replace(\"YYYY\", \"%04d\" % [datetime.year])\n\tresult = result.replace(\"MM\", \"%02d\" % [datetime.month])\n\tresult = result.replace(\"DD\", \"%02d\" % [datetime.day])\n\tresult = result.replace(\"hh\", \"%02d\" % [datetime.hour])\n\tresult = result.replace(\"mm\", \"%02d\" % [datetime.minute])\n\tresult = result.replace(\"ss\", \"%02d\" % [datetime.second])\n\tresult = result.replace(\"SSS\", \"%03d\" % [datetime.millisecond])\n\treturn result\n\n\nfunc format(template, level, module, message, error_code = -1):\n\tvar output = template\n\toutput = output.replace(FORMAT_IDS.level, LEVELS[level])\n\toutput = output.replace(FORMAT_IDS.module, module)\n\toutput = output.replace(FORMAT_IDS.message, str(message))\n\toutput = output.replace(FORMAT_IDS.time, get_formatted_datetime())\n\n\t# Error message substitution\n\tvar error_message = ERROR_MESSAGES.get(error_code)\n\tif error_message != null:\n\t\toutput = output.replace(FORMAT_IDS.error_message, \" \" + error_message)\n\telse:\n\t\toutput = output.replace(FORMAT_IDS.error_message, \"\")\n\n\treturn output\n\n\nfunc set_output_format(new_format):\n\t\"\"\"Set the output string format using the following identifiers:\n\t{LVL} for the level, {MOD} for the module, {MSG} for the message.\n\tThe three identifiers should be contained in the output format string.\n\t\"\"\"\n\tfor key in FORMAT_IDS:\n\t\tif new_format.find(FORMAT_IDS[key]) == -1:\n\t\t\terror(\"Invalid output string format. It lacks the '%s' identifier.\" % FORMAT_IDS[key], PLUGIN_NAME)\n\t\t\treturn\n\toutput_format = new_format\n\tinfo(\"Successfully changed the output format to '%s'.\" % output_format, PLUGIN_NAME)\n\n\nfunc get_output_format():\n\t\"\"\"Get the output string format.\"\"\"\n\treturn output_format\n\n\n# Strategy \"memory\"\n# -----------------\n\n\nfunc set_max_memory_size(new_size):\n\t\"\"\"Set the maximum amount of messages to be remembered when\n\tusing the STRATEGY_MEMORY output strategy.\"\"\"\n\tif new_size <= 0:\n\t\terror(\"The maximum amount of remembered messages must be a positive non-null integer. Received %d.\" % new_size, PLUGIN_NAME)\n\t\treturn\n\n\tvar new_buffer = []\n\tvar new_idx = 0\n\tnew_buffer.resize(new_size)\n\n\t# Better algorithm welcome :D\n\tif memory_first_loop:\n\t\tvar offset = 0\n\t\tif memory_idx > new_size:\n\t\t\toffset = memory_idx - new_size\n\t\t\tmemory_first_loop = false\n\t\telse:\n\t\t\tnew_idx = memory_idx\n\t\tfor i in range(0, min(memory_idx, new_size)):\n\t\t\tnew_buffer[i] = memory_buffer[i + offset]\n\telse:\n\t\tvar delta = 0\n\t\tif max_memory_size > new_size:\n\t\t\tdelta = max_memory_size - new_size\n\t\telse:\n\t\t\tnew_idx = max_memory_size\n\t\t\tmemory_first_loop = true\n\t\tfor i in range(0, min(max_memory_size, new_size)):\n\t\t\tnew_buffer[i] = memory_buffer[(memory_idx + delta + i) % max_memory_size]\n\n\tmemory_buffer = new_buffer\n\tmemory_idx = new_idx\n\tinvalid_memory_cache = true\n\tmax_memory_size = new_size\n\tinfo(\"Successfully set the maximum amount of remembered messages to %d.\" % max_memory_size, PLUGIN_NAME)\n\n\nfunc get_max_memory_size():\n\t\"\"\"Get the maximum amount of messages to be remembered when\n\tusing the STRATEGY_MEMORY output strategy.\"\"\"\n\treturn max_memory_size\n\n\nfunc get_memory():\n\t\"\"\"Get an array of the messages remembered following STRATEGY_MEMORY.\n\tThe messages are sorted from the oldest to the newest.\"\"\"\n\tif invalid_memory_cache:  # Need to recreate the cached ordered array\n\t\tmemory_cache = []\n\t\tif not memory_first_loop:  # else those would be uninitialized\n\t\t\tfor i in range(memory_idx, max_memory_size):\n\t\t\t\tmemory_cache.append(memory_buffer[i])\n\t\tfor i in range(0, memory_idx):\n\t\t\tmemory_cache.append(memory_buffer[i])\n\t\tinvalid_memory_cache = false\n\treturn memory_cache\n\n\nfunc clear_memory():\n\t\"\"\"Clear the buffer or remembered messages.\"\"\"\n\tmemory_buffer.clear()\n\tmemory_idx = 0\n\tmemory_first_loop = true\n\tinvalid_memory_cache = true\n\n\n# Configuration loading/saving\n# ----------------------------\n\nconst config_fields := {\n\tdefault_output_level = \"default_output_level\",\n\tdefault_output_strategies = \"default_output_strategies\",\n\tdefault_logfile_path = \"default_logfile_path\",\n\tmax_memory_size = \"max_memory_size\",\n\texternal_sinks = \"external_sinks\",\n\tmodules = \"modules\"\n}\n\n\nfunc save_config(configfile = default_configfile_path):\n\t\"\"\"Save the default configuration as well as the set of modules and\n\ttheir respective configurations.\n\tThe ConfigFile API is used to generate the config file passed as argument.\n\tA unique section is used, so that it can be merged in a project's engine.cfg.\n\tReturns an error code (OK or some ERR_*).\"\"\"\n\tvar config = ConfigFile.new()\n\n\t# Store default config\n\tconfig.set_value(PLUGIN_NAME, config_fields.default_output_level, default_output_level)\n\tconfig.set_value(PLUGIN_NAME, config_fields.default_output_strategies, default_output_strategies)\n\tconfig.set_value(PLUGIN_NAME, config_fields.default_logfile_path, default_logfile_path)\n\tconfig.set_value(PLUGIN_NAME, config_fields.max_memory_size, max_memory_size)\n\n\t# External sink config\n\tvar external_sinks_arr = []\n\tvar sorted_keys = external_sinks.keys()\n\tsorted_keys.sort()  # Sadly doesn't return the array, so we need to split it\n\tfor external_sink in sorted_keys:\n\t\texternal_sinks_arr.append(external_sinks[external_sink].get_config())\n\tconfig.set_value(PLUGIN_NAME, config_fields.external_sinks, external_sinks_arr)\n\n\t# Modules config\n\tvar modules_arr = []\n\tsorted_keys = modules.keys()\n\tsorted_keys.sort()\n\tfor module in sorted_keys:\n\t\tmodules_arr.append(modules[module].get_config())\n\tconfig.set_value(PLUGIN_NAME, config_fields.modules, modules_arr)\n\n\t# Save and return the corresponding error code\n\tvar err = config.save(configfile)\n\tif err:\n\t\terror(\"Could not save the config in '%s'; exited with error %d.\" % [configfile, err], PLUGIN_NAME)\n\t\treturn err\n\tinfo(\"Successfully saved the config to '%s'.\" % configfile, PLUGIN_NAME)\n\treturn OK\n\n\nfunc load_config(configfile = default_configfile_path):\n\t\"\"\"Load the configuration as well as the set of defined modules and\n\ttheir respective configurations. The expect file contents must be those\n\tproduced by the ConfigFile API.\n\tReturns an error code (OK or some ERR_*).\"\"\"\n\t# Look for the file\n\tif not FileAccess.file_exists(configfile):\n\t\twarn(\"Could not load the config in '%s', the file does not exist.\" % configfile, PLUGIN_NAME)\n\t\treturn ERR_FILE_NOT_FOUND\n\n\t# Load its contents\n\tvar config = ConfigFile.new()\n\tvar err = config.load(configfile)\n\tif err:\n\t\twarn(\"Could not load the config in '%s'; exited with error %d.\" % [configfile, err], PLUGIN_NAME)\n\t\treturn err\n\n\t# Load default config\n\tdefault_output_level = config.get_value(PLUGIN_NAME, config_fields.default_output_level, default_output_level)\n\tdefault_output_strategies = config.get_value(PLUGIN_NAME, config_fields.default_output_strategies, default_output_strategies)\n\tdefault_logfile_path = config.get_value(PLUGIN_NAME, config_fields.default_logfile_path, default_logfile_path)\n\tmax_memory_size = config.get_value(PLUGIN_NAME, config_fields.max_memory_size, max_memory_size)\n\n\t# Load external config and initialize them\n\tflush_buffers()\n\texternal_sinks = {}\n\tadd_logfile(default_logfile_path)\n\tfor logfile_cfg in config.get_value(PLUGIN_NAME, config_fields.external_sinks, []):\n\t\tvar logfile = Logfile.new(logfile_cfg[\"path\"], logfile_cfg[\"queue_mode\"])\n\t\texternal_sinks[logfile_cfg[\"path\"]] = logfile\n\n\t# Load modules config and initialize them\n\tmodules = {}\n\tadd_module(PLUGIN_NAME)\n\tadd_module(default_module_name)\n\tfor module_cfg in config.get_value(PLUGIN_NAME, config_fields.modules, []):\n\t\tvar module = Module.new(\n\t\t\tmodule_cfg[\"name\"], module_cfg[\"output_level\"], module_cfg[\"output_strategies\"], get_external_sink(module_cfg[\"external_sink\"][\"path\"])\n\t\t)\n\t\tmodules[module_cfg[\"name\"]] = module\n\n\tinfo(\"Successfully loaded the config from '%s'.\" % configfile, PLUGIN_NAME)\n\treturn OK\n\n\n##=============##\n##  Callbacks  ##\n##=============##\n\n\nfunc _init():\n\t# Default logfile\n\tadd_logfile(default_logfile_path)\n\t# Default modules\n\tadd_module(PLUGIN_NAME)  # needs to be instanced first\n\tadd_module(default_module_name)\n\tmemory_buffer.resize(max_memory_size)\n\n\nfunc _exit_tree():\n\tflush_buffers()\n"
  },
  {
    "path": "plugin.cfg",
    "content": "[plugin]\n\nname=\"Godot Logger\"\ndescription=\"The Godot Logger is a plugin that provides a logging API for projects developed with Godot Engine.\"\nauthor=\"KOBUGE Games\"\nversion=\"1.0.0\"\nscript=\"plugin.gd\"\n"
  },
  {
    "path": "plugin.gd",
    "content": "@tool\nextends EditorPlugin\n\n\nfunc _enter_tree() -> void:\n\tadd_autoload_singleton(\"KLogger\", \"logger.gd\")\n\n\nfunc _exit_tree() -> void:\n\tremove_autoload_singleton(\"KLogger\")\n"
  }
]