Compare commits

...

1 Commits
main ... main

Author SHA1 Message Date
BounceU
4510f97126 Generate tilemap on enter key being pressed 2025-10-10 18:23:22 -04:00
2422 changed files with 243 additions and 132309 deletions

View File

@ -1,7 +1,6 @@
[configuration]
entry_symbol = "gdext_rust_init"
compatibility_minimum = 4.1
reloadable = true
[libraries]
"android.debug" = "res://../rust/target/debug/godottest_rs.so"
@ -66,5 +65,6 @@ reloadable = true
"windows.editor.x86_64" = "res://../rust/target/x86_64-pc-windows-msvc/debug/godottest_rs.dll"
[icons]
Player = "res://addons/rust/NodeRustFerris.svg"
AsyncRuntime = "res://addons/rust/NodeRustFerris.svg"
Player = "res://addons/rust/NodeRustFerris.svg"
Mappy = "res://addons/rust/NodeRustFerris.svg"

View File

@ -1,7 +0,0 @@
[gd_resource type="InputEventKey" format=3 uid="uid://bafyb8y38ahfh"]
[resource]
device = -1
ctrl_pressed = true
keycode = 32
unicode = 32

View File

@ -1,110 +0,0 @@
@tool
extends EditorPlugin
# Padding from the bottom when popped out
var padding: int = 20
# Padding from the bottom when not popped out
var bottompadding: int = 60
# The file system
var FileDock: Object
# Toggle for when the file system is moved to bottom
var filesBottom: bool = false
var newSize: Vector2
var initialLoad: bool = false
var AssetDrawerShortcut: InputEventKey = InputEventKey.new()
var showing: bool = false
func _enter_tree() -> void:
# Add tool button to move shelf to editor bottom
add_tool_menu_item("Files to Bottom", Callable(self, "FilesToBottom"))
# Get our file system
FileDock = self.get_editor_interface().get_file_system_dock()
await get_tree().create_timer(0.1).timeout
FilesToBottom()
# Prevent file tree from being shrunk on load
await get_tree().create_timer(0.1).timeout
var file_split_container := FileDock.get_child(3) as SplitContainer
file_split_container .split_offset = 175
# Get shortcut
AssetDrawerShortcut = preload("res://addons/Asset_Drawer/AssetDrawerShortcut.tres")
#region show hide filesystem
func _input(event: InputEvent) -> void:
if (AssetDrawerShortcut.is_match(event) &&
event.is_pressed() &&
!event.is_echo()):
if filesBottom == true:
match showing:
false:
make_bottom_panel_item_visible(FileDock)
showing = true
true:
print("hide")
hide_bottom_panel()
showing = false
#endregion
func _exit_tree() -> void:
remove_tool_menu_item("Files to Bottom")
FilesToBottom()
func _process(delta: float) -> void:
newSize = FileDock.get_window().size
# Keeps the file system from being unusable in size
if FileDock.get_window().name == "root" && filesBottom == false:
FileDock.get_child(3).get_child(0).size.y = newSize.y - padding
FileDock.get_child(3).get_child(1).size.y = newSize.y - padding
return
# Adjust the size of the file system based on how far up
# the drawer has been pulled
if FileDock.get_window().name == "root" && filesBottom == true:
newSize = FileDock.get_parent().size
var editor = get_editor_interface()
var editorsettings = editor.get_editor_settings()
var fontsize: int = editorsettings.get_setting("interface/editor/main_font_size")
var editorscale = EditorInterface.get_editor_scale()
FileDock.get_child(3).get_child(0).size.y = newSize.y - (fontsize * 2) - (bottompadding * EditorInterface.get_editor_scale())
FileDock.get_child(3).get_child(1).size.y = newSize.y - (fontsize * 2) - (bottompadding * EditorInterface.get_editor_scale())
return
# Keeps our systems sized when popped out
if (FileDock.get_window().name != "root" && filesBottom == false):
FileDock.get_window().min_size.y = 50
FileDock.get_child(3).get_child(0).size.y = newSize.y - padding
FileDock.get_child(3).get_child(1).size.y = newSize.y - padding
# Centers window on first pop
if initialLoad == false:
initialLoad = true
var screenSize: Vector2 = DisplayServer.screen_get_size()
FileDock.get_window().position = screenSize/2
return
# Moves the files between the bottom panel and the original dock
func FilesToBottom() -> void:
if filesBottom == true:
remove_control_from_bottom_panel(FileDock)
add_control_to_dock(EditorPlugin.DOCK_SLOT_LEFT_BR, FileDock)
filesBottom = false
return
FileDock = self.get_editor_interface().get_file_system_dock()
remove_control_from_docks(FileDock)
add_control_to_bottom_panel(FileDock, "File System")
filesBottom = true

View File

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

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 Michael McGuire
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

@ -1,7 +0,0 @@
[plugin]
name="Asset Drawer"
description="Converts the File dock to an Asset Drawer at the bottom of the editor."
author="GlitchedCode"
version=""
script="FileSystem.gd"

View File

@ -1,212 +0,0 @@
class_name GedisCore
# Core data buckets
var _store: Dictionary = {}
var _hashes: Dictionary = {}
var _lists: Dictionary = {}
var _sets: Dictionary = {}
var _sorted_sets: Dictionary = {}
var _expiry: Dictionary = {}
# Pub/Sub registries
var _subscribers: Dictionary[String, Array] = {}
var _psubscribers: Dictionary[String, Array] = {}
var _gedis: Gedis
func _init(gedis_instance: Gedis) -> void:
_gedis = gedis_instance
func _now() -> int:
return _gedis._time_source.get_time()
func _delete_all_types_for_key(key: String) -> void:
_store.erase(key)
_hashes.erase(key)
_lists.erase(key)
_sets.erase(key)
_sorted_sets.erase(key)
_expiry.erase(key)
func _touch_type(key: String, type_bucket: Dictionary) -> void:
# When a key is used for a new type, remove it from other types.
if not type_bucket.has(key):
_store.erase(key)
_hashes.erase(key)
_lists.erase(key)
_sets.erase(key)
_sorted_sets.erase(key)
func key_exists(key: String) -> bool:
return _store.has(key) or _hashes.has(key) or _lists.has(key) or _sets.has(key) or _sorted_sets.has(key)
func _get_all_keys() -> Dictionary[String, bool]:
var all: Dictionary[String, bool] = {}
for k in _store.keys():
all[str(k)] = true
for k in _hashes.keys():
all[str(k)] = true
for k in _lists.keys():
all[str(k)] = true
for k in _sets.keys():
all[str(k)] = true
for k in _sorted_sets.keys():
all[str(k)] = true
return all
func flushall() -> void:
_store.clear()
_hashes.clear()
_lists.clear()
_sets.clear()
_sorted_sets.clear()
_expiry.clear()
_subscribers.clear()
_psubscribers.clear()
func dump_all(options: Dictionary = {}) -> Dictionary:
var state := {
"store": _store.duplicate(true),
"hashes": _hashes.duplicate(true),
"lists": _lists.duplicate(true),
"sets": _sets.duplicate(true),
"sorted_sets": _sorted_sets.duplicate(true),
"expiry": _expiry.duplicate(true),
}
var include: Array = options.get("include", [])
var exclude: Array = options.get("exclude", [])
if not include.is_empty():
for bucket_name in state:
var bucket: Dictionary = state[bucket_name]
for key in bucket.keys():
var keep = false
for prefix in include:
if key.begins_with(prefix):
keep = true
break
if not keep:
bucket.erase(key)
if not exclude.is_empty():
for bucket_name in state:
var bucket: Dictionary = state[bucket_name]
for key in bucket.keys():
for prefix in exclude:
if key.begins_with(prefix):
bucket.erase(key)
break
return state
func restore_all(state: Dictionary) -> void:
flushall()
_store = state.get("store", {})
_hashes = state.get("hashes", {})
_lists = state.get("lists", {})
_sets = state.get("sets", {})
_sorted_sets = state.get("sorted_sets", {})
_expiry = state.get("expiry", {})
# Discard expired keys
var now: float = _now()
for key in _expiry.keys():
if _expiry[key] < now:
_delete_all_types_for_key(key)
func restore_key(key: String, data: Dictionary) -> void:
_delete_all_types_for_key(key)
var value = data["value"]
var type = data["type"]
if type == "string":
_store[key] = value
elif type == "hash":
_hashes[key] = value
elif type == "list":
_lists[key] = value
elif type == "set":
_sets[key] = value
elif type == "sorted_set":
_sorted_sets[key] = value
if data.has("expiry"):
_expiry[key] = data["expiry"]
func rename(key: String, newkey: String) -> int:
if not key_exists(key):
return ERR_DOES_NOT_EXIST
if key_exists(newkey):
return 0
var value
if _store.has(key):
value = _store[key]
_store[newkey] = value
elif _hashes.has(key):
value = _hashes[key]
_hashes[newkey] = value
elif _lists.has(key):
value = _lists[key]
_lists[newkey] = value
elif _sets.has(key):
value = _sets[key]
_sets[newkey] = value
elif _sorted_sets.has(key):
value = _sorted_sets[key]
_sorted_sets[newkey] = value
else:
return ERR_DOES_NOT_EXIST
if _expiry.has(key):
var expiry_time = _expiry[key]
_expiry[newkey] = expiry_time
_delete_all_types_for_key(key)
return 1
func move(key: String, newkey: String) -> int:
if not key_exists(key):
return ERR_DOES_NOT_EXIST
if key_exists(newkey):
_delete_all_types_for_key(newkey)
var value
if _store.has(key):
value = _store[key]
_store[newkey] = value
elif _hashes.has(key):
value = _hashes[key]
_hashes[newkey] = value
elif _lists.has(key):
value = _lists[key]
_lists[newkey] = value
elif _sets.has(key):
value = _sets[key]
_sets[newkey] = value
elif _sorted_sets.has(key):
value = _sorted_sets[key]
_sorted_sets[newkey] = value
else:
return ERR_DOES_NOT_EXIST
if _expiry.has(key):
var expiry_time = _expiry[key]
_expiry[newkey] = expiry_time
_delete_all_types_for_key(key)
return 1
func ks(key: String) -> String:
return "gedis:keyspace:" + key
func rks(key: String) -> String:
var prefix = "gedis:keyspace:"
if key.begins_with(prefix):
return key.substr(prefix.length())
else:
return key

View File

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

View File

@ -1,162 +0,0 @@
extends RefCounted
class_name GedisDebugger
var _gedis: Gedis
func _init(gedis: Gedis):
_gedis = gedis
if _gedis and _gedis._pubsub:
_gedis._pubsub.subscribed.connect(_on_subscribed)
_gedis._pubsub.unsubscribed.connect(_on_unsubscribed)
func _on_subscribed(channel: String, subscriber: Object):
var debugger = Engine.get_singleton("EngineDebugger")
if debugger and debugger.is_active():
debugger.send_message("gedis:pubsub_event", ["subscribed", channel, str(subscriber)])
func _on_unsubscribed(channel: String, subscriber: Object):
var debugger = Engine.get_singleton("EngineDebugger")
if debugger and debugger.is_active():
debugger.send_message("gedis:pubsub_event", ["unsubscribed", channel, str(subscriber)])
static func _ensure_debugger_is_registered():
if Engine.is_editor_hint():
return
if not Gedis._debugger_registered:
if Engine.has_singleton("EngineDebugger"):
var debugger = Engine.get_singleton("EngineDebugger")
debugger.register_message_capture("gedis", Callable(Gedis, "_on_debugger_message"))
if debugger.is_active():
debugger.send_message("gedis:ping", [])
Gedis._debugger_registered = true
static func _on_debugger_message(message: String, data: Array) -> bool:
# EngineDebugger will call this with the suffix (the part after "gedis:")
# so message will be e.g. "request_instances" or "request_instance_data".
if not Engine.has_singleton("EngineDebugger"):
return false
match message:
"request_instances":
var instances_data = []
for instance_info in Gedis.get_all_instances():
instances_data.append({
"id": instance_info["id"],
"name": instance_info["name"]
})
var debugger = Engine.get_singleton("EngineDebugger")
if debugger and debugger.is_active():
debugger.send_message("gedis:instances_data", instances_data)
return true
"request_instance_data":
if data.size() < 2:
return false
var instance_id = data[0]
var command = data[1]
# Find the target instance in the static registry.
var target_instance = null
for inst in Gedis._instances:
if is_instance_valid(inst) and inst._instance_id == instance_id:
target_instance = inst
break
if target_instance == null:
return false
var debugger = Engine.get_singleton("EngineDebugger")
if not debugger or not debugger.is_active():
return false
match command:
"snapshot":
var pattern = data[2] if data.size() > 2 else "*"
var snapshot_data = target_instance.snapshot(pattern)
debugger.send_message("gedis:snapshot_data", [snapshot_data])
return true
"dump":
if data.size() < 3:
return false
var key = data[2]
var key_value_data = target_instance.dump_key(key)
debugger.send_message("gedis:key_value_data", [key_value_data])
return true
"set":
if data.size() < 4:
return false
var key = data[2]
var value = data[3]
target_instance.set_value(key, value)
var key_value_data = target_instance.dump_key(key)
debugger.send_message("gedis:key_value_data", [key_value_data])
return true
"pubsub":
var channels = target_instance._pubsub.list_channels()
var channels_data = {}
for channel in channels:
channels_data[channel] = target_instance._pubsub.list_subscribers(channel)
var patterns = target_instance._pubsub.list_patterns()
var patterns_data = {}
for pattern in patterns:
patterns_data[pattern] = target_instance._pubsub.list_pattern_subscribers(pattern)
debugger.send_message("gedis:pubsub_data", [channels_data, patterns_data])
return true
return false
# Debugger-like helpers: type/dump/snapshot
func type(key: String) -> String:
if _gedis._expiry._is_expired(key):
return "none"
if _gedis._core._store.has(key):
return "string"
if _gedis._core._hashes.has(key):
return "hash"
if _gedis._core._lists.has(key):
return "list"
if _gedis._core._sets.has(key):
return "set"
if _gedis._core._sorted_sets.has(key):
return "zset"
return "none"
func dump(key: String) -> Dictionary:
var t = type(key)
if t == "none":
return {}
var d: Dictionary = {}
d["type"] = t
d["ttl"] = _gedis.ttl(key)
match t:
"string":
d["value"] = _gedis._core._store.get(key, null)
"hash":
d["value"] = _gedis._core._hashes.get(key, {}).duplicate(true)
"list":
d["value"] = _gedis._core._lists.get(key, []).duplicate()
"set":
d["value"] = _gedis._core._sets.get(key, {}).keys()
"zset":
var data = _gedis._core._sorted_sets.get(key, {})
var value = []
if data.has("sorted_set"):
# The internal representation is [score, member] but for visualization
# it's more intuitive to show [member, score].
for entry in data.sorted_set:
value.append([entry[1], entry[0]])
d["value"] = value
_:
d["value"] = null
return d
func snapshot(pattern: String = "*") -> Dictionary:
var out: Dictionary = {}
for k in _gedis._strings.keys(pattern):
var key_data = dump(str(k))
key_data["ttl"] = _gedis._expiry.ttl(str(k))
out[str(k)] = key_data
return out

View File

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

View File

@ -1,98 +0,0 @@
extends RefCounted
class_name GedisExpiry
var _gedis: Gedis
# Provides an ordered view of expirations for efficient purging.
# The list is sorted by timestamp and stores [timestamp, key] pairs.
var _expiry_list: Array
func _init(p_gedis: Gedis):
_gedis = p_gedis
_expiry_list = []
func _now() -> int:
return _gedis._time_source.get_time()
func _is_expired(key: String) -> bool:
if _gedis._core._expiry.has(key) and _gedis._core._expiry[key] <= _now():
var timestamp = _gedis._core._expiry[key]
_gedis._core._delete_all_types_for_key(key)
var entry = [timestamp, key]
var index = _expiry_list.bsearch(entry)
if index < _expiry_list.size() and _expiry_list[index] == entry:
_expiry_list.remove_at(index)
return true
return false
func _purge_expired() -> void:
var now := _now()
var expired_count := 0
var ex := _gedis._core._expiry
for item in _expiry_list:
var timestamp: int = item[0]
var key: String = item[1]
if timestamp <= now:
if ex.has(key) and ex[key] == timestamp:
_gedis._core._delete_all_types_for_key(key)
_gedis.publish("gedis:keyspace:" + key, "expire")
expired_count += 1
else:
break
if expired_count > 0:
_expiry_list = _expiry_list.slice(expired_count)
# ----------------
# Expiry commands
# ----------------
func expire(key: String, seconds: int) -> bool:
if not _gedis.exists(key):
return false
# If the key already has an expiry, remove the old entry from the sorted list.
if _gedis._core._expiry.has(key):
var old_timestamp = _gedis._core._expiry[key]
var old_entry = [old_timestamp, key]
var index = _expiry_list.bsearch(old_entry)
if index < _expiry_list.size() and _expiry_list[index] == old_entry:
_expiry_list.remove_at(index)
var new_timestamp = _now() + (float(seconds) * 1000.0)
_gedis._core._expiry[key] = new_timestamp
# Add the new expiry to the sorted list.
var new_entry = [new_timestamp, key]
var insertion_index = _expiry_list.bsearch(new_entry)
_expiry_list.insert(insertion_index, new_entry)
return true
# TTL returns:
# -2 if the key does not exist
# -1 if the key exists but has no associated expire
# >= 0 number of seconds to expire
func ttl(key: String) -> int:
if not _gedis.exists(key):
return -2
if not _gedis._core._expiry.has(key):
return -1
return max(0, int(ceil((_gedis._core._expiry[key] - _now()) / 1000.0)))
func persist(key: String) -> bool:
if not _gedis.exists(key):
return false
if _gedis._core._expiry.has(key):
var timestamp = _gedis._core._expiry[key]
_gedis._core._expiry.erase(key)
var entry = [timestamp, key]
var index = _expiry_list.bsearch(entry)
if index < _expiry_list.size() and _expiry_list[index] == entry:
_expiry_list.remove_at(index)
return true
return false
func setex(key: String, seconds: int, value: Variant) -> void:
_gedis.set_value(key, value)
expire(key, seconds)

View File

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

View File

@ -1,132 +0,0 @@
extends RefCounted
class_name GedisHashes
var _gedis
func _init(gedis):
_gedis = gedis
# ------
# Hashes
# ------
func hset(key: String, field: String, value) -> int:
_gedis._core._touch_type(key, _gedis._core._hashes)
var d: Dictionary = _gedis._core._hashes.get(key, {})
var existed := int(d.has(field))
d[field] = value
_gedis._core._hashes[key] = d
_gedis.publish("gedis:keyspace:" + key, "set")
return 1 - existed
func hget(key: String, field: String, default_value: Variant = null):
if _gedis._expiry._is_expired(key):
return default_value
var d: Dictionary = _gedis._core._hashes.get(key, {})
return d.get(field, default_value)
# Gets the values of multiple fields in a hash.
# ---
# @param key: The key of the hash.
# @param fields: An array of fields to get the values of.
# @return: An array of values for the given fields. If a field does not exist, the corresponding value in the array will be null.
func hmget(key: String, fields: Array) -> Array:
if _gedis._expiry._is_expired(key):
return fields.map(func(_field): return null)
var d: Dictionary = _gedis._core._hashes.get(key, {})
var result: Array = []
for field in fields:
result.append(d.get(field, null))
return result
# Sets multiple fields and their values in a hash.
# ---
# @param key: The key of the hash.
# @param field_value_pairs: A dictionary of field-value pairs to set.
func hmset(key: String, field_value_pairs: Dictionary) -> void:
_gedis._core._touch_type(key, _gedis._core._hashes)
var d: Dictionary = _gedis._core._hashes.get(key, {})
for field in field_value_pairs:
d[field] = field_value_pairs[field]
_gedis._core._hashes[key] = d
_gedis.publish("gedis:keyspace:" + key, "set")
func hincrby(key: String, field: String, amount: int) -> Variant:
_gedis._core._touch_type(key, _gedis._core._hashes)
var d: Dictionary = _gedis._core._hashes.get(key, {})
var value = d.get(field, 0)
if not typeof(value) in [TYPE_INT, TYPE_FLOAT]:
push_error("WRONGTYPE Operation against a key holding the wrong kind of value")
return null
value += amount
d[field] = value
_gedis._core._hashes[key] = d
_gedis.publish("gedis:keyspace:" + key, "set")
return value
func hincrbyfloat(key: String, field: String, amount: float) -> Variant:
_gedis._core._touch_type(key, _gedis._core._hashes)
var d: Dictionary = _gedis._core._hashes.get(key, {})
var value = d.get(field, 0.0)
if not typeof(value) in [TYPE_INT, TYPE_FLOAT]:
push_error("WRONGTYPE Operation against a key holding the wrong kind of value")
return null
value += amount
d[field] = value
_gedis._core._hashes[key] = d
_gedis.publish("gedis:keyspace:" + key, "set")
return value
func hdel(key: String, fields) -> int:
# Accept single field (String) or Array of fields
if _gedis._expiry._is_expired(key):
return 0
if not _gedis._core._hashes.has(key):
return 0
var d: Dictionary = _gedis._core._hashes[key]
var removed = 0
if typeof(fields) == TYPE_ARRAY:
for f in fields:
if d.has(str(f)):
d.erase(str(f))
removed += 1
else:
var f = str(fields)
if d.has(f):
d.erase(f)
removed = 1
if d.is_empty():
_gedis._core._hashes.erase(key)
_gedis.publish("gedis:keyspace:" + key, "del")
else:
_gedis._core._hashes[key] = d
return removed
func hgetall(key: String) -> Dictionary:
if _gedis._expiry._is_expired(key):
return {}
return _gedis._core._hashes.get(key, {}).duplicate(true)
func hexists(key: String, field = null) -> bool:
if _gedis._expiry._is_expired(key):
return false
if field == null:
return _gedis._core._hashes.has(key)
var d: Dictionary = _gedis._core._hashes.get(key, {})
return d.has(field)
func hkeys(key: String) -> Array:
if _gedis._expiry._is_expired(key):
return []
return _gedis._core._hashes.get(key, {}).keys()
func hvals(key: String) -> Array:
if _gedis._expiry._is_expired(key):
return []
return _gedis._core._hashes.get(key, {}).values()
func hlen(key: String) -> int:
if _gedis._expiry._is_expired(key):
return 0
return _gedis._core._hashes.get(key, {}).size()

View File

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

View File

@ -1,293 +0,0 @@
extends RefCounted
class_name GedisLists
var _gedis
func _init(gedis):
_gedis = gedis
# -----
# Lists
# -----
func lpush(key: String, value) -> int:
_gedis._core._touch_type(key, _gedis._core._lists)
var a: Array = _gedis._core._lists.get(key, [])
if typeof(value) == TYPE_ARRAY:
a = value + a
else:
a.insert(0, value)
_gedis._core._lists[key] = a
_gedis.publish("gedis:keyspace:" + key, "set")
return a.size()
func rpush(key: String, value) -> int:
_gedis._core._touch_type(key, _gedis._core._lists)
var a: Array = _gedis._core._lists.get(key, [])
if typeof(value) == TYPE_ARRAY:
a += value
else:
a.append(value)
_gedis._core._lists[key] = a
_gedis.publish("gedis:keyspace:" + key, "set")
return a.size()
func lpop(key: String):
if _gedis._expiry._is_expired(key):
return null
if not _gedis._core._lists.has(key):
return null
var a: Array = _gedis._core._lists[key].duplicate()
if a.is_empty():
return null
var v = a.pop_front()
if a.is_empty():
_gedis._core._lists.erase(key)
_gedis.publish("gedis:keyspace:" + key, "del")
else:
_gedis._core._lists[key] = a
_gedis.publish("gedis:keyspace:" + key, "set")
return v
func rpop(key: String):
if _gedis._expiry._is_expired(key):
return null
if not _gedis._core._lists.has(key):
return null
var a: Array = _gedis._core._lists[key].duplicate()
if a.is_empty():
return null
var v = a.pop_back()
if a.is_empty():
_gedis._core._lists.erase(key)
_gedis.publish("gedis:keyspace:" + key, "del")
else:
_gedis._core._lists[key] = a
_gedis.publish("gedis:keyspace:" + key, "set")
return v
func llen(key: String) -> int:
if _gedis._expiry._is_expired(key):
return 0
var a: Array = _gedis._core._lists.get(key, [])
return a.size()
func lexists(key: String) -> bool:
if _gedis._expiry._is_expired(key):
return false
return _gedis._core._lists.has(key)
func lget(key: String) -> Array:
if _gedis._expiry._is_expired(key):
return []
return _gedis._core._lists.get(key, []).duplicate()
func lrange(key: String, start: int, stop: int) -> Array:
if _gedis._expiry._is_expired(key):
return []
var a: Array = _gedis._core._lists.get(key, [])
var n = a.size()
# normalize negative indices
if start < 0:
start = n + start
if stop < 0:
stop = n + stop
# clamp
start = max(0, start)
stop = min(n - 1, stop)
if start > stop or n == 0:
return []
var out: Array = []
for i in range(start, stop + 1):
out.append(a[i])
return out
func lindex(key: String, index: int):
if _gedis._expiry._is_expired(key):
return null
var a: Array = _gedis._core._lists.get(key, [])
var n = a.size()
if n == 0:
return null
if index < 0:
index = n + index
if index < 0 or index >= n:
return null
return a[index]
func lset(key: String, index: int, value) -> bool:
if _gedis._expiry._is_expired(key):
return false
if not _gedis._core._lists.has(key):
return false
var a: Array = _gedis._core._lists[key].duplicate()
var n = a.size()
if index < 0:
index = n + index
if index < 0 or index >= n:
return false
a[index] = value
_gedis._core._lists[key] = a
_gedis.publish("gedis:keyspace:" + key, "set")
return true
func lrem(key: String, count: int, value) -> int:
# Remove elements equal to value. Behavior similar to Redis.
if _gedis._expiry._is_expired(key):
return 0
if not _gedis._core._lists.has(key):
return 0
var a: Array = _gedis._core._lists[key].duplicate()
var removed = 0
if count == 0:
# remove all
var filtered: Array = []
for v in a:
if v == value:
removed += 1
else:
filtered.append(v)
a = filtered
elif count > 0:
var out: Array = []
for v in a:
if v == value and removed < count:
removed += 1
continue
out.append(v)
a = out
else:
# count < 0, remove from tail
var rev = a.duplicate()
rev.reverse()
var out2: Array = []
for v in rev:
if v == value and removed < abs(count):
removed += 1
continue
out2.append(v)
out2.reverse()
a = out2
if a.is_empty():
_gedis._core._lists.erase(key)
_gedis.publish("gedis:keyspace:" + key, "del")
else:
_gedis._core._lists[key] = a
_gedis.publish("gedis:keyspace:" + key, "set")
return removed
func lmove(source: String, destination: String, from: String, to: String):
if _gedis._expiry._is_expired(source):
return null
if not _gedis._core._lists.has(source):
return null
var source_list: Array = _gedis._core._lists[source]
if source_list.is_empty():
return null
var element
if from.to_upper() == "LEFT":
element = source_list.pop_front()
elif from.to_upper() == "RIGHT":
element = source_list.pop_back()
else:
return null
_gedis._core._touch_type(destination, _gedis._core._lists)
var dest_list: Array = _gedis._core._lists.get(destination, [])
if to.to_upper() == "LEFT":
dest_list.insert(0, element)
elif to.to_upper() == "RIGHT":
dest_list.append(element)
else:
# Invalid 'to', restore source list and return error
if from.to_upper() == "LEFT":
source_list.insert(0, element)
else:
source_list.append(element)
return null
if source_list.is_empty():
_gedis._core._lists.erase(source)
_gedis.publish("gedis:keyspace:" + source, "del")
else:
_gedis._core._lists[source] = source_list
_gedis.publish("gedis:keyspace:" + source, "set")
_gedis._core._lists[destination] = dest_list
_gedis.publish("gedis:keyspace:" + destination, "set")
return element
# Trims a list to the specified range of indices.
# Removes all elements from the list that are not in the range [start, stop].
# If start is greater than stop, or start is out of bounds, the list will be emptied.
# ---
# @param key: The key of the list to trim.
# @param start: The starting index of the range.
# @param stop: The ending index of the range.
# @return: Returns true if the list was trimmed, false otherwise.
func ltrim(key: String, start: int, stop: int) -> bool:
if _gedis._expiry._is_expired(key):
return false
if not _gedis._core._lists.has(key):
return false
var a: Array = _gedis._core._lists[key].duplicate()
var n = a.size()
if start < 0:
start = n + start
if stop < 0:
stop = n + stop
start = max(0, start)
stop = min(n - 1, stop)
if start > stop:
_gedis._core._lists.erase(key)
_gedis.publish("gedis:keyspace:" + key, "del")
return true
var trimmed_list: Array = []
for i in range(start, stop + 1):
trimmed_list.append(a[i])
_gedis._core._lists[key] = trimmed_list
_gedis.publish("gedis:keyspace:" + key, "set")
return true
# Inserts a value into a list before or after a pivot value.
# ---
# @param key: The key of the list.
# @param position: "BEFORE" or "AFTER" the pivot.
# @param pivot: The value to insert the new value next to.
# @param value: The value to insert.
# @return: The new size of the list, or -1 if the pivot was not found. Returns 0 if the key does not exist or the position is invalid.
func linsert(key: String, position: String, pivot: Variant, value: Variant) -> int:
if _gedis._expiry._is_expired(key):
return 0
if not _gedis._core._lists.has(key):
return 0
var a: Array = _gedis._core._lists[key].duplicate()
var n = a.size()
var index = -1
for i in range(n):
if a[i] == pivot:
index = i
break
if index == -1:
return -1
if position.to_upper() == "BEFORE":
a.insert(index, value)
elif position.to_upper() == "AFTER":
a.insert(index + 1, value)
else:
return 0
_gedis._core._lists[key] = a
_gedis.publish("gedis:keyspace:" + key, "set")
return a.size()

View File

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

View File

@ -1,106 +0,0 @@
extends RefCounted
class_name GedisPubSub
var _gedis: Gedis
signal pubsub_message(channel, message)
signal psub_message(pattern, channel, message)
signal subscribed(channel, subscriber)
signal unsubscribed(channel, subscriber)
func _init(gedis: Gedis):
_gedis = gedis
# --------
# Pub/Sub
# --------
func publish(channel: String, message) -> void:
# Backwards-compatible delivery:
# 1) If subscriber objects registered via subscribe/psubscribe expect direct signals,
# call their 'pubsub_message'/'psub_message' on the subscriber object.
# 2) Emit a single Gedis-level signal so external code can connect to this Gedis instance.
# This avoids emitting the same Gedis signal multiple times (which would cause duplicate callbacks).
# Direct subscribers (back-compat)
if _gedis._core._subscribers.has(channel):
for subscriber in _gedis._core._subscribers[channel]:
if is_instance_valid(subscriber):
# deliver directly to subscriber object if it exposes the signal
if subscriber.has_signal("pubsub_message"):
subscriber.pubsub_message.emit(channel, message)
# Emit a single Gedis-level pubsub notification for all listeners connected to this Gedis instance.
if _gedis._core._subscribers.has(channel) and _gedis._core._subscribers[channel].size() > 0:
pubsub_message.emit(channel, message)
# Pattern subscribers (back-compat + Gedis-level)
for pattern in _gedis._core._psubscribers.keys():
# Use simple glob matching: convert to RegEx
var rx = _gedis._utils._glob_to_regex(pattern)
if rx.search(channel) != null:
for subscriber in _gedis._core._psubscribers[pattern]:
if is_instance_valid(subscriber):
if subscriber.has_signal("psub_message"):
subscriber.psub_message.emit(pattern, channel, message)
# Emit one Gedis-level pattern message for this matching pattern
psub_message.emit(pattern, channel, message)
func subscribe(channel: String, subscriber: Object) -> void:
var arr: Array = _gedis._core._subscribers.get(channel, [])
# avoid duplicates
for s in arr:
if s == subscriber:
return
arr.append(subscriber)
_gedis._core._subscribers[channel] = arr
subscribed.emit(channel, subscriber)
func unsubscribe(channel: String, subscriber: Object) -> void:
if not _gedis._core._subscribers.has(channel):
return
var arr: Array = _gedis._core._subscribers[channel]
for i in range(arr.size()):
if arr[i] == subscriber:
arr.remove_at(i)
unsubscribed.emit(channel, subscriber)
break
if arr.is_empty():
_gedis._core._subscribers.erase(channel)
else:
_gedis._core._subscribers[channel] = arr
func psubscribe(pattern: String, subscriber: Object) -> void:
var arr: Array = _gedis._core._psubscribers.get(pattern, [])
for s in arr:
if s == subscriber:
return
arr.append(subscriber)
_gedis._core._psubscribers[pattern] = arr
subscribed.emit(pattern, subscriber)
func punsubscribe(pattern: String, subscriber: Object) -> void:
if not _gedis._core._psubscribers.has(pattern):
return
var arr: Array = _gedis._core._psubscribers[pattern]
for i in range(arr.size()):
if arr[i] == subscriber:
arr.remove_at(i)
unsubscribed.emit(pattern, subscriber)
break
if arr.is_empty():
_gedis._core._psubscribers.erase(pattern)
else:
_gedis._core._psubscribers[pattern] = arr
# -----------
# Introspection
# -----------
func list_channels() -> Array:
return _gedis._core._subscribers.keys()
func list_subscribers(channel: String) -> Array:
return _gedis._core._subscribers.get(channel, [])
func list_patterns() -> Array:
return _gedis._core._psubscribers.keys()
func list_pattern_subscribers(pattern: String) -> Array:
return _gedis._core._psubscribers.get(pattern, [])

View File

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

View File

@ -1,216 +0,0 @@
extends RefCounted
class_name GedisSets
var _gedis: Gedis
func _init(gedis: Gedis):
_gedis = gedis
# ----
# Sets
# ----
func sadd(key: String, member) -> int:
_gedis._core._touch_type(key, _gedis._core._sets)
var s: Dictionary = _gedis._core._sets.get(key, {})
var existed := int(s.has(member))
s[member] = true
_gedis._core._sets[key] = s
_gedis.publish("gedis:keyspace:" + key, "set")
return 1 - existed
func srem(key: String, member) -> int:
if _gedis._expiry._is_expired(key):
return 0
if not _gedis._core._sets.has(key):
return 0
var s: Dictionary = _gedis._core._sets[key]
var existed := int(s.has(member))
s.erase(member)
if s.is_empty():
_gedis._core._sets.erase(key)
_gedis.publish("gedis:keyspace:" + key, "del")
else:
_gedis._core._sets[key] = s
return existed
func smembers(key: String) -> Array:
if _gedis._expiry._is_expired(key):
return []
var s: Dictionary = _gedis._core._sets.get(key, {})
return s.keys()
func sismember(key: String, member) -> bool:
if _gedis._expiry._is_expired(key):
return false
var s: Dictionary = _gedis._core._sets.get(key, {})
return s.has(member)
func scard(key: String) -> int:
if _gedis._expiry._is_expired(key):
return 0
return _gedis._core._sets.get(key, {}).size()
func sexists(key: String) -> bool:
if _gedis._expiry._is_expired(key):
return false
return _gedis._core._sets.has(key)
func spop(key: String):
if _gedis._expiry._is_expired(key):
return null
if not _gedis._core._sets.has(key):
return null
var s: Dictionary = _gedis._core._sets[key]
var keys_arr: Array = s.keys()
if keys_arr.is_empty():
return null
var idx = randi() % keys_arr.size()
var member = keys_arr[idx]
s.erase(member)
if s.is_empty():
_gedis._core._sets.erase(key)
_gedis.publish("gedis:keyspace:" + key, "del")
else:
_gedis._core._sets[key] = s
return member
func smove(source: String, destination: String, member) -> bool:
if _gedis._expiry._is_expired(source):
return false
if not sismember(source, member):
return false
# remove from source
srem(source, member)
# add to destination (creates destination set)
sadd(destination, member)
return true
# Computes the union of multiple sets.
# ---
# @param keys: An array of set keys.
# @return: An array containing the members of the resulting union set.
func sunion(keys: Array) -> Array:
var result_set := {}
for key in keys:
var members = smembers(key)
for member in members:
result_set[member] = true
return result_set.keys()
# Computes the intersection of multiple sets.
# ---
# @param keys: An array of set keys.
# @return: An array containing the members of the resulting intersection set.
func sinter(keys: Array) -> Array:
if keys.is_empty():
return []
var result_set := {}
var first_set_members = smembers(keys[0])
for member in first_set_members:
result_set[member] = true
for i in range(1, keys.size()):
var next_set_members = smembers(keys[i])
var current_members = result_set.keys()
for member in current_members:
if not next_set_members.has(member):
result_set.erase(member)
return result_set.keys()
# Computes the difference between multiple sets.
# The difference is calculated as the members of the first set minus the members of all subsequent sets.
# ---
# @param keys: An array of set keys. The first key is the set to subtract from.
# @return: An array containing the members of the resulting difference set.
func sdiff(keys: Array) -> Array:
if keys.is_empty():
return []
var result_set := {}
var first_set_members = smembers(keys[0])
for member in first_set_members:
result_set[member] = true
for i in range(1, keys.size()):
var next_set_members = smembers(keys[i])
for member in next_set_members:
if result_set.has(member):
result_set.erase(member)
return result_set.keys()
# Computes the union of multiple sets and stores the result in a new set.
# ---
# @param destination: The key to store the resulting union set in.
# @param keys: An array of set keys.
# @return: The number of members in the resulting union set.
func sunionstore(destination: String, keys: Array) -> int:
var result_members = sunion(keys)
_gedis._core._sets.erase(destination)
for member in result_members:
sadd(destination, member)
return result_members.size()
# Computes the intersection of multiple sets and stores the result in a new set.
# ---
# @param destination: The key to store the resulting intersection set in.
# @param keys: An array of set keys.
# @return: The number of members in the resulting intersection set.
func sinterstore(destination: String, keys: Array) -> int:
var result_members = sinter(keys)
_gedis._core._sets.erase(destination)
for member in result_members:
sadd(destination, member)
return result_members.size()
# Computes the difference between multiple sets and stores the result in a new set.
# ---
# @param destination: The key to store the resulting difference set in.
# @param keys: An array of set keys. The first key is the set to subtract from.
# @return: The number of members in the resulting difference set.
func sdiffstore(destination: String, keys: Array) -> int:
var result_members = sdiff(keys)
_gedis._core._sets.erase(destination)
for member in result_members:
sadd(destination, member)
return result_members.size()
# Gets one or more random members from a set.
# ---
# @param key: The key of the set.
# @param count: The number of random members to return. If positive, returns unique members. If negative, allows for repetitions.
# @return: A single random member if count is 1, or an array of random members. Returns null or an empty array if the set is empty.
func srandmember(key: String, count: int = 1):
if _gedis._expiry._is_expired(key):
return null if count == 1 else []
var s: Dictionary = _gedis._core._sets.get(key, {})
var members = s.keys()
if members.is_empty():
return null if count == 1 else []
if count == 1:
return members.pick_random()
var result = []
if count > 0:
# Positive count: return unique elements
if count >= members.size():
members.shuffle()
return members
var used_indices = {}
while result.size() < count:
var idx = randi() % members.size()
if not used_indices.has(idx):
result.append(members[idx])
used_indices[idx] = true
else: # count < 0
# Negative count: repetitions are allowed
var abs_count = abs(count)
for _i in range(abs_count):
result.append(members.pick_random())
return result

View File

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

View File

@ -1,415 +0,0 @@
# This class provides a sorted set data structure, where each member is associated with a score.
# Members are stored in an array, sorted by score, allowing for efficient range queries. A dictionary
# is used for quick lookups of member scores.
#
# Features:
# - Add/update members with scores.
# - Remove members.
# - Retrieve members within a score range.
# - Pop members that are "ready" based on a timestamp score.
#
# The implementation aims to be simple and efficient for common use cases in game development,
# such as managing timed events, priority queues, or leaderboards.
extends RefCounted
class_name GedisSortedSets
var _gedis: Gedis
func _init(gedis: Gedis):
_gedis = gedis
# Adds a new member to the sorted set with the specified score.
# If the member already exists, its score is updated, and its position in the set is adjusted.
#
# @param member: The member to add or update.
# @param score: The score associated with the member.
func add(key: String, member: String, score: int) -> int:
_gedis._core._touch_type(key, _gedis._core._sorted_sets)
var data: Dictionary = _gedis._core._sorted_sets.get(key, {
"sorted_set": [],
"member_scores": {}
})
var new_member = not data.member_scores.has(member)
if not new_member:
remove(key, member)
data = _gedis._core._sorted_sets.get(key, {
"sorted_set": [],
"member_scores": {}
})
data.member_scores[member] = score
var entry = [score, member]
var inserted: bool = false
for i in range(data.sorted_set.size()):
if score < data.sorted_set[i][0]:
data.sorted_set.insert(i, entry)
inserted = true
break
if not inserted:
data.sorted_set.append(entry)
_gedis._core._sorted_sets[key] = data
if new_member:
_gedis.publish("gedis:keyspace:" + key, "set")
return 1 if new_member else 0
# Removes a member from the sorted set.
#
# @param member: The member to remove.
func remove(key: String, member: String) -> int:
if _gedis._expiry._is_expired(key):
return 0
if not _gedis._core._sorted_sets.has(key):
return 0
var data: Dictionary = _gedis._core._sorted_sets[key]
if not data.member_scores.has(member):
return 0
var score = data.member_scores[member]
data.member_scores.erase(member)
for i in range(data.sorted_set.size()):
if data.sorted_set[i][0] == score and data.sorted_set[i][1] == member:
data.sorted_set.remove_at(i)
if data.sorted_set.is_empty():
_gedis._core._sorted_sets.erase(key)
_gedis.publish("gedis:keyspace:" + key, "del")
else:
_gedis._core._sorted_sets[key] = data
return 1
return 0
func zexists(key: String) -> bool:
if _gedis._expiry._is_expired(key):
return false
return _gedis._core._sorted_sets.has(key)
func zcard(key: String) -> int:
if _gedis._expiry._is_expired(key):
return 0
if not _gedis._core._sorted_sets.has(key):
return 0
return _gedis._core._sorted_sets[key].sorted_set.size()
# Returns a range of members from the sorted set, ordered by score.
#
# @param start: The starting index of the range.
# @param stop: The ending index of the range.
# @param withscores: Whether to return scores along with members.
# @return: An array of members, or an array of [member, score] pairs if withscores is true.
func zrange(key: String, start, stop, withscores: bool = false) -> Array:
return _zrange(key, start, stop, withscores, false)
# Returns a range of members from the sorted set, ordered by score in reverse.
#
# @param start: The starting index of the range.
# @param stop: The ending index of the range.
# @param withscores: Whether to return scores along with members.
# @return: An array of members, or an array of [member, score] pairs if withscores is true.
func zrevrange(key: String, start, stop, withscores: bool = false) -> Array:
return _zrange(key, start, stop, withscores, true)
func _zrange(key: String, start, stop, withscores: bool, reverse: bool) -> Array:
if _gedis._expiry._is_expired(key):
return []
if not _gedis._core._sorted_sets.has(key):
return []
var data: Dictionary = _gedis._core._sorted_sets[key]
var sorted_set: Array = data.sorted_set
if reverse:
sorted_set = sorted_set.duplicate()
sorted_set.reverse()
var set_len: int = sorted_set.size()
var start_index = start
if start_index < 0:
start_index = set_len + start_index
var stop_index = stop
if stop_index < 0:
stop_index = set_len + stop_index
if start_index < 0:
start_index = 0
if stop_index >= set_len:
stop_index = set_len - 1
if start_index > stop_index or start_index >= set_len:
return []
var result: Array = []
for i in range(start_index, stop_index + 1):
var entry = sorted_set[i]
var score = entry[0]
var member = entry[1]
if withscores:
result.append([member, score])
else:
result.append(member)
return result
# Returns a range of members from the sorted set, ordered by score.
#
# @param min_score: The minimum score of the range.
# @param max_score: The maximum score of the range.
# @param withscores: Whether to return scores along with members.
# @return: An array of members, or an array of [member, score] pairs if withscores is true.
# Returns a range of members from the sorted set, ordered by score.
func zrangebyscore(key: String, min_score, max_score, withscores: bool = false) -> Array:
if _gedis._expiry._is_expired(key):
return []
if not _gedis._core._sorted_sets.has(key):
return []
var data: Dictionary = _gedis._core._sorted_sets[key]
var result: Array = []
for entry in data.sorted_set:
var score = entry[0]
var member = entry[1]
if score >= min_score and score <= max_score:
if withscores:
result.append([member, score])
else:
result.append(member)
return result
# Returns a range of members from the sorted set, ordered by score in reverse.
#
# @param min_score: The minimum score of the range.
# @param max_score: The maximum score of the range.
# @param withscores: Whether to return scores along with members.
# @return: An array of members, or an array of [member, score] pairs if withscores is true.
# Returns a range of members from the sorted set, ordered by score in reverse.
func zrevrangebyscore(key: String, min_score, max_score, withscores: bool = false) -> Array:
var result = zrangebyscore(key, min_score, max_score, withscores)
result.reverse()
return result
# Removes and returns members with scores up to the given 'now' value.
# This is useful for processing items that are due, like in a task scheduler.
#
# @param now: The current time or score to check against.
# @return: An array of members that were popped.
func pop_ready(key: String, now: int) -> Array:
if _gedis._expiry._is_expired(key):
return []
if not _gedis._core._sorted_sets.has(key):
return []
var data: Dictionary = _gedis._core._sorted_sets[key]
var ready_members: Array = []
var i: int = 0
while i < data.sorted_set.size():
var entry = data.sorted_set[i]
if entry[0] <= now:
ready_members.append(entry[1])
data.member_scores.erase(entry[1])
data.sorted_set.remove_at(i)
else:
break
if data.sorted_set.is_empty():
_gedis._core._sorted_sets.erase(key)
_gedis.publish("gedis:keyspace:" + key, "del")
else:
_gedis._core._sorted_sets[key] = data
return ready_members
# Returns the score of a member in the sorted set.
# ---
# @param key: The key of the sorted set.
# @param member: The member to get the score of.
# @return: The score of the member, or null if the member does not exist.
func zscore(key: String, member: String) -> Variant:
if _gedis._expiry._is_expired(key):
return null
if not _gedis._core._sorted_sets.has(key):
return null
var data: Dictionary = _gedis._core._sorted_sets[key]
if not data.member_scores.has(member):
return null
return data.member_scores[member]
# Returns the rank of a member in the sorted set, with scores ordered from low to high.
# The rank is 0-based.
# ---
# @param key: The key of the sorted set.
# @param member: The member to get the rank of.
# @return: The rank of the member, or null if the member does not exist.
func zrank(key: String, member: String) -> Variant:
if _gedis._expiry._is_expired(key):
return null
if not _gedis._core._sorted_sets.has(key):
return null
var data: Dictionary = _gedis._core._sorted_sets[key]
if not data.member_scores.has(member):
return null
for i in range(data.sorted_set.size()):
if data.sorted_set[i][1] == member:
return i
return null
# Returns the rank of a member in the sorted set, with scores ordered from high to low.
# The rank is 0-based.
# ---
# @param key: The key of the sorted set.
# @param member: The member to get the rank of.
# @return: The rank of the member, or null if the member does not exist.
func zrevrank(key: String, member: String) -> Variant:
if _gedis._expiry._is_expired(key):
return null
if not _gedis._core._sorted_sets.has(key):
return null
var data: Dictionary = _gedis._core._sorted_sets[key]
if not data.member_scores.has(member):
return null
for i in range(data.sorted_set.size() - 1, -1, -1):
if data.sorted_set[i][1] == member:
return data.sorted_set.size() - 1 - i
return null
# Returns the number of members in a sorted set with scores within the given range.
# ---
# @param key: The key of the sorted set.
# @param min_score: The minimum score of the range.
# @param max_score: The maximum score of the range.
# @return: The number of members in the specified score range.
func zcount(key: String, min_score, max_score) -> int:
if _gedis._expiry._is_expired(key):
return 0
if not _gedis._core._sorted_sets.has(key):
return 0
var data: Dictionary = _gedis._core._sorted_sets[key]
var count: int = 0
for entry in data.sorted_set:
var score = entry[0]
if score >= min_score and score <= max_score:
count += 1
return count
# Increments the score of a member in a sorted set.
# If the member does not exist, it is added with the increment as its score.
# ---
# @param key: The key of the sorted set.
# @param increment: The amount to increment the score by.
# @param member: The member whose score to increment.
# @return: The new score of the member.
func zincrby(key: String, increment, member: String) -> Variant:
_gedis._core._touch_type(key, _gedis._core._sorted_sets)
var current_score = zscore(key, member)
if current_score == null:
current_score = 0
var new_score = current_score + increment
add(key, member, new_score)
return new_score
# Computes the union of multiple sorted sets and stores the result in a new sorted set.
# ---
# @param destination: The key to store the resulting sorted set in.
# @param keys: An array of sorted set keys.
# @param aggregate: The aggregation strategy for scores of the same member ("SUM", "MIN", "MAX").
# @return: The number of members in the resulting sorted set.
func zunionstore(destination: String, keys: Array, aggregate: String = "SUM") -> int:
var temp_scores: Dictionary = {}
for key in keys:
if not _gedis._core._sorted_sets.has(key):
continue
var data: Dictionary = _gedis._core._sorted_sets[key]
for member in data.member_scores:
var score = data.member_scores[member]
if not temp_scores.has(member):
temp_scores[member] = score
else:
match aggregate.to_upper():
"SUM":
temp_scores[member] += score
"MIN":
temp_scores[member] = min(temp_scores[member], score)
"MAX":
temp_scores[member] = max(temp_scores[member], score)
if _gedis._core._sorted_sets.has(destination):
_gedis.del([destination])
for member in temp_scores:
add(destination, member, temp_scores[member])
if not _gedis._core._sorted_sets.has(destination):
return 0
return _gedis._core._sorted_sets[destination].sorted_set.size()
# Computes the intersection of multiple sorted sets and stores the result in a new sorted set.
# ---
# @param destination: The key to store the resulting sorted set in.
# @param keys: An array of sorted set keys.
# @param aggregate: The aggregation strategy for scores of the same member ("SUM", "MIN", "MAX").
# @return: The number of members in the resulting sorted set.
func zinterstore(destination: String, keys: Array, aggregate: String = "SUM") -> int:
if keys.is_empty():
return 0
var member_sets: Array = []
for key in keys:
if not _gedis._core._sorted_sets.has(key):
return 0
var data: Dictionary = _gedis._core._sorted_sets[key]
member_sets.append(data.member_scores.keys())
var intersection = member_sets[0]
for i in range(1, member_sets.size()):
var next_set = member_sets[i]
var current_intersection: Array = []
for member in intersection:
if member in next_set:
current_intersection.append(member)
intersection = current_intersection
var temp_scores: Dictionary = {}
for member in intersection:
var score_sum = 0
var score_min = INF
var score_max = - INF
for key in keys:
var data: Dictionary = _gedis._core._sorted_sets[key]
var score = data.member_scores[member]
score_sum += score
score_min = min(score_min, score)
score_max = max(score_max, score)
match aggregate.to_upper():
"SUM":
temp_scores[member] = score_sum
"MIN":
temp_scores[member] = score_min
"MAX":
temp_scores[member] = score_max
if _gedis._core._sorted_sets.has(destination):
_gedis.del([destination])
for member in temp_scores:
add(destination, member, temp_scores[member])
if not _gedis._core._sorted_sets.has(destination):
return 0
return _gedis._core._sorted_sets[destination].sorted_set.size()

View File

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

View File

@ -1,139 +0,0 @@
extends RefCounted
class_name GedisStrings
var _gedis: Gedis
func _init(gedis: Gedis):
_gedis = gedis
# -----------------
# String/number API
# -----------------
func set_value(key: StringName, value: Variant) -> void:
_gedis._core._touch_type(str(key), _gedis._core._store)
_gedis._core._store[str(key)] = value
_gedis.publish("gedis:keyspace:" + str(key), "set")
func get_value(key: StringName, default_value: Variant = null) -> Variant:
if _gedis._expiry._is_expired(str(key)):
return default_value
return _gedis._core._store.get(str(key), default_value)
# del: accept String or Array of keys
func del(keys) -> int:
if typeof(keys) == TYPE_ARRAY:
var count = 0
for k in keys:
if _gedis._expiry._is_expired(str(k)):
continue
if exists(str(k)):
_gedis.publish("gedis:keyspace:" + str(k), "del")
_gedis._core._delete_all_types_for_key(str(k))
count += 1
return count
else:
var k = str(keys)
var existed := int(exists(k))
if existed > 0:
_gedis.publish("gedis:keyspace:" + k, "del")
_gedis._core._delete_all_types_for_key(k)
return existed
# exists: if Array -> return number of existing keys, else boolean for single key
func exists(keys) -> Variant:
if typeof(keys) == TYPE_ARRAY:
var cnt = 0
for k in keys:
if not _gedis._expiry._is_expired(str(k)) and _gedis._core.key_exists(str(k)):
cnt += 1
return cnt
else:
var k = str(keys)
if _gedis._expiry._is_expired(k):
return false
return _gedis._core.key_exists(k)
# key_exists: explicit single-key boolean (keeps parity with C++ API)
func key_exists(key: String) -> bool:
return bool(exists(key))
func incrby(key: String, amount: int = 1) -> int:
var k := str(key)
var current: int = 0
if _gedis._expiry._is_expired(k):
current = 0
else:
var raw = get_value(k, 0)
match typeof(raw):
TYPE_NIL:
current = 0
TYPE_INT:
current = int(raw)
TYPE_FLOAT:
current = int(raw)
TYPE_STRING:
var s := str(raw).strip_edges()
if s.find(".") != -1:
current = int(float(s))
else:
# int(s) will raise on invalid strings; rely on Godot to convert or raise as needed.
current = int(s)
_:
current = int(raw)
var v: int = current + int(amount)
# Store as an integer to keep types consistent
_gedis._core._touch_type(k, _gedis._core._store)
_gedis._core._store[k] = v
_gedis.publish("gedis:keyspace:" + k, "set")
return v
func decrby(key: String, amount: int = 1) -> int:
return incrby(key, -int(amount))
func keys(pattern: String = "*") -> Array:
var all: Dictionary = _gedis._core._get_all_keys()
var rx := _gedis._utils._glob_to_regex(pattern)
var out: Array = []
for k in all.keys():
if not _gedis._expiry._is_expired(str(k)) and rx.search(str(k)) != null:
out.append(str(k))
return out
func mset(dict: Dictionary) -> void:
for k in dict.keys():
set_value(str(k), dict[k])
func mget(keys: Array) -> Array:
var out: Array = []
for k in keys:
out.append(get_value(str(k), null))
return out
func append(key: String, value: String) -> int:
var k := str(key)
var current_value := get_value(k, "")
if typeof(current_value) != TYPE_STRING:
current_value = str(current_value)
var new_value: String = current_value + value
set_value(k, new_value)
return new_value.length()
func getset(key: String, value: Variant) -> Variant:
var k := str(key)
var old_value = get_value(k)
set_value(k, value)
return old_value
func strlen(key: String) -> int:
var k := str(key)
var value = get_value(k)
if typeof(value) == TYPE_STRING:
return value.length()
return 0
func setnx(key: String, value: Variant) -> int:
var k := str(key)
if not _gedis._expiry._is_expired(k) and key_exists(k):
return 0
set_value(k, value)
return 1

View File

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

View File

@ -1,43 +0,0 @@
class_name GedisUtils
var _regex_cache := {}
# ----------------
# Utility functions
# ----------------
func _glob_to_regex(glob: String) -> RegEx:
if _regex_cache.has(glob):
return _regex_cache[glob]
var escaped := ""
for ch in glob:
match ch:
".":
escaped += "\\."
"*":
escaped += ".*"
"?":
escaped += "."
"+":
escaped += "\\+"
"(":
escaped += "\\("
")":
escaped += "\\)"
"[":
escaped += "\\["
"]":
escaped += "\\]"
"^":
escaped += "\\^"
"$":
escaped += "\\$"
"|":
escaped += "\\|"
"\\":
escaped += "\\\\"
_:
escaped += ch
var r := RegEx.new()
r.compile("^%s$" % escaped)
_regex_cache[glob] = r
return r

View File

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

View File

@ -1,107 +0,0 @@
class_name GedisJSONSnapshotBackend extends GedisPersistenceBackend
func save(data: Dictionary, options: Dictionary) -> int:
var path: String = options.get("path", "")
if path.is_empty():
push_error("JSON snapshot backend requires a 'path' in options.")
return FAILED
var file = FileAccess.open(path, FileAccess.WRITE)
if not file:
push_error("Failed to open file for writing: %s" % path)
return FAILED
# Serialize variants to strings
var serializable_data = {}
for bucket_name in data:
var bucket = data[bucket_name]
var serializable_bucket = {}
for key in bucket:
var value = bucket[key]
if typeof(value) in [TYPE_OBJECT, TYPE_RID, TYPE_CALLABLE, TYPE_SIGNAL]:
serializable_bucket[key] = var_to_str(value)
else:
serializable_bucket[key] = value
serializable_data[bucket_name] = serializable_bucket
var json_string = JSON.stringify(serializable_data, "\t")
file.store_string(json_string)
file.close()
return OK
func load(options: Dictionary) -> Dictionary:
var path: String = options.get("path", "")
if path.is_empty():
push_error("JSON snapshot backend requires a 'path' in options.")
return {}
if not FileAccess.file_exists(path):
return {} # Return empty dict if file doesn't exist, not an error
var file = FileAccess.open(path, FileAccess.READ)
if not file:
push_error("Failed to open file for reading: %s" % path)
return {}
var json_string: String = file.get_as_text()
file.close()
var json = JSON.new()
var error = json.parse(json_string)
if error != OK:
push_error("Failed to parse JSON from file: %s" % path)
return {}
var loaded_data: Dictionary = json.get_data()
# Deserialize variants from strings
for bucket_name in loaded_data:
var bucket = loaded_data[bucket_name]
if typeof(bucket) == TYPE_DICTIONARY:
for key in bucket:
var value = bucket[key]
if typeof(value) == TYPE_STRING and value.begins_with("@"):
bucket[key] = str_to_var(value)
elif typeof(bucket) == TYPE_STRING and bucket.begins_with("@"):
loaded_data[bucket_name] = str_to_var(bucket)
return loaded_data
func serialize(data: Dictionary) -> String:
var serializable_data = {}
for bucket_name in data:
var bucket = data[bucket_name]
var serializable_bucket = {}
for key in bucket:
var value = bucket[key]
if typeof(value) in [TYPE_OBJECT, TYPE_RID, TYPE_CALLABLE, TYPE_SIGNAL]:
serializable_bucket[key] = var_to_str(value)
else:
serializable_bucket[key] = value
serializable_data[bucket_name] = serializable_bucket
return JSON.stringify(serializable_data, "\t")
func deserialize(data: String) -> Dictionary:
var json = JSON.new()
var error = json.parse(data)
if error != OK:
push_error("Failed to parse JSON from string")
return {}
var loaded_data: Dictionary = json.get_data()
# Deserialize variants from strings
for bucket_name in loaded_data:
var bucket = loaded_data[bucket_name]
if typeof(bucket) == TYPE_DICTIONARY:
for key in bucket:
var value = bucket[key]
if typeof(value) == TYPE_STRING and value.begins_with("@"):
bucket[key] = str_to_var(value)
elif typeof(bucket) == TYPE_STRING and bucket.begins_with("@"):
loaded_data[bucket_name] = str_to_var(bucket)
return loaded_data

View File

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

View File

@ -1,29 +0,0 @@
class_name GedisPersistenceBackend
# Base class for persistence backends.
# All backends should inherit from this class and implement the `save` and `load` methods.
func save(data: Dictionary, options: Dictionary) -> int:
# This method should be implemented by the child class.
# It should save the data to the persistence layer and return OK or FAILED.
push_error("save() not implemented in the persistence backend.")
return FAILED
func load(options: Dictionary) -> Dictionary:
# This method should be implemented by the child class.
# It should load the data from the persistence layer and return it as a Dictionary.
push_error("load() not implemented in the persistence backend.")
return {}
func serialize(data: Dictionary) -> String:
# This method should be implemented by the child class.
# It should serialize the data to a string.
push_error("serialize() not implemented in the persistence backend.")
return ""
func deserialize(data: String) -> Dictionary:
# This method should be implemented by the child class.
# It should deserialize the data from a string.
push_error("deserialize() not implemented in the persistence backend.")
return {}

View File

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

View File

@ -1,15 +0,0 @@
class_name GedisProcessDeltaTimeSource
extends GedisTimeSource
var current_time: int = 0
func _init() -> void:
current_time = Time.get_unix_time_from_system() * 1000
func tick(delta: float) -> void:
current_time += int(delta * 1000)
instance.purge_expired()
# Returns the current time as a Unix timestamp with milliseconds.
func get_time() -> int:
return current_time

View File

@ -1,6 +0,0 @@
class_name GedisTickTimeSource
extends GedisTimeSource
# Returns the current time as a Unix timestamp with milliseconds.
func get_time() -> int:
return Time.get_ticks_msec()

View File

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

View File

@ -1,12 +0,0 @@
class_name GedisTimeSource
extends RefCounted
var instance: Gedis
# Returns the current time as a Unix timestamp in milliseconds.
func get_time() -> int:
return 0
# Increments the time [Optional]
func tick(_delta) -> void:
instance.purge_expired()

View File

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

View File

@ -1,6 +0,0 @@
class_name GedisUnixTimeSource
extends GedisTimeSource
# Returns the current time as a Unix timestamp with milliseconds.
func get_time() -> int:
return Time.get_unix_time_from_system() * 1000

View File

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

View File

@ -1,22 +0,0 @@
@tool
extends VBoxContainer
@export var pub_sub_tree: Tree
@export var fetch_pubsub_button: Button
@export var instance_selector: OptionButton
var plugin
func _ready() -> void:
_populate_pubsub_tree()
fetch_pubsub_button.pressed.connect(_on_fetch_pubsub_pressed)
func _on_fetch_pubsub_pressed():
if plugin:
var selected_id = instance_selector.get_item_id(instance_selector.selected)
plugin.send_message_to_game("request_instance_data", [selected_id, "pubsub"])
func _populate_pubsub_tree():
if not pub_sub_tree:
return
pub_sub_tree.clear()

View File

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

View File

@ -1,102 +0,0 @@
[gd_scene load_steps=3 format=3 uid="uid://c6w7w7w7w7w7"]
[ext_resource type="Script" uid="uid://dbrrt18c60tyc" path="res://addons/Gedis/debugger/gedis_debugger_panel.gd" id="1_z8w8w"]
[sub_resource type="Theme" id="Theme_1"]
Label/colors/font_color = Color(1, 0.647059, 0, 1)
[node name="Dashboard" type="VBoxContainer" node_paths=PackedStringArray("pub_sub_tree", "fetch_pubsub_button", "instance_selector")]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = -1152.0
offset_bottom = -648.0
script = ExtResource("1_z8w8w")
pub_sub_tree = NodePath("TabContainer/PubSub/HSplitContainer/PubSubTree")
fetch_pubsub_button = NodePath("TopPanel/FetchPubSubButton")
instance_selector = NodePath("TopPanel/instance_selector")
[node name="status_label" type="Label" parent="."]
layout_mode = 2
theme = SubResource("Theme_1")
text = "Waiting for game connection..."
[node name="TopPanel" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="instance_selector" type="OptionButton" parent="TopPanel"]
layout_mode = 2
size_flags_horizontal = 3
[node name="RefreshButton" type="Button" parent="TopPanel"]
layout_mode = 2
text = "Refresh Instances"
[node name="FetchKeysButton" type="Button" parent="TopPanel"]
layout_mode = 2
text = "Fetch Keys"
[node name="FetchPubSubButton" type="Button" parent="TopPanel"]
layout_mode = 2
text = "Fetch Pub/Sub"
[node name="SearchPanel" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="search_box" type="LineEdit" parent="SearchPanel"]
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Filter keys..."
[node name="filter_button" type="Button" parent="SearchPanel"]
layout_mode = 2
text = "Filter"
[node name="TabContainer" type="TabContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
current_tab = 0
[node name="Keys" type="VBoxContainer" parent="TabContainer"]
layout_mode = 2
metadata/_tab_index = 0
[node name="HSplitContainer" type="HSplitContainer" parent="TabContainer/Keys"]
layout_mode = 2
size_flags_vertical = 3
[node name="key_list" type="Tree" parent="TabContainer/Keys/HSplitContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
columns = 3
column_titles_visible = true
[node name="key_value_view" type="TextEdit" parent="TabContainer/Keys/HSplitContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
editable = false
[node name="PubSub" type="VBoxContainer" parent="TabContainer"]
visible = false
layout_mode = 2
metadata/_tab_index = 1
[node name="HSplitContainer" type="HSplitContainer" parent="TabContainer/PubSub"]
layout_mode = 2
size_flags_vertical = 3
[node name="PubSubTree" type="Tree" parent="TabContainer/PubSub/HSplitContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
hide_root = true
[node name="PubSubEventsTree" type="Tree" parent="TabContainer/PubSub/HSplitContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
columns = 2
column_titles_visible = true
hide_root = true

View File

@ -1,567 +0,0 @@
class_name Gedis extends Node
# Component instances
var _core: GedisCore
var _expiry: GedisExpiry
var _time_source: GedisTimeSource
var _strings: GedisStrings
var _hashes: GedisHashes
var _lists: GedisLists
var _sets: GedisSets
var _sorted_sets: GedisSortedSets
var _pubsub: GedisPubSub
var _debugger_component: GedisDebugger
var _utils: GedisUtils
var _persistence_backends: Dictionary = {}
var _default_persistence_backend: String = ""
# Instance registry
static var _instances: Array = []
static var _next_instance_id: int = 0
var _instance_id: int = -1
var _instance_name: String = ""
static var _debugger_registered = false
func _init() -> void:
# assign id and register
_instance_id = _next_instance_id
_next_instance_id += 1
_instance_name = "Gedis_%d" % _instance_id
_instances.append(self)
# Instantiate components
_core = GedisCore.new(self)
_utils = GedisUtils.new()
_expiry = GedisExpiry.new(self)
_strings = GedisStrings.new(self)
_hashes = GedisHashes.new(self)
_lists = GedisLists.new(self)
_sets = GedisSets.new(self)
_sorted_sets = GedisSortedSets.new(self)
_pubsub = GedisPubSub.new(self)
_debugger_component = GedisDebugger.new(self)
_time_source = GedisUnixTimeSource.new()
_time_source.instance = self
_pubsub.pubsub_message.connect(_on_pubsub_message)
_pubsub.psub_message.connect(_on_psub_message)
GedisDebugger._ensure_debugger_is_registered()
func _on_pubsub_message(channel: String, message: Variant) -> void:
pubsub_message.emit(channel, message)
func _on_psub_message(pattern: String, channel: String, message: Variant) -> void:
psub_message.emit(pattern, channel, message)
func _exit_tree() -> void:
# unregister instance
for i in range(_instances.size()):
if _instances[i] == self:
_instances.remove_at(i)
break
func _process(delta: float) -> void:
_time_source.tick(delta)
# --- Time Source ---
func set_time_source(p_time_source: GedisTimeSource) -> void:
p_time_source.instance = self
_time_source = p_time_source
func get_time_source() -> GedisTimeSource:
return _time_source
# --- Public API ---
signal pubsub_message(channel, message)
signal psub_message(pattern, channel, message)
## Sets a value for a key
func set_value(key: StringName, value: Variant) -> void:
_strings.set_value(key, value)
## Sets a key to a value with an expiration time in seconds.
func setex(key: StringName, seconds: int, value: Variant) -> void:
set_value(key, value)
expire(key, seconds)
## Gets the string value of a key.
func get_value(key: StringName, default_value: Variant = null) -> Variant:
return _strings.get_value(key, default_value)
## Deletes one or more keys.
func del(keys) -> int:
return _strings.del(keys)
## Checks if one or more keys exist.
func exists(keys) -> Variant:
return _strings.exists(keys)
## Checks if a key exists.
func key_exists(key: String) -> bool:
return _strings.key_exists(key)
## Increments the integer value of a key by a given amount.
func incrby(key: String, amount: int = 1) -> int:
return _strings.incrby(key, amount)
## Decrements the integer value of a key by a given amount.
func decrby(key: String, amount: int = 1) -> int:
return _strings.decrby(key, amount)
## Gets all keys matching a pattern.
func keys(pattern: String = "*") -> Array:
return _strings.keys(pattern)
## Sets multiple keys to multiple values.
func mset(dict: Dictionary) -> void:
_strings.mset(dict)
## Gets the values of all specified keys.
func mget(keys: Array) -> Array:
return _strings.mget(keys)
## Appends a value to a key.
func append(key: String, value: String) -> int:
return _strings.append(key, value)
## Atomically sets a key to a value and returns the old value.
func getset(key: String, value: Variant) -> Variant:
return _strings.getset(key, value)
## Gets the length of the string value of a key.
func strlen(key: String) -> int:
return _strings.strlen(key)
## Sets a key to a value, only if the key does not exist.
func setnx(key: String, value: Variant) -> int:
return _strings.setnx(key, value)
## Renames a key to newkey, only if newkey does not exist.
func rename(key: String, newkey: String) -> int:
return _core.rename(key, newkey)
## Moves a key to another key.
func move(key: String, newkey: String) -> int:
return _core.move(key, newkey)
## Returns a random key from the database.
func randomkey() -> String:
var all_keys = _core._get_all_keys().keys()
if all_keys.is_empty():
return ""
return all_keys.pick_random()
## Returns the number of keys in the database.
func dbsize() -> int:
return _core._get_all_keys().size()
## Adds the keyspace prefix to a key
func ks(key: String) -> String:
return _core.ks(key)
## Removes the keyspace prefix from a key if present, otherwise returns the key unchanged
func rks(key: String) -> String:
return _core.rks(key)
# Hashes
## Sets the string value of a hash field.
func hset(key: String, field: String, value) -> int:
return _hashes.hset(key, field, value)
## Gets the value of a hash field.
func hget(key: String, field: String, default_value: Variant = null):
return _hashes.hget(key, field, default_value)
## Gets the values of all the given hash fields.
func hmget(key: String, fields: Array) -> Array:
return _hashes.hmget(key, fields)
## Sets multiple hash fields to multiple values.
func hmset(key: String, field_value_pairs: Dictionary) -> void:
_hashes.hmset(key, field_value_pairs)
## Increments the integer value of a hash field by the given amount.
func hincrby(key: String, field: String, amount: int) -> Variant:
return _hashes.hincrby(key, field, amount)
## Increments the float value of a hash field by the given amount.
func hincrbyfloat(key: String, field: String, amount: float) -> Variant:
return _hashes.hincrbyfloat(key, field, amount)
## Deletes one or more hash fields.
func hdel(key: String, fields) -> int:
return _hashes.hdel(key, fields)
## Gets all the fields and values in a hash.
func hgetall(key: String) -> Dictionary:
return _hashes.hgetall(key)
## Checks if a hash field exists.
func hexists(key: String, field = null) -> bool:
return _hashes.hexists(key, field)
## Gets all the fields in a hash.
func hkeys(key: String) -> Array:
return _hashes.hkeys(key)
## Gets all the values in a hash.
func hvals(key: String) -> Array:
return _hashes.hvals(key)
## Gets the number of fields in a hash.
func hlen(key: String) -> int:
return _hashes.hlen(key)
# Lists
## Prepends one or multiple values to a list.
func lpush(key: String, value) -> int:
return _lists.lpush(key, value)
## Appends one or multiple values to a list.
func rpush(key: String, value) -> int:
return _lists.rpush(key, value)
## Removes and gets the first element in a list.
func lpop(key: String):
return _lists.lpop(key)
## Removes and gets the last element in a list.
func rpop(key: String):
return _lists.rpop(key)
## Gets the length of a list.
func llen(key: String) -> int:
return _lists.llen(key)
## Checks if a list exists.
func lexists(key: String) -> bool:
return _lists.lexists(key)
## Gets all elements from a list.
func lget(key: String) -> Array:
return _lists.lget(key)
## Gets a range of elements from a list.
func lrange(key: String, start: int, stop: int) -> Array:
return _lists.lrange(key, start, stop)
## Gets an element from a list by index.
func lindex(key: String, index: int):
return _lists.lindex(key, index)
## Sets the value of an element in a list by index.
func lset(key: String, index: int, value) -> bool:
return _lists.lset(key, index, value)
## Removes elements from a list.
func lrem(key: String, count: int, value) -> int:
return _lists.lrem(key, count, value)
## Trims a list to the specified range of indices.
func ltrim(key: String, start: int, stop: int) -> bool:
return _lists.ltrim(key, start, stop)
## Inserts a value into a list before or after a pivot value.
func linsert(key: String, position: String, pivot, value) -> int:
return _lists.linsert(key, position, pivot, value)
## Atomically returns and removes the first/last element of the list stored at source and pushes the element at the first/last element of the list stored at destination.
func lmove(source: String, destination: String, from: String, to: String):
return _lists.lmove(source, destination, from, to)
# Sets
## Adds one or more members to a set.
func sadd(key: String, member) -> int:
return _sets.sadd(key, member)
## Removes one or more members from a set.
func srem(key: String, member) -> int:
return _sets.srem(key, member)
## Gets all the members in a set.
func smembers(key: String) -> Array:
return _sets.smembers(key)
## Checks if a member is in a set.
func sismember(key: String, member) -> bool:
return _sets.sismember(key, member)
## Gets the number of members in a set.
func scard(key: String) -> int:
return _sets.scard(key)
## Checks if a set exists.
func sexists(key: String) -> bool:
return _sets.sexists(key)
## Removes and returns a random member from a set.
func spop(key: String):
return _sets.spop(key)
## Moves a member from one set to another.
func smove(source: String, destination: String, member) -> bool:
return _sets.smove(source, destination, member)
## Returns the union of the sets stored at the given keys.
func sunion(keys: Array) -> Array:
return _sets.sunion(keys)
## Returns the intersection of the sets stored at the given keys.
func sinter(keys: Array) -> Array:
return _sets.sinter(keys)
## Returns the difference of the sets stored at the given keys.
func sdiff(keys: Array) -> Array:
return _sets.sdiff(keys)
## Stores the union of the sets at keys in the destination key.
func sunionstore(destination: String, keys: Array) -> int:
return _sets.sunionstore(destination, keys)
## Stores the intersection of the sets at keys in the destination key.
func sinterstore(destination: String, keys: Array) -> int:
return _sets.sinterstore(destination, keys)
## Stores the difference of the sets at keys in the destination key.
func sdiffstore(destination: String, keys: Array) -> int:
return _sets.sdiffstore(destination, keys)
## Returns one or more random members from the set at key.
func srandmember(key: String, count: int = 1) -> Variant:
return _sets.srandmember(key, count)
# Sorted Sets
## Adds a member with a score to a sorted set.
func zadd(key: String, member: String, score: int):
return _sorted_sets.add(key, member, score)
## Checks if a sorted set exists.
func zexists(key: String) -> bool:
return _sorted_sets.zexists(key)
## Gets the number of members in a sorted set.
func zcard(key: String) -> int:
return _sorted_sets.zcard(key)
## Removes a member from a sorted set.
func zrem(key: String, member: String):
return _sorted_sets.remove(key, member)
## Gets members from a sorted set within a score range.
func zrange(key: String, start, stop, withscores: bool = false):
return _sorted_sets.zrange(key, start, stop, withscores)
## Gets members from a sorted set within a score range, in reverse order.
func zrevrange(key: String, start, stop, withscores: bool = false):
return _sorted_sets.zrevrange(key, start, stop, withscores)
## Removes and returns members with scores up to a certain value.
func zpopready(key: String, now: int):
return _sorted_sets.pop_ready(key, now)
## Returns the score of member in the sorted set at key.
func zscore(key: String, member: String) -> Variant:
return _sorted_sets.zscore(key, member)
## Returns the rank of member in the sorted set at key.
func zrank(key: String, member: String) -> Variant:
return _sorted_sets.zrank(key, member)
## Returns the rank of member in the sorted set at key, with scores ordered from high to low.
func zrevrank(key: String, member: String) -> Variant:
return _sorted_sets.zrevrank(key, member)
## Returns the number of elements in the sorted set at key with a score between min and max.
func zcount(key: String, min_score, max_score) -> int:
return _sorted_sets.zcount(key, min_score, max_score)
## Increments the score of member in the sorted set at key by increment.
func zincrby(key: String, increment, member: String) -> Variant:
return _sorted_sets.zincrby(key, increment, member)
## Returns a range of members in a sorted set, by score.
func zrangebyscore(key: String, min_score, max_score, withscores: bool = false) -> Array:
return _sorted_sets.zrangebyscore(key, min_score, max_score, withscores)
## Returns a range of members in a sorted set, by score, in reverse order.
func zrevrangebyscore(key: String, min_score, max_score, withscores: bool = false) -> Array:
return _sorted_sets.zrevrangebyscore(key, min_score, max_score, withscores)
## Computes the union of sorted sets and stores the result in a new key.
func zunionstore(destination: String, keys: Array, aggregate: String = "SUM") -> int:
return _sorted_sets.zunionstore(destination, keys, aggregate)
## Computes the intersection of sorted sets and stores the result in a new key.
func zinterstore(destination: String, keys: Array, aggregate: String = "SUM") -> int:
return _sorted_sets.zinterstore(destination, keys, aggregate)
# Pub/Sub
## Posts a message to a channel.
func publish(channel: String, message) -> void:
_pubsub.publish.call_deferred(channel, message)
## Subscribes to a channel.
func subscribe(channel: String, subscriber: Object) -> void:
_pubsub.subscribe(channel, subscriber)
## Unsubscribes from a channel.
func unsubscribe(channel: String, subscriber: Object) -> void:
_pubsub.unsubscribe(channel, subscriber)
## Subscribes to channels matching a pattern.
func psubscribe(pattern: String, subscriber: Object) -> void:
_pubsub.psubscribe(pattern, subscriber)
## Unsubscribes from channels matching a pattern.
func punsubscribe(pattern: String, subscriber: Object) -> void:
_pubsub.punsubscribe(pattern, subscriber)
## Returns a list of all active channels.
func list_channels() -> Array:
return _pubsub.list_channels()
## Returns a list of subscribers for a given channel.
func list_subscribers(channel: String) -> Array:
return _pubsub.list_subscribers(channel)
## Returns a list of all active patterns.
func list_patterns() -> Array:
return _pubsub.list_patterns()
## Returns a list of subscribers for a given pattern.
func list_pattern_subscribers(pattern: String) -> Array:
return _pubsub.list_pattern_subscribers(pattern)
# Expiry
## Sets a key's time to live in seconds.
func expire(key: String, seconds: int) -> bool:
return _expiry.expire(key, seconds)
## Gets the remaining time to live of a key.
func ttl(key: String) -> int:
return _expiry.ttl(key)
## Removes the expiration from a key.
func persist(key: String) -> bool:
return _expiry.persist(key)
## Purges all expired keys
func purge_expired() -> void:
_expiry._purge_expired()
# Admin
## Deletes all keys from the database.
func flushall() -> void:
_core.flushall()
# Persistence
## Registers a new persistence backend.
func register_persistence_backend(name: String, backend: GedisPersistenceBackend) -> void:
_persistence_backends[name] = backend
## Sets the default persistence backend.
func set_default_persistence_backend(name: String) -> bool:
if _persistence_backends.has(name):
_default_persistence_backend = name
return true
return false
## Saves the current state to a file using the default persistence backend.
func save(path: String, options: Dictionary = {}) -> int:
if _default_persistence_backend.is_empty():
register_persistence_backend("json", GedisJSONSnapshotBackend.new())
set_default_persistence_backend("json")
var backend: GedisPersistenceBackend = _persistence_backends[_default_persistence_backend]
var dump_options = options.duplicate()
if dump_options.has("path"):
dump_options.erase("path")
var data = _core.dump_all(dump_options)
var save_options = {"path": path}
return backend.save(data, save_options)
## Loads the state from a file using the default persistence backend.
func load(path: String, options: Dictionary = {}) -> int:
if _default_persistence_backend.is_empty():
register_persistence_backend("json", GedisJSONSnapshotBackend.new())
set_default_persistence_backend("json")
var backend: GedisPersistenceBackend = _persistence_backends[_default_persistence_backend]
var load_options = {"path": path}
var data = backend.load(load_options)
if data.is_empty():
return FAILED
_core.restore_all(data)
return OK
## Dumps the entire dataset to a variable.
func dump_all(options: Dictionary = {}) -> Dictionary:
return _core.dump_all(options)
## Restores the entire dataset from a variable.
func restore_all(data: Dictionary) -> void:
_core.restore_all(data)
## Restores a key from a serialized value.
func restore(key: String, data: String, backend: String = "") -> int:
var persistence_backend: GedisPersistenceBackend
if backend.is_empty():
if _default_persistence_backend.is_empty():
register_persistence_backend("json", GedisJSONSnapshotBackend.new())
set_default_persistence_backend("json")
persistence_backend = _persistence_backends[_default_persistence_backend]
elif _persistence_backends.has(backend):
persistence_backend = _persistence_backends[backend]
else:
return FAILED
var deserialized_data = persistence_backend.deserialize(data)
if deserialized_data.is_empty():
return FAILED
_core.restore_key(key, deserialized_data)
return OK
# Debugger
## Returns the type of the value stored at a key.
func type(key: String) -> String:
return _debugger_component.type(key)
## Returns a dictionary representation of the value stored at a key.
func dump_key(key: String) -> Dictionary:
return _debugger_component.dump(key)
## Returns a snapshot of the database for keys matching a pattern.
func snapshot(pattern: String = "*") -> Dictionary:
return _debugger_component.snapshot(pattern)
# Instance helpers
## Sets the name for this Gedis instance.
func set_instance_name(name: String) -> void:
_instance_name = name
## Gets the name for this Gedis instance.
func get_instance_name() -> String:
return _instance_name
## Gets all active Gedis instances.
static func get_all_instances() -> Array:
var result: Array = []
for inst in _instances:
if is_instance_valid(inst):
var info: Dictionary = {}
info["id"] = inst._instance_id
info["name"] = inst.name if inst.name else inst._instance_name
info["object"] = inst
result.append(info)
return result
static func _on_debugger_message(message: String, data: Array) -> bool:
return GedisDebugger._on_debugger_message(message, data)

View File

@ -1 +0,0 @@
uid://7l48sw6l2lq3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

View File

@ -1,7 +0,0 @@
[plugin]
name="Gedis"
description="Redis-like in-memory key-value store for Godot"
author="NodotProject"
version="0.1.9"
script="plugin.gd"
icon="icon.png"

View File

@ -1,412 +0,0 @@
@tool
extends EditorPlugin
const GedisDebuggerPanel = preload("res://addons/Gedis/debugger/gedis_debugger_panel.tscn")
class GedisDebuggerPlugin extends EditorDebuggerPlugin:
var dashboard_tabs = {} # session_id -> dashboard_control
var full_snapshot_data = {} # session_id -> full_snapshot_data
var pending_requests = {} # session_id -> { "command": String, "instance_id": int, "context": any }
var _pubsub_events = {} # session_id -> []
func _has_capture(capture):
return capture == "gedis"
func _capture(message, data, session_id):
if not _pubsub_events.has(session_id):
_pubsub_events[session_id] = []
# message is the full message string received by the editor (e.g. "gedis:instances_data")
# data is the Array payload sent from the game. Engines and editors may wrap values
# differently (sometimes the payload is passed as a single element array), so normalize.
var parts = message.split(":")
var kind = parts[1] if parts.size() > 1 else ""
match kind:
"ping":
_request_instances_update(session_id)
return true
"instances_data":
# instances may be either sent as the array itself or wrapped as [instances]
var instances = data
if data.size() == 1 and typeof(data[0]) == TYPE_ARRAY:
instances = data[0]
_update_instances(instances, session_id)
return true
"snapshot_data":
# snapshot is expected to be a Dictionary; it may be wrapped in an Array
var snapshot = data[0] if data.size() > 0 else {}
_update_snapshot_data(snapshot, session_id)
return true
"key_value_data":
# key/value payload may be wrapped similarly
var kv = data[0] if data.size() > 0 else {}
_update_key_value_data(kv, session_id)
return true
"pubsub_data":
var channels_data = data[0] if data.size() > 0 else {}
var patterns_data = data[1] if data.size() > 1 else {}
_update_pubsub_tree(session_id, channels_data, patterns_data)
return true
"pubsub_event":
if not session_id in _pubsub_events:
_pubsub_events[session_id] = []
_pubsub_events[session_id].append({"message": data[0], "data": [data[1], data[2]]})
_populate_pubsub_events(session_id)
_fetch_pubsub_for_selected_instance(session_id) # Refresh tree on event
return true
return false
func _setup_session(session_id):
var dashboard = GedisDebuggerPanel.instantiate()
dashboard.name = "Gedis"
dashboard.plugin = self
var session = get_session(session_id)
session.started.connect(func(): _on_session_started(session_id))
session.stopped.connect(func(): _on_session_stopped(session_id))
session.add_session_tab(dashboard)
dashboard_tabs[session_id] = dashboard
_pubsub_events[session_id] = []
func _on_session_started(session_id):
full_snapshot_data[session_id] = {}
_clear_views(session_id)
_populate_pubsub_events(session_id)
_update_pubsub_tree(session_id, {}, {})
var dashboard = dashboard_tabs[session_id]
var instance_selector = dashboard.find_child("instance_selector", true, false)
if instance_selector:
instance_selector.item_selected.connect(func(index): _on_instance_selected(index, session_id))
var refresh_button = dashboard.find_child("RefreshButton", true, false)
if refresh_button:
refresh_button.pressed.connect(func(): _request_instances_update(session_id))
var fetch_keys_button = dashboard.find_child("FetchKeysButton", true, false)
if fetch_keys_button:
fetch_keys_button.pressed.connect(func(): _fetch_keys_for_selected_instance(session_id))
var filter_button = dashboard.find_child("filter_button", true, false)
if filter_button:
filter_button.pressed.connect(func(): _on_filter_pressed(session_id))
var key_list = dashboard.find_child("key_list", true, false)
if key_list:
key_list.item_selected.connect(func(): _on_key_selected(session_id))
_request_instances_update(session_id)
func _on_session_stopped(session_id):
if session_id in dashboard_tabs:
var dashboard = dashboard_tabs[session_id]
var status_label = dashboard.find_child("status_label", true, false)
if status_label:
status_label.text = "Game disconnected"
status_label.add_theme_color_override("font_color", Color.RED)
func _request_instances_update(session_id):
var session = get_session(session_id)
if session and session.is_active():
session.send_message("gedis:request_instances", [])
func _update_instances(instances_data, session_id):
if not session_id in dashboard_tabs:
return
var dashboard = dashboard_tabs[session_id]
var status_label = dashboard.find_child("status_label", true, false)
var instance_selector = dashboard.find_child("instance_selector", true, false)
if not instance_selector:
print("Warning: instance_selector not found in dashboard")
return
instance_selector.clear()
if instances_data.size() > 0:
status_label.text = "Connected - Found %d Gedis instance(s)" % instances_data.size()
status_label.add_theme_color_override("font_color", Color.GREEN)
for instance_info in instances_data:
var name = instance_info.get("name", "Gedis_%d" % instance_info.get("id", -1))
var id = int(instance_info["id"]) if instance_info.has("id") else -1
instance_selector.add_item(name, id)
if instance_selector.get_item_count() > 0:
instance_selector.select(0)
# The select() method does not trigger the item_selected signal, so we must call the handler manually.
_fetch_keys_for_selected_instance(session_id)
_fetch_pubsub_for_selected_instance(session_id)
else:
status_label.text = "No Gedis instances found in running game"
status_label.add_theme_color_override("font_color", Color.ORANGE)
_clear_views(session_id)
func _on_instance_selected(_index, session_id):
_fetch_keys_for_selected_instance(session_id)
_fetch_pubsub_for_selected_instance(session_id)
func _fetch_keys_for_selected_instance(session_id):
if not session_id in dashboard_tabs:
return
var dashboard = dashboard_tabs[session_id]
var instance_selector = dashboard.find_child("instance_selector", true, false)
if not instance_selector or instance_selector.get_selected() < 0:
return
var instance_id = instance_selector.get_item_id(instance_selector.get_selected())
var session = get_session(session_id)
pending_requests[session_id] = {"command": "snapshot", "instance_id": instance_id}
if session and session.is_active():
session.send_message("gedis:request_instance_data", [instance_id, "snapshot", "*"])
func _fetch_pubsub_for_selected_instance(session_id):
if not session_id in dashboard_tabs:
return
var dashboard = dashboard_tabs[session_id]
var instance_selector = dashboard.find_child("instance_selector", true, false)
if not instance_selector or instance_selector.get_selected() < 0:
return
var instance_id = instance_selector.get_item_id(instance_selector.get_selected())
var session = get_session(session_id)
pending_requests[session_id] = {"command": "pubsub", "instance_id": instance_id}
if session and session.is_active():
session.send_message("gedis:request_instance_data", [instance_id, "pubsub"])
func _on_filter_pressed(session_id):
if not session_id in dashboard_tabs:
return
var dashboard = dashboard_tabs[session_id]
var search_box = dashboard.find_child("search_box", true, false)
var filter_text = search_box.text if search_box else ""
_populate_key_list(session_id, filter_text)
func _update_snapshot_data(snapshot_data, session_id):
if not session_id in dashboard_tabs:
return
full_snapshot_data[session_id] = snapshot_data
_populate_key_list(session_id)
func _populate_key_list(session_id, filter_text = ""):
var dashboard = dashboard_tabs[session_id]
var key_list = dashboard.find_child("key_list", true, false)
if not key_list:
return
key_list.clear()
var root = key_list.create_item()
var data_to_display = full_snapshot_data.get(session_id, {})
var regex = RegEx.new()
if not filter_text.is_empty():
var pattern = filter_text.replace("*", ".*").replace("?", ".")
regex.compile(pattern)
for redis_key in data_to_display.keys():
if filter_text.is_empty() or regex.search(redis_key):
var key_info = data_to_display[redis_key]
var item = key_list.create_item(root)
item.set_text(0, redis_key)
item.set_text(1, key_info.get("type", "UNKNOWN"))
var ttl_value = key_info.get("ttl", -1)
if ttl_value == -1:
item.set_text(2, "")
elif ttl_value == -2:
item.set_text(2, "EXPIRED")
else:
item.set_text(2, str(ttl_value) + "s")
func _update_key_value_data(key_value_data, session_id):
if not session_id in dashboard_tabs:
return
var dashboard = dashboard_tabs[session_id]
var key_value_view = dashboard.find_child("key_value_view", true, false)
if not key_value_view:
return
if key_value_data is Dictionary and "value" in key_value_data:
key_value_view.text = var_to_str(key_value_data.value)
else:
key_value_view.text = var_to_str(key_value_data)
func _on_key_selected(session_id):
if not session_id in dashboard_tabs:
return
var dashboard = dashboard_tabs[session_id]
var key_list = dashboard.find_child("key_list", true, false)
var key_value_view = dashboard.find_child("key_value_view", true, false)
var instance_selector = dashboard.find_child("instance_selector", true, false)
if not key_list or not key_value_view or not instance_selector:
return
var selected_item = key_list.get_selected()
if not selected_item:
key_value_view.text = ""
return
var selected_key = selected_item.get_text(0)
if instance_selector.get_selected() >= 0:
var instance_id = instance_selector.get_item_id(instance_selector.get_selected())
var session = get_session(session_id)
pending_requests[session_id] = {"command": "dump", "instance_id": instance_id, "key": selected_key}
if session and session.is_active():
session.send_message("gedis:request_instance_data", [instance_id, "dump", selected_key])
key_value_view.editable = false
func _on_edit_pressed(session_id):
if not session_id in dashboard_tabs:
return
var dashboard = dashboard_tabs[session_id]
var key_value_view = dashboard.find_child("key_value_view", true, false)
var edit_button = dashboard.find_child("edit_button", true, false)
var save_button = dashboard.find_child("save_button", true, false)
if not key_value_view or not edit_button or not save_button:
return
key_value_view.editable = true
save_button.disabled = false
edit_button.disabled = true
func _on_save_pressed(session_id):
if not session_id in dashboard_tabs:
return
var dashboard = dashboard_tabs[session_id]
var key_list = dashboard.find_child("key_list", true, false)
var key_value_view = dashboard.find_child("key_value_view", true, false)
var instance_selector = dashboard.find_child("instance_selector", true, false)
var edit_button = dashboard.find_child("edit_button", true, false)
var save_button = dashboard.find_child("save_button", true, false)
if not key_list or not key_value_view or not instance_selector or not edit_button or not save_button:
return
var selected_item = key_list.get_selected()
if not selected_item or instance_selector.get_selected() < 0:
return
var instance_id = instance_selector.get_item_id(instance_selector.get_selected())
var key = selected_item.get_text(0)
var new_value_text = key_value_view.text
var json = JSON.new()
var error = json.parse(new_value_text)
var new_value
if error == OK:
new_value = json.get_data()
else:
# Fallback for non-JSON strings
new_value = new_value_text
var session = get_session(session_id)
if session and session.is_active():
pending_requests[session_id] = {"command": "set", "instance_id": instance_id, "key": key}
session.send_message("gedis:request_instance_data", [instance_id, "set", key, new_value])
key_value_view.editable = false
save_button.disabled = true
edit_button.disabled = false
func _clear_views(session_id):
if not session_id in dashboard_tabs:
return
var dashboard = dashboard_tabs[session_id]
var key_list = dashboard.find_child("key_list", true, false)
var key_value_view = dashboard.find_child("key_value_view", true, false)
var edit_button = dashboard.find_child("edit_button", true, false)
var save_button = dashboard.find_child("save_button", true, false)
if key_list:
key_list.clear()
if key_value_view:
key_value_view.text = ""
if edit_button:
edit_button.disabled = true
if save_button:
save_button.disabled = true
func _populate_pubsub_events(session_id):
var dashboard = dashboard_tabs[session_id]
var events_tree = dashboard.find_child("PubSubEventsTree", true, false)
if not events_tree:
return
events_tree.clear()
var root = events_tree.create_item()
for event in _pubsub_events.get(session_id, []):
var event_item = events_tree.create_item(root)
event_item.set_text(0, event.message)
event_item.set_text(1, JSON.stringify(event.data))
func _update_pubsub_tree(session_id, channels_data, patterns_data):
var dashboard = dashboard_tabs[session_id]
var pub_sub_tree = dashboard.find_child("PubSubTree", true, false)
if not pub_sub_tree:
return
pub_sub_tree.clear()
var root = pub_sub_tree.create_item()
var channels_item = pub_sub_tree.create_item(root)
channels_item.set_text(0, "Channels")
for channel_name in channels_data:
var subscribers = channels_data[channel_name]
var channel_item = pub_sub_tree.create_item(channels_item)
channel_item.set_text(0, channel_name)
for sub in subscribers:
var sub_item = pub_sub_tree.create_item(channel_item)
sub_item.set_text(0, str(sub))
var patterns_item = pub_sub_tree.create_item(root)
patterns_item.set_text(0, "Patterns")
for pattern_name in patterns_data:
var subscribers = patterns_data[pattern_name]
var pattern_item = pub_sub_tree.create_item(patterns_item)
pattern_item.set_text(0, pattern_name)
for sub in subscribers:
var sub_item = pub_sub_tree.create_item(pattern_item)
sub_item.set_text(0, str(sub))
func send_message_to_game(message: String, data: Array):
var session_id = get_current_session_id()
if session_id != -1:
var session = get_session(session_id)
if session and session.is_active():
session.send_message("gedis:" + message, data)
func get_current_session_id():
for session_id in dashboard_tabs:
var session = get_session(session_id)
if session and session.is_active():
return session_id
return -1
var debugger_plugin
func _enter_tree():
debugger_plugin = GedisDebuggerPlugin.new()
add_debugger_plugin(debugger_plugin)
func _exit_tree():
remove_debugger_plugin(debugger_plugin)
debugger_plugin = null

View File

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

View File

@ -1,274 +0,0 @@
extends Node
class_name GedisQueue
## Manages job queues and workers.
##
## This class provides a high-level interface for creating and managing job queues
## that are processed by workers. It uses Gedis (a Godot Redis-like in-memory
## data structure server) for storing job information and queue state.
## Emitted when a job is completed successfully.
signal completed(job: GedisJob, return_value)
## Emitted when a job fails.
signal failed(job: GedisJob, error_message: String)
## Emitted when a job's progress is updated.
signal progress(job: GedisJob, value: float)
const QUEUE_PREFIX = "gedis_queue:"
var max_completed_jobs := 0
var max_failed_jobs := 0
const STATUS_WAITING = "waiting"
const STATUS_ACTIVE = "active"
const STATUS_COMPLETED = "completed"
const STATUS_FAILED = "failed"
var _gedis: Gedis
var _workers: Array[GedisWorker] = []
## Sets up the GedisQueue with a Gedis instance.
##
## If no Gedis instance is provided, a new one will be created automatically
## when needed.
##
## @param gedis_instance The Gedis instance to use.
func setup(gedis_instance: Gedis = null):
if gedis_instance:
_gedis = gedis_instance
else:
_gedis = Gedis.new()
_gedis.name = "Gedis"
add_child(_gedis)
## Adds a new job to a queue.
##
## @param queue_name The name of the queue to add the job to.
## @param job_data A dictionary containing the data for the job.
## @param opts A dictionary of options for the job.
## "add_to_front": (bool) If true, adds the job to the front of the queue.
## @return The newly created GedisJob.
func add(queue_name: String, job_data: Dictionary, opts: Dictionary = {}) -> GedisJob:
_ensure_gedis_instance()
var job_id = _generate_job_id()
var job_key = _get_job_key(queue_name, job_id)
var job = GedisJob.new(self, queue_name, job_id, job_data)
var job_hash = {
"id": job_id,
"data": job_data,
"status": STATUS_WAITING,
"progress": 0.0
}
for key in job_hash:
_gedis.hset(job_key, key, job_hash[key])
if opts.get("add_to_front", false):
_gedis.lpush(_get_queue_key(queue_name, STATUS_WAITING), job_id)
else:
_gedis.rpush(_get_queue_key(queue_name, STATUS_WAITING), job_id)
_gedis.publish(_get_event_channel(queue_name, "added"), {"job_id": job_id, "data": job_data})
return job
## Retrieves a job from a queue by its ID.
##
## @param queue_name The name of the queue.
## @param job_id The ID of the job to retrieve.
## @return The GedisJob if found, otherwise null.
func get_job(queue_name: String, job_id: String) -> GedisJob:
var job_key = _get_job_key(queue_name, job_id)
var job_hash = _gedis.hgetall(job_key)
if job_hash.is_empty():
return null
var job_data = job_hash.get("data", {})
var job_status = job_hash.get("status", GedisQueue.STATUS_WAITING)
var job = GedisJob.new(self, queue_name, job_id, job_data, job_status)
return job
## Retrieves a list of jobs from a queue.
##
## @param queue_name The name of the queue.
## @param types An array of job statuses to retrieve (e.g., ["waiting", "active"]).
## @param start The starting index.
## @param end The ending index.
## @param asc Whether to sort in ascending order (currently unused).
## @return An array of GedisJob objects.
func get_jobs(queue_name: String, types: Array, start: int = 0, end: int = -1, asc: bool = false) -> Array[GedisJob]:
var jobs: Array[GedisJob] = []
for type in types:
var queue_key = _get_queue_key(queue_name, type)
var job_ids = _gedis.lrange(queue_key, start, end)
for job_id in job_ids:
var job = get_job(queue_name, job_id)
if job:
jobs.append(job)
return jobs
## Pauses a queue.
##
## When a queue is paused, workers will not process any new jobs from it.
##
## @param queue_name The name of the queue to pause.
func pause(queue_name: String) -> void:
_ensure_gedis_instance()
var state_key = _get_state_key(queue_name)
_gedis.hset(state_key, "paused", "1")
## Resumes a paused queue.
##
## @param queue_name The name of the queue to resume.
func resume(queue_name: String) -> void:
_ensure_gedis_instance()
var state_key = _get_state_key(queue_name)
_gedis.hdel(state_key, "paused")
## Checks if a queue is paused.
##
## @param queue_name The name of the queue.
## @return True if the queue is paused, otherwise false.
func is_paused(queue_name: String) -> bool:
_ensure_gedis_instance()
var state_key = _get_state_key(queue_name)
return _gedis.hexists(state_key, "paused")
## Updates the progress of a job.
##
## @param queue_name The name of the queue.
## @param job_id The ID of the job.
## @param value The new progress value (0.0 to 1.0).
func update_job_progress(queue_name: String, job_id: String, value: float):
_ensure_gedis_instance()
var job_key = _get_job_key(queue_name, job_id)
_gedis.hset(job_key, "progress", value)
_gedis.publish(_get_event_channel(queue_name, "progress"), {"job_id": job_id, "progress": value})
progress.emit(get_job(queue_name, job_id), value)
## Removes a job from a queue.
##
## @param queue_name The name of the queue.
## @param job_id The ID of the job to remove.
func remove_job(queue_name: String, job_id: String):
_ensure_gedis_instance()
var job_key = _get_job_key(queue_name, job_id)
_gedis.del(job_key)
# Remove the job ID from all possible status lists
for status in [STATUS_WAITING, STATUS_ACTIVE, STATUS_COMPLETED, STATUS_FAILED]:
var queue_key = _get_queue_key(queue_name, status)
_gedis.lrem(queue_key, 0, job_id)
func _get_queue_key(queue_name: String, status: String = STATUS_WAITING) -> String:
return "%s%s:%s" % [QUEUE_PREFIX, queue_name, status]
func _get_job_key(queue_name: String, job_id: String) -> String:
return QUEUE_PREFIX + queue_name + ":job:" + job_id
func _get_state_key(queue_name: String) -> String:
return QUEUE_PREFIX + queue_name + ":state"
func _get_event_channel(queue_name: String, event: String) -> String:
return "%s%s:events:%s" % [QUEUE_PREFIX, queue_name, event]
func _generate_job_id() -> String:
var t = Time.get_unix_time_from_system()
var r = randi() % 1000
return "%s-%s" % [t, r]
func _ensure_gedis_instance():
if not _gedis:
var gedis_instance = Gedis.new()
gedis_instance.name = "Gedis"
add_child(gedis_instance)
setup(gedis_instance)
## Starts a worker to process jobs from a queue.
##
## @param queue_name The name of the queue to process.
## @param processor A callable that will be executed for each job.
## @return The newly created GedisWorker.
func process(queue_name: String, processor: Callable, p_batch_size: int = 1) -> GedisWorker:
var worker = GedisWorker.new(self, queue_name, processor, p_batch_size)
add_child(worker)
_workers.append(worker)
worker.start()
return worker
## Closes all workers for a specific queue.
##
## @param queue_name The name of the queue.
func close(queue_name: String) -> void:
var workers_to_remove: Array[GedisWorker] = []
for worker in _workers:
if worker._queue_name == queue_name:
workers_to_remove.append(worker)
for worker in workers_to_remove:
worker.close()
_workers.erase(worker)
worker.queue_free()
func _enter_tree() -> void:
if !_gedis:
var gedis_instance = Gedis.new()
gedis_instance.name = "Gedis"
add_child(gedis_instance)
_gedis = gedis_instance
func _exit_tree():
for worker in _workers:
if is_instance_valid(worker):
worker.close()
## Marks a job as completed.
##
## @param job The job to mark as completed.
## @param return_value The return value of the job.
func _job_completed(job: GedisJob, return_value):
_ensure_gedis_instance()
var job_key = _get_job_key(job.queue_name, job.id)
_gedis.lrem(_get_queue_key(job.queue_name, STATUS_ACTIVE), 1, job.id)
completed.emit(job, return_value)
_gedis.publish(_get_event_channel(job.queue_name, "completed"), {"job_id": job.id, "return_value": return_value})
if max_completed_jobs == 0:
_gedis.del(job_key)
else:
_gedis.hset(job_key, "status", STATUS_COMPLETED)
_gedis.hset(job_key, "returnvalue", return_value)
_gedis.lpush(_get_queue_key(job.queue_name, STATUS_COMPLETED), job.id)
if max_completed_jobs > 0:
_gedis.ltrim(_get_queue_key(job.queue_name, STATUS_COMPLETED), 0, max_completed_jobs - 1)
## Marks a job as failed.
##
## @param job The job to mark as failed.
## @param error_message The error message.
func _job_failed(job: GedisJob, error_message: String):
_ensure_gedis_instance()
var job_key = _get_job_key(job.queue_name, job.id)
_gedis.lrem(_get_queue_key(job.queue_name, STATUS_ACTIVE), 1, job.id)
failed.emit(job, error_message)
_gedis.publish(_get_event_channel(job.queue_name, "failed"), {"job_id": job.id, "error_message": error_message})
if max_failed_jobs == 0:
_gedis.del(job_key)
else:
_gedis.hset(job_key, "status", STATUS_FAILED)
_gedis.hset(job_key, "failed_reason", error_message)
_gedis.lpush(_get_queue_key(job.queue_name, STATUS_FAILED), job.id)
if max_failed_jobs > 0:
_gedis.ltrim(_get_queue_key(job.queue_name, STATUS_FAILED), 0, max_failed_jobs - 1)

View File

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

View File

@ -1,51 +0,0 @@
extends RefCounted
class_name GedisJob
## Represents a job in a GedisQueue.
##
## This class holds information about a job, such as its ID, data, and status.
## It also provides methods for interacting with the job, such as updating its
## progress and removing it from the queue.
var id: String
var data: Dictionary
var queue_name: String
var status: String
var _gedis_queue
func _init(p_gedis_queue, p_queue_name: String, p_id: String, p_data: Dictionary, p_status: String = GedisQueue.STATUS_WAITING):
_gedis_queue = p_gedis_queue
queue_name = p_queue_name
id = p_id
data = p_data
status = p_status
## Updates the progress of the job.
##
## @param value The new progress value (0.0 to 1.0).
func progress(value: float) -> void:
_gedis_queue.update_job_progress(queue_name, id, value)
## Removes the job from the queue.
func remove() -> void:
_gedis_queue.remove_job(queue_name, id)
## Marks the job as completed.
##
## This should be called by a worker's processor function when the job has
## been successfully processed.
##
## @param return_value An optional value to store as the result of the job.
func complete(return_value: Variant = null) -> void:
_gedis_queue._job_completed(self, return_value)
## Marks the job as failed.
##
## This should be called by a worker's processor function when the job has
## failed to be processed.
##
## @param error_message The error message to store for the failed job.
func fail(error_message: String) -> void:
_gedis_queue._job_failed(self, error_message)

View File

@ -1 +0,0 @@
uid://3qjcot47lxgw

View File

@ -1,106 +0,0 @@
extends Node
class_name GedisWorker
## Processes jobs from a GedisQueue.
##
## A worker is responsible for fetching jobs from a specific queue and executing
## a processor function for each job.
##
## The processor function receives the job and is responsible for calling
## `job.complete()` or `job.fail()` to finish the job. The worker itself does
## not handle the return value of the processor.
signal completed(job: GedisJob, return_value)
signal failed(job: GedisJob, error_message: String)
signal progress(job: GedisJob, value: float)
signal _batch_completed
var _gedis_queue: GedisQueue
var _queue_name: String
var _processor: Callable
var _is_running = false
var _gedis: Gedis
var _jobs_in_progress = {}
var batch_size = 1
func _init(p_gedis_queue: GedisQueue, p_queue_name: String, p_processor: Callable, p_batch_size: int = 1):
_gedis_queue = p_gedis_queue
_gedis = _gedis_queue._gedis
_queue_name = p_queue_name
_processor = p_processor
batch_size = p_batch_size
_gedis_queue.completed.connect(_on_job_completed)
_gedis_queue.failed.connect(_on_job_failed)
_gedis_queue.progress.connect(func(job: GedisJob, value: float): progress.emit(job, value))
## Starts the worker.
func start():
_is_running = true
_process_jobs()
## Stops the worker.
func close():
_is_running = false
func _on_job_completed(job: GedisJob, return_value):
if _jobs_in_progress.has(job.id):
_jobs_in_progress.erase(job.id)
if _jobs_in_progress.is_empty():
_batch_completed.emit()
completed.emit(job, return_value)
func _on_job_failed(job: GedisJob, error_message: String):
if _jobs_in_progress.has(job.id):
_jobs_in_progress.erase(job.id)
if _jobs_in_progress.is_empty():
_batch_completed.emit()
failed.emit(job, error_message)
func _process_jobs():
await get_tree().process_frame
while _is_running:
if _gedis_queue.is_paused(_queue_name):
await get_tree().create_timer(1.0).timeout
continue
var job_ids = []
for i in range(batch_size):
var job_id = _gedis.lpop(_gedis_queue._get_queue_key(_queue_name, GedisQueue.STATUS_WAITING))
if not job_id:
break
job_ids.append(job_id)
if job_ids.is_empty():
if Engine.is_editor_hint():
await get_tree().process_frame
else:
if get_tree():
await get_tree().process_frame
continue
for job_id in job_ids:
_gedis.lpush(_gedis_queue._get_queue_key(_queue_name, GedisQueue.STATUS_ACTIVE), job_id)
var job = _gedis_queue.get_job(_queue_name, job_id)
if not job:
push_warning("GedisQueueWorker: Job with id %s not found." % job_id)
continue
_jobs_in_progress[job_id] = job
var job_key = _gedis_queue._get_job_key(_queue_name, job_id)
_gedis.hset(job_key, "status", GedisQueue.STATUS_ACTIVE)
_gedis_queue._gedis.publish(_gedis_queue._get_event_channel(_queue_name, "active"), {"job_id": job.id})
_process_job(job)
if not _jobs_in_progress.is_empty():
await _batch_completed
func _process_job(job: GedisJob):
var result = await _processor.call(job)
if result is Object and result.has_method("is_valid"):
result = await result
if job.status == GedisQueue.STATUS_ACTIVE:
job.complete(result)

View File

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

View File

@ -1,67 +0,0 @@
@tool
extends VBoxContainer
# UI references will be set from the scene
@export var queues_tree: Tree
@export var jobs_tree: Tree
@export var job_details_text: TextEdit
@export var refresh_button: Button
var plugin
func _ready() -> void:
if refresh_button:
refresh_button.pressed.connect(_on_refresh_pressed)
# Set column titles for the queues tree
queues_tree.set_column_titles_visible(true)
queues_tree.set_column_title(0, "Queue")
queues_tree.set_column_title(1, "Jobs")
# Set column titles for the jobs tree
jobs_tree.set_column_titles_visible(true)
jobs_tree.set_column_title(0, "ID")
jobs_tree.set_column_title(1, "Queue")
jobs_tree.set_column_title(2, "Status")
func set_plugin(p) -> void:
plugin = p
func _on_refresh_pressed() -> void:
if plugin:
# This assumes the main dashboard has an instance selector
var selected_id = get_parent().get_parent().instance_selector.get_item_id(get_parent().get_parent().instance_selector.selected)
var session_id = plugin.get_current_session_id()
if session_id != -1:
plugin._fetch_keys_for_selected_instance(session_id)
func update_queues(data: Dictionary) -> void:
queues_tree.clear()
var root = queues_tree.create_item()
queues_tree.hide_root = true
for queue_name in data:
var queue_item = queues_tree.create_item(root)
queue_item.set_text(0, queue_name)
queue_item.set_text(1, str(data[queue_name].size()))
func update_jobs(jobs: Array) -> void:
jobs_tree.clear()
var root = jobs_tree.create_item()
jobs_tree.hide_root = true
for job in jobs:
var job_item = jobs_tree.create_item(root)
job_item.set_text(0, job.id)
job_item.set_text(1, job.queue_name)
job_item.set_text(2, job.status)
job_item.set_meta("data", job.data)
func _on_jobs_tree_item_selected() -> void:
var selected_item = jobs_tree.get_selected()
if selected_item:
job_details_text.text = selected_item.get_meta("data")
func _on_queues_tree_item_selected() -> void:
# TODO: should filter out jobs not the selected queue
pass

View File

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

View File

@ -1,48 +0,0 @@
[gd_scene load_steps=2 format=3 uid="uid://b5h5h5h5h5h5"]
[ext_resource type="Script" uid="uid://c5fv5yw1cok24" path="res://addons/GedisQueue/debugger/gedis_queue_debugger_panel.gd" id="1_abcde"]
[node name="GedisQueueDebuggerPanel" type="VBoxContainer" node_paths=PackedStringArray("queues_tree", "jobs_tree", "job_details_text", "refresh_button")]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1_abcde")
queues_tree = NodePath("HSplitContainer/QueuesTree")
jobs_tree = NodePath("HSplitContainer/JobsVBox/JobsTree")
job_details_text = NodePath("HSplitContainer/JobsVBox/JobDetailsText")
refresh_button = NodePath("RefreshButton")
[node name="RefreshButton" type="Button" parent="."]
layout_mode = 2
text = "Refresh Queues"
[node name="HSplitContainer" type="HSplitContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
[node name="QueuesTree" type="Tree" parent="HSplitContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 0.3
columns = 2
column_titles_visible = true
select_mode = 1
[node name="JobsVBox" type="VBoxContainer" parent="HSplitContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="JobsTree" type="Tree" parent="HSplitContainer/JobsVBox"]
layout_mode = 2
size_flags_vertical = 3
columns = 3
column_titles_visible = true
select_mode = 1
[node name="JobDetailsText" type="TextEdit" parent="HSplitContainer/JobsVBox"]
layout_mode = 2
size_flags_vertical = 3
editable = false
[connection signal="item_selected" from="HSplitContainer/QueuesTree" to="." method="_on_queues_tree_item_selected"]
[connection signal="item_selected" from="HSplitContainer/JobsVBox/JobsTree" to="." method="_on_jobs_tree_item_selected"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

View File

@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://48jxifffcg1y"
path="res://.godot/imported/icon.png-bff46c8d6bc629df6e2505dfe04b11b4.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/GedisQueue/icon.png"
dest_files=["res://.godot/imported/icon.png-bff46c8d6bc629df6e2505dfe04b11b4.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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

View File

@ -1,7 +0,0 @@
[plugin]
name="GedisQueue"
description="A BullMQ-like queue system for Godot, backed by Gedis."
author="NodotProject"
version="0.1.9"
script="plugin.gd"

View File

@ -1,87 +0,0 @@
@tool
extends EditorPlugin
const GedisQueueDebuggerPanel = preload("res://addons/GedisQueue/debugger/gedis_queue_debugger_panel.tscn")
var queue_debugger_plugin
var queue_panel
var dashboard
func _enter_tree():
queue_debugger_plugin = GedisQueueDebuggerPlugin.new()
add_debugger_plugin(queue_debugger_plugin)
var timer = Timer.new()
timer.wait_time = 1
timer.timeout.connect(_on_timer_timeout)
add_child(timer)
timer.start()
func _on_timer_timeout():
var editor_node = EditorInterface.get_base_control()
dashboard = editor_node.find_child("Gedis", true, false)
if dashboard:
var debugger = dashboard.plugin
if debugger:
var tab_container = dashboard.find_child("TabContainer", true, false)
if tab_container:
queue_panel = dashboard.find_child("Queue", true, false)
if !queue_panel:
queue_panel = GedisQueueDebuggerPanel.instantiate()
queue_panel.name = "Queue"
tab_container.add_child(queue_panel)
queue_panel.set_plugin(debugger)
var session_id = debugger.get_current_session_id()
if session_id != -1:
queue_debugger_plugin.set_queue_panel(session_id, queue_panel)
func _exit_tree():
if queue_debugger_plugin:
remove_debugger_plugin(queue_debugger_plugin)
queue_debugger_plugin = null
if queue_panel:
queue_panel.queue_free()
class GedisQueueDebuggerPlugin extends EditorDebuggerPlugin:
var queue_panels = {}
func set_queue_panel(session_id, panel):
queue_panels[session_id] = panel
func _has_capture(capture):
return capture == "gedis"
func _capture(message, data, session_id):
var parts = message.split(":")
var kind = parts[1] if parts.size() > 1 else ""
if session_id in queue_panels:
var queue_panel = queue_panels[session_id]
match kind:
"snapshot_data":
if queue_panel:
var snapshot = data[0]
var queues = {}
var jobs = []
for key in snapshot:
var value = snapshot[key]
if "job" in key:
var job_data = value["value"]
var key_parts = key.split(":")
if key_parts.size() > 2:
job_data["queue_name"] = key_parts[1]
jobs.append(job_data)
else:
var queue_name = key.replace("gedis_queue:", "").replace(":waiting", "").replace(":active", "")
if not queue_name in queues:
queues[queue_name] = []
queues[queue_name].append_array(value["value"])
queue_panel.update_queues(queues)
queue_panel.update_jobs(jobs)
return true
return false

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 B

View File

@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cim3uuxhxs3x4"
path="res://.godot/imported/Blank.png-e20d4d6a47b08c47420d456c717ca61f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/PaletteTools/Images/Blank.png"
dest_files=["res://.godot/imported/Blank.png-e20d4d6a47b08c47420d456c717ca61f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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.

Before

Width:  |  Height:  |  Size: 146 B

View File

@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bv0rua34hrcsa"
path="res://.godot/imported/Plus.png-e4e5eebf42f899e6f55e4d0ffe60c50a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/PaletteTools/Images/Plus.png"
dest_files=["res://.godot/imported/Plus.png-e4e5eebf42f899e6f55e4d0ffe60c50a.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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.

Before

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b6f62u2b00rdv"
path="res://.godot/imported/checked.png-d81f80362f72567a0fcdbf6bf355283e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/PaletteTools/Images/checked.png"
dest_files=["res://.godot/imported/checked.png-d81f80362f72567a0fcdbf6bf355283e.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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.

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://blxftvivd7x5x"
path="res://.godot/imported/checked_4k.png-342c0f16a75c36f211dc8d6291dcbfde.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/PaletteTools/Images/checked_4k.png"
dest_files=["res://.godot/imported/checked_4k.png-342c0f16a75c36f211dc8d6291dcbfde.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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.

Before

Width:  |  Height:  |  Size: 170 B

View File

@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://duuedcimehb1f"
path="res://.godot/imported/spyglass.png-c8a5839ba62e6621291233abb45e1ee9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/PaletteTools/Images/spyglass.png"
dest_files=["res://.godot/imported/spyglass.png-c8a5839ba62e6621291233abb45e1ee9.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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.

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://585mb8p87t5x"
path="res://.godot/imported/unchecked.png-5249d27fb8a908d32590159e278c99c8.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/PaletteTools/Images/unchecked.png"
dest_files=["res://.godot/imported/unchecked.png-5249d27fb8a908d32590159e278c99c8.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bpiotwvx0p7kp"
path="res://.godot/imported/unchecked_4k.png-4470f9e94d7b68e36513e292443cc559.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/PaletteTools/Images/unchecked_4k.png"
dest_files=["res://.godot/imported/unchecked_4k.png-4470f9e94d7b68e36513e292443cc559.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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

View File

@ -1,19 +0,0 @@
Copyright (c) 2023 RancidMilk
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

@ -1,63 +0,0 @@
# Godot 4 Palette Tools
This is a small addon that allows for easy color palette integration in Godot 4. Quickly Create/Download palettes. Load/Save for use any time. Comes with the ability to quickly switch the editor's color-picker swatches as well as a custom color picker that can switch between palettes on demand. Download option currently supports Lospec palettes.
## Setup
### Install Methods
#### Recommended:
Open the *AssetLib* tab in the editor and search for Palette Tools.
#### Manual Method:
From either the [project repo](https://github.com/RancidMilkGames/GodotPaletteTools) or [asset library website](https://godotengine.org/asset-library/asset) copy the "PaletteTools" folder to your project's "addons" folder. If this is your first time using a Godot plugin/addon, and you don't have an addons folder, make a folder named "addons" in the root of your project.
### Activate
Godot will automatically activate the addon if you used the previously mentioned recommended method. If done manually, go to `Project Settings->Plugins` to activate it.
<table>
<thead>
<tr>
<td align="center">
:warning: Notice
</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<ul>
<li>Plugin will not be immediately noticeable. It will appear in the right-side dock with Inspector/Node/History. You may need to either expand the dock or use the arrows to access it.</li>
</ul>
</td>
</tr>
</tbody>
</table>
## USE
* **Custom Palette Picker**: Replaces the default color picker with an extended one that can switch between palettes. For special use cases and not thoroughly tested so by default it's set to off.
* **Saved Palettes**: List of previously saved palettes. To load or delete a palette, select it in the list and then press the corresponding button.
* **Get from Lospec**: Either enter a Lospec palette URL or browse through popular palettes. The plugin will automatically keep track of the palette name, and it's author for referencing or crediting.
* **Palette Preview**: Preview/editor for palettes. Click the plus sign to add a color to the palette. Existing colors can be edited by clicking on them, or removed with the red close button in the top right corner of the button. The save palette button will store the palette.
* **Replace Editor Swatches**: If you have an eligible palette, an option will appear that allows you to save it to the editor's swatches, which can be accessed from both the default color picker and the extended one.
## Tips
* You can copy the "res://addons/PaletteTools/color_presets.cfg" file to move saved palettes/settings between projects
## Future
I consider this plugin mostly done, but still use it regularly, so if Godot has any breaking changes that affect it, those will probably be addressed. It also might get the occasional new feature. Some current thoughts are:
- [ ] Theme integration of some sort
- [ ] The ability to mass convert one color in the project to another
## Notes
* Tested on Godot 4.2. If you'd like to run it on an earlier version of Godot 4, [this is a link](https://github.com/RancidMilkGames/GodotPaletteTools/tree/47c09b8d6e43a0acc0380a1344a4b2282f95d49b) to the last commit before the 4.2 changes.
* Custom Color picker won't work when editing resources. I'm not sure if this is possible to implement in Godot's current state.

View File

@ -1,9 +0,0 @@
[gd_resource type="LabelSettings" format=3 uid="uid://d21bg0y0svxi3"]
[resource]
font_size = 24
outline_size = 4
outline_color = Color(0, 0, 0, 1)
shadow_size = 6
shadow_color = Color(0.714494, 0.571648, 0.404247, 1)
shadow_offset = Vector2(0, 0)

View File

@ -1,8 +0,0 @@
[gd_resource type="LabelSettings" format=3 uid="uid://k60qt3dd7ov2"]
[resource]
font_size = 50
font_color = Color(0.545098, 0.333333, 0.501961, 1)
outline_color = Color(0.772549, 0.8, 0.721569, 1)
shadow_color = Color(0.760784, 0.552941, 0.458824, 1)
shadow_offset = Vector2(0, 0)

View File

@ -1,83 +0,0 @@
[gd_scene load_steps=5 format=3 uid="uid://xj8jb4l74mf"]
[ext_resource type="Script" path="res://addons/PaletteTools/Scripts/browse_palette_preview.gd" id="1_j460a"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g4ruc"]
content_margin_left = 25.0
content_margin_top = 25.0
content_margin_right = 25.0
content_margin_bottom = 25.0
bg_color = Color(0.435294, 0.403922, 0.462745, 1)
border_width_left = 10
border_width_top = 10
border_width_right = 10
border_width_bottom = 10
border_color = Color(0, 0, 0, 1)
border_blend = true
corner_radius_top_left = 25
corner_radius_top_right = 25
corner_radius_bottom_right = 25
corner_radius_bottom_left = 25
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qogr7"]
content_margin_left = 7.0
content_margin_top = 4.0
content_margin_right = 7.0
content_margin_bottom = 4.0
bg_color = Color(0.713726, 0.572549, 0.403922, 1)
border_width_left = 5
border_width_top = 5
border_width_right = 5
border_width_bottom = 5
border_color = Color(0.00820202, 0.00820203, 0.00820202, 1)
border_blend = true
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
[sub_resource type="LabelSettings" id="LabelSettings_nkmub"]
font_size = 24
font_color = Color(0, 0, 0, 1)
outline_size = 4
outline_color = Color(0.552941, 0.384314, 0.407843, 1)
[node name="PanelContainer" type="PanelContainer" node_paths=PackedStringArray("info_label", "color_container")]
offset_top = 54.0
offset_right = 1152.0
offset_bottom = 54.0
theme_override_styles/panel = SubResource("StyleBoxFlat_g4ruc")
script = ExtResource("1_j460a")
info_label = NodePath("BrowsePalettePreview/HBoxContainer/Label")
color_container = NodePath("BrowsePalettePreview/MarginContainer/HFlowContainer")
[node name="BrowsePalettePreview" type="VBoxContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 10
[node name="HBoxContainer" type="HBoxContainer" parent="BrowsePalettePreview"]
layout_mode = 2
[node name="PaletteSelect" type="Button" parent="BrowsePalettePreview/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 0
mouse_default_cursor_shape = 2
theme_override_colors/font_color = Color(0, 0, 0, 1)
theme_override_styles/normal = SubResource("StyleBoxFlat_qogr7")
text = "Import"
[node name="Label" type="Label" parent="BrowsePalettePreview/HBoxContainer"]
layout_mode = 2
text = "Palette name by user"
label_settings = SubResource("LabelSettings_nkmub")
[node name="MarginContainer" type="MarginContainer" parent="BrowsePalettePreview"]
layout_mode = 2
[node name="HFlowContainer" type="HFlowContainer" parent="BrowsePalettePreview/MarginContainer"]
layout_mode = 2
theme_override_constants/h_separation = 0
theme_override_constants/v_separation = 0
[connection signal="pressed" from="BrowsePalettePreview/HBoxContainer/PaletteSelect" to="." method="_on_palette_select_pressed"]

View File

@ -1,70 +0,0 @@
[gd_scene load_steps=4 format=3 uid="uid://157npdwcov77"]
[ext_resource type="Script" path="res://addons/PaletteTools/Scripts/color_sample.gd" id="1_8rxjv"]
[ext_resource type="Texture2D" path="res://addons/PaletteTools/Images/Blank.png" id="1_jr35n"]
[sub_resource type="LabelSettings" id="LabelSettings_s1xg4"]
font_size = 20
font_color = Color(0.113725, 0.113725, 0.113725, 1)
[node name="ColorSample" type="MarginContainer" node_paths=PackedStringArray("color_picker_button", "remove_button", "remove_button_text")]
custom_minimum_size = Vector2(50, 50)
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -12.5
offset_top = -12.5
offset_right = 12.5
offset_bottom = 12.5
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
script = ExtResource("1_8rxjv")
color_picker_button = NodePath("Color")
remove_button = NodePath("Color/Remove")
remove_button_text = NodePath("Color/Remove/Label")
[node name="Color" type="ColorPickerButton" parent="."]
custom_minimum_size = Vector2(50, 50)
layout_mode = 2
mouse_default_cursor_shape = 2
[node name="Remove" type="TextureButton" parent="Color"]
self_modulate = Color(0.980392, 0.513726, 0.47451, 1)
custom_minimum_size = Vector2(10, 10)
layout_mode = 1
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -14.0
offset_top = 4.0
offset_right = -4.0
offset_bottom = 14.0
grow_horizontal = 0
mouse_default_cursor_shape = 2
texture_normal = ExtResource("1_jr35n")
ignore_texture_size = true
stretch_mode = 4
[node name="Label" type="Label" parent="Color/Remove"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -20.0
offset_top = -13.0
offset_right = 20.0
offset_bottom = 13.0
grow_horizontal = 2
grow_vertical = 2
text = "X"
label_settings = SubResource("LabelSettings_s1xg4")
horizontal_alignment = 1
vertical_alignment = 1

View File

@ -1,20 +0,0 @@
[gd_scene format=3 uid="uid://c41c3pd6clxuu"]
[node name="CustomPalette" type="ColorRect"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
color = Color(1, 1, 1, 0.996078)
[node name="Button" type="Button" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
flat = true

View File

@ -1,140 +0,0 @@
[gd_scene load_steps=10 format=3 uid="uid://te84lilqmwp0"]
[ext_resource type="Script" path="res://addons/PaletteTools/Scripts/custom_picker.gd" id="1_oxy04"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_igykn"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.745098, 0.584314, 0.360784, 1)
corner_radius_top_left = 10
corner_radius_top_right = 10
corner_radius_bottom_right = 10
corner_radius_bottom_left = 10
corner_detail = 6
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_etl6w"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.603922, 0.309804, 0.313726, 1)
corner_radius_top_left = 10
corner_radius_top_right = 10
corner_radius_bottom_right = 10
corner_radius_bottom_left = 10
corner_detail = 6
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jt85f"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.772549, 0.8, 0.721569, 1)
corner_radius_top_left = 10
corner_radius_top_right = 10
corner_radius_bottom_right = 10
corner_radius_bottom_left = 10
corner_detail = 6
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1lt0k"]
content_margin_left = 4.0
content_margin_top = 0.0
content_margin_right = 4.0
content_margin_bottom = 0.0
bg_color = Color(0.407843, 0.67451, 0.662745, 1)
corner_radius_top_left = 10
corner_radius_top_right = 10
corner_radius_bottom_right = 10
corner_radius_bottom_left = 10
corner_detail = 6
[sub_resource type="Theme" id="Theme_8u2vs"]
VScrollBar/styles/grabber = SubResource("StyleBoxFlat_igykn")
VScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_etl6w")
VScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_jt85f")
VScrollBar/styles/scroll = SubResource("StyleBoxFlat_1lt0k")
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_jcc3n"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_t77tn"]
bg_color = Color(0.839216, 0.839216, 0.839216, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ykbik"]
bg_color = Color(0.839216, 0.839216, 0.839216, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[node name="CustomPicker" type="PopupPanel" node_paths=PackedStringArray("color_picker", "saved_palettes", "apply_palette_button")]
disable_3d = true
title = "Palette Tools Color-Picker"
size = Vector2i(1280, 2700)
visible = true
min_size = Vector2i(640, 1350)
theme = SubResource("Theme_8u2vs")
script = ExtResource("1_oxy04")
color_picker = NodePath("ScrollContainer/VBoxContainer/ColorPicker")
saved_palettes = NodePath("ScrollContainer/VBoxContainer/SavedPalettes")
apply_palette_button = NodePath("ScrollContainer/VBoxContainer/HBoxContainer/Button")
[node name="ScrollContainer" type="ScrollContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 4.0
offset_top = 4.0
offset_right = -4.0
offset_bottom = -4.0
grow_horizontal = 2
grow_vertical = 2
size_flags_vertical = 3
[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer"]
custom_minimum_size = Vector2(300, 600)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
alignment = 1
[node name="ColorPicker" type="ColorPicker" parent="ScrollContainer/VBoxContainer"]
layout_mode = 2
deferred_mode = true
picker_shape = 3
[node name="HBoxContainer" type="HBoxContainer" parent="ScrollContainer/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="ScrollContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Saved Palettes:
"
[node name="Spacer" type="Control" parent="ScrollContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Button" type="Button" parent="ScrollContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Applies palette to Swatches. Alternatively you can double click a palette or press enter with a palette highlighted"
text = "Apply to Swatches"
[node name="SavedPalettes" type="ItemList" parent="ScrollContainer/VBoxContainer"]
custom_minimum_size = Vector2(300, 0)
layout_mode = 2
size_flags_horizontal = 4
tooltip_text = "Double click on palette or select one and hit enter to apply it."
theme_override_colors/font_selected_color = Color(0.0862745, 0.0862745, 0.0862745, 1)
theme_override_styles/focus = SubResource("StyleBoxEmpty_jcc3n")
theme_override_styles/selected = SubResource("StyleBoxFlat_t77tn")
theme_override_styles/selected_focus = SubResource("StyleBoxFlat_ykbik")
auto_height = true
item_count = 1
same_column_width = true
item_0/text = "No Palettes"

View File

@ -1,647 +0,0 @@
[gd_scene load_steps=42 format=3 uid="uid://3m5qn5hayqy3"]
[ext_resource type="Script" path="res://addons/PaletteTools/Scripts/palette_tool.gd" id="1_coeap"]
[ext_resource type="Script" path="res://addons/PaletteTools/Scripts/get_from_url.gd" id="2_4p84m"]
[ext_resource type="PackedScene" uid="uid://x40sgfxkrskd" path="res://addons/PaletteTools/Scenes/plus_box.tscn" id="2_85d0j"]
[ext_resource type="PackedScene" uid="uid://157npdwcov77" path="res://addons/PaletteTools/Scenes/color_sample.tscn" id="2_fn217"]
[ext_resource type="Shader" path="res://addons/PaletteTools/Shaders/rainbow_shader.gdshader" id="3_gaxkd"]
[ext_resource type="Texture2D" path="res://addons/PaletteTools/Images/Plus.png" id="3_m16i6"]
[ext_resource type="Texture2D" path="res://addons/PaletteTools/Images/spyglass.png" id="4_7wo4d"]
[ext_resource type="LabelSettings" uid="uid://k60qt3dd7ov2" path="res://addons/PaletteTools/Resources/section_label_settings_contrast.tres" id="4_qjnhp"]
[ext_resource type="Texture2D" path="res://addons/PaletteTools/Images/checked_4k.png" id="6_iloov"]
[ext_resource type="Script" path="res://addons/PaletteTools/Scripts/browse_popup.gd" id="7_ob3x8"]
[ext_resource type="Texture2D" path="res://addons/PaletteTools/Images/unchecked_4k.png" id="7_rs6oc"]
[ext_resource type="Script" path="res://addons/PaletteTools/Scripts/loading_palettes_screen.gd" id="9_xvc3k"]
[ext_resource type="Script" path="res://addons/PaletteTools/Scripts/alert_popup_panel.gd" id="11_hklnm"]
[ext_resource type="PackedScene" uid="uid://xj8jb4l74mf" path="res://addons/PaletteTools/Scenes/browse_palette_preview.tscn" id="13_a5syi"]
[sub_resource type="Theme" id="Theme_igl82"]
MarginContainer/constants/margin_bottom = 3
MarginContainer/constants/margin_left = 3
MarginContainer/constants/margin_right = 3
MarginContainer/constants/margin_top = 3
TextEdit/font_sizes/font_size = 16
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7qvmi"]
bg_color = Color(0.713726, 0.572549, 0.403922, 1)
[sub_resource type="Theme" id="Theme_8bgoc"]
PanelContainer/styles/panel = SubResource("StyleBoxFlat_7qvmi")
[sub_resource type="ShaderMaterial" id="ShaderMaterial_g1sn2"]
shader = ExtResource("3_gaxkd")
shader_parameter/strength = 0.25
shader_parameter/speed = 0.25
shader_parameter/angle = 0.0
[sub_resource type="LabelSettings" id="LabelSettings_yeikm"]
font_size = 80
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_u67lc"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1qaos"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_kl4yb"]
bg_color = Color(0.839216, 0.839216, 0.839216, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2u8j5"]
bg_color = Color(0.839216, 0.839216, 0.839216, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g30ga"]
bg_color = Color(0.929412, 0.882353, 0.619608, 0.823529)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_c6ft6"]
bg_color = Color(0.92549, 0.85098, 0.596078, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_b113t"]
bg_color = Color(0.415686, 0.415686, 0.415686, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_kk1ek"]
[sub_resource type="Theme" id="Theme_t5g4x"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4b8x1"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.890196, 0.890196, 0.890196, 0.6)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_inmej"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.1, 0.1, 0.1, 0.6)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_u8dak"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.0235294, 0.0235294, 0.0235294, 0.6)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 5
[sub_resource type="LabelSettings" id="LabelSettings_rgr51"]
font_size = 40
[sub_resource type="LabelSettings" id="LabelSettings_jnb0d"]
font_size = 40
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6d60a"]
content_margin_left = 5.0
content_margin_right = 5.0
bg_color = Color(0.890196, 0.337255, 0.341176, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_y2upq"]
bg_color = Color(0.768627, 0.764706, 0.509804, 1)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.121569, 0.121569, 0.121569, 1)
corner_radius_top_left = 10
corner_radius_top_right = 10
corner_radius_bottom_right = 10
corner_radius_bottom_left = 10
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7ta0s"]
bg_color = Color(0.25098, 0.25098, 0.25098, 1)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.933333, 0.772549, 0.509804, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ceb4a"]
bg_color = Color(0.333333, 0.333333, 0.333333, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="LabelSettings" id="LabelSettings_ww1rl"]
font_size = 40
font_color = Color(0.0823529, 0.0823529, 0.0823529, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tdaa4"]
bg_color = Color(0.172549, 0.172549, 0.172549, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="LabelSettings" id="LabelSettings_0flab"]
font_size = 32
[sub_resource type="LabelSettings" id="LabelSettings_26hsa"]
font_size = 100
font_color = Color(0.00820202, 0.00820203, 0.00820202, 1)
[node name="Palette Tools" type="ScrollContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
[node name="Colors" type="PanelContainer" parent="." node_paths=PackedStringArray("http", "url", "color_preview", "editor_swatch_save", "restart_editor", "alert", "p_name_text", "p_author_text", "saved_palettes", "clear_preview_button", "col_pick_popup", "browse_popup_panel", "custom_picker_check_box")]
layout_mode = 2
size_flags_horizontal = 3
theme = SubResource("Theme_igl82")
script = ExtResource("1_coeap")
http = NodePath("HTTPRequest")
url = NodePath("Palette/Search/LineEdit")
color_preview = NodePath("Palette/ColorPreview")
editor_swatch_save = NodePath("Palette/Container/VBoxContainer/SaveToEditor")
restart_editor = NodePath("Palette/Container/VBoxContainer/RestartEditor")
alert = NodePath("AlertPopup")
p_name_text = NodePath("Palette/Info/PaletteName/LineEdit")
p_author_text = NodePath("Palette/Info/Author/LineEdit")
saved_palettes = NodePath("Palette/SavedPalettes")
clear_preview_button = NodePath("Palette/HBoxContainer/Clear")
col_pick_popup = NodePath("ColorPickerPopup")
browse_popup_panel = NodePath("BrowsePopup")
add_color_scene = ExtResource("2_85d0j")
color_sample = ExtResource("2_fn217")
custom_picker_check_box = NodePath("Palette/CustomPicker/CustomPickerCheckBox")
[node name="HTTPRequest" type="HTTPRequest" parent="Colors" node_paths=PackedStringArray("colors")]
download_file = "res://addons/PaletteTools/temp_palette.txt"
script = ExtResource("2_4p84m")
colors = NodePath("..")
[node name="Palette" type="VBoxContainer" parent="Colors"]
layout_mode = 2
theme = SubResource("Theme_8bgoc")
theme_override_constants/separation = 5
alignment = 1
[node name="Title" type="Label" parent="Colors/Palette"]
material = SubResource("ShaderMaterial_g1sn2")
layout_mode = 2
size_flags_horizontal = 4
text = "Palette Tools"
label_settings = SubResource("LabelSettings_yeikm")
horizontal_alignment = 1
[node name="CustomPicker" type="HBoxContainer" parent="Colors/Palette"]
layout_mode = 2
alignment = 1
[node name="CustomPickerCheckBox" type="CheckBox" parent="Colors/Palette/CustomPicker"]
layout_mode = 2
tooltip_text = "Toggle an extended color picker that can switch palettes"
theme_override_font_sizes/font_size = 30
theme_override_icons/checked = ExtResource("6_iloov")
theme_override_icons/unchecked = ExtResource("7_rs6oc")
icon_alignment = 1
expand_icon = true
[node name="RichTextLabel" type="RichTextLabel" parent="Colors/Palette/CustomPicker"]
layout_mode = 2
size_flags_vertical = 4
theme_override_font_sizes/normal_font_size = 40
theme_override_styles/normal = SubResource("StyleBoxEmpty_u67lc")
bbcode_enabled = true
text = "Custom Palette Picker [color=9a4f50](beta)[/color]"
fit_content = true
scroll_active = false
autowrap_mode = 0
[node name="PaletteListTitle" type="Label" parent="Colors/Palette"]
layout_mode = 2
text = "--- Saved Palettes: ---"
label_settings = ExtResource("4_qjnhp")
horizontal_alignment = 1
vertical_alignment = 1
[node name="SavedPalettes" type="ItemList" parent="Colors/Palette"]
layout_mode = 2
theme_override_colors/font_selected_color = Color(0.0862745, 0.0862745, 0.0862745, 1)
theme_override_font_sizes/font_size = 30
theme_override_styles/focus = SubResource("StyleBoxEmpty_1qaos")
theme_override_styles/selected = SubResource("StyleBoxFlat_kl4yb")
theme_override_styles/selected_focus = SubResource("StyleBoxFlat_2u8j5")
auto_height = true
item_count = 1
same_column_width = true
item_0/text = "No Palettes"
[node name="IO" type="HBoxContainer" parent="Colors/Palette"]
layout_mode = 2
[node name="New" type="TextureButton" parent="Colors/Palette/IO"]
visible = false
self_modulate = Color(0.720467, 0.720468, 0.720467, 1)
texture_filter = 1
custom_minimum_size = Vector2(80, 0)
layout_mode = 2
tooltip_text = "Make a new palette. Erases current preview"
mouse_default_cursor_shape = 2
texture_normal = ExtResource("3_m16i6")
texture_pressed = ExtResource("3_m16i6")
texture_hover = ExtResource("3_m16i6")
ignore_texture_size = true
stretch_mode = 5
[node name="Load" type="Button" parent="Colors/Palette/IO"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "Load selected Saved Palette"
mouse_default_cursor_shape = 2
theme_override_font_sizes/font_size = 30
text = "Load"
[node name="Delete" type="Button" parent="Colors/Palette/IO"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "Delete selected Saved Palette"
mouse_default_cursor_shape = 2
theme_override_font_sizes/font_size = 30
text = "Delete"
[node name="SearchTitle" type="Label" parent="Colors/Palette"]
layout_mode = 2
text = "--- Get from Lospec: ---"
label_settings = ExtResource("4_qjnhp")
horizontal_alignment = 1
vertical_alignment = 1
[node name="Search" type="HBoxContainer" parent="Colors/Palette"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Colors/Palette/Search"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_font_sizes/font_size = 30
placeholder_text = "Lospec Palette URL"
expand_to_text_length = true
caret_blink = true
caret_blink_interval = 0.5
[node name="Search" type="Button" parent="Colors/Palette/Search"]
texture_filter = 1
custom_minimum_size = Vector2(80, 4)
layout_mode = 2
mouse_default_cursor_shape = 2
theme_override_font_sizes/font_size = 40
theme_override_styles/normal = SubResource("StyleBoxFlat_g30ga")
theme_override_styles/hover = SubResource("StyleBoxFlat_c6ft6")
theme_override_styles/pressed = SubResource("StyleBoxFlat_b113t")
theme_override_styles/focus = SubResource("StyleBoxEmpty_kk1ek")
icon = ExtResource("4_7wo4d")
icon_alignment = 1
expand_icon = true
[node name="BrowsePalettesButton" type="Button" parent="Colors/Palette"]
layout_mode = 2
tooltip_text = "Browse the most popular Lospec palettes"
mouse_default_cursor_shape = 2
theme = SubResource("Theme_t5g4x")
theme_override_colors/font_color = Color(0, 0, 0, 1)
theme_override_colors/font_pressed_color = Color(0.91664, 0.91664, 0.91664, 1)
theme_override_colors/font_hover_color = Color(0.813258, 0.813258, 0.813258, 1)
theme_override_font_sizes/font_size = 30
theme_override_styles/normal = SubResource("StyleBoxFlat_4b8x1")
theme_override_styles/hover = SubResource("StyleBoxFlat_inmej")
theme_override_styles/pressed = SubResource("StyleBoxFlat_u8dak")
text = "Browse Palettes"
[node name="PreviewTitle" type="Label" parent="Colors/Palette"]
layout_mode = 2
text = "--- Palette Preview: ---"
label_settings = ExtResource("4_qjnhp")
horizontal_alignment = 1
vertical_alignment = 1
[node name="Info" type="VBoxContainer" parent="Colors/Palette"]
layout_mode = 2
alignment = 1
[node name="PaletteName" type="HBoxContainer" parent="Colors/Palette/Info"]
layout_mode = 2
[node name="Label" type="Label" parent="Colors/Palette/Info/PaletteName"]
layout_mode = 2
text = "Palette Name:"
label_settings = SubResource("LabelSettings_rgr51")
[node name="LineEdit" type="LineEdit" parent="Colors/Palette/Info/PaletteName"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_font_sizes/font_size = 30
placeholder_text = "Name"
caret_blink = true
caret_blink_interval = 0.5
[node name="Author" type="HBoxContainer" parent="Colors/Palette/Info"]
layout_mode = 2
[node name="Label" type="Label" parent="Colors/Palette/Info/Author"]
layout_mode = 2
text = "Author:"
label_settings = SubResource("LabelSettings_jnb0d")
[node name="LineEdit" type="LineEdit" parent="Colors/Palette/Info/Author"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_font_sizes/font_size = 30
placeholder_text = "Name"
caret_blink = true
caret_blink_interval = 0.5
[node name="ColorPreview" type="HFlowContainer" parent="Colors/Palette"]
layout_mode = 2
size_flags_vertical = 3
alignment = 1
[node name="HBoxContainer" type="HBoxContainer" parent="Colors/Palette"]
layout_mode = 2
theme_override_constants/separation = 10
alignment = 1
[node name="Save" type="Button" parent="Colors/Palette/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "Save the current preview to Saved Palettes"
mouse_default_cursor_shape = 2
theme_override_font_sizes/font_size = 30
text = "Save Palette"
[node name="Clear" type="Button" parent="Colors/Palette/HBoxContainer"]
visible = false
layout_mode = 2
tooltip_text = "Save the current preview to Saved Palettes"
mouse_default_cursor_shape = 2
theme_override_colors/font_color = Color(0.125911, 0.125911, 0.125911, 1)
theme_override_font_sizes/font_size = 30
theme_override_styles/normal = SubResource("StyleBoxFlat_6d60a")
text = "Clear Preview"
[node name="Container" type="MarginContainer" parent="Colors/Palette"]
layout_mode = 2
[node name="ColorRect" type="Panel" parent="Colors/Palette/Container"]
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_y2upq")
[node name="VBoxContainer" type="VBoxContainer" parent="Colors/Palette/Container"]
layout_mode = 2
[node name="SaveToEditor" type="VBoxContainer" parent="Colors/Palette/Container/VBoxContainer"]
visible = false
layout_mode = 2
size_flags_horizontal = 4
alignment = 1
[node name="Spacer" type="Control" parent="Colors/Palette/Container/VBoxContainer/SaveToEditor"]
layout_mode = 2
[node name="SaveToEditorButton" type="Button" parent="Colors/Palette/Container/VBoxContainer/SaveToEditor"]
layout_mode = 2
size_flags_horizontal = 4
tooltip_text = "Replaces the current editor swatches with current palette"
mouse_default_cursor_shape = 2
theme_override_colors/font_color = Color(0.933333, 0.772549, 0.509804, 1)
theme_override_font_sizes/font_size = 30
theme_override_styles/normal = SubResource("StyleBoxFlat_7ta0s")
theme_override_styles/hover = SubResource("StyleBoxFlat_ceb4a")
text = "*Save to Editor's Swatches"
[node name="Label" type="Label" parent="Colors/Palette/Container/VBoxContainer/SaveToEditor"]
layout_mode = 2
text = "*Note: Editor must be restarted
for changes to take effect"
label_settings = SubResource("LabelSettings_ww1rl")
horizontal_alignment = 1
[node name="RestartEditor" type="VBoxContainer" parent="Colors/Palette/Container/VBoxContainer"]
visible = false
layout_mode = 2
size_flags_horizontal = 4
alignment = 1
[node name="RestartEditorButton" type="Button" parent="Colors/Palette/Container/VBoxContainer/RestartEditor"]
layout_mode = 2
size_flags_horizontal = 4
tooltip_text = "Replaces the current editor swatches with current palette"
theme_override_colors/font_color = Color(1, 0.435294, 0.364706, 1)
theme_override_font_sizes/font_size = 30
theme_override_styles/hover = SubResource("StyleBoxFlat_tdaa4")
text = "*Restart To finalize changes?"
[node name="Spacer" type="Control" parent="Colors/Palette/Container/VBoxContainer/RestartEditor"]
layout_mode = 2
[node name="AlertPopup" type="PopupPanel" parent="Colors" node_paths=PackedStringArray("alert_text_label")]
disable_3d = true
title = "Alert"
initial_position = 1
size = Vector2i(500, 300)
script = ExtResource("11_hklnm")
alert_text_label = NodePath("Control/Label")
[node name="Control" type="Control" parent="Colors/AlertPopup"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 4.0
offset_top = 4.0
offset_right = -4.0
offset_bottom = -4.0
grow_horizontal = 2
grow_vertical = 2
[node name="Label" type="Label" parent="Colors/AlertPopup/Control"]
custom_minimum_size = Vector2(400, 200)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -0.5
offset_top = -13.0
offset_right = 0.5
offset_bottom = 13.0
grow_horizontal = 2
grow_vertical = 2
text = "Alert"
label_settings = SubResource("LabelSettings_0flab")
horizontal_alignment = 1
vertical_alignment = 1
autowrap_mode = 2
[node name="CloseButton" type="Button" parent="Colors/AlertPopup/Control"]
layout_mode = 1
anchors_preset = 7
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -25.0
offset_top = -42.0
offset_right = 25.0
offset_bottom = -11.0
grow_horizontal = 2
grow_vertical = 0
size_flags_horizontal = 6
size_flags_vertical = 6
mouse_default_cursor_shape = 2
text = "Close"
[node name="ColorPickerPopup" type="PopupPanel" parent="Colors"]
disable_3d = true
sdf_oversize = 0
initial_position = 1
size = Vector2i(306, 514)
unresizable = false
[node name="ColorPicker" type="ColorPicker" parent="Colors/ColorPickerPopup"]
offset_left = 4.0
offset_top = 4.0
offset_right = 302.0
offset_bottom = 510.0
picker_shape = 3
can_add_swatches = false
presets_visible = false
[node name="BrowsePopup" type="Window" parent="Colors" node_paths=PackedStringArray("colors", "browse_http", "preview_container", "alert_popup", "loading_screen")]
disable_3d = true
title = "Browse Lospec Palettes"
initial_position = 2
size = Vector2i(1000, 600)
visible = false
wrap_controls = true
script = ExtResource("7_ob3x8")
colors = NodePath("..")
browse_http = NodePath("HTTPRequest")
browse_preview_scene = ExtResource("13_a5syi")
preview_container = NodePath("MarginContainer/VBoxContainer/ScrollContainer/VBoxContainer")
alert_popup = NodePath("../AlertPopup")
loading_screen = NodePath("MarginContainer/Loading")
[node name="MarginContainer" type="MarginContainer" parent="Colors/BrowsePopup"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 25
theme_override_constants/margin_top = 25
theme_override_constants/margin_right = 25
theme_override_constants/margin_bottom = 25
[node name="VBoxContainer" type="VBoxContainer" parent="Colors/BrowsePopup/MarginContainer"]
layout_mode = 2
[node name="ScrollContainer" type="ScrollContainer" parent="Colors/BrowsePopup/MarginContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="VBoxContainer" type="VBoxContainer" parent="Colors/BrowsePopup/MarginContainer/VBoxContainer/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="LoadMore" type="Button" parent="Colors/BrowsePopup/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "Load More"
[node name="Loading" type="ColorRect" parent="Colors/BrowsePopup/MarginContainer" node_paths=PackedStringArray("loading_text")]
layout_mode = 2
color = Color(0.878431, 0.878431, 0.878431, 1)
script = ExtResource("9_xvc3k")
loading_text = NodePath("Label")
[node name="Label" type="Label" parent="Colors/BrowsePopup/MarginContainer/Loading"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -191.0
offset_top = -68.5
offset_right = 191.0
offset_bottom = 68.5
grow_horizontal = 2
grow_vertical = 2
text = "Loading"
label_settings = SubResource("LabelSettings_26hsa")
horizontal_alignment = 1
vertical_alignment = 1
[node name="HTTPRequest" type="HTTPRequest" parent="Colors/BrowsePopup"]
[connection signal="request_completed" from="Colors/HTTPRequest" to="Colors/HTTPRequest" method="_on_request_completed"]
[connection signal="toggled" from="Colors/Palette/CustomPicker/CustomPickerCheckBox" to="Colors" method="_on_custom_picker_check_box_toggled"]
[connection signal="item_activated" from="Colors/Palette/SavedPalettes" to="Colors" method="_on_saved_palettes_item_activated"]
[connection signal="pressed" from="Colors/Palette/IO/New" to="Colors" method="_on_new_palette_pressed"]
[connection signal="pressed" from="Colors/Palette/IO/Load" to="Colors" method="_on_load_palette_pressed"]
[connection signal="pressed" from="Colors/Palette/IO/Delete" to="Colors" method="_on_delete_palette_pressed"]
[connection signal="text_submitted" from="Colors/Palette/Search/LineEdit" to="Colors" method="_on_line_edit_text_submitted"]
[connection signal="pressed" from="Colors/Palette/Search/Search" to="Colors" method="_on_search_pressed"]
[connection signal="pressed" from="Colors/Palette/BrowsePalettesButton" to="Colors" method="_on_browse_palettes_button_pressed"]
[connection signal="pressed" from="Colors/Palette/HBoxContainer/Save" to="Colors" method="_on_save_palette_pressed"]
[connection signal="pressed" from="Colors/Palette/HBoxContainer/Clear" to="Colors" method="_on_clear_pressed"]
[connection signal="pressed" from="Colors/Palette/Container/VBoxContainer/SaveToEditor/SaveToEditorButton" to="Colors" method="_on_save_to_editor_button_pressed"]
[connection signal="pressed" from="Colors/Palette/Container/VBoxContainer/RestartEditor/RestartEditorButton" to="Colors" method="_on_restart_editor_pressed"]
[connection signal="close_requested" from="Colors/AlertPopup" to="Colors/AlertPopup" method="_on_close_requested"]
[connection signal="pressed" from="Colors/AlertPopup/Control/CloseButton" to="Colors/AlertPopup" method="_on_close_button_pressed"]
[connection signal="color_changed" from="Colors/ColorPickerPopup/ColorPicker" to="Colors" method="_on_color_picker_color_changed"]
[connection signal="close_requested" from="Colors/BrowsePopup" to="Colors/BrowsePopup" method="_on_close_requested"]
[connection signal="focus_exited" from="Colors/BrowsePopup" to="Colors/BrowsePopup" method="_on_focus_exited"]
[connection signal="size_changed" from="Colors/BrowsePopup" to="Colors/BrowsePopup" method="_on_size_changed"]
[connection signal="visibility_changed" from="Colors/BrowsePopup" to="Colors/BrowsePopup" method="_on_visibility_changed"]
[connection signal="pressed" from="Colors/BrowsePopup/MarginContainer/VBoxContainer/LoadMore" to="Colors/BrowsePopup" method="_on_load_more_pressed"]
[connection signal="visibility_changed" from="Colors/BrowsePopup/MarginContainer/Loading" to="Colors/BrowsePopup/MarginContainer/Loading" method="_on_visibility_changed"]
[connection signal="request_completed" from="Colors/BrowsePopup/HTTPRequest" to="Colors/BrowsePopup" method="_on_http_request_completed"]

View File

@ -1,31 +0,0 @@
[gd_scene load_steps=2 format=3 uid="uid://x40sgfxkrskd"]
[ext_resource type="Texture2D" path="res://addons/PaletteTools/Images/Plus.png" id="1_o5f0x"]
[node name="ColorSample" type="MarginContainer"]
custom_minimum_size = Vector2(50, 50)
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -12.5
offset_top = -12.5
offset_right = 12.5
offset_bottom = 12.5
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
[node name="Color" type="TextureButton" parent="."]
texture_filter = 1
custom_minimum_size = Vector2(40, 40)
layout_mode = 2
tooltip_text = "Add color to palette"
mouse_default_cursor_shape = 2
texture_normal = ExtResource("1_o5f0x")
ignore_texture_size = true
stretch_mode = 5

View File

@ -1,17 +0,0 @@
@tool
extends PopupPanel
@export var alert_text_label: Label
func alert(message: String) -> void:
alert_text_label.text = message
show()
func _on_close_button_pressed() -> void:
hide()
func _on_close_requested() -> void:
hide()

View File

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

View File

@ -1,14 +0,0 @@
@tool
extends Control
const Palette := preload("res://addons/PaletteTools/Scripts/palette_tool.gd")
@export var info_label: Label
@export var color_container: Control
var colors: Palette
var palette_obj: Dictionary
func _on_palette_select_pressed() -> void:
colors.import_palette_from_browse(palette_obj)

View File

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

View File

@ -1,94 +0,0 @@
@tool
extends Window
const Palette := preload("res://addons/PaletteTools/Scripts/palette_tool.gd")
const Alert := preload("res://addons/PaletteTools/Scripts/alert_popup_panel.gd")
const BrowsePreview := preload("res://addons/PaletteTools/Scripts/browse_palette_preview.gd")
@export var colors: Palette
@export var browse_http: HTTPRequest
@export var browse_preview_scene: PackedScene
@export var preview_container: Control
@export var alert_popup: Alert
@export var loading_screen: Control
var current_page: int = 0
var close_delay: int = 4
func get_palette_list() -> void:
browse_http.request(
"https://lospec.com/palette-list/load?colorNumberFilterType=any&colorNumber=8&page="
+ str(current_page)
+ "&tag=&sortingType=downloads"
)
current_page += 1
func display_preview(palette_obj: Dictionary) -> void:
var prev: BrowsePreview = browse_preview_scene.instantiate()
prev.colors = colors
prev.palette_obj = palette_obj
prev.info_label.text = palette_obj.name + " by: " + palette_obj.author
preview_container.add_child(prev)
for col in palette_obj.colors:
var new_color := ColorRect.new()
new_color.custom_minimum_size = Vector2(75, 75)
new_color.color = Color.from_string(col, Color.WHITE)
prev.color_container.add_child(new_color)
func _on_close_button_pressed() -> void:
visible = false
func _on_http_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
var html_text := body.get_string_from_utf8()
var json := JSON.new()
var err := json.parse(html_text)
if err != OK:
alert_popup.alert("Error getting palettes")
return
loading_screen.visible = false
for pal in json.data.palettes:
var user := "Lospec"
var slug := ""
if pal.has("user"):
user = pal.user.name
if pal.has("slug"):
slug = pal.slug
var palette_obj := {
"name": pal.title,
"author": user,
"colors": pal.colors,
"slug": slug
}
display_preview(palette_obj)
func _on_load_more_pressed() -> void:
get_palette_list()
func _on_visibility_changed() -> void:
if visible and current_page == 0:
get_palette_list()
func _on_close_requested() -> void:
hide()
func _on_focus_exited() -> void:
while close_delay > 0:
close_delay -= 1
await get_tree().create_timer(1).timeout
if not has_focus():
hide()
close_delay = 3
func _on_size_changed() -> void:
close_delay = 3

View File

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

View File

@ -1,6 +0,0 @@
@tool
extends MarginContainer
@export var color_picker_button: ColorPickerButton
@export var remove_button: TextureButton
@export var remove_button_text: Label

View File

@ -1 +0,0 @@
uid://8epr28t2njss

View File

@ -1,15 +0,0 @@
@tool
extends PopupPanel
@export var color_picker: ColorPicker
@export var saved_palettes: ItemList
@export var apply_palette_button: Button
func _init() -> void:
hide()
min_size = Vector2i(320, 675)
size = Vector2i(320, 675)
if DisplayServer.screen_get_size().x > 2000:
min_size *= 2
size *= 2

View File

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

View File

@ -1,67 +0,0 @@
extends EditorProperty
signal mouse_released
const CustomPalette := preload("res://addons/PaletteTools/Scripts/custom_picker.gd")
var property_control: Control
var updating := false
var custom_palette: CustomPalette
func _init(cust_palette: CustomPalette, obj: Object, named = null) -> void:
property_control = load("res://addons/PaletteTools/Scenes/custom_palette.tscn").instantiate()
custom_palette = cust_palette
if named and obj[named]:
property_control.color = obj[named]
else:
property_control.color = Color.BLACK
add_child(property_control)
add_focusable(property_control)
property_control.get_child(0).pressed.connect(_on_button_pressed)
resource_selected.connect(resource_prop)
func resource_prop(path: String, prop: Resource) -> void:
if prop.get(path):
var p := prop.get(path)
if p is Vector4:
property_control.color = Color(p.x, p.y, p.z, p.w)
else:
property_control.color = p
else:
property_control.color = Color.WHITE
func _on_button_pressed() -> void:
if not custom_palette:
push_warning(
'Error in Palette Tools addon: If the "Palette Tools" addon was just activated, '
+ "please select a different node before trying to edit color properties of this one. "
+ "This error happens becuase the inspector was loaded before the addon. If you didn't "
+ "just activate the addon or start the Godot editor, you can try turning the addon "
+ "on and off. If the problem persists, you may need to disable the addon. If this happens, "
+ "please report any relevant info so a fix can be made. Thanks!"
)
return
custom_palette.popup(Rect2(get_global_mouse_position() - Vector2(size.x, (size.y * 20)), size))
custom_palette.color_picker.color = get_edited_object()[get_edited_property()]
custom_palette.popup_hide.connect(cust_palette_closed, CONNECT_ONE_SHOT)
custom_palette.color_picker.color_changed.connect(cust_palette_changed)
func cust_palette_changed(new_color: Color) -> void:
property_control.color = new_color
func cust_palette_closed() -> void:
custom_palette.color_picker.color_changed.disconnect(cust_palette_changed)
property_control.color = custom_palette.color_picker.color
emit_changed(get_edited_property(), property_control.color)
func _update_property() -> void:
if get_edited_object() and get_edited_object()[get_edited_property()]:
property_control.color = get_edited_object()[get_edited_property()]
else:
property_control.color = Color.BLACK

View File

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

View File

@ -1,29 +0,0 @@
@tool
extends HTTPRequest
const Palette := preload("res://addons/PaletteTools/Scripts/palette_tool.gd")
@export var colors: Palette
var searching := false
func get_palette(url: String) -> void:
if searching:
return
searching = true
if url.ends_with("/"):
url = url.left(-1)
var error := request(url + ".json")
if error != OK:
push_error("An error occurred in the HTTP request.")
func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
searching = false
var json := FileAccess.get_file_as_string(download_file)
var json_obj := JSON.parse_string(json)
colors.preview_colors(json_obj.colors)
colors.p_name_text.text = json_obj.name
colors.p_author_text.text = json_obj.author

View File

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

View File

@ -1,17 +0,0 @@
@tool
extends ColorRect
@export var loading_text: Label
func _ready() -> void:
visible = true
func _on_visibility_changed() -> void:
while visible:
await get_tree().create_timer(.7).timeout
if loading_text.text.ends_with("..."):
loading_text.text.replace("...", "")
else:
loading_text.text += "."

Some files were not shown because too many files have changed in this diff Show More