From 9c451bb53c52c6723fb1def9df1ba079502b6f34 Mon Sep 17 00:00:00 2001 From: Forest Anderson Date: Tue, 30 Jan 2024 16:50:10 -0500 Subject: [PATCH] Add godot ball example --- godot/bomber/project.godot | 1 + godot/circles/.gitattributes | 2 + godot/circles/.gitignore | 2 + godot/circles/Dockerfile | 19 ++ godot/circles/addons/rivet/api/rivet_api.gd | 132 ++++++++++++ .../addons/rivet/api/rivet_packages.gd | 97 +++++++++ .../circles/addons/rivet/api/rivet_request.gd | 57 +++++ .../addons/rivet/api/rivet_response.gd | 27 +++ .../addons/rivet/devtools/dock/deploy_tab.gd | 33 +++ .../rivet/devtools/dock/deploy_tab.tscn | 3 + .../addons/rivet/devtools/dock/dock.gd | 28 +++ .../addons/rivet/devtools/dock/dock.tscn | 3 + .../devtools/dock/elements/buttons_bar.gd | 38 ++++ .../devtools/dock/elements/buttons_bar.tscn | 3 + .../dock/elements/links_container.tscn | 3 + .../devtools/dock/elements/loading_button.gd | 32 +++ .../dock/elements/loading_button.tscn | 3 + .../devtools/dock/elements/logo_container.gd | 9 + .../dock/elements/logo_container.tscn | 3 + .../dock/elements/namespace_menu_button.gd | 31 +++ .../dock/elements/namespace_menu_button.tscn | 3 + .../addons/rivet/devtools/dock/installer.gd | 37 ++++ .../addons/rivet/devtools/dock/installer.tscn | 3 + .../addons/rivet/devtools/dock/loading.gd | 9 + .../addons/rivet/devtools/dock/loading.tscn | 3 + .../addons/rivet/devtools/dock/login.gd | 62 ++++++ .../addons/rivet/devtools/dock/login.tscn | 3 + .../rivet/devtools/dock/playtest_tab.gd | 113 ++++++++++ .../rivet/devtools/dock/playtest_tab.tscn | 3 + .../addons/rivet/devtools/dock/settings.gd | 15 ++ .../addons/rivet/devtools/dock/settings.tscn | 3 + .../rivet/devtools/dock/settings_tab.gd | 24 +++ .../rivet/devtools/dock/settings_tab.tscn | 3 + .../addons/rivet/devtools/rivet_cli.gd | 76 +++++++ .../addons/rivet/devtools/rivet_cli_output.gd | 38 ++++ .../rivet/devtools/rivet_editor_settings.gd | 28 +++ .../rivet/devtools/rivet_export_plugin.gd | 29 +++ .../rivet/devtools/rivet_plugin_bridge.gd | 128 +++++++++++ .../addons/rivet/devtools/rivet_thread.gd | 29 +++ .../rivet/images/icon-circle.png.import | 3 + .../addons/rivet/images/icon-text-black.svg | 9 + .../rivet/images/icon-text-black.svg.import | 3 + .../addons/rivet/images/icon-text-white.svg | 9 + .../rivet/images/icon-text-white.svg.import | 3 + .../addons/rivet/images/white-logo.svg.import | 3 + godot/circles/addons/rivet/plugin.cfg | 8 + godot/circles/addons/rivet/rivet.gd | 58 +++++ .../addons/rivet/rivet.version.toml.tpl | 52 +++++ godot/circles/addons/rivet/rivet_client.gd | 89 ++++++++ godot/circles/addons/rivet/rivet_global.gd | 36 ++++ godot/circles/addons/rivet/rivet_helper.gd | 115 ++++++++++ godot/circles/game.tscn | 45 ++++ godot/circles/gamestate.gd | 201 ++++++++++++++++++ godot/circles/godot_ball.gd | 47 ++++ godot/circles/godot_ball.tscn | 20 ++ godot/circles/icon.svg | 1 + godot/circles/icon.svg.import | 3 + godot/circles/loading.gd | 19 ++ godot/circles/loading.tscn | 28 +++ godot/circles/project.godot | 32 +++ godot/circles/rivet.yaml | 13 ++ 61 files changed, 1932 insertions(+) create mode 100644 godot/circles/.gitattributes create mode 100644 godot/circles/.gitignore create mode 100644 godot/circles/Dockerfile create mode 100644 godot/circles/addons/rivet/api/rivet_api.gd create mode 100644 godot/circles/addons/rivet/api/rivet_packages.gd create mode 100644 godot/circles/addons/rivet/api/rivet_request.gd create mode 100644 godot/circles/addons/rivet/api/rivet_response.gd create mode 100644 godot/circles/addons/rivet/devtools/dock/deploy_tab.gd create mode 100644 godot/circles/addons/rivet/devtools/dock/deploy_tab.tscn create mode 100644 godot/circles/addons/rivet/devtools/dock/dock.gd create mode 100644 godot/circles/addons/rivet/devtools/dock/dock.tscn create mode 100644 godot/circles/addons/rivet/devtools/dock/elements/buttons_bar.gd create mode 100644 godot/circles/addons/rivet/devtools/dock/elements/buttons_bar.tscn create mode 100644 godot/circles/addons/rivet/devtools/dock/elements/links_container.tscn create mode 100644 godot/circles/addons/rivet/devtools/dock/elements/loading_button.gd create mode 100644 godot/circles/addons/rivet/devtools/dock/elements/loading_button.tscn create mode 100644 godot/circles/addons/rivet/devtools/dock/elements/logo_container.gd create mode 100644 godot/circles/addons/rivet/devtools/dock/elements/logo_container.tscn create mode 100644 godot/circles/addons/rivet/devtools/dock/elements/namespace_menu_button.gd create mode 100644 godot/circles/addons/rivet/devtools/dock/elements/namespace_menu_button.tscn create mode 100644 godot/circles/addons/rivet/devtools/dock/installer.gd create mode 100644 godot/circles/addons/rivet/devtools/dock/installer.tscn create mode 100644 godot/circles/addons/rivet/devtools/dock/loading.gd create mode 100644 godot/circles/addons/rivet/devtools/dock/loading.tscn create mode 100644 godot/circles/addons/rivet/devtools/dock/login.gd create mode 100644 godot/circles/addons/rivet/devtools/dock/login.tscn create mode 100644 godot/circles/addons/rivet/devtools/dock/playtest_tab.gd create mode 100644 godot/circles/addons/rivet/devtools/dock/playtest_tab.tscn create mode 100644 godot/circles/addons/rivet/devtools/dock/settings.gd create mode 100644 godot/circles/addons/rivet/devtools/dock/settings.tscn create mode 100644 godot/circles/addons/rivet/devtools/dock/settings_tab.gd create mode 100644 godot/circles/addons/rivet/devtools/dock/settings_tab.tscn create mode 100644 godot/circles/addons/rivet/devtools/rivet_cli.gd create mode 100644 godot/circles/addons/rivet/devtools/rivet_cli_output.gd create mode 100644 godot/circles/addons/rivet/devtools/rivet_editor_settings.gd create mode 100644 godot/circles/addons/rivet/devtools/rivet_export_plugin.gd create mode 100644 godot/circles/addons/rivet/devtools/rivet_plugin_bridge.gd create mode 100644 godot/circles/addons/rivet/devtools/rivet_thread.gd create mode 100644 godot/circles/addons/rivet/images/icon-circle.png.import create mode 100644 godot/circles/addons/rivet/images/icon-text-black.svg create mode 100644 godot/circles/addons/rivet/images/icon-text-black.svg.import create mode 100644 godot/circles/addons/rivet/images/icon-text-white.svg create mode 100644 godot/circles/addons/rivet/images/icon-text-white.svg.import create mode 100644 godot/circles/addons/rivet/images/white-logo.svg.import create mode 100644 godot/circles/addons/rivet/plugin.cfg create mode 100644 godot/circles/addons/rivet/rivet.gd create mode 100644 godot/circles/addons/rivet/rivet.version.toml.tpl create mode 100644 godot/circles/addons/rivet/rivet_client.gd create mode 100644 godot/circles/addons/rivet/rivet_global.gd create mode 100644 godot/circles/addons/rivet/rivet_helper.gd create mode 100644 godot/circles/game.tscn create mode 100644 godot/circles/gamestate.gd create mode 100644 godot/circles/godot_ball.gd create mode 100644 godot/circles/godot_ball.tscn create mode 100644 godot/circles/icon.svg create mode 100644 godot/circles/icon.svg.import create mode 100644 godot/circles/loading.gd create mode 100644 godot/circles/loading.tscn create mode 100644 godot/circles/project.godot create mode 100644 godot/circles/rivet.yaml diff --git a/godot/bomber/project.godot b/godot/bomber/project.godot index a29235b..91bfe95 100644 --- a/godot/bomber/project.godot +++ b/godot/bomber/project.godot @@ -24,6 +24,7 @@ gamestate="*res://scripts/gamestate.gd" RivetHelper="*res://addons/rivet/rivet_helper.gd" Rivet="*res://addons/rivet/rivet_global.gd" RivetGlobal="*res://addons/rivet/rivet_global.gd" +RivetClient="*res://addons/rivet/rivet_client.gd" [display] diff --git a/godot/circles/.gitattributes b/godot/circles/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/godot/circles/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/godot/circles/.gitignore b/godot/circles/.gitignore new file mode 100644 index 0000000..4709183 --- /dev/null +++ b/godot/circles/.gitignore @@ -0,0 +1,2 @@ +# Godot 4+ specific ignores +.godot/ diff --git a/godot/circles/Dockerfile b/godot/circles/Dockerfile new file mode 100644 index 0000000..6d317e6 --- /dev/null +++ b/godot/circles/Dockerfile @@ -0,0 +1,19 @@ +FROM ghcr.io/rivet-gg/godot-docker/godot:4.2.1 AS builder +WORKDIR /app +COPY . . +RUN mkdir -p build/linux \ + && godot -v --export-release "Linux/X11" ./build/linux/game.x86_64 --headless + +FROM ubuntu:22.04 +RUN apt update -y \ + && apt install -y expect-dev \ + && rm -rf /var/lib/apt/lists/* \ + && useradd -ms /bin/bash rivet + +COPY --from=builder /app/build/linux/ /app + +# Change to user rivet +USER rivet + +# Unbuffer output so the logs get flushed +CMD ["sh", "-c", "unbuffer /app/game.x86_64 --verbose --headless -- --server | cat"] diff --git a/godot/circles/addons/rivet/api/rivet_api.gd b/godot/circles/addons/rivet/api/rivet_api.gd new file mode 100644 index 0000000..22488a3 --- /dev/null +++ b/godot/circles/addons/rivet/api/rivet_api.gd @@ -0,0 +1,132 @@ +class_name RivetApi +const RivetRequest = preload("rivet_request.gd") + +static var CONFIGURATION_CACHE + +static func _get_configuration(): + if CONFIGURATION_CACHE: + return CONFIGURATION_CACHE + + if FileAccess.file_exists(RivetPluginBridge.RIVET_CONFIGURATION_FILE_PATH): + var config_file = ResourceLoader.load(RivetPluginBridge.RIVET_CONFIGURATION_FILE_PATH) + if config_file and 'new' in config_file: + CONFIGURATION_CACHE = config_file.new() + return CONFIGURATION_CACHE + + if FileAccess.file_exists(RivetPluginBridge.RIVET_DEPLOYED_CONFIGURATION_FILE_PATH): + var deployed_config_file = ResourceLoader.load(RivetPluginBridge.RIVET_DEPLOYED_CONFIGURATION_FILE_PATH) + if deployed_config_file and 'new' in deployed_config_file: + CONFIGURATION_CACHE = deployed_config_file.new() + return CONFIGURATION_CACHE + + push_warning("Rivet configuration file not found") + CONFIGURATION_CACHE = null + return CONFIGURATION_CACHE + +static func _get_api_url(): + # Use plugin config if available + var plugin = RivetPluginBridge.get_plugin() + if plugin: + return plugin.api_endpoint + + # Override shipped configuration endpoint + var url_env = OS.get_environment("RIVET_API_ENDPOINT") + if url_env: + return url_env + + # Use configuration shipped with game + var config = _get_configuration() + if config: + return config.api_endpoint + + # Fallback + return "https://api.rivet.gg" + +## Get authorization token used from within only the plugin for cloud-specific +## actions. +static func _get_cloud_token(): + # Use plugin config if available + var plugin = RivetPluginBridge.get_plugin() + if plugin: + return plugin.cloud_token + + OS.crash("Rivet cloud token not found, this should only be called within the plugin") + +## Get authorization token used for making requests from within the game. +## +## The priority of tokens is: +## +## - If in editor, use the plugin token +## - If provided by environment, then use that (allows for testing) +## - Assume config is provided by the game client +static func _get_runtime_token(): + # Use plugin config if available + var plugin = RivetPluginBridge.get_plugin() + if plugin: + return plugin.namespace_token + + # Use configuration shipped with game + var token_env = OS.get_environment("RIVET_TOKEN") + if token_env: + return token_env + + # Use configuration shipped with game + var config = _get_configuration() + if config: + return config.namespace_token + + OS.crash("Rivet token not found, validate a config is shipped with the game in the .rivet folder") + +## Builds the headers for a request, including the authorization token +static func _build_headers(service: String) -> PackedStringArray: + var token = _get_cloud_token() if service == "cloud" else _get_runtime_token() + return [ + "Authorization: Bearer " + token, + ] + +## Builds a URL to Rivet cloud services +static func _build_url(path: String, service: String) -> String: + var path_segments := path.split("/", false) + path_segments.remove_at(0) + return _get_api_url() + "/%s/%s" % [service, "/".join(path_segments)] + +## Gets service name from a path (e.g. /users/123 -> users) +static func _get_service_from_path(path: String) -> String: + var path_segments := path.split("/", false) + return path_segments[0] + +## Creates a POST request to Rivet cloud services +## @experimental +static func POST(owner: Node, path: String, body: Dictionary) -> RivetRequest: + var service := _get_service_from_path(path) + var url := _build_url(path, service) + var body_json := JSON.stringify(body) + + return RivetRequest.new(owner, HTTPClient.METHOD_POST, url, { + "headers": _build_headers(service), + "body": body_json + }) + +## Creates a GET request to Rivet cloud services +## @experimental +static func GET(owner: Node, path: String, body: Dictionary) -> RivetRequest: + var service := _get_service_from_path(path) + var url := _build_url(path, service) + var body_json := JSON.stringify(body) + + return RivetRequest.new(owner, HTTPClient.METHOD_GET, url, { + "headers": _build_headers(service), + "body": body_json + }) + +## Creates a PUT request to Rivet cloud services +## @experimental +static func PUT(owner: Node, path: String, body: Dictionary) -> RivetRequest: + var service := _get_service_from_path(path) + var url := _build_url(path, service) + var body_json := JSON.stringify(body) + + return RivetRequest.new(owner, HTTPClient.METHOD_PUT, url, { + "headers": _build_headers(service), + "body": body_json + }) diff --git a/godot/circles/addons/rivet/api/rivet_packages.gd b/godot/circles/addons/rivet/api/rivet_packages.gd new file mode 100644 index 0000000..5f0bcd7 --- /dev/null +++ b/godot/circles/addons/rivet/api/rivet_packages.gd @@ -0,0 +1,97 @@ +const _RivetResponse = preload("rivet_response.gd") + +## Lobbies +## @experimental +class Lobbies: + ## Finds a lobby based on the given criteria. If a lobby is not found and + ## prevent_auto_create_lobby is true, a new lobby will be created. + ## + ## [url]{https://rivet.gg/docs/matchmaker/api/lobbies/find}[/url] + func find(body: Dictionary = {}): + return await Rivet.POST("matchmaker/lobbies/find", body).wait_completed() + + ## Joins a specific lobby. This request will use the direct player count + ## configured for the lobby group. + ## + ## [url]{https://rivet.gg/docs/matchmaker/api/lobbies/join}[/url] + func join(body: Dictionary = {}) -> _RivetResponse: + return await Rivet.POST("matchmaker/lobbies/join", body).wait_completed() + + ## Marks the current lobby as ready to accept connections. Players will not + ## be able to connect to this lobby until the lobby is flagged as ready. + ## + ## [url]{https://rivet.gg/docs/matchmaker/api/lobbies/ready}[/url] + func ready(body: Dictionary = {}) -> _RivetResponse: + return await Rivet.POST("matchmaker/lobbies/ready", body).wait_completed() + + ## If is_closed is true, the matchmaker will no longer route players to the + ## lobby. Players can still join using the /join endpoint (this can be disabled + ## by the developer by rejecting all new connections after setting the lobby + ## to closed). Does not shutdown the lobby. + ## + ## [url]{https://rivet.gg/docs/matchmaker/api/lobbies/set-closed}[/url] + func setClosed(body: Dictionary = {}) -> _RivetResponse: + return await Rivet.PUT("matchmaker/lobbies/set_closed", body).wait_completed() + + ## Creates a custom lobby. + ## + ## [url]{https://rivet.gg/docs/matchmaker/api/lobbies/create}[/url] + func create(body: Dictionary = {}) -> _RivetResponse: + return await Rivet.POST("matchmaker/lobbies/create", body).wait_completed() + + ## Lists all open lobbies. + ## + ## [url]{https://rivet.gg/docs/matchmaker/api/lobbies/list}[/url] + func list(body: Dictionary = {}) -> _RivetResponse: + return await Rivet.GET("matchmaker/lobbies/list", body).wait_completed() + + ## + ## + ## [url]{https://rivet.gg/docs/matchmaker/api/lobbies/set-state}[/url] + func setState(body: Dictionary = {}) -> _RivetResponse: + return await Rivet.PUT("matchmaker/lobbies/state", body).wait_completed() + + ## + ## + ## [url]{https://rivet.gg/docs/matchmaker/api/lobbies/get-state}[/url] + func getState(lobby_id, body: Dictionary = {}) -> _RivetResponse: + return await Rivet.GET("matchmaker/lobbies/{lobby_id}/state".format({"lobby_id": lobby_id}), body).wait_completed() + +## Players +## @experimental +class Players: + ## Validates the player token is valid and has not already been consumed then + ## marks the player as connected. + ## + ## [url]{https://rivet.gg/docs/matchmaker/api/players/connected}[/url] + func connected(body: Dictionary = {}) -> _RivetResponse: + return await Rivet.POST("matchmaker/players/connected", body).wait_completed() + + ## Marks a player as disconnected. # Ghost Players. + ## + ## [url]{https://rivet.gg/docs/matchmaker/api/players/disconnected}[/url] + func disconnected(body: Dictionary = {}) -> _RivetResponse: + return await Rivet.POST("matchmaker/players/disconnected", body).wait_completed() + + ## Gives matchmaker statistics about the players in game. + ## + ## [url]{https://rivet.gg/docs/matchmaker/api/players/statistics}[/url] + func getStatistics(body: Dictionary = {}) -> _RivetResponse: + return await Rivet.GET("matchmaker/players/statistics", body).wait_completed() + +class Regions: + ## Returns a list of regions available to this namespace. + ## Regions are sorted by most optimal to least optimal. + ## The player's IP address is used to calculate the regions' optimality. + ## + ## [url]{https://rivet.gg/docs/matchmaker/api/regions/list}[/url] + func list(body: Dictionary = {}) -> _RivetResponse: + return await Rivet.GET("matchmaker/regions", body).wait_completed() + +## Matchmaker +## @experimental +## @tutorial: https://rivet.gg/docs/matchmaker +class Matchmaker: + static var lobbies: Lobbies = Lobbies.new() + static var players: Players = Players.new() + static var regions: Regions = Regions.new() diff --git a/godot/circles/addons/rivet/api/rivet_request.gd b/godot/circles/addons/rivet/api/rivet_request.gd new file mode 100644 index 0000000..95bb483 --- /dev/null +++ b/godot/circles/addons/rivet/api/rivet_request.gd @@ -0,0 +1,57 @@ +extends RefCounted +## A wrapper around HTTPRequest that emits a signal when the request is completed. +## This is a workaround for the fact that `HTTPRequest.request()` is blocking. +## To run a request, create a new RivetRequest, connect to the completed signal, +## and call `request().wait_completed()` to wait for the request to complete. + + +const _RivetResponse := preload("rivet_response.gd") +const _RivetRequest := preload("rivet_request.gd") + +var response: _RivetResponse = null +var _opts: Dictionary +var _http_request: HTTPRequest + +var _success_callback: Callable +var _failure_callback: Callable + +signal completed(response: _RivetResponse) +signal succeeded(response: _RivetResponse) +signal failed(response: _RivetResponse) + +func _init(owner: Node, method: HTTPClient.Method, url: String, opts: Variant = null): + self._http_request = HTTPRequest.new() + self._http_request.request_completed.connect(_on_request_completed) + self._opts = { + "method": method, + "url": url, + "body": opts.body, + "headers": opts.headers, + } + owner.add_child(self._http_request) + self._http_request.request(_opts.url, _opts.headers, _opts.method, _opts.body) + +func set_success_callback(callback: Callable) -> _RivetRequest: + self._success_callback = callback + return self + +func set_failure_callback(callback: Callable) -> _RivetRequest: + self._failure_callback = callback + return self + +func _on_request_completed(result, response_code, headers, body): + self.response = _RivetResponse.new(result, response_code, headers, body) + if result == OK: + succeeded.emit(response) + if self._success_callback: + self._success_callback.call(response) + else: + failed.emit(response) + if self._failure_callback: + self._failure_callback.call(response) + completed.emit(response) + +## Waits for the request to complete and returns the response in non-blocking way +func wait_completed() -> _RivetResponse: + await completed + return response diff --git a/godot/circles/addons/rivet/api/rivet_response.gd b/godot/circles/addons/rivet/api/rivet_response.gd new file mode 100644 index 0000000..be954f9 --- /dev/null +++ b/godot/circles/addons/rivet/api/rivet_response.gd @@ -0,0 +1,27 @@ +extends RefCounted +## A response from the server. Contains the result, response code, headers, and body. +## The body is a dictionary of the JSON response. +## +## @experimental + +## The result of the request. 0 is success, 1 is failure. +var result: HTTPClient.Status + +## The response code from the server. +var response_code: HTTPClient.ResponseCode + +## The headers from the server. +var headers: PackedStringArray + +## The body of the response, as a JSON dictionary, could be a null. +var body: Variant + +func _init(result: int, response_code: int, headers: PackedStringArray, response_body: PackedByteArray) -> void: + self.result = result + self.response_code = response_code + self.headers = headers + + var json = JSON.new() + json.parse(response_body.get_string_from_utf8()) + body = json.get_data() + diff --git a/godot/circles/addons/rivet/devtools/dock/deploy_tab.gd b/godot/circles/addons/rivet/devtools/dock/deploy_tab.gd new file mode 100644 index 0000000..c2654ac --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/deploy_tab.gd @@ -0,0 +1,33 @@ +@tool extends MarginContainer + +@onready var namespace_selector: OptionButton = %DeployNamespaceSelector +@onready var manage_versions_button: Button = %ManageVersionButton +@onready var build_deploy_button: Button = %BuildDeployButton + +func _ready() -> void: + manage_versions_button.pressed.connect(_on_manage_versions_button_pressed) + build_deploy_button.pressed.connect(_on_build_deploy_button_pressed) + +func _on_manage_versions_button_pressed() -> void: + _all_actions_set_disabled(true) + + var result = await RivetPluginBridge.get_plugin().cli.run_command(["sidekick", "get-version", "--namespace", namespace_selector.current_value.namespace_id]) + if result.exit_code != 0 or !("Ok" in result.output): + RivetPluginBridge.display_cli_error(self, result) + + OS.shell_open(result.output["Ok"]["output"]) + _all_actions_set_disabled(false) + +func _on_build_deploy_button_pressed() -> void: + _all_actions_set_disabled(true) + + var result = await RivetPluginBridge.get_plugin().cli.run_command(["sidekick", "--show-terminal", "deploy", "--namespace", namespace_selector.current_value.name_id]) + if result.exit_code != 0: + RivetPluginBridge.display_cli_error(self, result) + + _all_actions_set_disabled(false) + +func _all_actions_set_disabled(disabled: bool) -> void: + namespace_selector.disabled = disabled + manage_versions_button.disabled = disabled + build_deploy_button.disabled = disabled diff --git a/godot/circles/addons/rivet/devtools/dock/deploy_tab.tscn b/godot/circles/addons/rivet/devtools/dock/deploy_tab.tscn new file mode 100644 index 0000000..54a6f22 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/deploy_tab.tscn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc59f7a53362220abc3d4858dab74f6b459cd8c585ee759450b252e018031b23 +size 1840 diff --git a/godot/circles/addons/rivet/devtools/dock/dock.gd b/godot/circles/addons/rivet/devtools/dock/dock.gd new file mode 100644 index 0000000..5eac679 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/dock.gd @@ -0,0 +1,28 @@ +@tool extends Control +## Mainpoint of the plugin's UI + +## Enum representing indexes of the children of this node +enum Screen { + Login, + Settings, + Loading, + Installer, +} + +func _ready() -> void: + change_current_screen(Screen.Installer) + + +func reload() -> void: + var instance = load("res://addons/rivet/devtools/dock/dock.tscn").instantiate() + replace_by(instance) + instance.grab_focus() + + +func change_current_screen(scene: Screen): + for idx in get_child_count(): + var child := get_child(idx) + if "visible" in child: + child.visible = idx == scene + if idx == scene and child.has_method("prepare"): + child.prepare() diff --git a/godot/circles/addons/rivet/devtools/dock/dock.tscn b/godot/circles/addons/rivet/devtools/dock/dock.tscn new file mode 100644 index 0000000..e440acf --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/dock.tscn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db5103c27568e19b20f335d96431211805427ecf621c8d4b0b8b5e6f8578e1ac +size 1383 diff --git a/godot/circles/addons/rivet/devtools/dock/elements/buttons_bar.gd b/godot/circles/addons/rivet/devtools/dock/elements/buttons_bar.gd new file mode 100644 index 0000000..2a1cecd --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/elements/buttons_bar.gd @@ -0,0 +1,38 @@ +@tool extends HBoxContainer + +signal selected() + +@export var tab_container: TabContainer + +var disabled: bool = false: + set(value): + disabled = value + for i in get_child_count(): + var child = get_child(i) + if child is Button: + child.disabled = disabled + +var current = 0 + +func _ready() -> void: + for i in get_child_count(): + var child = get_child(i) + if child is Button: + child.toggle_mode = true + child.pressed.connect(_select_button.bind(i)) + if i == 0: + child.set_pressed_no_signal(true) + +func _select_button(curr: int) -> void: + current = curr + if tab_container: + tab_container.set_current_tab(curr) + for i in get_child_count(): + var child = get_child(i) + if child is Button: + child.set_pressed_no_signal(curr==i) + selected.emit() + + +func set_current_button(button: int) -> void: + _select_button(button) \ No newline at end of file diff --git a/godot/circles/addons/rivet/devtools/dock/elements/buttons_bar.tscn b/godot/circles/addons/rivet/devtools/dock/elements/buttons_bar.tscn new file mode 100644 index 0000000..8f93b34 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/elements/buttons_bar.tscn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf33ec7b5197b987bc96ec1fc66ea82b78896b8f58581c3245a62e30fc6bfe51 +size 245 diff --git a/godot/circles/addons/rivet/devtools/dock/elements/links_container.tscn b/godot/circles/addons/rivet/devtools/dock/elements/links_container.tscn new file mode 100644 index 0000000..a666477 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/elements/links_container.tscn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f195f759d0e4967d4fd003875be75f8d912e727e698a2c27a111a260f238bdc5 +size 498 diff --git a/godot/circles/addons/rivet/devtools/dock/elements/loading_button.gd b/godot/circles/addons/rivet/devtools/dock/elements/loading_button.gd new file mode 100644 index 0000000..20e94c5 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/elements/loading_button.gd @@ -0,0 +1,32 @@ +@tool extends Button + +@export var loading: bool: set = _set_loading + +var _tween: Tween + +func _set_loading(value) -> void: + loading = value + disabled = value + + if _tween: + _tween.kill() + + if value: + _tween = get_tree().create_tween() + + var icons: Array[Texture2D] = [ + get_theme_icon("Progress1", "EditorIcons"), + get_theme_icon("Progress2", "EditorIcons"), + get_theme_icon("Progress3", "EditorIcons"), + get_theme_icon("Progress4", "EditorIcons"), + get_theme_icon("Progress5", "EditorIcons"), + get_theme_icon("Progress6", "EditorIcons"), + get_theme_icon("Progress7", "EditorIcons"), + get_theme_icon("Progress8", "EditorIcons"), + get_theme_icon("Progress9", "EditorIcons"), + ] + for idx in icons.size(): + _tween.tween_property(self, "icon", icons[idx], 0 if idx == 0 else 1) + _tween.set_loops() + else: + icon = null \ No newline at end of file diff --git a/godot/circles/addons/rivet/devtools/dock/elements/loading_button.tscn b/godot/circles/addons/rivet/devtools/dock/elements/loading_button.tscn new file mode 100644 index 0000000..ede7093 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/elements/loading_button.tscn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cedd17ddf60aca4995abef9cc739c86b6b1972b98abcca36ac8f187e093ef862 +size 383 diff --git a/godot/circles/addons/rivet/devtools/dock/elements/logo_container.gd b/godot/circles/addons/rivet/devtools/dock/elements/logo_container.gd new file mode 100644 index 0000000..d253415 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/elements/logo_container.gd @@ -0,0 +1,9 @@ +@tool extends HBoxContainer + +@onready var logo: TextureRect = %Logo +var logo_dark = preload("../../../images/icon-text-black.svg") +var logo_light = preload("../../../images/icon-text-white.svg") + +func _ready() -> void: + var is_dark = get_theme_color("font_color", "Editor").get_luminance() < 0.5 + logo.texture = logo_dark if is_dark else logo_light diff --git a/godot/circles/addons/rivet/devtools/dock/elements/logo_container.tscn b/godot/circles/addons/rivet/devtools/dock/elements/logo_container.tscn new file mode 100644 index 0000000..1c5b34e --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/elements/logo_container.tscn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40ed8e33ecc4ec11b5260a0d1c23e442d1fe0c6dbd9d834e878242b59f47166a +size 671 diff --git a/godot/circles/addons/rivet/devtools/dock/elements/namespace_menu_button.gd b/godot/circles/addons/rivet/devtools/dock/elements/namespace_menu_button.gd new file mode 100644 index 0000000..256ac6a --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/elements/namespace_menu_button.gd @@ -0,0 +1,31 @@ +@tool extends OptionButton +## A control that displays a list of namespaces and allows the user to select one. + +@export var current_value: Dictionary + +var namespaces: Array: + get: return RivetPluginBridge.instance.game_namespaces + +func _ready(): + if RivetPluginBridge.is_part_of_edited_scene(self): + return + disabled = true + _update_menu_button(namespaces) + item_selected.connect(_on_item_selected) + RivetPluginBridge.instance.bootstrapped.connect(_on_plugin_bootstrapped) + +func _update_menu_button(value: Array) -> void: + clear() + for i in value.size(): + add_item("%s (v%s)" % [namespaces[i].display_name, namespaces[i].version.display_name], i) + +func _on_item_selected(idx: int): + _select_menu_item(idx) + +func _select_menu_item(idx: int) -> void: + current_value = namespaces[idx] + +func _on_plugin_bootstrapped() -> void: + disabled = false + _update_menu_button(namespaces) + _select_menu_item(0) \ No newline at end of file diff --git a/godot/circles/addons/rivet/devtools/dock/elements/namespace_menu_button.tscn b/godot/circles/addons/rivet/devtools/dock/elements/namespace_menu_button.tscn new file mode 100644 index 0000000..d1ce660 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/elements/namespace_menu_button.tscn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a058e1f9c5c18dcdc8bd06d84477bb270ef8700fd981d45e8750f882a8f2124 +size 375 diff --git a/godot/circles/addons/rivet/devtools/dock/installer.gd b/godot/circles/addons/rivet/devtools/dock/installer.gd new file mode 100644 index 0000000..7c13b86 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/installer.gd @@ -0,0 +1,37 @@ +@tool extends Control + +@onready var InstallButton: Button = %InstallButton +@onready var InstallDialog: AcceptDialog = %InstallDialog +@onready var InstallLabel: RichTextLabel = %InstallLabel + +func prepare() -> void: + InstallLabel.add_theme_font_override(&"mono_font", get_theme_font(&"output_source_mono", &"EditorFonts")) + InstallLabel.add_theme_font_override(&"bold_font", get_theme_font(&"bold", &"EditorFonts")) + InstallLabel.add_theme_stylebox_override(&"normal", get_theme_stylebox(&"bg", &"AssetLib")) + + InstallLabel.text = InstallLabel.text.replace(&"%%version%%", RivetPluginBridge.get_plugin().cli.REQUIRED_RIVET_CLI_VERSION).replace(&"%%bin_dir%%", RivetPluginBridge.get_plugin().cli.get_bin_dir()) + InstallButton.loading = true + var error = await RivetPluginBridge.get_plugin().cli.check_existence() + if error: + InstallButton.loading = false + return + owner.change_current_screen(owner.Screen.Login) + +func _ready() -> void: + InstallButton.pressed.connect(_on_install_button_pressed) + +func _on_install_button_pressed() -> void: + InstallButton.loading = true + var result = await RivetPluginBridge.get_plugin().cli.install() + if result.exit_code == 0: + var error = await RivetPluginBridge.get_plugin().cli.check_existence() + if not error: + InstallDialog.title = &"Success!" + InstallDialog.dialog_text = &"Rivet installed successfully!\nInstalled Rivet %s in %s" % [RivetPluginBridge.get_plugin().cli.REQUIRED_RIVET_CLI_VERSION, RivetPluginBridge.get_plugin().cli.get_bin_dir()] + InstallDialog.popup_centered() + owner.change_current_screen(owner.Screen.Login) + return + InstallDialog.title = &"Error!" + InstallDialog.dialog_text = &"Rivet installation failed! Please try again.\n\n%s" % result.output + InstallDialog.popup_centered() + InstallButton.loading = false \ No newline at end of file diff --git a/godot/circles/addons/rivet/devtools/dock/installer.tscn b/godot/circles/addons/rivet/devtools/dock/installer.tscn new file mode 100644 index 0000000..1fd15ee --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/installer.tscn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1aa7a2556124445ad68f63b39037bfae4d557c1ff2f2e7ebc61e837499c08c40 +size 2039 diff --git a/godot/circles/addons/rivet/devtools/dock/loading.gd b/godot/circles/addons/rivet/devtools/dock/loading.gd new file mode 100644 index 0000000..ab2136e --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/loading.gd @@ -0,0 +1,9 @@ +extends VBoxContainer + + +func _ready() -> void: + %CancelButton.pressed.connect(_on_cancel_button_pressed) + +func _on_cancel_button_pressed() -> void: + # TODO(forest): cancel cli command + owner.change_current_screen(owner.Screen.Login) diff --git a/godot/circles/addons/rivet/devtools/dock/loading.tscn b/godot/circles/addons/rivet/devtools/dock/loading.tscn new file mode 100644 index 0000000..b088e51 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/loading.tscn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc2f108bb4344679e1d34c41b96af0d4293178de20dad872beb51a9db4a8de71 +size 1338 diff --git a/godot/circles/addons/rivet/devtools/dock/login.gd b/godot/circles/addons/rivet/devtools/dock/login.gd new file mode 100644 index 0000000..7a29fa4 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/login.gd @@ -0,0 +1,62 @@ +@tool extends Control +## A button that logs the user in to the Rivet using Rivet CLI. + +@onready var log_in_button: Button = %LogInButton +@onready var api_endpoint_line_edit: LineEdit = %ApiEndpointLineEdit +@onready var advanced_options_button: Button = %AdvancedOptionsButton +@onready var api_endpoint_field: Control = %ApiEndpointField + +func prepare() -> void: + var result = await RivetPluginBridge.get_plugin().cli.run_command([ + "sidekick", + "check-login-state", + ]) + if result.exit_code == result.ExitCode.SUCCESS and "Ok" in result.output: + owner.change_current_screen(owner.Screen.Settings) + return + +func _ready(): + log_in_button.pressed.connect(_on_button_pressed) + advanced_options_button.pressed.connect(_on_advanced_options_button_pressed) + advanced_options_button.icon = get_theme_icon("arrow", "OptionButton") + +func _on_button_pressed() -> void: + log_in_button.disabled = true + var api_address = api_endpoint_line_edit.text + var result = await RivetPluginBridge.get_plugin().cli.run_command([ + "--api-endpoint", + api_address, + "sidekick", + "get-link", + ]) + if result.exit_code != result.ExitCode.SUCCESS or !("Ok" in result.output): + RivetPluginBridge.display_cli_error(self, result) + log_in_button.disabled = false + return + var data: Dictionary = result.output["Ok"] + + # Now that we have the link, open it in the user's browser + OS.shell_open(data["device_link_url"]) + + owner.change_current_screen(owner.Screen.Loading) + + # Long-poll the Rivet API until the user has logged in + result = await RivetPluginBridge.get_plugin().cli.run_command([ + "--api-endpoint", + api_address, + "sidekick", + "wait-for-login", + "--device-link-token", + data["device_link_token"], + ]) + + if result.exit_code != result.ExitCode.SUCCESS or !("Ok" in result.output): + RivetPluginBridge.display_cli_error(self, result) + log_in_button.disabled = false + return + + log_in_button.disabled = false + owner.change_current_screen(owner.Screen.Settings) + +func _on_advanced_options_button_pressed(): + api_endpoint_field.visible = !api_endpoint_field.visible diff --git a/godot/circles/addons/rivet/devtools/dock/login.tscn b/godot/circles/addons/rivet/devtools/dock/login.tscn new file mode 100644 index 0000000..6eec717 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/login.tscn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50c720e4c5c0bafc7ada952ad8eae6a5aee0bc9e86b223a94f70861b142ba801 +size 4571 diff --git a/godot/circles/addons/rivet/devtools/dock/playtest_tab.gd b/godot/circles/addons/rivet/devtools/dock/playtest_tab.gd new file mode 100644 index 0000000..f00f5d4 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/playtest_tab.gd @@ -0,0 +1,113 @@ +@tool extends MarginContainer + +const ButtonsBar = preload("elements/buttons_bar.gd") + +@onready var namespace_description: RichTextLabel = %NamespaceDescription +@onready var buttons_bar: ButtonsBar = %ButtonsBar +@onready var warning: RichTextLabel = %WarningLabel +@onready var error: RichTextLabel = %ErrorLabel +@onready var deploy_button: Button = %DeployButton +@onready var namespace_selector = %AuthNamespaceSelector + +func _ready() -> void: + if get_tree().edited_scene_root == self: + return # This is the scene opened in the editor! + namespace_description.add_theme_font_override(&"mono_font", get_theme_font(&"output_source_mono", &"EditorFonts")) + namespace_description.add_theme_font_override(&"bold_font", get_theme_font(&"bold", &"EditorFonts")) + namespace_description.add_theme_stylebox_override(&"normal", get_theme_stylebox(&"bg", &"AssetLib")) + namespace_description.meta_clicked.connect(func(meta): OS.shell_open(str(meta))) + + warning.add_theme_color_override(&"default_color", get_theme_color(&"warning_color", &"Editor")) + warning.add_theme_stylebox_override(&"normal", get_theme_stylebox(&"bg", &"AssetLib")) + var warning_text = warning.text + warning.text = "" + warning.add_image(get_theme_icon("StatusWarning", "EditorIcons")) + warning.add_text(warning_text) + + error.add_theme_color_override(&"default_color", get_theme_color("error_color", "Editor")) + error.add_theme_stylebox_override(&"normal", get_theme_stylebox(&"bg", &"AssetLib")) + var error_text = error.text + error.text = "" + error.add_image(get_theme_icon("StatusError", "EditorIcons")) + error.add_text(error_text) + + warning.visible = false + error.visible = false + deploy_button.visible = false + + RivetPluginBridge.instance.bootstrapped.connect(_on_bootstrapped) + namespace_selector.item_selected.connect(_on_namespace_selector_item_selected) + deploy_button.pressed.connect(_on_deploy_button_pressed) + buttons_bar.selected.connect(_on_buttons_bar_selected) + +func _on_namespace_selector_item_selected(id: int) -> void: + _update_warnings() + +func _on_buttons_bar_selected() -> void: + _update_warnings() + +func _on_bootstrapped() -> void: + _update_warnings() + +func _update_warnings() -> void: + var is_local_machine = buttons_bar.current == 0 + var is_online_server = buttons_bar.current == 1 + var current_namespace = namespace_selector.current_value + + # Local machine + if is_local_machine: + warning.visible = false + error.visible = false + deploy_button.visible = false + _generate_dev_auth_token(current_namespace) + return + + # Online server + if is_online_server: + # It means that user hasn't deployed anything to this namespace yet + if current_namespace.version.display_name == "0.0.1": + warning.visible = false + error.visible = true + deploy_button.visible = true + else: + warning.visible = true + error.visible = false + deploy_button.visible = false + _generate_public_auth_token(current_namespace) + return + +func _all_actions_set_disabled(disabled: bool) -> void: + namespace_selector.disabled = disabled + buttons_bar.disabled = disabled + +func _generate_dev_auth_token(ns) -> void: + _actions_disabled_while(func(): + var result = await RivetPluginBridge.get_plugin().cli.run_command(["sidekick", "get-namespace-development-token", "--namespace", ns.name_id]) + if result.exit_code != 0 or !("Ok" in result.output): + RivetPluginBridge.display_cli_error(self, result) + return + + RivetPluginBridge.get_plugin().namespace_token = result.output["Ok"]["token"] + RivetPluginBridge.instance.save_configuration() + ) + +func _generate_public_auth_token(ns) -> void: + _actions_disabled_while(func(): + var result = await RivetPluginBridge.get_plugin().cli.run_command(["sidekick", "get-namespace-public-token", "--namespace", ns.name_id]) + if result.exit_code != 0 or !("Ok" in result.output): + RivetPluginBridge.display_cli_error(self, result) + return + + RivetPluginBridge.get_plugin().namespace_token = result.output["Ok"]["token"] + RivetPluginBridge.instance.save_configuration() + ) + +func _actions_disabled_while(fn: Callable) -> void: + _all_actions_set_disabled(true) + await fn.call() + _all_actions_set_disabled(false) + +func _on_deploy_button_pressed() -> void: + owner.change_tab(1) + owner.deploy_tab.namespace_selector.current_value = namespace_selector.current_value + owner.deploy_tab.namespace_selector.selected = namespace_selector.selected diff --git a/godot/circles/addons/rivet/devtools/dock/playtest_tab.tscn b/godot/circles/addons/rivet/devtools/dock/playtest_tab.tscn new file mode 100644 index 0000000..ca1e911 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/playtest_tab.tscn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35d858c11865b18cf58f396cf6cb29496b06c631ebb369ed96686246576c06af +size 2779 diff --git a/godot/circles/addons/rivet/devtools/dock/settings.gd b/godot/circles/addons/rivet/devtools/dock/settings.gd new file mode 100644 index 0000000..3ff0aa1 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/settings.gd @@ -0,0 +1,15 @@ +@tool extends Control +## Settings screens allow you to configure and deploy your game. + +@onready var errorDialog: AcceptDialog = %ErrorDialog +@onready var buttons_bar: HBoxContainer = %ButtonsBar +@onready var deploy_tab = %Deploy + +func prepare(): + var error = await RivetPluginBridge.instance.bootstrap() + if error: + errorDialog.popup_centered() + return + +func change_tab(tab: int): + buttons_bar.set_current_button(tab) \ No newline at end of file diff --git a/godot/circles/addons/rivet/devtools/dock/settings.tscn b/godot/circles/addons/rivet/devtools/dock/settings.tscn new file mode 100644 index 0000000..e1780a8 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/settings.tscn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:630988a52df20551ce8ab7ad69175422ca0dced601e1b3c0ccc46c745ff240aa +size 2802 diff --git a/godot/circles/addons/rivet/devtools/dock/settings_tab.gd b/godot/circles/addons/rivet/devtools/dock/settings_tab.gd new file mode 100644 index 0000000..cd0660d --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/settings_tab.gd @@ -0,0 +1,24 @@ +@tool extends Control + +@onready var unlink_game_button: Button = %UnlinkGameButton + + +func _ready() -> void: + unlink_game_button.pressed.connect(_on_unlink_game_button_pressed) + + +func _on_unlink_game_button_pressed() -> void: + unlink_game_button.disabled = true + + var result = await RivetPluginBridge.get_plugin().cli.run_command([ + "unlink" + ]) + + if result.exit_code != result.ExitCode.SUCCESS: + RivetPluginBridge.display_cli_error(self, result) + unlink_game_button.disabled = false + return + + unlink_game_button.disabled = false + owner.owner.reload() + owner.owner.change_current_screen(owner.owner.Screen.Login) \ No newline at end of file diff --git a/godot/circles/addons/rivet/devtools/dock/settings_tab.tscn b/godot/circles/addons/rivet/devtools/dock/settings_tab.tscn new file mode 100644 index 0000000..8342390 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/dock/settings_tab.tscn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55d3f65bac202d3f0022fe5357cab88c4946d6b6abc953edc9f1f834b5925af0 +size 390 diff --git a/godot/circles/addons/rivet/devtools/rivet_cli.gd b/godot/circles/addons/rivet/devtools/rivet_cli.gd new file mode 100644 index 0000000..3be050c --- /dev/null +++ b/godot/circles/addons/rivet/devtools/rivet_cli.gd @@ -0,0 +1,76 @@ +extends RefCounted +## Wrapper aroudn the Rivet CLI, allowing you to run it from GDScript in non-blocking way, and get the output. +## +## @experimental + +const REQUIRED_RIVET_CLI_VERSION = "v1.0.0" + +const _RivetEditorSettings = preload("rivet_editor_settings.gd") +const _RivetThread = preload("rivet_thread.gd") +const _RivetCliOutput = preload("rivet_cli_output.gd") + +func check_existence() -> Error: + var editor_rivet_path = _RivetEditorSettings.get_setting(_RivetEditorSettings.RIVET_CLI_PATH_SETTING.name) + if not editor_rivet_path or editor_rivet_path.is_empty(): + return FAILED + var result: _RivetCliOutput = await run_command(["sidekick", "get-cli-version"]) + if result.exit_code != 0 or !("Ok" in result.output): + return FAILED + var cli_version = result.output["Ok"].version + if cli_version != REQUIRED_RIVET_CLI_VERSION: + return FAILED + return OK + +func run_command(args: PackedStringArray) -> _RivetCliOutput: + var thread: _RivetThread = _RivetThread.new(_run.bind(args)) + return await thread.wait_to_finish() + +func get_bin_dir() -> String: + var home_path: String = OS.get_environment("HOME") + return home_path.path_join(".rivet").path_join(REQUIRED_RIVET_CLI_VERSION).path_join("bin") + +func get_cli_path() -> String: + var cli_path = _RivetEditorSettings.get_setting(_RivetEditorSettings.RIVET_CLI_PATH_SETTING.name) + if cli_path and !cli_path.is_empty(): + return cli_path + return get_bin_dir().path_join("rivet.exe" if OS.get_name() == "Windows" else "rivet") + +func install() -> _RivetCliOutput: + var thread: _RivetThread = _RivetThread.new(_install) + var result = await thread.wait_to_finish() + if result.exit_code == 0: + _RivetEditorSettings.set_setting_value(_RivetEditorSettings.RIVET_CLI_PATH_SETTING.name, get_bin_dir()) + return result + + +## region Internal functions + +## Runs Rivet CLI with given arguments. +func _run(args: PackedStringArray) -> _RivetCliOutput: + var output = [] + RivetPluginBridge.log(["Running Rivet CLI: ", "%s %s" % [get_cli_path(), " ".join(args)]]) + var code: int = OS.execute(get_cli_path(), args, output, true) + + return _RivetCliOutput.new(code, output) + +func _install() -> _RivetCliOutput: + var output = [] + var code: int + var bin_dir: String = get_bin_dir() + + OS.set_environment("RIVET_CLI_VERSION", REQUIRED_RIVET_CLI_VERSION) + OS.set_environment("BIN_DIR", bin_dir) + + # Double quotes issue: https://github.com/godotengine/godot/issues/37291#issuecomment-603821838 + if OS.get_name() == "Windows": + var args = ["-Commandi", "\"'iwr https://raw.githubusercontent.com/rivet-gg/cli/$env:RIVET_CLI_VERSION/install/windows.ps1 -useb | iex'\""] + code = OS.execute("powershell.exe", args, output, true, true) + else: + #var args = ["-c", "\"'curl -fsSL https://raw.githubusercontent.com/rivet-gg/cli/${RIVET_CLI_VERSION}/install/unix.sh | sh''\""] + var args = ["-c", "\"'curl -fsSL https://raw.githubusercontent.com/rivet-gg/cli/ac57796861d195230fa043e12c5f9fe1921f467f/install/unix.sh | sh'\""] + code = OS.execute("/bin/sh", args, output, true, true) + return _RivetCliOutput.new(code, output) + + + +## endregion \ No newline at end of file diff --git a/godot/circles/addons/rivet/devtools/rivet_cli_output.gd b/godot/circles/addons/rivet/devtools/rivet_cli_output.gd new file mode 100644 index 0000000..889499e --- /dev/null +++ b/godot/circles/addons/rivet/devtools/rivet_cli_output.gd @@ -0,0 +1,38 @@ +extends RefCounted +## It's a wrapper for the output of the command line tools + +var exit_code: ExitCode +var output: Dictionary + +## The exit code of the command line tool +enum ExitCode { + SUCCESS = 0 + # TODO: fill with the rest of the exit codes +} + +func _init(exit_code: int, internal_output: Array) -> void: + self.exit_code = exit_code + + if internal_output and not internal_output.is_empty(): + _parse_output(internal_output) + +func _parse_output(internal_output: Array) -> void: + var lines_with_json = internal_output.filter( + func (line: String): + return line.find("{") != -1 + ) + + if lines_with_json.is_empty(): + print("No JSON output found") + return + + var line_with_json: String = lines_with_json.front() + # Parse the output as JSON + var json: JSON = JSON.new() + var error: Error = json.parse(line_with_json) + + if error == OK: + self.output = json.data + else: + # If the output is not JSON, throw an error + RivetPluginBridge.error("Invalid response from the command line tool: " + str(error)) diff --git a/godot/circles/addons/rivet/devtools/rivet_editor_settings.gd b/godot/circles/addons/rivet/devtools/rivet_editor_settings.gd new file mode 100644 index 0000000..1517395 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/rivet_editor_settings.gd @@ -0,0 +1,28 @@ +const RIVET_CLI_PATH_SETTING = { + "name": "rivet/cli_executable_path", + "type": TYPE_STRING, +} +const RIVET_DEBUG_SETTING ={ + "name": "rivet/debug", + "type": TYPE_BOOL, +} + +## Returns the path to the Rivet CLI executable stored in the editor settings. +static func set_defaults(settings: EditorSettings = EditorInterface.get_editor_settings()) -> void: + set_default_setting_value(RIVET_CLI_PATH_SETTING["name"], "", settings) + settings.add_property_info(RIVET_CLI_PATH_SETTING) + set_default_setting_value(RIVET_DEBUG_SETTING["name"], false, settings) + settings.add_property_info(RIVET_DEBUG_SETTING) + +## Sets the path to the Rivet CLI executable in the editor settings, if it is not already set. +static func set_default_setting_value(name: String, default_value: Variant, settings: EditorSettings = EditorInterface.get_editor_settings()) -> void: + var existing_value = settings.get_setting(name) + settings.set_initial_value(name, default_value, false) + settings.set_setting(name, existing_value if existing_value else default_value) + +static func set_setting_value(name: String, value: Variant, settings: EditorSettings = EditorInterface.get_editor_settings()) -> void: + settings.set_setting(name, value) + +## Returns the path to the Rivet CLI executable stored in the editor settings. +static func get_setting(name: String, settings: EditorSettings = EditorInterface.get_editor_settings()) -> Variant: + return settings.get_setting(name) diff --git a/godot/circles/addons/rivet/devtools/rivet_export_plugin.gd b/godot/circles/addons/rivet/devtools/rivet_export_plugin.gd new file mode 100644 index 0000000..bbb7419 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/rivet_export_plugin.gd @@ -0,0 +1,29 @@ +@tool +extends EditorExportPlugin + +var has_added_file: bool = false +var _plugin_name = "RivetEditorPlugin" + +func _supports_platform(platform): + return true + +func _get_name(): + return _plugin_name + +func _export_begin(features: PackedStringArray, is_debug: bool, path: String, flags: int) -> void: + if not FileAccess.file_exists(RivetPluginBridge.RIVET_CONFIGURATION_FILE_PATH): + push_warning("Rivet plugin not configured. Please configure it using plugin interface.") + return + + var configuration_file = FileAccess.open(RivetPluginBridge.RIVET_CONFIGURATION_FILE_PATH, FileAccess.READ) + var source = configuration_file.get_as_text() + var script = GDScript.new() + script.source_code = source + var error: Error = ResourceSaver.save(script, RivetPluginBridge.RIVET_DEPLOYED_CONFIGURATION_FILE_PATH) + if not error: + has_added_file = true + + +func _export_end() -> void: + if has_added_file: + DirAccess.remove_absolute(RivetPluginBridge.RIVET_DEPLOYED_CONFIGURATION_FILE_PATH) \ No newline at end of file diff --git a/godot/circles/addons/rivet/devtools/rivet_plugin_bridge.gd b/godot/circles/addons/rivet/devtools/rivet_plugin_bridge.gd new file mode 100644 index 0000000..018e3b3 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/rivet_plugin_bridge.gd @@ -0,0 +1,128 @@ +@tool class_name RivetPluginBridge +## Scaffolding for the plugin to be used in the editor, this is not meant to be +## used in the game. It's a way to get the plugin instance from the engine's +## perspective. +## +## @experimental + +signal bootstrapped + +const RIVET_CONFIGURATION_PATH: String = "res://.rivet" +const RIVET_CONFIGURATION_FILE_PATH: String = "res://.rivet/config.gd" +const RIVET_DEPLOYED_CONFIGURATION_FILE_PATH: String = "res://.rivet_config.gd" +const SCRIPT_TEMPLATE: String = """ +extends RefCounted +const api_endpoint: String = \"{api_endpoint}\" +const namespace_token: String = \"{namespace_token}\" +const cloud_token: String = \"{cloud_token}\" +const game_id: String = \"{game_id}\" +""" +const _global := preload("../rivet_global.gd") +const _RivetEditorSettings = preload("./rivet_editor_settings.gd") + +static var game_namespaces: Array + +static var instance = RivetPluginBridge.new() + +static func _find_plugin(): + var tree: SceneTree = Engine.get_main_loop() + return tree.get_root().get_child(0).get_node_or_null("RivetPlugin") + +static func display_cli_error(node: Node, cli_output) -> AcceptDialog: + var error = cli_output.output["Err"].c_unescape() if "Err" in cli_output.output else "\n".join(cli_output.formatted_output) + var alert = AcceptDialog.new() + alert.title = "Error!" + alert.dialog_text = error + alert.dialog_autowrap = true + alert.close_requested.connect(func(): alert.queue_free() ) + node.add_child(alert) + alert.popup_centered_ratio(0.4) + return alert + +# https://github.com/godotengine/godot-proposals/issues/900#issuecomment-1812881718 +static func is_part_of_edited_scene(node: Node): + return Engine.is_editor_hint() && node.is_inside_tree() && node.get_tree().get_edited_scene_root() && (node.get_tree().get_edited_scene_root() == node || node.get_tree().get_edited_scene_root().is_ancestor_of(node)) + +## Autoload is not available for editor interfaces, we add a scoffolding to get +## the instance of the plugin from the engine's perspective +## @experimental +static func get_plugin() -> _global: + var plugin = _find_plugin() + if plugin: + return plugin.global + return null + +static func log(args): + if _RivetEditorSettings.get_setting(_RivetEditorSettings.RIVET_DEBUG_SETTING.name): + print("[Rivet] ", args) + +static func warning(args): + push_warning("[Rivet] ", args) + +static func error(args): + push_error("[Rivet] ", args) + +func save_configuration(): + DirAccess.make_dir_recursive_absolute(RIVET_CONFIGURATION_PATH) + + var gd_ignore_path = RIVET_CONFIGURATION_PATH.path_join(".gdignore") + if not FileAccess.file_exists(gd_ignore_path): + var gd_ignore = FileAccess.open(gd_ignore_path, FileAccess.WRITE) + gd_ignore.store_string("") + + var git_ignore_path = RIVET_CONFIGURATION_PATH.path_join(".gitignore") + if not FileAccess.file_exists(git_ignore_path): + var git_ignore = FileAccess.open(git_ignore_path, FileAccess.WRITE) + git_ignore.store_string("*") + + var plg = get_plugin() + var script: GDScript = GDScript.new() + script.source_code = SCRIPT_TEMPLATE.format({"api_endpoint": plg.api_endpoint, "namespace_token": plg.namespace_token, "cloud_token": plg.cloud_token, "game_id": plg.game_id}) + var err: Error = ResourceSaver.save(script, RIVET_CONFIGURATION_FILE_PATH) + if err: + push_warning("Error saving Rivet data: %s" % err) + +func bootstrap() -> Error: + var plugin = get_plugin() + if not plugin: + return FAILED + + var result = await get_plugin().cli.run_command([ + "sidekick", + "get-bootstrap-data", + ]) + + if result.exit_code != 0 or !("Ok" in result.output): + return FAILED + + get_plugin().api_endpoint = result.output["Ok"].api_endpoint + get_plugin().cloud_token = result.output["Ok"].token + get_plugin().game_id = result.output["Ok"].game_id + + save_configuration() + + var fetch_result = await _fetch_plugin_data() + if fetch_result == OK: + emit_signal("bootstrapped") + return fetch_result + +func _fetch_plugin_data() -> Error: + var response = await get_plugin().GET("/cloud/games/%s" % get_plugin().game_id).wait_completed() + # response.body: + # game.namespaces = {namespace_id, version_id, display_name}[] + # game.versions = {version_id, display_name}[] + if response.response_code != HTTPClient.ResponseCode.RESPONSE_OK: + return FAILED + + var namespaces = response.body.game.namespaces + for space in namespaces: + var versions: Array = response.body.game.versions.filter( + func (version): return version.version_id == space.version_id + ) + if versions.is_empty(): + space["version"] = null + else: + space["version"] = versions[0] + + game_namespaces = namespaces + return OK diff --git a/godot/circles/addons/rivet/devtools/rivet_thread.gd b/godot/circles/addons/rivet/devtools/rivet_thread.gd new file mode 100644 index 0000000..2f181f0 --- /dev/null +++ b/godot/circles/addons/rivet/devtools/rivet_thread.gd @@ -0,0 +1,29 @@ +extends RefCounted +## A wrapper around Thread that allows you to wait for the thread to finish and get the result. +## +## @experimental + +signal finished(output: Variant) + +var _mutex: Mutex +var _thread: Thread + +## Result of the thread. +var output: Variant = null + +## Returns the output of the thread. +func wait_to_finish(): + await finished + return output + +func _init(fn: Callable) -> void: + _thread = Thread.new() + _mutex = Mutex.new() + _thread.start(func(): + var result = fn.call() + _mutex.lock() + output = result + call_deferred("emit_signal", "finished", result) + _mutex.unlock() + return result + ) diff --git a/godot/circles/addons/rivet/images/icon-circle.png.import b/godot/circles/addons/rivet/images/icon-circle.png.import new file mode 100644 index 0000000..29517e9 --- /dev/null +++ b/godot/circles/addons/rivet/images/icon-circle.png.import @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4fe33ce77fa52db4e84e161f67a04fb69f7333444a70be68009447d35295066 +size 787 diff --git a/godot/circles/addons/rivet/images/icon-text-black.svg b/godot/circles/addons/rivet/images/icon-text-black.svg new file mode 100644 index 0000000..14f0fe0 --- /dev/null +++ b/godot/circles/addons/rivet/images/icon-text-black.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/godot/circles/addons/rivet/images/icon-text-black.svg.import b/godot/circles/addons/rivet/images/icon-text-black.svg.import new file mode 100644 index 0000000..102d6ef --- /dev/null +++ b/godot/circles/addons/rivet/images/icon-text-black.svg.import @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe3c270c3395b9324d775d72ca82b26448c8f2e19174e47587c48ce359974955 +size 895 diff --git a/godot/circles/addons/rivet/images/icon-text-white.svg b/godot/circles/addons/rivet/images/icon-text-white.svg new file mode 100644 index 0000000..1807618 --- /dev/null +++ b/godot/circles/addons/rivet/images/icon-text-white.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/godot/circles/addons/rivet/images/icon-text-white.svg.import b/godot/circles/addons/rivet/images/icon-text-white.svg.import new file mode 100644 index 0000000..d91ef5a --- /dev/null +++ b/godot/circles/addons/rivet/images/icon-text-white.svg.import @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:684547448294b6e186953ce9a56b565fa4c941dc17fadf196923148964821f13 +size 896 diff --git a/godot/circles/addons/rivet/images/white-logo.svg.import b/godot/circles/addons/rivet/images/white-logo.svg.import new file mode 100644 index 0000000..be20926 --- /dev/null +++ b/godot/circles/addons/rivet/images/white-logo.svg.import @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e412fc328b9960853c19fe1998527052651f0db4a58f1964643b0cfefca4dd4d +size 881 diff --git a/godot/circles/addons/rivet/plugin.cfg b/godot/circles/addons/rivet/plugin.cfg new file mode 100644 index 0000000..79be32c --- /dev/null +++ b/godot/circles/addons/rivet/plugin.cfg @@ -0,0 +1,8 @@ +[plugin] + +name="Rivet API" +description="" +author="Rivet Gaming, Inc." +version="1.0.0-rc.1" +script="rivet.gd" + diff --git a/godot/circles/addons/rivet/rivet.gd b/godot/circles/addons/rivet/rivet.gd new file mode 100644 index 0000000..db0d56c --- /dev/null +++ b/godot/circles/addons/rivet/rivet.gd @@ -0,0 +1,58 @@ +@tool extends EditorPlugin +## Mainpoint for the Rivet editor plugin. + +# MARK: Plugin +const AUTO_LOAD_RIVET_CLIENT = "RivetClient" +const AUTO_LOAD_RIVET_HELPER = "RivetHelper" +const AUTO_LOAD_RIVET_GLOBAL = "Rivet" + +const _RivetEditorSettings := preload("devtools/rivet_editor_settings.gd") +const _RivetGlobal := preload("rivet_global.gd") +const _RivetCLI = preload("devtools/rivet_cli.gd") + +var _dock: Control +var _export_plugin: EditorExportPlugin +var cli: _RivetCLI = _RivetCLI.new() + +## The global singleton for the Rivet plugin, only available in the editor. +var global: _RivetGlobal + +func _init() -> void: + name = "RivetPlugin" + +func _enter_tree(): + # Add singleton + add_autoload_singleton(AUTO_LOAD_RIVET_CLIENT, "rivet_client.gd") + add_autoload_singleton(AUTO_LOAD_RIVET_HELPER, "rivet_helper.gd") + + add_autoload_singleton(AUTO_LOAD_RIVET_GLOBAL, "rivet_global.gd") + + global = _RivetGlobal.new() + global.cli = cli + + _dock = preload("devtools/dock/dock.tscn").instantiate() + _dock.add_child(global) + + # Add export plugin + _export_plugin = preload("devtools/rivet_export_plugin.gd").new() + add_export_plugin(_export_plugin) + + # Add dock + add_control_to_dock(DOCK_SLOT_LEFT_BR, _dock) + _RivetEditorSettings.set_defaults() + + +func _exit_tree(): + # Remove singleton + remove_autoload_singleton(AUTO_LOAD_RIVET_CLIENT) + remove_autoload_singleton(AUTO_LOAD_RIVET_HELPER) + remove_autoload_singleton(AUTO_LOAD_RIVET_GLOBAL) + + # Remove export plugin + remove_export_plugin(_export_plugin) + _export_plugin = null + + # Remove dock + remove_control_from_docks(_dock) + _dock.free() + diff --git a/godot/circles/addons/rivet/rivet.version.toml.tpl b/godot/circles/addons/rivet/rivet.version.toml.tpl new file mode 100644 index 0000000..0916519 --- /dev/null +++ b/godot/circles/addons/rivet/rivet.version.toml.tpl @@ -0,0 +1,52 @@ +# === Rivet Version Configuration === +# +# - More info: https://docs.rivet.gg/general/concepts/rivet-version-config +# - Reference: https://docs.rivet.gg/cloud/api/post-games-versions#body +# - Publish a new version with `rivet publish` +# + +# How the game lobbies run and how players connect to the game. +# +# https://docs.rivet.gg/matchmaker/introduction +[matchmaker] + # How many players can join a specific lobby. + # + # Read more about matchmaking: https://docs.rivet.gg/matchmaker/concepts/finding-lobby + max_players = 32 + + # The hardware to provide for lobbies. + # + # Available tiers: https://docs.rivet.gg/serverless-lobbies/concepts/available-tiers + tier = "basic-1d1" + +# Which regions the game should be available in. +# +# Available regions: https://docs.rivet.gg/serverless-lobbies/concepts/available-regions +[matchmaker.regions] + lnd-sfo = {} + lnd-fra = {} + +# Runtime configuration for the lobby's Docker container. +[matchmaker.docker] + # If you're unfamiliar with Docker, here's how to write your own + # Dockerfile: + # https://docker-curriculum.com/#dockerfile + dockerfile = "Dockerfile" + + # Which ports to allow players to connect to. Multiple ports can be defined + # with different protocols. + # + # How ports work: https://docs.rivet.gg/serverless-lobbies/concepts/ports + ports.default = { port = 10567, protocol = "udp" } + +# What game modes are avaiable. +# +# Properties like `max_players`, `tier`, `dockerfile`, `regions`, and more can +# be overriden for specific game modes. +[matchmaker.game_modes] + default = {} + +[kv] + +[identity] + diff --git a/godot/circles/addons/rivet/rivet_client.gd b/godot/circles/addons/rivet/rivet_client.gd new file mode 100644 index 0000000..207e094 --- /dev/null +++ b/godot/circles/addons/rivet/rivet_client.gd @@ -0,0 +1,89 @@ +## @deprecated +extends Node + +var base_url = "https://api.rivet.gg/v1" + +## @deprecated +func get_token(): + var token_env = OS.get_environment("RIVET_TOKEN") + assert(!token_env.is_empty(), "missing RIVET_TOKEN environment") + return token_env + +## @deprecated +func lobby_ready(body: Variant, on_success: Callable, on_fail: Callable): + _rivet_request_with_body("POST", "matchmaker", "/lobbies/ready", body, on_success, on_fail) + +## @deprecated +func find_lobby(body: Variant, on_success: Callable, on_fail: Callable): + _rivet_request_with_body("POST", "matchmaker", "/lobbies/find", body, on_success, on_fail) + +## @deprecated +func player_connected(body: Variant, on_success: Callable, on_fail: Callable): + _rivet_request_with_body("POST", "matchmaker", "/players/connected", body, on_success, on_fail) + +## @deprecated +func player_disconnected(body: Variant, on_success: Callable, on_fail: Callable): + _rivet_request_with_body("POST", "matchmaker", "/players/disconnected", body, on_success, on_fail) + +func _build_url(service, path) -> String: + return base_url.replace("://", "://" + service + ".") + path + +func _build_headers() -> PackedStringArray: + return [ + "Authorization: Bearer " + get_token(), + ] + +## @deprecated +func _rivet_request(method: String, service: String, path: String, on_success: Callable, on_fail: Callable): + var url = _build_url(service, path) + RivetHelper.rivet_print("%s %s" % [method, url]) + + var http_request = HTTPRequest.new() + add_child(http_request) + http_request.request_completed.connect(self._http_request_completed.bind(on_success, on_fail)) + + var error = http_request.request(url, _build_headers()) + if error != OK: + push_error("An error occurred in the HTTP request.") + if on_fail != null: + on_fail.call("Request failed to send: %s" % error) + +## @deprecated +func _rivet_request_with_body(method: String, service: String, path: String, body: Variant, on_success: Callable, on_fail: Callable): + var url = _build_url(service, path) + RivetHelper.rivet_print("%s %s: %s" % [method, url, body]) + + var http_request = HTTPRequest.new() + add_child(http_request) + http_request.request_completed.connect(self._http_request_completed.bind(on_success, on_fail)) + + var body_json = JSON.stringify(body) + var error = http_request.request(url, _build_headers(), HTTPClient.METHOD_POST, body_json) + if error != OK: + push_error("An error occurred in the HTTP request.") + if on_fail != null: + on_fail.call("Request failed to send: %s" % error) + +## @deprecated +func _http_request_completed(result, response_code, _headers, body, on_success: Callable, on_fail: Callable): + if result != HTTPRequest.RESULT_SUCCESS: + push_error("Request error ", result) + if on_fail != null: + on_fail.call("Request error: %s" % result) + return + + RivetHelper.rivet_print("%s: %s" % [response_code, body.get_string_from_utf8()]) + + if response_code != 200: + push_error("Request failed ", response_code, " ", body.get_string_from_utf8()) + if on_fail != null: + on_fail.call("Request failed (%s): %s" % [response_code, body.get_string_from_utf8()]) + return + + var json = JSON.new() + json.parse(body.get_string_from_utf8()) + var response = json.get_data() + + if on_success != null: + on_success.call(response) + diff --git a/godot/circles/addons/rivet/rivet_global.gd b/godot/circles/addons/rivet/rivet_global.gd new file mode 100644 index 0000000..cfa63c2 --- /dev/null +++ b/godot/circles/addons/rivet/rivet_global.gd @@ -0,0 +1,36 @@ + +extends Node +## Rivet [/br] +## Mainpoint of the Rivet plugin. +## It includes an easy access to APIs, helpers and tools. [/br] +## @tutorial: https://rivet.gg/learn/godot +## @experimental + +const _api = preload("api/rivet_api.gd") + +const ApiResponse = preload("api/rivet_response.gd") +const ApiRequest = preload("api/rivet_request.gd") + +const _Packages = preload("api/rivet_packages.gd") + +var cloud_token: String +var namespace_token: String +var game_id: String +var api_endpoint: String + +var matchmaker: _Packages.Matchmaker = _Packages.Matchmaker.new() + +# This variable is only accessible from editor's scripts, please do not use it in your game. +var cli + +## @experimental +func POST(path: String, body: Dictionary) -> _api.RivetRequest: + return _api.POST(self, path, body) + +## @experimental +func GET(path: String, body: Dictionary = {}) -> _api.RivetRequest: + return _api.GET(self, path, body) + +## @experimental +func PUT(path: String, body: Dictionary = {}) -> _api.RivetRequest: + return _api.PUT(self, path, body) diff --git a/godot/circles/addons/rivet/rivet_helper.gd b/godot/circles/addons/rivet/rivet_helper.gd new file mode 100644 index 0000000..c0b88b3 --- /dev/null +++ b/godot/circles/addons/rivet/rivet_helper.gd @@ -0,0 +1,115 @@ +extends Node + +## Triggered if running a dedicated server. +signal start_server() + +## Triggered if running a client. +signal start_client() + +var multiplayer_setup = false + +## All player tokens for players that have authenticated. +## +## Server only +var player_tokens = {} + +## The player token for this client that will be sent on the next +## authentication. +## +## Client only +var player_token = null + + +## Determines if running as a dedicated server. +func is_dedicated_server() -> bool: + return OS.get_cmdline_user_args().has("--server") + + +## Sets up the authentication hooks on SceneMultiplayer. +func setup_multiplayer(): + RivetHelper._assert(!multiplayer_setup, "RivetHelper.setup_multiplayer already called") + multiplayer_setup = true + + var scene_multiplayer = multiplayer as SceneMultiplayer + + scene_multiplayer.auth_callback = _auth_callback + scene_multiplayer.auth_timeout = 15.0 + + scene_multiplayer.peer_authenticating.connect(self._player_authenticating) + scene_multiplayer.peer_authentication_failed.connect(self._player_authentication_failed) + + scene_multiplayer.peer_disconnected.connect(self._player_disconnected) + + if is_dedicated_server(): + rivet_print("Starting server") + start_server.emit() + else: + rivet_print("Starting client") + start_client.emit() + + +## Sets the player token for the next authentication challenge. +func set_player_token(_player_token: String): + RivetHelper._assert(multiplayer_setup, "RivetHelper.setup_multiplayer has not been called") + RivetHelper._assert(!is_dedicated_server(), "cannot called RivetHelper.set_player_token on server") + player_token = _player_token + + +# MARK: Authentication +func _auth_callback(id: int, buf: PackedByteArray): + if multiplayer.is_server(): + # Authenticate the client if connecting to server + + var json = JSON.new() + json.parse(buf.get_string_from_utf8()) + var data = json.get_data() + + rivet_print("Player authenticating %s: %s" % [id, data]) + player_tokens[id] = data.player_token + + var response = await Rivet.matchmaker.players.connected({ + "player_token": data.player_token + }) + + if response.result == OK: + rivet_print("Player authenticated for %s" % id) + (multiplayer as SceneMultiplayer).complete_auth(id) + else: + rivet_print("Player authentiation failed for %s: %s" % [id, response.body]) + (multiplayer as SceneMultiplayer).disconnect_peer(id) + else: + # Auto-approve if not a server + (multiplayer as SceneMultiplayer).complete_auth(id) + +func _player_authenticating(id): + rivet_print("Authenticating %s" % id) + var body = JSON.stringify({ "player_token": player_token }) + (multiplayer as SceneMultiplayer).send_auth(id, body.to_utf8_buffer()) + + +func _player_authentication_failed(id): + rivet_print("Authentication failed for %s" % id) + multiplayer.set_multiplayer_peer(null) + +func _player_disconnected(id): + if multiplayer.is_server(): + var player_token = player_tokens.get(id) + player_tokens.erase(id) + rivet_print("Removing player %s" % player_token) + + var response = await Rivet.matchmaker.players.disconnected({ + "player_token": player_token + }) + + +func rivet_print(message: String): + print("[Rivet] %s" % message) + +func _assert(condition: bool, message: String = "Assertion failed"): + if not condition: + # For now, we won't crash the game if an assertion fails. There are a + # few reasons for this. See discussion: + # https://app.graphite.dev/github/pr/rivet-gg/plugin-godot/33/Assert-fix# + # OS.crash(message) + + rivet_print(message) diff --git a/godot/circles/game.tscn b/godot/circles/game.tscn new file mode 100644 index 0000000..dc5da0c --- /dev/null +++ b/godot/circles/game.tscn @@ -0,0 +1,45 @@ +[gd_scene load_steps=5 format=3 uid="uid://djxiqhnjwr33t"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_7f2ku"] +size = Vector2(1206, 83) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_28ymd"] +size = Vector2(81.5, 711.5) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_qxyaq"] +size = Vector2(1206, 83) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_ond4j"] +size = Vector2(68, 710.5) + +[node name="Game" type="Node2D"] + +[node name="GodotBalls" type="Node2D" parent="."] + +[node name="MultiplayerSpawner" type="MultiplayerSpawner" parent="."] +_spawnable_scenes = PackedStringArray("res://godot_ball.tscn") +spawn_path = NodePath("../GodotBalls") + +[node name="Wall" type="StaticBody2D" parent="."] + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Wall"] +position = Vector2(585, 698) +shape = SubResource("RectangleShape2D_7f2ku") + +[node name="Wall2" type="StaticBody2D" parent="."] + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Wall2"] +position = Vector2(1201.25, 329) +shape = SubResource("RectangleShape2D_28ymd") + +[node name="Wall3" type="StaticBody2D" parent="."] + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Wall3"] +position = Vector2(571, -49) +shape = SubResource("RectangleShape2D_qxyaq") + +[node name="Wall4" type="StaticBody2D" parent="."] + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Wall4"] +position = Vector2(-43, 332) +shape = SubResource("RectangleShape2D_ond4j") diff --git a/godot/circles/gamestate.gd b/godot/circles/gamestate.gd new file mode 100644 index 0000000..7e8985b --- /dev/null +++ b/godot/circles/gamestate.gd @@ -0,0 +1,201 @@ +extends Node + +var godot_ball = preload("res://godot_ball.tscn") + +# Default game server port. Can be any number between 1024 and 49151. +# Not on the list of registered or common ports as of November 2020: +# https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers +const DEFAULT_PORT = 10567 + +# Max number of players. +const MAX_PEERS = 12 + +var peer = null + +# Name for my player. +var player_name = "The Warrior" + +# Names for remote players in id:name format. +var players = {} +var player_tokens = {} + +# Signals to let lobby GUI know what's going on. +signal player_list_changed() +signal connection_failed() +signal connection_succeeded() +signal game_ended() +signal game_error(what) + + +func _ready(): + RivetHelper.start_server.connect(start_server) + RivetHelper.setup_multiplayer() + + multiplayer.peer_connected.connect(self._player_connected) + multiplayer.peer_disconnected.connect(self._player_disconnected) + multiplayer.connected_to_server.connect(self._connected_ok) + multiplayer.connection_failed.connect(self._connected_fail) + multiplayer.server_disconnected.connect(self._server_disconnected) + + +func start_server(): + print("Starting server on %s" % DEFAULT_PORT) + + peer = ENetMultiplayerPeer.new() + var error = peer.create_server(DEFAULT_PORT, MAX_PEERS) + + RivetHelper._assert(!error, "Could not start server") + multiplayer.set_multiplayer_peer(peer) + + var response = await Rivet.matchmaker.lobbies.ready({}) + + if response.result == OK: + RivetHelper.rivet_print("Lobby ready") + else: + RivetHelper.rivet_print("Lobby ready failed") + OS.crash("Lobby ready failed") + + + +# Callback from SceneTree. +func _player_connected(id): + print("Player connected %s" % id) + + # Registration of a client beings here, tell the connected player that we are here. + if !multiplayer.is_server(): + register_player.rpc_id(id, player_name) + + # If we are the server, add the new player + if multiplayer.is_server(): + var new_player = godot_ball.instantiate() + new_player.name = str(id) + get_node("/root/Game/GodotBalls").add_child(new_player) + + new_player.set_position(Vector2(randf_range(100, 1000), 0)) + + # Update player list if no other players present + player_list_changed.emit() + + +# Callback from SceneTree. +func _player_disconnected(id): + if has_node("/root/World"): # Game is in progress. + if multiplayer.is_server(): + game_error.emit("Player " + players[id] + " disconnected") + end_game() + else: # Game is not in progress. + # Unregister this player. + unregister_player(id) + + +# Callback from SceneTree, only for clients (not server). +func _connected_ok(): + # We just connected to a server + connection_succeeded.emit() + + +# Callback from SceneTree, only for clients (not server). +func _server_disconnected(): + game_error.emit("Server disconnected") + end_game() + + +# Callback from SceneTree, only for clients (not server). +func _connected_fail(): + multiplayer.set_multiplayer_peer(null) # Remove peer + connection_failed.emit() + + +# Lobby management functions. +@rpc("any_peer", "reliable") +func register_player(new_player_name): + var id = multiplayer.get_remote_sender_id() + players[id] = new_player_name + player_list_changed.emit() + + +func unregister_player(id): + players.erase(id) + player_list_changed.emit() + + +@rpc("call_local", "reliable") +func load_world(): + # Change scene. + var world = load("res://scenes/world.tscn").instantiate() + get_tree().get_root().add_child(world) + get_tree().get_root().get_node("Lobby").hide() + + # Set up score. + world.get_node("Score").add_player(multiplayer.get_unique_id(), player_name) + for pn in players: + world.get_node("Score").add_player(pn, players[pn]) + get_tree().set_pause(false) # Unpause and unleash the game! + + +func join_game(): + print("Searching for lobbies") + var response = await Rivet.matchmaker.lobbies.find({ + "game_modes": ["default"] + }) + + if response.result == OK: + print(response.body) + RivetHelper.set_player_token(response.body.player.token) + + var port = response.body.ports.default + print("Connecting to ", port.host) + + peer = ENetMultiplayerPeer.new() + var error = peer.create_client(port.hostname, port.port) + RivetHelper._assert(!error, "Could not start server") + multiplayer.set_multiplayer_peer(peer) + else: + print("Lobby find failed: ", response) + game_error.emit(response.body) + + + +func get_player_list(): + return players.values() + + +func get_player_name(): + return player_name + + +# TODO: Figure out why this doesn't work as "authority" +@rpc("any_peer") +func begin_game(): + if !multiplayer.is_server(): + return + # Tell Rivet that this lobby is closed + await Rivet.matchmaker.lobbies.setClosed({}) + + load_world.rpc() + var world = get_tree().get_root().get_node("World") + var player_scene = load("res://scenes/player.tscn") + + # Create a dictionary with peer id and respective spawn points, could be improved by randomizing. + var spawn_points = {} + var spawn_point_idx = 1 + for p in players: + spawn_points[p] = spawn_point_idx + spawn_point_idx += 1 + + for p_id in spawn_points: + var spawn_pos = world.get_node("SpawnPoints/" + str(spawn_points[p_id])).position + var player = player_scene.instantiate() + player.synced_position = spawn_pos + player.name = str(p_id) + player.set_player_name(player_name if p_id == multiplayer.get_unique_id() else players[p_id]) + world.get_node("Players").add_child(player) + + +func end_game(): + if has_node("/root/World"): # Game is in progress. + # End it + get_node("/root/World").queue_free() + + game_ended.emit() + players.clear() diff --git a/godot/circles/godot_ball.gd b/godot/circles/godot_ball.gd new file mode 100644 index 0000000..0177a2f --- /dev/null +++ b/godot/circles/godot_ball.gd @@ -0,0 +1,47 @@ +extends RigidBody2D + + +# Called when the node enters the scene tree for the first time. +func _ready(): + if str(name).is_valid_int(): + print("setting authority for player ", str(name)) + get_node("MultiplayerSynchronizer").set_multiplayer_authority(str(name).to_int()) + + +func _physics_process(delta): + pass + # if is_multiplayer_authority(): + # # The server updates the position that will be notified to the clients. + # synced_position = position + # # And increase the bomb cooldown spawning one if the client wants to. + # last_bomb_time += delta + # if not stunned and is_multiplayer_authority() and inputs.bombing and last_bomb_time >= BOMB_RATE: + # last_bomb_time = 0.0 + # get_node("../../BombSpawner").spawn([position, str(name).to_int()]) + # else: + # # The client simply updates the position to the last known one. + # position = synced_position + + # if not stunned: + # # Everybody runs physics. I.e. clients tries to predict where they will be during the next frame. + # velocity = inputs.motion * MOTION_SPEED + # move_and_slide() + + # # Also update the animation based on the last known player input state + # var new_anim = "standing" + + # if inputs.motion.y < 0: + # new_anim = "walk_up" + # elif inputs.motion.y > 0: + # new_anim = "walk_down" + # elif inputs.motion.x < 0: + # new_anim = "walk_left" + # elif inputs.motion.x > 0: + # new_anim = "walk_right" + + # if stunned: + # new_anim = "stunned" + + # if new_anim != current_anim: + # current_anim = new_anim + # get_node("anim").play(current_anim) \ No newline at end of file diff --git a/godot/circles/godot_ball.tscn b/godot/circles/godot_ball.tscn new file mode 100644 index 0000000..73e52fc --- /dev/null +++ b/godot/circles/godot_ball.tscn @@ -0,0 +1,20 @@ +[gd_scene load_steps=4 format=3 uid="uid://cxrpwf6tg2gsq"] + +[ext_resource type="Texture2D" uid="uid://bagf0ahxiqqfk" path="res://icon.svg" id="1_1wl3h"] +[ext_resource type="Script" path="res://godot_ball.gd" id="1_3fv58"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_05och"] +size = Vector2(65, 65) + +[node name="GodotBall" type="CharacterBody2D"] +script = ExtResource("1_3fv58") + +[node name="Sprite2D" type="Sprite2D" parent="."] +scale = Vector2(0.5, 0.5) +texture = ExtResource("1_1wl3h") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +position = Vector2(0.5, 1) +shape = SubResource("RectangleShape2D_05och") + +[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."] diff --git a/godot/circles/icon.svg b/godot/circles/icon.svg new file mode 100644 index 0000000..b370ceb --- /dev/null +++ b/godot/circles/icon.svg @@ -0,0 +1 @@ + diff --git a/godot/circles/icon.svg.import b/godot/circles/icon.svg.import new file mode 100644 index 0000000..bf49e1c --- /dev/null +++ b/godot/circles/icon.svg.import @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9078c90f0a54585e5e4a64fb82883def9363db985e28e385d44f9d315976d137 +size 843 diff --git a/godot/circles/loading.gd b/godot/circles/loading.gd new file mode 100644 index 0000000..f4bc5ae --- /dev/null +++ b/godot/circles/loading.gd @@ -0,0 +1,19 @@ +extends Control + +var game = preload("res://game.tscn").instantiate() + +# Called when the node enters the scene tree for the first time. +func _ready(): + print("Loading game with ", multiplayer.is_server()) + if !RivetHelper.is_dedicated_server(): + print("Loading game2") + Gamestate.join_game() + print("Loading game3") + + get_tree().root.add_child.call_deferred(game) + get_node("/root/Loading").queue_free() + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _process(delta): + pass diff --git a/godot/circles/loading.tscn b/godot/circles/loading.tscn new file mode 100644 index 0000000..f54f3bf --- /dev/null +++ b/godot/circles/loading.tscn @@ -0,0 +1,28 @@ +[gd_scene load_steps=2 format=3 uid="uid://cfpr5hufdmtv0"] + +[ext_resource type="Script" path="res://loading.gd" id="1_dk6u4"] + +[node name="Loading" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_dk6u4") + +[node name="Label" type="Label" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -37.5 +offset_top = -11.5 +offset_right = 37.5 +offset_bottom = 11.5 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 4 +text = "Loading..." diff --git a/godot/circles/project.godot b/godot/circles/project.godot new file mode 100644 index 0000000..fa77571 --- /dev/null +++ b/godot/circles/project.godot @@ -0,0 +1,32 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Circles" +run/main_scene="res://loading.tscn" +config/features=PackedStringArray("4.2", "GL Compatibility") +config/icon="res://icon.svg" + +[autoload] + +RivetClient="*res://addons/rivet/rivet_client.gd" +RivetHelper="*res://addons/rivet/rivet_helper.gd" +Rivet="*res://addons/rivet/rivet_global.gd" +Gamestate="*res://gamestate.gd" + +[editor_plugins] + +enabled=PackedStringArray("res://addons/rivet/plugin.cfg") + +[rendering] + +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" diff --git a/godot/circles/rivet.yaml b/godot/circles/rivet.yaml new file mode 100644 index 0000000..3440b48 --- /dev/null +++ b/godot/circles/rivet.yaml @@ -0,0 +1,13 @@ +engine: + godot: + +matchmaker: + max_players: 12 + docker: + dockerfile: 'Dockerfile' + ports: + default: + port: 10567 + protocol: 'udp' + game_modes: + default: {}