Install Nakama
This commit is contained in:
5
addons/nakama/.gitignore
vendored
Normal file
5
addons/nakama/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
*.swp
|
||||
*.import
|
||||
test_suite/addons
|
||||
test_suite/addons/*
|
||||
test_suite/.import/*
|
||||
203
addons/nakama/LICENSE
Normal file
203
addons/nakama/LICENSE
Normal 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
320
addons/nakama/README.md
Normal 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).
|
||||
61
addons/nakama/addons/com.heroiclabs.nakama/Nakama.gd
Normal file
61
addons/nakama/addons/com.heroiclabs.nakama/Nakama.gd
Normal 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)
|
||||
1
addons/nakama/addons/com.heroiclabs.nakama/Nakama.gd.uid
Normal file
1
addons/nakama/addons/com.heroiclabs.nakama/Nakama.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dpwa6ykdkxalb
|
||||
6120
addons/nakama/addons/com.heroiclabs.nakama/api/NakamaAPI.gd
Normal file
6120
addons/nakama/addons/com.heroiclabs.nakama/api/NakamaAPI.gd
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
uid://di20rxd4epy7e
|
||||
902
addons/nakama/addons/com.heroiclabs.nakama/api/NakamaRTAPI.gd
Normal file
902
addons/nakama/addons/com.heroiclabs.nakama/api/NakamaRTAPI.gd
Normal 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"
|
||||
@@ -0,0 +1 @@
|
||||
uid://bkvy6qv3ie32g
|
||||
@@ -0,0 +1,635 @@
|
||||
extends RefCounted
|
||||
class_name NakamaRTMessage
|
||||
|
||||
# Send a channel join message to the server.
|
||||
class ChannelJoin:
|
||||
|
||||
const _SCHEMA = {
|
||||
"persistence": {"name": "persistence", "type": TYPE_BOOL, "required": true},
|
||||
"hidden": {"name": "hidden", "type": TYPE_BOOL, "required": true},
|
||||
"target": {"name": "target", "type": TYPE_STRING, "required": true},
|
||||
"type": {"name": "type", "type": TYPE_INT, "required": true},
|
||||
}
|
||||
|
||||
enum ChannelType {
|
||||
# A chat room which can be created dynamically with a name.
|
||||
Room = 1,
|
||||
# A private chat between two users.
|
||||
DirectMessage = 2,
|
||||
# A chat within a group on the server.
|
||||
Group = 3
|
||||
}
|
||||
|
||||
var persistence : bool
|
||||
var hidden : bool
|
||||
var target : String
|
||||
var type : int
|
||||
|
||||
func _init(p_target : String, p_type : int, p_persistence : bool, p_hidden : bool):
|
||||
persistence = p_persistence
|
||||
hidden = p_hidden
|
||||
target = p_target
|
||||
type = p_type if p_type >= ChannelType.Room and p_type <= ChannelType.Group else 0 # Will cause error server side
|
||||
|
||||
func serialize() -> Dictionary:
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "channel_join"
|
||||
|
||||
func _to_string():
|
||||
return "ChannelJoin<persistence=%s, hidden=%s, target=%s, type=%d>" % [persistence, hidden, target, type]
|
||||
|
||||
|
||||
# A leave message for a match on the server.
|
||||
class ChannelLeave extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"channel_id": {"name": "channel_id", "type": TYPE_STRING, "required": true}
|
||||
}
|
||||
var channel_id : String
|
||||
|
||||
func _init(p_channel_id : String):
|
||||
channel_id = p_channel_id
|
||||
|
||||
func serialize() -> Dictionary:
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "channel_leave"
|
||||
|
||||
func _to_string():
|
||||
return "ChannelLeave<channel_id=%s>" % [channel_id]
|
||||
|
||||
|
||||
class ChannelMessageRemove extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"channel_id": {"name": "channel_id", "type": TYPE_STRING, "required": true},
|
||||
"message_id": {"name": "message_id", "type": TYPE_STRING, "required": true}
|
||||
}
|
||||
|
||||
var channel_id : String
|
||||
var message_id : String
|
||||
|
||||
func _init(p_channel_id : String, p_message_id):
|
||||
channel_id = p_channel_id
|
||||
message_id = p_message_id
|
||||
|
||||
func serialize() -> Dictionary:
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "channel_message_remove"
|
||||
|
||||
func _to_string():
|
||||
return "ChannelMessageRemove<channel_id=%s, message_id=%s>" % [channel_id, message_id]
|
||||
|
||||
|
||||
# Send a chat message to a channel on the server.
|
||||
class ChannelMessageSend extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"channel_id": {"name": "channel_id", "type": TYPE_STRING, "required": true},
|
||||
"content": {"name": "content", "type": TYPE_STRING, "required": true}
|
||||
}
|
||||
|
||||
var channel_id : String
|
||||
var content : String
|
||||
|
||||
func _init(p_channel_id : String, p_content):
|
||||
channel_id = p_channel_id
|
||||
content = p_content
|
||||
|
||||
func serialize() -> Dictionary:
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "channel_message_send"
|
||||
|
||||
func _to_string():
|
||||
return "ChannelMessageSend<channel_id=%s, content=%s>" % [channel_id, content]
|
||||
|
||||
|
||||
class ChannelMessageUpdate extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"channel_id": {"name": "channel_id", "type": TYPE_STRING, "required": true},
|
||||
"message_id": {"name": "message_id", "type": TYPE_STRING, "required": true},
|
||||
"content": {"name": "content", "type": TYPE_STRING, "required": true}
|
||||
}
|
||||
|
||||
var channel_id : String
|
||||
var message_id : String
|
||||
var content : String
|
||||
|
||||
func _init(p_channel_id : String, p_message_id, p_content : String):
|
||||
channel_id = p_channel_id
|
||||
message_id = p_message_id
|
||||
content = p_content
|
||||
|
||||
func serialize() -> Dictionary:
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "channel_message_update"
|
||||
|
||||
func _to_string():
|
||||
return "ChannelMessageUpdate<channel_id=%s, message_id=%s, content=%s>" % [channel_id, message_id, content]
|
||||
|
||||
|
||||
# A create message for a match on the server.
|
||||
class MatchCreate extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"name": {"name": "name", "type": TYPE_STRING, "required": false},
|
||||
}
|
||||
|
||||
var name = null
|
||||
|
||||
func _init(p_name = null):
|
||||
name = p_name if p_name else null
|
||||
|
||||
func serialize():
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "match_create"
|
||||
|
||||
func _to_string():
|
||||
return "MatchCreate<name=%s>" % [name]
|
||||
|
||||
|
||||
# A join message for a match on the server.
|
||||
class MatchJoin extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"match_id": {"name": "match_id", "type": TYPE_STRING, "required": false},
|
||||
"token": {"name": "token", "type": TYPE_STRING, "required": false},
|
||||
"metadata": {"name": "metadata", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING},
|
||||
}
|
||||
|
||||
# These two are mutually exclusive and set manually by socket for now, so use null.
|
||||
var match_id = null
|
||||
var token = null
|
||||
var metadata = null
|
||||
|
||||
func _init(p_ex=null):
|
||||
super(p_ex)
|
||||
|
||||
func serialize() -> Dictionary:
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "match_join"
|
||||
|
||||
func _to_string():
|
||||
return "MatchJoin<match_id=%s, token=%s, metadata=%s>" % [match_id, token, metadata]
|
||||
|
||||
|
||||
# A leave message for a match on the server.
|
||||
class MatchLeave extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"match_id": {"name": "match_id", "type": TYPE_STRING, "required": true}
|
||||
}
|
||||
var match_id : String
|
||||
|
||||
func _init(p_match_id : String):
|
||||
match_id = p_match_id
|
||||
|
||||
func serialize() -> Dictionary:
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "match_leave"
|
||||
|
||||
func _to_string():
|
||||
return "MatchLeave<match_id=%s>" % [match_id]
|
||||
|
||||
|
||||
# Send new state to a match on the server.
|
||||
class MatchDataSend extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"match_id": {"name": "match_id", "type": TYPE_STRING, "required": true},
|
||||
"op_code": {"name": "op_code", "type": TYPE_INT, "required": true},
|
||||
"presences": {"name": "presences", "type": TYPE_ARRAY, "required": false, "content": "UserPresence"},
|
||||
"data": {"name": "data", "type": TYPE_STRING, "required": true},
|
||||
}
|
||||
|
||||
var match_id : String
|
||||
var presences = null
|
||||
var op_code : int
|
||||
var data : String
|
||||
|
||||
func _init(p_match_id : String, p_op_code : int, p_data : String, p_presences):
|
||||
match_id = p_match_id
|
||||
presences = p_presences
|
||||
op_code = p_op_code
|
||||
data = p_data
|
||||
|
||||
func serialize():
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key():
|
||||
return "match_data_send"
|
||||
|
||||
func _to_string():
|
||||
return "MatchDataSend<match_id=%s, op_code=%s, presences=%s, data=%s>" % [match_id, op_code, presences, data]
|
||||
|
||||
|
||||
# Add the user to the matchmaker pool with properties.
|
||||
class MatchmakerAdd extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"query": {"name": "query", "type": TYPE_STRING, "required": true},
|
||||
"max_count": {"name": "max_count", "type": TYPE_INT, "required": true},
|
||||
"min_count": {"name": "min_count", "type": TYPE_INT, "required": true},
|
||||
"numeric_properties": {"name": "numeric_properties", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_FLOAT},
|
||||
"string_properties": {"name": "string_properties", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING},
|
||||
"count_multiple": {"name": "count_multiple", "type": TYPE_INT, "required": false},
|
||||
}
|
||||
|
||||
var query : String = "*"
|
||||
var max_count : int = 8
|
||||
var min_count : int = 2
|
||||
var string_properties : Dictionary
|
||||
var numeric_properties : Dictionary
|
||||
var count_multiple
|
||||
|
||||
func _init(p_query : String = "*", p_min_count : int = 2, p_max_count : int = 8,
|
||||
p_string_props : Dictionary = Dictionary(), p_numeric_props : Dictionary = Dictionary(),
|
||||
p_count_multiple : int = 0):
|
||||
query = p_query
|
||||
min_count = p_min_count
|
||||
max_count = p_max_count
|
||||
string_properties = p_string_props
|
||||
numeric_properties = p_numeric_props
|
||||
count_multiple = p_count_multiple if p_count_multiple > 0 else null
|
||||
|
||||
func serialize() -> Dictionary:
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "matchmaker_add"
|
||||
|
||||
func _to_string():
|
||||
return "MatchmakerAdd<query=%s, max_count=%d, min_count=%d, numeric_properties=%s, string_properties=%s, count_multiple=%s>" % [query, max_count, min_count, numeric_properties, string_properties, count_multiple]
|
||||
|
||||
|
||||
# Remove the user from the matchmaker pool by ticket.
|
||||
class MatchmakerRemove extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"ticket": {"name": "ticket", "type": TYPE_STRING, "required": true}
|
||||
}
|
||||
|
||||
var ticket : String
|
||||
|
||||
func _init(p_ticket : String):
|
||||
ticket = p_ticket
|
||||
|
||||
func serialize():
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "matchmaker_remove"
|
||||
|
||||
func _to_string():
|
||||
return "MatchmakerRemove<ticket=%s>" % [ticket]
|
||||
|
||||
|
||||
# Follow one or more other users for status updates.
|
||||
class StatusFollow extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"user_ids": {"name": "user_ids", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING},
|
||||
"usernames": {"name": "usernames", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING},
|
||||
}
|
||||
|
||||
var user_ids := PackedStringArray()
|
||||
var usernames := PackedStringArray()
|
||||
|
||||
func _init(p_ids : PackedStringArray, p_usernames : PackedStringArray):
|
||||
user_ids = p_ids
|
||||
usernames = p_usernames
|
||||
|
||||
func serialize():
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "status_follow"
|
||||
|
||||
func _to_string():
|
||||
return "StatusFollow<user_ids=%s, usernames=%s>" % [user_ids, usernames]
|
||||
|
||||
|
||||
# Unfollow one or more users on the server.
|
||||
class StatusUnfollow extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"user_ids": {"name": "user_ids", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING},
|
||||
}
|
||||
|
||||
var user_ids := PackedStringArray()
|
||||
|
||||
func _init(p_ids : PackedStringArray):
|
||||
user_ids = p_ids
|
||||
|
||||
func serialize():
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "status_unfollow"
|
||||
|
||||
func _to_string():
|
||||
return "StatusUnfollow<user_ids=%s>" % [user_ids]
|
||||
|
||||
|
||||
# Unfollow one or more users on the server.
|
||||
class StatusUpdate extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"status": {"name": "status", "type": TYPE_STRING, "required": true},
|
||||
}
|
||||
|
||||
var status : String
|
||||
|
||||
func _init(p_status : String):
|
||||
status = p_status
|
||||
|
||||
func serialize():
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "status_update"
|
||||
|
||||
func _to_string():
|
||||
return "StatusUpdate<status=%s>" % [status]
|
||||
|
||||
# Create a party.
|
||||
class PartyCreate extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"open": {"name": "open", "type": TYPE_BOOL, "required": true},
|
||||
"max_size": {"name": "max_size", "type": TYPE_INT, "required": true},
|
||||
}
|
||||
# Whether or not the party will require join requests to be approved by the party leader.
|
||||
var open : bool
|
||||
# Maximum number of party members.
|
||||
var max_size : int
|
||||
|
||||
func _init(p_open : bool, p_max_size : int):
|
||||
open = p_open
|
||||
max_size = p_max_size
|
||||
|
||||
func serialize():
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "party_create"
|
||||
|
||||
func _to_string():
|
||||
return "PartyCreate<open=%s, max_size=%d>" % [open, max_size]
|
||||
|
||||
|
||||
# Join a party, or request to join if the party is not open.
|
||||
class PartyJoin extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
|
||||
}
|
||||
# Party ID to join.
|
||||
var party_id : String
|
||||
|
||||
func _init(p_id : String):
|
||||
party_id = p_id
|
||||
|
||||
func serialize():
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "party_join"
|
||||
|
||||
func _to_string():
|
||||
return "PartyJoin<party_id=%s>" % [party_id]
|
||||
|
||||
|
||||
# Leave a party.
|
||||
class PartyLeave extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
|
||||
}
|
||||
# Party ID to leave.
|
||||
var party_id : String
|
||||
|
||||
func _init(p_id : String):
|
||||
party_id = p_id
|
||||
|
||||
func serialize():
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "party_leave"
|
||||
|
||||
func _to_string():
|
||||
return "PartyLeave<party_id=%s>" % [party_id]
|
||||
|
||||
|
||||
# Promote a new party leader.
|
||||
class PartyPromote extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
|
||||
"presence": {"name": "presence", "type": "UserPresence", "required": true},
|
||||
}
|
||||
# Party ID to promote a new leader for.
|
||||
var party_id : String
|
||||
# The presence of an existing party member to promote as the new leader.
|
||||
var presence : NakamaRTAPI.UserPresence
|
||||
|
||||
func _init(p_id : String, p_presence : NakamaRTAPI.UserPresence):
|
||||
party_id = p_id
|
||||
presence = p_presence
|
||||
|
||||
func serialize():
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "party_promote"
|
||||
|
||||
func _to_string():
|
||||
return "PartyPromote<party_id=%s, presence=%s>" % [party_id, presence]
|
||||
|
||||
|
||||
# Accept a request to join.
|
||||
class PartyAccept extends NakamaAsyncResult:
|
||||
const _SCHEMA = {
|
||||
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
|
||||
"presence": {"name": "presence", "type": "UserPresence", "required": true},
|
||||
}
|
||||
# Party ID to accept a join request for.
|
||||
var party_id : String
|
||||
# The presence to accept as a party member.
|
||||
var presence : NakamaRTAPI.UserPresence
|
||||
|
||||
func _init(p_id : String, p_presence : NakamaRTAPI.UserPresence):
|
||||
party_id = p_id
|
||||
presence = p_presence
|
||||
|
||||
func serialize():
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "party_accept"
|
||||
|
||||
func _to_string():
|
||||
return "PartyAccept<party_id=%s, presence=%s>" % [party_id, presence]
|
||||
|
||||
|
||||
# Kick a party member, or decline a request to join.
|
||||
class PartyRemove extends NakamaAsyncResult:
|
||||
const _SCHEMA = {
|
||||
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
|
||||
"presence": {"name": "presence", "type": "UserPresence", "required": true},
|
||||
}
|
||||
# Party ID to remove/reject from.
|
||||
var party_id : String
|
||||
# The presence to remove or reject.
|
||||
var presence : NakamaRTAPI.UserPresence
|
||||
|
||||
func _init(p_id : String, p_presence : NakamaRTAPI.UserPresence):
|
||||
party_id = p_id
|
||||
presence = p_presence
|
||||
|
||||
func serialize():
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "party_remove"
|
||||
|
||||
func _to_string():
|
||||
return "PartyRemove<party_id=%s, presence=%s>" % [party_id, presence]
|
||||
|
||||
|
||||
# Request a list of pending join requests for a party.
|
||||
class PartyJoinRequestList extends NakamaAsyncResult:
|
||||
const _SCHEMA = {
|
||||
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
|
||||
}
|
||||
# Party ID to get a list of join requests for.
|
||||
var party_id : String
|
||||
|
||||
func _init(p_id : String):
|
||||
party_id = p_id
|
||||
|
||||
func serialize():
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "party_join_request_list"
|
||||
|
||||
func _to_string():
|
||||
return "PartyJoinRequestList<party_id=%s>" % [party_id]
|
||||
|
||||
|
||||
# Begin matchmaking as a party.
|
||||
class PartyMatchmakerAdd extends NakamaAsyncResult:
|
||||
|
||||
const _SCHEMA = {
|
||||
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
|
||||
"min_count": {"name": "min_count", "type": TYPE_INT, "required": true},
|
||||
"max_count": {"name": "max_count", "type": TYPE_INT, "required": true},
|
||||
"query": {"name": "query", "type": TYPE_STRING, "required": false},
|
||||
"string_properties": {"name": "string_properties", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_STRING},
|
||||
"numeric_properties": {"name": "numeric_properties", "type": TYPE_DICTIONARY, "required": false, "content": TYPE_FLOAT},
|
||||
}
|
||||
|
||||
# Party ID.
|
||||
var party_id : String
|
||||
# Minimum total user count to match together.
|
||||
var min_count : int
|
||||
# Maximum total user count to match together.
|
||||
var max_count : int
|
||||
# Filter query used to identify suitable users.
|
||||
var query : String
|
||||
# String properties.
|
||||
var string_properties : Dictionary
|
||||
# Numeric properties.
|
||||
var numeric_properties : Dictionary
|
||||
# Optional multiple of the count that must be satisfied.
|
||||
var count_multiple
|
||||
|
||||
func _init(p_id : String, p_min_count : int, p_max_count : int, p_query : String, p_string_properties = null, p_numeric_properties = null, p_count_multiple = null):
|
||||
party_id = p_id
|
||||
min_count = p_min_count
|
||||
max_count = p_max_count
|
||||
query = p_query
|
||||
string_properties = p_string_properties
|
||||
numeric_properties = p_numeric_properties
|
||||
count_multiple = p_count_multiple
|
||||
|
||||
func serialize():
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "party_matchmaker_add"
|
||||
|
||||
func _to_string():
|
||||
return "PartyMatchmakerAdd<party_id=%s, min_count=%d, max_count=%d, query=%s, string_properties=%s, numeric_properties=%s, count_multiple=%s>" % [party_id, min_count, max_count, query, string_properties, numeric_properties, count_multiple]
|
||||
|
||||
|
||||
# Cancel a party matchmaking process using a ticket.
|
||||
class PartyMatchmakerRemove extends NakamaAsyncResult:
|
||||
const _SCHEMA = {
|
||||
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
|
||||
"ticket": {"name": "ticket", "type": TYPE_STRING, "required": true},
|
||||
}
|
||||
# Party ID.
|
||||
var party_id : String
|
||||
# The ticket to cancel.
|
||||
var ticket : String
|
||||
|
||||
func _init(p_id : String, p_ticket : String):
|
||||
party_id = p_id
|
||||
ticket = p_ticket
|
||||
|
||||
func serialize():
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "party_matchmaker_remove"
|
||||
|
||||
func _to_string():
|
||||
return "PartyMatchmakerRemove<party_id=%s, ticket=%s>" % [party_id, ticket]
|
||||
|
||||
|
||||
# Send data to a party.
|
||||
class PartyDataSend extends NakamaAsyncResult:
|
||||
const _SCHEMA = {
|
||||
"party_id": {"name": "party_id", "type": TYPE_STRING, "required": true},
|
||||
"op_code": {"name": "op_code", "type": TYPE_INT, "required": true},
|
||||
"data": {"name": "data", "type": TYPE_STRING, "required": false}
|
||||
}
|
||||
# Party ID to send to.
|
||||
var party_id : String
|
||||
# Op code value.
|
||||
var op_code : int
|
||||
# Data payload, if any.
|
||||
var data = null
|
||||
|
||||
func _init(p_id : String, p_op_code : int, p_data = null):
|
||||
party_id = p_id
|
||||
op_code = p_op_code
|
||||
data = p_data
|
||||
|
||||
func serialize():
|
||||
return NakamaSerializer.serialize(self)
|
||||
|
||||
func get_msg_key() -> String:
|
||||
return "party_data_send"
|
||||
|
||||
func _to_string():
|
||||
return "PartyDataSend<party_id=%s, op_code=%d, data=%s>" % [party_id, op_code, data]
|
||||
@@ -0,0 +1 @@
|
||||
uid://bcqg066lj70kf
|
||||
157
addons/nakama/addons/com.heroiclabs.nakama/api/NakamaSession.gd
Normal file
157
addons/nakama/addons/com.heroiclabs.nakama/api/NakamaSession.gd
Normal 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 {}
|
||||
@@ -0,0 +1 @@
|
||||
uid://ck2yqcf1sykj
|
||||
@@ -0,0 +1,34 @@
|
||||
extends RefCounted
|
||||
class_name NakamaStorageObjectId
|
||||
|
||||
# The collection which stores the object.
|
||||
var collection : String
|
||||
|
||||
# The key of the object within the collection.
|
||||
var key : String
|
||||
|
||||
# The user owner of the object.
|
||||
var user_id : String
|
||||
|
||||
# The version hash of the object.
|
||||
var version : String
|
||||
|
||||
func _init(p_collection, p_key, p_user_id = "", p_version = ""):
|
||||
collection = p_collection
|
||||
key = p_key
|
||||
user_id = p_user_id
|
||||
version = p_version
|
||||
|
||||
func as_delete():
|
||||
return NakamaAPI.ApiDeleteStorageObjectId.create(NakamaAPI, {
|
||||
"collection": collection,
|
||||
"key": key,
|
||||
"version": version
|
||||
})
|
||||
|
||||
func as_read():
|
||||
return NakamaAPI.ApiReadStorageObjectId.create(NakamaAPI, {
|
||||
"collection": collection,
|
||||
"key": key,
|
||||
"user_id": user_id
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
uid://bvpfysvkyj0nr
|
||||
@@ -0,0 +1,28 @@
|
||||
extends RefCounted
|
||||
class_name NakamaWriteStorageObject
|
||||
|
||||
var collection : String
|
||||
var key : String
|
||||
var permission_read : int = 0
|
||||
var permission_write : int = 0
|
||||
var value : String
|
||||
var version : String
|
||||
|
||||
func _init(p_collection : String, p_key : String, p_permission_read : int,
|
||||
p_permission_write : int, p_value : String, p_version : String):
|
||||
collection = p_collection
|
||||
key = p_key
|
||||
permission_read = p_permission_read
|
||||
permission_write = p_permission_write
|
||||
value = p_value
|
||||
version = p_version
|
||||
|
||||
func as_write():
|
||||
return NakamaAPI.ApiWriteStorageObject.create(NakamaAPI, {
|
||||
"collection": collection,
|
||||
"key": key,
|
||||
"permission_read": permission_read,
|
||||
"permission_write": permission_write,
|
||||
"value": value,
|
||||
"version": version
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
uid://b1ma2i2ujotho
|
||||
1084
addons/nakama/addons/com.heroiclabs.nakama/client/NakamaClient.gd
Normal file
1084
addons/nakama/addons/com.heroiclabs.nakama/client/NakamaClient.gd
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
uid://4uxe86ur52yc
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://ctli782guhogc
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2022 The Nakama Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using Godot;
|
||||
|
||||
namespace Nakama {
|
||||
|
||||
/// <summary>
|
||||
/// A logger which prints to the Godot console.
|
||||
/// </summary>
|
||||
public class GodotLogger : ILogger {
|
||||
|
||||
/// <summary>
|
||||
/// The log level.
|
||||
/// </summary>
|
||||
public enum LogLevel {
|
||||
NONE,
|
||||
ERROR,
|
||||
WARNING,
|
||||
INFO,
|
||||
DEBUG,
|
||||
}
|
||||
|
||||
private string module;
|
||||
private LogLevel level;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a GodotLogger.
|
||||
/// </summary>
|
||||
/// <param name="p_module">The label to use for log entries.</param>
|
||||
/// <param name="p_level">The log level (or lower) to print to the console.</param>
|
||||
public GodotLogger(string p_module = "Nakama", LogLevel p_level = LogLevel.ERROR) {
|
||||
module = p_module;
|
||||
level = p_level;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ILogger"/>
|
||||
public void ErrorFormat(string format, params object[] args) {
|
||||
if (level >= LogLevel.ERROR) {
|
||||
GD.PrintErr("=== " + module + " : ERROR === " + String.Format(format, args));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ILogger"/>
|
||||
public void WarnFormat(string format, params object[] args) {
|
||||
if (level >= LogLevel.WARNING) {
|
||||
GD.Print("=== " + module + " : WARN === " + String.Format(format, args));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ILogger"/>
|
||||
public void InfoFormat(string format, params object[] args) {
|
||||
if (level >= LogLevel.INFO) {
|
||||
GD.Print("=== " + module + " : INFO === " + String.Format(format, args));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ILogger"/>
|
||||
public void DebugFormat(string format, params object[] args) {
|
||||
if (level >= LogLevel.DEBUG) {
|
||||
GD.Print("=== " + module + " : DEBUG === " + String.Format(format, args));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
// Copyright 2022 The Nakama Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Godot;
|
||||
|
||||
namespace Nakama
|
||||
{
|
||||
/// <summary>
|
||||
/// An exception that is thrown when the WebSocket is unable to connect.
|
||||
/// </summary>
|
||||
public class GodotWebSocketConnectionException : Exception {
|
||||
public GodotWebSocketConnectionException(string message = "WebSocket unable to connect")
|
||||
: base(message) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An exception that is thrown when the WebSocket is unable to send.
|
||||
/// </summary>
|
||||
public class GodotWebSocketSendException : Exception {
|
||||
public GodotWebSocketSendException() : base("Unable to send over WebSocket") { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A socket adapter which uses Godot's WebSocketPeer.
|
||||
/// </summary>
|
||||
public partial class GodotWebSocketAdapter : Node, ISocketAdapter
|
||||
{
|
||||
/// <inheritdoc cref="ISocketAdapter.Connected"/>
|
||||
public event Action Connected;
|
||||
|
||||
/// <inheritdoc cref="ISocketAdapter.Closed"/>
|
||||
public event Action Closed;
|
||||
|
||||
/// <inheritdoc cref="ISocketAdapter.ReceivedError"/>
|
||||
public event Action<Exception> ReceivedError;
|
||||
|
||||
/// <inheritdoc cref="ISocketAdapter.Received"/>
|
||||
public event Action<ArraySegment<byte>> Received;
|
||||
|
||||
/// <inheritdoc cref="ISocketAdapter.IsConnected"/>
|
||||
public new bool IsConnected
|
||||
{
|
||||
get
|
||||
{
|
||||
return ws.GetReadyState() == WebSocketPeer.State.Open;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ISocketAdapter.IsConnecting"/>
|
||||
public bool IsConnecting
|
||||
{
|
||||
get
|
||||
{
|
||||
return ws.GetReadyState() == WebSocketPeer.State.Connecting;
|
||||
}
|
||||
}
|
||||
|
||||
private WebSocketPeer ws;
|
||||
private WebSocketPeer.State wsLastState = WebSocketPeer.State.Closed;
|
||||
|
||||
private TaskCompletionSource<bool> connectionSource;
|
||||
private TaskCompletionSource<bool> closeSource;
|
||||
private int connectionTimeout;
|
||||
private double connectionStart;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a GodotWebSocketAdapter.
|
||||
/// </summary>
|
||||
public GodotWebSocketAdapter()
|
||||
{
|
||||
ws = new WebSocketPeer();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ISocketAdaptor.CloseAsync"/>
|
||||
public Task CloseAsync()
|
||||
{
|
||||
if (closeSource == null)
|
||||
{
|
||||
closeSource = new TaskCompletionSource<bool>();
|
||||
}
|
||||
ws.Close();
|
||||
return closeSource.Task;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ISocketAdaptor.ConnectAsync"/>
|
||||
public Task ConnectAsync(Uri uri, int timeout)
|
||||
{
|
||||
if (connectionSource != null)
|
||||
{
|
||||
connectionSource.SetException(new GodotWebSocketConnectionException("Connection attempt aborted due to new connection attempt"));
|
||||
connectionSource = null;
|
||||
}
|
||||
|
||||
if (ws.GetReadyState() != WebSocketPeer.State.Closed)
|
||||
{
|
||||
return Task.FromException(new GodotWebSocketConnectionException("Cannot connect until current socket is closed"));
|
||||
}
|
||||
|
||||
connectionTimeout = timeout;
|
||||
connectionStart = Time.GetUnixTimeFromSystem();
|
||||
|
||||
connectionSource = new TaskCompletionSource<bool>();
|
||||
|
||||
var err = ws.ConnectToUrl(uri.ToString());
|
||||
if (err != Error.Ok)
|
||||
{
|
||||
return Task.FromException(new GodotWebSocketConnectionException(String.Format("Error connecting: {0}", Enum.GetName(typeof(Error), err))));
|
||||
}
|
||||
|
||||
wsLastState = WebSocketPeer.State.Closed;
|
||||
|
||||
return connectionSource.Task;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ISocketAdaptor.SendAsync"/>
|
||||
public Task SendAsync(ArraySegment<byte> buffer, bool reliable = true, CancellationToken canceller = default)
|
||||
{
|
||||
byte[] temp;
|
||||
|
||||
if (buffer.Offset != 0 || buffer.Count != buffer.Array.Length)
|
||||
{
|
||||
temp = new byte[buffer.Count];
|
||||
Array.Copy(buffer.Array, buffer.Offset, temp, 0, buffer.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
temp = buffer.Array;
|
||||
}
|
||||
|
||||
var err = ws.Send(temp, WebSocketPeer.WriteMode.Text);
|
||||
if (err == Error.Ok)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return Task.FromException(new GodotWebSocketSendException());
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (ws.GetReadyState() != WebSocketPeer.State.Closed)
|
||||
{
|
||||
ws.Poll();
|
||||
}
|
||||
|
||||
var state = ws.GetReadyState();
|
||||
if (wsLastState != state)
|
||||
{
|
||||
wsLastState = state;
|
||||
|
||||
if (state == WebSocketPeer.State.Open)
|
||||
{
|
||||
Connected?.Invoke();
|
||||
connectionSource.SetResult(true);
|
||||
connectionSource = null;
|
||||
}
|
||||
else if (state == WebSocketPeer.State.Closed)
|
||||
{
|
||||
if (connectionSource != null)
|
||||
{
|
||||
Exception e = new GodotWebSocketConnectionException("Failed to connect");
|
||||
ReceivedError?.Invoke(e);
|
||||
connectionSource.SetException(e);
|
||||
connectionSource = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
Closed?.Invoke();
|
||||
}
|
||||
|
||||
if (closeSource != null)
|
||||
{
|
||||
closeSource.SetResult(true);
|
||||
closeSource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ws.GetReadyState() == WebSocketPeer.State.Connecting) {
|
||||
if (connectionStart + (double)connectionTimeout < Time.GetUnixTimeFromSystem())
|
||||
{
|
||||
ws.Close();
|
||||
|
||||
Exception e = new GodotWebSocketConnectionException("Connection timed out");
|
||||
ReceivedError?.Invoke(e);
|
||||
connectionSource.SetException(e);
|
||||
connectionSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
while (ws.GetReadyState() == WebSocketPeer.State.Open && ws.GetAvailablePacketCount() > 0)
|
||||
{
|
||||
Received?.Invoke(new ArraySegment<byte>(ws.GetPacket()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
extends RefCounted
|
||||
|
||||
# A socket to interact with Nakama server.
|
||||
class_name NakamaSocket
|
||||
|
||||
const ChannelType = NakamaRTMessage.ChannelJoin.ChannelType
|
||||
|
||||
# Emitted when a socket is closed.
|
||||
signal closed()
|
||||
|
||||
# Emitted when a socket is connected.
|
||||
signal connected()
|
||||
|
||||
# Emitted when an error occurs while connecting.
|
||||
signal connection_error(p_error)
|
||||
|
||||
# Emitted when a chat channel message is received
|
||||
signal received_channel_message(p_channel_message) # ApiChannelMessage
|
||||
|
||||
# Emitted when receiving a presence change for joins and leaves with users in a chat channel.
|
||||
signal received_channel_presence(p_channel_presence) # ChannelPresenceEvent
|
||||
|
||||
# Emitted when an error is received from the server.
|
||||
signal received_error(p_error) # Error
|
||||
|
||||
# Emitted when receiving a matchmaker matched message.
|
||||
signal received_matchmaker_matched(p_matchmaker_matched) # MatchmakerMatched
|
||||
|
||||
# Emitted when receiving a message from a multiplayer match.
|
||||
signal received_match_state(p_match_state) # MatchData
|
||||
|
||||
# Emitted when receiving a presence change for joins and leaves of users in a multiplayer match.
|
||||
signal received_match_presence(p_match_presence_event) # MatchPresenceEvent
|
||||
|
||||
# Emitted when receiving a notification for the current user.
|
||||
signal received_notification(p_api_notification) # ApiNotification
|
||||
|
||||
# Emitted when receiving a presence change for when a user updated their online status.
|
||||
signal received_status_presence(p_status_presence_event) # StatusPresenceEvent
|
||||
|
||||
# Emitted when receiving a presence change for joins and leaves on a realtime stream.
|
||||
signal received_stream_presence(p_stream_presence_event) # StreamPresenceEvent
|
||||
|
||||
# Emitted when receiving a message from a realtime stream.
|
||||
signal received_stream_state(p_stream_state) # StreamState
|
||||
|
||||
# Received a party event. This will occur when the current user's invitation request is accepted
|
||||
# the party leader of a closed party.
|
||||
signal received_party(p_party) # Party
|
||||
|
||||
# Received a party close event.
|
||||
signal received_party_close(p_party_close) # PartyClose
|
||||
|
||||
# Received custom party data.
|
||||
signal received_party_data(p_party_data) # PartyData
|
||||
|
||||
# Received a request to join the party.
|
||||
signal received_party_join_request(p_party_join_request) # PartyJoinRequest
|
||||
|
||||
# Received a change in the party leader.
|
||||
signal received_party_leader(p_party_leader) # PartyLeader
|
||||
|
||||
# Received a new matchmaker ticket for the party.
|
||||
signal received_party_matchmaker_ticket(p_party_matchmaker_ticket) # PartyMatchmakerTicket
|
||||
|
||||
# Received a new presence event in the party.
|
||||
signal received_party_presence(p_party_presence_event) # PartyPresenceEvent
|
||||
|
||||
var _adapter : NakamaSocketAdapter
|
||||
var _free_adapter : bool = false
|
||||
var _weak_ref : WeakRef
|
||||
var _base_uri : String
|
||||
var _requests : Dictionary
|
||||
var _last_id : int = 1
|
||||
var _conn = null
|
||||
var logger : NakamaLogger = null
|
||||
|
||||
class AsyncConnection:
|
||||
signal completed(result)
|
||||
|
||||
func resume(result) -> void:
|
||||
emit_signal("completed", result)
|
||||
|
||||
class AsyncRequest:
|
||||
var id : String
|
||||
var type
|
||||
var ns
|
||||
var result_key : String
|
||||
|
||||
signal completed(result)
|
||||
|
||||
func _init(p_id : String, p_type, p_ns, p_result_key = null):
|
||||
id = p_id
|
||||
type = p_type
|
||||
ns = p_ns
|
||||
|
||||
if type != NakamaAsyncResult:
|
||||
# Specifically defined key, or default for object.
|
||||
result_key = p_result_key if p_result_key != null else type.get_result_key()
|
||||
|
||||
func resume(data, logger = null) -> void:
|
||||
var result = _parse_result(data, logger)
|
||||
emit_signal("completed", result)
|
||||
|
||||
func _parse_result(data, logger):
|
||||
# We got an exception, maybe the task was cancelled?
|
||||
if data is NakamaException:
|
||||
return type.new(data as NakamaException)
|
||||
|
||||
# Error from server
|
||||
if data.has("error"):
|
||||
var err = data["error"]
|
||||
var code = -1
|
||||
var msg = str(err)
|
||||
if typeof(err) == TYPE_DICTIONARY:
|
||||
msg = err.get("message", "")
|
||||
code = err.get("code", -1)
|
||||
if logger:
|
||||
logger.warning("Error response from server: %s" % err)
|
||||
return type.new(NakamaException.new(msg, code))
|
||||
# Simple ack response
|
||||
elif type == NakamaAsyncResult:
|
||||
return NakamaAsyncResult.new()
|
||||
# Missing expected result key
|
||||
elif not data.has(result_key):
|
||||
if logger:
|
||||
logger.warning("Missing expected result key: %s" % result_key)
|
||||
return type.new(NakamaException.new("Missing expected result key: %s" % result_key))
|
||||
# All good, proceed with parsing
|
||||
else:
|
||||
return type.create(ns, data.get(result_key))
|
||||
|
||||
func _resume_conn(p_err : int):
|
||||
if _conn:
|
||||
if p_err: # Exception
|
||||
logger.warning("Connection error: %d" % p_err)
|
||||
_conn.resume(NakamaAsyncResult.new(NakamaException.new()))
|
||||
else:
|
||||
logger.info("Connected!")
|
||||
_conn.resume(NakamaAsyncResult.new())
|
||||
_conn = null
|
||||
|
||||
func _init(p_adapter : NakamaSocketAdapter,
|
||||
p_host : String,
|
||||
p_port : int,
|
||||
p_scheme : String,
|
||||
p_free_adapter : bool = false):
|
||||
logger = p_adapter.logger
|
||||
_adapter = p_adapter
|
||||
_weak_ref = weakref(_adapter)
|
||||
var port = ""
|
||||
if (p_scheme == "ws" and p_port != 80) or (p_scheme == "wss" and p_port != 443):
|
||||
port = ":%d" % p_port
|
||||
_base_uri = "%s://%s%s" % [p_scheme, p_host, port]
|
||||
_free_adapter = p_free_adapter
|
||||
_adapter.closed.connect(self._closed)
|
||||
_adapter.connected.connect(self._connected)
|
||||
_adapter.received_error.connect(self._connection_error)
|
||||
_adapter.received.connect(self._received)
|
||||
|
||||
func _notification(what):
|
||||
if what == NOTIFICATION_PREDELETE:
|
||||
# Is this a bug? Why can't I call a function? self is null...
|
||||
# _clear_responses()
|
||||
# _resume_conn(ERR_FILE_EOF)
|
||||
var keys = _requests.keys()
|
||||
for k in keys:
|
||||
_requests[k].resume(NakamaException.new("Cancelled!"))
|
||||
if _conn != null:
|
||||
_conn.resume(ERR_FILE_EOF)
|
||||
_conn = null
|
||||
if _weak_ref.get_ref() == null:
|
||||
return
|
||||
_adapter.close()
|
||||
if _free_adapter:
|
||||
_adapter.queue_free()
|
||||
|
||||
func _closed(p_error = null):
|
||||
emit_signal("closed")
|
||||
_resume_conn(ERR_CANT_CONNECT)
|
||||
_clear_requests()
|
||||
|
||||
func _connection_error(p_error):
|
||||
emit_signal("connection_error", p_error)
|
||||
_resume_conn(p_error)
|
||||
_clear_requests()
|
||||
|
||||
func _connected():
|
||||
emit_signal("connected")
|
||||
_resume_conn(OK)
|
||||
|
||||
func _received(p_bytes : PackedByteArray):
|
||||
var json = JSON.new()
|
||||
var json_str = p_bytes.get_string_from_utf8()
|
||||
var json_error := json.parse(json_str)
|
||||
if json_error != OK or typeof(json.get_data()) != TYPE_DICTIONARY:
|
||||
logger.error("Unable to parse response: %s" % json_str)
|
||||
return
|
||||
var dict : Dictionary = json.get_data()
|
||||
var cid = dict.get("cid")
|
||||
if cid:
|
||||
if _requests.has(cid):
|
||||
_resume_request(cid, dict)
|
||||
else:
|
||||
logger.error("Invalid call id received %s" % dict)
|
||||
else:
|
||||
if dict.has("error"):
|
||||
var res = NakamaRTAPI.Error.create(NakamaRTAPI, dict["error"])
|
||||
emit_signal("received_error", res)
|
||||
elif dict.has("channel_message"):
|
||||
var res = NakamaAPI.ApiChannelMessage.create(NakamaAPI, dict["channel_message"])
|
||||
emit_signal("received_channel_message", res)
|
||||
elif dict.has("channel_presence_event"):
|
||||
var res = NakamaRTAPI.ChannelPresenceEvent.create(NakamaRTAPI, dict["channel_presence_event"])
|
||||
emit_signal("received_channel_presence", res)
|
||||
elif dict.has("match_data"):
|
||||
var res = NakamaRTAPI.MatchData.create(NakamaRTAPI, dict["match_data"])
|
||||
emit_signal("received_match_state", res)
|
||||
elif dict.has("match_presence_event"):
|
||||
var res = NakamaRTAPI.MatchPresenceEvent.create(NakamaRTAPI, dict["match_presence_event"])
|
||||
emit_signal("received_match_presence", res)
|
||||
elif dict.has("matchmaker_matched"):
|
||||
var res = NakamaRTAPI.MatchmakerMatched.create(NakamaRTAPI, dict["matchmaker_matched"])
|
||||
emit_signal("received_matchmaker_matched", res)
|
||||
elif dict.has("notifications"):
|
||||
var res = NakamaAPI.ApiNotificationList.create(NakamaAPI, dict["notifications"])
|
||||
for n in res.notifications:
|
||||
emit_signal("received_notification", n)
|
||||
elif dict.has("status_presence_event"):
|
||||
var res = NakamaRTAPI.StatusPresenceEvent.create(NakamaRTAPI, dict["status_presence_event"])
|
||||
emit_signal("received_status_presence", res)
|
||||
elif dict.has("stream_presence_event"):
|
||||
var res = NakamaRTAPI.StreamPresenceEvent.create(NakamaRTAPI, dict["stream_presence_event"])
|
||||
emit_signal("received_stream_presence", res)
|
||||
elif dict.has("stream_data"):
|
||||
var res = NakamaRTAPI.StreamData.create(NakamaRTAPI, dict["stream_data"])
|
||||
emit_signal("received_stream_state", res)
|
||||
elif dict.has("party"):
|
||||
var res = NakamaRTAPI.Party.create(NakamaRTAPI, dict["party"])
|
||||
emit_signal("received_party", res)
|
||||
elif dict.has("party_close"):
|
||||
var res = NakamaRTAPI.PartyClose.create(NakamaRTAPI, dict["party_close"])
|
||||
emit_signal("received_party_close", res)
|
||||
elif dict.has("party_data"):
|
||||
var res = NakamaRTAPI.PartyData.create(NakamaRTAPI, dict["party_data"])
|
||||
emit_signal("received_party_data", res)
|
||||
elif dict.has("party_join_request"):
|
||||
var res = NakamaRTAPI.PartyJoinRequest.create(NakamaRTAPI, dict["party_join_request"])
|
||||
emit_signal("received_party_join_request", res)
|
||||
elif dict.has("party_leader"):
|
||||
var res = NakamaRTAPI.PartyLeader.create(NakamaRTAPI, dict["party_leader"])
|
||||
emit_signal("received_party_leader", res)
|
||||
elif dict.has("party_matchmaker_ticket"):
|
||||
var res = NakamaRTAPI.PartyMatchmakerTicket.create(NakamaRTAPI, dict["party_matchmaker_ticket"])
|
||||
emit_signal("received_party_matchmaker_ticket", res)
|
||||
elif dict.has("party_presence_event"):
|
||||
var res = NakamaRTAPI.PartyPresenceEvent.create(NakamaRTAPI, dict["party_presence_event"])
|
||||
emit_signal("received_party_presence", res)
|
||||
else:
|
||||
logger.warning("Unhandled response: %s" % dict)
|
||||
|
||||
func _resume_request(p_id : String, p_data):
|
||||
if _requests.has(p_id):
|
||||
logger.debug("Resuming request: %s: %s" % [p_id, p_data])
|
||||
_requests[p_id].resume(p_data, logger)
|
||||
_requests.erase(p_id)
|
||||
else:
|
||||
logger.warning("Trying to resume missing request: %s: %s" % [p_id, p_data])
|
||||
|
||||
func _cancel_request(p_id : String):
|
||||
logger.debug("Cancelling request: %s" % [p_id])
|
||||
_resume_request(p_id, NakamaException.new("Request cancelled."))
|
||||
|
||||
func _clear_requests():
|
||||
var ids = _requests.keys()
|
||||
for id in ids:
|
||||
_cancel_request(id)
|
||||
|
||||
func _send_async(p_message, p_parse_type = NakamaAsyncResult, p_ns = NakamaRTAPI, p_msg_key = null, p_result_key = null) -> AsyncRequest:
|
||||
logger.debug("Sending async request: %s" % p_message)
|
||||
# For messages coming from the API which does not have a key defined, so we can override it
|
||||
var msg = p_msg_key
|
||||
# For regular RT messages
|
||||
if msg == null:
|
||||
msg = p_message.get_msg_key()
|
||||
var id = str(_last_id)
|
||||
_last_id += 1
|
||||
|
||||
_requests[id] = AsyncRequest.new(id, p_parse_type, p_ns, p_result_key)
|
||||
|
||||
var json := JSON.stringify({
|
||||
"cid": id,
|
||||
msg: p_message.serialize()
|
||||
})
|
||||
var err = _adapter.send(json.to_utf8_buffer())
|
||||
if err != OK:
|
||||
call_deferred("_cancel_request", id)
|
||||
return _requests[id]
|
||||
|
||||
# If the socket is connected.
|
||||
func is_connected_to_host():
|
||||
return _adapter.is_connected_to_host()
|
||||
|
||||
# If the socket is connecting.
|
||||
func is_connecting_to_host():
|
||||
return _adapter.is_connecting_to_host()
|
||||
|
||||
# Close the socket connection to the server.
|
||||
func close():
|
||||
_adapter.close()
|
||||
|
||||
# Connect to the server.
|
||||
# @param p_session - The session of the user.
|
||||
# @param p_appear_online - If the user who appear online to other users.
|
||||
# @param p_connect_timeout - The time allowed for the socket connection to be established.
|
||||
# Returns a task to represent the asynchronous operation.
|
||||
func connect_async(p_session : NakamaSession, p_appear_online : bool = false, p_connect_timeout : int = 3):
|
||||
var uri = "%s/ws?lang=en&status=%s&token=%s" % [_base_uri, str(p_appear_online).to_lower(), p_session.token]
|
||||
logger.debug("Connecting to host: %s" % uri)
|
||||
_conn = AsyncConnection.new()
|
||||
_adapter.connect_to_host(uri, p_connect_timeout)
|
||||
return await _conn.completed
|
||||
|
||||
# Join the matchmaker pool and search for opponents on the server.
|
||||
# @param p_query - The matchmaker query to search for opponents.
|
||||
# @param p_min_count - The minimum number of players to compete against in a match.
|
||||
# @param p_max_count - The maximum number of players to compete against in a match.
|
||||
# @param p_string_properties - A set of key/value properties to provide to searches.
|
||||
# @param p_numeric_properties - A set of key/value numeric properties to provide to searches.
|
||||
# @param p_count_multiple - Optional multiple of the count that must be satisfied.
|
||||
# Returns a task which resolves to a matchmaker ticket object.
|
||||
func add_matchmaker_async(p_query : String = "*", p_min_count : int = 2, p_max_count : int = 8,
|
||||
p_string_props : Dictionary = {}, p_numeric_props : Dictionary = {},
|
||||
p_count_multiple : int = 0) -> NakamaRTAPI.MatchmakerTicket:
|
||||
return await _send_async(
|
||||
NakamaRTMessage.MatchmakerAdd.new(p_query, p_min_count, p_max_count, p_string_props, p_numeric_props, p_count_multiple),
|
||||
NakamaRTAPI.MatchmakerTicket
|
||||
).completed
|
||||
|
||||
# Create a multiplayer match on the server.
|
||||
# @param p_name - Optional name to use when creating the match.
|
||||
# Returns a task to represent the asynchronous operation.
|
||||
func create_match_async(p_name : String = ''):
|
||||
return await _send_async(NakamaRTMessage.MatchCreate.new(p_name), NakamaRTAPI.Match).completed
|
||||
|
||||
# Subscribe to one or more users for their status updates.
|
||||
# @param p_user_ids - The IDs of users.
|
||||
# @param p_usernames - The usernames of the users.
|
||||
# Returns a task which resolves to the current statuses for the users.
|
||||
func follow_users_async(p_ids : PackedStringArray, p_usernames : PackedStringArray) -> NakamaRTAPI.Status:
|
||||
return await _send_async(NakamaRTMessage.StatusFollow.new(p_ids, p_usernames), NakamaRTAPI.Status).completed
|
||||
|
||||
# Join a chat channel on the server.
|
||||
# @param p_target - The target channel to join.
|
||||
# @param p_type - The type of channel to join.
|
||||
# @param p_persistence - If chat messages should be stored.
|
||||
# @param p_hidden - If the current user should be hidden on the channel.
|
||||
# Returns a task which resolves to a chat channel object.
|
||||
func join_chat_async(p_target : String, p_type : int, p_persistence : bool = false, p_hidden : bool = false) -> NakamaRTAPI.Channel:
|
||||
return await _send_async(
|
||||
NakamaRTMessage.ChannelJoin.new(p_target, p_type, p_persistence, p_hidden),
|
||||
NakamaRTAPI.Channel
|
||||
).completed
|
||||
|
||||
# Join a multiplayer match with the matchmaker matched object.
|
||||
# @param p_matched - A matchmaker matched object.
|
||||
# Returns a task which resolves to a multiplayer match.
|
||||
func join_matched_async(p_matched):
|
||||
var msg := NakamaRTMessage.MatchJoin.new()
|
||||
if p_matched.match_id:
|
||||
msg.match_id = p_matched.match_id
|
||||
else:
|
||||
msg.token = p_matched.token
|
||||
return await _send_async(msg, NakamaRTAPI.Match).completed
|
||||
|
||||
# Join a multiplayer match by ID.
|
||||
# @param p_match_id - The ID of the match to attempt to join.
|
||||
# @param p_metadata - An optional set of key-value metadata pairs to be passed to the match handler.
|
||||
# Returns a task which resolves to a multiplayer match.
|
||||
func join_match_async(p_match_id : String, p_metadata = null):
|
||||
var msg := NakamaRTMessage.MatchJoin.new()
|
||||
msg.match_id = p_match_id
|
||||
msg.metadata = p_metadata
|
||||
return await _send_async(msg, NakamaRTAPI.Match).completed
|
||||
|
||||
# Leave a chat channel on the server.
|
||||
## @param p_channel_id - The ID of the chat channel to leave.
|
||||
# Returns a task which represents the asynchronous operation.
|
||||
func leave_chat_async(p_channel_id : String) -> NakamaAsyncResult:
|
||||
return await _send_async(NakamaRTMessage.ChannelLeave.new(p_channel_id)).completed
|
||||
|
||||
# Leave a multiplayer match on the server.
|
||||
# @param p_match_id - The multiplayer match to leave.
|
||||
# Returns a task which represents the asynchronous operation.
|
||||
func leave_match_async(p_match_id : String) -> NakamaAsyncResult:
|
||||
return await _send_async(NakamaRTMessage.MatchLeave.new(p_match_id)).completed
|
||||
|
||||
# Remove a chat message from a chat channel on the server.
|
||||
# @param p_channel - The chat channel with the message to remove.
|
||||
# @param p_message_id - The ID of the chat message to remove.
|
||||
# Returns a task which resolves to an acknowledgement of the removed message.
|
||||
func remove_chat_message_async(p_channel_id : String, p_message_id : String):
|
||||
return await _send_async(
|
||||
NakamaRTMessage.ChannelMessageRemove.new(p_channel_id, p_message_id),
|
||||
NakamaRTAPI.ChannelMessageAck
|
||||
).completed
|
||||
|
||||
# Leave the matchmaker pool with the ticket.
|
||||
# @param p_ticket - The ticket returned by the matchmaker on join.
|
||||
# Returns a task which represents the asynchronous operation.
|
||||
func remove_matchmaker_async(p_ticket : String) -> NakamaAsyncResult:
|
||||
return await _send_async(NakamaRTMessage.MatchmakerRemove.new(p_ticket)).completed
|
||||
|
||||
# Execute an RPC function to the server.
|
||||
# @param p_func_id - The ID of the function to execute.
|
||||
# @param p_payload - An (optional) String payload to send to the server.
|
||||
# Returns a task which resolves to the RPC function response object.
|
||||
func rpc_async(p_func_id : String, p_payload = null) -> NakamaAPI.ApiRpc:
|
||||
var payload = p_payload
|
||||
match typeof(p_payload):
|
||||
TYPE_NIL, TYPE_STRING:
|
||||
pass
|
||||
_:
|
||||
payload = JSON.stringify(p_payload)
|
||||
return await _send_async(NakamaAPI.ApiRpc.create(NakamaAPI, {
|
||||
"id": p_func_id,
|
||||
"payload": payload
|
||||
}), NakamaAPI.ApiRpc, NakamaAPI, "rpc", "rpc").completed
|
||||
|
||||
# Send input to a multiplayer match on the server.
|
||||
# When no presences are supplied the new match state will be sent to all presences.
|
||||
# @param p_match_id - The ID of the match.
|
||||
# @param p_op_code - An operation code for the input.
|
||||
# @param p_data - The input data to send.
|
||||
# @param p_presences - The presences in the match who should receive the input.
|
||||
# Returns a task which represents the asynchronous operation.
|
||||
func send_match_state_async(p_match_id, p_op_code : int, p_data : String, p_presences = null):
|
||||
var req = _send_async(NakamaRTMessage.MatchDataSend.new(
|
||||
p_match_id,
|
||||
p_op_code,
|
||||
Marshalls.utf8_to_base64(p_data),
|
||||
p_presences
|
||||
))
|
||||
# This do not return a response from server, you don't really need to wait for it.
|
||||
req.call_deferred("resume", {})
|
||||
return req.completed
|
||||
|
||||
# Send input to a multiplayer match on the server.
|
||||
# When no presences are supplied the new match state will be sent to all presences.
|
||||
# @param p_match_id - The ID of the match.
|
||||
# @param p_op_code - An operation code for the input.
|
||||
# @param p_data - The input data to send.
|
||||
# @param p_presences - The presences in the match who should receive the input.
|
||||
# Returns a task which represents the asynchronous operation.
|
||||
func send_match_state_raw_async(p_match_id, p_op_code : int, p_data : PackedByteArray, p_presences = null):
|
||||
var req = _send_async(NakamaRTMessage.MatchDataSend.new(
|
||||
p_match_id,
|
||||
p_op_code,
|
||||
Marshalls.raw_to_base64(p_data),
|
||||
p_presences
|
||||
))
|
||||
# This do not return a response from server, you don't really need to wait for it.
|
||||
req.call_deferred("resume", {})
|
||||
return req.completed
|
||||
|
||||
# Unfollow one or more users from their status updates.
|
||||
# @param p_user_ids - An array of user ids to unfollow.
|
||||
# Returns a task which represents the asynchronous operation.
|
||||
func unfollow_users_async(p_ids : PackedStringArray):
|
||||
return await _send_async(NakamaRTMessage.StatusUnfollow.new(p_ids)).completed
|
||||
|
||||
# Update a chat message on a chat channel in the server.
|
||||
# @param p_channel_id - The ID of the chat channel with the message to update.
|
||||
# @param p_message_id - The ID of the message to update.
|
||||
# @param p_content - The new contents of the chat message.
|
||||
# Returns a task which resolves to an acknowledgement of the updated message.
|
||||
func update_chat_message_async(p_channel_id : String, p_message_id : String, p_content : Dictionary):
|
||||
return await _send_async(
|
||||
NakamaRTMessage.ChannelMessageUpdate.new(p_channel_id, p_message_id, JSON.stringify(p_content)),
|
||||
NakamaRTAPI.ChannelMessageAck
|
||||
).completed
|
||||
|
||||
# Update the status for the current user online.
|
||||
# @param p_status - The new status for the user.
|
||||
# Returns a task which represents the asynchronous operation.
|
||||
func update_status_async(p_status : String):
|
||||
return await _send_async(NakamaRTMessage.StatusUpdate.new(p_status)).completed
|
||||
|
||||
# Send a chat message to a chat channel on the server.
|
||||
# @param p_channel_id - The ID of the chat channel to send onto.
|
||||
# @param p_content - The contents of the message to send.
|
||||
# Returns a task which resolves to the acknowledgement of the chat message write.
|
||||
func write_chat_message_async(p_channel_id : String, p_content : Dictionary):
|
||||
return await _send_async(
|
||||
NakamaRTMessage.ChannelMessageSend.new(p_channel_id, JSON.stringify(p_content)),
|
||||
NakamaRTAPI.ChannelMessageAck
|
||||
).completed
|
||||
|
||||
# Accept a party member's request to join the party.
|
||||
# @param p_party_id - The party ID to accept the join request for.
|
||||
# @param p_presence - The presence to accept as a party member.
|
||||
# Returns a task to represent the asynchronous operation.
|
||||
func accept_party_member_async(p_party_id : String, p_presence : NakamaRTAPI.UserPresence):
|
||||
return await _send_async(NakamaRTMessage.PartyAccept.new(p_party_id, p_presence)).completed
|
||||
|
||||
# Begin matchmaking as a party.
|
||||
# @param p_party_id - Party ID.
|
||||
# @param p_query - Filter query used to identify suitable users.
|
||||
# @param p_min_count - Minimum total user count to match together.
|
||||
# @param p_max_count - Maximum total user count to match together.
|
||||
# @param p_string_properties - String properties.
|
||||
# @param p_numeric_properties - Numeric properties.
|
||||
# @param p_count_multiple - Optional multiple of the count that must be satisfied.
|
||||
# Returns a task to represent the asynchronous operation.
|
||||
func add_matchmaker_party_async(p_party_id : String, p_query : String = "*", p_min_count : int = 2,
|
||||
p_max_count : int = 8, p_string_properties = {}, p_numeric_properties = {}, p_count_multiple : int = 0):
|
||||
return await _send_async(
|
||||
NakamaRTMessage.PartyMatchmakerAdd.new(p_party_id, p_min_count,
|
||||
p_max_count, p_query, p_string_properties, p_numeric_properties,
|
||||
p_count_multiple if p_count_multiple > 0 else null),
|
||||
NakamaRTAPI.PartyMatchmakerTicket).completed
|
||||
|
||||
# End a party, kicking all party members and closing it.
|
||||
# @param p_party_id - The ID of the party.
|
||||
# Returns a task to represent the asynchronous operation.
|
||||
func close_party_async(p_party_id : String):
|
||||
var msg := NakamaRTAPI.PartyClose.new()
|
||||
msg.party_id = p_party_id
|
||||
return await _send_async(msg).completed
|
||||
|
||||
# Create a party.
|
||||
# @param p_open - Whether or not the party will require join requests to be approved by the party leader.
|
||||
# @param p_max_size - Maximum number of party members. This maximum does not include the party leader.
|
||||
# Returns a task to represent the asynchronous operation.
|
||||
func create_party_async(p_open : bool, p_max_size : int) -> NakamaRTAPI.Party:
|
||||
return await _send_async(
|
||||
NakamaRTMessage.PartyCreate.new(p_open, p_max_size),
|
||||
NakamaRTAPI.Party
|
||||
).completed
|
||||
|
||||
# Join a party.
|
||||
# @param p_party_id - Party ID.
|
||||
# Returns a task to represent the asynchronous operation.
|
||||
func join_party_async(p_party_id : String):
|
||||
return await _send_async(NakamaRTMessage.PartyJoin.new(p_party_id)).completed
|
||||
|
||||
# Leave the party.
|
||||
# @param p_party_id - Party ID.
|
||||
# Returns a task to represent the asynchronous operation.
|
||||
func leave_party_async(p_party_id : String):
|
||||
return await _send_async(NakamaRTMessage.PartyLeave.new(p_party_id)).completed
|
||||
|
||||
# Request a list of pending join requests for a party.
|
||||
# @param p_party_id - Party ID.
|
||||
# Returns a task which resolves to a list of all party join requests.
|
||||
func list_party_join_requests_async(p_party_id : String) -> NakamaRTAPI.PartyJoinRequest:
|
||||
return await _send_async(
|
||||
NakamaRTMessage.PartyJoinRequestList.new(p_party_id),
|
||||
NakamaRTAPI.PartyJoinRequest).completed
|
||||
|
||||
# Promote a new party leader.
|
||||
# @param p_party_id - Party ID.
|
||||
# @param p_party_member - The presence of an existing party member to promote as the new leader.
|
||||
# Returns a which represents the asynchronous operation.
|
||||
func promote_party_member(p_party_id : String, p_party_member : NakamaRTAPI.UserPresence):
|
||||
return await _send_async(NakamaRTMessage.PartyPromote.new(p_party_id, p_party_member)).completed
|
||||
|
||||
# Cancel a party matchmaking process using a ticket.
|
||||
# @param p_party_id - Party ID.
|
||||
# @param p_ticket - The ticket to cancel.
|
||||
# Returns a task which represents the asynchronous operation.
|
||||
func remove_matchmaker_party_async(p_party_id : String, p_ticket : String):
|
||||
return await _send_async(NakamaRTMessage.PartyMatchmakerRemove.new(p_party_id, p_ticket)).completed
|
||||
|
||||
# Kick a party member, or decline a request to join.
|
||||
# @param p_party_id - Party ID to remove/reject from.
|
||||
# @param p_presence - The presence to remove or reject.
|
||||
# Returns a task which represents the asynchronous operation.
|
||||
func remove_party_member_async(p_party_id : String, p_presence : NakamaRTAPI.UserPresence):
|
||||
return await _send_async(NakamaRTMessage.PartyRemove.new(p_party_id, p_presence)).completed
|
||||
|
||||
# Send data to a party.
|
||||
# @param p_party_id - Party ID to send to.
|
||||
# @param p_op_code - Op code value.
|
||||
# @param data - Data payload, if any.
|
||||
# Returns a task which represents the asynchronous operation.
|
||||
func send_party_data_async(p_party_id : String, p_op_code : int, p_data:String = ""):
|
||||
var base64_data = null if p_data.is_empty() else Marshalls.utf8_to_base64(p_data)
|
||||
return await _send_async(NakamaRTMessage.PartyDataSend.new(p_party_id, p_op_code, base64_data)).completed
|
||||
|
||||
# Send data to a party.
|
||||
# @param p_party_id - Party ID to send to.
|
||||
# @param p_op_code - Op code value.
|
||||
# @param data - Data payload, if any.
|
||||
# Returns a task which represents the asynchronous operation.
|
||||
func send_party_data_raw_async(p_party_id : String, p_op_code : int, p_data:PackedByteArray):
|
||||
var base64_data = null if p_data.is_empty() else Marshalls.raw_to_base64(p_data)
|
||||
return await _send_async(NakamaRTMessage.PartyDataSend.new(p_party_id, p_op_code, base64_data)).completed
|
||||
@@ -0,0 +1 @@
|
||||
uid://frra7b0vpolf
|
||||
@@ -0,0 +1,75 @@
|
||||
@tool
|
||||
extends Node
|
||||
|
||||
# An adapter which implements a socket with a protocol supported by Nakama.
|
||||
class_name NakamaSocketAdapter
|
||||
|
||||
var _ws := WebSocketPeer.new()
|
||||
var _ws_last_state := WebSocketPeer.STATE_CLOSED
|
||||
var _timeout : int = 30
|
||||
var _start : int = 0
|
||||
var logger = NakamaLogger.new()
|
||||
|
||||
# A signal emitted when the socket is connected.
|
||||
signal connected()
|
||||
|
||||
# A signal emitted when the socket is disconnected.
|
||||
signal closed()
|
||||
|
||||
# A signal emitted when the socket has an error when connecting.
|
||||
signal received_error(p_exception)
|
||||
|
||||
# A signal emitted when the socket receives a message.
|
||||
signal received(p_bytes) # PackedByteArray
|
||||
|
||||
# If the socket is connected.
|
||||
func is_connected_to_host():
|
||||
return _ws.get_ready_state() == WebSocketPeer.STATE_OPEN
|
||||
|
||||
# If the socket is connecting.
|
||||
func is_connecting_to_host():
|
||||
return _ws.get_ready_state() == WebSocketPeer.STATE_CONNECTING
|
||||
|
||||
# Close the socket with an asynchronous operation.
|
||||
func close():
|
||||
_ws.close()
|
||||
|
||||
# Connect to the server with an asynchronous operation.
|
||||
# @param p_uri - The URI of the server.
|
||||
# @param p_timeout - The timeout for the connect attempt on the socket.
|
||||
func connect_to_host(p_uri : String, p_timeout : int):
|
||||
_timeout = p_timeout
|
||||
_start = Time.get_unix_time_from_system()
|
||||
var err = _ws.connect_to_url(p_uri)
|
||||
if err != OK:
|
||||
logger.debug("Error connecting to host %s" % p_uri)
|
||||
call_deferred("emit_signal", "received_error", err)
|
||||
return
|
||||
_ws_last_state = WebSocketPeer.STATE_CLOSED
|
||||
|
||||
# Send data to the server with an asynchronous operation.
|
||||
# @param p_buffer - The buffer with the message to send.
|
||||
# @param p_reliable - If the message should be sent reliably (will be ignored by some protocols).
|
||||
func send(p_buffer : PackedByteArray, p_reliable : bool = true) -> int:
|
||||
return _ws.send(p_buffer, WebSocketPeer.WRITE_MODE_TEXT)
|
||||
|
||||
func _process(delta):
|
||||
if _ws.get_ready_state() != WebSocketPeer.STATE_CLOSED:
|
||||
_ws.poll()
|
||||
|
||||
var state = _ws.get_ready_state()
|
||||
if _ws_last_state != state:
|
||||
_ws_last_state = state
|
||||
if state == WebSocketPeer.STATE_OPEN:
|
||||
connected.emit()
|
||||
elif state == WebSocketPeer.STATE_CLOSED:
|
||||
closed.emit()
|
||||
|
||||
if state == WebSocketPeer.STATE_CONNECTING:
|
||||
if _start + _timeout < Time.get_unix_time_from_system():
|
||||
logger.debug("Timeout when connecting to socket")
|
||||
received_error.emit(ERR_TIMEOUT)
|
||||
_ws.close()
|
||||
|
||||
while _ws.get_ready_state() == WebSocketPeer.STATE_OPEN and _ws.get_available_packet_count():
|
||||
received.emit(_ws.get_packet())
|
||||
@@ -0,0 +1 @@
|
||||
uid://d0a2333a4shsg
|
||||
@@ -0,0 +1,34 @@
|
||||
extends RefCounted
|
||||
class_name NakamaAsyncResult
|
||||
|
||||
var exception : NakamaException:
|
||||
set(v):
|
||||
pass
|
||||
get:
|
||||
return get_exception()
|
||||
|
||||
var _ex = null
|
||||
|
||||
func _init(p_ex = null):
|
||||
_ex = p_ex
|
||||
|
||||
func is_exception():
|
||||
return get_exception() != null
|
||||
|
||||
func was_cancelled():
|
||||
return is_exception() and get_exception().cancelled
|
||||
|
||||
func get_exception() -> NakamaException:
|
||||
return _ex as NakamaException
|
||||
|
||||
func _to_string():
|
||||
if is_exception():
|
||||
return get_exception()._to_string()
|
||||
return "NakamaAsyncResult<>"
|
||||
|
||||
static func _safe_ret(p_obj, p_type : GDScript):
|
||||
if is_instance_of(p_obj, p_type):
|
||||
return p_obj # Correct type
|
||||
elif p_obj is NakamaException:
|
||||
return p_type.new(p_obj) # It's an exception. Incapsulate it
|
||||
return p_type.new(NakamaException.new()) # It's something else. generate an exception
|
||||
@@ -0,0 +1 @@
|
||||
uid://c45jgr825qlw3
|
||||
@@ -0,0 +1,42 @@
|
||||
extends RefCounted
|
||||
|
||||
# An exception generated during a request.
|
||||
# Usually contains at least an error message.
|
||||
class_name NakamaException
|
||||
|
||||
var _status_code : int = -1
|
||||
var status_code : int:
|
||||
set(v):
|
||||
pass
|
||||
get:
|
||||
return _status_code
|
||||
|
||||
var _grpc_status_code : int = -1
|
||||
var grpc_status_code : int:
|
||||
set(v):
|
||||
pass
|
||||
get:
|
||||
return _grpc_status_code
|
||||
|
||||
var _message : String = ""
|
||||
var message : String:
|
||||
set(v):
|
||||
pass
|
||||
get:
|
||||
return _message
|
||||
|
||||
var _cancelled : bool = false
|
||||
var cancelled : bool:
|
||||
set(v):
|
||||
pass
|
||||
get:
|
||||
return _cancelled
|
||||
|
||||
func _init(p_message : String = "", p_status_code : int = -1, p_grpc_status_code : int = -1, p_cancelled : bool = false):
|
||||
_status_code = p_status_code
|
||||
_grpc_status_code = p_grpc_status_code
|
||||
_message = p_message
|
||||
_cancelled = p_cancelled
|
||||
|
||||
func _to_string() -> String:
|
||||
return "NakamaException(StatusCode={%s}, Message='{%s}', GrpcStatusCode={%s})" % [_status_code, _message, _grpc_status_code]
|
||||
@@ -0,0 +1 @@
|
||||
uid://dkxlcxbw7e6si
|
||||
@@ -0,0 +1,38 @@
|
||||
extends RefCounted
|
||||
class_name NakamaLogger
|
||||
|
||||
enum LOG_LEVEL {NONE, ERROR, WARNING, INFO, VERBOSE, DEBUG}
|
||||
|
||||
var _level = LOG_LEVEL.ERROR
|
||||
var _module = "Nakama"
|
||||
|
||||
func _init(p_module : String = "Nakama", p_level : int = LOG_LEVEL.ERROR):
|
||||
_level = p_level
|
||||
_module = p_module
|
||||
|
||||
func _log(level : int, msg):
|
||||
if level <= _level:
|
||||
if level == LOG_LEVEL.ERROR:
|
||||
printerr("=== %s : ERROR === %s" % [_module, str(msg)])
|
||||
else:
|
||||
var what = "=== UNKNOWN === "
|
||||
for k in LOG_LEVEL:
|
||||
if level == LOG_LEVEL[k]:
|
||||
what = "=== %s : %s === " % [_module, k]
|
||||
break
|
||||
print(what + str(msg))
|
||||
|
||||
func error(msg):
|
||||
_log(LOG_LEVEL.ERROR, msg)
|
||||
|
||||
func warning(msg):
|
||||
_log(LOG_LEVEL.WARNING, msg)
|
||||
|
||||
func info(msg):
|
||||
_log(LOG_LEVEL.INFO, msg)
|
||||
|
||||
func verbose(msg):
|
||||
_log(LOG_LEVEL.VERBOSE, msg)
|
||||
|
||||
func debug(msg):
|
||||
_log(LOG_LEVEL.DEBUG, msg)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dm4l8n2hjcwle
|
||||
@@ -0,0 +1,363 @@
|
||||
extends RefCounted
|
||||
class_name NakamaMultiplayerBridge
|
||||
|
||||
enum MatchState {
|
||||
DISCONNECTED,
|
||||
JOINING,
|
||||
CONNECTED,
|
||||
SOCKET_CLOSED,
|
||||
}
|
||||
|
||||
enum MetaMessageType {
|
||||
CLAIM_HOST,
|
||||
ASSIGN_PEER_ID,
|
||||
}
|
||||
|
||||
# Read-only variables.
|
||||
var _nakama_socket: NakamaSocket
|
||||
var nakama_socket: NakamaSocket:
|
||||
get: return _nakama_socket
|
||||
set(_v): pass
|
||||
var _match_state: int = MatchState.DISCONNECTED
|
||||
var match_state: int:
|
||||
get: return _match_state
|
||||
set(_v): pass
|
||||
var _match_id := ''
|
||||
var match_id: String:
|
||||
get: return _match_id
|
||||
set(_v): pass
|
||||
var _multiplayer_peer: NakamaMultiplayerPeer = NakamaMultiplayerPeer.new()
|
||||
var multiplayer_peer: NakamaMultiplayerPeer:
|
||||
get: return _multiplayer_peer
|
||||
set(_v): pass
|
||||
|
||||
# Configuration that can be set by the developer.
|
||||
var meta_op_code: int = 9001
|
||||
var rpc_op_code: int = 9002
|
||||
|
||||
# Internal variables.
|
||||
var _my_session_id: String
|
||||
var _my_peer_id: int = 0
|
||||
var _id_map := {}
|
||||
var _users := {}
|
||||
var _matchmaker_ticket := ''
|
||||
|
||||
class User extends RefCounted:
|
||||
var presence
|
||||
var peer_id: int = 0
|
||||
|
||||
func _init(p_presence) -> void:
|
||||
presence = p_presence
|
||||
|
||||
signal match_join_error (exception)
|
||||
signal match_joined ()
|
||||
|
||||
func _set_readonly(_value) -> void:
|
||||
pass
|
||||
|
||||
func _init(p_nakama_socket: NakamaSocket) -> void:
|
||||
_nakama_socket = p_nakama_socket
|
||||
_nakama_socket.received_match_presence.connect(self._on_nakama_socket_received_match_presence)
|
||||
_nakama_socket.received_matchmaker_matched.connect(self._on_nakama_socket_received_matchmaker_matched)
|
||||
_nakama_socket.received_match_state.connect(self._on_nakama_socket_received_match_state)
|
||||
_nakama_socket.closed.connect(self._on_nakama_socket_closed)
|
||||
|
||||
_multiplayer_peer.packet_generated.connect(self._on_multiplayer_peer_packet_generated)
|
||||
_multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTING)
|
||||
|
||||
func create_match() -> void:
|
||||
if _match_state != MatchState.DISCONNECTED:
|
||||
push_error("Cannot create match when state is %s" % MatchState.keys()[_match_state])
|
||||
return
|
||||
|
||||
_match_state = MatchState.JOINING
|
||||
multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTING)
|
||||
|
||||
var res = await _nakama_socket.create_match_async()
|
||||
if res.is_exception():
|
||||
match_join_error.emit(res.get_exception())
|
||||
leave()
|
||||
return
|
||||
|
||||
_setup_match(res)
|
||||
_setup_host()
|
||||
|
||||
func join_match(p_match_id: String) -> void:
|
||||
if _match_state != MatchState.DISCONNECTED:
|
||||
push_error("Cannot join match when state is %s" % MatchState.keys()[_match_state])
|
||||
return
|
||||
|
||||
_match_state = MatchState.JOINING
|
||||
multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTING)
|
||||
|
||||
var res = await _nakama_socket.join_match_async(p_match_id)
|
||||
if res.is_exception():
|
||||
match_join_error.emit(res.get_exception())
|
||||
leave()
|
||||
return
|
||||
|
||||
_setup_match(res)
|
||||
|
||||
func join_named_match(_match_name: String) -> void:
|
||||
if _match_state != MatchState.DISCONNECTED:
|
||||
push_error("Cannot join match when state is %s" % MatchState.keys()[_match_state])
|
||||
return
|
||||
|
||||
_match_state = MatchState.JOINING
|
||||
multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTING)
|
||||
|
||||
var res = await _nakama_socket.create_match_async(_match_name)
|
||||
if res.is_exception():
|
||||
match_join_error.emit(res.get_exception())
|
||||
leave()
|
||||
return
|
||||
|
||||
_setup_match(res)
|
||||
if res.size == 0 or (res.size == 1 and res.presences.size() == 0):
|
||||
_setup_host()
|
||||
|
||||
func start_matchmaking(ticket) -> void:
|
||||
if _match_state != MatchState.DISCONNECTED:
|
||||
push_error("Cannot start matchmaking when state is %s" % MatchState.keys()[_match_state])
|
||||
return
|
||||
if ticket.is_exception():
|
||||
push_error("Ticket with exception passed into start_matchmaking()")
|
||||
return
|
||||
|
||||
_match_state = MatchState.JOINING
|
||||
multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTING)
|
||||
|
||||
_matchmaker_ticket = ticket.ticket
|
||||
|
||||
func _on_nakama_socket_received_matchmaker_matched(matchmaker_matched) -> void:
|
||||
if _matchmaker_ticket != matchmaker_matched.ticket:
|
||||
return
|
||||
|
||||
# Get a list of sorted session ids.
|
||||
var session_ids := []
|
||||
for matchmaker_user in matchmaker_matched.users:
|
||||
session_ids.append(matchmaker_user.presence.session_id)
|
||||
session_ids.sort()
|
||||
|
||||
var res = await _nakama_socket.join_matched_async(matchmaker_matched)
|
||||
if res.is_exception():
|
||||
match_join_error.emit(res.get_exception())
|
||||
leave()
|
||||
return
|
||||
|
||||
_setup_match(res)
|
||||
|
||||
# If our session is the first alphabetically, then we'll be the host.
|
||||
if _my_session_id == session_ids[0]:
|
||||
_setup_host()
|
||||
|
||||
# Add all of the existing peers.
|
||||
for presence in res.presences:
|
||||
if presence.session_id != _my_session_id:
|
||||
_host_add_peer(presence)
|
||||
|
||||
func _on_nakama_socket_closed() -> void:
|
||||
match_state = MatchState.SOCKET_CLOSED
|
||||
_cleanup()
|
||||
|
||||
func get_user_presence_for_peer(peer_id: int) -> NakamaRTAPI.UserPresence:
|
||||
var session_id = _id_map.get(peer_id)
|
||||
if session_id == null:
|
||||
return null
|
||||
var user = _users.get(session_id)
|
||||
if user == null:
|
||||
return null
|
||||
return user.presence
|
||||
|
||||
func leave() -> void:
|
||||
if _match_state == MatchState.DISCONNECTED:
|
||||
return
|
||||
_match_state = MatchState.DISCONNECTED
|
||||
|
||||
if _match_id:
|
||||
await _nakama_socket.leave_match_async(_match_id)
|
||||
if _matchmaker_ticket:
|
||||
await _nakama_socket.remove_matchmaker_async(_matchmaker_ticket)
|
||||
|
||||
_cleanup()
|
||||
|
||||
func _cleanup() -> void:
|
||||
for peer_id in _id_map:
|
||||
multiplayer_peer.peer_disconnected.emit(peer_id)
|
||||
|
||||
_match_id = ''
|
||||
_matchmaker_ticket = ''
|
||||
_my_session_id = ''
|
||||
_my_peer_id = 0
|
||||
_id_map.clear()
|
||||
_users.clear()
|
||||
|
||||
_multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_DISCONNECTED)
|
||||
|
||||
func _setup_match(res) -> void:
|
||||
_match_id = res.match_id
|
||||
_my_session_id = res.self_user.session_id
|
||||
|
||||
_users[_my_session_id] = User.new(res.self_user)
|
||||
|
||||
for presence in res.presences:
|
||||
if not _users.has(presence.session_id):
|
||||
_users[presence.session_id] = User.new(presence)
|
||||
|
||||
func _setup_host() -> void:
|
||||
# Claim id 1 and start the match.
|
||||
_my_peer_id = 1
|
||||
_map_id_to_session(1, _my_session_id)
|
||||
_match_state = MatchState.CONNECTED
|
||||
_multiplayer_peer.initialize(_my_peer_id)
|
||||
match_joined.emit()
|
||||
|
||||
func _generate_id(session_id: String) -> int:
|
||||
# Peer ids can only be positive 32-bit signed integers.
|
||||
var peer_id: int = session_id.hash() & 0x7FFFFFFF
|
||||
|
||||
# If this peer id is already taken, try to find another.
|
||||
while peer_id <= 1 or _id_map.has(peer_id):
|
||||
peer_id += 1
|
||||
if peer_id > 0x7FFFFFFF or peer_id <= 0:
|
||||
peer_id = randi() & 0x7FFFFFFF
|
||||
|
||||
return peer_id
|
||||
|
||||
func _map_id_to_session(peer_id: int, session_id: String) -> void:
|
||||
_id_map[peer_id] = session_id
|
||||
_users[session_id].peer_id = peer_id
|
||||
|
||||
func _host_add_peer(presence) -> void:
|
||||
var peer_id = _generate_id(presence.session_id)
|
||||
_map_id_to_session(peer_id, presence.session_id)
|
||||
|
||||
# Tell them we are the host.
|
||||
_nakama_socket.send_match_state_async(_match_id, meta_op_code, JSON.stringify({
|
||||
type = MetaMessageType.CLAIM_HOST,
|
||||
}), [presence])
|
||||
|
||||
# Tell them about all the other connected peers.
|
||||
for other_peer_id in _id_map:
|
||||
var other_session_id = _id_map[other_peer_id]
|
||||
if other_session_id == presence.session_id or other_session_id == _my_session_id:
|
||||
continue
|
||||
_nakama_socket.send_match_state_async(_match_id, meta_op_code, JSON.stringify({
|
||||
type = MetaMessageType.ASSIGN_PEER_ID,
|
||||
session_id = other_session_id,
|
||||
peer_id = other_peer_id,
|
||||
}), [presence])
|
||||
|
||||
# Assign them a peer_id (tell everyone about it).
|
||||
_nakama_socket.send_match_state_async(_match_id, meta_op_code, JSON.stringify({
|
||||
type = MetaMessageType.ASSIGN_PEER_ID,
|
||||
session_id = presence.session_id,
|
||||
peer_id = peer_id,
|
||||
}))
|
||||
|
||||
_multiplayer_peer.peer_connected.emit(peer_id)
|
||||
|
||||
func _on_nakama_socket_received_match_presence(event) -> void:
|
||||
if _match_state == MatchState.DISCONNECTED:
|
||||
return
|
||||
if event.match_id != _match_id:
|
||||
return
|
||||
|
||||
for presence in event.joins:
|
||||
if not _users.has(presence.session_id):
|
||||
_users[presence.session_id] = User.new(presence)
|
||||
|
||||
# If we are the host, and they don't yet have a peer id, then let's
|
||||
# generate a new id for them and send all the necessary messages.
|
||||
if _my_peer_id == 1 and _users[presence.session_id].peer_id == 0:
|
||||
_host_add_peer(presence)
|
||||
|
||||
for presence in event.leaves:
|
||||
if not _users.has(presence.session_id):
|
||||
continue
|
||||
|
||||
var peer_id = _users[presence.session_id].peer_id
|
||||
|
||||
_multiplayer_peer.peer_disconnected.emit(peer_id)
|
||||
|
||||
_users.erase(presence.session_id)
|
||||
_id_map.erase(peer_id)
|
||||
|
||||
func _parse_json(data: String):
|
||||
var json = JSON.new()
|
||||
if json.parse(data) != OK:
|
||||
return null
|
||||
var content = json.get_data()
|
||||
if not content is Dictionary:
|
||||
return null
|
||||
return content
|
||||
|
||||
func _on_nakama_socket_received_match_state(data) -> void:
|
||||
if _match_state == MatchState.DISCONNECTED:
|
||||
return
|
||||
if data.match_id != _match_id:
|
||||
return
|
||||
|
||||
if data.op_code == meta_op_code:
|
||||
var content = _parse_json(data.data)
|
||||
if content == null:
|
||||
return
|
||||
var type = content['type']
|
||||
#print ("RECEIVED: ", content)
|
||||
|
||||
if type == MetaMessageType.CLAIM_HOST:
|
||||
if _id_map.has(1):
|
||||
# @todo Can we mediate this dispute?
|
||||
push_error("User %s claiming to be host, when user %s has already claimed it" % [data.presence.session_id, _id_map[1]])
|
||||
else:
|
||||
_map_id_to_session(1, data.presence.session_id)
|
||||
return
|
||||
|
||||
# Ensure that any meta messages are coming from the host!
|
||||
if data.presence.session_id != _id_map[1]:
|
||||
push_error("Received meta message from user %s who isn't the host: %s" % [data.presence.session_id, content])
|
||||
return
|
||||
|
||||
if type == MetaMessageType.ASSIGN_PEER_ID:
|
||||
var session_id = content['session_id']
|
||||
var peer_id = content['peer_id']
|
||||
|
||||
if _users.has(session_id) and _users[session_id].peer_id != 0:
|
||||
push_error("Attempting to assign peer id %s to %s which already has id %s" % [
|
||||
peer_id,
|
||||
session_id,
|
||||
_users[session_id].peer_id,
|
||||
])
|
||||
return
|
||||
|
||||
_map_id_to_session(peer_id, session_id)
|
||||
|
||||
if _my_session_id == session_id:
|
||||
_match_state = MatchState.CONNECTED
|
||||
_multiplayer_peer.initialize(peer_id)
|
||||
_multiplayer_peer.set_connection_status(MultiplayerPeer.CONNECTION_CONNECTED)
|
||||
match_joined.emit()
|
||||
_multiplayer_peer.peer_connected.emit(1)
|
||||
else:
|
||||
_multiplayer_peer.peer_connected.emit(peer_id)
|
||||
else:
|
||||
_nakama_socket.logger.error("Received meta message with unknown type: %s" % type)
|
||||
elif data.op_code == rpc_op_code:
|
||||
var from_session_id: String = data.presence.session_id
|
||||
if not _users.has(from_session_id) or _users[from_session_id].peer_id == 0:
|
||||
push_error("Received RPC from %s which isn't assigned a peer id" % data.presence.session_id)
|
||||
return
|
||||
var from_peer_id = _users[from_session_id].peer_id
|
||||
_multiplayer_peer.deliver_packet(data.binary_data, from_peer_id)
|
||||
|
||||
func _on_multiplayer_peer_packet_generated(peer_id: int, buffer: PackedByteArray) -> void:
|
||||
if match_state == MatchState.CONNECTED:
|
||||
var target_presences = null
|
||||
if peer_id > 0:
|
||||
if not _id_map.has(peer_id):
|
||||
push_error("Attempting to send RPC to unknown peer id: %s" % peer_id)
|
||||
return
|
||||
target_presences = [ _users[_id_map[peer_id]].presence ]
|
||||
_nakama_socket.send_match_state_raw_async(_match_id, rpc_op_code, buffer, target_presences)
|
||||
else:
|
||||
push_error("RPC sent while the NakamaMultiplayerBridge isn't connected!")
|
||||
@@ -0,0 +1 @@
|
||||
uid://cmp474tw2m25i
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dg4tevx6mxts3
|
||||
@@ -0,0 +1,160 @@
|
||||
extends RefCounted
|
||||
class_name NakamaSerializer
|
||||
|
||||
static func serialize(p_obj : Object) -> Dictionary:
|
||||
var out = {}
|
||||
var schema = p_obj.get("_SCHEMA")
|
||||
if schema == null:
|
||||
return {} # No schema defined
|
||||
for k in schema:
|
||||
var prop = schema[k]
|
||||
var val = p_obj.get(prop["name"])
|
||||
if val == null:
|
||||
continue
|
||||
var type = prop["type"]
|
||||
var content = prop.get("content", TYPE_NIL)
|
||||
if typeof(content) == TYPE_STRING:
|
||||
content = TYPE_OBJECT
|
||||
var val_type = typeof(val)
|
||||
match val_type:
|
||||
TYPE_OBJECT: # Simple objects
|
||||
out[k] = serialize(val)
|
||||
TYPE_ARRAY: # Array of objects
|
||||
var arr = []
|
||||
for e in val:
|
||||
if typeof(e) != TYPE_OBJECT:
|
||||
continue
|
||||
arr.append(serialize(e))
|
||||
out[k] = arr
|
||||
TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_STRING_ARRAY: # Array of ints, bools, or strings
|
||||
var arr = []
|
||||
for e in val:
|
||||
if content == TYPE_BOOL:
|
||||
e = bool(e)
|
||||
if typeof(e) != content:
|
||||
continue
|
||||
arr.append(e)
|
||||
out[k] = arr
|
||||
TYPE_DICTIONARY: # Maps
|
||||
var dict = {}
|
||||
if content == TYPE_OBJECT: # Map of objects
|
||||
for l in val:
|
||||
if typeof(val[l]) != TYPE_OBJECT:
|
||||
continue
|
||||
dict[l] = serialize(val[l])
|
||||
else: # Map of simple types
|
||||
for l in val:
|
||||
var e = val[l]
|
||||
if content == TYPE_FLOAT:
|
||||
e = float(e)
|
||||
elif content == TYPE_INT:
|
||||
e = int(e)
|
||||
elif content == TYPE_BOOL:
|
||||
e = bool(e)
|
||||
if typeof(e) != content:
|
||||
continue
|
||||
dict[l] = e
|
||||
out[k] = dict
|
||||
_:
|
||||
out[k] = val
|
||||
return out
|
||||
|
||||
static func deserialize(p_ns : GDScript, p_cls_name : String, p_dict : Dictionary) -> Object:
|
||||
var cls : GDScript = p_ns.get(p_cls_name)
|
||||
var schema = cls.get("_SCHEMA")
|
||||
if schema == null:
|
||||
return NakamaException.new() # No schema defined
|
||||
var obj = cls.new()
|
||||
for k in schema:
|
||||
var prop = schema[k]
|
||||
var pname = prop["name"]
|
||||
var type = prop["type"]
|
||||
var required = prop["required"]
|
||||
var content = prop.get("content", TYPE_NIL)
|
||||
var type_cmp = type
|
||||
if typeof(type) == TYPE_STRING: # A class
|
||||
type_cmp = TYPE_DICTIONARY
|
||||
if type_cmp == TYPE_PACKED_STRING_ARRAY or type_cmp == TYPE_PACKED_INT32_ARRAY: # A specialized array
|
||||
type_cmp = TYPE_ARRAY
|
||||
|
||||
var content_cmp = content
|
||||
if typeof(content) == TYPE_STRING: # A dictionary or array of classes
|
||||
content_cmp = TYPE_DICTIONARY
|
||||
|
||||
var val = p_dict.get(k, null)
|
||||
|
||||
# Ints might and up being recognized as floats. Change that if needed
|
||||
if type_cmp == TYPE_INT:
|
||||
if typeof(val) == TYPE_FLOAT:
|
||||
val = int(val)
|
||||
elif typeof(val) == TYPE_STRING and val.is_valid_int():
|
||||
val = val.to_int()
|
||||
|
||||
if typeof(val) == type_cmp:
|
||||
if typeof(type) == TYPE_STRING:
|
||||
obj.set(pname, deserialize(p_ns, type, val))
|
||||
elif type_cmp == TYPE_DICTIONARY:
|
||||
var v = {}
|
||||
for l in val:
|
||||
if typeof(content) == TYPE_STRING:
|
||||
v[l] = deserialize(p_ns, content, val[l])
|
||||
elif content == TYPE_FLOAT:
|
||||
v[l] = float(val[l])
|
||||
elif content == TYPE_INT:
|
||||
v[l] = int(val[l])
|
||||
elif content == TYPE_BOOL:
|
||||
v[l] = bool(val[l])
|
||||
else:
|
||||
v[l] = str(val[l])
|
||||
obj.set(pname, v)
|
||||
elif type_cmp == TYPE_ARRAY:
|
||||
var v
|
||||
match content:
|
||||
TYPE_INT, TYPE_BOOL: v = PackedInt32Array()
|
||||
TYPE_STRING: v = PackedStringArray()
|
||||
_: v = Array()
|
||||
for e in val:
|
||||
if typeof(content) == TYPE_STRING:
|
||||
v.append(deserialize(p_ns, content, e))
|
||||
elif content == TYPE_FLOAT:
|
||||
v.append(float(e))
|
||||
elif content == TYPE_INT:
|
||||
v.append(int(e))
|
||||
elif content == TYPE_BOOL:
|
||||
v.append(bool(e))
|
||||
else:
|
||||
v.append(str(e))
|
||||
obj.set(pname, v)
|
||||
else:
|
||||
obj.set(pname, val)
|
||||
elif required:
|
||||
obj._ex = NakamaException.new("ERROR [%s]: Missing or invalid required prop %s = %s:\n\t%s" % [p_cls_name, prop, p_dict.get(k), p_dict])
|
||||
return obj
|
||||
return obj
|
||||
|
||||
|
||||
###
|
||||
# Compatibility with Godot 3.1 which does not expose String.http_escape
|
||||
###
|
||||
const HEX = ["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"]
|
||||
|
||||
static func escape_http(p_str : String) -> String:
|
||||
var out : String = ""
|
||||
for o in p_str:
|
||||
if (o == '.' or o == '-' or o == '_' or o == '~' or
|
||||
(o >= 'a' and o <= 'z') or
|
||||
(o >= 'A' and o <= 'Z') or
|
||||
(o >= '0' and o <= '9')):
|
||||
out += o
|
||||
else:
|
||||
for b in o.to_utf8_buffer():
|
||||
out += "%%%s" % to_hex(b)
|
||||
return out
|
||||
|
||||
static func to_hex(p_val : int) -> String:
|
||||
var v := p_val
|
||||
var o := ""
|
||||
while v != 0:
|
||||
o = HEX[v % 16] + o
|
||||
v /= 16
|
||||
return o
|
||||
@@ -0,0 +1 @@
|
||||
uid://dolmk08fvbx3a
|
||||
21
addons/nakama/codegen/README.md
Normal file
21
addons/nakama/codegen/README.md
Normal 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.
|
||||
|
||||
639
addons/nakama/codegen/main.go
Normal file
639
addons/nakama/codegen/main.go
Normal 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()
|
||||
}
|
||||
@@ -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
0
server/.gdignore
Normal file
8
server/data/config.yml
Normal file
8
server/data/config.yml
Normal 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
53
server/docker-compose.yml
Normal 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:
|
||||
Reference in New Issue
Block a user