Add LimboConsole plugin with command interpreter and configuration options

This commit is contained in:
2025-08-23 16:48:39 +02:00
parent 9da3b02a46
commit 846c3a6d83
60 changed files with 3201 additions and 16 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -3,13 +3,61 @@
{ {
"tierEnum": "Tier1", "tierEnum": "Tier1",
"threshold": 150, "threshold": 150,
"imagePath": "res://Mods/Tiers/Huts/hut_tier_1.png", "imagePath": "res://Sprites/Hut.png",
"scale": {"x": 0.05, "y": 0.05} "scale": {"x": 0.05, "y": 0.05}
}, },
{ {
"tierEnum": "Tier2", "tierEnum": "Tier2",
"threshold": 750, "threshold": 750,
"imagePath": "res://Mods/Tiers/Huts/hut_tier_2.png", "imagePath": "res://Sprites/hut_tier_2.png",
"scale": {"x": 0.05, "y": 0.05}
},
{
"tierEnum": "Tier3",
"threshold": 1000,
"imagePath": "res://Sprites/hut_tier_3.png",
"scale": {"x": 0.05, "y": 0.05}
},
{
"tierEnum": "Tier4",
"threshold": 5000,
"imagePath": "res://Sprites/castle.png",
"scale": {"x": 0.05, "y": 0.05}
},
{
"tierEnum": "Tier5",
"threshold": 7500,
"imagePath": "res://Sprites/house.png",
"scale": {"x": 0.05, "y": 0.05}
},
{
"tierEnum": "Tier6",
"threshold": 10000,
"imagePath": "res://Sprites/house_tier_2.png",
"scale": {"x": 0.05, "y": 0.05}
},
{
"tierEnum": "Tier7",
"threshold": 50000,
"imagePath": "res://Sprites/house_tier_3.png",
"scale": {"x": 0.05, "y": 0.05}
},
{
"tierEnum": "Tier8",
"threshold": 150000,
"imagePath": "res://Sprites/Skyscraper.png",
"scale": {"x": 0.05, "y": 0.05}
},
{
"tierEnum": "Tier9",
"threshold": 350000,
"imagePath": "res://Sprites/skyscraper_tier_2.png",
"scale": {"x": 0.05, "y": 0.05}
},
{
"tierEnum": "Tier10",
"threshold": 550000,
"imagePath": "res://Sprites/skyscraper_tier_3.png",
"scale": {"x": 0.05, "y": 0.05} "scale": {"x": 0.05, "y": 0.05}
} }
] ]

View File

@@ -5,6 +5,7 @@
<RootNamespace>parasiticgod</RootNamespace> <RootNamespace>parasiticgod</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="LimboConsole.Sharp" Version="0.0.1-beta-008" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4-beta1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.4-beta1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -5,6 +5,6 @@ namespace ParasiticGod.Scripts;
[GlobalClass] [GlobalClass]
public partial class Follower : Node2D public partial class Follower : Node2D
{ {
public enum FollowerTier { Tier1, Tier2, Tier3, Tier4, Tier5 } public enum FollowerTier { Tier1, Tier2, Tier3, Tier4, Tier5, Tier6, Tier7, Tier8, Tier9, Tier10 }
[Export] public FollowerTier Tier { get; private set; } [Export] public FollowerTier Tier { get; private set; }
} }

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Godot; using Godot;
using Limbo.Console.Sharp;
using ParasiticGod.Scripts.Core; using ParasiticGod.Scripts.Core;
using ParasiticGod.Scripts.Core.Effects; using ParasiticGod.Scripts.Core.Effects;
@@ -38,6 +39,11 @@ public partial class GameBus : Node
Instance = null; Instance = null;
} }
public override void _Ready()
{
RegisterConsoleCommands();
}
public override void _Process(double delta) public override void _Process(double delta)
{ {
_gameLogic.UpdateGameState(_gameState, delta); _gameLogic.UpdateGameState(_gameState, delta);
@@ -66,7 +72,7 @@ public partial class GameBus : Node
if (AllMiracles.TryGetValue(id, out var def) && !_gameState.IsMiracleUnlocked(id)) if (AllMiracles.TryGetValue(id, out var def) && !_gameState.IsMiracleUnlocked(id))
{ {
miraclesToUnlock.Add(def); miraclesToUnlock.Add(def);
_gameState.AddUnlockedMiracle(id); _gameState.AddUnlockedMiracle(def.Id);
} }
} }
} }
@@ -108,4 +114,7 @@ public partial class GameBus : Node
public void UnsubscribeFromStat(Stat stat, Action<double> listener) => _gameState.Unsubscribe(stat, listener); public void UnsubscribeFromStat(Stat stat, Action<double> listener) => _gameState.Unsubscribe(stat, listener);
public GameState CurrentState => _gameState; public GameState CurrentState => _gameState;
[ConsoleCommand("set_stat", "Sets the value of a specified stat.")]
private void SetStatCommand(Stat stat, double value) => _gameState.Set(stat, value);
} }

BIN
Sprites/Skyscraper.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://c88ltenh4ghit" uid="uid://b7shwf6ob3qk5"
path="res://.godot/imported/hut_tier_1.png-95c994b0565c43d199344569cb1a91ab.ctex" path="res://.godot/imported/Skyscraper.png-dbe8acef55267af18c54925cfa634290.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://Mods/Tiers/Huts/hut_tier_1.png" source_file="res://Sprites/Skyscraper.png"
dest_files=["res://.godot/imported/hut_tier_1.png-95c994b0565c43d199344569cb1a91ab.ctex"] dest_files=["res://.godot/imported/Skyscraper.png-dbe8acef55267af18c54925cfa634290.ctex"]
[params] [params]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

BIN
Sprites/house_tier_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://2wsb1jsmtifq" uid="uid://8omh4jhf3dwy"
path="res://.godot/imported/follower_tier_1.png-d66a290ad46b1bb9ea69b54444ab5725.ctex" path="res://.godot/imported/house_tier_2.png-81b05409415d85bd1a2017792c00d1d0.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://Mods/Tiers/Followers/follower_tier_1.png" source_file="res://Sprites/house_tier_2.png"
dest_files=["res://.godot/imported/follower_tier_1.png-d66a290ad46b1bb9ea69b54444ab5725.ctex"] dest_files=["res://.godot/imported/house_tier_2.png-81b05409415d85bd1a2017792c00d1d0.ctex"]
[params] [params]

BIN
Sprites/house_tier_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://6swd4nukhqw3"
path="res://.godot/imported/house_tier_3.png-6cbc031313f2c66f5365fe6cc5406e8f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Sprites/house_tier_3.png"
dest_files=["res://.godot/imported/house_tier_3.png-6cbc031313f2c66f5365fe6cc5406e8f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
Sprites/hut_tier_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://cgoigfok3s0fc" uid="uid://bvmc6om3x08a"
path="res://.godot/imported/hut_tier_2.png-7e373825ef1bbf63f359ae57d709b394.ctex" path="res://.godot/imported/hut_tier_2.png-3b01622803083ef93f5d9958304c62a4.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://Mods/Tiers/Huts/hut_tier_2.png" source_file="res://Sprites/hut_tier_2.png"
dest_files=["res://.godot/imported/hut_tier_2.png-7e373825ef1bbf63f359ae57d709b394.ctex"] dest_files=["res://.godot/imported/hut_tier_2.png-3b01622803083ef93f5d9958304c62a4.ctex"]
[params] [params]

BIN
Sprites/hut_tier_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c047caxjt7n1h"
path="res://.godot/imported/hut_tier_3.png-7223c535ac426228250d1a42c2991446.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Sprites/hut_tier_3.png"
dest_files=["res://.godot/imported/hut_tier_3.png-7223c535ac426228250d1a42c2991446.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bjp0e05j0rut5"
path="res://.godot/imported/skyscraper_tier_2.png-08c46a0e23a9922ac087d37397d5de1d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Sprites/skyscraper_tier_2.png"
dest_files=["res://.godot/imported/skyscraper_tier_2.png-08c46a0e23a9922ac087d37397d5de1d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cu5env6dkuc1u"
path="res://.godot/imported/skyscraper_tier_3.png-4b648ec2b3736510201be45be1a2c6a7.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Sprites/skyscraper_tier_3.png"
dest_files=["res://.godot/imported/skyscraper_tier_3.png-4b648ec2b3736510201be45be1a2c6a7.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

39
addons/limbo_console.cfg Normal file
View File

@@ -0,0 +1,39 @@
[main]
aliases={
"exit": "quit",
"source": "exec",
"usage": "help"
}
disable_in_release_build=false
print_to_stdout=false
pause_when_open=true
commands_disabled_in_release=["eval"]
[appearance]
custom_theme="res://addons/limbo_console_theme.tres"
height_ratio=0.5
open_speed=5.0
opacity=1.0
sparse_mode=false
[greet]
greet_user=true
greeting_message="{project_name}"
greet_using_ascii_art=true
[history]
persist_history=true
history_lines=1000
[autocomplete]
autocomplete_use_history_with_matches=true
[autoexec]
autoexec_script="user://autoexec.lcs"
autoexec_auto_create=true

View File

@@ -0,0 +1,15 @@
# Contributing
Thanks for your interest in contributing. Please follow these guidelines to keep the project consistent and maintainable in the long-term.
## Vision
- This is a simple text-based interface. Follow the [KISS principle](https://en.wikipedia.org/wiki/KISS_principle) keep the code simple and easy to maintain, abstractions minimal, and the API easy to use without reading a manual.
- Don't run any logic if the console is closed or hidden.
## Code Style & Recommendations
- Follow the official [GDScript Style Guide](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_styleguide.html).
- Use [static typing](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/static_typing.html) wherever possible.
- Apply the 80/20 rule: if a feature only benefits a small number of users, make it optional or dont include it.
- To avoid unnecessary whitespace changes, please enable this setting in Godot: `Editor Settings > Text Editor > Behavior > Files > Trim Trailing Whitespace on Save`

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Serhii Snitsaruk
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,128 @@
<p align="left">
<img src=".github/logo.png" width=128 alt="LimboConsole logo">
</p>
---
![Limbo Console](.github/demonstration.gif)
![Static Badge](https://img.shields.io/badge/Godot-4.3-blue?style=flat)
[![GitHub License](https://img.shields.io/github/license/limbonaut/limbo_console)](https://github.com/limbonaut/limbo_console/blob/master/LICENSE.md)
A simple and easy-to-use in-game dev console with a command interpreter for Godot Engine 4.
It supports auto-completion with `TAB` for commands and history, auto-correction, inline hints and highlighting, command help text generation, argument parsing for basic types, aliases, custom theming, and more.
This plugin is currently in development, so expect breaking changes.
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Y8Y2TCNH0)
## How to use
> LimboConsole can be added as a Git submodule
Place the source code in the `res://addons/limbo_console/` directory, and enable this plugin in the project settings, then reload the project. Toggle the console with the `GRAVE ACCENT` key (aka backtick - the key to the left of the `1` key). This can be changed in the Input Map tab in the project settings.
Adding a new command is quite simple:
```gdscript
func _ready() -> void:
LimboConsole.register_command(multiply)
func multiply(a: float, b: float) -> void:
LimboConsole.info("a * b: " + str(a * b))
```
> For C# support, see the next section.
The example above adds a command that multiplies two numbers and prints the result (type `multiply 2 4`). Additionally, you can specify a name and a description:
```gdscript
LimboConsole.register_command(multiply, "multiply", "multiply two numbers")
```
You can add a command as a subcommand of another command:
```gdscript
# Register `multiply` as a subcommand under a new `math` command.
LimboConsole.register_command(multiply, "math multiply", "Multiply two numbers")
```
Now, you can enter `math multiply 2 4` in the console. By the way, the parent command doesn't have to exist.
Several basic types are supported for command arguments, such as `bool`, `int`, `float`, `String` and `Vector{2,3,4}` types. To enter a `Vector2` argument, enclose its components in parentheses, like this: `(1 2)`. String arguments can also be enclosed in double quotation marks `"`.
Autocompletion works for both command names and history. It can also be implemented for specific command arguments, as shown in the following example:
```gdscript
LimboConsole.register_command(teleport, "teleport", "teleport to site on this level")
LimboConsole.add_argument_autocomplete_source("teleport", 1,
func(): return ["entrance", "caves", "boss"]
)
```
For a dynamically generated list of autocomplete values, the code could look like this:
```gdscript
LimboConsole.add_argument_autocomplete_source("teleport", 1,
func(): return get_tree().get_nodes_in_group("teleportation_site").map(
func(node): return node.name)
)
```
### Using in C#
A community-maintained C# wrapper for this project is available as a NuGet package: https://github.com/ryan-linehan/limbo_console_sharp
Thanks to @ryan-linehan for maintaining it!
### Methods and properties
Some notable methods and properties:
- LimboConsole.enabled
- LimboConsole.register_command(callable, command_name, description)
- LimboConsole.unregister_command(callable_or_command_name)
- LimboConsole.add_alias(alias_name, command_name)
- LimboConsole.info(text_line)
- LimboConsole.error(text_line)
- LimboConsole.warning(text_line)
- LimboConsole.toggle_console()
- LimboConsole.add_argument_autocomplete_source(command_name, argument, callable)
- LimboConsole.execute_script(path, silent)
This is not a complete list. For the rest, check out `limbo_console.gd`.
### Keyboard Shortcuts
- `Grave Accent` *(aka backtick - the key to the left of the `1` key)* — Toggle the console.
- `Enter` — Run entered command.
- `Tab` — Autocomplete command entry or cycle through autocomplete suggestions.
- `Shift+Tab` — Cycle through autocomplete suggestions in reverse.
- `Right` *(when cursor is at the end of command entry)* — Autocomplete according to inline hint (doesn't cycle like `Tab`).
- `Up/Down` — Cycle through command history, replacing contents of command entry.
- `Ctrl+R` — Toggle the history search interface (similar to [fzf](https://github.com/junegunn/fzf)).
- `Ctrl+C` *(when no text selected)* — Clear the command entry.
### Configuration
Options can be modified in the project-specific configuration file located at `res://addons/limbo_console.cfg`. This file is stored outside the plugin's directory to support adding the plugin as a Git submodule.
LimboConsole also supports UI theming. Simply duplicate the `default_theme.tres` file and rename it to `limbo_console_theme.tres`. The file path is important - it should be located at `res://addons/limbo_console_theme.tres`. You can change this location in the config file.
Open the theme resource in Godot to customize it for your game. Console text colors can be adjusted in the `ConsoleColors` category.
### Scripting
You can execute simple scripts containing a sequence of commands:
```shell
exec lcs/my_script.lcs
```
Simple rules:
- Commands must be provided in the same syntax as in the prompt, with each command on a separate line.
- The script must exist at the specified path, either in the `res://` or `user://` directory.
- The script must have the `.lcs` extension, but when running the `exec` command, you can omit the extension in the command line.
- A line that starts with a '#' is treated as a comment and is not executed as part of the script.
You can have a script execute automatically every time the game starts. There is a special script called `user://autoexec.lcs` that runs each time the game starts. This can be customized in the configuration file.
### Contributing
Check out [CONTRIBUTING.md](CONTRIBUTING.md).

View File

@@ -0,0 +1,208 @@
extends RefCounted
const boxed_map: Dictionary = {
'a': """
▒▄▀█
░█▀█
""",
'b': """
░█▄▄
▒█▄█
""",
'c': """
░▄▀▀
░▀▄▄
""",
'd': """
▒█▀▄
░█▄▀
""",
'e': """
░██▀
▒█▄▄
""",
'f': """
░█▀▀
░█▀░
""",
'g': """
▒▄▀▀
░▀▄█
""",
'h': """
░█▄█
▒█▒█
""",
'i': """
░█
░█
""",
'j': """
░░▒█
░▀▄█
""",
'k': """
░█▄▀
░█▒█
""",
'l': """
░█▒░
▒█▄▄
""",
'm': """
▒█▀▄▀█
░█▒▀▒█
""",
'n': """
░█▄░█
░█▒▀█
""",
'o': """
░█▀█
▒█▄█
""",
'p': """
▒█▀█
░█▀▀
""",
'q': """
░▄▀▄
░▀▄█
""",
'r': """
▒█▀█
░█▀▄
""",
's': """
░▄▀
▒▄█
""",
't': """
░▀█▀
░▒█▒
""",
'u': """
░█░█
▒█▄█
""",
'v': """
░█░█
▒▀▄▀
""",
'w': """
▒█░█░█
░▀▄▀▄▀
""",
'x': """
░▀▄▀
░█▒█
""",
'y': """
░▀▄▀
░▒█▒
""",
'z': """
░▀█
▒█▄
""",
' ': """
""",
'_': """
░░░
▒▄▄
""",
',': """
░▒
░█
""",
'.': """
░░
░▄
""",
'!': """
░█
░▄
""",
'-': """
░▒░
░▀▀
""",
'?': """
░▀▀▄
░▒█▀
""",
'\'': """
░▀
░░
""",
':': """
░▄░
▒▄▒
""",
'0': """
░▄▀▄
░▀▄▀
""",
'1': """
░▄█
░░█
""",
'2': """
░▀█
░█▄
""",
'3': """
░▀██
░▄▄█
""",
'4': """
░█▄
░░█
""",
'5': """
░█▀
░▄█
""",
'6': """
░█▀
░██
""",
'7': """
░▀█
░█░
""",
'8': """
░█▄█
░█▄█
""",
'9': """
░██
░▄█
""",
}
const unsupported_char := """
░▒░
▒░▒
"""
static func str_to_boxed_art(p_text: String) -> PackedStringArray:
var lines: PackedStringArray = []
lines.resize(2)
for c in p_text:
var ascii: String = boxed_map.get(c.to_lower(), unsupported_char)
var parts: PackedStringArray = ascii.split('\n')
lines[0] += parts[1]
lines[1] += parts[2]
return lines
static func is_boxed_art_supported(p_text: String) -> bool:
for c in p_text:
if not boxed_map.has(c.to_lower()):
return false
return true

View File

@@ -0,0 +1 @@
uid://coot1au4b7np

View File

@@ -0,0 +1,169 @@
extends RefCounted
## BuiltinCommands
const Util := preload("res://addons/limbo_console/util.gd")
static func register_commands() -> void:
LimboConsole.register_command(cmd_alias, "alias", "add command alias")
LimboConsole.register_command(cmd_aliases, "aliases", "list all aliases")
LimboConsole.register_command(LimboConsole.clear_console, "clear", "clear console screen")
LimboConsole.register_command(cmd_commands, "commands", "list all commands")
LimboConsole.register_command(LimboConsole.info, "echo", "display a line of text")
LimboConsole.register_command(cmd_eval, "eval", "evaluate an expression")
LimboConsole.register_command(cmd_exec, "exec", "execute commands from file")
LimboConsole.register_command(cmd_fps_max, "fps_max", "limit framerate")
LimboConsole.register_command(cmd_fullscreen, "fullscreen", "toggle fullscreen mode")
LimboConsole.register_command(cmd_help, "help", "show command info")
LimboConsole.register_command(cmd_log, "log", "show recent log entries")
LimboConsole.register_command(cmd_quit, "quit", "exit the application")
LimboConsole.register_command(cmd_unalias, "unalias", "remove command alias")
LimboConsole.register_command(cmd_vsync, "vsync", "adjust V-Sync")
LimboConsole.register_command(LimboConsole.erase_history, "erase_history", "erases current history and persisted history")
LimboConsole.add_argument_autocomplete_source("help", 1, LimboConsole.get_command_names.bind(true))
static func cmd_alias(p_alias: String, p_command: String) -> void:
LimboConsole.info("Adding %s => %s" % [LimboConsole.format_name(p_alias), p_command])
LimboConsole.add_alias(p_alias, p_command)
static func cmd_aliases() -> void:
var aliases: Array = LimboConsole.get_aliases()
aliases.sort()
for alias in aliases:
var alias_argv: PackedStringArray = LimboConsole.get_alias_argv(alias)
var cmd_name: String = alias_argv[0]
var desc: String = LimboConsole.get_command_description(cmd_name)
alias_argv[0] = LimboConsole.format_name(cmd_name)
if desc.is_empty():
LimboConsole.info(LimboConsole.format_name(alias))
else:
LimboConsole.info("%s is alias of: %s %s" % [
LimboConsole.format_name(alias),
' '.join(alias_argv),
LimboConsole.format_tip(" // " + desc)
])
static func cmd_commands() -> void:
LimboConsole.info("Available commands:")
for name in LimboConsole.get_command_names(false):
var desc: String = LimboConsole.get_command_description(name)
name = LimboConsole.format_name(name)
LimboConsole.info(name if desc.is_empty() else "%s -- %s" % [name, desc])
static func cmd_eval(p_expression: String) -> Error:
var exp := Expression.new()
var err: int = exp.parse(p_expression, LimboConsole.get_eval_input_names())
if err != OK:
LimboConsole.error(exp.get_error_text())
return err
var result = exp.execute(LimboConsole.get_eval_inputs(),
LimboConsole.get_eval_base_instance())
if not exp.has_execute_failed():
if result != null:
LimboConsole.info(str(result))
return OK
else:
LimboConsole.error(exp.get_error_text())
return ERR_SCRIPT_FAILED
static func cmd_exec(p_file: String, p_silent: bool = true) -> void:
if not p_file.ends_with(".lcs"):
# Prevent users from reading other game assets.
p_file += ".lcs"
if not FileAccess.file_exists(p_file):
p_file = "user://" + p_file
LimboConsole.execute_script(p_file, p_silent)
static func cmd_fps_max(p_limit: int = -1) -> void:
if p_limit < 0:
if Engine.max_fps == 0:
LimboConsole.info("Framerate is unlimited.")
else:
LimboConsole.info("Framerate is limited to %d FPS." % [Engine.max_fps])
return
Engine.max_fps = p_limit
if p_limit > 0:
LimboConsole.info("Limiting framerate to %d FPS." % [p_limit])
elif p_limit == 0:
LimboConsole.info("Removing framerate limits.")
static func cmd_fullscreen() -> void:
if LimboConsole.get_viewport().mode == Window.MODE_WINDOWED:
# get_viewport().mode = Window.MODE_EXCLUSIVE_FULLSCREEN
LimboConsole.get_viewport().mode = Window.MODE_FULLSCREEN
LimboConsole.info("Window switched to fullscreen mode.")
else:
LimboConsole.get_viewport().mode = Window.MODE_WINDOWED
LimboConsole.info("Window switched to windowed mode.")
static func cmd_help(p_command_name: String = "") -> Error:
if p_command_name.is_empty():
LimboConsole.print_line(LimboConsole.format_tip("Type %s to list all available commands." %
[LimboConsole.format_name("commands")]))
LimboConsole.print_line(LimboConsole.format_tip("Type %s to get more info about the command." %
[LimboConsole.format_name("help command")]))
return OK
else:
return LimboConsole.usage(p_command_name)
static func cmd_log(p_num_lines: int = 10) -> Error:
var fn: String = ProjectSettings.get_setting("debug/file_logging/log_path")
var file = FileAccess.open(fn, FileAccess.READ)
if not file:
LimboConsole.error("Can't open file: " + fn)
return ERR_CANT_OPEN
var contents := file.get_as_text()
var lines := contents.split('\n')
if lines.size() and lines[lines.size() - 1].strip_edges() == "":
lines.remove_at(lines.size() - 1)
lines = lines.slice(maxi(lines.size() - p_num_lines, 0))
for line in lines:
LimboConsole.print_line(Util.bbcode_escape(line), false)
return OK
static func cmd_quit() -> void:
LimboConsole.get_tree().quit()
static func cmd_unalias(p_alias: String) -> void:
if LimboConsole.has_alias(p_alias):
LimboConsole.remove_alias(p_alias)
LimboConsole.info("Alias removed.")
else:
LimboConsole.warn("Alias not found.")
static func cmd_vsync(p_mode: int = -1) -> void:
if p_mode < 0:
var current: int = DisplayServer.window_get_vsync_mode()
if current == 0:
LimboConsole.info("V-Sync: disabled.")
elif current == 1:
LimboConsole.info('V-Sync: enabled.')
elif current == 2:
LimboConsole.info('Current V-Sync mode: adaptive.')
LimboConsole.info("Adjust V-Sync mode with an argument: 0 - disabled, 1 - enabled, 2 - adaptive.")
elif p_mode == DisplayServer.VSYNC_DISABLED:
LimboConsole.info("Changing to disabled.")
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
elif p_mode == DisplayServer.VSYNC_ENABLED:
LimboConsole.info("Changing to default V-Sync.")
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED)
elif p_mode == DisplayServer.VSYNC_ADAPTIVE:
LimboConsole.info("Changing to adaptive V-Sync.")
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ADAPTIVE)
else:
LimboConsole.error("Invalid mode.")
LimboConsole.info("Acceptable modes: 0 - disabled, 1 - enabled, 2 - adaptive.")

View File

@@ -0,0 +1 @@
uid://npao6ckwv7cm

View File

@@ -0,0 +1,130 @@
extends TextEdit
## CommandEntry
signal text_submitted(command_line: String)
signal autocomplete_requested()
var autocomplete_hint: String:
set(value):
if autocomplete_hint != value:
autocomplete_hint = value
queue_redraw()
var _font: Font
var _font_size: int
var _hint_color: Color
var _sb_normal: StyleBox
func _init() -> void:
syntax_highlighter = CommandEntryHighlighter.new()
func _ready() -> void:
caret_multiple = false
autowrap_mode = TextServer.AUTOWRAP_OFF
scroll_fit_content_height = true
# placeholder_text = ""
get_v_scroll_bar().visibility_changed.connect(_hide_scrollbars)
get_h_scroll_bar().visibility_changed.connect(_hide_scrollbars)
_hide_scrollbars()
_font = get_theme_font("font")
_font_size = get_theme_font_size("font_size")
_hint_color = get_theme_color("hint_color")
_sb_normal = get_theme_stylebox("normal")
func _notification(what: int) -> void:
match what:
NOTIFICATION_FOCUS_ENTER:
set_process_input(true)
NOTIFICATION_FOCUS_EXIT:
set_process_input(false)
func _input(event: InputEvent) -> void:
if not has_focus():
return
if event is InputEventKey:
if event.keycode == KEY_ENTER or event.keycode == KEY_KP_ENTER:
if event.is_pressed():
submit_text()
get_viewport().set_input_as_handled()
elif event.keycode == KEY_C and event.get_modifiers_mask() == KEY_MASK_CTRL and get_selected_text().is_empty():
# Clear input on Ctrl+C if no text selected.
if event.is_pressed():
text = ""
text_changed.emit()
get_viewport().set_input_as_handled()
elif event.keycode in [KEY_RIGHT, KEY_END] and get_caret_column() == text.length():
# Request autocomplete on RIGHT & END.
if event.is_pressed() and not autocomplete_hint.is_empty():
autocomplete_requested.emit()
get_viewport().set_input_as_handled()
func _draw() -> void:
var offset_x: int = 0
offset_x += _sb_normal.get_offset().x * 0.5
offset_x += get_line_width(0)
var offset_y: int = 0
offset_y += _sb_normal.get_offset().y * 0.5
offset_y += get_line_height() + 0.5 # + line_spacing
offset_y -= _font.get_descent(_font_size)
draw_string(_font, Vector2(offset_x, offset_y), autocomplete_hint, 0, -1, _font_size, _hint_color)
func submit_text() -> void:
text_submitted.emit(text)
func _hide_scrollbars() -> void:
get_v_scroll_bar().hide()
get_h_scroll_bar().hide()
class CommandEntryHighlighter extends SyntaxHighlighter:
var command_found_color: Color
var subcommand_color: Color
var command_not_found_color: Color
var text_color: Color
func _get_line_syntax_highlighting(line: int) -> Dictionary:
var text: String = get_text_edit().text
var command_end_idx: int = -1 # index where last recognized command ends (with subcommands)
var argv: PackedStringArray = [] # argument vector (aka tokens)
var argi: PackedInt32Array = [] # argument starting indices in text
var start: int = 0
var cur: int = 0
for char in text + ' ':
if char == ' ':
if cur > start:
argv.append(text.substr(start, cur - start))
argi.append(start)
var maybe_command: String = ' '.join(argv)
if LimboConsole.has_command(maybe_command) or LimboConsole.has_alias(maybe_command):
command_end_idx = cur
start = cur + 1
cur += 1
var command_color: Color
var arg_start_idx: int = 0 # index where arguments start
if command_end_idx > -1:
command_color = command_found_color
arg_start_idx = command_end_idx + 1
else:
command_color = command_not_found_color
arg_start_idx = argi[1] if argi.size() > 1 else text.length()
var result: Dictionary
result[0] = { "color": command_color }
if command_end_idx > -1 and argi.size() > 1:
result[argi[1]] = { "color": subcommand_color }
result[arg_start_idx] = { "color": text_color }
return result

View File

@@ -0,0 +1 @@
uid://ddtreeatktyov

View File

@@ -0,0 +1,165 @@
extends RefCounted
## Manages command history.
const HISTORY_FILE := "user://limbo_console_history.log"
var _entries: PackedStringArray
var _hist_idx = -1
var _iterators: Array[WrappingIterator]
var _is_dirty: bool = false
func push_entry(p_entry: String) -> void:
_push_entry(p_entry)
_reset_iterators()
func _push_entry(p_entry: String) -> void:
var idx: int = _entries.find(p_entry)
if idx != -1:
# Duplicate commands not allowed in history.
_entries.remove_at(idx)
_entries.append(p_entry)
_is_dirty = true
func get_entry(p_index: int) -> String:
return _entries[clampi(p_index, 0, _entries.size())]
func create_iterator() -> WrappingIterator:
var it := WrappingIterator.new(_entries)
_iterators.append(it)
return it
func release_iterator(p_iter: WrappingIterator) -> void:
_iterators.erase(p_iter)
func size() -> int:
return _entries.size()
func trim(p_max_size: int) -> void:
if _entries.size() > p_max_size:
_entries.slice(p_max_size - _entries.size())
_reset_iterators()
func clear() -> void:
_entries.clear()
func load(p_path: String = HISTORY_FILE) -> void:
var file := FileAccess.open(p_path, FileAccess.READ)
if not file:
return
while not file.eof_reached():
var line: String = file.get_line().strip_edges()
if not line.is_empty():
_push_entry(line)
file.close()
_reset_iterators()
_is_dirty = false
func save(p_path: String = HISTORY_FILE) -> void:
if not _is_dirty:
return
var file := FileAccess.open(p_path, FileAccess.WRITE)
if not file:
push_error("LimboConsole: Failed to save console history to file: ", p_path)
return
for line in _entries:
file.store_line(line)
file.close()
_is_dirty = false
## Searches history and returns an array starting with most relevant entries.
func fuzzy_match(p_query: String) -> PackedStringArray:
if len(p_query) == 0:
var copy := _entries.duplicate()
copy.reverse()
return copy
var results: Array = []
for entry: String in _entries:
var score: int = _compute_match_score(p_query.to_lower(), entry.to_lower())
if score > 0:
results.append({"entry": entry, "score": score})
results.sort_custom(func(a, b): return a.score > b.score)
return results.map(func(rec): return rec.entry)
func _reset_iterators() -> void:
for it in _iterators:
it._reassign(_entries)
## Scoring function for fuzzy matching.
static func _compute_match_score(query: String, target: String) -> int:
var score: int = 0
var query_index: int = 0
# Exact match. give unbeatable score
if query == target:
score = 99999
return score
for i in range(target.length()):
if query_index < query.length() and target[i] == query[query_index]:
score += 10 # Base score for a match
if i == 0 or target[i - 1] == " ": # Bonus for word start
score += 5
query_index += 1
if query_index == query.length():
break
# Ensure full query matches
return score if query_index == query.length() else 0
## Iterator that wraps around and resets on history change.
class WrappingIterator:
extends RefCounted
var _idx: int = -1
var _entries: PackedStringArray
func _init(p_entries: PackedStringArray) -> void:
_entries = p_entries
func prev() -> String:
_idx = wrapi(_idx - 1, -1, _entries.size())
if _idx == -1:
return String()
return _entries[_idx]
func next() -> String:
_idx = wrapi(_idx + 1, -1, _entries.size())
if _idx == -1:
return String()
return _entries[_idx]
func current() -> String:
if _idx < 0 or _idx >= _entries.size():
return String()
return _entries[_idx]
func reset() -> void:
_idx = -1
func _reassign(p_entries: PackedStringArray) -> void:
_idx = -1
_entries = p_entries

View File

@@ -0,0 +1 @@
uid://dc55ouwu3ylf

View File

@@ -0,0 +1,77 @@
@tool
extends RefCounted
## Store object properties in an INI-style configuration file.
const CONFIG_PATH_PROPERTY := &"CONFIG_PATH"
const MAIN_SECTION_PROPERTY := &"MAIN_SECTION"
const MAIN_SECTION_DEFAULT := "main"
static var verbose: bool = false
static func _get_config_file(p_object: Object) -> String:
var from_object_constant = p_object.get(CONFIG_PATH_PROPERTY)
return from_object_constant if from_object_constant is String else ""
static func _get_main_section(p_object: Object) -> String:
var from_object_constant = p_object.get(MAIN_SECTION_PROPERTY)
return from_object_constant if from_object_constant != null else MAIN_SECTION_DEFAULT
static func _msg(p_text: String, p_arg1 = "") -> void:
if verbose:
print(p_text, p_arg1)
## Load object properties from config file and return status. [br]
## If p_config_path is empty, configuration path is taken from object's CONFIG_PATH property.
static func load_from_config(p_object: Object, p_config_path: String = "") -> int:
var config_path: String = p_config_path
if config_path.is_empty():
config_path = _get_config_file(p_object)
var section: String = _get_main_section(p_object)
var config := ConfigFile.new()
var err: int = config.load(config_path)
if err != OK:
_msg("ConfigMapper: Failed to load config: %s err_code: %d" % [config_path, err])
return err
_msg("ConfigMapper: Loading config: ", config_path)
for prop_info in p_object.get_property_list():
if prop_info.usage & PROPERTY_USAGE_CATEGORY and prop_info.hint_string.is_empty():
_msg("ConfigMapper: Processing category: ", prop_info.name)
section = prop_info.name
elif prop_info.usage & PROPERTY_USAGE_SCRIPT_VARIABLE and prop_info.usage & PROPERTY_USAGE_STORAGE:
var value = null
if config.has_section_key(section, prop_info.name):
value = config.get_value(section, prop_info.name)
if value != null and typeof(value) == prop_info.type:
_msg("ConfigMapper: Loaded setting: %s section: %s value: %s" % [prop_info.name, section, value])
p_object.set(prop_info.name, value)
_msg("ConfigMapper: Finished with code: ", OK)
return OK
## Save object properties to config file and return status. [br]
## If p_config_path is empty, configuration path is taken from object's CONFIG_PATH property. [br]
## WARNING: replaces file contents!
static func save_to_config(p_object: Object, p_config_path: String = "") -> int:
var config_path: String = p_config_path
if config_path.is_empty():
config_path = _get_config_file(p_object)
var section: String = _get_main_section(p_object)
var config := ConfigFile.new()
_msg("ConfigMapper: Saving config: ", config_path)
for prop_info in p_object.get_property_list():
if prop_info.usage & PROPERTY_USAGE_CATEGORY and prop_info.hint_string.is_empty():
_msg("ConfigMapper: Processing category: ", prop_info.name)
section = prop_info.name
elif prop_info.usage & PROPERTY_USAGE_SCRIPT_VARIABLE and prop_info.usage & PROPERTY_USAGE_STORAGE:
_msg("ConfigMapper: Saving setting: %s section: %s value: %s" % [prop_info.name, section, p_object.get(prop_info.name)])
config.set_value(section, prop_info.name, p_object.get(prop_info.name))
var err: int = config.save(config_path)
_msg("ConfigMapper: Finished with code: ", err)
return err

View File

@@ -0,0 +1 @@
uid://dvokj0q23nb50

View File

@@ -0,0 +1,40 @@
extends RefCounted
# Configuration is outside of limbo_console directory for compatibility with GIT submodules
const CONFIG_PATH := "res://addons/limbo_console.cfg"
@export var aliases := {
"exit": "quit",
"source": "exec",
"usage": "help",
}
@export var disable_in_release_build: bool = false
@export var print_to_stdout: bool = false
@export var pause_when_open: bool = true
@export var commands_disabled_in_release: Array = [
"eval" # enables arbitrary code execution and asset extraction in the running game.
]
@export_category("appearance")
@export var custom_theme: String = "res://addons/limbo_console_theme.tres"
@export var height_ratio: float = 0.5
@export var open_speed: float = 5.0 # For instant, set to a really high float like 99999.0
@export var opacity: float = 1.0
@export var sparse_mode: bool = false # Print empty line after each command execution.
@export_category("greet")
@export var greet_user: bool = true
@export var greeting_message: String = "{project_name}"
@export var greet_using_ascii_art: bool = true
@export_category("history")
@export var persist_history: bool = true
@export var history_lines: int = 1000
@export_category("autocomplete")
@export var autocomplete_use_history_with_matches: bool = true
@export_category("autoexec")
@export var autoexec_script: String = "user://autoexec.lcs"
@export var autoexec_auto_create: bool = true

View File

@@ -0,0 +1 @@
uid://c7a12a1pe5esr

View File

@@ -0,0 +1,256 @@
extends Panel
const CommandHistory := preload("res://addons/limbo_console/command_history.gd")
# Visual Elements
var _last_highlighted_label: Label
var _history_labels: Array[Label]
var _scroll_bar: VScrollBar
var _scroll_bar_width: int = 12
# Indexing Results
var _command: String = "<placeholder>" # Needs default value so first search always processes
var _history: CommandHistory # Command history to search through
var _filter_results: PackedStringArray # Most recent results of performing a search for the _command in _history
var _display_count: int = 0 # Number of history items to display in search
var _offset: int = 0 # The offset _filter_results
var _sub_index: int = 0 # The highlight index
# Theme Cache
var _highlight_color: Color
# *** GODOT / VIRTUAL
func _init(p_history: CommandHistory) -> void:
_history = p_history
set_anchors_preset(Control.PRESET_FULL_RECT)
size_flags_horizontal = Control.SIZE_EXPAND_FILL
size_flags_vertical = Control.SIZE_EXPAND_FILL
# Create first label, and set placeholder text to determine the display size
# once this node is _ready(). There should always be one label at minimum
# anyways since this search is usless without a way to show results.
var new_item := Label.new()
new_item.size_flags_vertical = Control.SIZE_SHRINK_END
new_item.size_flags_horizontal = Control.SIZE_EXPAND_FILL
new_item.text = "<Placeholder>"
add_child(new_item)
_history_labels.append(new_item)
_scroll_bar = VScrollBar.new()
add_child(_scroll_bar)
func _ready() -> void:
# The sizing of the labels is dependant on visiblity.
visibility_changed.connect(_calculate_display_count)
_scroll_bar.scrolling.connect(_scroll_bar_scrolled)
_highlight_color = get_theme_color(&"history_highlight_color", &"ConsoleColors")
func _input(event: InputEvent) -> void:
if not is_visible_in_tree():
return
# Scroll up/down on mouse wheel up/down
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
_increment_index()
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
_decrement_index()
# Remaining inputs are key press handles
if event is not InputEventKey:
return
# Increment/Decrement index
if event.keycode == KEY_UP and event.is_pressed():
_increment_index()
get_viewport().set_input_as_handled()
elif event.keycode == KEY_DOWN and event.is_pressed():
_decrement_index()
get_viewport().set_input_as_handled()
# *** PUBLIC
## Set visibility of history search
func set_visibility(p_visible: bool) -> void:
if not visible and p_visible:
# It's possible the _history has updated while not visible
# make sure the filtered list is up-to-date
_search_and_filter()
visible = p_visible
## Move cursor downwards
func _decrement_index() -> void:
var current_index: int = _get_current_index()
if current_index - 1 < 0:
return
if _sub_index == 0:
_offset -= 1
_update_scroll_list()
else:
_sub_index -= 1
_update_highlight()
## Move cursor upwards
func _increment_index() -> void:
var current_index: int = _get_current_index()
if current_index + 1 >= _filter_results.size():
return
if _sub_index >= _display_count - 1:
_offset += 1
_update_scroll_list()
else:
_sub_index += 1
_update_highlight()
## Get the current selected text
func get_current_text() -> String:
var current_text: String = _command
if _history_labels.size() != 0 and _filter_results.size() != 0:
current_text = _filter_results[_get_current_index()]
return current_text
## Search for the command in the history
func search(command: String) -> void:
# Don't process if we used the same command before
if command == _command:
return
_command = command
_search_and_filter()
# *** PRIVATE
## Update the text in the scroll list to match current offset and filtered results
func _update_scroll_list() -> void:
# Iterate through the number of displayed history items
for i in range(0, _display_count):
var filter_index: int = _offset + i
# Default empty
_history_labels[i].text = ""
# Set non empty if in range
var index_in_range: bool = filter_index < _filter_results.size()
if index_in_range:
_history_labels[i].text += _filter_results[filter_index]
_update_scroll_bar()
## Highlight the subindex
func _update_highlight() -> void:
if _sub_index < 0 or _history.size() == 0:
return
var style := StyleBoxFlat.new()
style.bg_color = _highlight_color
# Always clear out the highlight of the last label
if is_instance_valid(_last_highlighted_label):
_last_highlighted_label.remove_theme_stylebox_override("normal")
if _filter_results.size() <= 0:
return
_history_labels[_sub_index].add_theme_stylebox_override("normal", style)
_last_highlighted_label = _history_labels[_sub_index]
## Get the current index of the selected item
func _get_current_index() -> int:
return _offset + _sub_index
## Reset offset and sub_indexes to scroll list back to bottom
func _reset_indexes() -> void:
_offset = 0
_sub_index = 0
## When the scrollbar has been scrolled (by mouse), scroll the list
func _scroll_bar_scrolled() -> void:
_offset = _scroll_bar.max_value - _display_count - _scroll_bar.value
_update_highlight()
_update_scroll_list()
func _calculate_display_count():
if not visible:
return
# The display count is finnicky to get right due to the label needing to be
# rendered so the fize can be determined. This gets the job done, it ain't
# pretty, but it works
var max_y: float = size.y
var label_size_y: float = (_history_labels[0] as Control).size.y
var label_size_x: float = size.x - _scroll_bar_width
var display_count: int = int(max_y) / int(label_size_y)
if _display_count != display_count and display_count != 0 and display_count > _display_count:
_display_count = (display_count as int)
# Since the labels are going from the bottom to the top, the label
# coordinates are offset from the bottom by label size.
# The first label already exists, so it's handlded by itself
_history_labels[0].position.y = size.y - label_size_y
_history_labels[0].set_size(Vector2(label_size_x, label_size_y))
# The remaining labels may or may not exist already, create them
for i in range(0, _display_count - _history_labels.size()):
var new_item := Label.new()
new_item.size_flags_vertical = Control.SIZE_SHRINK_END
new_item.size_flags_horizontal = Control.SIZE_EXPAND_FILL
# The +1 is due to the labels going upwards from the bottom, otherwise
# their position will be 1 row lower than they should be
var position_offset: int = _history_labels.size() + 1
new_item.position.y = size.y - (position_offset * label_size_y)
new_item.set_size(Vector2(label_size_x, label_size_y))
_history_labels.append(new_item)
add_child(new_item)
# Update the scroll bar to be positioned correctly
_scroll_bar.size.x = _scroll_bar_width
_scroll_bar.size.y = size.y
_scroll_bar.position.x = label_size_x
_reset_history_to_beginning()
func _update_scroll_bar() -> void:
if _display_count > 0:
var max_size: int = _filter_results.size()
_scroll_bar.max_value = max_size
_scroll_bar.page = _display_count
_scroll_bar.set_value_no_signal((max_size - _display_count) - _offset)
## Reset indexes to 0, scroll to the bottom of the history list, and update visuals
func _reset_history_to_beginning() -> void:
_reset_indexes()
_update_highlight()
_update_scroll_list()
## Search for the current command and filter the results
func _search_and_filter() -> void:
_filter_results = _history.fuzzy_match(_command)
_reset_history_to_beginning()

View File

@@ -0,0 +1 @@
uid://cpl5jb0mwxanh

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
uid://dyxornv8vwibg

View File

@@ -0,0 +1,7 @@
[plugin]
name="LimboConsole"
description="Yet another in-game console with a simple command interpreter."
author="Serhii Snitsaruk"
version="0.4.1"
script="plugin.gd"

View File

@@ -0,0 +1,57 @@
@tool
extends EditorPlugin
const ConsoleOptions := preload("res://addons/limbo_console/console_options.gd")
const ConfigMapper := preload("res://addons/limbo_console/config_mapper.gd")
func _enter_tree() -> void:
add_autoload_singleton("LimboConsole", "res://addons/limbo_console/limbo_console.gd")
# Sync config file (create if not exists)
var console_options := ConsoleOptions.new()
var do_project_setting_save: bool = false
ConfigMapper.load_from_config(console_options)
ConfigMapper.save_to_config(console_options)
if not ProjectSettings.has_setting("input/limbo_console_toggle"):
print("LimboConsole: Adding \"limbo_console_toggle\" input action to project settings...")
var key_event := InputEventKey.new()
key_event.keycode = KEY_QUOTELEFT
ProjectSettings.set_setting("input/limbo_console_toggle", {
"deadzone": 0.5,
"events": [key_event],
})
do_project_setting_save = true
if not ProjectSettings.has_setting("input/limbo_auto_complete_reverse"):
print("LimboConsole: Adding \"limbo_auto_complete_reverse\" input action to project settings...")
var key_event = InputEventKey.new()
key_event.keycode = KEY_TAB
key_event.shift_pressed = true
ProjectSettings.set_setting("input/limbo_auto_complete_reverse", {
"deadzone": 0.5,
"events": [key_event],
})
do_project_setting_save = true
if not ProjectSettings.has_setting("input/limbo_console_search_history"):
print("LimboConsole: Adding \"limbo_console_search_history\" input action to project settings...")
var key_event = InputEventKey.new()
key_event.keycode = KEY_R
key_event.ctrl_pressed = true
ProjectSettings.set_setting("input/limbo_console_search_history", {
"deadzone": 0.5,
"events": [key_event],
})
do_project_setting_save = true
if do_project_setting_save:
ProjectSettings.save()
func _exit_tree() -> void:
remove_autoload_singleton("LimboConsole")

View File

@@ -0,0 +1 @@
uid://b4t0bfyjdn8i0

View File

@@ -0,0 +1,176 @@
[gd_resource type="Theme" load_steps=23 format=3 uid="uid://dq4nntds66bix"]
[ext_resource type="FontFile" uid="uid://dbbw833hg2v7o" path="res://addons/limbo_console/res/fonts/monaspace_argon_bold.otf" id="1_cry2i"]
[ext_resource type="FontFile" uid="uid://dmeyp84repfbw" path="res://addons/limbo_console/res/fonts/monaspace_argon_bold_italic.otf" id="2_h3f73"]
[ext_resource type="FontFile" uid="uid://dhm45nttm5i3s" path="res://addons/limbo_console/res/fonts/monaspace_argon_italic.otf" id="3_2qq11"]
[ext_resource type="FontFile" uid="uid://ds7dvyquauqub" path="res://addons/limbo_console/res/fonts/monaspace_argon_medium.otf" id="4_06w3f"]
[ext_resource type="FontFile" uid="uid://d4js20k8kslqt" path="res://addons/limbo_console/res/fonts/monaspace_argon_regular.otf" id="5_p4ppy"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_yia2g"]
content_margin_left = 6.0
content_margin_top = 4.0
content_margin_right = 6.0
content_margin_bottom = 4.0
bg_color = Color(0.147, 0.168, 0.203, 1)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 3
anti_aliasing = false
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_bt363"]
content_margin_left = 2.0
content_margin_top = 4.0
content_margin_right = 2.0
content_margin_bottom = 4.0
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_txn37"]
content_margin_left = 2.0
content_margin_top = 4.0
content_margin_right = 2.0
content_margin_bottom = 4.0
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ypc4n"]
content_margin_left = 2.0
content_margin_top = 4.0
content_margin_right = 2.0
content_margin_bottom = 4.0
bg_color = Color(0, 0, 0, 0)
border_width_top = 1
border_color = Color(0.211765, 0.239216, 0.290196, 1)
[sub_resource type="Image" id="Image_jqg6r"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 3, 255, 255, 255, 41, 255, 255, 255, 67, 255, 255, 255, 67, 255, 255, 255, 40, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 41, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 40, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 67, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 67, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 67, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 67, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 40, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 40, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 3, 255, 255, 255, 40, 255, 255, 255, 67, 255, 255, 255, 67, 255, 255, 255, 40, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 12,
"mipmaps": false,
"width": 12
}
[sub_resource type="ImageTexture" id="ImageTexture_utg8u"]
image = SubResource("Image_jqg6r")
[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_xismo"]
content_margin_left = 7.0
content_margin_top = 7.0
content_margin_right = 7.0
content_margin_bottom = 7.0
texture = SubResource("ImageTexture_utg8u")
texture_margin_left = 6.0
texture_margin_top = 6.0
texture_margin_right = 6.0
texture_margin_bottom = 6.0
[sub_resource type="Image" id="Image_rt1dl"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 6, 248, 248, 248, 102, 249, 249, 249, 168, 249, 249, 249, 168, 248, 248, 248, 101, 213, 213, 213, 6, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 248, 248, 248, 102, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 248, 248, 248, 101, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 249, 249, 249, 168, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 168, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 249, 249, 249, 168, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 248, 248, 248, 168, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 248, 248, 248, 101, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 250, 250, 250, 99, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 213, 213, 213, 6, 248, 248, 248, 101, 249, 249, 249, 168, 248, 248, 248, 168, 250, 250, 250, 99, 213, 213, 213, 6, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 12,
"mipmaps": false,
"width": 12
}
[sub_resource type="ImageTexture" id="ImageTexture_foc76"]
image = SubResource("Image_rt1dl")
[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_oajxf"]
content_margin_left = 6.0
content_margin_top = 6.0
content_margin_right = 6.0
content_margin_bottom = 6.0
texture = SubResource("ImageTexture_foc76")
texture_margin_left = 5.0
texture_margin_top = 5.0
texture_margin_right = 5.0
texture_margin_bottom = 5.0
[sub_resource type="Image" id="Image_2c5qw"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 213, 213, 213, 6, 180, 180, 180, 102, 181, 181, 181, 168, 181, 181, 181, 168, 179, 179, 179, 101, 170, 170, 170, 6, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 180, 180, 180, 102, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 179, 179, 179, 101, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 181, 181, 181, 168, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 181, 181, 181, 168, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 181, 181, 181, 168, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 179, 179, 179, 168, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 179, 179, 179, 101, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 181, 181, 181, 99, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 170, 170, 170, 6, 179, 179, 179, 101, 181, 181, 181, 168, 179, 179, 179, 168, 181, 181, 181, 99, 170, 170, 170, 6, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 12,
"mipmaps": false,
"width": 12
}
[sub_resource type="ImageTexture" id="ImageTexture_alomt"]
image = SubResource("Image_2c5qw")
[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_ev3il"]
content_margin_left = 7.0
content_margin_top = 7.0
content_margin_right = 7.0
content_margin_bottom = 7.0
texture = SubResource("ImageTexture_alomt")
texture_margin_left = 6.0
texture_margin_top = 6.0
texture_margin_right = 6.0
texture_margin_bottom = 6.0
[sub_resource type="Image" id="Image_c2fg1"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 7, 255, 255, 255, 19, 255, 255, 255, 19, 255, 255, 255, 7, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 19, 255, 255, 255, 21, 255, 255, 255, 21, 255, 255, 255, 19, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 19, 255, 255, 255, 21, 255, 255, 255, 21, 255, 255, 255, 19, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 7, 255, 255, 255, 19, 255, 255, 255, 19, 255, 255, 255, 6, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 12,
"mipmaps": false,
"width": 12
}
[sub_resource type="ImageTexture" id="ImageTexture_lun2q"]
image = SubResource("Image_c2fg1")
[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_lk88d"]
content_margin_left = 6.0
content_margin_top = 0.0
content_margin_right = 6.0
content_margin_bottom = 0.0
texture = SubResource("ImageTexture_lun2q")
texture_margin_left = 5.0
texture_margin_top = 5.0
texture_margin_right = 5.0
texture_margin_bottom = 5.0
[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_4ui4a"]
content_margin_left = 6.0
content_margin_top = 6.0
content_margin_right = 6.0
content_margin_bottom = 6.0
texture = SubResource("ImageTexture_lun2q")
texture_margin_left = 5.0
texture_margin_top = 5.0
texture_margin_right = 5.0
texture_margin_bottom = 5.0
[resource]
default_font = ExtResource("5_p4ppy")
default_font_size = 18
ConsoleColors/colors/entry_command_found_color = Color(0.729412, 0.901961, 0.494118, 1)
ConsoleColors/colors/entry_command_not_found_color = Color(1, 0.2, 0.2, 1)
ConsoleColors/colors/entry_hint_color = Color(0.439216, 0.478431, 0.54902, 1)
ConsoleColors/colors/entry_subcommand_color = Color(0.584314, 0.901961, 0.796078, 1)
ConsoleColors/colors/entry_text_color = Color(0.796078, 0.8, 0.776471, 1)
ConsoleColors/colors/history_highlight_color = Color(0.317647, 0.364706, 0.439216, 1)
ConsoleColors/colors/output_command_color = Color(0.729412, 0.901961, 0.494118, 1)
ConsoleColors/colors/output_command_mention_color = Color(0.584314, 0.901961, 0.796078, 1)
ConsoleColors/colors/output_debug_color = Color(0.439216, 0.478431, 0.54902, 1)
ConsoleColors/colors/output_error_color = Color(1, 0.2, 0.2, 1)
ConsoleColors/colors/output_text_color = Color(0.796078, 0.8, 0.776471, 1)
ConsoleColors/colors/output_warning_color = Color(1, 0.654902, 0.34902, 1)
Panel/styles/panel = SubResource("StyleBoxFlat_yia2g")
PanelContainer/styles/panel = SubResource("StyleBoxFlat_yia2g")
RichTextLabel/fonts/bold_font = ExtResource("1_cry2i")
RichTextLabel/fonts/bold_italics_font = ExtResource("2_h3f73")
RichTextLabel/fonts/italics_font = ExtResource("3_2qq11")
RichTextLabel/fonts/mono_font = ExtResource("4_06w3f")
RichTextLabel/fonts/normal_font = ExtResource("5_p4ppy")
RichTextLabel/styles/focus = SubResource("StyleBoxEmpty_bt363")
RichTextLabel/styles/normal = SubResource("StyleBoxEmpty_txn37")
TextEdit/styles/focus = SubResource("StyleBoxFlat_ypc4n")
TextEdit/styles/normal = SubResource("StyleBoxFlat_ypc4n")
VScrollBar/styles/grabber = SubResource("StyleBoxTexture_xismo")
VScrollBar/styles/grabber_highlight = SubResource("StyleBoxTexture_oajxf")
VScrollBar/styles/grabber_pressed = SubResource("StyleBoxTexture_ev3il")
VScrollBar/styles/scroll = SubResource("StyleBoxTexture_lk88d")
VScrollBar/styles/scroll_focus = SubResource("StyleBoxTexture_4ui4a")

View File

@@ -0,0 +1,93 @@
Copyright (c) 2023, GitHub https://github.com/githubnext/monaspace
with Reserved Font Name "Monaspace", including subfamilies: "Argon", "Neon", "Xenon", "Radon", and "Krypton"
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,35 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://dbbw833hg2v7o"
path="res://.godot/imported/monaspace_argon_bold.otf-5d916059e7810f56d30161557f70d71b.fontdata"
[deps]
source_file="res://addons/limbo_console/res/fonts/monaspace_argon_bold.otf"
dest_files=["res://.godot/imported/monaspace_argon_bold.otf-5d916059e7810f56d30161557f70d71b.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -0,0 +1,35 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://dmeyp84repfbw"
path="res://.godot/imported/monaspace_argon_bold_italic.otf-cd05eebec36875096d59dc2e6dfb87db.fontdata"
[deps]
source_file="res://addons/limbo_console/res/fonts/monaspace_argon_bold_italic.otf"
dest_files=["res://.godot/imported/monaspace_argon_bold_italic.otf-cd05eebec36875096d59dc2e6dfb87db.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -0,0 +1,35 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://dhm45nttm5i3s"
path="res://.godot/imported/monaspace_argon_italic.otf-69d64783adde526699a99a191cd14ed6.fontdata"
[deps]
source_file="res://addons/limbo_console/res/fonts/monaspace_argon_italic.otf"
dest_files=["res://.godot/imported/monaspace_argon_italic.otf-69d64783adde526699a99a191cd14ed6.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -0,0 +1,35 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://ds7dvyquauqub"
path="res://.godot/imported/monaspace_argon_medium.otf-2c090420a59a2bfcd1b6b28a7c3469ad.fontdata"
[deps]
source_file="res://addons/limbo_console/res/fonts/monaspace_argon_medium.otf"
dest_files=["res://.godot/imported/monaspace_argon_medium.otf-2c090420a59a2bfcd1b6b28a7c3469ad.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -0,0 +1,35 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://d4js20k8kslqt"
path="res://.godot/imported/monaspace_argon_regular.otf-7622e0becc7f9143f21c9951dd015f30.fontdata"
[deps]
source_file="res://addons/limbo_console/res/fonts/monaspace_argon_regular.otf"
dest_files=["res://.godot/imported/monaspace_argon_regular.otf-7622e0becc7f9143f21c9951dd015f30.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -0,0 +1,119 @@
extends Object
## Utility functions
static func bbcode_escape(p_text: String) -> String:
return p_text \
.replace("[", "~LB~") \
.replace("]", "~RB~") \
.replace("~LB~", "[lb]") \
.replace("~RB~", "[rb]")
static func bbcode_strip(p_text: String) -> String:
var stripped := ""
var in_brackets: bool = false
for c: String in p_text:
if c == '[':
in_brackets = true
elif c == ']':
in_brackets = false
elif not in_brackets:
stripped += c
return stripped
static func get_method_info(p_callable: Callable) -> Dictionary:
var method_info: Dictionary
var method_list: Array[Dictionary]
if p_callable.get_object() is GDScript:
method_list = p_callable.get_object().get_script_method_list()
else:
method_list = p_callable.get_object().get_method_list()
for m in method_list:
if m.name == p_callable.get_method():
method_info = m
break
if !method_info and p_callable.is_custom():
var args: Array
var default_args: Array
for i in p_callable.get_argument_count():
var argument: Dictionary
argument["name"] = "arg%d" % i
argument["type"] = TYPE_NIL
args.push_back(argument)
method_info["name"] = "<anonymous lambda>"
method_info["args"] = args
method_info["default_args"] = default_args
return method_info
## Finds the most similar string in an array.
static func fuzzy_match_string(p_string: String, p_max_edit_distance: int, p_array) -> String:
if typeof(p_array) < TYPE_ARRAY:
push_error("LimboConsole: Internal error: p_array is not an array")
return ""
if p_array.size() == 0:
return ""
var best_distance: int = 9223372036854775807
var best_match: String = ""
for i in p_array.size():
var elem := str(p_array[i])
var dist: float = _calculate_osa_distance(p_string, elem)
if dist < best_distance:
best_distance = dist
best_match = elem
return best_match if best_distance <= p_max_edit_distance else ""
## Calculates optimal string alignment distance [br]
## See: https://en.wikipedia.org/wiki/Levenshtein_distance
static func _calculate_osa_distance(s1: String, s2: String) -> int:
var s1_len: int = s1.length()
var s2_len: int = s2.length()
# Iterative approach with 3 matrix rows.
# Most of the work is done on row1 and row2 - row0 is only needed to calculate transposition cost.
var row0: PackedInt32Array # previous-previous
var row1: PackedInt32Array # previous
var row2: PackedInt32Array # current aka the one we need to calculate
row0.resize(s2_len + 1)
row1.resize(s2_len + 1)
row2.resize(s2_len + 1)
# edit distance is the number of characters to insert to get from empty string to s2
for i in range(s2_len + 1):
row1[i] = i
for i in range(s1_len):
# edit distance is the number of characters to delete from s1 to match empty s2
row2[0] = i + 1
for j in range(s2_len):
var deletion_cost: int = row1[j + 1] + 1
var insertion_cost: int = row2[j] + 1
var substitution_cost: int = row1[j] if s1[i] == s2[j] else row1[j] + 1
row2[j + 1] = min(deletion_cost, insertion_cost, substitution_cost)
if i > 1 and j > 1 and s1[i - 1] == s2[j] and s1[i - 1] == s2[j]:
var transposition_cost: int = row0[j - 1] + 1
row2[j + 1] = mini(transposition_cost, row2[j + 1])
# Swap rows.
var tmp: PackedInt32Array = row0
row0 = row1
row1 = row2
row2 = tmp
return row1[s2_len]
## Returns true, if a string is constructed of one or more space-separated valid
## command identifiers ("command" or "command sub1 sub2").
## A valid command identifier may contain only letters, digits, and underscores (_),
## and the first character may not be a digit.
static func is_valid_command_sequence(p_string: String) -> bool:
for part in p_string.split(' '):
if not part.is_valid_ascii_identifier():
return false
return true

View File

@@ -0,0 +1 @@
uid://cw6s1es6yjip5

View File

@@ -21,6 +21,7 @@ config/icon="uid://dihjvrhshhklu"
[autoload] [autoload]
GameBus="*res://Scripts/Singletons/GameBus.cs" GameBus="*res://Scripts/Singletons/GameBus.cs"
LimboConsole="*res://addons/limbo_console/limbo_console.gd"
[display] [display]
@@ -30,10 +31,32 @@ window/stretch/mode="canvas_items"
project/assembly_name="ParasiticGod" project/assembly_name="ParasiticGod"
[editor_plugins]
enabled=PackedStringArray("res://addons/limbo_console/plugin.cfg")
[gui] [gui]
theme/custom_font="uid://b8tf6dbm2j0ju" theme/custom_font="uid://b8tf6dbm2j0ju"
[input]
limbo_console_toggle={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":96,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
limbo_auto_complete_reverse={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194306,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
limbo_console_search_history={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":82,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
[rendering] [rendering]
renderer/rendering_method="gl_compatibility" renderer/rendering_method="gl_compatibility"