Initial Void Bastion 0.20 prototype
This commit is contained in:
commit
7362ca0f02
251 files changed
+29688
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user