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

+203
View File
@@ -0,0 +1,203 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+61
View File
@@ -0,0 +1,61 @@
@tool
extends Node
# The default host address of the server.
const DEFAULT_HOST : String = "127.0.0.1"
# The default port number of the server.
const DEFAULT_PORT : int = 7350
# The default timeout for the connections.
const DEFAULT_TIMEOUT = 3
# The default protocol scheme for the client connection.
const DEFAULT_CLIENT_SCHEME : String = "http"
# The default protocol scheme for the socket connection.
const DEFAULT_SOCKET_SCHEME : String = "ws"
# The default log level for the Nakama logger.
const DEFAULT_LOG_LEVEL = NakamaLogger.LOG_LEVEL.DEBUG
var _http_adapter = null
var logger = NakamaLogger.new()
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
func get_client_adapter() -> NakamaHTTPAdapter:
if _http_adapter == null:
_http_adapter = NakamaHTTPAdapter.new()
_http_adapter.logger = logger
_http_adapter.name = "NakamaHTTPAdapter"
add_child(_http_adapter)
return _http_adapter
func create_socket_adapter() -> NakamaSocketAdapter:
var adapter = NakamaSocketAdapter.new()
adapter.name = "NakamaWebSocketAdapter"
adapter.logger = logger
add_child(adapter)
return adapter
func create_client(p_server_key : String,
p_host : String = DEFAULT_HOST,
p_port : int = DEFAULT_PORT,
p_scheme : String = DEFAULT_CLIENT_SCHEME,
p_timeout : int = DEFAULT_TIMEOUT,
p_log_level : int = DEFAULT_LOG_LEVEL) -> NakamaClient:
logger._level = p_log_level
return NakamaClient.new(get_client_adapter(), p_server_key, p_scheme, p_host, p_port, p_timeout)
func create_socket(p_host : String = DEFAULT_HOST,
p_port : int = DEFAULT_PORT,
p_scheme : String = DEFAULT_SOCKET_SCHEME) -> NakamaSocket:
return NakamaSocket.new(create_socket_adapter(), p_host, p_port, p_scheme, true)
func create_socket_from(p_client : NakamaClient) -> NakamaSocket:
var scheme = "ws"
if p_client.scheme == "https":
scheme = "wss"
return NakamaSocket.new(create_socket_adapter(), p_client.host, p_client.port, scheme, true)
@@ -0,0 +1 @@
uid://dwg1d18n8xo3d
+41
View File
@@ -0,0 +1,41 @@
@tool
extends Node
# The default host address of the server.
const DEFAULT_HOST : String = "127.0.0.1"
# The default port number of the server.
const DEFAULT_PORT : int = 7450
# The default timeout for the connections.
const DEFAULT_TIMEOUT = 15
# The default protocol scheme for the client connection.
const DEFAULT_CLIENT_SCHEME : String = "http"
# The default log level for the Satori logger.
const DEFAULT_LOG_LEVEL = SatoriLogger.LOG_LEVEL.DEBUG
var _http_adapter = null
var logger = SatoriLogger.new()
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
func get_client_adapter() -> SatoriHTTPAdapter:
if _http_adapter == null:
_http_adapter = SatoriHTTPAdapter.new()
_http_adapter.logger = logger
_http_adapter.name = "SatoriHTTPAdapter"
add_child(_http_adapter)
return _http_adapter
func create_client(p_api_key : String,
p_host : String = DEFAULT_HOST,
p_port : int = DEFAULT_PORT,
p_scheme : String = DEFAULT_CLIENT_SCHEME,
p_timeout : int = DEFAULT_TIMEOUT,
p_log_level : int = DEFAULT_LOG_LEVEL,
) -> SatoriClient:
logger._level = p_log_level
return SatoriClient.new(get_client_adapter(), p_api_key, p_scheme, p_host, p_port, p_timeout)
@@ -0,0 +1 @@
uid://b054oblo2phs
@@ -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
File diff suppressed because it is too large. Load diff
@@ -0,0 +1 @@
uid://ubpf6wdqaitt
@@ -0,0 +1,902 @@
extends NakamaAsyncResult
class_name NakamaRTAPI
# A chat channel on the server.
class Channel extends NakamaAsyncResult:
const _SCHEMA = {
"id": {"name": "id", "type": TYPE_STRING, "required": true},
"presences": {"name": "presences", "type": TYPE_ARRAY, "required": false, "content": "UserPresence"},
"self": {"name": "self_presence", "type": "UserPresence", "required": true},
"room_name": {"name": "room_name", "type": TYPE_STRING, "required": false},
"group_id": {"name": "group_id", "type": TYPE_STRING, "required": false},
"user_id_one": {"name": "user_id_one", "type": TYPE_STRING, "required": false},
"user_id_two": {"name": "user_id_two", "type": TYPE_STRING, "required": false}
}
# The server-assigned channel ID.
var id : String
# The presences visible on the chat channel.
var presences : Array # of objects NakamaUserPresence
# The presence of the current user. i.e. Your self.
var self_presence : NakamaRTAPI.UserPresence
# The name of the chat room, or an empty string if this message was not sent through a chat room.
var room_name : String
# The ID of the group, or an empty string if this message was not sent through a group channel.
var group_id : String
# The ID of the first DM user, or an empty string if this message was not sent through a DM chat.
var user_id_one : String
# The ID of the second DM user, or an empty string if this message was not sent through a DM chat.
var user_id_two : String
func _init(p_ex = null):
super(p_ex)
func _to_string():
if is_exception(): return get_exception()._to_string()
return "Channel<id=%s, presences=%s, self=%s, room_name=%s, group_id=%s, user_id_one=%s, user_id_two=%s>" % [
id, presences, self_presence, room_name, group_id, user_id_one, user_id_two
]
static func create(p_ns : GDScript, p_dict : Dictionary) -> Channel:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "Channel", p_dict), Channel) as Channel
static func get_result_key() -> String:
return "channel"
class ChannelMessageAck extends NakamaAsyncResult:
const _SCHEMA = {
"channel_id": {"name": "channel_id", "type": TYPE_STRING, "required": true},
"code": {"name": "code", "type": TYPE_INT, "required": true},
"create_time": {"name": "create_time", "type": TYPE_STRING, "required": false},
"message_id": {"name": "message_id", "type": TYPE_STRING, "required": true},
"persistent": {"name": "persistent", "type": TYPE_BOOL, "required": false},
"update_time": {"name": "update_time", "type": TYPE_STRING, "required": false},
"username": {"name": "username", "type": TYPE_STRING, "required": false},
"room_name": {"name": "room_name", "type": TYPE_STRING, "required": false},
"group_id": {"name": "group_id", "type": TYPE_STRING, "required": false},
"user_id_one": {"name": "user_id_one", "type": TYPE_STRING, "required": false},
"user_id_two": {"name": "user_id_two", "type": TYPE_STRING, "required": false}
}
# The server-assigned channel ID.
var channel_id : String
# A user-defined code for the chat message.
var code : int
# The UNIX time when the message was created.
var create_time : String
# A unique ID for the chat message.
var message_id : String
# True if the chat message has been stored in history.
var persistent : bool
# The UNIX time when the message was updated.
var update_time : String
# The username of the sender of the message.
var username : String
# The name of the chat room, or an empty string if this message was not sent through a chat room.
var room_name : String
# The ID of the group, or an empty string if this message was not sent through a group channel.
var group_id : String
# The ID of the first DM user, or an empty string if this message was not sent through a DM chat.
var user_id_one : String
# The ID of the second DM user, or an empty string if this message was not sent through a DM chat.
var user_id_two : String
func _init(p_ex = null):
super(p_ex)
func _to_string():
if is_exception(): return get_exception()._to_string()
return "ChannelMessageAck<channel_id=%s, code=%d, create_time=%s, message_id=%s, persistent=%s, update_time=%s, username=%s room_name=%s, group_id=%s, user_id_one=%s, user_id_two=%s>" % [
channel_id, code, create_time, message_id, persistent, update_time, username, room_name, group_id, user_id_one, user_id_two
]
static func create(p_ns : GDScript, p_dict : Dictionary) -> ChannelMessageAck:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "ChannelMessageAck", p_dict), ChannelMessageAck) as ChannelMessageAck
static func get_result_key() -> String:
return "channel_message_ack"
# A batch of join and leave presences on a chat channel.
class ChannelPresenceEvent extends NakamaAsyncResult:
const _SCHEMA = {
"channel_id": {"name": "channel_id", "type": TYPE_STRING, "required": true},
"joins": {"name": "joins", "type": TYPE_ARRAY, "required": false, "content" : "UserPresence"},
"leaves": {"name": "leaves", "type": TYPE_ARRAY, "required": false, "content" : "UserPresence"},
"room_name": {"name": "room_name", "type": TYPE_STRING, "required": false},
"group_id": {"name": "group_id", "type": TYPE_STRING, "required": false},
"user_id_one": {"name": "user_id_one", "type": TYPE_STRING, "required": false},
"user_id_two": {"name": "user_id_two", "type": TYPE_STRING, "required": false}
}
# The unique identifier of the chat channel.
var channel_id : String
# Presences of the users who joined the channel.
var joins : Array # UserPresence
# Presences of users who left the channel.
var leaves : Array # UserPresence
# The name of the chat room, or an empty string if this message was not sent through a chat room.
var room_name : String
# The ID of the group, or an empty string if this message was not sent through a group channel.
var group_id : String
# The ID of the first DM user, or an empty string if this message was not sent through a DM chat.
var user_id_one : String
# The ID of the second DM user, or an empty string if this message was not sent through a DM chat.
var user_id_two : String
func _init(p_ex = null):
super(p_ex)
func _to_string():
if is_exception(): return get_exception()._to_string()
return "ChannelPresenceEvent<channel_id=%s, joins=%s, leaves=%s, room_name=%s, group_id=%s, user_id_one=%s, user_id_two=%s>" % [
channel_id, joins, leaves, room_name, group_id, user_id_one, user_id_two
]
static func create(p_ns : GDScript, p_dict : Dictionary) -> ChannelPresenceEvent:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "ChannelPresenceEvent", p_dict), ChannelPresenceEvent) as ChannelPresenceEvent
static func get_result_key() -> String:
return "channel_presence_event"
# Describes an error which occurred on the server.
class Error extends NakamaAsyncResult:
const _SCHEMA = {
"code": {"name": "code", "type": TYPE_INT, "required": true},
"message": {"name": "message", "type": TYPE_STRING, "required": true},
"context": {"name": "context", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING},
}
# The selection of possible error codes.
enum Code {
# An unexpected result from the server.
RUNTIME_EXCEPTION = 0,
# The server received a message which is not recognised.
UNRECOGNIZED_PAYLOAD = 1,
# A message was expected but contains no content.
MISSING_PAYLOAD = 2,
# Fields in the message have an invalid format.
BAD_INPUT = 3,
# The match id was not found.
MATCH_NOT_FOUND = 4,
# The match join was rejected.
MATCH_JOIN_REJECTED = 5,
# The runtime function does not exist on the server.
RUNTIME_FUNCTION_NOT_FOUND = 6,
#The runtime function executed with an error.
RUNTIME_FUNCTION_EXCEPTION = 7,
}
# The error code which should be one of "Error.Code" enums.
var code : int
# A message in English to help developers debug the response.
var message : String
# Additional error details which may be different for each response.
var context : Dictionary
func _init(p_ex = null):
super(p_ex)
func _to_string():
if is_exception(): return get_exception()._to_string()
return "Error<code=%s, messages=%s, context=%s>" % [code, message, context]
static func create(p_ns : GDScript, p_dict : Dictionary) -> Error:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "Error", p_dict), Error) as Error
static func get_result_key() -> String:
return "error"
# A multiplayer match.
class Match extends NakamaAsyncResult:
const _SCHEMA = {
"authoritative": {"name": "authoritative", "type": TYPE_BOOL, "required": false},
"match_id": {"name": "match_id", "type": TYPE_STRING, "required": true},
"label": {"name": "label", "type": TYPE_STRING, "required": false},
"presences": {"name": "presences", "type": TYPE_ARRAY, "required": false, "content": "UserPresence"},
"size": {"name": "size", "type": TYPE_INT, "required": false},
"self": {"name": "self_user", "type": "UserPresence", "required": true}
}
# If this match has an authoritative handler on the server.
var authoritative : bool
# The unique match identifier.
var match_id : String
# A label for the match which can be filtered on.
var label : String
# The presences already in the match.
var presences : Array # UserPresence
# The number of users currently in the match.
var size : int
# The current user in this match. i.e. Yourself.
var self_user : UserPresence
func _init(p_ex = null):
super(p_ex)
static func create(p_ns : GDScript, p_dict : Dictionary):
return _safe_ret(NakamaSerializer.deserialize(p_ns, "Match", p_dict), Match) as Match
func _to_string():
if is_exception(): return get_exception()._to_string()
return "Match<authoritative=%s, match_id=%s, label=%s, presences=%s, size=%d, self=%s>" % [authoritative, match_id, label, presences, size, self_user]
static func get_result_key() -> String:
return "match"
# Some game state update in a match.
class MatchData extends NakamaAsyncResult:
const _SCHEMA = {
"match_id": {"name": "match_id", "type": TYPE_STRING, "required": true},
"presence": {"name": "presence", "type": "UserPresence", "required": false},
"op_code": {"name": "op_code", "type": TYPE_INT, "required": false},
"data": {"name": "data", "type": TYPE_STRING, "required": false}
}
# The unique match identifier.
var match_id : String
# The operation code for the state change.
# This value can be used to mark the type of the contents of the state.
var op_code : int = 0
# The user that sent this game state update.
var presence : UserPresence
# The raw base64-encoded contents of the state change.
var base64_data : String
# The contents of the state change decoded as a UTF-8 string.
var _data
var data : String:
get:
if _data == null and base64_data != '':
_data = Marshalls.base64_to_utf8(base64_data)
return _data if _data != null else ''
set(v):
_data = v
# The contents of the state change decoded as binary data.
var _binary_data
var binary_data : PackedByteArray:
get:
if _binary_data == null and base64_data != '':
_binary_data = Marshalls.base64_to_raw(base64_data)
return _binary_data
func _init(p_ex = null):
super(p_ex)
func _to_string():
if is_exception(): return get_exception()._to_string()
return "MatchData<match_id=%s, op_code=%s, presence=%s, data=%s>" % [match_id, op_code, presence, data]
static func create(p_ns : GDScript, p_dict : Dictionary) -> MatchData:
var out = _safe_ret(NakamaSerializer.deserialize(p_ns, "MatchData", p_dict), MatchData) as MatchData
# Store the base64 data, ready to be decoded when the developer requests it.
if out._data != null:
out.base64_data = out._data
out._data = null
return out
static func get_result_key() -> String:
return "match_data"
# A batch of join and leave presences for a match.
class MatchPresenceEvent extends NakamaAsyncResult:
const _SCHEMA = {
"match_id": {"name": "match_id", "type": TYPE_STRING, "required": true},
"joins": {"name": "joins", "type": TYPE_ARRAY, "required": false, "content" : "UserPresence"},
"leaves": {"name": "leaves", "type": TYPE_ARRAY, "required": false, "content" : "UserPresence"},
}
# Presences of users who joined the match.
var joins : Array
# Presences of users who left the match.
var leaves : Array
# The unique match identifier.
var match_id : String
func _init(p_ex = null):
super(p_ex)
func _to_string():
if is_exception(): return get_exception()._to_string()
return "MatchPresenceEvent<match_id=%s, joins=%s, leaves=%s>" % [match_id, joins, leaves]
static func create(p_ns : GDScript, p_dict : Dictionary) -> MatchPresenceEvent:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "MatchPresenceEvent", p_dict), MatchPresenceEvent) as MatchPresenceEvent
static func get_result_key() -> String:
return "match_presence_event"
# The result of a successful matchmaker operation sent to the server.
class MatchmakerMatched extends NakamaAsyncResult:
const _SCHEMA = {
"match_id": {"name": "match_id", "type": TYPE_STRING, "required": false},
"ticket": {"name": "ticket", "type": TYPE_STRING, "required": true},
"token": {"name": "token", "type": TYPE_STRING, "required": false},
"users": {"name": "users", "type": TYPE_ARRAY, "required": false, "content": "MatchmakerUser"},
"self": {"name": "self_user", "type": "MatchmakerUser", "required": true}
}
# The id used to join the match.
# A match ID used to join the match.
var match_id : String
# The ticket sent by the server when the user requested to matchmake for other players.
var ticket : String
# The token used to join a match.
var token : String
# The other users matched with this user and the parameters they sent.
var users : Array # MatchmakerUser
# The current user who matched with opponents.
var self_user : MatchmakerUser
func _init(p_ex = null):
super(p_ex)
func _to_string():
if is_exception(): return get_exception()._to_string()
return "<MatchmakerMatched match_id=%s, ticket=%s, token=%s, users=%s, self=%s>" % [
match_id, ticket, token, users, self_user
]
static func create(p_ns : GDScript, p_dict : Dictionary) -> MatchmakerMatched:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "MatchmakerMatched", p_dict), MatchmakerMatched) as MatchmakerMatched
static func get_result_key() -> String:
return "matchmaker_matched"
# The matchmaker ticket received from the server.
class MatchmakerTicket extends NakamaAsyncResult:
const _SCHEMA = {
"ticket": {"name": "ticket", "type": TYPE_STRING, "required": true}
}
# The ticket generated by the matchmaker.
var ticket : String
func _init(p_ex = null):
super(p_ex)
static func create(p_ns : GDScript, p_dict : Dictionary) -> MatchmakerTicket:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "MatchmakerTicket", p_dict), MatchmakerTicket) as MatchmakerTicket
func _to_string():
if is_exception(): return get_exception()._to_string()
return "<MatchmakerTicket ticket=%s>" % ticket
static func get_result_key() -> String:
return "matchmaker_ticket"
# The user with the parameters they sent to the server when asking for opponents.
class MatchmakerUser extends NakamaAsyncResult:
const _SCHEMA = {
"presence": {"name": "presence", "type": "UserPresence", "required": true},
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": false},
"string_properties": {"name": "string_properties", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING},
"numeric_properties": {"name": "numeric_properties", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_FLOAT},
}
# The presence of the user.
var presence : UserPresence
# Party identifier, if this user was matched as a party member.
var party_id : String
# The numeric properties which this user asked to matchmake with.
var numeric_properties : Dictionary
# The string properties which this user asked to matchmake with.
var string_properties : Dictionary
func _init(p_ex = null):
super(p_ex)
func _to_string():
if is_exception(): return get_exception()._to_string()
return "<MatchmakerUser presence=%s, numeric_properties=%s, string_properties=%s>" % [
presence, numeric_properties, string_properties]
static func create(p_ns : GDScript, p_dict : Dictionary) -> MatchmakerUser:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "MatchmakerUser", p_dict), MatchmakerUser) as MatchmakerUser
static func get_result_key() -> String:
return "matchmaker_user"
# Receive status updates for users.
class Status extends NakamaAsyncResult:
const _SCHEMA = {
"presences": {"name": "presences", "type": TYPE_ARRAY, "required": true, "content": "UserPresence"},
}
# The status events for the users followed.
var presences := Array()
func _init(p_ex = null):
super(p_ex)
static func create(p_ns : GDScript, p_dict : Dictionary) -> Status:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "Status", p_dict), Status) as Status
func _to_string():
if is_exception(): return get_exception()._to_string()
return "<Status presences=%s>" % [presences]
static func get_result_key() -> String:
return "status"
# A status update event about other users who've come online or gone offline.
class StatusPresenceEvent extends NakamaAsyncResult:
const _SCHEMA = {
"joins": {"name": "joins", "type": TYPE_ARRAY, "required": false, "content" : "UserPresence"},
"leaves": {"name": "leaves", "type": TYPE_ARRAY, "required": false, "content" : "UserPresence"},
}
# Presences of users who joined the server.
# This join information is in response to a subscription made to be notified when a user comes online.
var joins : Array
# Presences of users who left the server.
# This leave information is in response to a subscription made to be notified when a user goes offline.
var leaves : Array
func _init(p_ex = null):
super(p_ex)
func _to_string():
if is_exception(): return get_exception()._to_string()
return "StatusPresenceEvent<joins=%s, leaves=%s>" % [joins, leaves]
static func create(p_ns : GDScript, p_dict : Dictionary) -> StatusPresenceEvent:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "StatusPresenceEvent", p_dict), StatusPresenceEvent) as StatusPresenceEvent
static func get_result_key() -> String:
return "status_presence_event"
# A realtime socket stream on the server.
class Stream extends NakamaAsyncResult:
const _SCHEMA = {
"mode": {"name": "mode", "type": TYPE_INT, "required": true},
"subject": {"name": "subject", "type": TYPE_STRING, "required": false},
"subcontext": {"name": "subcontext", "type": TYPE_STRING, "required": false},
"label": {"name": "label", "type": TYPE_STRING, "required": false},
}
# The mode of the stream.
var mode : int
# The subject of the stream. This is usually a user id.
var subject : String
# The descriptor of the stream. Used with direct chat messages and contains a second user id.
var subcontext : String
# Identifies streams which have a context across users like a chat channel room.
var label : String
func _init(p_ex = null):
super(p_ex)
func _to_string():
if is_exception(): return get_exception()._to_string()
return "Stream<mode=%s, subject=%s, subcontext=%s, label=%s>" % [mode, subject, subcontext, label]
static func create(p_ns : GDScript, p_dict : Dictionary) -> Stream:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "Stream", p_dict), Stream) as Stream
static func get_result_key() -> String:
return "stream"
# A batch of joins and leaves on the low level stream.
# Streams are built on to provide abstractions for matches, chat channels, etc. In most cases you'll never need to
# interact with the low level stream itself.
class StreamPresenceEvent extends NakamaAsyncResult:
const _SCHEMA = {
"stream": {"name": "stream", "type": "Stream", "required": true},
"joins": {"name": "joins", "type": TYPE_ARRAY, "required": false, "content" : "UserPresence"},
"leaves": {"name": "leaves", "type": TYPE_ARRAY, "required": false, "content" : "UserPresence"},
}
# Presences of users who left the stream.
var joins : Array
# Presences of users who joined the stream.
var leaves : Array
# The identifier for the stream.
var stream : Stream = null
func _to_string():
if is_exception(): return get_exception()._to_string()
return "StreamPresenceEvent<stream=%s, joins=%s, leaves=%s>" % [stream, joins, leaves]
static func create(p_ns : GDScript, p_dict : Dictionary) -> StreamPresenceEvent:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "StreamPresenceEvent", p_dict), StreamPresenceEvent) as StreamPresenceEvent
static func get_result_key() -> String:
return "stream_presence_event"
# A state change received from a stream.
class StreamData extends NakamaAsyncResult:
const _SCHEMA = {
"stream": {"name": "stream", "type": "Stream", "required": true},
"sender": {"name": "sender", "type": "UserPresence", "required": false},
"data": {"name": "state", "type": TYPE_STRING, "required": false},
"reliable": {"name": "reliable", "type": TYPE_BOOL, "required": false},
}
# The user who sent the state change. May be `null`.
var sender : UserPresence = null
# The contents of the state change.
var state : String
# The identifier for the stream.
var stream : Stream
# True if this data was delivered reliably, false otherwise.
var reliable : bool
func _to_string():
if is_exception(): return get_exception()._to_string()
return "StreamData<sender=%s, state=%s, stream=%s>" % [sender, state, stream]
static func create(p_ns : GDScript, p_dict : Dictionary) -> StreamData:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "StreamData", p_dict), StreamData) as StreamData
static func get_result_key() -> String:
return "stream_data"
# An object which represents a connected user in the server.
# The server allows the same user to be connected with multiple sessions. To uniquely identify them a tuple of
# `{ node_id, user_id, session_id }` is used which is exposed as this object.
class UserPresence extends NakamaAsyncResult:
const _SCHEMA = {
"persistence": {"name": "persistence", "type": TYPE_BOOL, "required": false},
"session_id": {"name": "session_id", "type": TYPE_STRING, "required": true},
"status": {"name": "status", "type": TYPE_STRING, "required": false},
"username": {"name": "username", "type": TYPE_STRING, "required": false},
"user_id": {"name": "user_id", "type": TYPE_STRING, "required": true},
}
# If this presence generates stored events like persistent chat messages or notifications.
var persistence : bool
# The session id of the user.
var session_id : String
# The status of the user with the presence on the server.
var status : String
# The username for the user.
var username : String
# The id of the user.
var user_id : String
func _init(p_ex = null):
super(p_ex)
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func _to_string():
if is_exception(): return get_exception()._to_string()
return "UserPresence<persistence=%s, session_id=%s, status=%s, username=%s, user_id=%s>" % [
persistence, session_id, status, username, user_id]
static func create(p_ns : GDScript, p_dict : Dictionary) -> UserPresence:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "UserPresence", p_dict), UserPresence) as UserPresence
static func get_result_key() -> String:
return "user_presence"
class Party extends NakamaAsyncResult:
const _SCHEMA = {
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
"open": {"name": "open", "type": TYPE_BOOL, "required": false},
"max_size": {"name": "max_size", "type": TYPE_INT, "required": true},
"self": {"name": "self_presence", "type": "UserPresence", "required": true},
"leader": {"name": "leader", "type": "UserPresence", "required": true},
"presences": {"name": "presences", "type": TYPE_ARRAY, "required": false, "content": "UserPresence"},
}
# Unique party identifier.
var party_id : String
# Open flag.
var open : bool = false
# Maximum number of party members.
var max_size : int
# The presence of the current user. i.e. Your self.
var self_presence : NakamaRTAPI.UserPresence
# Leader.
var leader : NakamaRTAPI.UserPresence
# All current party members.
var presences : Array # of objects NakamaUserPresence
func _init(p_ex = null):
super(p_ex)
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func _to_string():
if is_exception(): return get_exception()._to_string()
return "Party<party_id=%s, open=%s, max_size=%d, self=%s, leader=%s, presences=%s>" % [
party_id, open, max_size, self_presence, leader, presences]
static func create(p_ns : GDScript, p_dict : Dictionary) -> Party:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "Party", p_dict), Party) as Party
static func get_result_key() -> String:
return "party"
# Presence update for a particular party.
class PartyPresenceEvent extends NakamaAsyncResult:
const _SCHEMA = {
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
"joins": {"name": "joins", "type": TYPE_ARRAY, "required": false, "content": "UserPresence"},
"leaves": {"name": "leaves", "type": TYPE_ARRAY, "required": false, "content": "UserPresence"},
}
# The party ID.
var party_id : String
# User presences that have just joined the party.
var joins : Array
# User presences that have just left the party.
var leaves : Array
func _init(p_ex = null):
super(p_ex)
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func _to_string():
if is_exception(): return get_exception()._to_string()
return "PartyPresenceEvent<party_id=%s, joins=%s, leaves=%s>" % [party_id, joins, leaves]
static func create(p_ns : GDScript, p_dict : Dictionary) -> PartyPresenceEvent:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "PartyPresenceEvent", p_dict), PartyPresenceEvent) as PartyPresenceEvent
static func get_result_key() -> String:
return "party_presence_event"
# Announcement of a new party leader.
class PartyLeader extends NakamaAsyncResult:
const _SCHEMA = {
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
"presence": {"name": "presence", "type": "UserPresence", "required": true},
}
# Party ID to promote a new leader for.
var party_id : String
# The presence of an existing party member to promote as the new leader.
var presence : NakamaRTAPI.UserPresence
func _init(p_ex = null):
super(p_ex)
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func _to_string():
if is_exception(): return get_exception()._to_string()
return "PartyLeader<party_id=%s, presence=%s>" % [party_id, presence]
static func create(p_ns : GDScript, p_dict : Dictionary) -> PartyLeader:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "PartyLeader", p_dict), PartyLeader) as PartyLeader
static func get_result_key() -> String:
return "party_leader"
# Incoming notification for one or more new presences attempting to join the party.
class PartyJoinRequest extends NakamaAsyncResult:
const _SCHEMA = {
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
"presences": {"name": "presences", "type": TYPE_ARRAY, "required": false, "content": "UserPresence"},
}
# Party ID these presences are attempting to join.
var party_id : String
# Presences attempting to join.
var presences : Array
func _init(p_ex = null):
super(p_ex)
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func _to_string():
if is_exception(): return get_exception()._to_string()
return "PartyJoinRequest<party_id=%s, presences=%s>" % [party_id, presences]
static func create(p_ns : GDScript, p_dict : Dictionary) -> PartyJoinRequest:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "PartyJoinRequest", p_dict), PartyJoinRequest) as PartyJoinRequest
static func get_result_key() -> String:
return "party_join_request"
# A response from starting a new party matchmaking process.
class PartyMatchmakerTicket extends NakamaAsyncResult:
const _SCHEMA = {
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
"ticket": {"name": "ticket", "type": TYPE_STRING, "required": true},
}
# Party ID.
var party_id : String
# The ticket that can be used to cancel matchmaking.
var ticket : String
func _init(p_ex = null):
super(p_ex)
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func _to_string():
if is_exception(): return get_exception()._to_string()
return "PartyMatchmakerTicket<party_id=%s, ticket=%s>" % [party_id, ticket]
static func create(p_ns : GDScript, p_dict : Dictionary) -> PartyMatchmakerTicket:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "PartyMatchmakerTicket", p_dict), PartyMatchmakerTicket) as PartyMatchmakerTicket
static func get_result_key() -> String:
return "party_matchmaker_ticket"
# Incoming party data delivered from the server.
class PartyData extends NakamaAsyncResult:
const _SCHEMA = {
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
"presence": {"name": "presence", "type": "UserPresence", "required": false},
"op_code": {"name": "op_code", "type": TYPE_INT, "required": true},
"data": {"name": "data", "type": TYPE_STRING, "required": false}
}
# The party ID.
var party_id : String
# A reference to the user presence that sent this data, if any.
var presence : NakamaRTAPI.UserPresence
# Op code value.
var op_code : int
# The raw base64-encoded contents of the state change.
var base64_data : String
# The contents of the state change decoded as a UTF-8 string.
var _data
var data : String:
get:
if _data == null and base64_data != '':
_data = Marshalls.base64_to_utf8(base64_data)
return _data if _data != null else ''
set(v):
_data = v
# The contents of the state change decoded as binary data.
var _binary_data
var binary_data : PackedByteArray:
get:
if _binary_data == null and base64_data != '':
_binary_data = Marshalls.base64_to_raw(base64_data)
return _binary_data
func _init(p_ex = null):
super(p_ex)
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func _to_string():
if is_exception(): return get_exception()._to_string()
return "PartyData<party_id=%s, presence=%s, op_code=%d, data%s>" % [party_id, presence, op_code, data]
static func create(p_ns : GDScript, p_dict : Dictionary) -> PartyData:
var out := _safe_ret(NakamaSerializer.deserialize(p_ns, "PartyData", p_dict), PartyData) as PartyData
# Store the base64 data, ready to be decoded when the developer requests it.
if out._data != null:
out.base64_data = out._data
out._data = null
return out
static func get_result_key() -> String:
return "party_data"
# End a party, kicking all party members and closing it. (this is both a message and a result)
class PartyClose extends NakamaAsyncResult:
const _SCHEMA = {
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
}
# Party ID to close.
var party_id : String
func _init(p_ex = null):
super(p_ex)
func serialize():
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "party_close"
func _to_string():
if is_exception(): return get_exception()._to_string()
return "PartyClose<party_id=%s>" % [party_id]
static func create(p_ns : GDScript, p_dict : Dictionary) -> PartyClose:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "PartyClose", p_dict), PartyClose) as PartyClose
static func get_result_key() -> String:
return "party_close"
@@ -0,0 +1 @@
uid://bmho1of843y0x
@@ -0,0 +1,635 @@
extends RefCounted
class_name NakamaRTMessage
# Send a channel join message to the server.
class ChannelJoin:
const _SCHEMA = {
"persistence": {"name": "persistence", "type": TYPE_BOOL, "required": true},
"hidden": {"name": "hidden", "type": TYPE_BOOL, "required": true},
"target": {"name": "target", "type": TYPE_STRING, "required": true},
"type": {"name": "type", "type": TYPE_INT, "required": true},
}
enum ChannelType {
# A chat room which can be created dynamically with a name.
Room = 1,
# A private chat between two users.
DirectMessage = 2,
# A chat within a group on the server.
Group = 3
}
var persistence : bool
var hidden : bool
var target : String
var type : int
func _init(p_target : String, p_type : int, p_persistence : bool, p_hidden : bool):
persistence = p_persistence
hidden = p_hidden
target = p_target
type = p_type if p_type >= ChannelType.Room and p_type <= ChannelType.Group else 0 # Will cause error server side
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "channel_join"
func _to_string():
return "ChannelJoin<persistence=%s, hidden=%s, target=%s, type=%d>" % [persistence, hidden, target, type]
# A leave message for a match on the server.
class ChannelLeave extends NakamaAsyncResult:
const _SCHEMA = {
"channel_id": {"name": "channel_id", "type": TYPE_STRING, "required": true}
}
var channel_id : String
func _init(p_channel_id : String):
channel_id = p_channel_id
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "channel_leave"
func _to_string():
return "ChannelLeave<channel_id=%s>" % [channel_id]
class ChannelMessageRemove extends NakamaAsyncResult:
const _SCHEMA = {
"channel_id": {"name": "channel_id", "type": TYPE_STRING, "required": true},
"message_id": {"name": "message_id", "type": TYPE_STRING, "required": true}
}
var channel_id : String
var message_id : String
func _init(p_channel_id : String, p_message_id):
channel_id = p_channel_id
message_id = p_message_id
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "channel_message_remove"
func _to_string():
return "ChannelMessageRemove<channel_id=%s, message_id=%s>" % [channel_id, message_id]
# Send a chat message to a channel on the server.
class ChannelMessageSend extends NakamaAsyncResult:
const _SCHEMA = {
"channel_id": {"name": "channel_id", "type": TYPE_STRING, "required": true},
"content": {"name": "content", "type": TYPE_STRING, "required": true}
}
var channel_id : String
var content : String
func _init(p_channel_id : String, p_content):
channel_id = p_channel_id
content = p_content
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "channel_message_send"
func _to_string():
return "ChannelMessageSend<channel_id=%s, content=%s>" % [channel_id, content]
class ChannelMessageUpdate extends NakamaAsyncResult:
const _SCHEMA = {
"channel_id": {"name": "channel_id", "type": TYPE_STRING, "required": true},
"message_id": {"name": "message_id", "type": TYPE_STRING, "required": true},
"content": {"name": "content", "type": TYPE_STRING, "required": true}
}
var channel_id : String
var message_id : String
var content : String
func _init(p_channel_id : String, p_message_id, p_content : String):
channel_id = p_channel_id
message_id = p_message_id
content = p_content
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "channel_message_update"
func _to_string():
return "ChannelMessageUpdate<channel_id=%s, message_id=%s, content=%s>" % [channel_id, message_id, content]
# A create message for a match on the server.
class MatchCreate extends NakamaAsyncResult:
const _SCHEMA = {
"name": {"name": "name", "type": TYPE_STRING, "required": false},
}
var name = null
func _init(p_name = null):
name = p_name if p_name else null
func serialize():
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "match_create"
func _to_string():
return "MatchCreate<name=%s>" % [name]
# A join message for a match on the server.
class MatchJoin extends NakamaAsyncResult:
const _SCHEMA = {
"match_id": {"name": "match_id", "type": TYPE_STRING, "required": false},
"token": {"name": "token", "type": TYPE_STRING, "required": false},
"metadata": {"name": "metadata", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING},
}
# These two are mutually exclusive and set manually by socket for now, so use null.
var match_id = null
var token = null
var metadata = null
func _init(p_ex=null):
super(p_ex)
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "match_join"
func _to_string():
return "MatchJoin<match_id=%s, token=%s, metadata=%s>" % [match_id, token, metadata]
# A leave message for a match on the server.
class MatchLeave extends NakamaAsyncResult:
const _SCHEMA = {
"match_id": {"name": "match_id", "type": TYPE_STRING, "required": true}
}
var match_id : String
func _init(p_match_id : String):
match_id = p_match_id
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "match_leave"
func _to_string():
return "MatchLeave<match_id=%s>" % [match_id]
# Send new state to a match on the server.
class MatchDataSend extends NakamaAsyncResult:
const _SCHEMA = {
"match_id": {"name": "match_id", "type": TYPE_STRING, "required": true},
"op_code": {"name": "op_code", "type": TYPE_INT, "required": true},
"presences": {"name": "presences", "type": TYPE_ARRAY, "required": false, "content": "UserPresence"},
"data": {"name": "data", "type": TYPE_STRING, "required": true},
}
var match_id : String
var presences = null
var op_code : int
var data : String
func _init(p_match_id : String, p_op_code : int, p_data : String, p_presences):
match_id = p_match_id
presences = p_presences
op_code = p_op_code
data = p_data
func serialize():
return NakamaSerializer.serialize(self)
func get_msg_key():
return "match_data_send"
func _to_string():
return "MatchDataSend<match_id=%s, op_code=%s, presences=%s, data=%s>" % [match_id, op_code, presences, data]
# Add the user to the matchmaker pool with properties.
class MatchmakerAdd extends NakamaAsyncResult:
const _SCHEMA = {
"query": {"name": "query", "type": TYPE_STRING, "required": true},
"max_count": {"name": "max_count", "type": TYPE_INT, "required": true},
"min_count": {"name": "min_count", "type": TYPE_INT, "required": true},
"numeric_properties": {"name": "numeric_properties", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_FLOAT},
"string_properties": {"name": "string_properties", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING},
"count_multiple": {"name": "count_multiple", "type": TYPE_INT, "required": false},
}
var query : String = "*"
var max_count : int = 8
var min_count : int = 2
var string_properties : Dictionary
var numeric_properties : Dictionary
var count_multiple
func _init(p_query : String = "*", p_min_count : int = 2, p_max_count : int = 8,
p_string_props : Dictionary = Dictionary(), p_numeric_props : Dictionary = Dictionary(),
p_count_multiple : int = 0):
query = p_query
min_count = p_min_count
max_count = p_max_count
string_properties = p_string_props
numeric_properties = p_numeric_props
count_multiple = p_count_multiple if p_count_multiple > 0 else null
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "matchmaker_add"
func _to_string():
return "MatchmakerAdd<query=%s, max_count=%d, min_count=%d, numeric_properties=%s, string_properties=%s, count_multiple=%s>" % [query, max_count, min_count, numeric_properties, string_properties, count_multiple]
# Remove the user from the matchmaker pool by ticket.
class MatchmakerRemove extends NakamaAsyncResult:
const _SCHEMA = {
"ticket": {"name": "ticket", "type": TYPE_STRING, "required": true}
}
var ticket : String
func _init(p_ticket : String):
ticket = p_ticket
func serialize():
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "matchmaker_remove"
func _to_string():
return "MatchmakerRemove<ticket=%s>" % [ticket]
# Follow one or more other users for status updates.
class StatusFollow extends NakamaAsyncResult:
const _SCHEMA = {
"user_ids": {"name": "user_ids", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING},
"usernames": {"name": "usernames", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING},
}
var user_ids := PackedStringArray()
var usernames := PackedStringArray()
func _init(p_ids : PackedStringArray, p_usernames : PackedStringArray):
user_ids = p_ids
usernames = p_usernames
func serialize():
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "status_follow"
func _to_string():
return "StatusFollow<user_ids=%s, usernames=%s>" % [user_ids, usernames]
# Unfollow one or more users on the server.
class StatusUnfollow extends NakamaAsyncResult:
const _SCHEMA = {
"user_ids": {"name": "user_ids", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING},
}
var user_ids := PackedStringArray()
func _init(p_ids : PackedStringArray):
user_ids = p_ids
func serialize():
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "status_unfollow"
func _to_string():
return "StatusUnfollow<user_ids=%s>" % [user_ids]
# Unfollow one or more users on the server.
class StatusUpdate extends NakamaAsyncResult:
const _SCHEMA = {
"status": {"name": "status", "type": TYPE_STRING, "required": true},
}
var status : String
func _init(p_status : String):
status = p_status
func serialize():
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "status_update"
func _to_string():
return "StatusUpdate<status=%s>" % [status]
# Create a party.
class PartyCreate extends NakamaAsyncResult:
const _SCHEMA = {
"open": {"name": "open", "type": TYPE_BOOL, "required": true},
"max_size": {"name": "max_size", "type": TYPE_INT, "required": true},
}
# Whether or not the party will require join requests to be approved by the party leader.
var open : bool
# Maximum number of party members.
var max_size : int
func _init(p_open : bool, p_max_size : int):
open = p_open
max_size = p_max_size
func serialize():
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "party_create"
func _to_string():
return "PartyCreate<open=%s, max_size=%d>" % [open, max_size]
# Join a party, or request to join if the party is not open.
class PartyJoin extends NakamaAsyncResult:
const _SCHEMA = {
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
}
# Party ID to join.
var party_id : String
func _init(p_id : String):
party_id = p_id
func serialize():
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "party_join"
func _to_string():
return "PartyJoin<party_id=%s>" % [party_id]
# Leave a party.
class PartyLeave extends NakamaAsyncResult:
const _SCHEMA = {
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
}
# Party ID to leave.
var party_id : String
func _init(p_id : String):
party_id = p_id
func serialize():
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "party_leave"
func _to_string():
return "PartyLeave<party_id=%s>" % [party_id]
# Promote a new party leader.
class PartyPromote extends NakamaAsyncResult:
const _SCHEMA = {
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
"presence": {"name": "presence", "type": "UserPresence", "required": true},
}
# Party ID to promote a new leader for.
var party_id : String
# The presence of an existing party member to promote as the new leader.
var presence : NakamaRTAPI.UserPresence
func _init(p_id : String, p_presence : NakamaRTAPI.UserPresence):
party_id = p_id
presence = p_presence
func serialize():
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "party_promote"
func _to_string():
return "PartyPromote<party_id=%s, presence=%s>" % [party_id, presence]
# Accept a request to join.
class PartyAccept extends NakamaAsyncResult:
const _SCHEMA = {
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
"presence": {"name": "presence", "type": "UserPresence", "required": true},
}
# Party ID to accept a join request for.
var party_id : String
# The presence to accept as a party member.
var presence : NakamaRTAPI.UserPresence
func _init(p_id : String, p_presence : NakamaRTAPI.UserPresence):
party_id = p_id
presence = p_presence
func serialize():
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "party_accept"
func _to_string():
return "PartyAccept<party_id=%s, presence=%s>" % [party_id, presence]
# Kick a party member, or decline a request to join.
class PartyRemove extends NakamaAsyncResult:
const _SCHEMA = {
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
"presence": {"name": "presence", "type": "UserPresence", "required": true},
}
# Party ID to remove/reject from.
var party_id : String
# The presence to remove or reject.
var presence : NakamaRTAPI.UserPresence
func _init(p_id : String, p_presence : NakamaRTAPI.UserPresence):
party_id = p_id
presence = p_presence
func serialize():
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "party_remove"
func _to_string():
return "PartyRemove<party_id=%s, presence=%s>" % [party_id, presence]
# Request a list of pending join requests for a party.
class PartyJoinRequestList extends NakamaAsyncResult:
const _SCHEMA = {
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
}
# Party ID to get a list of join requests for.
var party_id : String
func _init(p_id : String):
party_id = p_id
func serialize():
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "party_join_request_list"
func _to_string():
return "PartyJoinRequestList<party_id=%s>" % [party_id]
# Begin matchmaking as a party.
class PartyMatchmakerAdd extends NakamaAsyncResult:
const _SCHEMA = {
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
"min_count": {"name": "min_count", "type": TYPE_INT, "required": true},
"max_count": {"name": "max_count", "type": TYPE_INT, "required": true},
"query": {"name": "query", "type": TYPE_STRING, "required": false},
"string_properties": {"name": "string_properties", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING},
"numeric_properties": {"name": "numeric_properties", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_FLOAT},
}
# Party ID.
var party_id : String
# Minimum total user count to match together.
var min_count : int
# Maximum total user count to match together.
var max_count : int
# Filter query used to identify suitable users.
var query : String
# String properties.
var string_properties : Dictionary
# Numeric properties.
var numeric_properties : Dictionary
# Optional multiple of the count that must be satisfied.
var count_multiple
func _init(p_id : String, p_min_count : int, p_max_count : int, p_query : String, p_string_properties = null, p_numeric_properties = null, p_count_multiple = null):
party_id = p_id
min_count = p_min_count
max_count = p_max_count
query = p_query
string_properties = p_string_properties
numeric_properties = p_numeric_properties
count_multiple = p_count_multiple
func serialize():
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "party_matchmaker_add"
func _to_string():
return "PartyMatchmakerAdd<party_id=%s, min_count=%d, max_count=%d, query=%s, string_properties=%s, numeric_properties=%s, count_multiple=%s>" % [party_id, min_count, max_count, query, string_properties, numeric_properties, count_multiple]
# Cancel a party matchmaking process using a ticket.
class PartyMatchmakerRemove extends NakamaAsyncResult:
const _SCHEMA = {
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
"ticket": {"name": "ticket", "type": TYPE_STRING, "required": true},
}
# Party ID.
var party_id : String
# The ticket to cancel.
var ticket : String
func _init(p_id : String, p_ticket : String):
party_id = p_id
ticket = p_ticket
func serialize():
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "party_matchmaker_remove"
func _to_string():
return "PartyMatchmakerRemove<party_id=%s, ticket=%s>" % [party_id, ticket]
# Send data to a party.
class PartyDataSend extends NakamaAsyncResult:
const _SCHEMA = {
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
"op_code": {"name": "op_code", "type": TYPE_INT, "required": true},
"data": {"name": "data", "type": TYPE_STRING, "required": false}
}
# Party ID to send to.
var party_id : String
# Op code value.
var op_code : int
# Data payload, if any.
var data = null
func _init(p_id : String, p_op_code : int, p_data = null):
party_id = p_id
op_code = p_op_code
data = p_data
func serialize():
return NakamaSerializer.serialize(self)
func get_msg_key() -> String:
return "party_data_send"
func _to_string():
return "PartyDataSend<party_id=%s, op_code=%d, data=%s>" % [party_id, op_code, data]
@@ -0,0 +1 @@
uid://cgdcphgdnrgof
@@ -0,0 +1,157 @@
extends NakamaAsyncResult
class_name NakamaSession
var _created : bool = false
var created : bool:
set(v):
pass
get:
return _created
var _token : String = ""
var token : String:
set(v):
pass
get:
return _token
var _create_time : int = 0
var create_time : int:
set(v):
pass
get:
return _create_time
var _expire_time : int = 0
var expire_time : int:
set(v):
pass
get:
return _expire_time
var expired : bool:
set(v):
pass
get:
return is_expired()
var _vars : Dictionary = {}
var vars : Dictionary:
set(v):
pass
get:
return _vars
var _username : String = ""
var username : String:
set(v):
pass
get:
return _username
var _user_id : String = ""
var user_id : String:
set(v):
pass
get:
return _user_id
var _refresh_token : String = ""
var refresh_token : String:
set(v):
pass
get:
return _refresh_token
var _refresh_expire_time : int = 0
var refresh_expire_time : int:
set(v):
pass
get:
return _refresh_expire_time
var _valid : bool = false
var valid : bool:
set(v):
pass
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 is_refresh_expired() -> bool:
return _refresh_expire_time < Time.get_unix_time_from_system()
func is_valid():
return _valid
func _init(p_token = null, p_created : bool = false, p_refresh_token = null, p_exception = null):
super(p_exception)
if p_token:
_created = p_created
_parse_token(p_token)
if p_refresh_token:
_parse_refresh_token(p_refresh_token)
func refresh(p_session):
if p_session.token:
_parse_token(p_session.token)
if p_session.refresh_token:
_parse_refresh_token(p_session.refresh_token)
func _parse_token(p_token):
var decoded = _jwt_unpack(p_token)
if decoded.is_empty():
_valid = false
return
_valid = true
_token = p_token
_create_time = Time.get_unix_time_from_system()
_expire_time = int(decoded.get("exp", 0))
_username = str(decoded.get("usn", ""))
_user_id = str(decoded.get("uid", ""))
_vars = {}
if decoded.has("vrs") and typeof(decoded["vrs"]) == TYPE_DICTIONARY:
for k in decoded["vrs"]:
_vars[k] = decoded["vrs"][k]
func _parse_refresh_token(p_refresh_token):
var decoded = _jwt_unpack(p_refresh_token)
if decoded.is_empty():
return
_refresh_expire_time = int(decoded.get("exp", 0))
_refresh_token = p_refresh_token
func _to_string():
if is_exception():
return get_exception()._to_string()
return "Session<created=%s, token=%s, create_time=%d, username=%s, user_id=%s, vars=%s, expire_time=%d, refresh_token=%s refresh_expire_time=%d>" % [
_created, _token, _create_time, _username, _user_id, str(_vars), _expire_time, _refresh_token, _refresh_expire_time]
func _jwt_unpack(p_token : String) -> Dictionary:
# Hack decode JSON payload from JWT.
if p_token.find(".") == -1:
_ex = NakamaException.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 = NakamaException.new("Unable to unpack token: %s" % p_token)
return {}
@@ -0,0 +1 @@
uid://dsh8a634ucqal
@@ -0,0 +1,34 @@
extends RefCounted
class_name NakamaStorageObjectId
# The collection which stores the object.
var collection : String
# The key of the object within the collection.
var key : String
# The user owner of the object.
var user_id : String
# The version hash of the object.
var version : String
func _init(p_collection, p_key, p_user_id = "", p_version = ""):
collection = p_collection
key = p_key
user_id = p_user_id
version = p_version
func as_delete():
return NakamaAPI.ApiDeleteStorageObjectId.create(NakamaAPI, {
"collection": collection,
"key": key,
"version": version
})
func as_read():
return NakamaAPI.ApiReadStorageObjectId.create(NakamaAPI, {
"collection": collection,
"key": key,
"user_id": user_id
})
@@ -0,0 +1 @@
uid://78rssnx4qycs
@@ -0,0 +1,28 @@
extends RefCounted
class_name NakamaWriteStorageObject
var collection : String
var key : String
var permission_read : int = 0
var permission_write : int = 0
var value : String
var version : String
func _init(p_collection : String, p_key : String, p_permission_read : int,
p_permission_write : int, p_value : String, p_version : String):
collection = p_collection
key = p_key
permission_read = p_permission_read
permission_write = p_permission_write
value = p_value
version = p_version
func as_write():
return NakamaAPI.ApiWriteStorageObject.create(NakamaAPI, {
"collection": collection,
"key": key,
"permission_read": permission_read,
"permission_write": permission_write,
"value": value,
"version": version
})
@@ -0,0 +1 @@
uid://b6u4m5kmvo0s3
File diff suppressed because it is too large. Load diff
@@ -0,0 +1 @@
uid://cvfls3uixjbl1
@@ -0,0 +1,208 @@
@tool
extends Node
# An adapter which implements the HTTP protocol.
class_name NakamaHTTPAdapter
# The logger to use with the adapter.
var logger : RefCounted = NakamaLogger.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 : NakamaLogger
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 : NakamaLogger):
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)
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 NakamaException.new("Request cancelled", -1, -1, true)
elif result != HTTPRequest.RESULT_SUCCESS:
if result == null:
result = 0
return NakamaException.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 NakamaException.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 NakamaException.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://bb1hexmpk3uue
@@ -0,0 +1,112 @@
// Copyright 2022 The Nakama Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Nakama.TinyJson;
using Godot;
namespace Nakama {
/// <summary>
/// An Http adapter which uses Godot's HttpRequest node.
/// </summary>
/// <remarks>
/// Note Content-Type header is always set as 'application/json'.
/// </remarks>
public partial class GodotHttpAdapter : Node, IHttpAdapter {
/// <inheritdoc cref="IHttpAdapter.Logger"/>
public ILogger Logger { get; set; }
/// <inheritdoc cref="IHttpAdapter.TransientExceptionDelegate"/>
public TransientExceptionDelegate TransientExceptionDelegate => IsTransientException;
/// <inheritdoc cref="IHttpAdapter"/>
public async Task<string> SendAsync(string method, Uri uri, IDictionary<string, string> headers,
byte[] body, int timeout, CancellationToken? cancellationToken)
{
var req = new HttpRequest();
req.Timeout = timeout;
if (OS.GetName() != "HTML5") {
req.UseThreads = true;
}
var godot_method = HttpClient.Method.Get;
if (method == "POST") {
godot_method = HttpClient.Method.Post;
}
else if (method == "PUT") {
godot_method = HttpClient.Method.Put;
}
else if (method == "DELETE") {
godot_method = HttpClient.Method.Delete;
}
else if (method == "HEAD") {
godot_method = HttpClient.Method.Head;
}
var headers_array = new String[headers.Count + 1];
headers_array[0] = "Accept: application/json";
int index = 1;
foreach (var item in headers) {
headers_array[index] = item.Key + ": " + item.Value;
index++;
}
string body_string = body != null ? System.Text.Encoding.UTF8.GetString(body) : "";
AddChild(req);
req.Request(uri.ToString(), headers_array, godot_method, body_string);
Logger?.InfoFormat("Send: method='{0}', uri='{1}', body='{2}'", method, uri, body_string);
Variant[] resultObjects = await ToSignal(req, "request_completed");
HttpRequest.Result result = (HttpRequest.Result)(long)resultObjects[0];
long response_code = (long)resultObjects[1];
string response_body = System.Text.Encoding.UTF8.GetString((byte[])resultObjects[3]);
req.QueueFree();
Logger?.InfoFormat("Received: status={0}, contents='{1}'", response_code, response_body);
if (result == HttpRequest.Result.Success && response_code >= 200 && response_code <= 299) {
return response_body;
}
var decoded = response_body.FromJson<Dictionary<string, object>>();
string message = decoded.ContainsKey("message") ? decoded["message"].ToString() : string.Empty;
int grpcCode = decoded.ContainsKey("code") ? (int) decoded["code"] : -1;
var exception = new ApiResponseException(response_code, message, grpcCode);
if (decoded.ContainsKey("error"))
{
IHttpAdapterUtil.CopyResponseError(this, decoded["error"], exception);
}
throw exception;
}
private static bool IsTransientException(Exception e) {
return e is ApiResponseException apiException && (apiException.StatusCode >= 500 || apiException.StatusCode == -1);
}
}
}
@@ -0,0 +1,79 @@
// Copyright 2022 The Nakama Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using Godot;
namespace Nakama {
/// <summary>
/// A logger which prints to the Godot console.
/// </summary>
public class GodotLogger : ILogger {
/// <summary>
/// The log level.
/// </summary>
public enum LogLevel {
NONE,
ERROR,
WARNING,
INFO,
DEBUG,
}
private string module;
private LogLevel level;
/// <summary>
/// Constructs a GodotLogger.
/// </summary>
/// <param name="p_module">The label to use for log entries.</param>
/// <param name="p_level">The log level (or lower) to print to the console.</param>
public GodotLogger(string p_module = "Nakama", LogLevel p_level = LogLevel.ERROR) {
module = p_module;
level = p_level;
}
/// <inheritdoc cref="ILogger"/>
public void ErrorFormat(string format, params object[] args) {
if (level >= LogLevel.ERROR) {
GD.PrintErr("=== " + module + " : ERROR === " + String.Format(format, args));
}
}
/// <inheritdoc cref="ILogger"/>
public void WarnFormat(string format, params object[] args) {
if (level >= LogLevel.WARNING) {
GD.Print("=== " + module + " : WARN === " + String.Format(format, args));
}
}
/// <inheritdoc cref="ILogger"/>
public void InfoFormat(string format, params object[] args) {
if (level >= LogLevel.INFO) {
GD.Print("=== " + module + " : INFO === " + String.Format(format, args));
}
}
/// <inheritdoc cref="ILogger"/>
public void DebugFormat(string format, params object[] args) {
if (level >= LogLevel.DEBUG) {
GD.Print("=== " + module + " : DEBUG === " + String.Format(format, args));
}
}
}
}
@@ -0,0 +1,211 @@
// Copyright 2022 The Nakama Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Threading;
using System.Threading.Tasks;
using Godot;
namespace Nakama
{
/// <summary>
/// An exception that is thrown when the WebSocket is unable to connect.
/// </summary>
public class GodotWebSocketConnectionException : Exception {
public GodotWebSocketConnectionException(string message = "WebSocket unable to connect")
: base(message) { }
}
/// <summary>
/// An exception that is thrown when the WebSocket is unable to send.
/// </summary>
public class GodotWebSocketSendException : Exception {
public GodotWebSocketSendException() : base("Unable to send over WebSocket") { }
}
/// <summary>
/// A socket adapter which uses Godot's WebSocketPeer.
/// </summary>
public partial class GodotWebSocketAdapter : Node, ISocketAdapter
{
/// <inheritdoc cref="ISocketAdapter.Connected"/>
public event Action Connected;
/// <inheritdoc cref="ISocketAdapter.Closed"/>
public event Action Closed;
/// <inheritdoc cref="ISocketAdapter.ReceivedError"/>
public event Action<Exception> ReceivedError;
/// <inheritdoc cref="ISocketAdapter.Received"/>
public event Action<ArraySegment<byte>> Received;
/// <inheritdoc cref="ISocketAdapter.IsConnected"/>
public new bool IsConnected
{
get
{
return ws.GetReadyState() == WebSocketPeer.State.Open;
}
}
/// <inheritdoc cref="ISocketAdapter.IsConnecting"/>
public bool IsConnecting
{
get
{
return ws.GetReadyState() == WebSocketPeer.State.Connecting;
}
}
private WebSocketPeer ws;
private WebSocketPeer.State wsLastState = WebSocketPeer.State.Closed;
private TaskCompletionSource<bool> connectionSource;
private TaskCompletionSource<bool> closeSource;
private int connectionTimeout;
private double connectionStart;
/// <summary>
/// Constructs a GodotWebSocketAdapter.
/// </summary>
public GodotWebSocketAdapter()
{
ws = new WebSocketPeer();
}
/// <inheritdoc cref="ISocketAdaptor.CloseAsync"/>
public Task CloseAsync()
{
if (closeSource == null)
{
closeSource = new TaskCompletionSource<bool>();
}
ws.Close();
return closeSource.Task;
}
/// <inheritdoc cref="ISocketAdaptor.ConnectAsync"/>
public Task ConnectAsync(Uri uri, int timeout)
{
if (connectionSource != null)
{
connectionSource.SetException(new GodotWebSocketConnectionException("Connection attempt aborted due to new connection attempt"));
connectionSource = null;
}
if (ws.GetReadyState() != WebSocketPeer.State.Closed)
{
return Task.FromException(new GodotWebSocketConnectionException("Cannot connect until current socket is closed"));
}
connectionTimeout = timeout;
connectionStart = Time.GetUnixTimeFromSystem();
connectionSource = new TaskCompletionSource<bool>();
var err = ws.ConnectToUrl(uri.ToString());
if (err != Error.Ok)
{
return Task.FromException(new GodotWebSocketConnectionException(String.Format("Error connecting: {0}", Enum.GetName(typeof(Error), err))));
}
wsLastState = WebSocketPeer.State.Closed;
return connectionSource.Task;
}
/// <inheritdoc cref="ISocketAdaptor.SendAsync"/>
public Task SendAsync(ArraySegment<byte> buffer, bool reliable = true, CancellationToken canceller = default)
{
byte[] temp;
if (buffer.Offset != 0 || buffer.Count != buffer.Array.Length)
{
temp = new byte[buffer.Count];
Array.Copy(buffer.Array, buffer.Offset, temp, 0, buffer.Count);
}
else
{
temp = buffer.Array;
}
var err = ws.Send(temp, WebSocketPeer.WriteMode.Text);
if (err == Error.Ok)
{
return Task.CompletedTask;
}
return Task.FromException(new GodotWebSocketSendException());
}
public override void _Process(double delta)
{
if (ws.GetReadyState() != WebSocketPeer.State.Closed)
{
ws.Poll();
}
var state = ws.GetReadyState();
if (wsLastState != state)
{
wsLastState = state;
if (state == WebSocketPeer.State.Open)
{
Connected?.Invoke();
connectionSource.SetResult(true);
connectionSource = null;
}
else if (state == WebSocketPeer.State.Closed)
{
if (connectionSource != null)
{
Exception e = new GodotWebSocketConnectionException("Failed to connect");
ReceivedError?.Invoke(e);
connectionSource.SetException(e);
connectionSource = null;
}
else
{
Closed?.Invoke();
}
if (closeSource != null)
{
closeSource.SetResult(true);
closeSource = null;
}
}
}
if (ws.GetReadyState() == WebSocketPeer.State.Connecting) {
if (connectionStart + (double)connectionTimeout < Time.GetUnixTimeFromSystem())
{
ws.Close();
Exception e = new GodotWebSocketConnectionException("Connection timed out");
ReceivedError?.Invoke(e);
connectionSource.SetException(e);
connectionSource = null;
}
}
while (ws.GetReadyState() == WebSocketPeer.State.Open && ws.GetAvailablePacketCount() > 0)
{
Received?.Invoke(new ArraySegment<byte>(ws.GetPacket()));
}
}
}
}
@@ -0,0 +1,598 @@
extends RefCounted
# A socket to interact with Nakama server.
class_name NakamaSocket
const ChannelType = NakamaRTMessage.ChannelJoin.ChannelType
# Emitted when a socket is closed.
signal closed()
# Emitted when a socket is connected.
signal connected()
# Emitted when an error occurs while connecting.
signal connection_error(p_error)
# Emitted when a chat channel message is received
signal received_channel_message(p_channel_message) # ApiChannelMessage
# Emitted when receiving a presence change for joins and leaves with users in a chat channel.
signal received_channel_presence(p_channel_presence) # ChannelPresenceEvent
# Emitted when an error is received from the server.
signal received_error(p_error) # Error
# Emitted when receiving a matchmaker matched message.
signal received_matchmaker_matched(p_matchmaker_matched) # MatchmakerMatched
# Emitted when receiving a message from a multiplayer match.
signal received_match_state(p_match_state) # MatchData
# Emitted when receiving a presence change for joins and leaves of users in a multiplayer match.
signal received_match_presence(p_match_presence_event) # MatchPresenceEvent
# Emitted when receiving a notification for the current user.
signal received_notification(p_api_notification) # ApiNotification
# Emitted when receiving a presence change for when a user updated their online status.
signal received_status_presence(p_status_presence_event) # StatusPresenceEvent
# Emitted when receiving a presence change for joins and leaves on a realtime stream.
signal received_stream_presence(p_stream_presence_event) # StreamPresenceEvent
# Emitted when receiving a message from a realtime stream.
signal received_stream_state(p_stream_state) # StreamState
# Received a party event. This will occur when the current user's invitation request is accepted
# the party leader of a closed party.
signal received_party(p_party) # Party
# Received a party close event.
signal received_party_close(p_party_close) # PartyClose
# Received custom party data.
signal received_party_data(p_party_data) # PartyData
# Received a request to join the party.
signal received_party_join_request(p_party_join_request) # PartyJoinRequest
# Received a change in the party leader.
signal received_party_leader(p_party_leader) # PartyLeader
# Received a new matchmaker ticket for the party.
signal received_party_matchmaker_ticket(p_party_matchmaker_ticket) # PartyMatchmakerTicket
# Received a new presence event in the party.
signal received_party_presence(p_party_presence_event) # PartyPresenceEvent
var _adapter : NakamaSocketAdapter
var _free_adapter : bool = false
var _weak_ref : WeakRef
var _base_uri : String
var _requests : Dictionary
var _last_id : int = 1
var _conn = null
var logger : NakamaLogger = null
class AsyncConnection:
signal completed(result)
func resume(result) -> void:
emit_signal("completed", result)
class AsyncRequest:
var id : String
var type
var ns
var result_key : String
signal completed(result)
func _init(p_id : String, p_type, p_ns, p_result_key = null):
id = p_id
type = p_type
ns = p_ns
if type != NakamaAsyncResult:
# Specifically defined key, or default for object.
result_key = p_result_key if p_result_key != null else type.get_result_key()
func resume(data, logger = null) -> void:
var result = _parse_result(data, logger)
emit_signal("completed", result)
func _parse_result(data, logger):
# We got an exception, maybe the task was cancelled?
if data is NakamaException:
return type.new(data as NakamaException)
# Error from server
if data.has("error"):
var err = data["error"]
var code = -1
var msg = str(err)
if typeof(err) == TYPE_DICTIONARY:
msg = err.get("message", "")
code = err.get("code", -1)
if logger:
logger.warning("Error response from server: %s" % err)
return type.new(NakamaException.new(msg, code))
# Simple ack response
elif type == NakamaAsyncResult:
return NakamaAsyncResult.new()
# Missing expected result key
elif not data.has(result_key):
if logger:
logger.warning("Missing expected result key: %s" % result_key)
return type.new(NakamaException.new("Missing expected result key: %s" % result_key))
# All good, proceed with parsing
else:
return type.create(ns, data.get(result_key))
func _resume_conn(p_err : int):
if _conn:
if p_err: # Exception
logger.warning("Connection error: %d" % p_err)
_conn.resume(NakamaAsyncResult.new(NakamaException.new()))
else:
logger.info("Connected!")
_conn.resume(NakamaAsyncResult.new())
_conn = null
func _init(p_adapter : NakamaSocketAdapter,
p_host : String,
p_port : int,
p_scheme : String,
p_free_adapter : bool = false):
logger = p_adapter.logger
_adapter = p_adapter
_weak_ref = weakref(_adapter)
var port = ""
if (p_scheme == "ws" and p_port != 80) or (p_scheme == "wss" and p_port != 443):
port = ":%d" % p_port
_base_uri = "%s://%s%s" % [p_scheme, p_host, port]
_free_adapter = p_free_adapter
_adapter.closed.connect(self._closed)
_adapter.connected.connect(self._connected)
_adapter.received_error.connect(self._connection_error)
_adapter.received.connect(self._received)
func _notification(what):
if what == NOTIFICATION_PREDELETE:
# Is this a bug? Why can't I call a function? self is null...
# _clear_responses()
# _resume_conn(ERR_FILE_EOF)
var keys = _requests.keys()
for k in keys:
_requests[k].resume(NakamaException.new("Cancelled!"))
if _conn != null:
_conn.resume(ERR_FILE_EOF)
_conn = null
if _weak_ref.get_ref() == null:
return
_adapter.close()
if _free_adapter:
_adapter.queue_free()
func _closed(p_error = null):
emit_signal("closed")
_resume_conn(ERR_CANT_CONNECT)
_clear_requests()
func _connection_error(p_error):
emit_signal("connection_error", p_error)
_resume_conn(p_error)
_clear_requests()
func _connected():
emit_signal("connected")
_resume_conn(OK)
func _received(p_bytes : PackedByteArray):
var json = JSON.new()
var json_str = p_bytes.get_string_from_utf8()
var json_error := json.parse(json_str)
if json_error != OK or typeof(json.get_data()) != TYPE_DICTIONARY:
logger.error("Unable to parse response: %s" % json_str)
return
var dict : Dictionary = json.get_data()
var cid = dict.get("cid")
if cid:
if _requests.has(cid):
_resume_request(cid, dict)
else:
logger.error("Invalid call id received %s" % dict)
else:
if dict.has("error"):
var res = NakamaRTAPI.Error.create(NakamaRTAPI, dict["error"])
emit_signal("received_error", res)
elif dict.has("channel_message"):
var res = NakamaAPI.ApiChannelMessage.create(NakamaAPI, dict["channel_message"])
emit_signal("received_channel_message", res)
elif dict.has("channel_presence_event"):
var res = NakamaRTAPI.ChannelPresenceEvent.create(NakamaRTAPI, dict["channel_presence_event"])
emit_signal("received_channel_presence", res)
elif dict.has("match_data"):
var res = NakamaRTAPI.MatchData.create(NakamaRTAPI, dict["match_data"])
emit_signal("received_match_state", res)
elif dict.has("match_presence_event"):
var res = NakamaRTAPI.MatchPresenceEvent.create(NakamaRTAPI, dict["match_presence_event"])
emit_signal("received_match_presence", res)
elif dict.has("matchmaker_matched"):
var res = NakamaRTAPI.MatchmakerMatched.create(NakamaRTAPI, dict["matchmaker_matched"])
emit_signal("received_matchmaker_matched", res)
elif dict.has("notifications"):
var res = NakamaAPI.ApiNotificationList.create(NakamaAPI, dict["notifications"])
for n in res.notifications:
emit_signal("received_notification", n)
elif dict.has("status_presence_event"):
var res = NakamaRTAPI.StatusPresenceEvent.create(NakamaRTAPI, dict["status_presence_event"])
emit_signal("received_status_presence", res)
elif dict.has("stream_presence_event"):
var res = NakamaRTAPI.StreamPresenceEvent.create(NakamaRTAPI, dict["stream_presence_event"])
emit_signal("received_stream_presence", res)
elif dict.has("stream_data"):
var res = NakamaRTAPI.StreamData.create(NakamaRTAPI, dict["stream_data"])
emit_signal("received_stream_state", res)
elif dict.has("party"):
var res = NakamaRTAPI.Party.create(NakamaRTAPI, dict["party"])
emit_signal("received_party", res)
elif dict.has("party_close"):
var res = NakamaRTAPI.PartyClose.create(NakamaRTAPI, dict["party_close"])
emit_signal("received_party_close", res)
elif dict.has("party_data"):
var res = NakamaRTAPI.PartyData.create(NakamaRTAPI, dict["party_data"])
emit_signal("received_party_data", res)
elif dict.has("party_join_request"):
var res = NakamaRTAPI.PartyJoinRequest.create(NakamaRTAPI, dict["party_join_request"])
emit_signal("received_party_join_request", res)
elif dict.has("party_leader"):
var res = NakamaRTAPI.PartyLeader.create(NakamaRTAPI, dict["party_leader"])
emit_signal("received_party_leader", res)
elif dict.has("party_matchmaker_ticket"):
var res = NakamaRTAPI.PartyMatchmakerTicket.create(NakamaRTAPI, dict["party_matchmaker_ticket"])
emit_signal("received_party_matchmaker_ticket", res)
elif dict.has("party_presence_event"):
var res = NakamaRTAPI.PartyPresenceEvent.create(NakamaRTAPI, dict["party_presence_event"])
emit_signal("received_party_presence", res)
else:
logger.warning("Unhandled response: %s" % dict)
func _resume_request(p_id : String, p_data):
if _requests.has(p_id):
logger.debug("Resuming request: %s: %s" % [p_id, p_data])
_requests[p_id].resume(p_data, logger)
_requests.erase(p_id)
else:
logger.warning("Trying to resume missing request: %s: %s" % [p_id, p_data])
func _cancel_request(p_id : String):
logger.debug("Cancelling request: %s" % [p_id])
_resume_request(p_id, NakamaException.new("Request cancelled."))
func _clear_requests():
var ids = _requests.keys()
for id in ids:
_cancel_request(id)
func _send_async(p_message, p_parse_type = NakamaAsyncResult, p_ns = NakamaRTAPI, p_msg_key = null, p_result_key = null) -> AsyncRequest:
logger.debug("Sending async request: %s" % p_message)
# For messages coming from the API which does not have a key defined, so we can override it
var msg = p_msg_key
# For regular RT messages
if msg == null:
msg = p_message.get_msg_key()
var id = str(_last_id)
_last_id += 1
_requests[id] = AsyncRequest.new(id, p_parse_type, p_ns, p_result_key)
var json := JSON.stringify({
"cid": id,
msg: p_message.serialize()
})
var err = _adapter.send(json.to_utf8_buffer())
if err != OK:
call_deferred("_cancel_request", id)
return _requests[id]
# If the socket is connected.
func is_connected_to_host():
return _adapter.is_connected_to_host()
# If the socket is connecting.
func is_connecting_to_host():
return _adapter.is_connecting_to_host()
# Close the socket connection to the server.
func close():
_adapter.close()
# Connect to the server.
# @param p_session - The session of the user.
# @param p_appear_online - If the user who appear online to other users.
# @param p_connect_timeout - The time allowed for the socket connection to be established.
# Returns a task to represent the asynchronous operation.
func connect_async(p_session : NakamaSession, p_appear_online : bool = false, p_connect_timeout : int = 3):
var uri = "%s/ws?lang=en&status=%s&token=%s" % [_base_uri, str(p_appear_online).to_lower(), p_session.token]
logger.debug("Connecting to host: %s" % uri)
_conn = AsyncConnection.new()
_adapter.connect_to_host(uri, p_connect_timeout)
return await _conn.completed
# Join the matchmaker pool and search for opponents on the server.
# @param p_query - The matchmaker query to search for opponents.
# @param p_min_count - The minimum number of players to compete against in a match.
# @param p_max_count - The maximum number of players to compete against in a match.
# @param p_string_properties - A set of key/value properties to provide to searches.
# @param p_numeric_properties - A set of key/value numeric properties to provide to searches.
# @param p_count_multiple - Optional multiple of the count that must be satisfied.
# Returns a task which resolves to a matchmaker ticket object.
func add_matchmaker_async(p_query : String = "*", p_min_count : int = 2, p_max_count : int = 8,
p_string_props : Dictionary = {}, p_numeric_props : Dictionary = {},
p_count_multiple : int = 0) -> NakamaRTAPI.MatchmakerTicket:
return await _send_async(
NakamaRTMessage.MatchmakerAdd.new(p_query, p_min_count, p_max_count, p_string_props, p_numeric_props, p_count_multiple),
NakamaRTAPI.MatchmakerTicket
).completed
# Create a multiplayer match on the server.
# @param p_name - Optional name to use when creating the match.
# Returns a task to represent the asynchronous operation.
func create_match_async(p_name : String = ''):
return await _send_async(NakamaRTMessage.MatchCreate.new(p_name), NakamaRTAPI.Match).completed
# Subscribe to one or more users for their status updates.
# @param p_user_ids - The IDs of users.
# @param p_usernames - The usernames of the users.
# Returns a task which resolves to the current statuses for the users.
func follow_users_async(p_ids : PackedStringArray, p_usernames : PackedStringArray) -> NakamaRTAPI.Status:
return await _send_async(NakamaRTMessage.StatusFollow.new(p_ids, p_usernames), NakamaRTAPI.Status).completed
# Join a chat channel on the server.
# @param p_target - The target channel to join.
# @param p_type - The type of channel to join.
# @param p_persistence - If chat messages should be stored.
# @param p_hidden - If the current user should be hidden on the channel.
# Returns a task which resolves to a chat channel object.
func join_chat_async(p_target : String, p_type : int, p_persistence : bool = false, p_hidden : bool = false) -> NakamaRTAPI.Channel:
return await _send_async(
NakamaRTMessage.ChannelJoin.new(p_target, p_type, p_persistence, p_hidden),
NakamaRTAPI.Channel
).completed
# Join a multiplayer match with the matchmaker matched object.
# @param p_matched - A matchmaker matched object.
# Returns a task which resolves to a multiplayer match.
func join_matched_async(p_matched):
var msg := NakamaRTMessage.MatchJoin.new()
if p_matched.match_id:
msg.match_id = p_matched.match_id
else:
msg.token = p_matched.token
return await _send_async(msg, NakamaRTAPI.Match).completed
# Join a multiplayer match by ID.
# @param p_match_id - The ID of the match to attempt to join.
# @param p_metadata - An optional set of key-value metadata pairs to be passed to the match handler.
# Returns a task which resolves to a multiplayer match.
func join_match_async(p_match_id : String, p_metadata = null):
var msg := NakamaRTMessage.MatchJoin.new()
msg.match_id = p_match_id
msg.metadata = p_metadata
return await _send_async(msg, NakamaRTAPI.Match).completed
# Leave a chat channel on the server.
## @param p_channel_id - The ID of the chat channel to leave.
# Returns a task which represents the asynchronous operation.
func leave_chat_async(p_channel_id : String) -> NakamaAsyncResult:
return await _send_async(NakamaRTMessage.ChannelLeave.new(p_channel_id)).completed
# Leave a multiplayer match on the server.
# @param p_match_id - The multiplayer match to leave.
# Returns a task which represents the asynchronous operation.
func leave_match_async(p_match_id : String) -> NakamaAsyncResult:
return await _send_async(NakamaRTMessage.MatchLeave.new(p_match_id)).completed
# Remove a chat message from a chat channel on the server.
# @param p_channel - The chat channel with the message to remove.
# @param p_message_id - The ID of the chat message to remove.
# Returns a task which resolves to an acknowledgement of the removed message.
func remove_chat_message_async(p_channel_id : String, p_message_id : String):
return await _send_async(
NakamaRTMessage.ChannelMessageRemove.new(p_channel_id, p_message_id),
NakamaRTAPI.ChannelMessageAck
).completed
# Leave the matchmaker pool with the ticket.
# @param p_ticket - The ticket returned by the matchmaker on join.
# Returns a task which represents the asynchronous operation.
func remove_matchmaker_async(p_ticket : String) -> NakamaAsyncResult:
return await _send_async(NakamaRTMessage.MatchmakerRemove.new(p_ticket)).completed
# Execute an RPC function to the server.
# @param p_func_id - The ID of the function to execute.
# @param p_payload - An (optional) String payload to send to the server.
# Returns a task which resolves to the RPC function response object.
func rpc_async(p_func_id : String, p_payload = null) -> NakamaAPI.ApiRpc:
var payload = p_payload
match typeof(p_payload):
TYPE_NIL, TYPE_STRING:
pass
_:
payload = JSON.stringify(p_payload)
return await _send_async(NakamaAPI.ApiRpc.create(NakamaAPI, {
"id": p_func_id,
"payload": payload
}), NakamaAPI.ApiRpc, NakamaAPI, "rpc", "rpc").completed
# Send input to a multiplayer match on the server.
# When no presences are supplied the new match state will be sent to all presences.
# @param p_match_id - The ID of the match.
# @param p_op_code - An operation code for the input.
# @param p_data - The input data to send.
# @param p_presences - The presences in the match who should receive the input.
# Returns a task which represents the asynchronous operation.
func send_match_state_async(p_match_id, p_op_code : int, p_data : String, p_presences = null):
var req = _send_async(NakamaRTMessage.MatchDataSend.new(
p_match_id,
p_op_code,
Marshalls.utf8_to_base64(p_data),
p_presences
))
# This do not return a response from server, you don't really need to wait for it.
req.call_deferred("resume", {})
return req.completed
# Send input to a multiplayer match on the server.
# When no presences are supplied the new match state will be sent to all presences.
# @param p_match_id - The ID of the match.
# @param p_op_code - An operation code for the input.
# @param p_data - The input data to send.
# @param p_presences - The presences in the match who should receive the input.
# Returns a task which represents the asynchronous operation.
func send_match_state_raw_async(p_match_id, p_op_code : int, p_data : PackedByteArray, p_presences = null):
var req = _send_async(NakamaRTMessage.MatchDataSend.new(
p_match_id,
p_op_code,
Marshalls.raw_to_base64(p_data),
p_presences
))
# This do not return a response from server, you don't really need to wait for it.
req.call_deferred("resume", {})
return req.completed
# Unfollow one or more users from their status updates.
# @param p_user_ids - An array of user ids to unfollow.
# Returns a task which represents the asynchronous operation.
func unfollow_users_async(p_ids : PackedStringArray):
return await _send_async(NakamaRTMessage.StatusUnfollow.new(p_ids)).completed
# Update a chat message on a chat channel in the server.
# @param p_channel_id - The ID of the chat channel with the message to update.
# @param p_message_id - The ID of the message to update.
# @param p_content - The new contents of the chat message.
# Returns a task which resolves to an acknowledgement of the updated message.
func update_chat_message_async(p_channel_id : String, p_message_id : String, p_content : Dictionary):
return await _send_async(
NakamaRTMessage.ChannelMessageUpdate.new(p_channel_id, p_message_id, JSON.stringify(p_content)),
NakamaRTAPI.ChannelMessageAck
).completed
# Update the status for the current user online.
# @param p_status - The new status for the user.
# Returns a task which represents the asynchronous operation.
func update_status_async(p_status : String):
return await _send_async(NakamaRTMessage.StatusUpdate.new(p_status)).completed
# Send a chat message to a chat channel on the server.
# @param p_channel_id - The ID of the chat channel to send onto.
# @param p_content - The contents of the message to send.
# Returns a task which resolves to the acknowledgement of the chat message write.
func write_chat_message_async(p_channel_id : String, p_content : Dictionary):
return await _send_async(
NakamaRTMessage.ChannelMessageSend.new(p_channel_id, JSON.stringify(p_content)),
NakamaRTAPI.ChannelMessageAck
).completed
# Accept a party member's request to join the party.
# @param p_party_id - The party ID to accept the join request for.
# @param p_presence - The presence to accept as a party member.
# Returns a task to represent the asynchronous operation.
func accept_party_member_async(p_party_id : String, p_presence : NakamaRTAPI.UserPresence):
return await _send_async(NakamaRTMessage.PartyAccept.new(p_party_id, p_presence)).completed
# Begin matchmaking as a party.
# @param p_party_id - Party ID.
# @param p_query - Filter query used to identify suitable users.
# @param p_min_count - Minimum total user count to match together.
# @param p_max_count - Maximum total user count to match together.
# @param p_string_properties - String properties.
# @param p_numeric_properties - Numeric properties.
# @param p_count_multiple - Optional multiple of the count that must be satisfied.
# Returns a task to represent the asynchronous operation.
func add_matchmaker_party_async(p_party_id : String, p_query : String = "*", p_min_count : int = 2,
p_max_count : int = 8, p_string_properties = {}, p_numeric_properties = {}, p_count_multiple : int = 0):
return await _send_async(
NakamaRTMessage.PartyMatchmakerAdd.new(p_party_id, p_min_count,
p_max_count, p_query, p_string_properties, p_numeric_properties,
p_count_multiple if p_count_multiple > 0 else null),
NakamaRTAPI.PartyMatchmakerTicket).completed
# End a party, kicking all party members and closing it.
# @param p_party_id - The ID of the party.
# Returns a task to represent the asynchronous operation.
func close_party_async(p_party_id : String):
var msg := NakamaRTAPI.PartyClose.new()
msg.party_id = p_party_id
return await _send_async(msg).completed
# Create a party.
# @param p_open - Whether or not the party will require join requests to be approved by the party leader.
# @param p_max_size - Maximum number of party members. This maximum does not include the party leader.
# Returns a task to represent the asynchronous operation.
func create_party_async(p_open : bool, p_max_size : int) -> NakamaRTAPI.Party:
return await _send_async(
NakamaRTMessage.PartyCreate.new(p_open, p_max_size),
NakamaRTAPI.Party
).completed
# Join a party.
# @param p_party_id - Party ID.
# Returns a task to represent the asynchronous operation.
func join_party_async(p_party_id : String):
return await _send_async(NakamaRTMessage.PartyJoin.new(p_party_id)).completed
# Leave the party.
# @param p_party_id - Party ID.
# Returns a task to represent the asynchronous operation.
func leave_party_async(p_party_id : String):
return await _send_async(NakamaRTMessage.PartyLeave.new(p_party_id)).completed
# Request a list of pending join requests for a party.
# @param p_party_id - Party ID.
# Returns a task which resolves to a list of all party join requests.
func list_party_join_requests_async(p_party_id : String) -> NakamaRTAPI.PartyJoinRequest:
return await _send_async(
NakamaRTMessage.PartyJoinRequestList.new(p_party_id),
NakamaRTAPI.PartyJoinRequest).completed
# Promote a new party leader.
# @param p_party_id - Party ID.
# @param p_party_member - The presence of an existing party member to promote as the new leader.
# Returns a which represents the asynchronous operation.
func promote_party_member(p_party_id : String, p_party_member : NakamaRTAPI.UserPresence):
return await _send_async(NakamaRTMessage.PartyPromote.new(p_party_id, p_party_member)).completed
# Cancel a party matchmaking process using a ticket.
# @param p_party_id - Party ID.
# @param p_ticket - The ticket to cancel.
# Returns a task which represents the asynchronous operation.
func remove_matchmaker_party_async(p_party_id : String, p_ticket : String):
return await _send_async(NakamaRTMessage.PartyMatchmakerRemove.new(p_party_id, p_ticket)).completed
# Kick a party member, or decline a request to join.
# @param p_party_id - Party ID to remove/reject from.
# @param p_presence - The presence to remove or reject.
# Returns a task which represents the asynchronous operation.
func remove_party_member_async(p_party_id : String, p_presence : NakamaRTAPI.UserPresence):
return await _send_async(NakamaRTMessage.PartyRemove.new(p_party_id, p_presence)).completed
# Send data to a party.
# @param p_party_id - Party ID to send to.
# @param p_op_code - Op code value.
# @param data - Data payload, if any.
# Returns a task which represents the asynchronous operation.
func send_party_data_async(p_party_id : String, p_op_code : int, p_data:String = ""):
var base64_data = null if p_data.is_empty() else Marshalls.utf8_to_base64(p_data)
return await _send_async(NakamaRTMessage.PartyDataSend.new(p_party_id, p_op_code, base64_data)).completed
# Send data to a party.
# @param p_party_id - Party ID to send to.
# @param p_op_code - Op code value.
# @param data - Data payload, if any.
# Returns a task which represents the asynchronous operation.
func send_party_data_raw_async(p_party_id : String, p_op_code : int, p_data:PackedByteArray):
var base64_data = null if p_data.is_empty() else Marshalls.raw_to_base64(p_data)
return await _send_async(NakamaRTMessage.PartyDataSend.new(p_party_id, p_op_code, base64_data)).completed
@@ -0,0 +1 @@
uid://qiv21fhs1smm
@@ -0,0 +1,75 @@
@tool
extends Node
# An adapter which implements a socket with a protocol supported by Nakama.
class_name NakamaSocketAdapter
var _ws := WebSocketPeer.new()
var _ws_last_state := WebSocketPeer.STATE_CLOSED
var _timeout : int = 30
var _start : int = 0
var logger = NakamaLogger.new()
# A signal emitted when the socket is connected.
signal connected()
# A signal emitted when the socket is disconnected.
signal closed()
# A signal emitted when the socket has an error when connecting.
signal received_error(p_exception)
# A signal emitted when the socket receives a message.
signal received(p_bytes) # PackedByteArray
# If the socket is connected.
func is_connected_to_host():
return _ws.get_ready_state() == WebSocketPeer.STATE_OPEN
# If the socket is connecting.
func is_connecting_to_host():
return _ws.get_ready_state() == WebSocketPeer.STATE_CONNECTING
# Close the socket with an asynchronous operation.
func close():
_ws.close()
# Connect to the server with an asynchronous operation.
# @param p_uri - The URI of the server.
# @param p_timeout - The timeout for the connect attempt on the socket.
func connect_to_host(p_uri : String, p_timeout : int):
_timeout = p_timeout
_start = Time.get_unix_time_from_system()
var err = _ws.connect_to_url(p_uri)
if err != OK:
logger.debug("Error connecting to host %s" % p_uri)
call_deferred("emit_signal", "received_error", err)
return
_ws_last_state = WebSocketPeer.STATE_CLOSED
# Send data to the server with an asynchronous operation.
# @param p_buffer - The buffer with the message to send.
# @param p_reliable - If the message should be sent reliably (will be ignored by some protocols).
func send(p_buffer : PackedByteArray, p_reliable : bool = true) -> int:
return _ws.send(p_buffer, WebSocketPeer.WRITE_MODE_TEXT)
func _process(delta):
if _ws.get_ready_state() != WebSocketPeer.STATE_CLOSED:
_ws.poll()
var state = _ws.get_ready_state()
if _ws_last_state != state:
_ws_last_state = state
if state == WebSocketPeer.STATE_OPEN:
connected.emit()
elif state == WebSocketPeer.STATE_CLOSED:
closed.emit()
if state == WebSocketPeer.STATE_CONNECTING:
if _start + _timeout < Time.get_unix_time_from_system():
logger.debug("Timeout when connecting to socket")
received_error.emit(ERR_TIMEOUT)
_ws.close()
while _ws.get_ready_state() == WebSocketPeer.STATE_OPEN and _ws.get_available_packet_count():
received.emit(_ws.get_packet())
@@ -0,0 +1 @@
uid://co7iapf61xel1
@@ -0,0 +1,34 @@
extends RefCounted
class_name NakamaAsyncResult
var exception : NakamaException:
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() -> NakamaException:
return _ex as NakamaException
func _to_string():
if is_exception():
return get_exception()._to_string()
return "NakamaAsyncResult<>"
static func _safe_ret(p_obj, p_type : GDScript):
if is_instance_of(p_obj, p_type):
return p_obj # Correct type
elif p_obj is NakamaException:
return p_type.new(p_obj) # It's an exception. Incapsulate it
return p_type.new(NakamaException.new()) # It's something else. generate an exception
@@ -0,0 +1 @@
uid://duw24m22ooo88
@@ -0,0 +1,42 @@
extends RefCounted
# An exception generated during a request.
# Usually contains at least an error message.
class_name NakamaException
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 "NakamaException(StatusCode={%s}, Message='{%s}', GrpcStatusCode={%s})" % [_status_code, _message, _grpc_status_code]
@@ -0,0 +1 @@
uid://c2j7kr0qoeumk
@@ -0,0 +1,38 @@
extends RefCounted
class_name NakamaLogger
enum LOG_LEVEL {NONE, ERROR, WARNING, INFO, VERBOSE, DEBUG}
var _level = LOG_LEVEL.ERROR
var _module = "Nakama"
func _init(p_module : String = "Nakama", 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://df0d3xhfk7kwe
@@ -0,0 +1,363 @@
extends RefCounted
class_name NakamaMultiplayerBridge
enum MatchState {
DISCONNECTED,
JOINING,
CONNECTED,
SOCKET_CLOSED,
}
enum MetaMessageType {
CLAIM_HOST,
ASSIGN_PEER_ID,
}
# Read-only variables.
var _nakama_socket: NakamaSocket
var nakama_socket: NakamaSocket:
get: return _nakama_socket
set(_v): pass
var _match_state: int = MatchState.DISCONNECTED
var match_state: int:
get: return _match_state
set(_v): pass
var _match_id := ''
var match_id: String:
get: return _match_id
set(_v): pass
var _multiplayer_peer: NakamaMultiplayerPeer = NakamaMultiplayerPeer.new()
var multiplayer_peer: NakamaMultiplayerPeer:
get: return _multiplayer_peer
set(_v): pass
# Configuration that can be set by the developer.
var meta_op_code: int = 9001
var rpc_op_code: int = 9002
# Internal variables.
var _my_session_id: String
var _my_peer_id: int = 0
var _id_map := {}
var _users := {}
var _matchmaker_ticket := ''
class User extends RefCounted:
var presence
var peer_id: int = 0
func _init(p_presence) -> void:
presence = p_presence
signal match_join_error (exception)
signal match_joined ()
func _set_readonly(_value) -> void:
pass
func _init(p_nakama_socket: NakamaSocket) -> void:
_nakama_socket = p_nakama_socket
_nakama_socket.received_match_presence.connect(self._on_nakama_socket_received_match_presence)
_nakama_socket.received_matchmaker_matched.connect(self._on_nakama_socket_received_matchmaker_matched)
_nakama_socket.received_match_state.connect(self._on_nakama_socket_received_match_state)
_nakama_socket.closed.connect(self._on_nakama_socket_closed)
_multiplayer_peer.packet_generated.connect(self._on_multiplayer_peer_packet_generated)
_multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTING)
func create_match() -> void:
if _match_state != MatchState.DISCONNECTED:
push_error("Cannot create match when state is %s" % MatchState.keys()[_match_state])
return
_match_state = MatchState.JOINING
multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTING)
var res = await _nakama_socket.create_match_async()
if res.is_exception():
match_join_error.emit(res.get_exception())
leave()
return
_setup_match(res)
_setup_host()
func join_match(p_match_id: String) -> void:
if _match_state != MatchState.DISCONNECTED:
push_error("Cannot join match when state is %s" % MatchState.keys()[_match_state])
return
_match_state = MatchState.JOINING
multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTING)
var res = await _nakama_socket.join_match_async(p_match_id)
if res.is_exception():
match_join_error.emit(res.get_exception())
leave()
return
_setup_match(res)
func join_named_match(_match_name: String) -> void:
if _match_state != MatchState.DISCONNECTED:
push_error("Cannot join match when state is %s" % MatchState.keys()[_match_state])
return
_match_state = MatchState.JOINING
multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTING)
var res = await _nakama_socket.create_match_async(_match_name)
if res.is_exception():
match_join_error.emit(res.get_exception())
leave()
return
_setup_match(res)
if res.size == 0 or (res.size == 1 and res.presences.size() == 0):
_setup_host()
func start_matchmaking(ticket) -> void:
if _match_state != MatchState.DISCONNECTED:
push_error("Cannot start matchmaking when state is %s" % MatchState.keys()[_match_state])
return
if ticket.is_exception():
push_error("Ticket with exception passed into start_matchmaking()")
return
_match_state = MatchState.JOINING
multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTING)
_matchmaker_ticket = ticket.ticket
func _on_nakama_socket_received_matchmaker_matched(matchmaker_matched) -> void:
if _matchmaker_ticket != matchmaker_matched.ticket:
return
# Get a list of sorted session ids.
var session_ids := []
for matchmaker_user in matchmaker_matched.users:
session_ids.append(matchmaker_user.presence.session_id)
session_ids.sort()
var res = await _nakama_socket.join_matched_async(matchmaker_matched)
if res.is_exception():
match_join_error.emit(res.get_exception())
leave()
return
_setup_match(res)
# If our session is the first alphabetically, then we'll be the host.
if _my_session_id == session_ids[0]:
_setup_host()
# Add all of the existing peers.
for presence in res.presences:
if presence.session_id != _my_session_id:
_host_add_peer(presence)
func _on_nakama_socket_closed() -> void:
match_state = MatchState.SOCKET_CLOSED
_cleanup()
func get_user_presence_for_peer(peer_id: int) -> NakamaRTAPI.UserPresence:
var session_id = _id_map.get(peer_id)
if session_id == null:
return null
var user = _users.get(session_id)
if user == null:
return null
return user.presence
func leave() -> void:
if _match_state == MatchState.DISCONNECTED:
return
_match_state = MatchState.DISCONNECTED
if _match_id:
await _nakama_socket.leave_match_async(_match_id)
if _matchmaker_ticket:
await _nakama_socket.remove_matchmaker_async(_matchmaker_ticket)
_cleanup()
func _cleanup() -> void:
for peer_id in _id_map:
multiplayer_peer.peer_disconnected.emit(peer_id)
_match_id = ''
_matchmaker_ticket = ''
_my_session_id = ''
_my_peer_id = 0
_id_map.clear()
_users.clear()
_multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_DISCONNECTED)
func _setup_match(res) -> void:
_match_id = res.match_id
_my_session_id = res.self_user.session_id
_users[_my_session_id] = User.new(res.self_user)
for presence in res.presences:
if not _users.has(presence.session_id):
_users[presence.session_id] = User.new(presence)
func _setup_host() -> void:
# Claim id 1 and start the match.
_my_peer_id = 1
_map_id_to_session(1, _my_session_id)
_match_state = MatchState.CONNECTED
_multiplayer_peer.initialize(_my_peer_id)
match_joined.emit()
func _generate_id(session_id: String) -> int:
# Peer ids can only be positive 32-bit signed integers.
var peer_id: int = session_id.hash() & 0x7FFFFFFF
# If this peer id is already taken, try to find another.
while peer_id <= 1 or _id_map.has(peer_id):
peer_id += 1
if peer_id > 0x7FFFFFFF or peer_id <= 0:
peer_id = randi() & 0x7FFFFFFF
return peer_id
func _map_id_to_session(peer_id: int, session_id: String) -> void:
_id_map[peer_id] = session_id
_users[session_id].peer_id = peer_id
func _host_add_peer(presence) -> void:
var peer_id = _generate_id(presence.session_id)
_map_id_to_session(peer_id, presence.session_id)
# Tell them we are the host.
_nakama_socket.send_match_state_async(_match_id, meta_op_code, JSON.stringify({
type = MetaMessageType.CLAIM_HOST,
}), [presence])
# Tell them about all the other connected peers.
for other_peer_id in _id_map:
var other_session_id = _id_map[other_peer_id]
if other_session_id == presence.session_id or other_session_id == _my_session_id:
continue
_nakama_socket.send_match_state_async(_match_id, meta_op_code, JSON.stringify({
type = MetaMessageType.ASSIGN_PEER_ID,
session_id = other_session_id,
peer_id = other_peer_id,
}), [presence])
# Assign them a peer_id (tell everyone about it).
_nakama_socket.send_match_state_async(_match_id, meta_op_code, JSON.stringify({
type = MetaMessageType.ASSIGN_PEER_ID,
session_id = presence.session_id,
peer_id = peer_id,
}))
_multiplayer_peer.peer_connected.emit(peer_id)
func _on_nakama_socket_received_match_presence(event) -> void:
if _match_state == MatchState.DISCONNECTED:
return
if event.match_id != _match_id:
return
for presence in event.joins:
if not _users.has(presence.session_id):
_users[presence.session_id] = User.new(presence)
# If we are the host, and they don't yet have a peer id, then let's
# generate a new id for them and send all the necessary messages.
if _my_peer_id == 1 and _users[presence.session_id].peer_id == 0:
_host_add_peer(presence)
for presence in event.leaves:
if not _users.has(presence.session_id):
continue
var peer_id = _users[presence.session_id].peer_id
_multiplayer_peer.peer_disconnected.emit(peer_id)
_users.erase(presence.session_id)
_id_map.erase(peer_id)
func _parse_json(data: String):
var json = JSON.new()
if json.parse(data) != OK:
return null
var content = json.get_data()
if not content is Dictionary:
return null
return content
func _on_nakama_socket_received_match_state(data) -> void:
if _match_state == MatchState.DISCONNECTED:
return
if data.match_id != _match_id:
return
if data.op_code == meta_op_code:
var content = _parse_json(data.data)
if content == null:
return
var type = content['type']
#print ("RECEIVED: ", content)
if type == MetaMessageType.CLAIM_HOST:
if _id_map.has(1):
# @todo Can we mediate this dispute?
push_error("User %s claiming to be host, when user %s has already claimed it" % [data.presence.session_id, _id_map[1]])
else:
_map_id_to_session(1, data.presence.session_id)
return
# Ensure that any meta messages are coming from the host!
if data.presence.session_id != _id_map[1]:
push_error("Received meta message from user %s who isn't the host: %s" % [data.presence.session_id, content])
return
if type == MetaMessageType.ASSIGN_PEER_ID:
var session_id = content['session_id']
var peer_id = content['peer_id']
if _users.has(session_id) and _users[session_id].peer_id != 0:
push_error("Attempting to assign peer id %s to %s which already has id %s" % [
peer_id,
session_id,
_users[session_id].peer_id,
])
return
_map_id_to_session(peer_id, session_id)
if _my_session_id == session_id:
_match_state = MatchState.CONNECTED
_multiplayer_peer.initialize(peer_id)
_multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTED)
match_joined.emit()
_multiplayer_peer.peer_connected.emit(1)
else:
_multiplayer_peer.peer_connected.emit(peer_id)
else:
_nakama_socket.logger.error("Received meta message with unknown type: %s" % type)
elif data.op_code == rpc_op_code:
var from_session_id: String = data.presence.session_id
if not _users.has(from_session_id) or _users[from_session_id].peer_id == 0:
push_error("Received RPC from %s which isn't assigned a peer id" % data.presence.session_id)
return
var from_peer_id = _users[from_session_id].peer_id
_multiplayer_peer.deliver_packet(data.binary_data, from_peer_id)
func _on_multiplayer_peer_packet_generated(peer_id: int, buffer: PackedByteArray) -> void:
if match_state == MatchState.CONNECTED:
var target_presences = null
if peer_id > 0:
if not _id_map.has(peer_id):
push_error("Attempting to send RPC to unknown peer id: %s" % peer_id)
return
target_presences = [ _users[_id_map[peer_id]].presence ]
_nakama_socket.send_match_state_raw_async(_match_id, rpc_op_code, buffer, target_presences)
else:
push_error("RPC sent while the NakamaMultiplayerBridge isn't connected!")
@@ -0,0 +1 @@
uid://cilli1r108eys
@@ -0,0 +1,97 @@
extends MultiplayerPeerExtension
class_name NakamaMultiplayerPeer
const MAX_PACKET_SIZE := 1 << 24
var _self_id := 0
var _connection_status: ConnectionStatus = CONNECTION_DISCONNECTED
var _refusing_new_connections := false
var _target_id := 0
class Packet extends RefCounted:
var data: PackedByteArray
var from: int
func _init(p_data: PackedByteArray, p_from: int) -> void:
data = p_data
from = p_from
var _incoming_packets := []
signal packet_generated (peer_id, buffer)
func _get_packet_script() -> PackedByteArray:
if _incoming_packets.size() == 0:
return PackedByteArray()
return _incoming_packets.pop_front().data
func _get_packet_mode() -> TransferMode:
return TRANSFER_MODE_RELIABLE
func _get_packet_channel() -> int:
return 0
func _put_packet_script(p_buffer: PackedByteArray) -> Error:
packet_generated.emit(_target_id, p_buffer)
return OK
func _get_available_packet_count() -> int:
return _incoming_packets.size()
func _get_max_packet_size() -> int:
return MAX_PACKET_SIZE
func _set_transfer_channel(p_channel) -> void:
pass
func _get_transfer_channel() -> int:
return 0
func _set_transfer_mode(p_mode: TransferMode) -> void:
pass
func _get_transfer_mode() -> TransferMode:
return TRANSFER_MODE_RELIABLE
func _set_target_peer(p_peer_id: int) -> void:
_target_id = p_peer_id
func _get_packet_peer() -> int:
if _connection_status != CONNECTION_CONNECTED:
return 1
if _incoming_packets.size() == 0:
return 1
return _incoming_packets[0].from
func _is_server() -> bool:
return _self_id == 1
func _poll() -> void:
pass
func _get_unique_id() -> int:
return _self_id
func _set_refuse_new_connections(p_enable: bool) -> void:
_refusing_new_connections = p_enable
func _is_refusing_new_connections() -> bool:
return _refusing_new_connections
func _get_connection_status() -> ConnectionStatus:
return _connection_status
func initialize(p_self_id: int) -> void:
if _connection_status != CONNECTION_CONNECTING:
return
_self_id = p_self_id
if _self_id == 1:
_connection_status = CONNECTION_CONNECTED
func set_connection_status(p_connection_status: int) -> void:
_connection_status = p_connection_status
func deliver_packet(p_data: PackedByteArray, p_from_peer_id: int) -> void:
var packet = Packet.new(p_data, p_from_peer_id);
_incoming_packets.push_back(packet)
@@ -0,0 +1 @@
uid://eo86fkn3pu0m
@@ -0,0 +1,160 @@
extends RefCounted
class_name NakamaSerializer
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 = []
for e in val:
if typeof(e) != TYPE_OBJECT:
continue
arr.append(serialize(e))
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 NakamaException.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(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 = NakamaException.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://cx6deypxgnwjs