2025-08-16 14:36:47 +03:00
|
|
|
class_name LuaCrumbsUtils
|
|
|
|
|
extends RefCounted
|
|
|
|
|
|
|
|
|
|
const CRUMBS_DIR_PATH = "user://crumbs/"
|
|
|
|
|
|
|
|
|
|
class Crumb:
|
|
|
|
|
var name: String
|
|
|
|
|
var value: String
|
|
|
|
|
var created_at: float
|
|
|
|
|
var lifespan: float = -1.0 # -1 = no expiry, otherwise lifespan in seconds
|
|
|
|
|
|
|
|
|
|
func _init(n: String, v: String, lifetime: float = -1.0):
|
|
|
|
|
name = n
|
|
|
|
|
value = v
|
|
|
|
|
created_at = Time.get_unix_time_from_system()
|
|
|
|
|
lifespan = lifetime
|
|
|
|
|
|
|
|
|
|
func is_expired() -> bool:
|
|
|
|
|
if lifespan < 0:
|
|
|
|
|
return false
|
|
|
|
|
var current_time = Time.get_unix_time_from_system()
|
|
|
|
|
return current_time > (created_at + lifespan)
|
|
|
|
|
|
|
|
|
|
func get_expiry_time() -> float:
|
|
|
|
|
if lifespan < 0:
|
|
|
|
|
return -1.0
|
|
|
|
|
return created_at + lifespan
|
|
|
|
|
|
|
|
|
|
func to_dict() -> Dictionary:
|
|
|
|
|
return {
|
|
|
|
|
"name": name,
|
|
|
|
|
"value": value,
|
|
|
|
|
"created_at": created_at,
|
|
|
|
|
"lifespan": lifespan
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static func from_dict(data: Dictionary) -> Crumb:
|
|
|
|
|
var crumb = Crumb.new(data.get("name", ""), data.get("value", ""))
|
|
|
|
|
crumb.created_at = data.get("created_at", Time.get_unix_time_from_system())
|
|
|
|
|
crumb.lifespan = data.get("lifespan", -1.0)
|
|
|
|
|
return crumb
|
|
|
|
|
|
|
|
|
|
static func setup_crumbs_api(vm: LuauVM):
|
|
|
|
|
# Ensure crumbs directory exists
|
|
|
|
|
if not DirAccess.dir_exists_absolute(CRUMBS_DIR_PATH):
|
|
|
|
|
DirAccess.make_dir_recursive_absolute(CRUMBS_DIR_PATH)
|
|
|
|
|
|
|
|
|
|
vm.lua_newtable()
|
|
|
|
|
|
|
|
|
|
vm.lua_pushcallable(_crumbs_set_handler, "gurt.crumbs.set")
|
|
|
|
|
vm.lua_setfield(-2, "set")
|
|
|
|
|
|
|
|
|
|
vm.lua_pushcallable(_crumbs_get_handler, "gurt.crumbs.get")
|
|
|
|
|
vm.lua_setfield(-2, "get")
|
|
|
|
|
|
|
|
|
|
vm.lua_pushcallable(_crumbs_delete_handler, "gurt.crumbs.delete")
|
|
|
|
|
vm.lua_setfield(-2, "delete")
|
|
|
|
|
|
|
|
|
|
vm.lua_pushcallable(_crumbs_get_all_handler, "gurt.crumbs.getAll")
|
|
|
|
|
vm.lua_setfield(-2, "getAll")
|
|
|
|
|
|
|
|
|
|
vm.lua_getglobal("gurt")
|
|
|
|
|
if vm.lua_isnil(-1):
|
|
|
|
|
vm.lua_pop(1)
|
|
|
|
|
vm.lua_newtable()
|
|
|
|
|
vm.lua_setglobal("gurt")
|
|
|
|
|
vm.lua_getglobal("gurt")
|
|
|
|
|
|
|
|
|
|
vm.lua_pushvalue(-2)
|
|
|
|
|
vm.lua_setfield(-2, "crumbs")
|
|
|
|
|
vm.lua_pop(2)
|
|
|
|
|
|
|
|
|
|
static func get_current_domain() -> String:
|
|
|
|
|
var main_node = Engine.get_main_loop().current_scene
|
|
|
|
|
if main_node and main_node.has_method("get_current_url"):
|
|
|
|
|
var current_url = main_node.get_current_url()
|
|
|
|
|
return sanitize_domain_for_filename(current_url)
|
|
|
|
|
return "default"
|
|
|
|
|
|
|
|
|
|
static func sanitize_domain_for_filename(domain: String) -> String:
|
|
|
|
|
# Remove protocol prefix
|
2025-11-06 20:02:53 +08:00
|
|
|
if domain.begins_with("lw://"):
|
2025-08-16 14:36:47 +03:00
|
|
|
domain = domain.substr(7)
|
|
|
|
|
elif domain.contains("://"):
|
|
|
|
|
var parts = domain.split("://")
|
|
|
|
|
if parts.size() > 1:
|
|
|
|
|
domain = parts[1]
|
|
|
|
|
|
|
|
|
|
# Extract only the domain part (remove path)
|
|
|
|
|
if domain.contains("/"):
|
|
|
|
|
domain = domain.split("/")[0]
|
|
|
|
|
|
|
|
|
|
# Replace invalid filename characters (mainly colons for ports)
|
|
|
|
|
domain = domain.replace(":", "_")
|
|
|
|
|
domain = domain.replace("\\", "_")
|
|
|
|
|
domain = domain.replace("*", "_")
|
|
|
|
|
domain = domain.replace("?", "_")
|
|
|
|
|
domain = domain.replace("\"", "_")
|
|
|
|
|
domain = domain.replace("<", "_")
|
|
|
|
|
domain = domain.replace(">", "_")
|
|
|
|
|
domain = domain.replace("|", "_")
|
|
|
|
|
|
|
|
|
|
# Ensure it's not empty
|
|
|
|
|
if domain.is_empty():
|
|
|
|
|
domain = "default"
|
|
|
|
|
|
|
|
|
|
return domain
|
|
|
|
|
|
|
|
|
|
static func get_domain_file_path(domain: String) -> String:
|
|
|
|
|
return CRUMBS_DIR_PATH + domain + ".json"
|
|
|
|
|
|
|
|
|
|
static func _crumbs_set_handler(vm: LuauVM) -> int:
|
|
|
|
|
vm.luaL_checktype(1, vm.LUA_TTABLE)
|
|
|
|
|
|
|
|
|
|
vm.lua_getfield(1, "name")
|
|
|
|
|
if vm.lua_isnil(-1):
|
|
|
|
|
vm.luaL_error("crumb 'name' field is required")
|
|
|
|
|
return 0
|
|
|
|
|
var name: String = vm.lua_tostring(-1)
|
|
|
|
|
vm.lua_pop(1)
|
|
|
|
|
|
|
|
|
|
vm.lua_getfield(1, "value")
|
|
|
|
|
if vm.lua_isnil(-1):
|
|
|
|
|
vm.luaL_error("crumb 'value' field is required")
|
|
|
|
|
return 0
|
|
|
|
|
var value: String = vm.lua_tostring(-1)
|
|
|
|
|
vm.lua_pop(1)
|
|
|
|
|
|
|
|
|
|
var lifetime: float = -1.0
|
|
|
|
|
vm.lua_getfield(1, "lifetime")
|
|
|
|
|
if not vm.lua_isnil(-1):
|
|
|
|
|
lifetime = vm.lua_tonumber(-1)
|
|
|
|
|
vm.lua_pop(1)
|
|
|
|
|
|
|
|
|
|
var domain = get_current_domain()
|
|
|
|
|
var crumb = Crumb.new(name, value, lifetime)
|
|
|
|
|
save_crumb(domain, crumb)
|
|
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
static func _crumbs_get_handler(vm: LuauVM) -> int:
|
|
|
|
|
var name: String = vm.luaL_checkstring(1)
|
|
|
|
|
var domain = get_current_domain()
|
|
|
|
|
var crumb = load_crumb(domain, name)
|
|
|
|
|
|
|
|
|
|
if crumb and not crumb.is_expired():
|
|
|
|
|
vm.lua_pushstring(crumb.value)
|
|
|
|
|
else:
|
|
|
|
|
vm.lua_pushnil()
|
|
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
static func _crumbs_delete_handler(vm: LuauVM) -> int:
|
|
|
|
|
var name: String = vm.luaL_checkstring(1)
|
|
|
|
|
var domain = get_current_domain()
|
|
|
|
|
var existed = delete_crumb(domain, name)
|
|
|
|
|
|
|
|
|
|
vm.lua_pushboolean(existed)
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
static func _crumbs_get_all_handler(vm: LuauVM) -> int:
|
|
|
|
|
var domain = get_current_domain()
|
|
|
|
|
var all_crumbs = load_all_crumbs(domain)
|
|
|
|
|
|
|
|
|
|
vm.lua_newtable()
|
|
|
|
|
|
|
|
|
|
for crumb_name in all_crumbs:
|
|
|
|
|
var crumb = all_crumbs[crumb_name]
|
|
|
|
|
if not crumb.is_expired():
|
|
|
|
|
vm.lua_newtable()
|
|
|
|
|
vm.lua_pushstring(crumb.name)
|
|
|
|
|
vm.lua_setfield(-2, "name")
|
|
|
|
|
vm.lua_pushstring(crumb.value)
|
|
|
|
|
vm.lua_setfield(-2, "value")
|
|
|
|
|
|
|
|
|
|
# Include expiry time if it exists (but not created_at)
|
|
|
|
|
var expiry_time = crumb.get_expiry_time()
|
|
|
|
|
if expiry_time > 0:
|
|
|
|
|
vm.lua_pushnumber(expiry_time)
|
|
|
|
|
vm.lua_setfield(-2, "expiry")
|
|
|
|
|
|
|
|
|
|
vm.lua_setfield(-2, crumb_name)
|
|
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
static func load_all_crumbs(domain: String) -> Dictionary:
|
|
|
|
|
var file_path = get_domain_file_path(domain)
|
|
|
|
|
if not FileAccess.file_exists(file_path):
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
var file = FileAccess.open(file_path, FileAccess.READ)
|
|
|
|
|
if not file:
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
var json_string = file.get_as_text()
|
|
|
|
|
file.close()
|
|
|
|
|
|
|
|
|
|
var json = JSON.new()
|
|
|
|
|
var parse_result = json.parse(json_string)
|
|
|
|
|
if parse_result != OK:
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
var crumbs_data = json.data
|
|
|
|
|
if not crumbs_data is Dictionary:
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
var crumbs = {}
|
|
|
|
|
var changed = false
|
|
|
|
|
|
|
|
|
|
for crumb_name in crumbs_data:
|
|
|
|
|
var crumb_dict = crumbs_data[crumb_name]
|
|
|
|
|
if crumb_dict is Dictionary:
|
|
|
|
|
var crumb = Crumb.from_dict(crumb_dict)
|
|
|
|
|
if crumb.is_expired():
|
|
|
|
|
changed = true
|
|
|
|
|
else:
|
|
|
|
|
crumbs[crumb_name] = crumb
|
|
|
|
|
|
|
|
|
|
# Save back if we removed expired crumbs
|
|
|
|
|
if changed:
|
|
|
|
|
save_all_crumbs(domain, crumbs)
|
|
|
|
|
|
|
|
|
|
return crumbs
|
|
|
|
|
|
|
|
|
|
static func save_all_crumbs(domain: String, crumbs: Dictionary):
|
|
|
|
|
var crumbs_data = {}
|
|
|
|
|
for crumb_name in crumbs:
|
|
|
|
|
var crumb = crumbs[crumb_name]
|
|
|
|
|
crumbs_data[crumb_name] = crumb.to_dict()
|
|
|
|
|
|
|
|
|
|
var file_path = get_domain_file_path(domain)
|
|
|
|
|
var file = FileAccess.open(file_path, FileAccess.WRITE)
|
|
|
|
|
if not file:
|
|
|
|
|
push_error("Failed to open crumbs file for writing: " + file_path)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
var json_string = JSON.stringify(crumbs_data)
|
|
|
|
|
file.store_string(json_string)
|
|
|
|
|
file.close()
|
|
|
|
|
|
|
|
|
|
static func load_crumb(domain: String, name: String) -> Crumb:
|
|
|
|
|
var all_crumbs = load_all_crumbs(domain)
|
|
|
|
|
return all_crumbs.get(name, null)
|
|
|
|
|
|
|
|
|
|
static func save_crumb(domain: String, crumb: Crumb):
|
|
|
|
|
var all_crumbs = load_all_crumbs(domain)
|
|
|
|
|
all_crumbs[crumb.name] = crumb
|
|
|
|
|
save_all_crumbs(domain, all_crumbs)
|
|
|
|
|
|
|
|
|
|
static func delete_crumb(domain: String, name: String) -> bool:
|
|
|
|
|
var all_crumbs = load_all_crumbs(domain)
|
|
|
|
|
var existed = all_crumbs.has(name)
|
|
|
|
|
if existed:
|
|
|
|
|
all_crumbs.erase(name)
|
|
|
|
|
save_all_crumbs(domain, all_crumbs)
|
|
|
|
|
return existed
|
2025-09-07 18:49:32 +03:00
|
|
|
|
|
|
|
|
static func clear_all_crumbs():
|
|
|
|
|
if DirAccess.dir_exists_absolute(CRUMBS_DIR_PATH):
|
|
|
|
|
var dir = DirAccess.open(CRUMBS_DIR_PATH)
|
|
|
|
|
if dir:
|
|
|
|
|
dir.list_dir_begin()
|
|
|
|
|
var file_name = dir.get_next()
|
|
|
|
|
while file_name != "":
|
|
|
|
|
if file_name.ends_with(".json"):
|
|
|
|
|
dir.remove(file_name)
|
|
|
|
|
file_name = dir.get_next()
|
|
|
|
|
dir.list_dir_end()
|