Install Nakama

This commit is contained in:
2026-04-15 12:16:30 -05:00
parent ef9bf1c93c
commit 80a27e4d4e
46 changed files with 12322 additions and 2 deletions

5
addons/nakama/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
*.swp
*.import
test_suite/addons
test_suite/addons/*
test_suite/.import/*

203
addons/nakama/LICENSE Normal file
View File

@@ -0,0 +1,203 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

320
addons/nakama/README.md Normal file
View File

@@ -0,0 +1,320 @@
Nakama Godot
===========
> Godot client for Nakama server written in GDScript.
[Nakama](https://github.com/heroiclabs/nakama) is an open-source server designed to power modern games and apps. Features include user accounts, chat, social, matchmaker, realtime multiplayer, and much [more](https://heroiclabs.com).
This client implements the full API and socket options with the server. It's written in GDScript to support Godot Engine `4.0+`.
Full documentation is online - https://heroiclabs.com/docs
## Godot 3 & 4
You're currently looking at the Godot 4 version of the Nakama client for Godot.
If you are using Godot 3, you need to use the ['master'
branch](https://github.com/heroiclabs/nakama-godot/tree/godot-4) on GitHub.
## Getting Started
You'll need to setup the server and database before you can connect with the client. The simplest way is to use Docker but have a look at the [server documentation](https://github.com/heroiclabs/nakama#getting-started) for other options.
1. Install and run the servers. Follow these [instructions](https://heroiclabs.com/docs/install-docker-quickstart).
2. Download the client from the [releases page](https://github.com/heroiclabs/nakama-godot/releases) and import it into your project. You can also [download it from the asset repository](#asset-repository).
3. Add the `Nakama.gd` singleton (in `addons/com.heroiclabs.nakama/`) as an [autoload in Godot](https://docs.godotengine.org/en/stable/getting_started/step_by_step/singletons_autoload.html).
4. Use the connection credentials to build a client object using the singleton.
```gdscript
extends Node
func _ready():
var scheme = "http"
var host = "127.0.0.1"
var port = 7350
var server_key = "defaultkey"
var client := Nakama.create_client(server_key, host, port, scheme)
```
## Usage
The client object has many methods to execute various features in the server or open realtime socket connections with the server.
### Authenticate
There's a variety of ways to [authenticate](https://heroiclabs.com/docs/authentication) with the server. Authentication can create a user if they don't already exist with those credentials. It's also easy to authenticate with a social profile from Google Play Games, Facebook, Game Center, etc.
```gdscript
var email = "super@heroes.com"
var password = "batsignal"
# Use 'await' to wait for the request to complete.
var session : NakamaSession = await client.authenticate_email_async(email, password)
print(session)
```
### Sessions
When authenticated the server responds with an auth token (JWT) which contains useful properties and gets deserialized into a `NakamaSession` object.
```gdscript
print(session.token) # raw JWT token
print(session.user_id)
print(session.username)
print("Session has expired: %s" % session.expired)
print("Session expires at: %s" % session.expire_time)
```
It is recommended to store the auth token from the session and check at startup if it has expired. If the token has expired you must reauthenticate. The expiry time of the token can be changed as a setting in the server.
```gdscript
var authtoken = "restored from somewhere"
var session2 = NakamaClient.restore_session(authtoken)
if session2.expired:
print("Session has expired. Must reauthenticate!")
```
NOTE: The length of the lifetime of a session can be changed on the server with the `--session.token_expiry_sec` command flag argument.
### Requests
The client includes lots of builtin APIs for various features of the game server. These can be accessed with the async methods. It can also call custom logic in RPC functions on the server. These can also be executed with a socket object.
All requests are sent with a session object which authorizes the client.
```gdscript
var account = await client.get_account_async(session)
print(account.user.id)
print(account.user.username)
print(account.wallet)
```
### Exceptions
Since Godot Engine does not support exceptions, whenever you make an async request via the client or socket, you can check if an error occurred via the `is_exception()` method.
```gdscript
var an_invalid_session = NakamaSession.new() # An empty session, which will cause and error when we use it.
var account2 = await client.get_account_async(an_invalid_session)
print(account2) # This will print the exception
if account2.is_exception():
print("We got an exception")
```
### Socket
The client can create one or more sockets with the server. Each socket can have it's own event listeners registered for responses received from the server.
```gdscript
var socket = Nakama.create_socket_from(client)
socket.connected.connect(self._on_socket_connected)
socket.closed.connect(self._on_socket_closed)
socket.received_error.connect(self._on_socket_error)
await socket.connect_async(session)
print("Done")
func _on_socket_connected():
print("Socket connected.")
func _on_socket_closed():
print("Socket closed.")
func _on_socket_error(err):
printerr("Socket error %s" % err)
```
## Integration with Godot's High-level Multiplayer API
Godot provides a [High-level Multiplayer
API](https://docs.godotengine.org/en/latest/tutorials/networking/high_level_multiplayer.html),
allowing developers to make RPCs, calling functions that run on other peers in
a multiplayer match.
For example:
```gdscript
func _process(delta):
if not is_multiplayer_authority():
return
var input_vector = get_input_vector()
# Move the player locally.
velocity = input_vector * SPEED
move_and_slide()
# Then update the player's position on all other connected clients.
update_remote_position.rpc(position)
@rpc(any_peer)
func update_remote_position(new_position):
position = new_position
```
Godot provides a number of built-in backends for sending the RPCs, including:
ENet, WebSockets, and WebRTC.
However, you can also use the Nakama client as a backend! This can allow you to
continue using Godot's familiar High-level Multiplayer API, but with the RPCs
transparently sent over a realtime Nakama match.
To do that, you need to use the `NakamaMultiplayerBridge` class:
```gdscript
var multiplayer_bridge
func _ready():
# [...]
# You must have a working 'socket', created as described above.
multiplayer_bridge = NakamaMultiplayerBridge.new(socket)
multiplayer_bridge.match_join_error.connect(self._on_match_join_error)
multiplayer_bridge.match_joined.connect(self._on_match_joined)
get_tree().get_multiplayer().set_multiplayer_peer(multiplayer_bridge.multiplayer_peer)
func _on_match_join_error(error):
print ("Unable to join match: ", error.message)
func _on_match_join() -> void:
print ("Joined match with id: ", multiplayer_bridge.match_id)
```
You can also connect to any of the usual signals on `MultiplayerAPI`, for
example:
```gdscript
get_tree().get_multiplayer().peer_connected.connect(self._on_peer_connected)
get_tree().get_multiplayer().peer_disconnected.connect(self._on_peer_disconnected)
func _on_peer_connected(peer_id):
print ("Peer joined match: ", peer_id)
func _on_peer_disconnected(peer_id):
print ("Peer left match: ", peer_id)
```
Then you need to join a match, using one of the following methods:
- Create a new private match, with your client as the host.
```gdscript
multiplayer_bridge.create_match()
```
- Join a private match.
```gdscript
multiplayer_bridge.join_match(match_id)
```
- Create or join a private match with the given name.
```gdscript
multiplayer_bridge.join_named_match(match_name)
```
- Use the matchmaker to find and join a public match.
```gdscript
var ticket = await socket.add_matchmaker_async()
if ticket.is_exception():
print ("Error joining matchmaking pool: ", ticket.get_exception().message)
return
multiplayer_bridge.start_matchmaking(ticket)
```
After the the "match_joined" signal is emitted, you can start sending RPCs as
usual with the `rpc()` function, and calling any other functions associated with
the High-level Multiplayer API, such as `get_tree().get_multiplayer().get_unique_id()`
and `node.set_network_authority(peer_id)` and `node.is_network_authority()`.
## .NET / C#
If you're using the .NET version of Godot with C# support, you can use the
[Nakama .NET client](https://github.com/heroiclabs/nakama-dotnet/), which can be
installed via NuGet:
```
dotnet add package NakamaClient
```
This addon includes some C# classes for use with the .NET client, to provide deeper
integration with Godot:
- `GodotLogger`: A logger which prints to the Godot console.
- `GodotHttpAdapter`: An HTTP adapter which uses Godot's HTTPRequest node.
- `GodotWebSocketAdapter`: A socket adapter which uses Godot's WebSocketClient.
Here's an example of how to use them:
```csharp
var http_adapter = new GodotHttpAdapter();
// It's a Node, so it needs to be added to the scene tree.
// Consider putting this in an autoload singleton so it won't go away unexpectedly.
AddChild(http_adapter);
const string scheme = "http";
const string host = "127.0.0.1";
const int port = 7350;
const string serverKey = "defaultkey";
// Pass in the 'http_adapter' as the last argument.
var client = new Client(scheme, host, port, serverKey, http_adapter);
// To log DEBUG messages to the Godot console.
client.Logger = new GodotLogger("Nakama", GodotLogger.LogLevel.DEBUG);
ISession session;
try {
session = await client.AuthenticateDeviceAsync(OS.GetUniqueId(), "TestUser", true);
}
catch (ApiResponseException e) {
GD.PrintErr(e.ToString());
return;
}
var websocket_adapter = new GodotWebSocketAdapter();
// Like the HTTP adapter, it's a Node, so it needs to be added to the scene tree.
// Consider putting this in an autoload singleton so it won't go away unexpectedly.
AddChild(websocket_adapter);
// Pass in the 'websocket_adapter' as the last argument.
var socket = Socket.From(client, websocket_adapter);
```
**Note:** _The out-of-the-box Nakama .NET client will work fine with desktop builds of your game! However, it won't work with HTML5 builds, unless you use the `GodotHttpAdapter` and `GodotWebSocketAdapter` classes._
## Contribute
The development roadmap is managed as GitHub issues and pull requests are welcome. If you're interested to improve the code please open an issue to discuss the changes or drop in and discuss it in the [community forum](https://forum.heroiclabs.com).
### Run Tests
To run tests you will need to run the server and database. Most tests are written as integration tests which execute against the server. A quick approach we use with our test workflow is to use the Docker compose file described in the [documentation](https://heroiclabs.com/docs/install-docker-quickstart).
Additionally, you will need to copy (or symlink) the `addons` folder inside the `test_suite` folder. You can now run the `test_suite` project from the Godot Editor.
To run the tests on a headless machine (without a GPU) you can download a copy of [Godot Headless](https://godotengine.org/download/server) and run it from the command line.
To automate this procedure, move the headless binary to `test_suite/bin/godot.elf`, and run the tests via the `test_suite/run_tests.sh` shell script (exit code will report test failure/success).
```shell
cd nakama
docker-compose -f ./docker-compose-postgres.yml up
cd ..
cd nakama-godot
sh test_suite/run_tests.sh
```
### Make a new release
To make a new release ready for distribution, simply zip the addons folder recursively (possibly adding `CHANGELOG`, `LICENSE`, and `README.md` too).
On unix systems, you can run the following command (replacing `$VERSION` with the desired version number). Remember to update the `CHANGELOG` file first.
```shell
zip -r nakama-$VERSION.zip addons/ LICENSE CHANGELOG.md README.md
```
### License
This project is licensed under the [Apache-2 License](https://github.com/heroiclabs/nakama-godot/blob/master/LICENSE).

View File

@@ -0,0 +1,61 @@
@tool
extends Node
# The default host address of the server.
const DEFAULT_HOST : String = "127.0.0.1"
# The default port number of the server.
const DEFAULT_PORT : int = 7350
# The default timeout for the connections.
const DEFAULT_TIMEOUT = 3
# The default protocol scheme for the client connection.
const DEFAULT_CLIENT_SCHEME : String = "http"
# The default protocol scheme for the socket connection.
const DEFAULT_SOCKET_SCHEME : String = "ws"
# The default log level for the Nakama logger.
const DEFAULT_LOG_LEVEL = NakamaLogger.LOG_LEVEL.DEBUG
var _http_adapter = null
var logger = NakamaLogger.new()
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
func get_client_adapter() -> NakamaHTTPAdapter:
if _http_adapter == null:
_http_adapter = NakamaHTTPAdapter.new()
_http_adapter.logger = logger
_http_adapter.name = "NakamaHTTPAdapter"
add_child(_http_adapter)
return _http_adapter
func create_socket_adapter() -> NakamaSocketAdapter:
var adapter = NakamaSocketAdapter.new()
adapter.name = "NakamaWebSocketAdapter"
adapter.logger = logger
add_child(adapter)
return adapter
func create_client(p_server_key : String,
p_host : String = DEFAULT_HOST,
p_port : int = DEFAULT_PORT,
p_scheme : String = DEFAULT_CLIENT_SCHEME,
p_timeout : int = DEFAULT_TIMEOUT,
p_log_level : int = DEFAULT_LOG_LEVEL) -> NakamaClient:
logger._level = p_log_level
return NakamaClient.new(get_client_adapter(), p_server_key, p_scheme, p_host, p_port, p_timeout)
func create_socket(p_host : String = DEFAULT_HOST,
p_port : int = DEFAULT_PORT,
p_scheme : String = DEFAULT_SOCKET_SCHEME) -> NakamaSocket:
return NakamaSocket.new(create_socket_adapter(), p_host, p_port, p_scheme, true)
func create_socket_from(p_client : NakamaClient) -> NakamaSocket:
var scheme = "ws"
if p_client.scheme == "https":
scheme = "wss"
return NakamaSocket.new(create_socket_adapter(), p_client.host, p_client.port, scheme, true)

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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"

View File

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

View File

@@ -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]

View File

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

View File

@@ -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 {}

View File

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

View File

@@ -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
})

View File

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

View File

@@ -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
})

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
uid://4uxe86ur52yc

View File

@@ -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, response code: %d" % [
id, json.error, 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()

View File

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

View File

@@ -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, true, 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);
}
}
}

View File

@@ -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));
}
}
}
}

View File

@@ -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()));
}
}
}
}

View File

@@ -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

View File

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

View File

@@ -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())

View File

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

View File

@@ -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

View File

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

View File

@@ -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]

View File

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

View File

@@ -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)

View File

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

View File

@@ -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!")

View File

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

View File

@@ -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() -> int:
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)

View File

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

View File

@@ -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

View File

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

View File

@@ -0,0 +1,21 @@
codegen
=======
> A util tool to generate a client from the Swagger spec of Nakama's server API.
## Usage
```shell
go run main.go "$GOPATH/src/github.com/heroiclabs/nakama/apigrpc/apigrpc.swagger.json" > ../addons/com.heroiclabs.nakama/api/NakamaAPI.gd
```
### Rationale
We want to maintain a simple lean low level client within our GDScript client which has minimal dependencies so we built our own. This gives us complete control over the dependencies required and structure of the code generated.
The generated code is designed to be supported Godot Engine `3.1+`.
### Limitations
The code generator has __only__ been checked against the Swagger specification generated for Nakama server. YMMV.

View File

@@ -0,0 +1,639 @@
// Copyright 2018 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.
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"strings"
"text/template"
)
var utilities = map[string]string {
"ApiAccount":
`
var _wallet_dict = null
var wallet_dict : Dictionary:
get:
if _wallet_dict == null:
if _wallet == null:
return {}
var json = JSON.new()
if json.parse(_wallet) != OK:
return {}
_wallet_dict = json.get_data()
return _wallet_dict as Dictionary
`,
}
const codeTemplate string = `### Code generated by codegen/main.go. DO NOT EDIT. ###
extends RefCounted
class_name NakamaAPI
{{- range $defname, $definition := .Definitions }}
{{- $classname := $defname | title }}
{{- if isRefToEnum $classname }}
# {{ enumSummary $definition | stripNewlines }}
{{- range $idx, $val := ($definition | enumDescriptions) }}
# {{ $val }}
{{- end -}}
# {{ $definition | enumDescriptions }}
enum {{ $classname | title }} { {{- range $idx, $enum := $definition.Enum }}{{ $enum }} = {{ $idx }},{{- end -}} }
{{- else }}
# {{ $definition.Description | stripNewlines }}
class {{ $classname }} extends NakamaAsyncResult:
const _SCHEMA = {
{{- range $propname, $property := $definition.Properties }}
{{- $fieldname := $propname | pascalToSnake }}
{{- $_field := printf "_%s" $fieldname }}
{{- $gdType := godotType $property.Type $property.Ref $property.Items.Type $property.Items.Ref (isRefToEnum (cleanRef $property.Ref)) }}
"{{ $fieldname }}": {"name": "{{ $_field }}", "type": {{ $gdType | godotSchemaType }}, "required": false
{{- if eq $property.Type "array" -}}
, "content": {{ (godotType $property.Items.Type $property.Items.Ref "" "" false) | godotSchemaType }}
{{- else if eq $property.Type "object" -}}
, "content": {{ (godotType $property.AdditionalProperties.Type "" "" "" false) | godotSchemaType }}
{{- end -}}
},
{{- end }}
}
{{- range $propname, $property := $definition.Properties }}
{{- $fieldname := $propname | pascalToSnake }}
{{- $_field := printf "_%s" $fieldname }}
{{- $gdType := godotType $property.Type $property.Ref $property.Items.Type $property.Items.Ref (isRefToEnum (cleanRef $property.Ref)) }}
{{- $gdDef := $gdType | godotDef }}
# {{ $property.Description }}
var {{ $_field }}
var {{ $fieldname }} : {{ $gdType }}:
get:
{{- if $property.Ref }}
{{- if isRefToEnum (cleanRef $property.Ref) }}{{/* Enums */}}
return {{ cleanRef $property.Ref }}.values()[0] if not {{ cleanRef $property.Ref }}.values().has({{ $_field }}) else {{ $_field }}
{{- else }}{{/* Object reference */}}
return _{{ $fieldname }} as {{ $gdType }}
{{- end }}
{{- else if eq $property.Type "object"}}{{/* Dictionaries */}}
return Dictionary() if not {{ $_field }} is Dictionary else {{ $_field }}.duplicate()
{{- else }}{{/* Simple type */}}
return {{ $gdDef }} if not {{ $_field }} is {{ $gdType }} else {{ $gdType }}({{ $_field }})
{{- end }}
{{- end }}
{{- godotClassUtils $classname }}
func _init(p_exception = null):
super(p_exception)
static func create(p_ns : GDScript, p_dict : Dictionary) -> {{ $classname }}:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "{{ $classname }}", p_dict), {{ $classname }}) as {{ $classname }}
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func _to_string() -> String:
if is_exception():
return get_exception()._to_string()
var output : String = ""
{{- range $propname, $property := $definition.Properties }}
{{- $fieldname := $propname | pascalToSnake }}
{{- $_field := printf "_%s" $fieldname }}
{{- if eq $property.Type "array" }}
output += "{{ $fieldname }}: %s, " % [{{ $_field }}]
{{- else if eq $property.Type "object" }}
var map_string : String = ""
if typeof({{ $_field }}) == TYPE_DICTIONARY:
for k in {{ $_field }}:
map_string += "{%s=%s}, " % [k, {{ $_field }}[k]]
output += "{{ $fieldname }}: [%s], " % map_string
{{- else }}
output += "{{ $fieldname }}: %s, " % {{ $_field }}
{{- end }}
{{- end }}
return output
{{- end }}
{{- end }}
# The low level client for the Nakama API.
class ApiClient extends RefCounted:
var _base_uri : String
var _http_adapter
var _namespace : GDScript
var _server_key : String
var auto_refresh := true
var auto_refresh_time := 300
var auto_retry : bool:
set(p_value):
_http_adapter.auto_retry = p_value
get:
return _http_adapter.auto_retry
var auto_retry_count : int:
set(p_value):
_http_adapter.auto_retry_count = p_value
get:
return _http_adapter.auto_retry_count
var auto_retry_backoff_base : int:
set(p_value):
_http_adapter.auto_retry_backoff_base = p_value
get:
return _http_adapter.auto_retry_backoff_base
var last_cancel_token:
get:
return _http_adapter.get_last_token()
func _init(p_base_uri : String, p_http_adapter, p_namespace : GDScript, p_server_key : String, p_timeout : int = 10):
_base_uri = p_base_uri
_http_adapter = p_http_adapter
_http_adapter.timeout = p_timeout
_namespace = p_namespace
_server_key = p_server_key
func _refresh_session(p_session : NakamaSession):
if auto_refresh and p_session.is_valid() and p_session.refresh_token and not p_session.is_refresh_expired() and p_session.would_expire_in(auto_refresh_time):
var request = ApiSessionRefreshRequest.new()
request._token = p_session.refresh_token
return await session_refresh_async(_server_key, "", request)
return null
func cancel_request(p_token):
if p_token:
_http_adapter.cancel_request(p_token)
{{- range $url, $path := .Paths }}
{{- range $method, $operation := $path}}
# {{ $operation.Summary | stripNewlines }}
{{- if $operation.Responses.Ok.Schema.Ref }}
func {{ $operation.OperationId | apiFuncName }}_async(
{{- else }}
func {{ $operation.OperationId | apiFuncName }}_async(
{{- end}}
{{- if $operation.Security }}
{{- with (index $operation.Security 0) }}
{{- range $key, $value := . }}
{{- if eq $key "BasicAuth" }}
p_basic_auth_username : String
, p_basic_auth_password : String
{{- else if eq $key "HttpKeyAuth" }}
p_bearer_token : String
{{- end }}
{{- end }}
{{- end }}
{{- else }}
p_session : NakamaSession
{{- end }}
{{- range $parameter := $operation.Parameters }}
{{- $argument := $parameter.Name | prependParameter }}
{{- if not $parameter.Required }}{{/* Godot does not support typed optional parameters yet. */}}
, {{ $argument }} = null # : {{ $parameter.Type }}
{{- else if eq $parameter.In "body" }}
{{- if eq $parameter.Schema.Type "string" }}
, {{ $argument }} : String
{{- else }}
, {{ $argument }} : {{ $parameter.Schema.Ref | cleanRef }}
{{- end }}
{{- else }}
, {{ $argument }} : {{ godotType $parameter.Type $parameter.Schema.Ref $parameter.Items.Type "" (isRefToEnum (cleanRef $parameter.Schema.Ref)) }}
{{- end }}
{{- end }}
)
{{- if $operation.Responses.Ok.Schema.Ref }} -> {{ $operation.Responses.Ok.Schema.Ref | cleanRef }}
{{- else }} -> NakamaAsyncResult
{{- end }}:
{{- $classname := "NakamaAsyncResult" }}
{{- if $operation.Responses.Ok.Schema.Ref }}
{{- $classname = $operation.Responses.Ok.Schema.Ref | cleanRef }}
{{- end }}
{{- if not $operation.Security }}
var try_refresh = await _refresh_session(p_session)
if try_refresh != null:
if try_refresh.is_exception():
return {{ $classname }}.new(try_refresh.get_exception())
await p_session.refresh(try_refresh)
{{- end }}
var urlpath : String = "{{- $url }}"
{{- range $parameter := $operation.Parameters }}
{{- $argument := $parameter.Name | prependParameter }}
{{- if eq $parameter.In "path" }}
urlpath = urlpath.replace("{{- print "{" $parameter.Name "}"}}", NakamaSerializer.escape_http({{ $argument }}))
{{- end }}
{{- end }}
var query_params = ""
{{- range $parameter := $operation.Parameters }}
{{- $argument := $parameter.Name | prependParameter }}
{{- $snakecase := $parameter.Name | pascalToSnake }}
{{- if eq $parameter.In "query"}}
{{- if $parameter.Required }}
if true: # Hack for static checks
{{- else }}
if {{ $argument }} != null:
{{- end }}
{{- if eq $parameter.Type "integer" }}
query_params += "{{- $snakecase }}=%d&" % {{ $argument }}
{{- else if eq $parameter.Type "string" }}
query_params += "{{- $snakecase }}=%s&" % NakamaSerializer.escape_http({{ $argument }})
{{- else if eq $parameter.Type "boolean" }}
query_params += "{{- $snakecase }}=%s&" % str(bool({{ $argument }})).to_lower()
{{- else if eq $parameter.Type "array" }}
for elem in {{ $argument }}:
query_params += "{{- $snakecase }}=%s&" % elem
{{- else }}
{{ $parameter }} // ERROR
{{- end }}
{{- end }}
{{- end }}
var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""]
var method = "{{- $method | uppercase }}"
var headers = {}
{{- if $operation.Security }}
{{- with (index $operation.Security 0) }}
{{- range $key, $value := . }}
{{- if eq $key "BasicAuth" }}
var credentials = Marshalls.utf8_to_base64(p_basic_auth_username + ":" + p_basic_auth_password)
var header = "Basic %s" % credentials
headers["Authorization"] = header
{{- else if eq $key "HttpKeyAuth" }}
if (p_bearer_token):
var header = "Bearer %s" % p_bearer_token
headers["Authorization"] = header
{{- end }}
{{- end }}
{{- end }}
{{- else }}
var header = "Bearer %s" % p_session.token
headers["Authorization"] = header
{{- end }}
var content : PackedByteArray
{{- range $parameter := $operation.Parameters }}
{{- $argument := $parameter.Name | prependParameter }}
{{- if eq $parameter.In "body" }}
{{- if eq $parameter.Schema.Type "string" }}
content = JSON.stringify({{ $argument }}).to_utf8_buffer()
{{- else }}
content = JSON.stringify({{ $argument }}.serialize()).to_utf8_buffer()
{{- end }}
{{- end }}
{{- end }}
var result = await _http_adapter.send_async(method, uri, headers, content)
if result is NakamaException:
return {{ $classname }}.new(result)
{{- if $operation.Responses.Ok.Schema.Ref }}
var out : {{ $classname }} = NakamaSerializer.deserialize(_namespace, "{{ $classname }}", result)
return out
{{- else }}
return NakamaAsyncResult.new()
{{- end}}
{{- end }}
{{- end }}
`
func convertRefToClassName(input string) (className string) {
cleanRef := strings.TrimPrefix(input, "#/definitions/")
className = strings.Title(cleanRef)
return
}
func stripNewlines(input string) (output string) {
output = strings.Replace(input, "\n", " ", -1)
return
}
func prependParameter(input string) (output string) {
output = "p_" + pascalToSnake(input)
return
}
func pascalToSnake(input string) (output string) {
output = ""
prev_low := false
for _, v := range input {
is_cap := v >= 'A' && v <= 'Z'
is_low := v >= 'a' && v <= 'z'
if is_cap && prev_low {
output = output + "_"
}
output += strings.ToLower(string(v))
prev_low = is_low
}
return
}
func apiFuncName(input string) (output string) {
output = pascalToSnake(input[7:])
return
}
func godotType(p_type string, p_ref string, p_item_type string, p_extra string, p_is_enum bool) (out string) {
is_array := false
is_dict := false
switch p_type {
case "integer":
out = "int"
case "string":
out = "String"
case "boolean":
out = "bool"
case "array":
is_array = true
case "object":
is_dict = true
default:
if p_is_enum {
out = "int"
} else {
out = convertRefToClassName(p_ref)
}
}
if is_array {
switch p_item_type {
case "integer":
out = "PackedIntArray"
return
case "string":
out = "PackedStringArray"
return
case "boolean":
out = "PackedIntArray"
return
default:
out = "Array"
}
}
if is_dict {
out = "Dictionary"
}
return
}
func godotDef(p_type string) (out string) {
switch(p_type) {
case "bool": out = "false"
case "int": out = "0"
case "String": out = "\"\""
case "PackedIntArray": out = "PackedIntArray()"
case "PackedStringArray": out = "PackedStringArray()"
case "Array": out = "Array()"
case "Dictionary": out = "Dictionary()"
}
return
}
func godotLooseType(p_type string) (out string) {
switch(p_type) {
case "PackedStringArray", "PackedIntArray":
out = "Array"
default:
out = p_type
}
return
}
func godotSchemaType(p_type string) (out string) {
out = "TYPE_"
switch(p_type) {
case "bool": out += "BOOL"
case "int": out += "INT"
case "String": out += "STRING"
case "PackedIntArray": out += "ARRAY"
case "PackedStringArray": out += "ARRAY"
case "Array": out += "ARRAY"
case "Dictionary": out += "DICTIONARY"
default: out = "\"" + p_type + "\""
}
return
}
func pascalToCamel(input string) (camelCase string) {
if input == "" {
return ""
}
camelCase = strings.ToLower(string(input[0]))
camelCase += string(input[1:])
return camelCase
}
func camelToPascal(camelCase string) (pascalCase string) {
if len(camelCase) <= 0 {
return ""
}
pascalCase = strings.ToUpper(string(camelCase[0])) + camelCase[1:]
return
}
func enumSummary(def Definition) string {
// quirk of swagger generation: if enum doesn't have a title
// then the title can be found as the first entry in the split description.
if def.Title != "" {
return def.Title
}
split := strings.Split(def.Description, "\n")
if len(split) <= 0 {
panic("No newlines in enum description found.")
}
return split[0]
}
func enumDescriptions(def Definition) (output []string) {
split := strings.Split(def.Description, "\n")
if len(split) <= 0 {
panic("No newlines in enum description found.")
}
if def.Title != "" {
return split
}
// quirk of swagger generation: if enum doesn't have a title
// then the title can be found as the first entry in the split description.
// so ignore for individual enum descriptions.
return split[2:]
}
type Definition struct {
Properties map[string]struct {
Type string
Ref string `json:"$ref"` // used with object
Items struct { // used with type "array"
Type string
Ref string `json:"$ref"`
}
AdditionalProperties struct {
Type string // used with type "map"
}
Format string // used with type "boolean"
Description string
}
Enum []string
Description string
// used only by enums
Title string
}
func godotClassUtils(p_name string) string {
if val, ok := utilities[p_name]; ok {
return val
}
return ""
}
func main() {
// Argument flags
var output = flag.String("output", "", "The output for generated code.")
flag.Parse()
inputs := flag.Args()
if len(inputs) < 1 {
fmt.Printf("No input file found: %s\n\n", inputs)
fmt.Println("openapi-gen [flags] inputs...")
flag.PrintDefaults()
return
}
input := inputs[0]
content, err := ioutil.ReadFile(input)
if err != nil {
fmt.Printf("Unable to read file: %s\n", err)
return
}
var schema struct {
Paths map[string]map[string]struct {
Summary string
OperationId string
Responses struct {
Ok struct {
Schema struct {
Ref string `json:"$ref"`
}
} `json:"200"`
}
Parameters []struct {
Name string
In string
Required bool
Type string // used with primitives
Items struct { // used with type "array"
Type string
}
Schema struct { // used with http body
Type string
Ref string `json:"$ref"`
}
Format string // used with type "boolean"
}
Security []map[string][]struct {
}
}
Definitions map[string]Definition
}
if err := json.Unmarshal(content, &schema); err != nil {
fmt.Printf("Unable to decode input %s : %s\n", input, err)
return
}
fmap := template.FuncMap{
"cleanRef": convertRefToClassName,
"stripNewlines": stripNewlines,
"title": strings.Title,
"uppercase": strings.ToUpper,
"prependParameter": prependParameter,
"pascalToSnake": pascalToSnake,
"apiFuncName": apiFuncName,
"godotType": godotType,
"godotLooseType": godotLooseType,
"godotSchemaType": godotSchemaType,
"godotDef": godotDef,
"isRefToEnum": func(ref string) bool {
if len(ref) == 0 {
return false
}
// swagger schema definition keys have inconsistent casing
var camelOk bool
var pascalOk bool
var enums []string
asCamel := pascalToCamel(ref)
if _, camelOk = schema.Definitions[asCamel]; camelOk {
enums = schema.Definitions[asCamel].Enum
}
asPascal := camelToPascal(ref)
if _, pascalOk = schema.Definitions[asPascal]; pascalOk {
enums = schema.Definitions[asPascal].Enum
}
if !pascalOk && !camelOk {
fmt.Printf("no definition found: %v", ref)
return false
}
return len(enums) > 0
},
"enumDescriptions": enumDescriptions,
"enumSummary": enumSummary,
"godotClassUtils": godotClassUtils,
}
tmpl, err := template.New(input).Funcs(fmap).Parse(codeTemplate)
if err != nil {
fmt.Printf("Template parse error: %s\n", err)
return
}
if len(*output) < 1 {
tmpl.Execute(os.Stdout, schema)
return
}
f, err := os.Create(*output)
if err != nil {
fmt.Printf("Unable to create file: %s\n", err)
return
}
defer f.Close()
writer := bufio.NewWriter(f)
tmpl.Execute(writer, schema)
writer.Flush()
}

View File

@@ -8,13 +8,29 @@
config_version=5
[animation]
compatibility/default_parent_skeleton_in_mesh_instance_3d=true
[application]
config/name="Mercury"
run/main_scene="uid://biwfc6l2lnu11"
config/features=PackedStringArray("4.5", "Forward Plus")
run/main_scene="uid://lobbyscene01"
config/features=PackedStringArray("4.6", "Forward Plus")
config/icon="res://icon.svg"
[autoload]
Nakama="*uid://dpwa6ykdkxalb"
[file_customization]
folder_colors={
"res://assets/": "red",
"res://scenes/": "green",
"res://src/": "blue"
}
[input]
accelerate={

0
server/.gdignore Normal file
View File

8
server/data/config.yml Normal file
View File

@@ -0,0 +1,8 @@
name: nakama1
database:
address:
- "postgres:localdb@postgres:5432/nakama"
logger:
level: DEBUG
session:
token_expiry_sec: 7200

53
server/docker-compose.yml Normal file
View File

@@ -0,0 +1,53 @@
version: "3"
services:
postgres:
container_name: postgres
image: postgres:12.2-alpine
environment:
- POSTGRES_DB=nakama
- POSTGRES_PASSWORD=localdb
volumes:
- data:/var/lib/postgresql/data
expose:
- "8080"
- "5432"
ports:
- "5432:5432"
- "8080:8080"
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres", "-d", "nakama"]
interval: 3s
timeout: 3s
retries: 5
nakama:
container_name: nakama
image: registry.heroiclabs.com/heroiclabs/nakama:3.22.0
entrypoint:
- "/bin/sh"
- "-ecx"
- >
/nakama/nakama migrate up --database.address postgres:localdb@postgres:5432/nakama &&
exec /nakama/nakama --name nakama1 --database.address postgres:localdb@postgres:5432/nakama --logger.level DEBUG --session.token_expiry_sec 7200 --config /nakama/data/config.yml
restart: always
links:
- "postgres:db"
depends_on:
postgres:
condition: service_healthy
volumes:
- ./data:/nakama/data
expose:
- "7349"
- "7350"
- "7351"
ports:
- "7349:7349"
- "7350:7350"
- "7351:7351"
healthcheck:
test: ["CMD", "/nakama/nakama", "healthcheck"]
interval: 10s
timeout: 5s
retries: 5
volumes:
data: