Initial Void Bastion 0.20 prototype

This commit is contained in:
Emporex committed 2026-10-03 21:28:56 +02:00
commit 7362ca0f02
251 files changed
+29688

No files matched your search

@@ -0,0 +1,65 @@
extends SatoriAsyncResult
class_name Event
# The name of the event.
var name: String
# The time when the event was triggered.
var timestamp: String
# Optional value.
var value: String
# Event metadata, if any.
var metadata: Dictionary
# Optional event ID assigned by the client, used to de-duplicate in retransmission scenarios.
# If not supplied the server will assign a randomly generated unique event identifier.
var id: String
# The event constructor.
# Initializes a new Event object.
#
# @param name The name of the event.
# @param timestamp The timestamp of the event.
# @param value The value associated with the event (optional).
# @param metadata The metadata associated with the event (optional).
# @param id The ID of the event (optional).
func _init(name: String, timestamp: float, value: String = "", metadata: Dictionary = {}, id: String = "", p_exception = null):
super(p_exception)
self.name = name
self.timestamp = unix_to_protobuf_timestamp_format(timestamp)
self.value = value
self.metadata = metadata
self.id = id
func to_api_event_dict() -> Dictionary:
return {
"name": self.name,
"timestamp": self.timestamp,
"value": self.value,
"metadata": self.metadata,
"id": self.id
}
func unix_to_protobuf_timestamp_format(unix_time: float) -> String:
# Extract microseconds precision from unix time
var microseconds = int(fmod(unix_time, 1.0) * 1_000_000)
# Convert seconds to datetime structure
var datetime = Time.get_datetime_dict_from_unix_time(int(unix_time))
var year = datetime.year
var month = str(datetime.month).pad_zeros(2)
var day = str(datetime.day).pad_zeros(2)
var hour = str(datetime.hour).pad_zeros(2)
var minute = str(datetime.minute).pad_zeros(2)
var second = str(datetime.second).pad_zeros(2)
var microsecond = str(microseconds).pad_zeros(6)
# Construct the protobuf timestamp format string
var timestamp_str = "%s-%s-%sT%s:%s:%s.%sZ" % [year, month, day, hour, minute, second, microsecond]
return timestamp_str
@@ -0,0 +1 @@
uid://ia0o54hg21m
File diff suppressed because it is too large. Load diff
@@ -0,0 +1 @@
uid://bvfs02d0j8q7g
@@ -0,0 +1,212 @@
extends RefCounted
## A client for the API in Satori Server.
class_name SatoriClient
#region Properties
var _host
## The host address of the server.
var host : String:
get:
return _host
var _port
## The port number of the server.
var port : int:
get:
return _port
var _scheme
## The protocol scheme used to connect with the server. Must be either "http" or "https".
var scheme : String:
get:
return _scheme
## The key used to authenticate with the server without a session.
var api_key : String
## Set the timeout in seconds on requests sent to the server.
var timeout : int
var _api_client : SatoriAPI.ApiClient
var auto_refresh : bool = false:
set(v):
set_auto_refresh(v)
get:
return get_auto_refresh()
func get_auto_refresh():
return _api_client.auto_refresh
func set_auto_refresh(p_value):
_api_client.auto_refresh = p_value
#endregion
#region Initialization
func _init(p_adapter : SatoriHTTPAdapter,
p_api_key : String,
p_scheme : String,
p_host : String,
p_port : int,
p_timeout : int):
api_key = p_api_key
_scheme = p_scheme
_host = p_host
_port = p_port
timeout = p_timeout
_api_client = SatoriAPI.ApiClient.new(_scheme + "://" + _host + ":" + str(_port), p_adapter, SatoriAPI, api_key, p_timeout)
#endregion
#region Client APIs
## Authenticate against the server.
## [p_id]: An optional user id.
## [p_default_properties]: Optional default properties to update with this call.
## If not set, properties are left as they are on the server.
## [p_custom_properties]: Optional custom properties to update with this call.
## If not set, properties are left as they are on the server.
func authenticate_async(p_id: String, p_default_properties: Dictionary = {}, p_custom_properties: Dictionary = {}) -> SatoriSession:
return _parse_session(await _api_client.authenticate_async(api_key, "",
SatoriAPI.ApiAuthenticateRequest.create(SatoriAPI, {
"id": p_id,
"default": p_default_properties,
"custom": p_custom_properties
})))
## Log out a session, invalidate a refresh token, or log out all sessions/refresh tokens for a user.
## [p_session]: The session of the user.
func authenticate_logout_async(p_session: SatoriSession) -> SatoriAsyncResult:
return await _api_client.authenticate_logout_async(p_session,
SatoriAPI.ApiAuthenticateLogoutRequest.create(SatoriAPI, {
"refresh_token": p_session.refresh_token,
"token": p_session.token
}))
# Parses the Satori API session and returns a SatoriSession object.
func _parse_session(p_session: SatoriAPI.ApiSession) -> SatoriSession:
if p_session.is_exception():
return SatoriSession.new(null, null, p_session.get_exception())
return SatoriSession.new(p_session.token, p_session.refresh_token)
## Refresh a user's session using a refresh token retrieved from a previous authentication request.
## [p_sesison]: The session of the user.
func session_refresh_async(p_session : SatoriSession) -> SatoriSession:
return _parse_session(await _api_client.authenticate_refresh_async(api_key, "",
SatoriAPI.ApiAuthenticateRefreshRequest.create(SatoriAPI, {
"refresh_token": p_session.refresh_token,
})
))
## Send an event for this session.
## [p_session]: The session of the user.
## [p_event]: The event which will be sent.
func event_async(p_session: SatoriSession, p_event: Event) -> SatoriAsyncResult:
return await events_async(p_session, [
p_event
])
## Send a batch of events for this session.
## [p_session]: The session of the user.
## [p_events]: The batch of events which will be sent.
func events_async(p_session: SatoriSession, p_events: Array) -> SatoriAsyncResult:
var p_dict = {
"events": p_events.map(func(e):
return e.to_api_event_dict())
}
var req = SatoriAPI.ApiEventRequest.create(SatoriAPI, p_dict)
return await _api_client.event_async(p_session,
req)
## Get all experiments data.
## [p_session]: The session of the user.
func get_all_experiments_async(p_session: SatoriSession) -> SatoriAsyncResult:
return await _api_client.get_experiments_async(p_session)
## Get specific experiments data.
## [p_session]: The session of the user.
## [p_names]: Experiment names.
func get_experiments_async(p_session: SatoriSession, p_names: Array) -> SatoriAPI.ApiExperimentList:
return await _api_client.get_experiments_async(p_session, p_names)
## Get a single flag for this identity.
## This method will return the default value
## specified and will not raise an exception if the network is unavailable
## [p_session]: The session of the user.
## [p_name]: The name of the flag.
## [p_default]: The default value if the server is unreachable.
func get_flag_async(p_session: SatoriSession, p_name: String, p_default: String = "") -> SatoriAPI.ApiFlag:
var p_names = [p_name]
var flags = await get_flags_async(p_session, p_names)
if flags.is_exception():
return SatoriAPI.ApiFlag.create(SatoriAPI, {
"name": p_name,
"value": p_default
})
for flag in flags.flags:
if flag.name == p_name:
return flag
return null
## List all available flags for this identity.
## [p_session]: The session of the user.
## [p_names]: Flag names, if empty all flags will be returned.
func get_flags_async(p_session: SatoriSession, p_names: Array) -> SatoriAPI.ApiFlagList:
return await _api_client.get_flags_async(p_session.token, p_names)
## List available live events.
## [p_session]: The session of the user.
## [p_names]: Live event names, if null or empty all live events are returned.
func get_live_events_async(p_session: SatoriSession, p_names: Array = []) -> SatoriAPI.ApiLiveEventList:
return await _api_client.get_live_events_async(p_session, p_names)
## Identify a session with a new ID.
## [p_session]: The session of the user.
## [p_id]: Identity ID to enrich the current session and return a new session.
## The old session will no longer be usable.
## Must be between eight and 128 characters (inclusive).
## Must be an alphanumeric string with only underscores and hyphens allowed.
## [p_default_properties]: The default properties.
## [p_custom_properties]: The custom event properties.
func identify_async(p_session: SatoriSession, p_id: String, p_default_properties: Dictionary = {}, p_custom_properties: Dictionary = {}) -> SatoriSession:
var req = SatoriAPI.ApiIdentifyRequest.create(SatoriAPI, {
"id": p_id,
"default": p_default_properties,
"custom": p_custom_properties
})
return _parse_session(await _api_client.identify_async(p_session, req))
## List properties associated with this identity.
## [p_session]: The session of the user.
func list_properties_async(p_session: SatoriSession) -> SatoriAsyncResult:
return await _api_client.list_properties_async(p_session)
## Update properties associated with this identity.
## [p_session]: The session of the user.
## [p_default_properties]: The default properties to update.
## [p_custom_properties]: The custom properties to update.
## [p_recompute]: Whether or not to recompute the user's audience membership immediately after property update.
func update_properties_async(p_session: SatoriSession, p_default_properties: Dictionary, p_custom_properties: Dictionary, p_recompute: bool = false) -> SatoriAsyncResult:
var req = SatoriAPI.ApiUpdatePropertiesRequest.create(SatoriAPI, {
"default": p_default_properties,
"custom": p_custom_properties,
"recompute": p_recompute
})
return await _api_client.update_properties_async(p_session, req)
## Delete the caller's identity and associated data.
## [p_session]: The session of the user.
func delete_identity_async(p_session: SatoriSession) -> SatoriAsyncResult:
return await _api_client.delete_identity_async(p_session)
#endregion
@@ -0,0 +1 @@
uid://cd25d8rmdqk2c
@@ -0,0 +1,208 @@
@tool
extends Node
# An adapter which implements the HTTP protocol.
class_name SatoriHTTPAdapter
# The logger to use with the adapter.
var logger : RefCounted = SatoriLogger.new()
# The timeout for requests
var timeout : int = 3
# If request should be automatically retried when a network error occurs.
var auto_retry : bool = true
# The maximum number of time a request will be retried when auto_retry is true
var auto_retry_count : int = 3
var auto_retry_backoff_base : int = 10
# Whether or not to use threads when making HTTP requests.
var use_threads : bool = true
var _pending = {}
var id : int = 0
class AsyncRequest:
var id : int
var request : HTTPRequest
var uri : String
var method : int
var headers : PackedStringArray
var body : PackedByteArray
var retry_count := 3
var backoff_time := 10
var logger : SatoriLogger
var cancelled = false
var result : int = HTTPRequest.RESULT_NO_RESPONSE
var response_code : int = -1
var response_body : PackedByteArray
var timer : SceneTreeTimer = null
var cur_try : int = 1
var rng = RandomNumberGenerator.new()
func _init(p_id : int, p_request : HTTPRequest, p_uri : String,
p_method : int, p_headers : PackedStringArray, p_body : PackedByteArray,
p_retry_count : int, p_backoff_time : int, p_logger : SatoriLogger):
rng.seed = Time.get_ticks_usec()
id = p_id
request = p_request
uri = p_uri
method = p_method
headers = p_headers
body = p_body
retry_count = p_retry_count
backoff_time = p_backoff_time
logger = p_logger
func should_retry():
return cur_try < retry_count and not cancelled
func retry():
var time = pow(backoff_time, cur_try) * rng.randf_range(0.5, 1)
logger.debug("Retrying request %d. Tries left: %d. Backoff: %d ms" % [
id, retry_count - cur_try, time
])
cur_try += 1
await backoff(time)
if cancelled:
return
return await make_request()
func make_request():
var err = request.request(uri, headers, method, body.get_string_from_utf8())
if err != OK:
await request.get_tree().process_frame
result = HTTPRequest.RESULT_CANT_CONNECT
logger.debug("Request %d failed to start, error: %d" % [id, err])
return
var args = await request.request_completed
result = args[0]
response_code = args[1]
response_body = args[3]
func backoff(p_time : int):
timer = request.get_tree().create_timer(p_time / 1000.0)
await timer.timeout
timer = null
func cancel():
cancelled = true
request.cancel_request()
if timer:
timer.time_left = 0
else:
request.call_deferred("emit_signal", "request_completed", HTTPRequest.RESULT_REQUEST_FAILED, 0, [], [])
func parse_result():
if cancelled:
return SatoriException.new("Request cancelled", -1, -1, true)
elif result != HTTPRequest.RESULT_SUCCESS:
if result == null:
result = 0
return SatoriException.new("HTTPRequest failed!", result)
var json = JSON.new()
var json_error = json.parse(response_body.get_string_from_utf8())
if json_error != OK:
logger.debug("Unable to parse request %d response. JSON error: %d, JSON error message: %s, response code: %d" % [
id, json_error, json.get_error_message(), response_code
])
return SatoriException.new("Failed to decode JSON response", response_code)
var parsed = json.get_data()
if response_code != HTTPClient.RESPONSE_OK:
var error = ""
var code = -1
if typeof(parsed) == TYPE_DICTIONARY:
if "message" in parsed:
error = parsed["message"]
elif "error" in parsed:
error = parsed["error"]
else:
error = str(parsed)
code = parsed["code"] if "code" in parsed else -1
else:
error = str(parsed)
if typeof(error) == TYPE_DICTIONARY:
error = JSON.stringify(error)
logger.debug("Request %d returned response code: %d, RPC code: %d, error: %s" % [
id, response_code, code, error
])
return SatoriException.new(error, response_code, code)
return parsed
# Send a HTTP request.
# @param method - HTTP method to use for this request.
# @param uri - The fully qualified URI to use.
# @param headers - Request headers to set.
# @param body - Request content body to set.
# @param timeoutSec - Request timeout.
# Returns a task which resolves to the contents of the response.
func send_async(p_method : String, p_uri : String, p_headers : Dictionary, p_body : PackedByteArray):
var req = HTTPRequest.new()
req.timeout = timeout
if use_threads and OS.get_name() != 'Web':
req.use_threads = true # Threads not available nor needed on the web.
# Parse method
var method = HTTPClient.METHOD_GET
if p_method == "POST":
method = HTTPClient.METHOD_POST
elif p_method == "PUT":
method = HTTPClient.METHOD_PUT
elif p_method == "DELETE":
method = HTTPClient.METHOD_DELETE
elif p_method == "HEAD":
method = HTTPClient.METHOD_HEAD
var headers = PackedStringArray()
# Parse headers
headers.append("Accept: application/json")
for k in p_headers:
headers.append("%s: %s" % [k, p_headers[k]])
id += 1
var retry = auto_retry_count if auto_retry else 0
var backoff = auto_retry_backoff_base
_pending[id] = AsyncRequest.new(id, req, p_uri, method, headers, p_body, retry, backoff, logger)
logger.debug("Sending request [ID: %d, Method: %s, Uri: %s, Headers: %s, Body: %s, Timeout: %d, Retries: %d, Backoff base: %d ms]" % [
id, p_method, p_uri, p_headers, p_body.get_string_from_utf8(), timeout, retry, backoff
])
add_child(req)
return await _send_async(id, _pending)
func get_last_token():
return id
func cancel_request(p_token):
if _pending.has(p_token):
_pending[p_token].cancel()
static func _clear_request(p_request : AsyncRequest, p_pending : Dictionary, p_id : int):
if not p_request.request.is_queued_for_deletion():
p_request.logger.debug("Freeing request %d" % p_id)
p_request.request.queue_free()
p_pending.erase(p_id)
static func _send_async(p_id : int, p_pending : Dictionary):
var req : AsyncRequest = p_pending[p_id]
await req.make_request()
while req.result != HTTPRequest.RESULT_SUCCESS:
req.logger.debug("Request %d failed with result: %d, response code: %d" % [
p_id, req.result, req.response_code
])
if not req.should_retry():
break
await req.retry()
_clear_request(req, p_pending, p_id)
return req.parse_result()
@@ -0,0 +1 @@
uid://d4cdv6h3e50q3
@@ -0,0 +1,107 @@
extends SatoriAsyncResult
class_name SatoriSession
var _token: String = ""
var token: String:
get:
return _token
var _refresh_token: String = ""
var refresh_token: String:
get:
return _refresh_token
var _expire_time: int = 0
var expire_time: int:
get:
return _expire_time
var expired: bool:
get:
return is_expired()
var _refresh_expire_time: int = 0
var refresh_expire_time: int:
get:
return _refresh_expire_time
var _identity_id: String = ""
var identity_id: String:
get:
return _identity_id
var _valid : bool = false
var valid : bool:
get:
return _valid
func is_expired() -> bool:
return _expire_time < Time.get_unix_time_from_system()
func would_expire_in(p_secs : int) -> bool:
return _expire_time < Time.get_unix_time_from_system() + p_secs
func has_refresh_expired(offset: float) -> bool:
return _expire_time < offset
func is_refresh_expired() -> bool:
return _refresh_expire_time < Time.get_unix_time_from_system()
func is_valid():
return _valid
# Initializes a new instance of the SatoriSession class.
#
# @param p_token - The authentication token.
# @param p_refresh_token - The refresh token.
# @param p_exception - The exception to be thrown, if any.
func _init(p_token = null, p_refresh_token = null, p_exception = null):
super(p_exception)
_refresh_expire_time = 0
if p_token:
_update(p_token, p_refresh_token)
func _update(p_token, p_refresh_token):
_token = p_token
_refresh_token = p_refresh_token
var decoded = _jwt_unpack(p_token)
if decoded.is_empty():
_valid = false
return
_valid = true
_expire_time = int(decoded.get("exp", 0))
_identity_id = str(decoded.get("iid", ""))
_refresh_expire_time = int(_jwt_unpack(refresh_token).get("exp", 0)) if !refresh_token.is_empty() else 0
func _to_string():
if is_exception():
return get_exception()._to_string()
return "Session<AuthToken=%s, ExpireTime=%d, RefreshToken=%s, RefreshExpireTime=%d, IdentityId=%s>" % [
_token, _expire_time, _refresh_token, _refresh_expire_time, identity_id]
func _jwt_unpack(p_token : String) -> Dictionary:
# Hack decode JSON payload from JWT.
if p_token.find(".") == -1:
_ex = SatoriException.new("Missing payload: %s" % p_token)
return {}
var payload = p_token.split('.')[1];
var pad_length = ceil(payload.length() / 4.0) * 4;
# Pad base64
for i in range(0, pad_length - payload.length()):
payload += "="
payload = payload.replace("-", "+").replace("_", "/")
var unpacked = Marshalls.base64_to_utf8(payload)
var json = JSON.new()
var error = json.parse(unpacked)
if error == OK:
var decoded = json.get_data()
if typeof(decoded) == TYPE_DICTIONARY:
return decoded
_ex = SatoriException.new("Unable to unpack token: %s" % p_token)
return {}
@@ -0,0 +1 @@
uid://btjsbd35ljish
@@ -0,0 +1,34 @@
extends RefCounted
class_name SatoriAsyncResult
var exception : SatoriException:
set(v):
pass
get:
return get_exception()
var _ex = null
func _init(p_ex = null):
_ex = p_ex
func is_exception():
return get_exception() != null
func was_cancelled():
return is_exception() and get_exception().cancelled
func get_exception() -> SatoriException:
return _ex as SatoriException
func _to_string():
if is_exception():
return get_exception()._to_string()
return "SatoriAsyncResult<>"
static func _safe_ret(p_obj, p_type : GDScript):
if is_instance_of(p_obj, p_type):
return p_obj
elif p_obj is SatoriException:
return p_type.new(p_obj)
return p_type.new(SatoriException.new())
@@ -0,0 +1 @@
uid://7fruq6sqrt8x
@@ -0,0 +1,42 @@
extends RefCounted
# An exception generated during a request.
# Usually contains at least an error message.
class_name SatoriException
var _status_code : int = -1
var status_code : int:
set(v):
pass
get:
return _status_code
var _grpc_status_code : int = -1
var grpc_status_code : int:
set(v):
pass
get:
return _grpc_status_code
var _message : String = ""
var message : String:
set(v):
pass
get:
return _message
var _cancelled : bool = false
var cancelled : bool:
set(v):
pass
get:
return _cancelled
func _init(p_message : String = "", p_status_code : int = -1, p_grpc_status_code : int = -1, p_cancelled : bool = false):
_status_code = p_status_code
_grpc_status_code = p_grpc_status_code
_message = p_message
_cancelled = p_cancelled
func _to_string() -> String:
return "SatoriException(StatusCode={%s}, Message='{%s}', GrpcStatusCode={%s})" % [_status_code, _message, _grpc_status_code]
@@ -0,0 +1 @@
uid://byri0ssrp022t
@@ -0,0 +1,38 @@
extends RefCounted
class_name SatoriLogger
enum LOG_LEVEL {NONE, ERROR, WARNING, INFO, VERBOSE, DEBUG}
var _level = LOG_LEVEL.ERROR
var _module = "Satori"
func _init(p_module : String = "Satori", p_level : int = LOG_LEVEL.ERROR):
_level = p_level
_module = p_module
func _log(level : int, msg):
if level <= _level:
if level == LOG_LEVEL.ERROR:
printerr("=== %s : ERROR === %s" % [_module, str(msg)])
else:
var what = "=== UNKNOWN === "
for k in LOG_LEVEL:
if level == LOG_LEVEL[k]:
what = "=== %s : %s === " % [_module, k]
break
print(what + str(msg))
func error(msg):
_log(LOG_LEVEL.ERROR, msg)
func warning(msg):
_log(LOG_LEVEL.WARNING, msg)
func info(msg):
_log(LOG_LEVEL.INFO, msg)
func verbose(msg):
_log(LOG_LEVEL.VERBOSE, msg)
func debug(msg):
_log(LOG_LEVEL.DEBUG, msg)
@@ -0,0 +1 @@
uid://cmslry0dhyhhc
@@ -0,0 +1,163 @@
extends RefCounted
class_name SatoriSerializer
static func serialize(p_obj : Object) -> Dictionary:
var out = {}
var schema = p_obj.get("_SCHEMA")
if schema == null:
return {} # No schema defined
for k in schema:
var prop = schema[k]
var val = p_obj.get(prop["name"])
if val == null:
continue
var type = prop["type"]
var content = prop.get("content", TYPE_NIL)
if typeof(content) == TYPE_STRING:
content = TYPE_OBJECT
var val_type = typeof(val)
match val_type:
TYPE_OBJECT: # Simple objects
out[k] = serialize(val)
TYPE_ARRAY: # Array of objects
var arr = []
if val.size() > 0 and typeof(val[0]) == TYPE_OBJECT: # Array of objects
for e in val:
arr.append(serialize(e))
else:
arr = val
out[k] = arr
TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_STRING_ARRAY: # Array of ints, bools, or strings
var arr = []
for e in val:
if content == TYPE_BOOL:
e = bool(e)
if typeof(e) != content:
continue
arr.append(e)
out[k] = arr
TYPE_DICTIONARY: # Maps
var dict = {}
if content == TYPE_OBJECT: # Map of objects
for l in val:
if typeof(val[l]) != TYPE_OBJECT:
continue
dict[l] = serialize(val[l])
else: # Map of simple types
for l in val:
var e = val[l]
if content == TYPE_FLOAT:
e = float(e)
elif content == TYPE_INT:
e = int(e)
elif content == TYPE_BOOL:
e = bool(e)
if typeof(e) != content:
continue
dict[l] = e
out[k] = dict
_:
out[k] = val
return out
static func deserialize(p_ns : GDScript, p_cls_name : String, p_dict : Dictionary) -> Object:
var cls : GDScript = p_ns.get(p_cls_name)
var schema = cls.get("_SCHEMA")
if schema == null:
return SatoriException.new() # No schema defined
var obj = cls.new()
for k in schema:
var prop = schema[k]
var pname = prop["name"]
var type = prop["type"]
var required = prop["required"]
var content = prop.get("content", TYPE_NIL)
var type_cmp = type
if typeof(type) == TYPE_STRING: # A class
type_cmp = TYPE_DICTIONARY
if type_cmp == TYPE_PACKED_STRING_ARRAY or type_cmp == TYPE_PACKED_INT32_ARRAY: # A specialized array
type_cmp = TYPE_ARRAY
var content_cmp = content
if typeof(content) == TYPE_STRING: # A dictionary or array of classes
content_cmp = TYPE_DICTIONARY
var val = p_dict.get(k, null)
# Ints might and up being recognized as floats. Change that if needed
if type_cmp == TYPE_INT:
if typeof(val) == TYPE_FLOAT:
val = int(val)
elif typeof(val) == TYPE_STRING and val.is_valid_int():
val = val.to_int()
if typeof(val) == type_cmp:
if typeof(type) == TYPE_STRING:
obj.set(pname, deserialize(p_ns, type, val))
elif type_cmp == TYPE_DICTIONARY:
var v = {}
for l in val:
if typeof(content) == TYPE_STRING:
v[l] = deserialize(p_ns, content, val[l])
elif content == TYPE_FLOAT:
v[l] = float(val[l])
elif content == TYPE_INT:
v[l] = int(val[l])
elif content == TYPE_BOOL:
v[l] = bool(val[l])
else:
v[l] = str(val[l])
obj.set(pname, v)
elif type_cmp == TYPE_ARRAY:
var v
match content:
TYPE_INT, TYPE_BOOL: v = PackedInt32Array()
TYPE_STRING: v = PackedStringArray()
_: v = Array()
for e in val:
if typeof(e) == TYPE_DICTIONARY:
v.append(e) # Avoid deserialization if e is already a dictionary
elif typeof(content) == TYPE_STRING:
v.append(deserialize(p_ns, content, e))
elif content == TYPE_FLOAT:
v.append(float(e))
elif content == TYPE_INT:
v.append(int(e))
elif content == TYPE_BOOL:
v.append(bool(e))
else:
v.append(str(e))
obj.set(pname, v)
else:
obj.set(pname, val)
elif required:
obj._ex = SatoriException.new("ERROR [%s]: Missing or invalid required prop %s = %s:\n\t%s" % [p_cls_name, prop, p_dict.get(k), p_dict])
return obj
return obj
###
# Compatibility with Godot 3.1 which does not expose String.http_escape
###
const HEX = ["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"]
static func escape_http(p_str : String) -> String:
var out : String = ""
for o in p_str:
if (o == '.' or o == '-' or o == '_' or o == '~' or
(o >= 'a' and o <= 'z') or
(o >= 'A' and o <= 'Z') or
(o >= '0' and o <= '9')):
out += o
else:
for b in o.to_utf8_buffer():
out += "%%%s" % to_hex(b)
return out
static func to_hex(p_val : int) -> String:
var v := p_val
var o := ""
while v != 0:
o = HEX[v % 16] + o
v /= 16
return o
@@ -0,0 +1 @@
uid://cg7wctbblbvn7