generated from krampus/template-godot4
initial commit
Some checks failed
Some checks failed
This commit is contained in:
parent
dea39d77f3
commit
99de9e8b40
7
addons/tube/LICENCE.md
Normal file
7
addons/tube/LICENCE.md
Normal file
@ -0,0 +1,7 @@
|
||||
Copyright (c) 2025 Koop Myers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
264
addons/tube/README.md
Normal file
264
addons/tube/README.md
Normal file
@ -0,0 +1,264 @@
|
||||
# Tube
|
||||
|
||||
A lightweight Godot addon that helps create simple multiplayer sessions.
|
||||
|
||||
One player creates a session and shares the session ID with others through an external channel (WhatsApp, Discord, etc.). The other players can then join and play together. That’s it, no server deployment needed.
|
||||
|
||||
## Use case & limitation
|
||||
|
||||
Tube works on any platform that supports WebRTC over the internet, see [Requirements](#requirements).
|
||||
It also runs on non-web platforms (Windows, macOS, Linux, Android, iOS) over a local network, without needing an internet connection.
|
||||
|
||||
However, the benefit of not having to deploy a server comes with a trade-off: in some cases, two peers may fail to connect. To better understand why this happens, see [How it works](#how-it-works).
|
||||
|
||||
Because no server is deployed by default, Tube may not be suitable for projects that require high stability or support for a large user base. If stability is critical, you can deploy your own servers to ensure reliable connectivity [Using your own servers](#using-your-own-servers).
|
||||
As it is, Tube is a great option for:
|
||||
- Rapid prototyping of peer-to-peer multiplayer
|
||||
- Testing mutliplayer games
|
||||
- Learning Godot High-level multiplayer
|
||||
- Local multiplayer game
|
||||
- Game demo
|
||||
- Simple indie game
|
||||
- Private multiplayer game
|
||||
|
||||
There’s no strict technical limit on the number of players in a session, but each additional player increases the load on the server peer.
|
||||
|
||||
Tube was developed and tested with Godot 4.5, and it may also work with other Godot 4.x versions. It is not compatible with Godot 3.
|
||||
|
||||
## How to use
|
||||
|
||||
### Requirements
|
||||
|
||||
**Tube** uses WebRTC, as it, it works automatically on HTML5 export, but require an external GDExtension plugin on other platforms. You can find everything you need in the [webrtc-native plugin repository](https://github.com/godotengine/webrtc-native/releases).
|
||||
> [!WARNING]
|
||||
> No **specific** error message will appear if WebRTC implementation is missing. Make sure it’s set up correctly!
|
||||
|
||||
When exporting to Android, make sure to enable the `INTERNET` and `CHANGE_WIFI_MULTICAST_STATE` permission in the Android export preset before exporting the project or using one-click deploy. Otherwise, network communication of any kind will be blocked by Android.
|
||||
|
||||
To use this add-on effectively, it is essential to understand [Godot High-Level Multiplayer](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html)
|
||||
|
||||
### Installation
|
||||
|
||||
To install copy the *addons/tube* folder into *addons* Godot project's *addons* folder.
|
||||
Or download it directly from the [Godot asset library](https://godotengine.org/asset-library/asset/4419)
|
||||
|
||||
Verify that the addon is activated in your godot project in `Project Settings -> Plugins`.
|
||||
|
||||
### Configuration & Utilisation
|
||||
|
||||
**Tube** is composed of two main elements:
|
||||
- `TubeContext`: A `Resource` defining the configuration the session connexions.
|
||||
- `TubeClient`: A `Node` managing network connection and multiplayer peers.
|
||||
|
||||
#### 1. Creating a `TubeContext`
|
||||
|
||||
First, create a new `TubeContext` for your project `in Godot FileSystem inspector -> Create New -> TubeContext`. And do the following :
|
||||
1. Enter a `App ID` in your `TubeContext`. App ID must be exactly 15 ASCII characters. You can generate one automatically by clicking `Generate App ID`. App ID must be the same on all instance of your game.
|
||||
|
||||
> [!TIP]
|
||||
> If your game is only intended for local play, you can skip the following steps 2 and 3.
|
||||
For Web builds, however, steps 2 and 3 are mandatory, since local connections do not work on Web.
|
||||
|
||||
2. Add `Trackers URLs` , you can use the following:
|
||||
- wss://tracker.openwebtorrent.com
|
||||
- wss://tracker.files.fm:7073/announce
|
||||
- wss://tracker.btorrent.xyz/
|
||||
- wss://tracker.ghostchu-services.top:443/announce
|
||||
|
||||
3. Add `Stun Servers URLs`, you can use the following:
|
||||
- stun:stun.l.google.com:19302
|
||||
- stun:stun.cloudflare.com:3478
|
||||
- stun:stun.bethesda.net:3478
|
||||
|
||||
|
||||
#### 2. Adding a `TubeClient` to Your Scene
|
||||
|
||||
Next add a `TubeClient` to our game scene : `in Godot Scene inspector -> Add Child Node -> TubeClient`.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> `TubeClient` must be present in the scene tree to function, and it can be placed anywhere.
|
||||
However, it should not be removed while a session is open (either, creating, joining, created or joined).
|
||||
|
||||
Assign the previously created TubeContext to the Context property of your TubeClient.
|
||||
Optionally, you can also configure:
|
||||
- `peer_signaling_timeout`
|
||||
- `peer_signaling_max_attempts`
|
||||
- `multiplayer_root_node`
|
||||
|
||||
For more details about the available properties and functions:
|
||||
- In Godot Scene inspector -> Right click on your `TubeClient` -> `Open Documentation`.
|
||||
- In the Script tab, search for `TubeClient` in the Help panel.
|
||||
|
||||
|
||||
#### 3. Creating and Joining Sessions
|
||||
|
||||
On only one instance of the game call `create_session()`, for example:
|
||||
```GDScript
|
||||
@onready var label: Label = $Label # Label to display session id
|
||||
@onready var tube_client: TubeClient = $TubeClient # reference to tube client in scene tree
|
||||
|
||||
|
||||
func _on_button_pressed(): # User press create session button
|
||||
tube_client.create_session()
|
||||
label.text = tube_client.session_id
|
||||
```
|
||||
This player becomes the server (`is_server = true`) and have acces to the created session ID in the `session_id` property.
|
||||
|
||||
The server player should share this session ID with others through an external channel (e.g. Discord).
|
||||
Other players can join by calling `join_session(session_id)`, for example:
|
||||
```GDScript
|
||||
@onready var line_edit: LineEdit = $LineEdit # text user input for session id
|
||||
|
||||
func _on_button_pressed(): # User press join session button
|
||||
tube_client.join_session(line_edit.text)
|
||||
```
|
||||
|
||||
When the session is successfully created or joined, the corresponding signals are emitted:
|
||||
- `session_created`
|
||||
- `session_joined`
|
||||
|
||||
If an error occurs during creation or joining, the client emits:
|
||||
`error_raised(code: ErrorCode, message: String)`
|
||||
(see the `TubeClient` documentation in Godot for details on signals and error codes).
|
||||
|
||||
Any player can leave the session by calling `leave_session()`.
|
||||
If the server calls it, the session will close for everyone, and `session_left` will be emitted.
|
||||
|
||||
The server can:
|
||||
- Kick a player using `kick_peer(p_peer_id: int)`
|
||||
- Refuse new connections automatically by setting `refuse_new_connections = true`
|
||||
|
||||
|
||||
#### 4. Implementing Multiplayer Logic
|
||||
|
||||
By default, `TubeClient` automatically configures Godot’s `MultiplayerAPI` and `MultiplayerPeer` on the SceneTree root node.
|
||||
You can customize this behavior by setting the multiplayer_root_node property on TubeClient (see [SceneTree.set_multiplayer](https://docs.godotengine.org/en/stable/classes/class_scenetree.html#class-scenetree-method-set-multiplayer) for more information).
|
||||
|
||||
Once peers are connected, use [Godot High-level multiplayer](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html) to implement your game logic.
|
||||
You can make use of tools such as:
|
||||
- Godot RPC
|
||||
- [MultiplayerSpawner](https://docs.godotengine.org/en/stable/classes/)
|
||||
- [MultiplayerSynchronizer](https://docs.godotengine.org/en/stable/classes/class_multiplayersynchronizer.html)
|
||||
|
||||
For exemple:
|
||||
```GDScript
|
||||
func _on_some_input(): # Connected to some input.
|
||||
transfer_some_input.rpc_id(1) # Send the input only to the server.
|
||||
|
||||
|
||||
# Call local is required if the server is also a player.
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func transfer_some_input():
|
||||
# The server knows who sent the input.
|
||||
var sender_id = multiplayer.get_remote_sender_id()
|
||||
# Process the input and affect game logic.
|
||||
```
|
||||
|
||||
To know more about how to configure and use it, you can look into the [demo project](https://github.com/koopmyers/pixelary)
|
||||
|
||||
### Trouble shooting
|
||||
|
||||
**Tube** includes a helpful tool called `TubeInspector` for debugging and visualizing internal network activity.
|
||||
To use it, add the scene located at `/addons/tube/tube_inspector.tscn` to your project and assign your `TubeClient` to it.
|
||||
|
||||
> [!NOTE]
|
||||
> Some features, such as latency display and chat, are only available if `TubeInspector` is part of the `MultiplayerAPI` scene tree.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/koopmyers/tube/refs/heads/main/screenshots/inspector2.png" alt="Tube inspector" width="200"/>
|
||||
<img src="https://raw.githubusercontent.com/koopmyers/tube/b47f12c37505baa57a5c89281d6d2fd9263c3cd4/screenshots/inspector.png" alt="Tube inspector" width="200"/>
|
||||
|
||||
#### Major known issues
|
||||
|
||||
The most common reason a player cannot connect is a **symmetric NAT**.
|
||||
A symmetric NAT is a router configuration that prevents NAT hole punching. This means that if both peers are behind a symmetric NAT, the connection will likely fail.
|
||||
|
||||
You can check whether you are behind a symmetric NAT using the **NAT hole punching** field in `TubeInspector`. Multiple STUN servers with different addresses are required. If the result is `unknown`, try different STUN domains. This tool is not available on Web platform.
|
||||
You can also test here: [Symmetric NAT test](https://tomchen.github.io/symmetric-nat-test/), but note that false positives are common due to browser privacy behavior.
|
||||
|
||||
Tube will attempt to map public ports via **UPnP**. Port mapping can help bypass symmetric NAT.
|
||||
However, UPnP is not supported on all networks, commonly disabled on corporate, public, or VPN networks.
|
||||
You can verify UPnP support using the **UPnP port mapping** field in `TubeInspector`. Port mapping is not available on Web platform.
|
||||
If UPnP is available but connections still fail, the timeout may occur before the port opens. Try increasing the client's `peer_signaling_timeout` or `peer_signaling_max_attempts`.
|
||||
|
||||
If both **NAT hole punching** and **UPnP port mapping** show `likely to fail` for two players, then a direct Internet connection is likely impossible without a relay server.
|
||||
You can still use **Tube** with your own servers to ensure reliable connectivity. See: [Using your own servers](#using-your-own-servers).
|
||||
|
||||
#### Minor known issues
|
||||
|
||||
> [!CAUTION]
|
||||
> Class 'UPNPDeviceMiniUPNP' already exists
|
||||
|
||||
This is a core Godot Engine issue caused by multithreading. There is currently no known way to fix or suppress it without modifying the engine itself.
|
||||
|
||||
</br>
|
||||
|
||||
> [!CAUTION]
|
||||
> Invalid status code. Got 'XXX', expected 101.
|
||||
|
||||
This refers to a [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status#server_error_responses), indicating that a tracker is unavailable or encountered an issue. `TubeInspector` will show which trackers failed to connect.
|
||||
This problem is related to tracker availability or network conditions. There is no reliable way to handle this error in GDScript. Because public trackers can occasionally be unstable, we recommend using **multiple trackers** to improve connection reliability.
|
||||
|
||||
## How it works
|
||||
|
||||
**Tube** establishes a server–client architecture between peers.
|
||||
One peer acts as the server, while all other peers connect to it as clients.
|
||||
The server is responsible for relaying Godot’s RPC (see [Godot High-level multiplayer](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html)) between peers.
|
||||
|
||||
To connect peers to the server, Tube uses WebRTC (Web Real-Time Communication), an open-source technology that enables secure, real-time peer-to-peer data transmission.
|
||||
Establishing a WebRTC connection requires an initial signaling phase, which depends on three external components:
|
||||
- Signaling servers: Used to exchange connection initialization messages between peers.
|
||||
- STUN servers: Help peers determine their public address and how they can be reached.
|
||||
- TURN servers (optional): Act as relays when a direct peer-to-peer connection cannot be established.
|
||||
|
||||
For more details on WebRTC, visit the [Official WebRTC web site](https://webrtc.org) and the [WebRTC Godot documentation](https://docs.godotengine.org/en/stable/classes/class_webrtcpeerconnection.html)
|
||||
|
||||
|
||||
### Local signaling
|
||||
|
||||
On a local network, the server peer listens on determined port. When joining, other peers broadcast their signaling data across the network at destination of the server. Once signaling is complete, peers automatically switch to a WebRTC connection.
|
||||
|
||||
STUN and TURN servers are not needed in this mode.
|
||||
|
||||
Because the Web platform cannot open listening ports, local signaling is unavailable on Web builds.
|
||||
|
||||
|
||||
### Online signaling
|
||||
|
||||
For Signaling servers, **Tube** use WebTorrent tracker servers as signaling servers. Several public trackers are available, such as those listed in [Configuration & Utilisation](#configuration--utilisation).
|
||||
|
||||
It is recommended to use multiple trackers to improve connection reliability, as public trackers can occasionally be unstable.
|
||||
|
||||
To learn more about BitTorrent trackers and WebTorrent, see the [WebTorrent github](https://github.com/webtorrent/webtorrent) and the [Wikipedia BitTorrent Tracker page](https://en.wikipedia.org/wiki/BitTorrent_tracker).
|
||||
|
||||
If you need more stable connections for your game, you can deploy your own tracker servers, see [Using your own servers](#using-your-own-servers).
|
||||
|
||||
Many public STUN servers are available, such as those provided by Google.
|
||||
You can find an updated list here: [Public STUN list](https://gist.github.com/mondain/b0ec1cf5f60ae726202e)
|
||||
|
||||
Currently, there are no reliable public TURN servers.
|
||||
Without a TURN server, there is no fallback mechanism when peers cannot establish a direct connection, for example, if both peers are behind a *symmetric NAT*. To mitigate this, Tube attempts to open ports automatically using *UPnP port mapping*. However, this feature is not supported on the Web platform.
|
||||
|
||||
For maximum reliability, you can deploy your own TURN server and add it to your `TubeContext` configuration see [Using your own servers](#using-your-own-servers).
|
||||
|
||||
## Using your own servers
|
||||
|
||||
### WebTorrent tracker
|
||||
You can deploy your own WebTorrent tracker using the [Official Webtorrent Tracker](https://github.com/webtorrent/bittorrent-tracker) or the [OpenWebTorrent Tracker](https://github.com/OpenWebTorrent/openwebtorrent-tracker).
|
||||
|
||||
Make sure to configure it with WebSocket support, availbale on Internet and set its URL in your TubeContext.
|
||||
|
||||
It is strongly recommended to use secure WebSockets (WSS/TLS) for to ensure reliable and encrypted communication and some browser will block non secure communication.
|
||||
|
||||
### Turn server
|
||||
To improve connection reliability, you can host your own TURN server using [coturn](https://github.com/coturn/coturn) or [eturnal](https://github.com/processone/eturnal). They can also be used as STUN servers.
|
||||
|
||||
Once deployed, add your TURN server’s URL and credentials to your `TubeContext`.
|
||||
|
||||
For security reasons, it’s recommended to use ephemeral credentials to prevent unauthorized access to your TURN server.
|
||||
This approach requires additional setup, such as generating credentials dynamically through a secure backend.
|
||||
|
||||
There are also third-party TURN hosting services available, but most are paid solutions.
|
||||
|
||||
|
||||
## Credits
|
||||
Inspector icons: https://www.kenney.nl/assets/game-icons
|
||||
BIN
addons/tube/icons/tube_client.svg
(Stored with Git LFS)
Normal file
BIN
addons/tube/icons/tube_client.svg
(Stored with Git LFS)
Normal file
Binary file not shown.
43
addons/tube/icons/tube_client.svg.import
Normal file
43
addons/tube/icons/tube_client.svg.import
Normal file
@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b4o7vx0n8db1s"
|
||||
path="res://.godot/imported/tube_client.svg-3544df422a27b401f812416126d79a81.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/tube/icons/tube_client.svg"
|
||||
dest_files=["res://.godot/imported/tube_client.svg-3544df422a27b401f812416126d79a81.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
BIN
addons/tube/icons/tube_context.svg
(Stored with Git LFS)
Normal file
BIN
addons/tube/icons/tube_context.svg
(Stored with Git LFS)
Normal file
Binary file not shown.
43
addons/tube/icons/tube_context.svg.import
Normal file
43
addons/tube/icons/tube_context.svg.import
Normal file
@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cqibqku84s77x"
|
||||
path="res://.godot/imported/tube_context.svg-febea6fb8bda9c0e97caaf283ec71d2f.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/tube/icons/tube_context.svg"
|
||||
dest_files=["res://.godot/imported/tube_context.svg-febea6fb8bda9c0e97caaf283ec71d2f.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
BIN
addons/tube/icons/tube_inspector.svg
(Stored with Git LFS)
Normal file
BIN
addons/tube/icons/tube_inspector.svg
(Stored with Git LFS)
Normal file
Binary file not shown.
43
addons/tube/icons/tube_inspector.svg.import
Normal file
43
addons/tube/icons/tube_inspector.svg.import
Normal file
@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://x56krrgxw70m"
|
||||
path="res://.godot/imported/tube_inspector.svg-327f0bac19dd37d51cef3c03e256b924.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/tube/icons/tube_inspector.svg"
|
||||
dest_files=["res://.godot/imported/tube_inspector.svg-327f0bac19dd37d51cef3c03e256b924.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
36
addons/tube/inspector/channel_control.gd
Normal file
36
addons/tube/inspector/channel_control.gd
Normal file
@ -0,0 +1,36 @@
|
||||
class_name EditorTubeChannelControl extends Control
|
||||
## @experimental: This class is used as part of the TubeClientDebugPanel scene and is part of a scene. Should not be used as itself.
|
||||
|
||||
|
||||
@export var channel_item: EditorTubeChannelItemControl:
|
||||
set(x):
|
||||
channel_item = x
|
||||
show()
|
||||
|
||||
if is_instance_valid(id_label):
|
||||
id_label.text = str(channel_item.channel.get_id())
|
||||
|
||||
if is_instance_valid(label_label):
|
||||
label_label.text = channel_item.channel.get_label()
|
||||
|
||||
update_messages()
|
||||
|
||||
|
||||
@export var messages_container: EditorTubeMessagesContainer
|
||||
|
||||
|
||||
@onready var id_label: Label = %IdLabel
|
||||
@onready var label_label: Label = %LabelLabel
|
||||
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
hide()
|
||||
|
||||
|
||||
func update_messages():
|
||||
if null == channel_item:
|
||||
return
|
||||
|
||||
if is_instance_valid(messages_container):
|
||||
messages_container.display_messages(channel_item.message_item_controls)
|
||||
1
addons/tube/inspector/channel_control.gd.uid
Normal file
1
addons/tube/inspector/channel_control.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dxd3thybq7mc2
|
||||
30
addons/tube/inspector/channel_control.tscn
Normal file
30
addons/tube/inspector/channel_control.tscn
Normal file
@ -0,0 +1,30 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://budxt0ohhps46"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dxd3thybq7mc2" path="res://addons/tube/inspector/channel_control.gd" id="1_t40fg"]
|
||||
|
||||
[node name="ChannelControl" type="MarginContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_type_variation = &"H1PanelContainer"
|
||||
script = ExtResource("1_t40fg")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HeaderContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="IdLabel" type="Label" parent="VBoxContainer/HeaderContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"HeaderMedium"
|
||||
text = "00000000000000000000"
|
||||
|
||||
[node name="LabelLabel" type="Label" parent="VBoxContainer/HeaderContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"HeaderMedium"
|
||||
text = "00000000000000000000"
|
||||
82
addons/tube/inspector/channel_item_control.gd
Normal file
82
addons/tube/inspector/channel_item_control.gd
Normal file
@ -0,0 +1,82 @@
|
||||
class_name EditorTubeChannelItemControl extends Control
|
||||
## @experimental: This class is used as part of the TubeClientDebugPanel scene and is part of a scene. Should not be used as itself.
|
||||
|
||||
signal pressed
|
||||
|
||||
|
||||
const MESSAGE_ITEM_CONTROL_SCENE := preload("uid://cfsei3airwx4s")
|
||||
|
||||
|
||||
const STATE_COLOR_DEFAULT := Color.WHITE
|
||||
const STATE_COLOR := {
|
||||
WebRTCDataChannel.STATE_CONNECTING: Color.CYAN,
|
||||
WebRTCDataChannel.STATE_OPEN: Color.PALE_GREEN,
|
||||
WebRTCDataChannel.STATE_CLOSING: Color.GOLDENROD,
|
||||
WebRTCDataChannel.STATE_CLOSED: Color.CRIMSON,
|
||||
}
|
||||
const STATE_TEXT_DEFAULT := "Unknown"
|
||||
const STATE_TEXT := {
|
||||
WebRTCDataChannel.STATE_CONNECTING: "Connecting",
|
||||
WebRTCDataChannel.STATE_OPEN: "Open",
|
||||
WebRTCDataChannel.STATE_CLOSING: "Closing",
|
||||
WebRTCDataChannel.STATE_CLOSED: "Closed",
|
||||
}
|
||||
|
||||
|
||||
@export var channel_control: EditorTubeChannelControl
|
||||
|
||||
|
||||
var peer: TubePeer:
|
||||
set(x):
|
||||
if null != peer:
|
||||
peer.channel_state_changed.disconnect(
|
||||
_on_peer_channel_state_changed
|
||||
)
|
||||
|
||||
if null != x:
|
||||
x.channel_state_changed.connect(
|
||||
_on_peer_channel_state_changed
|
||||
)
|
||||
|
||||
peer = x
|
||||
update()
|
||||
|
||||
|
||||
var channel: WebRTCDataChannel:
|
||||
set(x):
|
||||
channel = x
|
||||
update()
|
||||
|
||||
|
||||
#var message_item_controls: Array[EditorTubeMessagesItemControl] = []
|
||||
|
||||
@onready var name_label: Label = %NameLabel
|
||||
@onready var state_indicator: Control = %StateIndicator
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
update()
|
||||
|
||||
|
||||
func _on_button_pressed() -> void:
|
||||
if is_instance_valid(channel_control):
|
||||
channel_control.channel_item = self
|
||||
|
||||
pressed.emit()
|
||||
|
||||
|
||||
func update():
|
||||
if is_instance_valid(name_label):
|
||||
name_label.text = channel.get_label()
|
||||
|
||||
if is_instance_valid(state_indicator):
|
||||
state_indicator.modulate = STATE_COLOR_DEFAULT if not channel else STATE_COLOR[channel.get_ready_state()]
|
||||
state_indicator.tooltip_text = STATE_TEXT_DEFAULT if not channel else STATE_TEXT[channel.get_ready_state()]
|
||||
|
||||
if is_instance_valid(channel_control):
|
||||
if self == channel_control.channel_item:
|
||||
channel_control.update_messages()
|
||||
|
||||
|
||||
func _on_peer_channel_state_changed(_channel: WebRTCDataChannel):
|
||||
update()
|
||||
1
addons/tube/inspector/channel_item_control.gd.uid
Normal file
1
addons/tube/inspector/channel_item_control.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b1h5h73j26j8v
|
||||
46
addons/tube/inspector/channel_item_control.tscn
Normal file
46
addons/tube/inspector/channel_item_control.tscn
Normal file
@ -0,0 +1,46 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://dc3ssinymllca"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b1h5h73j26j8v" path="res://addons/tube/inspector/channel_item_control.gd" id="1_ep4vf"]
|
||||
[ext_resource type="Theme" uid="uid://bcibt73qths3g" path="res://addons/tube/inspector/theme.tres" id="1_gkg3v"]
|
||||
|
||||
[sub_resource type="ButtonGroup" id="ButtonGroup_ep4vf"]
|
||||
resource_local_to_scene = false
|
||||
allow_unpress = true
|
||||
|
||||
[node name="ChannelControl" type="MarginContainer"]
|
||||
anchors_preset = 10
|
||||
anchor_right = 1.0
|
||||
offset_bottom = 28.0
|
||||
grow_horizontal = 2
|
||||
theme = ExtResource("1_gkg3v")
|
||||
script = ExtResource("1_ep4vf")
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="StateIndicator" type="Panel" parent="HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(24, 12)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_type_variation = &"PanelIndicator"
|
||||
|
||||
[node name="NameLabel" type="Label" parent="HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_type_variation = &"LabelH2"
|
||||
text = "CHANNEL"
|
||||
text_overrun_behavior = 1
|
||||
|
||||
[node name="Button" type="Button" parent="."]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"FlatButton"
|
||||
toggle_mode = true
|
||||
button_group = SubResource("ButtonGroup_ep4vf")
|
||||
alignment = 0
|
||||
|
||||
[connection signal="pressed" from="Button" to="." method="_on_button_pressed"]
|
||||
76
addons/tube/inspector/chat_control.gd
Normal file
76
addons/tube/inspector/chat_control.gd
Normal file
@ -0,0 +1,76 @@
|
||||
class_name EditorTubeChatControl extends Control
|
||||
## @experimental: This class is used as part of the TubeClientDebugPanel scene and is part of a scene. Should not be used as itself.
|
||||
|
||||
|
||||
const MESSAGE_ITEM_CONTROL_SCENE := preload("uid://cfsei3airwx4s")
|
||||
|
||||
|
||||
@export var messages_container: EditorTubeMessagesContainer
|
||||
@export var max_messages_amount: int = 100
|
||||
|
||||
var message_item_controls: Array[EditorTubeMessagesItemControl] = []
|
||||
var message_item_button_group := ButtonGroup.new()
|
||||
|
||||
@onready var line_edit: LineEdit = %LineEdit
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
message_item_button_group.allow_unpress = true
|
||||
|
||||
|
||||
func add_message_item_control(data) -> EditorTubeMessagesItemControl:
|
||||
if max_messages_amount <= message_item_controls.size():
|
||||
var item := message_item_controls.pop_front()
|
||||
item.queue_free()
|
||||
|
||||
var message_item_control := MESSAGE_ITEM_CONTROL_SCENE.instantiate()
|
||||
message_item_controls.append(message_item_control)
|
||||
message_item_control.data = data
|
||||
message_item_control.button_group = message_item_button_group
|
||||
return message_item_control
|
||||
|
||||
|
||||
func update():
|
||||
if is_instance_valid(messages_container):
|
||||
if messages_container.is_displaying_from(self):
|
||||
messages_container.display_messages(
|
||||
message_item_controls,
|
||||
self
|
||||
)
|
||||
|
||||
|
||||
func send_chat_message(p_message: String):
|
||||
add_message_item_control(p_message).sent()
|
||||
update()
|
||||
receive_chat_message.rpc(p_message)
|
||||
line_edit.text = ""
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func receive_chat_message(p_message: String):
|
||||
var peer_id := multiplayer.get_remote_sender_id()
|
||||
var item := add_message_item_control(p_message)
|
||||
item.received(peer_id)
|
||||
|
||||
update()
|
||||
|
||||
|
||||
|
||||
func _on_send_button_pressed() -> void:
|
||||
send_chat_message(line_edit.text)
|
||||
|
||||
|
||||
func _on_line_edit_text_submitted(new_text: String) -> void:
|
||||
send_chat_message(new_text)
|
||||
|
||||
|
||||
func _on_visibility_changed() -> void:
|
||||
if not is_visible_in_tree():
|
||||
return
|
||||
|
||||
if is_instance_valid(messages_container):
|
||||
if not messages_container.is_displaying_from(self):
|
||||
messages_container.display_messages(
|
||||
message_item_controls,
|
||||
self
|
||||
)
|
||||
1
addons/tube/inspector/chat_control.gd.uid
Normal file
1
addons/tube/inspector/chat_control.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://ccqt34u2k2rbo
|
||||
41
addons/tube/inspector/chat_control.tscn
Normal file
41
addons/tube/inspector/chat_control.tscn
Normal file
@ -0,0 +1,41 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://dyfuyauko76jj"]
|
||||
|
||||
[ext_resource type="Theme" uid="uid://bcibt73qths3g" path="res://addons/tube/inspector/theme.tres" id="1_ecj0n"]
|
||||
[ext_resource type="Script" uid="uid://ccqt34u2k2rbo" path="res://addons/tube/inspector/chat_control.gd" id="1_yvw80"]
|
||||
[ext_resource type="Texture2D" uid="uid://b4jdl1ipes5d0" path="res://addons/tube/inspector/icons/send_icon.tres" id="2_ho0s2"]
|
||||
|
||||
[node name="ChatControl" type="MarginContainer"]
|
||||
theme = ExtResource("1_ecj0n")
|
||||
script = ExtResource("1_yvw80")
|
||||
metadata/_tab_index = 0
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH2"
|
||||
text = "Chat"
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="LineEdit" type="LineEdit" parent="VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 40)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "Enter message"
|
||||
|
||||
[node name="SendButton" type="Button" parent="VBoxContainer/HBoxContainer"]
|
||||
custom_minimum_size = Vector2(40, 40)
|
||||
layout_mode = 2
|
||||
tooltip_text = "Send"
|
||||
icon = ExtResource("2_ho0s2")
|
||||
icon_alignment = 1
|
||||
expand_icon = true
|
||||
|
||||
[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"]
|
||||
[connection signal="text_submitted" from="VBoxContainer/HBoxContainer/LineEdit" to="." method="_on_line_edit_text_submitted"]
|
||||
[connection signal="pressed" from="VBoxContainer/HBoxContainer/SendButton" to="." method="_on_send_button_pressed"]
|
||||
142
addons/tube/inspector/client_control.gd
Normal file
142
addons/tube/inspector/client_control.gd
Normal file
@ -0,0 +1,142 @@
|
||||
extends Control
|
||||
|
||||
|
||||
const HOLE_PUNCHING_COMPLIANCE_TEXT: Dictionary[TubeNetworkDiagnosisPeer.Compliance, String] = {
|
||||
TubeNetworkDiagnosisPeer.Compliance.UNKNOWN: "Unknown",
|
||||
TubeNetworkDiagnosisPeer.Compliance.YES: "likely to succeed",
|
||||
TubeNetworkDiagnosisPeer.Compliance.NO: "likely to fail",
|
||||
}
|
||||
const HOLE_PUNCHING_COMPLIANCE_COLOR: Dictionary[TubeNetworkDiagnosisPeer.Compliance, Color] = {
|
||||
TubeNetworkDiagnosisPeer.Compliance.UNKNOWN: Color.BEIGE,
|
||||
TubeNetworkDiagnosisPeer.Compliance.YES: Color.PALE_GREEN,
|
||||
TubeNetworkDiagnosisPeer.Compliance.NO: Color.CRIMSON,
|
||||
}
|
||||
|
||||
@export var inspector: EditorTubeClientPanel
|
||||
|
||||
var client: TubeClient:
|
||||
set(x):
|
||||
if client != x:
|
||||
if null != client:
|
||||
|
||||
client._upnp.port_mapped.disconnect(
|
||||
sucess_port_mapping
|
||||
)
|
||||
client._upnp.warning_raised.disconnect(
|
||||
fail_port_mapping
|
||||
)
|
||||
|
||||
if null != x:
|
||||
x._upnp.port_mapped.connect(
|
||||
sucess_port_mapping
|
||||
)
|
||||
x._upnp.warning_raised.connect(
|
||||
fail_port_mapping
|
||||
)
|
||||
|
||||
client = x
|
||||
|
||||
if not is_instance_valid(client):
|
||||
return
|
||||
|
||||
if is_instance_valid(client_label):
|
||||
client_label.text = client.name
|
||||
|
||||
if is_instance_valid(context_label):
|
||||
context_label.text = client.context.resource_name
|
||||
|
||||
if is_instance_valid(app_id_label):
|
||||
app_id_label.text = client.context.app_id
|
||||
|
||||
if is_instance_valid(root_node_label):
|
||||
root_node_label.text = client.multiplayer_root_node.get_path()
|
||||
|
||||
detect_nat()
|
||||
detect_upnp_port_mapping()
|
||||
|
||||
|
||||
var network_diagnosis_peer := TubeNetworkDiagnosisPeer.new(4443)
|
||||
|
||||
@onready var client_label: Label = %ClientLabel
|
||||
@onready var context_label: Label = %ContextLabel
|
||||
@onready var app_id_label: Label = %AppIdLabel
|
||||
@onready var root_node_label: Label = %RootNodeLabel
|
||||
@onready var nat_detection_label: Label = %NATDetectionLabel
|
||||
@onready var upnp_port_mapping_label: Label = %UPNPPortMappingLabel
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
network_diagnosis_peer.warning_raised.connect(
|
||||
_on_network_diagnosis_peer_warning_raised
|
||||
)
|
||||
network_diagnosis_peer.nat_hole_punching_compliance_updated.connect(
|
||||
_on_network_diagnosis_peer_nat_hole_punching_compliance_updated
|
||||
)
|
||||
|
||||
|
||||
func _process(delta: float):
|
||||
network_diagnosis_peer._process(delta)
|
||||
|
||||
|
||||
func _on_network_diagnosis_peer_warning_raised(message: String):
|
||||
if is_instance_valid(inspector):
|
||||
inspector.add_message_item_control("Network diagnosis: " + message).warning()
|
||||
|
||||
|
||||
func _on_network_diagnosis_peer_nat_hole_punching_compliance_updated(compliance: TubeNetworkDiagnosisPeer.Compliance):
|
||||
if is_instance_valid(nat_detection_label):
|
||||
nat_detection_label.text = HOLE_PUNCHING_COMPLIANCE_TEXT[compliance]
|
||||
nat_detection_label.modulate = HOLE_PUNCHING_COMPLIANCE_COLOR[compliance]
|
||||
|
||||
|
||||
func _on_nat_detection_button_pressed() -> void:
|
||||
detect_nat()
|
||||
|
||||
|
||||
func detect_nat():
|
||||
if OS.get_name() == "Web":
|
||||
if is_instance_valid(inspector):
|
||||
inspector.add_message_item_control("NAT hole punching detection is not available on Web").warning()
|
||||
return
|
||||
|
||||
if not is_instance_valid(client):
|
||||
inspector.add_message_item_control("NAT hole punching detection needs a tube client set on inspector").warning()
|
||||
return
|
||||
|
||||
if len(client.context.stun_servers_urls) < 2:
|
||||
if is_instance_valid(inspector):
|
||||
inspector.add_message_item_control("NAT hole punching detection can only be done with 2 or more STUN urls").warning()
|
||||
return
|
||||
|
||||
network_diagnosis_peer.start_nat_hole_punching_detection(client.context.stun_servers_urls)
|
||||
|
||||
|
||||
func _on_upnp_port_mapping_button_pressed() -> void:
|
||||
detect_upnp_port_mapping()
|
||||
|
||||
|
||||
func detect_upnp_port_mapping():
|
||||
if OS.get_name() == "Web":
|
||||
if is_instance_valid(inspector):
|
||||
inspector.add_message_item_control("Port mapping detection is not available on Web").warning()
|
||||
return
|
||||
|
||||
if not is_instance_valid(client):
|
||||
inspector.add_message_item_control("Port mapping detection needs a tube client set on inspector").warning()
|
||||
return
|
||||
|
||||
var port := 4443
|
||||
client._upnp.add_port_mapping(port, port)
|
||||
client._upnp.delete_port_mapping(port)
|
||||
|
||||
|
||||
func sucess_port_mapping(public_port: int, local_port: int):
|
||||
if is_instance_valid(upnp_port_mapping_label):
|
||||
upnp_port_mapping_label.text = HOLE_PUNCHING_COMPLIANCE_TEXT[TubeNetworkDiagnosisPeer.Compliance.YES]
|
||||
upnp_port_mapping_label.modulate = HOLE_PUNCHING_COMPLIANCE_COLOR[TubeNetworkDiagnosisPeer.Compliance.YES]
|
||||
|
||||
|
||||
func fail_port_mapping(message: String):
|
||||
if is_instance_valid(upnp_port_mapping_label):
|
||||
upnp_port_mapping_label.text = HOLE_PUNCHING_COMPLIANCE_TEXT[TubeNetworkDiagnosisPeer.Compliance.NO]
|
||||
upnp_port_mapping_label.modulate = HOLE_PUNCHING_COMPLIANCE_COLOR[TubeNetworkDiagnosisPeer.Compliance.NO]
|
||||
1
addons/tube/inspector/client_control.gd.uid
Normal file
1
addons/tube/inspector/client_control.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dadtx2xi0157
|
||||
120
addons/tube/inspector/client_control.tscn
Normal file
120
addons/tube/inspector/client_control.tscn
Normal file
@ -0,0 +1,120 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://c3p410vwblsb3"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dadtx2xi0157" path="res://addons/tube/inspector/client_control.gd" id="1_xtewr"]
|
||||
[ext_resource type="Theme" uid="uid://bcibt73qths3g" path="res://addons/tube/inspector/theme.tres" id="1_y7bgu"]
|
||||
|
||||
[node name="ClientControl" type="MarginContainer"]
|
||||
offset_right = 245.0
|
||||
offset_bottom = 77.0
|
||||
theme = ExtResource("1_y7bgu")
|
||||
script = ExtResource("1_xtewr")
|
||||
metadata/_tab_index = 0
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ClientContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/ClientContainer"]
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Client"
|
||||
|
||||
[node name="ClientLabel" type="Label" parent="VBoxContainer/ClientContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_type_variation = &"LabelH2"
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="ContextContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/ContextContainer"]
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Context"
|
||||
|
||||
[node name="ContextLabel" type="Label" parent="VBoxContainer/ContextContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_type_variation = &"LabelH2"
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="AppIdContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/AppIdContainer"]
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "App ID"
|
||||
|
||||
[node name="AppIdLabel" type="Label" parent="VBoxContainer/AppIdContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_type_variation = &"LabelH2"
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="RootNodeContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/RootNodeContainer"]
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Root node"
|
||||
|
||||
[node name="RootNodeLabel" type="Label" parent="VBoxContainer/RootNodeContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_type_variation = &"LabelH2"
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="NatContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="NATDetectionButton" type="Button" parent="VBoxContainer/NatContainer"]
|
||||
custom_minimum_size = Vector2(180, 0)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
tooltip_text = "Reload"
|
||||
theme_type_variation = &"FlatButton"
|
||||
text = "NAT hole punching"
|
||||
|
||||
[node name="NATDetectionLabel" type="Label" parent="VBoxContainer/NatContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_type_variation = &"LabelH2"
|
||||
text = "Unknow"
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="UPNPContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="UPNPPortMappingButton" type="Button" parent="VBoxContainer/UPNPContainer"]
|
||||
custom_minimum_size = Vector2(180, 0)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
tooltip_text = "Reload"
|
||||
theme_type_variation = &"FlatButton"
|
||||
text = "UPnP port mapping"
|
||||
|
||||
[node name="UPNPPortMappingLabel" type="Label" parent="VBoxContainer/UPNPContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_type_variation = &"LabelH2"
|
||||
text = "Unknow"
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[connection signal="pressed" from="VBoxContainer/NatContainer/NATDetectionButton" to="." method="_on_nat_detection_button_pressed"]
|
||||
[connection signal="pressed" from="VBoxContainer/UPNPContainer/UPNPPortMappingButton" to="." method="_on_upnp_port_mapping_button_pressed"]
|
||||
7
addons/tube/inspector/icons/clipboard_icon.tres
Normal file
7
addons/tube/inspector/icons/clipboard_icon.tres
Normal file
@ -0,0 +1,7 @@
|
||||
[gd_resource type="AtlasTexture" load_steps=2 format=3 uid="uid://pxj3b3e28fg3"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://bblpobvrbvblp" path="res://addons/tube/inspector/icons/icons_sheet_white.png" id="1_jdxrn"]
|
||||
|
||||
[resource]
|
||||
atlas = ExtResource("1_jdxrn")
|
||||
region = Rect2(100, 1900, 100, 100)
|
||||
7
addons/tube/inspector/icons/error_icon.tres
Normal file
7
addons/tube/inspector/icons/error_icon.tres
Normal file
@ -0,0 +1,7 @@
|
||||
[gd_resource type="AtlasTexture" load_steps=2 format=3 uid="uid://wcdvmyl01v1p"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://bblpobvrbvblp" path="res://addons/tube/inspector/icons/icons_sheet_white.png" id="1_e61nn"]
|
||||
|
||||
[resource]
|
||||
atlas = ExtResource("1_e61nn")
|
||||
region = Rect2(400, 1200, 100, 100)
|
||||
BIN
addons/tube/inspector/icons/icons_sheet_white.png
(Stored with Git LFS)
Normal file
BIN
addons/tube/inspector/icons/icons_sheet_white.png
(Stored with Git LFS)
Normal file
Binary file not shown.
40
addons/tube/inspector/icons/icons_sheet_white.png.import
Normal file
40
addons/tube/inspector/icons/icons_sheet_white.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bblpobvrbvblp"
|
||||
path="res://.godot/imported/icons_sheet_white.png-2f4a5023d48df0787006ef991a38d766.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/tube/inspector/icons/icons_sheet_white.png"
|
||||
dest_files=["res://.godot/imported/icons_sheet_white.png-2f4a5023d48df0787006ef991a38d766.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
7
addons/tube/inspector/icons/info_icon.tres
Normal file
7
addons/tube/inspector/icons/info_icon.tres
Normal file
@ -0,0 +1,7 @@
|
||||
[gd_resource type="AtlasTexture" load_steps=2 format=3 uid="uid://bw1nalj6ph1ul"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://bblpobvrbvblp" path="res://addons/tube/inspector/icons/icons_sheet_white.png" id="1_5n4tk"]
|
||||
|
||||
[resource]
|
||||
atlas = ExtResource("1_5n4tk")
|
||||
region = Rect2(300, 1400, 100, 100)
|
||||
7
addons/tube/inspector/icons/received_icon.tres
Normal file
7
addons/tube/inspector/icons/received_icon.tres
Normal file
@ -0,0 +1,7 @@
|
||||
[gd_resource type="AtlasTexture" load_steps=2 format=3 uid="uid://d3r4nf1lyap1q"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://bblpobvrbvblp" path="res://addons/tube/inspector/icons/icons_sheet_white.png" id="1_b014o"]
|
||||
|
||||
[resource]
|
||||
atlas = ExtResource("1_b014o")
|
||||
region = Rect2(0, 1200, 100, 100)
|
||||
7
addons/tube/inspector/icons/send_icon.tres
Normal file
7
addons/tube/inspector/icons/send_icon.tres
Normal file
@ -0,0 +1,7 @@
|
||||
[gd_resource type="AtlasTexture" load_steps=2 format=3 uid="uid://b4jdl1ipes5d0"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://bblpobvrbvblp" path="res://addons/tube/inspector/icons/icons_sheet_white.png" id="1_rmrpv"]
|
||||
|
||||
[resource]
|
||||
atlas = ExtResource("1_rmrpv")
|
||||
region = Rect2(100, 0, 100, 100)
|
||||
7
addons/tube/inspector/icons/sent_icon.tres
Normal file
7
addons/tube/inspector/icons/sent_icon.tres
Normal file
@ -0,0 +1,7 @@
|
||||
[gd_resource type="AtlasTexture" load_steps=2 format=3 uid="uid://c141mpg6pwbt8"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://bblpobvrbvblp" path="res://addons/tube/inspector/icons/icons_sheet_white.png" id="1_yfsny"]
|
||||
|
||||
[resource]
|
||||
atlas = ExtResource("1_yfsny")
|
||||
region = Rect2(0, 900, 100, 100)
|
||||
7
addons/tube/inspector/icons/success_icon.tres
Normal file
7
addons/tube/inspector/icons/success_icon.tres
Normal file
@ -0,0 +1,7 @@
|
||||
[gd_resource type="AtlasTexture" load_steps=2 format=3 uid="uid://vj4v31qi6u4d"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://bblpobvrbvblp" path="res://addons/tube/inspector/icons/icons_sheet_white.png" id="1_43xo5"]
|
||||
|
||||
[resource]
|
||||
atlas = ExtResource("1_43xo5")
|
||||
region = Rect2(100, 1000, 100, 100)
|
||||
7
addons/tube/inspector/icons/warning_icon.tres
Normal file
7
addons/tube/inspector/icons/warning_icon.tres
Normal file
@ -0,0 +1,7 @@
|
||||
[gd_resource type="AtlasTexture" load_steps=2 format=3 uid="uid://cxdxdrs7t6fbp"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://bblpobvrbvblp" path="res://addons/tube/inspector/icons/icons_sheet_white.png" id="1_mgvxc"]
|
||||
|
||||
[resource]
|
||||
atlas = ExtResource("1_mgvxc")
|
||||
region = Rect2(0, 1800, 100, 100)
|
||||
145
addons/tube/inspector/local_signaling_control.gd
Normal file
145
addons/tube/inspector/local_signaling_control.gd
Normal file
@ -0,0 +1,145 @@
|
||||
class_name EditorTubeLocalSignalingControl extends Control
|
||||
## @experimental: This class is used as part of the TubeClientDebugPanel scene and is part of a scene. Should not be used as itself.
|
||||
|
||||
|
||||
const MESSAGE_ITEM_CONTROL_SCENE := preload("uid://cfsei3airwx4s")
|
||||
|
||||
|
||||
@export var messages_container: EditorTubeMessagesContainer
|
||||
@export var max_messages_amount: int = 100
|
||||
|
||||
|
||||
var local_signaling_peer: TubeLocalSignalingPeer:
|
||||
set(x):
|
||||
|
||||
if null != local_signaling_peer:
|
||||
local_signaling_peer.warning_raised.disconnect(
|
||||
_on_local_signaling_peer_warning_raised
|
||||
)
|
||||
local_signaling_peer.data_sent.disconnect(
|
||||
_on_local_signaling_peer_data_sent
|
||||
)
|
||||
local_signaling_peer.received_data.disconnect(
|
||||
_on_local_signaling_peer_data_received
|
||||
)
|
||||
|
||||
if null != x:
|
||||
x.warning_raised.connect(
|
||||
_on_local_signaling_peer_warning_raised
|
||||
)
|
||||
x.data_sent.connect(
|
||||
_on_local_signaling_peer_data_sent
|
||||
)
|
||||
x.received_data.connect(
|
||||
_on_local_signaling_peer_data_received
|
||||
)
|
||||
|
||||
local_signaling_peer = x
|
||||
update()
|
||||
|
||||
|
||||
var message_item_controls: Array[EditorTubeMessagesItemControl] = []
|
||||
var message_item_button_group := ButtonGroup.new()
|
||||
|
||||
@onready var name_label: Label = %NameLabel
|
||||
#@onready var state_indicator: Control = %StateIndicator
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
message_item_button_group.allow_unpress = true
|
||||
update()
|
||||
|
||||
|
||||
func update():
|
||||
if is_instance_valid(local_signaling_peer):
|
||||
|
||||
if is_instance_valid(name_label):
|
||||
name_label.text = str(local_signaling_peer.udp_peer.get_local_port())
|
||||
|
||||
if is_instance_valid(messages_container):
|
||||
if messages_container.is_displaying_from(self):
|
||||
messages_container.display_messages(
|
||||
message_item_controls,
|
||||
self
|
||||
)
|
||||
|
||||
else:
|
||||
if is_instance_valid(name_label):
|
||||
name_label.text = "Unset"
|
||||
|
||||
|
||||
func add_message_item_control(data) -> EditorTubeMessagesItemControl:
|
||||
if max_messages_amount <= message_item_controls.size():
|
||||
var item := message_item_controls.pop_front()
|
||||
item.queue_free()
|
||||
|
||||
var message_item_control:= MESSAGE_ITEM_CONTROL_SCENE.instantiate()
|
||||
message_item_controls.append(message_item_control)
|
||||
message_item_control.data = data
|
||||
message_item_control.button_group = message_item_button_group
|
||||
return message_item_control
|
||||
|
||||
|
||||
func _on_local_signaling_peer_warning_raised(message: String):
|
||||
add_message_item_control(message).warning()
|
||||
update()
|
||||
|
||||
|
||||
#func _on_local_signaling_peer_connected():
|
||||
#add_message_item_control("Connected").success()
|
||||
#update()
|
||||
|
||||
|
||||
#func _on_local_signaling_peer_failed():
|
||||
#add_message_item_control(
|
||||
#"Connection failed: {error}".format({
|
||||
#"error": local_signaling_peer.error_message
|
||||
#})
|
||||
#).error()
|
||||
#update()
|
||||
#
|
||||
#
|
||||
#func _on_local_signaling_peer_disconnected():
|
||||
#add_message_item_control("Disconneted")
|
||||
#update()
|
||||
#
|
||||
#
|
||||
#func _on_local_signaling_peer_state_changed():
|
||||
##if WebSocketPeer.STATE_OPEN == local_signaling_peer.state:
|
||||
##add_message_item_control("Connection open")
|
||||
#
|
||||
#if WebSocketPeer.STATE_CLOSING == local_signaling_peer.state:
|
||||
#add_message_item_control("Connection closing")
|
||||
#
|
||||
##elif WebSocketPeer.STATE_CLOSED == local_signaling_peer.state:
|
||||
##add_message_item_control("Connection closed")
|
||||
#
|
||||
#update()
|
||||
|
||||
|
||||
func _on_local_signaling_peer_data_received(data: Variant, address: String, port: int):
|
||||
var control := add_message_item_control(data)
|
||||
control.received()
|
||||
control.from_address = address
|
||||
control.from_id = port
|
||||
update()
|
||||
|
||||
|
||||
func _on_local_signaling_peer_data_sent(data: Dictionary, address: String, port: int):
|
||||
var control := add_message_item_control(data)
|
||||
control.sent()
|
||||
control.from_address = address
|
||||
control.from_id = port
|
||||
update()
|
||||
|
||||
|
||||
func _on_visibility_changed() -> void:
|
||||
if not is_visible_in_tree():
|
||||
return
|
||||
|
||||
if is_instance_valid(messages_container):
|
||||
if not messages_container.is_displaying_from(self):
|
||||
messages_container.display_messages(
|
||||
message_item_controls,
|
||||
self
|
||||
)
|
||||
1
addons/tube/inspector/local_signaling_control.gd.uid
Normal file
1
addons/tube/inspector/local_signaling_control.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://c77pnnu05pblc
|
||||
20
addons/tube/inspector/local_signaling_control.tscn
Normal file
20
addons/tube/inspector/local_signaling_control.tscn
Normal file
@ -0,0 +1,20 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://5f8u55hvqq4w"]
|
||||
|
||||
[ext_resource type="Theme" uid="uid://bcibt73qths3g" path="res://addons/tube/inspector/theme.tres" id="1_gfvfe"]
|
||||
[ext_resource type="Script" uid="uid://c77pnnu05pblc" path="res://addons/tube/inspector/local_signaling_control.gd" id="2_gfvfe"]
|
||||
|
||||
[node name="LocalSignalingControl" type="MarginContainer"]
|
||||
offset_right = 217.0
|
||||
offset_bottom = 24.0
|
||||
theme = ExtResource("1_gfvfe")
|
||||
script = ExtResource("2_gfvfe")
|
||||
|
||||
[node name="NameLabel" type="Label" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "PORT"
|
||||
text_overrun_behavior = 1
|
||||
|
||||
[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"]
|
||||
49
addons/tube/inspector/message_control.gd
Normal file
49
addons/tube/inspector/message_control.gd
Normal file
@ -0,0 +1,49 @@
|
||||
class_name EditorTubeMessageControl extends Control
|
||||
## @experimental: This class is used as part of the TubeClientDebugPanel scene and is part of a scene. Should not be used as itself.
|
||||
|
||||
var message_item_control: EditorTubeMessagesItemControl:
|
||||
set(x):
|
||||
message_item_control = x
|
||||
|
||||
if null == message_item_control:
|
||||
hide()
|
||||
return
|
||||
|
||||
show()
|
||||
|
||||
if is_instance_valid(type_texture_rect):
|
||||
type_texture_rect.texture = message_item_control.icons.get(message_item_control.type)
|
||||
type_texture_rect.modulate = message_item_control.colors.get(message_item_control.type)
|
||||
|
||||
if is_instance_valid(time_label):
|
||||
time_label.text = message_item_control.time
|
||||
|
||||
if is_instance_valid(from_label):
|
||||
from_label.visible = bool(message_item_control.from_id)
|
||||
from_label.text = EditorTubePeerItemControl.get_peer_string(
|
||||
message_item_control.from_id
|
||||
)
|
||||
from_label.modulate = EditorTubePeerItemControl.get_peer_color(
|
||||
message_item_control.from_id
|
||||
)
|
||||
|
||||
if is_instance_valid(message_code_edit):
|
||||
if message_item_control.data is String:
|
||||
message_code_edit.text = message_item_control.data
|
||||
|
||||
if not message_item_control.data is String:
|
||||
message_code_edit.text = JSON.stringify(message_item_control.data, " ")
|
||||
|
||||
|
||||
@onready var type_texture_rect: TextureRect = %TypeTextureRect
|
||||
@onready var time_label: Label = %TimeLabel
|
||||
@onready var from_label: Label = %FromLabel
|
||||
@onready var message_code_edit: CodeEdit = %MessageCodeEdit
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
hide()
|
||||
|
||||
|
||||
func _on_clipboard_button_pressed() -> void:
|
||||
DisplayServer.clipboard_set(str(message_item_control.data))
|
||||
1
addons/tube/inspector/message_control.gd.uid
Normal file
1
addons/tube/inspector/message_control.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://chqe7mwhwv255
|
||||
75
addons/tube/inspector/message_control.tscn
Normal file
75
addons/tube/inspector/message_control.tscn
Normal file
@ -0,0 +1,75 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://bi8vgsoslhvrb"]
|
||||
|
||||
[ext_resource type="Theme" uid="uid://bcibt73qths3g" path="res://addons/tube/inspector/theme.tres" id="1_5ms28"]
|
||||
[ext_resource type="Script" uid="uid://chqe7mwhwv255" path="res://addons/tube/inspector/message_control.gd" id="1_gw4sm"]
|
||||
[ext_resource type="Texture2D" uid="uid://pxj3b3e28fg3" path="res://addons/tube/inspector/icons/clipboard_icon.tres" id="2_78cti"]
|
||||
[ext_resource type="Texture2D" uid="uid://c141mpg6pwbt8" path="res://addons/tube/inspector/icons/sent_icon.tres" id="3_qi5c7"]
|
||||
|
||||
[node name="MessageControl" type="PanelContainer"]
|
||||
anchors_preset = 9
|
||||
anchor_bottom = 1.0
|
||||
offset_right = 238.0
|
||||
grow_vertical = 2
|
||||
theme = ExtResource("1_5ms28")
|
||||
theme_type_variation = &"PanelH1Container"
|
||||
script = ExtResource("1_gw4sm")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="TypeTextureRect" type="TextureRect" parent="VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(32, 32)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
mouse_filter = 2
|
||||
texture = ExtResource("3_qi5c7")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="TimeLabel" type="Label" parent="VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(70, 0)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 1
|
||||
theme_type_variation = &"LabelH2"
|
||||
text = "00:00:00"
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="FromLabel" type="Label" parent="VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 1
|
||||
theme_type_variation = &"LabelH2"
|
||||
text = "00"
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="ClipboardButton" type="Button" parent="VBoxContainer/HBoxContainer"]
|
||||
custom_minimum_size = Vector2(40, 40)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 10
|
||||
tooltip_text = "Copy message to clipboard"
|
||||
theme_type_variation = &"ButtonFlat"
|
||||
icon = ExtResource("2_78cti")
|
||||
icon_alignment = 1
|
||||
expand_icon = true
|
||||
|
||||
[node name="MessageCodeEdit" type="CodeEdit" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
editable = false
|
||||
context_menu_enabled = false
|
||||
virtual_keyboard_enabled = false
|
||||
virtual_keyboard_show_on_focus = false
|
||||
wrap_mode = 1
|
||||
highlight_all_occurrences = true
|
||||
gutters_draw_line_numbers = true
|
||||
auto_brace_completion_highlight_matching = true
|
||||
|
||||
[connection signal="pressed" from="VBoxContainer/HBoxContainer/ClipboardButton" to="." method="_on_clipboard_button_pressed"]
|
||||
158
addons/tube/inspector/message_item_control.gd
Normal file
158
addons/tube/inspector/message_item_control.gd
Normal file
@ -0,0 +1,158 @@
|
||||
class_name EditorTubeMessagesItemControl extends Control
|
||||
## @experimental: This class is used as part of the TubeClientDebugPanel scene and is part of a scene. Should not be used as itself.
|
||||
|
||||
|
||||
enum Type {INFO, ERROR, WARNING, SENT, RECEIVED, SUCCESS}
|
||||
|
||||
|
||||
@export var type := Type.INFO:
|
||||
set(x):
|
||||
type = x
|
||||
if is_instance_valid(type_texture_rect):
|
||||
type_texture_rect.texture = icons.get(type)
|
||||
type_texture_rect.modulate = colors.get(type)
|
||||
|
||||
@export var message_control: EditorTubeMessageControl
|
||||
|
||||
|
||||
@export var icons: Dictionary[Type, Texture] = {}
|
||||
|
||||
@export var colors: Dictionary[Type, Color] = {
|
||||
Type.INFO: Color.BEIGE,
|
||||
Type.ERROR: Color.CRIMSON,
|
||||
Type.WARNING: Color.GOLDENROD,
|
||||
Type.SUCCESS: Color.PALE_GREEN,
|
||||
Type.SENT: Color.DODGER_BLUE,
|
||||
Type.RECEIVED: Color.PLUM,
|
||||
}
|
||||
|
||||
@export var strings: Dictionary[Type, String] = {
|
||||
Type.INFO: "-",
|
||||
Type.ERROR: "X",
|
||||
Type.WARNING: "!",
|
||||
Type.SUCCESS: "*",
|
||||
Type.SENT: "<",
|
||||
Type.RECEIVED: ">",
|
||||
}
|
||||
|
||||
|
||||
var data: Variant:
|
||||
set(x):
|
||||
data = x
|
||||
if is_instance_valid(data_label):
|
||||
data_label.text = str(data)
|
||||
|
||||
|
||||
var from_address: String:
|
||||
set(x):
|
||||
from_address = x
|
||||
if from_address.is_empty():
|
||||
return
|
||||
|
||||
if is_instance_valid(from_label):
|
||||
from_label.text = "{address}:{port}".format({
|
||||
"address": from_address,
|
||||
"port": str(from_id)
|
||||
})
|
||||
|
||||
if is_instance_valid(from_label):
|
||||
from_label.modulate = EditorTubePeerItemControl.get_peer_color(from_address.hash() + from_id)
|
||||
|
||||
|
||||
var from_id: int:
|
||||
set(x):
|
||||
from_id = x
|
||||
if is_instance_valid(from_label):
|
||||
if from_address.is_empty():
|
||||
from_label.text = EditorTubePeerItemControl.get_peer_string(from_id)
|
||||
from_label.visible = bool(from_id)
|
||||
else:
|
||||
from_label.text = "{address}:{port}".format({
|
||||
"address": from_address,
|
||||
"port": str(from_id)
|
||||
})
|
||||
|
||||
if is_instance_valid(from_label):
|
||||
if from_address.is_empty():
|
||||
from_label.modulate = EditorTubePeerItemControl.get_peer_color(from_id)
|
||||
else:
|
||||
from_label.modulate = EditorTubePeerItemControl.get_peer_color(from_address.hash() + from_id)
|
||||
|
||||
|
||||
var time: String:
|
||||
set(x):
|
||||
time = x
|
||||
if is_instance_valid(time_label):
|
||||
time_label.text = time
|
||||
|
||||
|
||||
var button_group: ButtonGroup
|
||||
|
||||
@onready var button: Button = %Button
|
||||
@onready var type_texture_rect: TextureRect = %TypeTextureRect
|
||||
@onready var time_label: Label = %TimeLabel
|
||||
@onready var from_label: Label = %FromLabel
|
||||
@onready var data_label: Label = %DataLabel
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
time = Time.get_time_string_from_system()
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
type = type
|
||||
data = data
|
||||
time = time
|
||||
from_id = from_id
|
||||
button.button_group = button_group
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return "{type}\t{time}\t{address}\t{from}\t{data}".format({
|
||||
"type": strings.get(type, "-"),
|
||||
"time": time,
|
||||
"address": from_address,
|
||||
"from": from_id,
|
||||
"data": str(data),
|
||||
})
|
||||
|
||||
|
||||
func _on_button_toggled(toggled_on: bool) -> void:
|
||||
if not is_instance_valid(message_control):
|
||||
return
|
||||
|
||||
if toggled_on:
|
||||
message_control.message_item_control = self
|
||||
else:
|
||||
message_control.message_item_control = null
|
||||
|
||||
#pressed.emit()
|
||||
|
||||
|
||||
func is_pressed() -> bool:
|
||||
return button.button_pressed
|
||||
|
||||
|
||||
func info():
|
||||
type = Type.INFO
|
||||
|
||||
|
||||
func error():
|
||||
type = Type.ERROR
|
||||
|
||||
|
||||
func warning():
|
||||
type = Type.WARNING
|
||||
|
||||
|
||||
func success():
|
||||
type = Type.SUCCESS
|
||||
|
||||
|
||||
func received(p_from_id: int = 0):
|
||||
type = Type.RECEIVED
|
||||
from_id = p_from_id
|
||||
|
||||
|
||||
func sent():
|
||||
type = Type.SENT
|
||||
1
addons/tube/inspector/message_item_control.gd.uid
Normal file
1
addons/tube/inspector/message_item_control.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://pbxxonhemirk
|
||||
83
addons/tube/inspector/message_item_control.tscn
Normal file
83
addons/tube/inspector/message_item_control.tscn
Normal file
@ -0,0 +1,83 @@
|
||||
[gd_scene load_steps=9 format=3 uid="uid://cfsei3airwx4s"]
|
||||
|
||||
[ext_resource type="Theme" uid="uid://bcibt73qths3g" path="res://addons/tube/inspector/theme.tres" id="1_5a58g"]
|
||||
[ext_resource type="Script" uid="uid://pbxxonhemirk" path="res://addons/tube/inspector/message_item_control.gd" id="1_eynp4"]
|
||||
[ext_resource type="Texture2D" uid="uid://bw1nalj6ph1ul" path="res://addons/tube/inspector/icons/info_icon.tres" id="2_83orp"]
|
||||
[ext_resource type="Texture2D" uid="uid://wcdvmyl01v1p" path="res://addons/tube/inspector/icons/error_icon.tres" id="3_hv8fo"]
|
||||
[ext_resource type="Texture2D" uid="uid://c141mpg6pwbt8" path="res://addons/tube/inspector/icons/sent_icon.tres" id="4_815v1"]
|
||||
[ext_resource type="Texture2D" uid="uid://cxdxdrs7t6fbp" path="res://addons/tube/inspector/icons/warning_icon.tres" id="4_hv8fo"]
|
||||
[ext_resource type="Texture2D" uid="uid://d3r4nf1lyap1q" path="res://addons/tube/inspector/icons/received_icon.tres" id="5_5a58g"]
|
||||
[ext_resource type="Texture2D" uid="uid://vj4v31qi6u4d" path="res://addons/tube/inspector/icons/success_icon.tres" id="7_815v1"]
|
||||
|
||||
[node name="MessageItemControl" type="MarginContainer"]
|
||||
custom_minimum_size = Vector2(0, 32)
|
||||
anchors_preset = 10
|
||||
anchor_right = 1.0
|
||||
offset_bottom = 27.0
|
||||
grow_horizontal = 2
|
||||
size_flags_horizontal = 3
|
||||
theme = ExtResource("1_5a58g")
|
||||
script = ExtResource("1_eynp4")
|
||||
icons = Dictionary[int, Texture]({
|
||||
0: ExtResource("2_83orp"),
|
||||
1: ExtResource("3_hv8fo"),
|
||||
2: ExtResource("4_hv8fo"),
|
||||
3: ExtResource("4_815v1"),
|
||||
4: ExtResource("5_5a58g"),
|
||||
5: ExtResource("7_815v1")
|
||||
})
|
||||
|
||||
[node name="Button" type="Button" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"ButtonFlat"
|
||||
toggle_mode = true
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="."]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/margin_left = 4
|
||||
theme_override_constants/margin_top = 2
|
||||
theme_override_constants/margin_right = 4
|
||||
theme_override_constants/margin_bottom = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="TypeTextureRect" type="TextureRect" parent="MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(28, 28)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
mouse_filter = 2
|
||||
texture = ExtResource("4_815v1")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="TimeLabel" type="Label" parent="MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 1
|
||||
theme_type_variation = &"HeaderSmall"
|
||||
text = "00:00:00"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="FromLabel" type="Label" parent="MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"HeaderSmall"
|
||||
text = "00"
|
||||
|
||||
[node name="DataLabel" type="Label" parent="MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 1
|
||||
theme_type_variation = &"HeaderSmall"
|
||||
text = "DATA"
|
||||
vertical_alignment = 1
|
||||
text_overrun_behavior = 1
|
||||
|
||||
[connection signal="toggled" from="Button" to="." method="_on_button_toggled"]
|
||||
71
addons/tube/inspector/messages_container.gd
Normal file
71
addons/tube/inspector/messages_container.gd
Normal file
@ -0,0 +1,71 @@
|
||||
class_name EditorTubeMessagesContainer extends Control
|
||||
## @experimental: This class is used as part of the TubeClientDebugPanel scene and is part of a scene. Should not be used as itself.
|
||||
|
||||
@export var message_control: EditorTubeMessageControl
|
||||
@export var max_messages_amount := 100:
|
||||
set(x):
|
||||
max_messages_amount = x
|
||||
if is_instance_valid(messages_amount_label):
|
||||
messages_amount_label.text = "(last {value})".format({
|
||||
"value": max_messages_amount
|
||||
})
|
||||
|
||||
|
||||
var _display_from: Object
|
||||
|
||||
|
||||
@onready var messages_amount_label: Label = %MessagesAmountLabel
|
||||
@onready var scroll_container: ScrollContainer = %ScrollContainer
|
||||
@onready var list_container: Container = %ListContainer
|
||||
|
||||
|
||||
func is_displaying_from(p_from) -> bool:
|
||||
return _display_from == p_from
|
||||
|
||||
|
||||
func display_messages(p_controls: Array[EditorTubeMessagesItemControl], p_from: Object = null):
|
||||
|
||||
_display_from = p_from
|
||||
|
||||
for child in list_container.get_children():
|
||||
list_container.remove_child(child)
|
||||
|
||||
var last_control
|
||||
var pressed_control
|
||||
for control in p_controls:
|
||||
if not is_instance_valid(control):
|
||||
continue
|
||||
|
||||
if control.is_queued_for_deletion():
|
||||
continue
|
||||
|
||||
control.message_control = message_control
|
||||
list_container.add_child(control)
|
||||
last_control = control
|
||||
|
||||
if control.is_pressed():
|
||||
pressed_control = control
|
||||
|
||||
|
||||
if pressed_control:
|
||||
last_control = pressed_control
|
||||
message_control.message_item_control = last_control
|
||||
else:
|
||||
message_control.message_item_control = null
|
||||
|
||||
if not last_control:
|
||||
return
|
||||
|
||||
await get_tree().process_frame
|
||||
if not is_instance_valid(last_control) or last_control.is_queued_for_deletion():
|
||||
return
|
||||
scroll_container.ensure_control_visible(last_control)
|
||||
|
||||
|
||||
|
||||
func _on_clipboard_button_pressed() -> void:
|
||||
var clipboard := ""
|
||||
for child in list_container.get_children():
|
||||
clipboard += str(child) + "\n"
|
||||
|
||||
DisplayServer.clipboard_set(clipboard)
|
||||
1
addons/tube/inspector/messages_container.gd.uid
Normal file
1
addons/tube/inspector/messages_container.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bh6v4r3cb8nfa
|
||||
61
addons/tube/inspector/messages_container.tscn
Normal file
61
addons/tube/inspector/messages_container.tscn
Normal file
@ -0,0 +1,61 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://btfc8o5xfs14w"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bh6v4r3cb8nfa" path="res://addons/tube/inspector/messages_container.gd" id="1_cbqc8"]
|
||||
[ext_resource type="Theme" uid="uid://bcibt73qths3g" path="res://addons/tube/inspector/theme.tres" id="1_kof4f"]
|
||||
[ext_resource type="Texture2D" uid="uid://pxj3b3e28fg3" path="res://addons/tube/inspector/icons/clipboard_icon.tres" id="2_kj7ih"]
|
||||
|
||||
[node name="MessagesContainer" type="MarginContainer"]
|
||||
custom_minimum_size = Vector2(0, 96)
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_right = -366.0
|
||||
offset_bottom = -194.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme = ExtResource("1_kof4f")
|
||||
script = ExtResource("1_cbqc8")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Messages"
|
||||
|
||||
[node name="MessagesAmountLabel" type="Label" parent="VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "(last 100)"
|
||||
|
||||
[node name="ClipboardButton" type="Button" parent="VBoxContainer/HBoxContainer"]
|
||||
custom_minimum_size = Vector2(40, 40)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 10
|
||||
size_flags_vertical = 0
|
||||
tooltip_text = "Copy all messages to clipboard"
|
||||
theme_type_variation = &"ButtonFlat"
|
||||
icon = ExtResource("2_kj7ih")
|
||||
icon_alignment = 1
|
||||
expand_icon = true
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
follow_focus = true
|
||||
horizontal_scroll_mode = 0
|
||||
vertical_scroll_mode = 4
|
||||
|
||||
[node name="ListContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
[connection signal="pressed" from="VBoxContainer/HBoxContainer/ClipboardButton" to="." method="_on_clipboard_button_pressed"]
|
||||
185
addons/tube/inspector/peer_control.gd
Normal file
185
addons/tube/inspector/peer_control.gd
Normal file
@ -0,0 +1,185 @@
|
||||
class_name EditorTubePeerControl extends Control
|
||||
## @experimental: This class is used as part of the TubeClientDebugPanel scene and is part of a scene. Should not be used as itself.
|
||||
|
||||
const STATE_COLOR_DEFAULT := Color.WHITE
|
||||
const STATE_TEXT_DEFAULT := "Unknown"
|
||||
|
||||
|
||||
@export var client: TubeClient
|
||||
@export var peer_item: EditorTubePeerItemControl:
|
||||
set(x):
|
||||
show()
|
||||
|
||||
if peer_item != x:
|
||||
latency_label.text = "Unknown..."
|
||||
|
||||
if null != peer_item:
|
||||
peer_item.updated.disconnect(update)
|
||||
|
||||
if null != x:
|
||||
x.updated.connect(update)
|
||||
|
||||
if null != x:
|
||||
if not waiting_for_pong:
|
||||
send_ping(x.peer)
|
||||
|
||||
if is_instance_valid(messages_container):
|
||||
if peer_item != x or not messages_container.is_displaying_from(self):
|
||||
messages_container.display_messages(
|
||||
x.message_item_controls,
|
||||
self
|
||||
)
|
||||
|
||||
if is_instance_valid(id_label):
|
||||
id_label.text = EditorTubePeerItemControl.get_peer_string(x.peer.id)
|
||||
id_label.modulate = EditorTubePeerItemControl.get_peer_color(x.peer.id)
|
||||
|
||||
if is_instance_valid(channels_containers):
|
||||
for child in channels_containers.get_children():
|
||||
channels_containers.remove_child(child)
|
||||
|
||||
for channel_item_control in x.channel_item_controls:
|
||||
channels_containers.add_child(
|
||||
channel_item_control
|
||||
)
|
||||
|
||||
peer_item = x
|
||||
update()
|
||||
|
||||
@export var messages_container: EditorTubeMessagesContainer
|
||||
|
||||
|
||||
var last_ping_time: float
|
||||
var waiting_for_pong := false
|
||||
|
||||
@onready var ping_timer: Timer = %PingTimer
|
||||
|
||||
@onready var id_label: Label = %IdLabel
|
||||
|
||||
@onready var connection_state_indicator: Control = %ConnectionStateIndicator
|
||||
@onready var connection_state_label: Label = %ConnectionStateLabel
|
||||
|
||||
@onready var gathering_state_indicator: Control = %GatheringStateIndicator
|
||||
@onready var gathering_state_label: Label = %GatheringStateLabel
|
||||
|
||||
@onready var signaling_state_indicator: Control = %SignalingStateIndicator
|
||||
@onready var signaling_state_label: Label = %SignalingStateLabel
|
||||
|
||||
@onready var channels_containers: Container = %ChannelsContainer
|
||||
|
||||
@onready var connection_time_label: Label = %ConnectingTimeLabel
|
||||
@onready var up_time_label: Label = %UpTimeLabel
|
||||
@onready var latency_label: Label = %LatencyLabel
|
||||
|
||||
@onready var fake_disconnection_timer: Timer = %FakeDisconnectionTimer
|
||||
@onready var fake_disconnection_button: Button = %FakeDisconnectionButton
|
||||
@onready var fake_disconnection_spin_box: SpinBox = %FakeDisconnectionSpinBox
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
hide()
|
||||
|
||||
|
||||
func update():
|
||||
if is_instance_valid(connection_state_indicator):
|
||||
connection_state_indicator.modulate = STATE_COLOR_DEFAULT if not peer_item else peer_item.get_connection_state_color()
|
||||
|
||||
if is_instance_valid(connection_state_label):
|
||||
connection_state_label.text = STATE_TEXT_DEFAULT if not peer_item else peer_item.get_connection_state_text()
|
||||
|
||||
if is_instance_valid(gathering_state_indicator):
|
||||
gathering_state_indicator.modulate = STATE_COLOR_DEFAULT if not peer_item else peer_item.get_gathering_state_color()
|
||||
|
||||
if is_instance_valid(gathering_state_label):
|
||||
gathering_state_label.text = STATE_TEXT_DEFAULT if not peer_item else peer_item.get_gathering_state_text()
|
||||
|
||||
if is_instance_valid(signaling_state_indicator):
|
||||
signaling_state_indicator.modulate = STATE_COLOR_DEFAULT if not peer_item else peer_item.get_signaling_state_color()
|
||||
|
||||
if is_instance_valid(signaling_state_label):
|
||||
signaling_state_label.text = STATE_TEXT_DEFAULT if not peer_item else peer_item.get_signaling_state_text()
|
||||
|
||||
|
||||
if null == peer_item:
|
||||
return
|
||||
|
||||
if is_instance_valid(fake_disconnection_button):
|
||||
fake_disconnection_button.disabled = not peer_item.peer.is_peer_connected()
|
||||
|
||||
if is_instance_valid(messages_container):
|
||||
if messages_container.is_displaying_from(self):
|
||||
messages_container.display_messages(
|
||||
peer_item.message_item_controls,
|
||||
self
|
||||
)
|
||||
|
||||
|
||||
func add_channel_item_control(channel_item_control: EditorTubeChannelItemControl):
|
||||
channels_containers.add_child(channel_item_control)
|
||||
|
||||
|
||||
func send_ping(peer: TubePeer):
|
||||
if not peer.is_peer_connected():
|
||||
return
|
||||
|
||||
|
||||
last_ping_time = Time.get_ticks_msec()
|
||||
ping_timer.start(5.0)
|
||||
waiting_for_pong = true
|
||||
receive_ping.rpc_id(peer.id)
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func receive_ping():
|
||||
var sender_id := multiplayer.get_remote_sender_id()
|
||||
send_pong(sender_id)
|
||||
|
||||
|
||||
func send_pong(to: int):
|
||||
receive_pong.rpc_id(to)
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func receive_pong():
|
||||
waiting_for_pong = false
|
||||
ping_timer.start(1.0)
|
||||
|
||||
var ping := Time.get_ticks_msec() - last_ping_time
|
||||
latency_label.text = str(ping).pad_decimals(0)
|
||||
|
||||
|
||||
func _on_ping_timer_timeout() -> void:
|
||||
if not is_visible_in_tree():
|
||||
return
|
||||
|
||||
if not peer_item:
|
||||
return
|
||||
|
||||
send_ping(peer_item.peer)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if null == peer_item:
|
||||
return
|
||||
|
||||
connection_time_label.text = str(
|
||||
peer_item.peer.connecting_time
|
||||
).pad_decimals(3)
|
||||
up_time_label.text = str(
|
||||
peer_item.peer.up_time
|
||||
).pad_decimals(3)
|
||||
|
||||
|
||||
func _on_fake_disconnection_button_pressed() -> void:
|
||||
if not fake_disconnection_timer.is_stopped():
|
||||
fake_disconnection_timer.stop()
|
||||
fake_disconnection_timer.timeout.emit()
|
||||
|
||||
var peer := peer_item.peer
|
||||
peer._disconnected()
|
||||
|
||||
fake_disconnection_timer.start(
|
||||
fake_disconnection_spin_box.value
|
||||
)
|
||||
await fake_disconnection_timer.timeout
|
||||
peer._connected()
|
||||
1
addons/tube/inspector/peer_control.gd.uid
Normal file
1
addons/tube/inspector/peer_control.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://qcomcx6e48wn
|
||||
229
addons/tube/inspector/peer_control.tscn
Normal file
229
addons/tube/inspector/peer_control.tscn
Normal file
@ -0,0 +1,229 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://ckrifxh4o768d"]
|
||||
|
||||
[ext_resource type="Theme" uid="uid://bcibt73qths3g" path="res://addons/tube/inspector/theme.tres" id="1_6pmfw"]
|
||||
[ext_resource type="Script" uid="uid://qcomcx6e48wn" path="res://addons/tube/inspector/peer_control.gd" id="1_kiw0h"]
|
||||
|
||||
[node name="PeerControl" type="MarginContainer"]
|
||||
offset_right = 523.0
|
||||
offset_bottom = 161.0
|
||||
theme = ExtResource("1_6pmfw")
|
||||
script = ExtResource("1_kiw0h")
|
||||
|
||||
[node name="PingTimer" type="Timer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
one_shot = true
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HeaderContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="IdLabel" type="Label" parent="VBoxContainer/HeaderContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "00000000000000000000"
|
||||
|
||||
[node name="AddressLabel" type="Label" parent="VBoxContainer/HeaderContainer"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "00000000000000000000"
|
||||
|
||||
[node name="GridContainer" type="GridContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
columns = 2
|
||||
|
||||
[node name="StatesContainer" type="VBoxContainer" parent="VBoxContainer/GridContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ConnectionStateContainer" type="HBoxContainer" parent="VBoxContainer/GridContainer/StatesContainer"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/GridContainer/StatesContainer/ConnectionStateContainer"]
|
||||
custom_minimum_size = Vector2(114, 0)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Connection"
|
||||
|
||||
[node name="ConnectionStateIndicator" type="Panel" parent="VBoxContainer/GridContainer/StatesContainer/ConnectionStateContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(24, 12)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_type_variation = &"PanelIndicator"
|
||||
|
||||
[node name="ConnectionStateLabel" type="Label" parent="VBoxContainer/GridContainer/StatesContainer/ConnectionStateContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH2"
|
||||
text = "STATE"
|
||||
|
||||
[node name="GatheringStateContainer" type="HBoxContainer" parent="VBoxContainer/GridContainer/StatesContainer"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/GridContainer/StatesContainer/GatheringStateContainer"]
|
||||
custom_minimum_size = Vector2(114, 0)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Gathering"
|
||||
|
||||
[node name="GatheringStateIndicator" type="Panel" parent="VBoxContainer/GridContainer/StatesContainer/GatheringStateContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(24, 12)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_type_variation = &"PanelIndicator"
|
||||
|
||||
[node name="GatheringStateLabel" type="Label" parent="VBoxContainer/GridContainer/StatesContainer/GatheringStateContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH2"
|
||||
text = "STATE"
|
||||
|
||||
[node name="SignalingStateContainer" type="HBoxContainer" parent="VBoxContainer/GridContainer/StatesContainer"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/GridContainer/StatesContainer/SignalingStateContainer"]
|
||||
custom_minimum_size = Vector2(114, 0)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Signaling"
|
||||
|
||||
[node name="SignalingStateIndicator" type="Panel" parent="VBoxContainer/GridContainer/StatesContainer/SignalingStateContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(24, 12)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_type_variation = &"PanelIndicator"
|
||||
|
||||
[node name="SignalingStateLabel" type="Label" parent="VBoxContainer/GridContainer/StatesContainer/SignalingStateContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH2"
|
||||
text = "STATE"
|
||||
|
||||
[node name="ChannelsContainer" type="HBoxContainer" parent="VBoxContainer/GridContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="ChannelsLabel" type="Label" parent="VBoxContainer/GridContainer/ChannelsContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 0
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Channels"
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer/GridContainer/ChannelsContainer"]
|
||||
custom_minimum_size = Vector2(0, 96)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
follow_focus = true
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="ChannelsContainer" type="VBoxContainer" parent="VBoxContainer/GridContainer/ChannelsContainer/ScrollContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="TimesContainer" type="VBoxContainer" parent="VBoxContainer/GridContainer"]
|
||||
custom_minimum_size = Vector2(250, 0)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ConnectingTimeContainer" type="HBoxContainer" parent="VBoxContainer/GridContainer/TimesContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/GridContainer/TimesContainer/ConnectingTimeContainer"]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Connecting time"
|
||||
|
||||
[node name="ConnectingTimeLabel" type="Label" parent="VBoxContainer/GridContainer/TimesContainer/ConnectingTimeContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH2"
|
||||
text = "00.000"
|
||||
|
||||
[node name="SecondLabel" type="Label" parent="VBoxContainer/GridContainer/TimesContainer/ConnectingTimeContainer"]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "s"
|
||||
|
||||
[node name="UpTimeContainer" type="HBoxContainer" parent="VBoxContainer/GridContainer/TimesContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/GridContainer/TimesContainer/UpTimeContainer"]
|
||||
custom_minimum_size = Vector2(146, 0)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Up time"
|
||||
|
||||
[node name="UpTimeLabel" type="Label" parent="VBoxContainer/GridContainer/TimesContainer/UpTimeContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH2"
|
||||
text = "0.0"
|
||||
|
||||
[node name="SecondLabel" type="Label" parent="VBoxContainer/GridContainer/TimesContainer/UpTimeContainer"]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "s"
|
||||
|
||||
[node name="LatencyContainer" type="HBoxContainer" parent="VBoxContainer/GridContainer/TimesContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/GridContainer/TimesContainer/LatencyContainer"]
|
||||
custom_minimum_size = Vector2(146, 0)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Latency"
|
||||
|
||||
[node name="LatencyLabel" type="Label" parent="VBoxContainer/GridContainer/TimesContainer/LatencyContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH2"
|
||||
text = "00000"
|
||||
|
||||
[node name="SecondLabel" type="Label" parent="VBoxContainer/GridContainer/TimesContainer/LatencyContainer"]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "ms"
|
||||
|
||||
[node name="UtilsContainer" type="VBoxContainer" parent="VBoxContainer/GridContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="FakeDisconnectionContainer" type="HBoxContainer" parent="VBoxContainer/GridContainer/UtilsContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="FakeDisconnectionTimer" type="Timer" parent="VBoxContainer/GridContainer/UtilsContainer/FakeDisconnectionContainer"]
|
||||
unique_name_in_owner = true
|
||||
one_shot = true
|
||||
|
||||
[node name="FakeDisconnectionButton" type="Button" parent="VBoxContainer/GridContainer/UtilsContainer/FakeDisconnectionContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(192, 0)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
tooltip_text = "Fake disconnectionon on code level, not on network level, for a few secondes.
|
||||
Peer will emit disconneted (unstabilized) signal to client. Will emit connected (stabilized) to client after few seconde."
|
||||
text = "FAKE DISCONNECTED"
|
||||
|
||||
[node name="FakeDisconnectionSpinBox" type="SpinBox" parent="VBoxContainer/GridContainer/UtilsContainer/FakeDisconnectionContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
max_value = 10.0
|
||||
step = 0.001
|
||||
value = 1.0
|
||||
allow_greater = true
|
||||
suffix = "s"
|
||||
|
||||
[connection signal="timeout" from="PingTimer" to="." method="_on_ping_timer_timeout"]
|
||||
[connection signal="pressed" from="VBoxContainer/GridContainer/UtilsContainer/FakeDisconnectionContainer/FakeDisconnectionButton" to="." method="_on_fake_disconnection_button_pressed"]
|
||||
390
addons/tube/inspector/peer_item_control.gd
Normal file
390
addons/tube/inspector/peer_item_control.gd
Normal file
@ -0,0 +1,390 @@
|
||||
class_name EditorTubePeerItemControl extends Control
|
||||
## @experimental: This class is used as part of the TubeClientDebugPanel scene and is part of a scene. Should not be used as itself.
|
||||
|
||||
signal pressed
|
||||
signal updated
|
||||
|
||||
const MESSAGE_ITEM_CONTROL_SCENE := preload("uid://cfsei3airwx4s")
|
||||
const CHANNEL_ITEM_CONTROL_SCENE := preload("uid://dc3ssinymllca")
|
||||
|
||||
const STATE_COLOR_DEFAULT := Color.WHITE
|
||||
const STATE_TEXT_DEFAULT := "Unknown"
|
||||
|
||||
const CONNECTION_STATE_COLOR := {
|
||||
WebRTCPeerConnection.STATE_NEW: Color.BEIGE,
|
||||
WebRTCPeerConnection.STATE_CONNECTING: Color.CYAN,
|
||||
WebRTCPeerConnection.STATE_CONNECTED: Color.PALE_GREEN,
|
||||
WebRTCPeerConnection.STATE_DISCONNECTED: Color.CYAN,
|
||||
WebRTCPeerConnection.STATE_FAILED: Color.GOLDENROD,
|
||||
WebRTCPeerConnection.STATE_CLOSED: Color.CRIMSON,
|
||||
}
|
||||
const CONNECTION_STATE_TEXT := {
|
||||
WebRTCPeerConnection.STATE_NEW: "New",
|
||||
WebRTCPeerConnection.STATE_CONNECTING: "Connecting",
|
||||
WebRTCPeerConnection.STATE_CONNECTED: "Connected",
|
||||
WebRTCPeerConnection.STATE_DISCONNECTED: 'Disconnected',
|
||||
WebRTCPeerConnection.STATE_FAILED: "Failed",
|
||||
WebRTCPeerConnection.STATE_CLOSED: "Closed",
|
||||
}
|
||||
|
||||
|
||||
const GATHERING_STATE_COLOR := {
|
||||
WebRTCPeerConnection.GATHERING_STATE_NEW: Color.BEIGE,
|
||||
WebRTCPeerConnection.GATHERING_STATE_GATHERING: Color.CYAN,
|
||||
WebRTCPeerConnection.GATHERING_STATE_COMPLETE: Color.PALE_GREEN,
|
||||
}
|
||||
const GATHERING_STATE_TEXT := {
|
||||
WebRTCPeerConnection.GATHERING_STATE_NEW: "New",
|
||||
WebRTCPeerConnection.GATHERING_STATE_GATHERING: "Gathering",
|
||||
WebRTCPeerConnection.GATHERING_STATE_COMPLETE: "Complete",
|
||||
}
|
||||
|
||||
|
||||
const SIGNALING_STATE_COLOR := {
|
||||
WebRTCPeerConnection.SIGNALING_STATE_STABLE: Color.PALE_GREEN,
|
||||
WebRTCPeerConnection.SIGNALING_STATE_HAVE_LOCAL_OFFER: Color.CYAN,
|
||||
WebRTCPeerConnection.SIGNALING_STATE_HAVE_REMOTE_OFFER: Color.CYAN,
|
||||
WebRTCPeerConnection.SIGNALING_STATE_HAVE_LOCAL_PRANSWER: Color.CYAN,
|
||||
WebRTCPeerConnection.SIGNALING_STATE_HAVE_REMOTE_PRANSWER: Color.CYAN,
|
||||
WebRTCPeerConnection.SIGNALING_STATE_CLOSED: Color.CRIMSON,
|
||||
}
|
||||
const SIGNALING_STATE_TEXT := {
|
||||
WebRTCPeerConnection.SIGNALING_STATE_STABLE: "Stable",
|
||||
WebRTCPeerConnection.SIGNALING_STATE_HAVE_LOCAL_OFFER: "Have local offer",
|
||||
WebRTCPeerConnection.SIGNALING_STATE_HAVE_REMOTE_OFFER: "Have remote offer",
|
||||
WebRTCPeerConnection.SIGNALING_STATE_HAVE_LOCAL_PRANSWER: "Have local answer",
|
||||
WebRTCPeerConnection.SIGNALING_STATE_HAVE_REMOTE_PRANSWER: "Have remote answer",
|
||||
WebRTCPeerConnection.SIGNALING_STATE_CLOSED: "Closed",
|
||||
}
|
||||
|
||||
|
||||
@export var peer_control: EditorTubePeerControl
|
||||
@export var channel_control: EditorTubeChannelControl
|
||||
|
||||
@export var client: TubeClient # to call kick
|
||||
@export var max_messages_amount: int = 100
|
||||
|
||||
var peer: TubePeer:
|
||||
set(x):
|
||||
|
||||
if null != peer:
|
||||
peer.warning_raised.disconnect(
|
||||
_on_peer_warning_raised
|
||||
)
|
||||
peer.failed.disconnect(
|
||||
_on_peer_failed
|
||||
)
|
||||
peer.connected.disconnect(
|
||||
_on_peer_connected
|
||||
)
|
||||
peer.disconnected.disconnect(
|
||||
_on_peer_disconnected
|
||||
)
|
||||
|
||||
peer.signaling_readied.disconnect(
|
||||
_on_peer_signaling_readied
|
||||
)
|
||||
peer.signaling_timeout.disconnect(
|
||||
_on_peer_signaling_timeout
|
||||
)
|
||||
|
||||
peer.connection_state_changed.disconnect(
|
||||
_on_peer_connection_state_changed
|
||||
)
|
||||
peer.port_mapped.disconnect(
|
||||
_on_peer_port_mapped
|
||||
)
|
||||
peer.channel_initiated.disconnect(
|
||||
_on_peer_channel_initiated
|
||||
)
|
||||
|
||||
peer.session_description_created.disconnect(
|
||||
_on_peer_session_description_created
|
||||
)
|
||||
peer.ice_candidate_created.disconnect(
|
||||
_on_peer_ice_candidate_created
|
||||
)
|
||||
peer.remote_description_setted.disconnect(
|
||||
_on_peer_remote_description_setted
|
||||
)
|
||||
peer.ice_candidate_added.disconnect(
|
||||
_on_peer_ice_candidate_added
|
||||
)
|
||||
|
||||
if null != x:
|
||||
x.warning_raised.connect(
|
||||
_on_peer_warning_raised
|
||||
)
|
||||
x.failed.connect(
|
||||
_on_peer_failed
|
||||
)
|
||||
x.connected.connect(
|
||||
_on_peer_connected
|
||||
)
|
||||
x.disconnected.connect(
|
||||
_on_peer_disconnected
|
||||
)
|
||||
|
||||
x.signaling_readied.connect(
|
||||
_on_peer_signaling_readied
|
||||
)
|
||||
x.signaling_timeout.connect(
|
||||
_on_peer_signaling_timeout
|
||||
)
|
||||
|
||||
x.connection_state_changed.connect(
|
||||
_on_peer_connection_state_changed
|
||||
)
|
||||
x.port_mapped.connect(
|
||||
_on_peer_port_mapped
|
||||
)
|
||||
x.channel_initiated.connect(
|
||||
_on_peer_channel_initiated
|
||||
)
|
||||
|
||||
x.session_description_created.connect(
|
||||
_on_peer_session_description_created
|
||||
)
|
||||
x.ice_candidate_created.connect(
|
||||
_on_peer_ice_candidate_created
|
||||
)
|
||||
x.remote_description_setted.connect(
|
||||
_on_peer_remote_description_setted
|
||||
)
|
||||
x.ice_candidate_added.connect(
|
||||
_on_peer_ice_candidate_added
|
||||
)
|
||||
|
||||
peer = x
|
||||
update()
|
||||
|
||||
|
||||
var message_item_controls: Array[EditorTubeMessagesItemControl] = []
|
||||
var message_item_button_group := ButtonGroup.new()
|
||||
|
||||
var channel_item_controls: Array[EditorTubeChannelItemControl] = []
|
||||
|
||||
@onready var name_label: Label = %NameLabel
|
||||
@onready var state_indicator: Control = %StateIndicator
|
||||
@onready var kick_button: Button = %KickButton
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
message_item_button_group.allow_unpress = true
|
||||
update()
|
||||
|
||||
|
||||
static var peers_color: Dictionary[int, Color] = {}
|
||||
static func get_peer_color(p_peer_id: int) -> Color:
|
||||
if 0 == p_peer_id:
|
||||
return Color.BLACK
|
||||
|
||||
if 1 == p_peer_id:
|
||||
return Color.WHITE
|
||||
|
||||
if peers_color.has(p_peer_id):
|
||||
return peers_color[p_peer_id]
|
||||
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = p_peer_id
|
||||
var color := Color.from_hsv(
|
||||
rng.randf_range(0.4, 0.9),
|
||||
rng.randf_range(0.4, 0.8),
|
||||
rng.randf_range(0.9, 1.0),
|
||||
1.0
|
||||
)
|
||||
|
||||
peers_color[p_peer_id] = color
|
||||
|
||||
return color
|
||||
|
||||
|
||||
static func get_peer_string(p_peer_id: int) -> String:
|
||||
if 0 == p_peer_id:
|
||||
return ""
|
||||
|
||||
if 1 == p_peer_id:
|
||||
return "1 (Server)"
|
||||
|
||||
return str(p_peer_id)
|
||||
|
||||
|
||||
func get_connection_state_color() -> Color:
|
||||
return STATE_COLOR_DEFAULT if not peer else CONNECTION_STATE_COLOR[peer.connection_state]
|
||||
|
||||
func get_connection_state_text() -> String:
|
||||
return STATE_TEXT_DEFAULT if not peer else CONNECTION_STATE_TEXT[peer.connection_state]
|
||||
|
||||
|
||||
func get_gathering_state_color() -> Color:
|
||||
return STATE_COLOR_DEFAULT if not peer else GATHERING_STATE_COLOR[peer.gathering_state]
|
||||
|
||||
func get_gathering_state_text() -> String:
|
||||
return STATE_TEXT_DEFAULT if not peer else GATHERING_STATE_TEXT[peer.gathering_state]
|
||||
|
||||
|
||||
func get_signaling_state_color() -> Color:
|
||||
return STATE_COLOR_DEFAULT if not peer else SIGNALING_STATE_COLOR[peer.signaling_state]
|
||||
|
||||
|
||||
func get_signaling_state_text() -> String:
|
||||
return STATE_TEXT_DEFAULT if not peer else SIGNALING_STATE_TEXT[peer.signaling_state]
|
||||
|
||||
|
||||
func _on_button_pressed() -> void:
|
||||
if is_instance_valid(peer_control):
|
||||
peer_control.peer_item = self
|
||||
|
||||
pressed.emit()
|
||||
|
||||
|
||||
func _on_kick_button_pressed() -> void:
|
||||
client.kick_peer(peer.id)
|
||||
|
||||
|
||||
func update():
|
||||
if null == peer:
|
||||
return
|
||||
|
||||
if is_instance_valid(name_label):
|
||||
name_label.text = get_peer_string(peer.id)
|
||||
name_label.modulate = get_peer_color(peer.id)
|
||||
|
||||
if is_instance_valid(state_indicator):
|
||||
state_indicator.modulate = get_connection_state_color()
|
||||
state_indicator.tooltip_text = get_connection_state_text()
|
||||
|
||||
if is_instance_valid(peer_control):
|
||||
if self == peer_control.peer_item:
|
||||
peer_control.update()
|
||||
|
||||
if is_instance_valid(kick_button):
|
||||
if client:
|
||||
kick_button.visible = client.is_server
|
||||
|
||||
updated.emit()
|
||||
|
||||
|
||||
func add_message_item_control(data) -> EditorTubeMessagesItemControl:
|
||||
if max_messages_amount <= message_item_controls.size():
|
||||
var item := message_item_controls.pop_front()
|
||||
item.queue_free()
|
||||
|
||||
var message_item_control := MESSAGE_ITEM_CONTROL_SCENE.instantiate()
|
||||
message_item_controls.append(message_item_control)
|
||||
message_item_control.data = data
|
||||
message_item_control.button_group = message_item_button_group
|
||||
return message_item_control
|
||||
|
||||
|
||||
func add_channel_item_control(channel: WebRTCDataChannel):
|
||||
var channel_item_control := CHANNEL_ITEM_CONTROL_SCENE.instantiate()
|
||||
channel_item_controls.append(channel_item_control)
|
||||
|
||||
channel_item_control.peer = peer
|
||||
channel_item_control.channel = channel
|
||||
channel_item_control.channel_control = channel_control
|
||||
|
||||
if is_instance_valid(peer_control):
|
||||
if peer_control.peer_item == self:
|
||||
peer_control.add_channel_item_control(
|
||||
channel_item_control
|
||||
)
|
||||
|
||||
|
||||
func _on_peer_warning_raised(message: String):
|
||||
add_message_item_control(message).warning()
|
||||
update()
|
||||
|
||||
|
||||
func _on_peer_connected():
|
||||
add_message_item_control("Connected").success()
|
||||
update()
|
||||
|
||||
|
||||
func _on_peer_failed():
|
||||
add_message_item_control("Connection failed: {error}".format({
|
||||
"error": peer.error_message
|
||||
})).error()
|
||||
update()
|
||||
|
||||
|
||||
func _on_peer_disconnected():
|
||||
add_message_item_control("Disconnected")
|
||||
update()
|
||||
|
||||
|
||||
func _on_peer_signaling_readied():
|
||||
add_message_item_control("Signaling ready")
|
||||
update()
|
||||
|
||||
|
||||
func _on_peer_signaling_timeout():
|
||||
add_message_item_control("Signaling timeout").warning()
|
||||
update()
|
||||
|
||||
|
||||
func _on_peer_connection_state_changed():
|
||||
add_message_item_control("State changed to {connection}/{gathering}/{signaling}".format({
|
||||
"connection": get_connection_state_text(),
|
||||
"gathering": get_gathering_state_text(),
|
||||
"signaling": get_signaling_state_text(),
|
||||
}))
|
||||
|
||||
update()
|
||||
|
||||
|
||||
func _on_peer_channel_initiated(p_channel: WebRTCDataChannel):
|
||||
add_channel_item_control(p_channel)
|
||||
add_message_item_control(
|
||||
"Channel {label} initiated".format({
|
||||
"label": p_channel.get_label(),
|
||||
})
|
||||
)
|
||||
update()
|
||||
|
||||
|
||||
func _on_peer_port_mapped(public_port: int, local_port: int):
|
||||
add_message_item_control(
|
||||
"Port {port} mapped to internal port {internal_port}".format({
|
||||
"port": public_port,
|
||||
"internal_port": local_port
|
||||
})
|
||||
)
|
||||
update()
|
||||
|
||||
|
||||
func _on_peer_session_description_created(): # local
|
||||
add_message_item_control(
|
||||
"Session description created: {description}".format({
|
||||
"description": peer.local_session_description
|
||||
})
|
||||
)
|
||||
update()
|
||||
|
||||
|
||||
func _on_peer_ice_candidate_created(): # local
|
||||
add_message_item_control(
|
||||
"Ice candidate created: {candidate}".format({
|
||||
"candidate": peer.ice_candidates[-1]
|
||||
})
|
||||
)
|
||||
update()
|
||||
|
||||
|
||||
func _on_peer_remote_description_setted():
|
||||
add_message_item_control(
|
||||
"Remote session description setted: {description}".format({
|
||||
"description": peer.remote_session_description
|
||||
})
|
||||
)
|
||||
update()
|
||||
|
||||
|
||||
func _on_peer_ice_candidate_added(ice_candidate: Dictionary): # remote
|
||||
add_message_item_control(
|
||||
"Ice candidate added: {candidate}".format({
|
||||
"candidate": ice_candidate
|
||||
})
|
||||
)
|
||||
update()
|
||||
1
addons/tube/inspector/peer_item_control.gd.uid
Normal file
1
addons/tube/inspector/peer_item_control.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://c2jyudi8823qj
|
||||
65
addons/tube/inspector/peer_item_control.tscn
Normal file
65
addons/tube/inspector/peer_item_control.tscn
Normal file
@ -0,0 +1,65 @@
|
||||
[gd_scene load_steps=6 format=3 uid="uid://dq2d125kftur6"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c2jyudi8823qj" path="res://addons/tube/inspector/peer_item_control.gd" id="1_2007k"]
|
||||
[ext_resource type="Theme" uid="uid://bcibt73qths3g" path="res://addons/tube/inspector/theme.tres" id="1_uctpu"]
|
||||
[ext_resource type="ButtonGroup" uid="uid://fko7ise7cj31" path="res://addons/tube/inspector/tracker_peer_item_button_group.tres" id="2_nna7f"]
|
||||
[ext_resource type="Texture2D" uid="uid://bblpobvrbvblp" path="res://addons/tube/inspector/icons/icons_sheet_white.png" id="3_hvdy4"]
|
||||
|
||||
[sub_resource type="AtlasTexture" id="AtlasTexture_uctpu"]
|
||||
atlas = ExtResource("3_hvdy4")
|
||||
region = Rect2(400, 1200, 100, 100)
|
||||
|
||||
[node name="PeerItemControl" type="MarginContainer"]
|
||||
offset_right = 202.0
|
||||
offset_bottom = 38.0
|
||||
theme = ExtResource("1_uctpu")
|
||||
script = ExtResource("1_2007k")
|
||||
|
||||
[node name="Button" type="Button" parent="."]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"ButtonFlat"
|
||||
toggle_mode = true
|
||||
button_group = ExtResource("2_nna7f")
|
||||
alignment = 0
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="."]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/margin_left = 4
|
||||
theme_override_constants/margin_top = 2
|
||||
theme_override_constants/margin_right = 4
|
||||
theme_override_constants/margin_bottom = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="StateIndicator" type="Panel" parent="MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(24, 12)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
theme_type_variation = &"PanelIndicator"
|
||||
|
||||
[node name="NameLabel" type="Label" parent="MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_type_variation = &"HeaderMedium"
|
||||
text = "PEER_ID"
|
||||
text_overrun_behavior = 1
|
||||
|
||||
[node name="KickButton" type="Button" parent="MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(34, 0)
|
||||
layout_mode = 2
|
||||
tooltip_text = "Kick"
|
||||
theme_type_variation = &"ButtonFlat"
|
||||
icon = SubResource("AtlasTexture_uctpu")
|
||||
icon_alignment = 1
|
||||
expand_icon = true
|
||||
|
||||
[connection signal="pressed" from="Button" to="." method="_on_button_pressed"]
|
||||
[connection signal="pressed" from="MarginContainer/HBoxContainer/KickButton" to="." method="_on_kick_button_pressed"]
|
||||
537
addons/tube/inspector/theme.tres
Normal file
537
addons/tube/inspector/theme.tres
Normal file
@ -0,0 +1,537 @@
|
||||
[gd_resource type="Theme" format=3 uid="uid://bcibt73qths3g"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8xdx6"]
|
||||
bg_color = Color(0, 0, 0, 0.3)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_qlom4"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_h3tqh"]
|
||||
content_margin_left = 2.0
|
||||
content_margin_top = 2.0
|
||||
content_margin_right = 2.0
|
||||
content_margin_bottom = 2.0
|
||||
bg_color = Color(0, 0, 0, 0.5)
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
border_color = Color(1, 1, 1, 0.2)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
shadow_color = Color(0, 0, 0, 0.005)
|
||||
shadow_size = 16
|
||||
shadow_offset = Vector2(0, 8)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rmtr8"]
|
||||
content_margin_left = 2.0
|
||||
content_margin_top = 2.0
|
||||
content_margin_right = 2.0
|
||||
content_margin_bottom = 2.0
|
||||
bg_color = Color(0, 0, 0, 0.5)
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
border_color = Color(1, 1, 1, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_l5cq1"]
|
||||
content_margin_left = 2.0
|
||||
content_margin_top = 2.0
|
||||
content_margin_right = 2.0
|
||||
content_margin_bottom = 2.0
|
||||
bg_color = Color(0.20906562, 0.20906562, 0.20906562, 0)
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
border_color = Color(1, 1, 1, 0.3)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_22oxg"]
|
||||
bg_color = Color(1, 1, 1, 0.1)
|
||||
draw_center = false
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lxenf"]
|
||||
bg_color = Color(1, 1, 1, 0.15)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5vlfp"]
|
||||
bg_color = Color(1, 1, 1, 0.1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_udha7"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_84y7l"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ob1qp"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color(0.145098, 0.145098, 0.145098, 1)
|
||||
draw_center = false
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hmtxw"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 8.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 8.0
|
||||
bg_color = Color(0.145098, 0.145098, 0.145098, 1)
|
||||
draw_center = false
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
border_color = Color(1, 1, 1, 0.3)
|
||||
corner_radius_top_left = 512
|
||||
corner_radius_top_right = 512
|
||||
corner_radius_bottom_right = 512
|
||||
corner_radius_bottom_left = 512
|
||||
corner_detail = 20
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_s60ke"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 8.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 8.0
|
||||
bg_color = Color(0.145098, 0.145098, 0.145098, 1)
|
||||
draw_center = false
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
border_color = Color(1, 1, 1, 0.3)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wopcr"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 8.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 8.0
|
||||
bg_color = Color(0, 0, 0, 0.5)
|
||||
border_width_left = 4
|
||||
border_width_top = 4
|
||||
border_width_right = 4
|
||||
border_width_bottom = 4
|
||||
border_color = Color(0.145098, 0.145098, 0.145098, 0)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qy4ym"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 8.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 8.0
|
||||
bg_color = Color(0, 0, 0, 0.3)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7lltk"]
|
||||
content_margin_left = 0.0
|
||||
content_margin_top = 12.0
|
||||
content_margin_right = 0.0
|
||||
content_margin_bottom = 12.0
|
||||
bg_color = Color(0.145098, 0.145098, 0.145098, 1)
|
||||
draw_center = false
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxLine" id="StyleBoxLine_urpwp"]
|
||||
color = Color(0.101569, 0.101569, 0.101569, 1)
|
||||
grow_begin = -8.0
|
||||
grow_end = -8.0
|
||||
thickness = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xw7tr"]
|
||||
content_margin_left = 4.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_right = 4.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color(1, 1, 1, 0.75)
|
||||
draw_center = false
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
corner_detail = 5
|
||||
expand_margin_left = 2.0
|
||||
expand_margin_top = 2.0
|
||||
expand_margin_right = 2.0
|
||||
expand_margin_bottom = 2.0
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hoenv"]
|
||||
bg_color = Color(0.145098, 0.145098, 0.145098, 1)
|
||||
draw_center = false
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jnksh"]
|
||||
content_margin_left = 16.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 16.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.09985284, 0.09985284, 0.09985284, 0.5)
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
border_color = Color(1, 1, 1, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e1cyt"]
|
||||
content_margin_left = 16.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 16.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.11207495, 0.11207495, 0.11207495, 0)
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
border_color = Color(1, 1, 1, 0.3)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7uqqi"]
|
||||
content_margin_left = 16.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 16.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0, 0, 0, 0.3)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_k2ivb"]
|
||||
content_margin_left = 2.0
|
||||
content_margin_top = 2.0
|
||||
content_margin_right = 2.0
|
||||
content_margin_bottom = 2.0
|
||||
bg_color = Color(0.187843, 0.187843, 0.187843, 1)
|
||||
border_color = Color(0.235931, 0.235931, 0.235931, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
shadow_color = Color(0, 0, 0, 0.005)
|
||||
shadow_size = 16
|
||||
shadow_offset = Vector2(0, 8)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0p70m"]
|
||||
content_margin_left = 0.0
|
||||
content_margin_top = 0.0
|
||||
content_margin_right = 0.0
|
||||
content_margin_bottom = 0.0
|
||||
bg_color = Color(0.145098, 0.145098, 0.145098, 1)
|
||||
draw_center = false
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_owuhx"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 8.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 8.0
|
||||
bg_color = Color(0.016954614, 0.052993968, 0.09495633, 0.9019608)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_urpwp"]
|
||||
content_margin_left = 16.0
|
||||
content_margin_top = 9.0
|
||||
content_margin_right = 16.0
|
||||
content_margin_bottom = 8.0
|
||||
bg_color = Color(1, 1, 1, 0.1)
|
||||
corner_radius_top_left = 4
|
||||
corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_n7dtj"]
|
||||
bg_color = Color(1, 1, 1, 0.85)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
shadow_color = Color(1, 1, 1, 0.2)
|
||||
shadow_size = 2
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_281ff"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 8.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 8.0
|
||||
bg_color = Color(0.0979412, 0.0979412, 0.0979412, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
expand_margin_top = 4.0
|
||||
expand_margin_bottom = 4.0
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lt73j"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 8.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 8.0
|
||||
bg_color = Color(0.230588, 0.230588, 0.230588, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
expand_margin_top = 4.0
|
||||
expand_margin_bottom = 4.0
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6eiqv"]
|
||||
content_margin_left = 16.0
|
||||
content_margin_top = 16.0
|
||||
content_margin_right = 16.0
|
||||
content_margin_bottom = 16.0
|
||||
bg_color = Color(0.126961, 0.126961, 0.126961, 1)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_8xdx6"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_g0kxi"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_84y7l"]
|
||||
content_margin_left = 16.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 16.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.11207495, 0.11207495, 0.11207495, 0)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_5arv8"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_im384"]
|
||||
content_margin_left = 0.0
|
||||
content_margin_top = 0.0
|
||||
content_margin_right = 0.0
|
||||
content_margin_bottom = 0.0
|
||||
bg_color = Color(0.116078, 0.116078, 0.116078, 1)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_doa6c"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 0.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 0.0
|
||||
bg_color = Color(0.145098, 0.145098, 0.145098, 1)
|
||||
draw_center = false
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxLine" id="StyleBoxLine_5clyl"]
|
||||
color = Color(0.101569, 0.101569, 0.101569, 1)
|
||||
grow_begin = -8.0
|
||||
grow_end = -8.0
|
||||
thickness = 4
|
||||
vertical = true
|
||||
|
||||
[resource]
|
||||
Button/colors/font_color = Color(1, 1, 1, 1)
|
||||
Button/colors/font_disabled_color = Color(1, 1, 1, 0.3)
|
||||
Button/colors/font_focus_color = Color(1, 1, 1, 1)
|
||||
Button/colors/font_hover_color = Color(1, 1, 1, 1)
|
||||
Button/colors/font_hover_pressed_color = Color(1, 1, 1, 1)
|
||||
Button/colors/font_pressed_color = Color(1, 1, 1, 1)
|
||||
Button/colors/icon_disabled_color = Color(1, 1, 1, 0.3)
|
||||
Button/colors/icon_normal_color = Color(1, 1, 1, 1)
|
||||
Button/colors/icon_pressed_color = Color(1, 1, 1, 1)
|
||||
Button/constants/outline_size = 0
|
||||
Button/font_sizes/font_size = 18
|
||||
Button/styles/disabled = SubResource("StyleBoxFlat_8xdx6")
|
||||
Button/styles/disabled_mirrored = null
|
||||
Button/styles/focus = SubResource("StyleBoxEmpty_qlom4")
|
||||
Button/styles/hover = SubResource("StyleBoxFlat_h3tqh")
|
||||
Button/styles/hover_pressed = SubResource("StyleBoxFlat_rmtr8")
|
||||
Button/styles/normal = SubResource("StyleBoxFlat_l5cq1")
|
||||
Button/styles/pressed = SubResource("StyleBoxFlat_rmtr8")
|
||||
ButtonFlat/base_type = &"Button"
|
||||
ButtonFlat/styles/disabled = SubResource("StyleBoxFlat_22oxg")
|
||||
ButtonFlat/styles/disabled_mirrored = SubResource("StyleBoxFlat_22oxg")
|
||||
ButtonFlat/styles/hover = SubResource("StyleBoxFlat_lxenf")
|
||||
ButtonFlat/styles/hover_mirrored = SubResource("StyleBoxFlat_lxenf")
|
||||
ButtonFlat/styles/hover_pressed = SubResource("StyleBoxFlat_5vlfp")
|
||||
ButtonFlat/styles/hover_pressed_mirrored = SubResource("StyleBoxFlat_5vlfp")
|
||||
ButtonFlat/styles/normal = SubResource("StyleBoxEmpty_udha7")
|
||||
ButtonFlat/styles/normal_mirrored = SubResource("StyleBoxEmpty_84y7l")
|
||||
ButtonFlat/styles/pressed = SubResource("StyleBoxFlat_5vlfp")
|
||||
ButtonFlat/styles/pressed_mirrored = SubResource("StyleBoxFlat_5vlfp")
|
||||
CheckBox/colors/font_hover_pressed_color = Color(1, 1, 1, 1)
|
||||
CheckBox/colors/font_pressed_color = Color(1, 1, 1, 0.7)
|
||||
CheckBox/styles/normal = SubResource("StyleBoxFlat_ob1qp")
|
||||
CheckBox/styles/normal_mirrored = SubResource("StyleBoxFlat_ob1qp")
|
||||
CheckButton/colors/font_focus_color = Color(1, 1, 1, 0.7)
|
||||
CheckButton/colors/font_hover_pressed_color = Color(1, 1, 1, 1)
|
||||
CheckButton/colors/font_pressed_color = Color(1, 1, 1, 0.7)
|
||||
CodeEdit/colors/line_number_color = Color(1, 1, 1, 0.6)
|
||||
ColorPicker/styles/picker_focus_circle = SubResource("StyleBoxFlat_hmtxw")
|
||||
ColorPicker/styles/picker_focus_rectangle = SubResource("StyleBoxFlat_s60ke")
|
||||
ColorPicker/styles/sample_focus = SubResource("StyleBoxFlat_s60ke")
|
||||
HBoxContainer/constants/separation = 4
|
||||
HScrollBar/styles/grabber = SubResource("StyleBoxFlat_wopcr")
|
||||
HScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_qy4ym")
|
||||
HScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_qy4ym")
|
||||
HScrollBar/styles/scroll = SubResource("StyleBoxFlat_7lltk")
|
||||
HScrollBar/styles/scroll_focus = SubResource("StyleBoxFlat_7lltk")
|
||||
HSeparator/constants/separation = 16
|
||||
HSeparator/styles/separator = SubResource("StyleBoxLine_urpwp")
|
||||
HSplitContainer/constants/autohide = 0
|
||||
HSplitContainer/constants/minimum_grab_thickness = 12
|
||||
HSplitContainer/constants/separation = 4
|
||||
Label/colors/font_color = Color(1, 1, 1, 1)
|
||||
Label/colors/font_outline_color = Color(0, 0, 0, 1)
|
||||
Label/colors/font_shadow_color = Color(0, 0, 0, 0)
|
||||
Label/constants/line_spacing = 3
|
||||
Label/constants/outline_size = 0
|
||||
Label/constants/paragraph_spacing = 0
|
||||
Label/constants/shadow_offset_x = 1
|
||||
Label/constants/shadow_offset_y = 1
|
||||
Label/constants/shadow_outline_size = 1
|
||||
Label/font_sizes/font_size = 16
|
||||
Label/styles/focus = SubResource("StyleBoxFlat_xw7tr")
|
||||
Label/styles/normal = SubResource("StyleBoxFlat_hoenv")
|
||||
LabelH1/base_type = &"Label"
|
||||
LabelH1/font_sizes/font_size = 28
|
||||
LabelH2/base_type = &"Label"
|
||||
LabelH2/font_sizes/font_size = 24
|
||||
LabelH3/base_type = &"Label"
|
||||
LabelH3/font_sizes/font_size = 20
|
||||
LineEdit/colors/font_placeholder_color = Color(1, 1, 1, 0.4)
|
||||
LineEdit/font_sizes/font_size = 18
|
||||
LineEdit/styles/focus = SubResource("StyleBoxFlat_jnksh")
|
||||
LineEdit/styles/normal = SubResource("StyleBoxFlat_e1cyt")
|
||||
LineEdit/styles/read_only = SubResource("StyleBoxFlat_7uqqi")
|
||||
MenuButton/colors/font_color = Color(1, 1, 1, 0.7)
|
||||
MenuButton/colors/font_disabled_color = Color(1, 1, 1, 0.3)
|
||||
MenuButton/colors/font_focus_color = Color(1, 1, 1, 1)
|
||||
MenuButton/colors/font_hover_color = Color(1, 1, 1, 1)
|
||||
MenuButton/colors/font_hover_pressed_color = Color(1, 1, 1, 1)
|
||||
MenuButton/colors/font_pressed_color = Color(1, 1, 1, 1)
|
||||
MenuButton/colors/icon_disabled_color = Color(1, 1, 1, 0.3)
|
||||
MenuButton/colors/icon_focus_color = Color(1, 1, 1, 1)
|
||||
MenuButton/colors/icon_hover_color = Color(1, 1, 1, 1)
|
||||
MenuButton/colors/icon_hover_pressed_color = Color(1, 1, 1, 1)
|
||||
MenuButton/colors/icon_normal_color = Color(1, 1, 1, 0.7)
|
||||
MenuButton/colors/icon_pressed_color = Color(1, 1, 1, 1)
|
||||
MenuButton/styles/disabled = SubResource("StyleBoxFlat_22oxg")
|
||||
MenuButton/styles/disabled_mirrored = SubResource("StyleBoxFlat_22oxg")
|
||||
MenuButton/styles/focus = SubResource("StyleBoxFlat_22oxg")
|
||||
MenuButton/styles/hover = SubResource("StyleBoxFlat_lxenf")
|
||||
MenuButton/styles/hover_mirrored = SubResource("StyleBoxFlat_lxenf")
|
||||
MenuButton/styles/hover_pressed = SubResource("StyleBoxFlat_lxenf")
|
||||
MenuButton/styles/hover_pressed_mirrored = SubResource("StyleBoxFlat_lxenf")
|
||||
MenuButton/styles/normal = SubResource("StyleBoxFlat_22oxg")
|
||||
MenuButton/styles/normal_mirrored = SubResource("StyleBoxFlat_22oxg")
|
||||
MenuButton/styles/pressed = SubResource("StyleBoxFlat_5vlfp")
|
||||
MenuButton/styles/pressed_mirrored = SubResource("StyleBoxFlat_5vlfp")
|
||||
OptionButton/colors/font_color = Color(1, 1, 1, 0.7)
|
||||
OptionButton/colors/font_disabled_color = Color(1, 1, 1, 0.3)
|
||||
OptionButton/colors/font_focus_color = Color(1, 1, 1, 1)
|
||||
OptionButton/colors/font_hover_color = Color(1, 1, 1, 1)
|
||||
OptionButton/colors/font_hover_pressed_color = Color(1, 1, 1, 1)
|
||||
OptionButton/colors/font_pressed_color = Color(1, 1, 1, 1)
|
||||
OptionButton/colors/icon_disabled_color = Color(1, 1, 1, 0.3)
|
||||
OptionButton/colors/icon_normal_color = Color(1, 1, 1, 0.7)
|
||||
OptionButton/constants/arrow_margin = 14
|
||||
OptionButton/styles/disabled = SubResource("StyleBoxFlat_k2ivb")
|
||||
OptionButton/styles/disabled_mirrored = SubResource("StyleBoxFlat_k2ivb")
|
||||
OptionButton/styles/focus = SubResource("StyleBoxFlat_0p70m")
|
||||
OptionButton/styles/hover = SubResource("StyleBoxFlat_h3tqh")
|
||||
OptionButton/styles/hover_mirrored = SubResource("StyleBoxFlat_h3tqh")
|
||||
OptionButton/styles/hover_pressed = SubResource("StyleBoxFlat_rmtr8")
|
||||
OptionButton/styles/hover_pressed_mirrored = SubResource("StyleBoxFlat_rmtr8")
|
||||
OptionButton/styles/normal = SubResource("StyleBoxFlat_l5cq1")
|
||||
OptionButton/styles/normal_mirrored = SubResource("StyleBoxFlat_l5cq1")
|
||||
OptionButton/styles/pressed = SubResource("StyleBoxFlat_rmtr8")
|
||||
OptionButton/styles/pressed_mirrored = SubResource("StyleBoxFlat_rmtr8")
|
||||
PanelContainer/styles/panel = SubResource("StyleBoxFlat_owuhx")
|
||||
PanelH1Container/base_type = &"PanelContainer"
|
||||
PanelH1Container/styles/panel = SubResource("StyleBoxFlat_urpwp")
|
||||
PanelIndicator/base_type = &"Panel"
|
||||
PanelIndicator/styles/panel = SubResource("StyleBoxFlat_n7dtj")
|
||||
ProgressBar/styles/background = SubResource("StyleBoxFlat_281ff")
|
||||
ProgressBar/styles/fill = SubResource("StyleBoxFlat_lt73j")
|
||||
RichTextLabel/styles/normal = SubResource("StyleBoxFlat_6eiqv")
|
||||
ScrollContainer/styles/focus = SubResource("StyleBoxFlat_0p70m")
|
||||
ScrollContainer/styles/panel = SubResource("StyleBoxFlat_0p70m")
|
||||
SplitContainer/constants/autohide = 0
|
||||
SplitContainer/constants/minimum_grab_thickness = 16
|
||||
SplitContainer/constants/separation = 6
|
||||
TabContainer/styles/panel = SubResource("StyleBoxEmpty_8xdx6")
|
||||
TextEdit/colors/font_color = Color(1, 1, 1, 1)
|
||||
TextEdit/colors/font_placeholder_color = Color(1, 1, 1, 1)
|
||||
TextEdit/colors/font_readonly_color = Color(1, 1, 1, 1)
|
||||
TextEdit/font_sizes/font_size = 20
|
||||
TextEdit/styles/focus = SubResource("StyleBoxEmpty_g0kxi")
|
||||
TextEdit/styles/normal = SubResource("StyleBoxFlat_84y7l")
|
||||
TextEdit/styles/read_only = SubResource("StyleBoxEmpty_5arv8")
|
||||
TooltipPanel/styles/panel = SubResource("StyleBoxFlat_im384")
|
||||
VBoxContainer/constants/separation = 4
|
||||
VScrollBar/styles/grabber = SubResource("StyleBoxFlat_wopcr")
|
||||
VScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_qy4ym")
|
||||
VScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_qy4ym")
|
||||
VScrollBar/styles/scroll = SubResource("StyleBoxFlat_doa6c")
|
||||
VScrollBar/styles/scroll_focus = SubResource("StyleBoxFlat_doa6c")
|
||||
VSeparator/constants/separation = 16
|
||||
VSeparator/styles/separator = SubResource("StyleBoxLine_5clyl")
|
||||
VSplitContainer/constants/autohide = 0
|
||||
VSplitContainer/constants/minimum_grab_thickness = 12
|
||||
VSplitContainer/constants/separation = 4
|
||||
64
addons/tube/inspector/tracker_control.gd
Normal file
64
addons/tube/inspector/tracker_control.gd
Normal file
@ -0,0 +1,64 @@
|
||||
class_name EditorTubeTrackerControl extends Control
|
||||
## @experimental: This class is used as part of the TubeClientDebugPanel scene and is part of a scene. Should not be used as itself.
|
||||
|
||||
@export var tracker_item: EditorTubeTrackerItemControl:
|
||||
set(x):
|
||||
show()
|
||||
|
||||
if is_instance_valid(messages_container):
|
||||
if tracker_item != x or not messages_container.is_displaying_from(self):
|
||||
messages_container.display_messages(
|
||||
x.message_item_controls,
|
||||
self
|
||||
)
|
||||
|
||||
if is_instance_valid(url_label):
|
||||
url_label.text = str(x.tracker)
|
||||
|
||||
tracker_item = x
|
||||
|
||||
|
||||
@export var messages_container: EditorTubeMessagesContainer
|
||||
|
||||
|
||||
@onready var url_label: Label = %UrlLabel
|
||||
@onready var connection_time_label: Label = %ConnectingTimeLabel
|
||||
@onready var up_time_label: Label = %UpTimeLabel
|
||||
@onready var interval_time_left_label: Label = %IntervalTimeLeftLabel
|
||||
@onready var interval_time_label: Label = %IntervalTimeLabel
|
||||
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
hide()
|
||||
|
||||
|
||||
func update_messages():
|
||||
if null == tracker_item:
|
||||
return
|
||||
|
||||
if is_instance_valid(messages_container):
|
||||
if messages_container.is_displaying_from(self):
|
||||
messages_container.display_messages(
|
||||
tracker_item.message_item_controls,
|
||||
self
|
||||
)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if null == tracker_item:
|
||||
return
|
||||
|
||||
connection_time_label.text = str(
|
||||
tracker_item.tracker.connecting_time
|
||||
).pad_decimals(3)
|
||||
up_time_label.text = str(
|
||||
tracker_item.tracker.up_time
|
||||
).pad_decimals(3)
|
||||
|
||||
interval_time_left_label.text = str(
|
||||
tracker_item.tracker.interval_time_left
|
||||
).pad_decimals(3)
|
||||
interval_time_label.text = str(
|
||||
tracker_item.tracker.interval_time
|
||||
).pad_decimals(3)
|
||||
1
addons/tube/inspector/tracker_control.gd.uid
Normal file
1
addons/tube/inspector/tracker_control.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cygcobx75tkey
|
||||
97
addons/tube/inspector/tracker_control.tscn
Normal file
97
addons/tube/inspector/tracker_control.tscn
Normal file
@ -0,0 +1,97 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://ja0u2vuivo8b"]
|
||||
|
||||
[ext_resource type="Theme" uid="uid://bcibt73qths3g" path="res://addons/tube/inspector/theme.tres" id="1_noh0l"]
|
||||
[ext_resource type="Script" uid="uid://cygcobx75tkey" path="res://addons/tube/inspector/tracker_control.gd" id="1_pu2xr"]
|
||||
|
||||
[node name="TrackerControl" type="MarginContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme = ExtResource("1_noh0l")
|
||||
script = ExtResource("1_pu2xr")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HeaderContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="UrlLabel" type="Label" parent="VBoxContainer/HeaderContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "00000000000000000000"
|
||||
text_overrun_behavior = 1
|
||||
|
||||
[node name="ConnectingTimeContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/ConnectingTimeContainer"]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Connecting time"
|
||||
|
||||
[node name="ConnectingTimeLabel" type="Label" parent="VBoxContainer/ConnectingTimeContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH2"
|
||||
text = "0.0"
|
||||
|
||||
[node name="SecondLabel" type="Label" parent="VBoxContainer/ConnectingTimeContainer"]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "s"
|
||||
|
||||
[node name="UpTimeContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/UpTimeContainer"]
|
||||
custom_minimum_size = Vector2(146, 0)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Up time"
|
||||
|
||||
[node name="UpTimeLabel" type="Label" parent="VBoxContainer/UpTimeContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH2"
|
||||
text = "0.0"
|
||||
|
||||
[node name="SecondLabel" type="Label" parent="VBoxContainer/UpTimeContainer"]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "s"
|
||||
|
||||
[node name="IntervalTimeContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/IntervalTimeContainer"]
|
||||
custom_minimum_size = Vector2(146, 0)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Interval time"
|
||||
|
||||
[node name="IntervalTimeLeftLabel" type="Label" parent="VBoxContainer/IntervalTimeContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH2"
|
||||
text = "0.0"
|
||||
|
||||
[node name="SlashLabel" type="Label" parent="VBoxContainer/IntervalTimeContainer"]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "/"
|
||||
|
||||
[node name="IntervalTimeLabel" type="Label" parent="VBoxContainer/IntervalTimeContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH2"
|
||||
text = "0.0"
|
||||
|
||||
[node name="SecondLabel" type="Label" parent="VBoxContainer/IntervalTimeContainer"]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "s"
|
||||
186
addons/tube/inspector/tracker_item_control.gd
Normal file
186
addons/tube/inspector/tracker_item_control.gd
Normal file
@ -0,0 +1,186 @@
|
||||
class_name EditorTubeTrackerItemControl extends Control
|
||||
## @experimental: This class is used as part of the TubeClientDebugPanel scene and is part of a scene. Should not be used as itself.
|
||||
|
||||
signal pressed
|
||||
|
||||
|
||||
const MESSAGE_ITEM_CONTROL_SCENE := preload("uid://cfsei3airwx4s")
|
||||
|
||||
|
||||
const STATE_COLOR_DEFAULT := Color.WHITE
|
||||
const STATE_COLOR := {
|
||||
WebSocketPeer.STATE_CONNECTING: Color.CYAN,
|
||||
WebSocketPeer.STATE_OPEN: Color.PALE_GREEN,
|
||||
WebSocketPeer.STATE_CLOSING: Color.GOLDENROD,
|
||||
WebSocketPeer.STATE_CLOSED: Color.CRIMSON,
|
||||
}
|
||||
const STATE_TEXT_DEFAULT := "Unknown"
|
||||
const STATE_TEXT := {
|
||||
WebSocketPeer.STATE_CONNECTING: "Connecting",
|
||||
WebSocketPeer.STATE_OPEN: "Open",
|
||||
WebSocketPeer.STATE_CLOSING: "Closing",
|
||||
WebSocketPeer.STATE_CLOSED: "Closed",
|
||||
}
|
||||
|
||||
|
||||
@export var tracker_control: EditorTubeTrackerControl
|
||||
@export var max_messages_amount: int = 100
|
||||
|
||||
|
||||
var tracker: TubeTracker:
|
||||
set(x):
|
||||
|
||||
if null != tracker:
|
||||
tracker.warning_raised.disconnect(
|
||||
_on_tracker_warning_raised
|
||||
)
|
||||
tracker.connected.disconnect(
|
||||
_on_tracker_connected
|
||||
)
|
||||
tracker.failed.disconnect(
|
||||
_on_tracker_failed
|
||||
)
|
||||
tracker.disconnected.disconnect(
|
||||
_on_tracker_disconnected
|
||||
)
|
||||
|
||||
tracker.state_changed.disconnect(
|
||||
_on_tracker_state_changed
|
||||
)
|
||||
tracker.data_sent.disconnect(
|
||||
_on_tracker_data_sent
|
||||
)
|
||||
tracker.received_data.disconnect(
|
||||
_on_tracker_data_received
|
||||
)
|
||||
|
||||
if null != x:
|
||||
x.warning_raised.connect(
|
||||
_on_tracker_warning_raised
|
||||
)
|
||||
x.connected.connect(
|
||||
_on_tracker_connected
|
||||
)
|
||||
x.failed.connect(
|
||||
_on_tracker_failed
|
||||
)
|
||||
x.disconnected.connect(
|
||||
_on_tracker_disconnected
|
||||
)
|
||||
|
||||
|
||||
x.state_changed.connect(
|
||||
_on_tracker_state_changed
|
||||
)
|
||||
x.data_sent.connect(
|
||||
_on_tracker_data_sent
|
||||
)
|
||||
x.received_data.connect(
|
||||
_on_tracker_data_received
|
||||
)
|
||||
|
||||
tracker = x
|
||||
update()
|
||||
|
||||
|
||||
var message_item_controls: Array[EditorTubeMessagesItemControl] = []
|
||||
var message_item_button_group := ButtonGroup.new()
|
||||
|
||||
@onready var name_label: Label = %NameLabel
|
||||
@onready var state_indicator: Control = %StateIndicator
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
message_item_button_group.allow_unpress = true
|
||||
update()
|
||||
|
||||
|
||||
func _on_button_pressed() -> void:
|
||||
if is_instance_valid(tracker_control):
|
||||
tracker_control.tracker_item = self
|
||||
|
||||
pressed.emit()
|
||||
|
||||
|
||||
func update():
|
||||
if is_instance_valid(tracker):
|
||||
|
||||
if is_instance_valid(name_label):
|
||||
name_label.text = tracker.socket.get_requested_url()
|
||||
|
||||
if is_instance_valid(state_indicator):
|
||||
state_indicator.modulate = STATE_COLOR[tracker.state]
|
||||
state_indicator.tooltip_text = STATE_TEXT[tracker.state]
|
||||
|
||||
if is_instance_valid(tracker_control):
|
||||
if self == tracker_control.tracker_item:
|
||||
tracker_control.update_messages()
|
||||
|
||||
else:
|
||||
if is_instance_valid(name_label):
|
||||
name_label.text = "Unset"
|
||||
|
||||
if is_instance_valid(state_indicator):
|
||||
state_indicator.modulate = STATE_COLOR_DEFAULT
|
||||
state_indicator.tooltip_text = STATE_TEXT_DEFAULT
|
||||
|
||||
|
||||
|
||||
|
||||
func add_message_item_control(data) -> EditorTubeMessagesItemControl:
|
||||
if max_messages_amount <= message_item_controls.size():
|
||||
var item := message_item_controls.pop_front()
|
||||
item.queue_free()
|
||||
|
||||
var message_item_control := MESSAGE_ITEM_CONTROL_SCENE.instantiate()
|
||||
message_item_controls.append(message_item_control)
|
||||
message_item_control.data = data
|
||||
message_item_control.button_group = message_item_button_group
|
||||
return message_item_control
|
||||
|
||||
|
||||
func _on_tracker_warning_raised(message: String):
|
||||
add_message_item_control(message).warning()
|
||||
update()
|
||||
|
||||
|
||||
func _on_tracker_connected():
|
||||
add_message_item_control("Connected").success()
|
||||
update()
|
||||
|
||||
|
||||
func _on_tracker_failed():
|
||||
add_message_item_control(
|
||||
"Connection failed: {error}".format({
|
||||
"error": tracker.error_message
|
||||
})
|
||||
).error()
|
||||
update()
|
||||
|
||||
|
||||
func _on_tracker_disconnected():
|
||||
add_message_item_control("Disconneted")
|
||||
update()
|
||||
|
||||
|
||||
func _on_tracker_state_changed():
|
||||
#if WebSocketPeer.STATE_OPEN == tracker.state:
|
||||
#add_message_item_control("Connection open")
|
||||
|
||||
if WebSocketPeer.STATE_CLOSING == tracker.state:
|
||||
add_message_item_control("Connection closing")
|
||||
|
||||
#elif WebSocketPeer.STATE_CLOSED == tracker.state:
|
||||
#add_message_item_control("Connection closed")
|
||||
|
||||
update()
|
||||
|
||||
|
||||
func _on_tracker_data_received(data: Dictionary):
|
||||
add_message_item_control(data).received()
|
||||
update()
|
||||
|
||||
|
||||
func _on_tracker_data_sent(data: Dictionary):
|
||||
add_message_item_control(data).sent()
|
||||
update()
|
||||
1
addons/tube/inspector/tracker_item_control.gd.uid
Normal file
1
addons/tube/inspector/tracker_item_control.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://c8vrv4ynnxskv
|
||||
49
addons/tube/inspector/tracker_item_control.tscn
Normal file
49
addons/tube/inspector/tracker_item_control.tscn
Normal file
@ -0,0 +1,49 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://bc0iqgoaed12"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c8vrv4ynnxskv" path="res://addons/tube/inspector/tracker_item_control.gd" id="1_2wqop"]
|
||||
[ext_resource type="Theme" uid="uid://bcibt73qths3g" path="res://addons/tube/inspector/theme.tres" id="1_x2s1b"]
|
||||
[ext_resource type="ButtonGroup" uid="uid://fko7ise7cj31" path="res://addons/tube/inspector/tracker_peer_item_button_group.tres" id="2_daba7"]
|
||||
|
||||
[node name="TrackerControl" type="MarginContainer"]
|
||||
custom_minimum_size = Vector2(196, 32)
|
||||
offset_right = 196.0
|
||||
offset_bottom = 38.0
|
||||
size_flags_horizontal = 3
|
||||
theme = ExtResource("1_x2s1b")
|
||||
script = ExtResource("1_2wqop")
|
||||
|
||||
[node name="Button" type="Button" parent="."]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"ButtonFlat"
|
||||
toggle_mode = true
|
||||
button_group = ExtResource("2_daba7")
|
||||
alignment = 0
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="."]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/margin_left = 4
|
||||
theme_override_constants/margin_top = 2
|
||||
theme_override_constants/margin_right = 4
|
||||
theme_override_constants/margin_bottom = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="StateIndicator" type="Panel" parent="MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(24, 12)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_type_variation = &"PanelIndicator"
|
||||
|
||||
[node name="NameLabel" type="Label" parent="MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_type_variation = &"HeaderMedium"
|
||||
text = "wss://TRACKER_URL"
|
||||
text_overrun_behavior = 1
|
||||
|
||||
[connection signal="pressed" from="Button" to="." method="_on_button_pressed"]
|
||||
@ -0,0 +1,4 @@
|
||||
[gd_resource type="ButtonGroup" format=3 uid="uid://fko7ise7cj31"]
|
||||
|
||||
[resource]
|
||||
resource_local_to_scene = false
|
||||
462
addons/tube/inspector/tube_inspector.gd
Normal file
462
addons/tube/inspector/tube_inspector.gd
Normal file
@ -0,0 +1,462 @@
|
||||
@icon("../icons/tube_inspector.svg")
|
||||
class_name EditorTubeClientPanel extends Control
|
||||
## @experimental: This class is used as part of the TubeClientDebugPanel scene, and is part of a scene. Should not be used as itself
|
||||
|
||||
|
||||
const TRACKER_ITEM_CONTROL_SCENE := preload("uid://bc0iqgoaed12")
|
||||
const PEER_ITEM_CONTROL_SCENE := preload("uid://dq2d125kftur6")
|
||||
|
||||
const MESSAGE_ITEM_CONTROL_SCENE := preload("uid://cfsei3airwx4s")
|
||||
|
||||
|
||||
const STATE_COLORS := {
|
||||
TubeClient.State.IDLE: Color.BEIGE,
|
||||
TubeClient.State.CREATING_SESSION: Color.CYAN,
|
||||
TubeClient.State.JOINING_SESSION: Color.CYAN,
|
||||
TubeClient.State.SESSION_CREATED: Color.PALE_GREEN,
|
||||
TubeClient.State.SESSION_JOINED: Color.PALE_GREEN,
|
||||
}
|
||||
|
||||
|
||||
const SIGNALING_COLORS := {
|
||||
false: Color.CRIMSON,
|
||||
true: Color.PALE_GREEN,
|
||||
}
|
||||
|
||||
|
||||
@export var client: TubeClient:
|
||||
set(x):
|
||||
if client != x:
|
||||
if null != client:
|
||||
|
||||
client.error_raised.disconnect(
|
||||
_on_client_error_raised
|
||||
)
|
||||
client.session_created.disconnect(
|
||||
_on_client_session_created
|
||||
)
|
||||
client.session_joined.disconnect(
|
||||
_on_client_session_joined
|
||||
)
|
||||
client.session_left.disconnect(
|
||||
_on_client_session_left
|
||||
)
|
||||
client.peer_refused.disconnect(
|
||||
_on_client_peer_refused
|
||||
)
|
||||
client.peer_connected.disconnect(
|
||||
_on_client_peer_connected
|
||||
)
|
||||
client.peer_disconnected.disconnect(
|
||||
_on_client_peer_disconnected
|
||||
)
|
||||
client.peer_unstabilized.disconnect(
|
||||
_on_client_peer_unstabilized
|
||||
)
|
||||
client.peer_stabilized.disconnect(
|
||||
_on_client_peer_stabilized
|
||||
)
|
||||
|
||||
client._session_initiated.disconnect(
|
||||
_on_client_session_initiated
|
||||
)
|
||||
client._local_signaling_peer_initiated.disconnect(
|
||||
_on_client_local_signaling_initiated
|
||||
)
|
||||
client._tracker_initiated.disconnect(
|
||||
_on_client_tracker_initiated
|
||||
)
|
||||
client._peer_initiated.disconnect(
|
||||
_on_client_peer_initiated
|
||||
)
|
||||
client._upnp.port_mapped.disconnect(
|
||||
_on_client_port_mapped
|
||||
)
|
||||
client._upnp.warning_raised.disconnect(
|
||||
_on_client_upnp_warning_raised
|
||||
)
|
||||
|
||||
|
||||
if null != x:
|
||||
x.error_raised.connect(
|
||||
_on_client_error_raised
|
||||
)
|
||||
x.session_created.connect(
|
||||
_on_client_session_created
|
||||
)
|
||||
x.session_joined.connect(
|
||||
_on_client_session_joined
|
||||
)
|
||||
x.session_left.connect(
|
||||
_on_client_session_left
|
||||
)
|
||||
x.peer_refused.connect(
|
||||
_on_client_peer_refused
|
||||
)
|
||||
x.peer_connected.connect(
|
||||
_on_client_peer_connected
|
||||
)
|
||||
x.peer_disconnected.connect(
|
||||
_on_client_peer_disconnected
|
||||
)
|
||||
x.peer_unstabilized.connect(
|
||||
_on_client_peer_unstabilized
|
||||
)
|
||||
x.peer_stabilized.connect(
|
||||
_on_client_peer_stabilized
|
||||
)
|
||||
|
||||
x._session_initiated.connect(
|
||||
_on_client_session_initiated
|
||||
)
|
||||
x._local_signaling_peer_initiated.connect(
|
||||
_on_client_local_signaling_initiated
|
||||
)
|
||||
x._tracker_initiated.connect(
|
||||
_on_client_tracker_initiated
|
||||
)
|
||||
x._peer_initiated.connect(
|
||||
_on_client_peer_initiated
|
||||
)
|
||||
x._upnp.port_mapped.connect(
|
||||
_on_client_port_mapped
|
||||
)
|
||||
x._upnp.warning_raised.connect(
|
||||
_on_client_upnp_warning_raised
|
||||
)
|
||||
|
||||
|
||||
|
||||
client = x
|
||||
if is_instance_valid(client_control):
|
||||
client_control.client = client
|
||||
|
||||
if is_instance_valid(peer_control):
|
||||
peer_control.client = client
|
||||
|
||||
update()
|
||||
|
||||
## Maximum of messages available, the oldest message will be removed when a new message is pushed. It is to prevent memory leak.
|
||||
## [br][br]
|
||||
## The amount is by item, meaning is max_messages_amount is set to 100. Client can store 100 messages, each trackers 100 messages, each peers 100 messages...
|
||||
@export var max_messages_amount: int = 100
|
||||
|
||||
var message_item_controls: Array[EditorTubeMessagesItemControl] = []
|
||||
var message_item_button_group := ButtonGroup.new()
|
||||
|
||||
@onready var peer_label: Label = %PeerLabel
|
||||
@onready var session_line_edit: LineEdit = %SessionLineEdit
|
||||
@onready var session_state_indicator: Control = %SessionIndicator
|
||||
|
||||
@onready var join_button: Button = %JoinButton
|
||||
@onready var create_button: Button = %CreateButton
|
||||
@onready var refuse_new_button: Button = %RefuseNewButton
|
||||
@onready var close_button: Button = %CloseButton
|
||||
|
||||
@onready var local_signaling_indicator: Control = %LocalSignalingIndicator
|
||||
@onready var trackers_indicator: Control = %TrackersIndicator
|
||||
@onready var trackers_container: Container = %TrackersContainer
|
||||
@onready var peers_container: Container = %PeersContainer
|
||||
|
||||
@onready var client_control: Control = %ClientControl
|
||||
@onready var local_signaling_control: EditorTubeLocalSignalingControl = %LocalSignalingControl
|
||||
@onready var tracker_control: EditorTubeTrackerControl = %TrackerControl
|
||||
@onready var peer_control: EditorTubePeerControl = %PeerControl
|
||||
@onready var chat_control: Control = %ChatControl
|
||||
@onready var messages_container: EditorTubeMessagesContainer = %MessagesContainer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
messages_container.max_messages_amount = max_messages_amount
|
||||
chat_control.max_messages_amount = max_messages_amount
|
||||
local_signaling_control.max_messages_amount = max_messages_amount
|
||||
client_control.show()
|
||||
|
||||
message_item_button_group.allow_unpress = true
|
||||
switch_to_idle_config()
|
||||
messages_container.display_messages([], self)
|
||||
|
||||
client = client
|
||||
update()
|
||||
|
||||
|
||||
func clear():
|
||||
for child in trackers_container.get_children():
|
||||
trackers_container.remove_child(child)
|
||||
child.queue_free()
|
||||
|
||||
for child in peers_container.get_children():
|
||||
peers_container.remove_child(child)
|
||||
child.queue_free()
|
||||
|
||||
message_item_controls.clear()
|
||||
messages_container.display_messages(
|
||||
message_item_controls,
|
||||
self
|
||||
)
|
||||
update()
|
||||
|
||||
|
||||
func switch_to_idle_config():
|
||||
session_state_indicator.modulate = STATE_COLORS[TubeClient.State.IDLE]
|
||||
local_signaling_indicator.modulate = session_state_indicator.modulate
|
||||
trackers_indicator.modulate = session_state_indicator.modulate
|
||||
session_line_edit.editable = true
|
||||
#session_line_edit.clear()
|
||||
|
||||
join_button.visible = true
|
||||
create_button.visible = true
|
||||
refuse_new_button.visible = false
|
||||
close_button.visible = false
|
||||
|
||||
|
||||
func switch_to_joined_config():
|
||||
session_line_edit.editable = false
|
||||
join_button.visible = false
|
||||
create_button.visible = false
|
||||
close_button.visible = true
|
||||
|
||||
|
||||
func switch_to_created_config():
|
||||
session_line_edit.editable = false
|
||||
join_button.visible = false
|
||||
create_button.visible = false
|
||||
refuse_new_button.visible = true
|
||||
close_button.visible = true
|
||||
|
||||
|
||||
func update():
|
||||
if not is_instance_valid(client):
|
||||
return
|
||||
|
||||
if is_instance_valid(session_state_indicator):
|
||||
session_state_indicator.modulate = STATE_COLORS[client.state]
|
||||
|
||||
if is_instance_valid(local_signaling_indicator):
|
||||
local_signaling_indicator.modulate = SIGNALING_COLORS[client._is_local_signaling()]
|
||||
if TubeClient.State.IDLE == client.state:
|
||||
local_signaling_indicator.modulate = STATE_COLORS[client.state]
|
||||
|
||||
if is_instance_valid(trackers_indicator):
|
||||
trackers_indicator.modulate = SIGNALING_COLORS[client._is_online_signaling()]
|
||||
if TubeClient.State.IDLE == client.state:
|
||||
trackers_indicator.modulate = STATE_COLORS[client.state]
|
||||
|
||||
if is_instance_valid(peer_label):
|
||||
peer_label.text = EditorTubePeerItemControl.get_peer_string(client.peer_id)
|
||||
peer_label.modulate = EditorTubePeerItemControl.get_peer_color(client.peer_id)
|
||||
|
||||
if is_instance_valid(session_line_edit):
|
||||
session_line_edit.text = client.session_id
|
||||
|
||||
if is_instance_valid(messages_container):
|
||||
if messages_container.is_displaying_from(self):
|
||||
messages_container.display_messages(
|
||||
message_item_controls,
|
||||
self
|
||||
)
|
||||
|
||||
|
||||
func add_message_item_control(data) -> EditorTubeMessagesItemControl:
|
||||
if max_messages_amount <= message_item_controls.size():
|
||||
var item := message_item_controls.pop_front()
|
||||
item.queue_free()
|
||||
|
||||
var message_item_control := MESSAGE_ITEM_CONTROL_SCENE.instantiate()
|
||||
message_item_controls.append(message_item_control)
|
||||
message_item_control.data = data
|
||||
message_item_control.button_group = message_item_button_group
|
||||
update()
|
||||
return message_item_control
|
||||
|
||||
|
||||
func add_tracker(p_tracker: TubeTracker):
|
||||
var item_control := TRACKER_ITEM_CONTROL_SCENE.instantiate()
|
||||
trackers_container.add_child(item_control)
|
||||
|
||||
#item_control.client = client
|
||||
item_control.tracker = p_tracker
|
||||
item_control.tracker_control = tracker_control
|
||||
item_control.max_messages_amount = max_messages_amount
|
||||
|
||||
|
||||
func add_peer(peer: TubePeer):
|
||||
for i_control in peers_container.get_children():
|
||||
if i_control.peer.id == peer.id:
|
||||
i_control.peer = peer
|
||||
return
|
||||
|
||||
var item_control := PEER_ITEM_CONTROL_SCENE.instantiate()
|
||||
peers_container.add_child(item_control)
|
||||
|
||||
item_control.client = client
|
||||
item_control.peer = peer
|
||||
item_control.peer_control = peer_control
|
||||
item_control.max_messages_amount = max_messages_amount
|
||||
|
||||
|
||||
func _on_header_button_pressed() -> void:
|
||||
client_control.show()
|
||||
if not messages_container.is_displaying_from(self):
|
||||
messages_container.display_messages(
|
||||
message_item_controls,
|
||||
self
|
||||
)
|
||||
|
||||
|
||||
func _on_join_button_pressed() -> void:
|
||||
if not is_instance_valid(client):
|
||||
return
|
||||
|
||||
var session_id := session_line_edit.text
|
||||
client.join_session(session_id)
|
||||
|
||||
|
||||
func _on_create_button_pressed() -> void:
|
||||
if not is_instance_valid(client):
|
||||
return
|
||||
|
||||
client.create_session()
|
||||
|
||||
|
||||
func _on_refuse_new_button_toggled(toggled_on: bool) -> void:
|
||||
client.refuse_new_connections = toggled_on
|
||||
|
||||
|
||||
func _on_close_button_pressed() -> void:
|
||||
if not is_instance_valid(client):
|
||||
return
|
||||
|
||||
client.leave_session()
|
||||
|
||||
|
||||
#
|
||||
|
||||
|
||||
func _on_client_error_raised(code: int, message: String):
|
||||
var item := add_message_item_control(message)
|
||||
item.error()
|
||||
|
||||
match code:
|
||||
TubeClient.SessionError.CREATE_SESSION_FAILED, TubeClient.SessionError.JOIN_SESSION_FAILED:
|
||||
switch_to_idle_config()
|
||||
|
||||
update()
|
||||
|
||||
|
||||
func _on_client_session_initiated():
|
||||
clear()
|
||||
match client.state:
|
||||
TubeClient.State.JOINING_SESSION:
|
||||
switch_to_joined_config()
|
||||
|
||||
TubeClient.State.CREATING_SESSION:
|
||||
switch_to_created_config()
|
||||
|
||||
TubeClient.State.IDLE:
|
||||
switch_to_idle_config()
|
||||
|
||||
update()
|
||||
|
||||
|
||||
func _on_client_session_created():
|
||||
switch_to_created_config()
|
||||
local_signaling_indicator.modulate = SIGNALING_COLORS[true]
|
||||
trackers_indicator.modulate = SIGNALING_COLORS[true]
|
||||
add_message_item_control("Create session {id}".format({
|
||||
"id": client.session_id
|
||||
})).success()
|
||||
|
||||
|
||||
func _on_client_session_joined():
|
||||
switch_to_joined_config()
|
||||
local_signaling_indicator.modulate = SIGNALING_COLORS[true]
|
||||
trackers_indicator.modulate = SIGNALING_COLORS[true]
|
||||
add_message_item_control("Join session {id}".format({
|
||||
"id": client.session_id
|
||||
})).success()
|
||||
|
||||
|
||||
func _on_client_session_left():
|
||||
switch_to_idle_config()
|
||||
add_message_item_control("Leave session {id}".format({
|
||||
"id": client.session_id
|
||||
}))
|
||||
|
||||
|
||||
func _on_client_peer_refused(peer_id: int):
|
||||
add_message_item_control("Peer {peer_id} connection refused".format({
|
||||
"peer_id": peer_id,
|
||||
})).warning()
|
||||
|
||||
|
||||
func _on_client_peer_connected(peer_id: int):
|
||||
var will_add_peer := true
|
||||
for i_control in peers_container.get_children():
|
||||
if i_control.peer.id == peer_id:
|
||||
will_add_peer = false
|
||||
break
|
||||
|
||||
if will_add_peer:
|
||||
var peer := TubePeer.new(peer_id)
|
||||
peer.connection_state = WebRTCPeerConnection.STATE_CONNECTED
|
||||
add_peer(peer)
|
||||
|
||||
add_message_item_control("Peer {peer_id} connected".format({
|
||||
"peer_id": peer_id,
|
||||
})).success()
|
||||
|
||||
|
||||
func _on_client_peer_disconnected(peer_id: int):
|
||||
add_message_item_control("Peer {peer_id} disconnected".format({
|
||||
"peer_id": peer_id,
|
||||
}))
|
||||
|
||||
|
||||
func _on_client_peer_unstabilized(peer_id: int):
|
||||
add_message_item_control("Peer {peer_id} unstabilized".format({
|
||||
"peer_id": peer_id,
|
||||
})).warning()
|
||||
|
||||
|
||||
func _on_client_peer_stabilized(peer_id: int):
|
||||
add_message_item_control("Peer {peer_id} stabilized".format({
|
||||
"peer_id": peer_id,
|
||||
}))
|
||||
|
||||
|
||||
func _on_client_local_signaling_initiated(local_signaling_peer: TubeLocalSignalingPeer):
|
||||
local_signaling_control.local_signaling_peer = local_signaling_peer
|
||||
add_message_item_control("Local signaling on {port} initiated".format({
|
||||
"port": local_signaling_peer.udp_peer.get_local_port()
|
||||
}))
|
||||
update.call_deferred()
|
||||
|
||||
|
||||
func _on_client_tracker_initiated(tracker: TubeTracker):
|
||||
tracker.failed.connect(update.call_deferred)
|
||||
tracker.disconnected.connect(update.call_deferred)
|
||||
add_tracker(tracker)
|
||||
add_message_item_control("Tracker {tracker} initiated".format({
|
||||
"tracker": str(tracker)
|
||||
}))
|
||||
update.call_deferred()
|
||||
|
||||
|
||||
func _on_client_peer_initiated(peer: TubePeer):
|
||||
add_peer(peer)
|
||||
add_message_item_control("Peer {peer_id} initiated".format({
|
||||
"peer_id": peer.id,
|
||||
}))
|
||||
update.call_deferred()
|
||||
|
||||
|
||||
func _on_client_port_mapped(public_port: int, local_port: int):
|
||||
add_message_item_control("Port {port} mapped to internal port {internal_port}".format({
|
||||
"port": public_port,
|
||||
"internal_port": local_port
|
||||
}))
|
||||
|
||||
|
||||
func _on_client_upnp_warning_raised(message: String):
|
||||
add_message_item_control("Upnp: " + message).warning()
|
||||
1
addons/tube/inspector/tube_inspector.gd.uid
Normal file
1
addons/tube/inspector/tube_inspector.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://c6txv1voyurrl
|
||||
8
addons/tube/plugin.cfg
Normal file
8
addons/tube/plugin.cfg
Normal file
@ -0,0 +1,8 @@
|
||||
[plugin]
|
||||
|
||||
name="Tube"
|
||||
description="A lightweight Godot addon that helps create simple multiplayer sessions.
|
||||
One player creates a session and shares the session ID with others through a external channel (WhatsApp, Discord, etc.). The other players can then join and play together. That’s it, no server deployment needed."
|
||||
author="Koop Myers"
|
||||
version="1.1"
|
||||
script="tube_addon.gd"
|
||||
33
addons/tube/tube_addon.gd
Normal file
33
addons/tube/tube_addon.gd
Normal file
@ -0,0 +1,33 @@
|
||||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
|
||||
func _enable_plugin() -> void:
|
||||
# Add autoloads here.
|
||||
pass
|
||||
|
||||
|
||||
func _disable_plugin() -> void:
|
||||
# Remove autoloads here.
|
||||
pass
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
add_custom_type(
|
||||
"TubeContext",
|
||||
"Resource",
|
||||
preload("tube_context.gd"),
|
||||
null
|
||||
)
|
||||
|
||||
add_custom_type(
|
||||
"TubeClient",
|
||||
"Node",
|
||||
preload("tube_client.gd"),
|
||||
null
|
||||
)
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
remove_custom_type("TubeContext")
|
||||
remove_custom_type("TubeClient")
|
||||
1
addons/tube/tube_addon.gd.uid
Normal file
1
addons/tube/tube_addon.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://wlh8u3rl6hmh
|
||||
797
addons/tube/tube_client.gd
Normal file
797
addons/tube/tube_client.gd
Normal file
@ -0,0 +1,797 @@
|
||||
@icon("./icons/tube_client.svg")
|
||||
class_name TubeClient extends Node
|
||||
## Node to create or join multiplayer session as simple as possible.
|
||||
##
|
||||
## One player creates a session and shares the session ID with others. The other players can then join and play together. That’s it, no server deployment needed.
|
||||
## [br][br]
|
||||
## This class will set up all the High-level multiplayer api for [member multiplayer_root_node] Node.
|
||||
## [br][br]
|
||||
## [b]Note[/b]: It uses WebRTC for peer connections, as it, it works automatically in HTML5, but require an external GDExtension plugin on other non-HTML5 platforms. Check out the [url=https://github.com/godotengine/webrtc-native/releases]webrtc-native plugin repository[/url] for instructions. No specific error message will appear if WebRTC implementation is missing.
|
||||
## [br][br]
|
||||
## When exporting to Android, make sure to enable the [code]INTERNET[/code] permission in the Android export preset before exporting the project or using one-click deploy. Otherwise, network communication of any kind will be blocked by Android.
|
||||
##
|
||||
## @tutorial(README): https://github.com/koopmyers/tube
|
||||
## @tutorial(Demo project): https://github.com/koopmyers/pixelary
|
||||
|
||||
## Emitted when a session has been successfully created.
|
||||
signal session_created
|
||||
|
||||
## Emitted when the client has successfully joined a session.
|
||||
signal session_joined
|
||||
|
||||
## Emitted when the client has left the current session. Emitted after calling [method leave_session]. Also emitted on non-sever when the server leaves (closes) the session or the connection is unrecoverable.
|
||||
signal session_left
|
||||
|
||||
## Emitted when a peer connection is refused. Only emitted on server if [member refuse_new_connections] is set to [code]true[/code] while player try to connect to the session.
|
||||
signal peer_refused(peer_id: int)
|
||||
|
||||
## Emitted when a peer successfully joins the session.
|
||||
## Emitted on all peers for every other peer.
|
||||
## [br][br]
|
||||
## When joining a session, it will be emitted for all peers, both server and non-server, already connected to the session.
|
||||
## This is equivalent to [signal MultiplayerPeer.peer_connected].
|
||||
signal peer_connected(peer_id: int)
|
||||
|
||||
## Emitted when a peer leaves the session or its connection becomes unrecoverable.
|
||||
## Emitted on all peers for every other peer.
|
||||
## [br][br]
|
||||
## This is equivalent to [signal MultiplayerPeer.peer_disconnected].
|
||||
signal peer_disconnected(peer_id: int)
|
||||
|
||||
## Emitted when a peer becomes temporarily unavailable, indicating a lost connection to the server.
|
||||
## [br][br]
|
||||
## This condition may represent a temporary network issue. If the connection recovers, [signal peer_stabilized] will be emitted. Otherwise, [signal peer_disconnected] will be emitted later. RPC on transport mode [code]"reliable"[/code] will be received when connection stabilizes again.
|
||||
## [br][br]
|
||||
## Emitted on both server and non-server peers. On non-server peers, this signal is only emitted for the server (where [param peer_id] equals 1), in this state, communication with other peers are also unstable.
|
||||
signal peer_unstabilized(peer_id: int)
|
||||
|
||||
## Emitted when the connection to a peer stabilizes after being unstable ([signal peer_unstabilized]). Communication with other peers is now possible again.
|
||||
## [br][br]
|
||||
## Emitted on both server and non-server peers. On non-server peers, this signal is only emitted for the server (where [param peer_id] equals 1).
|
||||
signal peer_stabilized(peer_id: int)
|
||||
|
||||
## Emitted when an error occurs during session. [code]message[/code] is a human-readable description of the error.
|
||||
signal error_raised(code: SessionError, message: String)
|
||||
|
||||
|
||||
signal _session_initiated
|
||||
signal _local_signaling_peer_initiated(signaling_peer: TubeLocalSignalingPeer)
|
||||
signal _tracker_initiated(tracker: TubeTracker)
|
||||
signal _peer_initiated(peer: TubePeer)
|
||||
|
||||
|
||||
enum State {
|
||||
## No active session. Can only create or join new session in this state.
|
||||
IDLE,
|
||||
|
||||
## Attempting to create a session.
|
||||
CREATING_SESSION,
|
||||
|
||||
## The session has been successfully created. Waiting for other player to join.
|
||||
SESSION_CREATED,
|
||||
|
||||
## Attempting to join a session.
|
||||
JOINING_SESSION,
|
||||
|
||||
## A session has been successfully joined. Connected to server.
|
||||
SESSION_JOINED,
|
||||
}
|
||||
|
||||
|
||||
enum SessionError {
|
||||
## Failed to create a session.
|
||||
CREATE_SESSION_FAILED,
|
||||
|
||||
## Failed to join a session.
|
||||
JOIN_SESSION_FAILED,
|
||||
|
||||
## Failed to kick a peer from the session.
|
||||
KICK_PEER_FAILED,
|
||||
|
||||
## Session signaling failed, only for server. Meaning new players will not be able to join the session. The session is still considerated open as communication will connected peer is still possible.
|
||||
## [br][br]
|
||||
## Signaling is composed of local and online signaling.
|
||||
SIGNALING_FAILED,
|
||||
|
||||
## Session online signaling failed, only for server. Meaning new players will not be able to join the via Internet session. The session is still considerated open as communication will connected peer is still possible.
|
||||
## [br][br]
|
||||
## Local signaling is not available on Web platform, meaing if online signaling failed on Web platform there is no other way for player to join the session. [signal error_raised] will be emitted once with [enum SessionError.ONLINE_SIGNALING_FAILED] and a second time with [enum SessionError.SIGNALING_FAILED].
|
||||
ONLINE_SIGNALING_FAILED,
|
||||
}
|
||||
|
||||
const _SERVER_PEER_ID: int = 1
|
||||
|
||||
|
||||
## Session context used to create or join a session.
|
||||
@export var context: TubeContext
|
||||
|
||||
## Timeout (in seconds) before signaling with a peer is considered failed. Will try again util [member peer_signaling_max_attempts] is reached.
|
||||
@export var peer_signaling_timeout:float = 2.0
|
||||
|
||||
## Maximum number of signaling attempts with a peer before failing.
|
||||
@export var peer_signaling_max_attempts: int = 3
|
||||
|
||||
## Root node to which the multiplayer API should attach.
|
||||
## If null, scene tree's root node will be used.
|
||||
@export var multiplayer_root_node: Node
|
||||
|
||||
## Current state of the session client.
|
||||
var state := State.IDLE
|
||||
|
||||
## The ID of the current session, if any.
|
||||
var session_id := ""
|
||||
|
||||
## The unique ID of this peer in the session.
|
||||
var peer_id: int
|
||||
|
||||
## Whether this peer is acting as the server, creator of a session.
|
||||
var is_server: bool:
|
||||
get:
|
||||
return _SERVER_PEER_ID == peer_id
|
||||
|
||||
## Instance of [MultiplayerAPI] used for High-level multiplayer
|
||||
var multiplayer_api := MultiplayerAPI.create_default_interface()
|
||||
|
||||
## Instance of [MultiplayerPeer] used for managing peer connections.
|
||||
var multiplayer_peer := WebRTCMultiplayerPeer.new()
|
||||
|
||||
## Server will refuse new connections if set to [code]true[/code].
|
||||
var refuse_new_connections: bool = false:
|
||||
get:
|
||||
if not is_server:
|
||||
return false
|
||||
|
||||
return refuse_new_connections
|
||||
|
||||
set(x):
|
||||
if not is_server:
|
||||
push_error("Cannot refuse new connections, not server")
|
||||
return
|
||||
|
||||
refuse_new_connections = x
|
||||
multiplayer_peer.refuse_new_connections = x
|
||||
|
||||
|
||||
var _local_signaling_peer: TubeLocalSignalingPeer
|
||||
var _trackers: Array[TubeTracker] = []
|
||||
var _peers: Dictionary[int, TubePeer] = {}
|
||||
var _upnp := TubeUPNP.new()
|
||||
|
||||
|
||||
func _raise_error(p_code: int, p_message: String):
|
||||
printerr(p_message)
|
||||
error_raised.emit(p_code, p_message)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var node_path := NodePath()
|
||||
if is_instance_valid(multiplayer_root_node):
|
||||
node_path = multiplayer_root_node.get_path()
|
||||
|
||||
get_tree().set_multiplayer(multiplayer_api, node_path)
|
||||
|
||||
if not multiplayer_api.peer_connected.is_connected(
|
||||
peer_connected.emit
|
||||
):
|
||||
multiplayer_api.peer_connected.connect(
|
||||
peer_connected.emit
|
||||
)
|
||||
multiplayer_api.peer_disconnected.connect(
|
||||
peer_disconnected.emit
|
||||
)
|
||||
|
||||
|
||||
# API ###
|
||||
|
||||
## Creates a new multiplayer session.
|
||||
## Emits [signal session_created] if successful, or [signal error_raised] with [code]SessionError.CREATE_SESSION_FAILED[/code] if failed.
|
||||
func create_session() -> void:
|
||||
if not is_inside_tree():
|
||||
_session_initiated.emit()
|
||||
_raise_error(SessionError.CREATE_SESSION_FAILED, "Session creation failed, client is not inside tree")
|
||||
return
|
||||
|
||||
if State.IDLE != state:
|
||||
_session_initiated.emit()
|
||||
_raise_error(SessionError.CREATE_SESSION_FAILED, "Session creation failed, not in idle state")
|
||||
return
|
||||
|
||||
if null == context:
|
||||
_session_initiated.emit()
|
||||
_raise_error(SessionError.CREATE_SESSION_FAILED, "Session creation failed, context is missing")
|
||||
return
|
||||
|
||||
if not context.is_valid():
|
||||
_session_initiated.emit()
|
||||
_raise_error(SessionError.CREATE_SESSION_FAILED, "Session creation failed, context is invalid")
|
||||
return
|
||||
|
||||
state = State.CREATING_SESSION
|
||||
session_id = context.generate_session_id()
|
||||
peer_id = _SERVER_PEER_ID
|
||||
refuse_new_connections = false
|
||||
_session_initiated.emit()
|
||||
|
||||
var error := multiplayer_peer.create_server()
|
||||
if error:
|
||||
_terminate_session()
|
||||
_raise_error(SessionError.CREATE_SESSION_FAILED, "Session creation failed, cannot create mutiplayer peer server: {error}".format({
|
||||
"error": error_string(error),
|
||||
}))
|
||||
return
|
||||
|
||||
multiplayer_api.multiplayer_peer = multiplayer_peer
|
||||
|
||||
_initiate_local_signaling()
|
||||
for url in context.trackers_urls:
|
||||
_initiate_tracker(url)
|
||||
|
||||
if _is_local_signaling() and not _is_online_signaling():
|
||||
state = State.SESSION_CREATED
|
||||
session_created.emit()
|
||||
|
||||
## Attempts to join a active session [param p_session_id] created by a server.
|
||||
## Emits [signal session_joined] if successful, or [signal error_raised] with [code]SessionError.JOIN_SESSION_FAILED[/code] if failed.
|
||||
func join_session(p_session_id: String) -> void:
|
||||
if not is_inside_tree():
|
||||
_session_initiated.emit()
|
||||
_raise_error(SessionError.JOIN_SESSION_FAILED, "Joining session failed, client is not inside tree")
|
||||
return
|
||||
|
||||
if State.IDLE != state:
|
||||
_session_initiated.emit()
|
||||
_raise_error(SessionError.JOIN_SESSION_FAILED, "Joining session failed, not in idle state")
|
||||
return
|
||||
|
||||
if null == context:
|
||||
_session_initiated.emit()
|
||||
_raise_error(SessionError.JOIN_SESSION_FAILED, "Joining session failed, context is missing")
|
||||
return
|
||||
|
||||
if not context.is_valid():
|
||||
_session_initiated.emit()
|
||||
_raise_error(SessionError.JOIN_SESSION_FAILED, "Joining session failed, context is invalid")
|
||||
return
|
||||
|
||||
if not context.is_session_id_valid(p_session_id):
|
||||
_session_initiated.emit()
|
||||
_raise_error(SessionError.JOIN_SESSION_FAILED, "Joining session failed, session id invalid '{id}'".format({
|
||||
"id": p_session_id,
|
||||
}))
|
||||
return
|
||||
|
||||
state = State.JOINING_SESSION
|
||||
session_id = p_session_id
|
||||
peer_id = multiplayer_peer.generate_unique_id()
|
||||
_session_initiated.emit()
|
||||
|
||||
var error := multiplayer_peer.create_client(peer_id)
|
||||
if error:
|
||||
_terminate_session()
|
||||
_raise_error(SessionError.JOIN_SESSION_FAILED, "Joining session failed, cannot create mutiplayer peer client: {error}".format({
|
||||
"error": error_string(error),
|
||||
}))
|
||||
return
|
||||
|
||||
multiplayer_api.multiplayer_peer = multiplayer_peer
|
||||
|
||||
var peer := _initiate_peer(_SERVER_PEER_ID)
|
||||
if not peer.valid:
|
||||
return
|
||||
|
||||
_initiate_local_signaling()
|
||||
for url in context.trackers_urls:
|
||||
_initiate_tracker(url)
|
||||
|
||||
## Attempts to remove a peer [param p_peer_id from the session.
|
||||
## Emits [signal peer_disconnected] if successful [signal error_raised] with [code]SessionError.KICK_PEER_FAILED[/code] if the operation fails.
|
||||
func kick_peer(p_peer_id: int) -> void:
|
||||
if not is_server:
|
||||
_raise_error(SessionError.KICK_PEER_FAILED, "Kick peer failed, not server")
|
||||
return
|
||||
|
||||
if not _peers.has(p_peer_id):
|
||||
_raise_error(SessionError.KICK_PEER_FAILED, "Kick peer failed, peer {peer_id}".format({
|
||||
"peer_id": p_peer_id
|
||||
}))
|
||||
return
|
||||
|
||||
multiplayer_peer.disconnect_peer(p_peer_id)
|
||||
|
||||
|
||||
## Leaves the current session. Will close the session for all other client if called by server.
|
||||
func leave_session() -> void:
|
||||
session_left.emit()
|
||||
_terminate_session()
|
||||
|
||||
|
||||
# SIGNALING ###
|
||||
|
||||
|
||||
func _terminate_signaling():
|
||||
if null != _local_signaling_peer:
|
||||
_local_signaling_peer.close()
|
||||
_local_signaling_peer = null
|
||||
|
||||
for i_tracker in _trackers:
|
||||
i_tracker.close(
|
||||
context.get_info_hash(session_id),
|
||||
context.get_peer_id_hash(peer_id)
|
||||
)
|
||||
|
||||
|
||||
func _terminate_session():
|
||||
state = State.IDLE
|
||||
_upnp.clear_port_mapping()
|
||||
|
||||
if null != _local_signaling_peer:
|
||||
_local_signaling_peer.close()
|
||||
_local_signaling_peer = null
|
||||
|
||||
for i_tracker in _trackers:
|
||||
i_tracker.close(
|
||||
context.get_info_hash(session_id),
|
||||
context.get_peer_id_hash(peer_id)
|
||||
)
|
||||
|
||||
for i_peer: TubePeer in _peers.values():
|
||||
i_peer.close() # will be clean collected
|
||||
|
||||
session_id = ""
|
||||
multiplayer_peer.close()
|
||||
|
||||
|
||||
func _is_local_signaling() -> bool:
|
||||
if null == _local_signaling_peer:
|
||||
return false
|
||||
|
||||
return _local_signaling_peer.is_bound()
|
||||
|
||||
|
||||
func _is_online_signaling() -> bool:
|
||||
return not _trackers.is_empty()
|
||||
|
||||
|
||||
func _initiate_local_signaling() -> void:
|
||||
if not TubeLocalSignalingPeer.is_local_signaling_available():
|
||||
return
|
||||
|
||||
_local_signaling_peer = TubeLocalSignalingPeer.new()
|
||||
var error := _local_signaling_peer.bind(
|
||||
context.app_id,
|
||||
session_id,
|
||||
peer_id
|
||||
)
|
||||
_local_signaling_peer_initiated.emit(
|
||||
_local_signaling_peer
|
||||
)
|
||||
|
||||
if error:
|
||||
_local_signaling_peer = null
|
||||
return
|
||||
|
||||
_local_signaling_peer.received_signaling_data.connect(
|
||||
_handle_local_signaling_data
|
||||
)
|
||||
|
||||
|
||||
func _initiate_tracker(p_url: String) -> void:
|
||||
var tracker := TubeTracker.new()
|
||||
var error := tracker.connect_to_url(p_url)
|
||||
_tracker_initiated.emit(tracker)
|
||||
|
||||
if error:
|
||||
return
|
||||
|
||||
_trackers.append(tracker)
|
||||
tracker.connected.connect(
|
||||
_on_tracker_connected.bind(tracker)
|
||||
)
|
||||
tracker.received_answer.connect(
|
||||
_handle_tracker_answer.bind(tracker)
|
||||
)
|
||||
tracker.interval_timeout.connect(
|
||||
_on_tracker_interval_timeout.bind(tracker)
|
||||
)
|
||||
|
||||
|
||||
func _on_tracker_connected(p_tracker: TubeTracker):
|
||||
p_tracker.send_announce(
|
||||
context.get_info_hash(session_id),
|
||||
context.get_peer_id_hash(peer_id),
|
||||
)
|
||||
|
||||
if State.CREATING_SESSION == state:
|
||||
state = State.SESSION_CREATED
|
||||
session_created.emit()
|
||||
|
||||
if is_server:
|
||||
return
|
||||
|
||||
if not _peers.has(_SERVER_PEER_ID):
|
||||
return
|
||||
|
||||
var server_peer := _peers[_SERVER_PEER_ID]
|
||||
if server_peer.is_signaling_ready():
|
||||
_send_signaling_data(server_peer, p_tracker)
|
||||
|
||||
|
||||
func _all_trackers_disconnected(): # is_online_signaling false
|
||||
if State.CREATING_SESSION == state:
|
||||
if _is_local_signaling():
|
||||
_raise_error(
|
||||
SessionError.ONLINE_SIGNALING_FAILED,
|
||||
"Online signaling failed, cannot connect to any tracker"
|
||||
)
|
||||
return
|
||||
|
||||
_raise_error(
|
||||
SessionError.CREATE_SESSION_FAILED,
|
||||
"Session creation failed, cannot connect to any tracker"
|
||||
)
|
||||
_terminate_session()
|
||||
|
||||
elif State.SESSION_CREATED == state:
|
||||
_raise_error(
|
||||
SessionError.ONLINE_SIGNALING_FAILED,
|
||||
"Signaling failed, lost all trackers connections"
|
||||
)
|
||||
|
||||
if not _is_local_signaling():
|
||||
_raise_error(
|
||||
SessionError.ONLINE_SIGNALING_FAILED,
|
||||
"Signaling failed, lost all trackers connections"
|
||||
)
|
||||
|
||||
|
||||
elif State.JOINING_SESSION == state:
|
||||
if _peers.has(_SERVER_PEER_ID):
|
||||
var peer = _peers[_SERVER_PEER_ID]
|
||||
if peer.remote_session_description.is_empty():
|
||||
_raise_error(
|
||||
SessionError.JOIN_SESSION_FAILED,
|
||||
"Joining session failed, cannot connect to any tracker"
|
||||
)
|
||||
_terminate_session()
|
||||
|
||||
|
||||
func _handle_local_signaling_data(p_data: Dictionary, p_address: String):
|
||||
var from_app_id := TubeLocalSignalingPeer.get_app_id_from_signaling_data(p_data)
|
||||
if from_app_id != context.app_id:
|
||||
_local_signaling_peer.raise_warning("Received signaling data from other app ID")
|
||||
return
|
||||
|
||||
var from_session_id := TubeLocalSignalingPeer.get_session_id_from_signaling_data(p_data)
|
||||
if from_session_id != session_id:
|
||||
_local_signaling_peer.raise_warning("Received signaling data from other session ID")
|
||||
return
|
||||
|
||||
var from_peer_id := TubeLocalSignalingPeer.get_peer_id_from_signaling_data(p_data)
|
||||
if is_server and from_peer_id == _SERVER_PEER_ID:
|
||||
_local_signaling_peer.raise_warning("Received signaling data from peer id 1 as server")
|
||||
return
|
||||
|
||||
if not is_server and from_peer_id != _SERVER_PEER_ID:
|
||||
_local_signaling_peer.raise_warning("Received signaling data from peer {peer_id}, but not as server".format({
|
||||
"peer_id": from_peer_id
|
||||
}))
|
||||
return
|
||||
|
||||
if refuse_new_connections:
|
||||
peer_refused.emit(from_peer_id)
|
||||
return
|
||||
|
||||
var peer: TubePeer = _peers.get(from_peer_id)
|
||||
if null == peer:
|
||||
peer = _initiate_peer(from_peer_id)
|
||||
if not peer.valid:
|
||||
return
|
||||
|
||||
peer.local_address = p_address
|
||||
|
||||
if WebRTCPeerConnection.ConnectionState.STATE_CONNECTED == peer.get_connection_state():
|
||||
peer.raise_warning(
|
||||
"Receive signaling data but already connected"
|
||||
)
|
||||
return
|
||||
|
||||
if peer.remote_session_description.is_empty():
|
||||
var type := TubeLocalSignalingPeer.get_type_from_signaling_data(p_data)
|
||||
var sdp := TubeLocalSignalingPeer.get_sdp_from_signaling_data(p_data)
|
||||
if peer.set_remote_description(type, sdp): # error
|
||||
return
|
||||
|
||||
for candidate_data in TubeLocalSignalingPeer.get_ice_candidates_from_signaling_data(p_data):
|
||||
|
||||
if not TubeLocalSignalingPeer.is_ice_candidate_data_valid(candidate_data):
|
||||
peer.raise_warning(
|
||||
"Cannot add ice candidate, ice data invalid"
|
||||
)
|
||||
continue
|
||||
|
||||
peer.add_ice_candidate(
|
||||
TubeLocalSignalingPeer.get_media_from_ice_candidate_data(
|
||||
candidate_data
|
||||
),
|
||||
TubeLocalSignalingPeer.get_index_from_ice_candidate_data(
|
||||
candidate_data
|
||||
),
|
||||
TubeLocalSignalingPeer.get_sdp_from_ice_candidate_data(
|
||||
candidate_data
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _handle_tracker_answer(data: Dictionary, p_tracker: TubeTracker):
|
||||
var from_peer_id_hash := TubeTracker.get_peer_id_hash_from_answer_data(data)
|
||||
if not context.is_peer_id_hash_valid(from_peer_id_hash):
|
||||
p_tracker.raise_warning("answer peer id invalid")
|
||||
return
|
||||
|
||||
var from_peer_id := context.get_peer_id(from_peer_id_hash)
|
||||
if refuse_new_connections:
|
||||
peer_refused.emit(from_peer_id)
|
||||
return
|
||||
|
||||
var peer: TubePeer = _peers.get(from_peer_id)
|
||||
if null == peer:
|
||||
peer = _initiate_peer(from_peer_id)
|
||||
if not peer.valid:
|
||||
return
|
||||
|
||||
|
||||
if WebRTCPeerConnection.ConnectionState.STATE_CONNECTED == peer.get_connection_state():
|
||||
peer.raise_warning(
|
||||
"Receive signaling data but already connected"
|
||||
)
|
||||
return
|
||||
|
||||
if peer.remote_session_description.is_empty():
|
||||
var type := TubeTracker.get_type_from_answer_data(data)
|
||||
var sdp := TubeTracker.get_sdp_from_answer_data(data)
|
||||
if peer.set_remote_description(type, sdp): # error
|
||||
return
|
||||
|
||||
for candidate_data in TubeTracker.get_ice_candidates_from_answer_data(data):
|
||||
|
||||
if not TubeTracker.is_ice_candidate_data_valid(candidate_data):
|
||||
peer.raise_warning(
|
||||
"Cannot add ice candidate, ice data invalid"
|
||||
)
|
||||
continue
|
||||
|
||||
peer.add_ice_candidate(
|
||||
TubeTracker.get_media_from_ice_candidate_data(
|
||||
candidate_data
|
||||
),
|
||||
TubeTracker.get_index_from_ice_candidate_data(
|
||||
candidate_data
|
||||
),
|
||||
TubeTracker.get_sdp_from_ice_candidate_data(
|
||||
candidate_data
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _on_tracker_interval_timeout(p_tracker: TubeTracker):
|
||||
p_tracker.send_announce(
|
||||
context.get_info_hash(session_id),
|
||||
context.get_peer_id_hash(peer_id),
|
||||
)
|
||||
|
||||
|
||||
func _send_signaling_data(p_peer: TubePeer, p_tracker: TubeTracker = null):
|
||||
|
||||
if _local_signaling_peer:
|
||||
if is_server:
|
||||
_local_signaling_peer.send_signaling_data(
|
||||
p_peer.local_address,
|
||||
context.app_id,
|
||||
session_id,
|
||||
peer_id,
|
||||
p_peer.id,
|
||||
p_peer.local_session_description,
|
||||
p_peer.ice_candidates,
|
||||
)
|
||||
else:
|
||||
_local_signaling_peer.broadcast_signaling_data(
|
||||
context.app_id,
|
||||
session_id,
|
||||
peer_id,
|
||||
p_peer.id,
|
||||
p_peer.local_session_description,
|
||||
p_peer.ice_candidates,
|
||||
)
|
||||
|
||||
var info_hash := context.get_info_hash(session_id)
|
||||
var peer_id_hash := context.get_peer_id_hash(peer_id)
|
||||
var to_peer_id_hash := context.get_peer_id_hash(p_peer.id)
|
||||
|
||||
var to_trackers = _trackers
|
||||
if p_tracker:
|
||||
to_trackers = [p_tracker]
|
||||
|
||||
for i_tracker in to_trackers:
|
||||
if not i_tracker.is_open():
|
||||
continue
|
||||
|
||||
i_tracker.send_answer(
|
||||
info_hash,
|
||||
peer_id_hash,
|
||||
to_peer_id_hash,
|
||||
p_peer.local_session_description,
|
||||
p_peer.ice_candidates,
|
||||
)
|
||||
|
||||
|
||||
# PEER CONNECTION ###
|
||||
|
||||
func _initiate_peer(p_peer_id: int) -> TubePeer:
|
||||
var peer := TubePeer.new(p_peer_id)
|
||||
peer.signaling_timeout_time = peer_signaling_timeout
|
||||
peer.signaling_max_attempts = peer_signaling_max_attempts
|
||||
var error := peer.initialize(
|
||||
context.get_ice_servers()
|
||||
)
|
||||
|
||||
if error: # error raised with peer.failed
|
||||
return peer
|
||||
|
||||
|
||||
peer.signaling_readied.connect(
|
||||
_on_peer_signaling_readied.bind(peer)
|
||||
)
|
||||
peer.signaling_timeout.connect(
|
||||
_on_peer_signaling_timeout.bind(peer)
|
||||
)
|
||||
peer.connected.connect(
|
||||
_on_peer_connected.bind(peer)
|
||||
)
|
||||
peer.disconnected.connect(
|
||||
_on_peer_disconnected.bind(peer)
|
||||
)
|
||||
peer.failed.connect(
|
||||
_on_peer_failed.bind(peer)
|
||||
)
|
||||
peer.closed.connect(
|
||||
_on_peer_closed.bind(peer)
|
||||
)
|
||||
peer.port_mapped.connect(
|
||||
_upnp.add_port_mapping
|
||||
)
|
||||
|
||||
_peers[p_peer_id] = peer
|
||||
_peer_initiated.emit(peer)
|
||||
error = multiplayer_peer.add_peer(peer, p_peer_id)
|
||||
if error:
|
||||
peer.valid = false
|
||||
peer.error_message = "cannot add to multiplayer: ".format({
|
||||
"error": error_string(error)
|
||||
})
|
||||
peer.failed.emit()
|
||||
|
||||
if not is_server:
|
||||
error = peer.create_offer()
|
||||
if error: # error raised with peer.failed
|
||||
return peer
|
||||
|
||||
return peer
|
||||
|
||||
|
||||
func _clean_peer(p_peer: TubePeer):
|
||||
if multiplayer_peer.has_peer(p_peer.id):
|
||||
multiplayer_peer.remove_peer(p_peer.id)
|
||||
|
||||
for port in p_peer.mapped_ports:
|
||||
_upnp.delete_port_mapping(port)
|
||||
|
||||
#if _peers.has(p_peer.id): # garbage collected
|
||||
#_peers.erase(p_peer.id)
|
||||
p_peer.has_joined_session = false
|
||||
p_peer.close()
|
||||
|
||||
|
||||
|
||||
func _on_peer_signaling_readied(p_peer: TubePeer):
|
||||
_send_signaling_data(p_peer)
|
||||
p_peer.start_connection_attempt()
|
||||
|
||||
|
||||
func _on_peer_signaling_timeout(p_peer: TubePeer):
|
||||
_send_signaling_data(p_peer)
|
||||
p_peer.start_connection_attempt()
|
||||
|
||||
|
||||
func _on_peer_connected(p_peer: TubePeer):
|
||||
if State.IDLE == state:
|
||||
_clean_peer(p_peer)
|
||||
return
|
||||
|
||||
if State.JOINING_SESSION == state:
|
||||
state = State.SESSION_JOINED
|
||||
_terminate_signaling()
|
||||
session_joined.emit()
|
||||
|
||||
if p_peer.has_joined_session:
|
||||
peer_stabilized.emit(p_peer.id)
|
||||
p_peer.has_joined_session = true
|
||||
#peer connected will be emitted by multiplayer
|
||||
|
||||
|
||||
func _on_peer_disconnected(p_peer: TubePeer): # temporary disconnection
|
||||
if State.IDLE == state:
|
||||
_clean_peer(p_peer)
|
||||
return
|
||||
|
||||
peer_unstabilized.emit(p_peer.id)
|
||||
|
||||
|
||||
func _on_peer_failed(p_peer: TubePeer):
|
||||
_clean_peer(p_peer)
|
||||
if State.IDLE == state:
|
||||
return
|
||||
|
||||
if not is_server:
|
||||
if State.JOINING_SESSION == state:
|
||||
_raise_error(
|
||||
SessionError.JOIN_SESSION_FAILED,
|
||||
"Joining session failed, peer {peer_id} connection failed: {error}".format({
|
||||
"peer_id": p_peer.id,
|
||||
"error": p_peer.error_message
|
||||
})
|
||||
)
|
||||
|
||||
elif State.SESSION_JOINED == state:
|
||||
session_left.emit()
|
||||
|
||||
_terminate_session()
|
||||
|
||||
|
||||
func _on_peer_closed(p_peer: TubePeer):
|
||||
_clean_peer(p_peer)
|
||||
if State.IDLE == state:
|
||||
return
|
||||
|
||||
if not is_server:
|
||||
if State.SESSION_JOINED == state:
|
||||
session_left.emit()
|
||||
|
||||
_terminate_session()
|
||||
|
||||
|
||||
# PROCESS ###
|
||||
|
||||
func _process(delta):
|
||||
|
||||
if _upnp:
|
||||
_upnp._process(delta)
|
||||
|
||||
if _local_signaling_peer:
|
||||
_local_signaling_peer._process(delta)
|
||||
|
||||
var tracker_closed := false
|
||||
var updated_trackers: Array[TubeTracker] = []
|
||||
for i_tracker in _trackers:
|
||||
i_tracker._process(delta)
|
||||
if i_tracker.is_close():
|
||||
tracker_closed = true
|
||||
continue
|
||||
|
||||
updated_trackers.append(i_tracker)
|
||||
|
||||
_trackers = updated_trackers
|
||||
if tracker_closed and not _is_online_signaling():
|
||||
_all_trackers_disconnected()
|
||||
|
||||
|
||||
var updated_peers: Dictionary[int, TubePeer] = {}
|
||||
for i_peer_id in _peers:
|
||||
var i_peer := _peers[i_peer_id]
|
||||
i_peer._process(delta)
|
||||
if WebRTCPeerConnection.STATE_CLOSED == i_peer.connection_state: # don't use is_close to use connection_state
|
||||
_clean_peer(i_peer)
|
||||
continue
|
||||
|
||||
updated_peers[i_peer_id] = i_peer
|
||||
|
||||
_peers = updated_peers
|
||||
1
addons/tube/tube_client.gd.uid
Normal file
1
addons/tube/tube_client.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cy006uvidc4y
|
||||
152
addons/tube/tube_context.gd
Normal file
152
addons/tube/tube_context.gd
Normal file
@ -0,0 +1,152 @@
|
||||
@icon("./icons/tube_context.svg")
|
||||
@tool
|
||||
class_name TubeContext extends Resource
|
||||
## A resource that holds configuration and helper methods for managing simple multiplayer session.
|
||||
|
||||
## Character set to generate app IDs. Contains most printable ASCII characters.
|
||||
const _APP_ID_CHARACTER_SET := "!#$%&()*+,-./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890:;<=>?@[]^_{|}~"
|
||||
|
||||
@export_tool_button("Generate app id", "RandomNumberGenerator") var _generate_app_id_tool_button = (func():
|
||||
app_id = _get_random_string(15, _APP_ID_CHARACTER_SET)
|
||||
)
|
||||
|
||||
## Application identifier for this multiplayer context.
|
||||
## Must be exactly 15 ASCII characters long.
|
||||
@export var app_id: String
|
||||
|
||||
## Character set used to generate session IDs.
|
||||
## Must not be empty and should only contain ASCII characters.
|
||||
## A larger set reduces the probability of collision. With 62 characters
|
||||
## (A–Z, a–z, 0–9), the chance of two random 5-character IDs matching is approximately 1 in 916 million.
|
||||
## For readability by players, consider removing ambiguous characters (e.g., oO0, ilj1I, z2).
|
||||
@export_multiline var session_id_characters_set: String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"
|
||||
|
||||
## List of tracker server URLs used for session signaling.
|
||||
@export var trackers_urls: Array[String] = []
|
||||
|
||||
## List of STUN server URLs used for WebRTC ICE candidate resolution.
|
||||
@export var stun_servers_urls: Array[String] = []
|
||||
|
||||
## List of TURN servers (optional). Turn server are dictionnary in the form:
|
||||
## [codeblock]
|
||||
## {
|
||||
## "urls": "turn:turn.example.com:3478",
|
||||
## "username: "my-username",
|
||||
## "credential": "my-credential",
|
||||
## }
|
||||
@export var turn_servers: Array[Dictionary] = []
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return "AppID: %s | Trackers: %s | STUN: %s" % [app_id, str(trackers_urls), str(stun_servers_urls)]
|
||||
|
||||
|
||||
func _is_ascii(string: String) -> bool:
|
||||
for char_index in range(string.length()):
|
||||
if string.unicode_at(char_index) >= 128:
|
||||
return false
|
||||
return true
|
||||
|
||||
## Checks if the context configuration is valid.
|
||||
func is_valid() -> bool:
|
||||
if 0 == session_id_characters_set.length():
|
||||
printerr("Session ID Character Set is empty")
|
||||
return false
|
||||
|
||||
if not _is_ascii(session_id_characters_set):
|
||||
printerr("Session ID Character Set can only contain ASCII characters")
|
||||
return false
|
||||
|
||||
if null == app_id or 15 != app_id.length() or not _is_ascii(app_id):
|
||||
printerr("App id is invalid")
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
## Returns ICE server configuration dictionary for WebRTC peer connection.
|
||||
##
|
||||
## Example:
|
||||
## [codeblock]
|
||||
## {
|
||||
## "iceServers": [
|
||||
## {
|
||||
## "urls": [ "stun:stun.example.com:3478" ], # One or more STUN servers.
|
||||
## },
|
||||
## {
|
||||
## "urls": [ "turn:turn.example.com:3478" ], # One or more TURN servers.
|
||||
## "username": "a_username", # Optional username for the TURN server.
|
||||
## "credential": "a_password", # Optional password for the TURN server.
|
||||
## }
|
||||
## ]
|
||||
## }
|
||||
|
||||
## [/codeblock]
|
||||
func get_ice_servers() -> Dictionary:
|
||||
var ice_servers := []
|
||||
|
||||
if null != stun_servers_urls:
|
||||
for url in stun_servers_urls:
|
||||
ice_servers.append({
|
||||
"urls": url
|
||||
})
|
||||
|
||||
if null != turn_servers:
|
||||
for turn_server in turn_servers:
|
||||
ice_servers.append(turn_server)
|
||||
|
||||
if ice_servers.is_empty():
|
||||
return {}
|
||||
|
||||
return {
|
||||
"iceServers": ice_servers
|
||||
}
|
||||
|
||||
|
||||
func _get_random_string(p_size: int, character_set: String) -> String:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.randomize()
|
||||
|
||||
var character_set_length := character_set.length()
|
||||
|
||||
var out := ""
|
||||
for i in range(p_size):
|
||||
var index := rng.randi()%character_set_length
|
||||
out += character_set[index]
|
||||
|
||||
return out
|
||||
|
||||
|
||||
## Generates a random 5-character session ID.
|
||||
func generate_session_id() -> String:
|
||||
return _get_random_string(5, session_id_characters_set)
|
||||
|
||||
|
||||
## Validates if a session ID is correct
|
||||
func is_session_id_valid(p_session_id: String) -> bool:
|
||||
return 5 == p_session_id.length()
|
||||
|
||||
|
||||
## Validates if a peer ID hash is numeric and valid.
|
||||
func is_peer_id_hash_valid(p_peer_id_hash: String) -> bool:
|
||||
return p_peer_id_hash.is_valid_int()
|
||||
|
||||
## Returns the combined "info hash" (app ID and session ID) for tracker usage.
|
||||
func get_info_hash(p_session_id: String) -> String:
|
||||
if not is_session_id_valid(p_session_id):
|
||||
printerr("Invalid session id")
|
||||
return ""
|
||||
|
||||
return app_id + p_session_id
|
||||
|
||||
|
||||
## Converts a integer peer ID hash into an peer ID hash for tracker usage.
|
||||
func get_peer_id_hash(p_peer_id: int) -> String:
|
||||
return str(p_peer_id).pad_zeros(20)
|
||||
|
||||
|
||||
## Converts a peer ID hash into an integer peer ID.
|
||||
func get_peer_id(p_peer_id_hash: String) -> int:
|
||||
if not is_peer_id_hash_valid(p_peer_id_hash):
|
||||
return 0
|
||||
|
||||
return int(p_peer_id_hash)
|
||||
1
addons/tube/tube_context.gd.uid
Normal file
1
addons/tube/tube_context.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://t4pe7yqc3pnt
|
||||
302
addons/tube/tube_inspector.tscn
Normal file
302
addons/tube/tube_inspector.tscn
Normal file
@ -0,0 +1,302 @@
|
||||
[gd_scene load_steps=11 format=3 uid="uid://dujkendqt6ls1"]
|
||||
|
||||
[ext_resource type="Theme" uid="uid://bcibt73qths3g" path="res://addons/tube/inspector/theme.tres" id="1_xsva5"]
|
||||
[ext_resource type="Script" uid="uid://c6txv1voyurrl" path="res://addons/tube/inspector/tube_inspector.gd" id="2_6ede3"]
|
||||
[ext_resource type="ButtonGroup" uid="uid://fko7ise7cj31" path="res://addons/tube/inspector/tracker_peer_item_button_group.tres" id="3_v58fy"]
|
||||
[ext_resource type="PackedScene" uid="uid://c3p410vwblsb3" path="res://addons/tube/inspector/client_control.tscn" id="4_6ede3"]
|
||||
[ext_resource type="PackedScene" uid="uid://dyfuyauko76jj" path="res://addons/tube/inspector/chat_control.tscn" id="4_geh7p"]
|
||||
[ext_resource type="PackedScene" uid="uid://ja0u2vuivo8b" path="res://addons/tube/inspector/tracker_control.tscn" id="5_2rkog"]
|
||||
[ext_resource type="PackedScene" uid="uid://5f8u55hvqq4w" path="res://addons/tube/inspector/local_signaling_control.tscn" id="5_v58fy"]
|
||||
[ext_resource type="PackedScene" uid="uid://ckrifxh4o768d" path="res://addons/tube/inspector/peer_control.tscn" id="6_xyrfc"]
|
||||
[ext_resource type="PackedScene" uid="uid://btfc8o5xfs14w" path="res://addons/tube/inspector/messages_container.tscn" id="7_6bfdn"]
|
||||
[ext_resource type="PackedScene" uid="uid://bi8vgsoslhvrb" path="res://addons/tube/inspector/message_control.tscn" id="8_pnoa1"]
|
||||
|
||||
[node name="TubeInspector" type="PanelContainer"]
|
||||
offset_right = 524.0
|
||||
offset_bottom = 565.0
|
||||
theme = ExtResource("1_xsva5")
|
||||
script = ExtResource("2_6ede3")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="HeaderContainer" type="MarginContainer" parent="VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 48)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer/HeaderContainer"]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"PanelH1Container"
|
||||
|
||||
[node name="HeaderButton" type="Button" parent="VBoxContainer/HeaderContainer"]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"ButtonFlat"
|
||||
toggle_mode = true
|
||||
button_pressed = true
|
||||
button_group = ExtResource("3_v58fy")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/HeaderContainer"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/margin_left = 16
|
||||
theme_override_constants/margin_top = 9
|
||||
theme_override_constants/margin_right = 16
|
||||
theme_override_constants/margin_bottom = 9
|
||||
|
||||
[node name="VContainer" type="VBoxContainer" parent="VBoxContainer/HeaderContainer/MarginContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="SessionContainer" type="HBoxContainer" parent="VBoxContainer/HeaderContainer/MarginContainer/VContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/HeaderContainer/MarginContainer/VContainer/SessionContainer"]
|
||||
custom_minimum_size = Vector2(96, 0)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Session"
|
||||
|
||||
[node name="SessionLineEdit" type="LineEdit" parent="VBoxContainer/HeaderContainer/MarginContainer/VContainer/SessionContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(128, 40)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "ABCDE"
|
||||
placeholder_text = "Enter session id"
|
||||
alignment = 1
|
||||
|
||||
[node name="JoinButton" type="Button" parent="VBoxContainer/HeaderContainer/MarginContainer/VContainer/SessionContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(96, 40)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"FlatButton"
|
||||
text = "JOIN"
|
||||
|
||||
[node name="CreateButton" type="Button" parent="VBoxContainer/HeaderContainer/MarginContainer/VContainer/SessionContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(96, 40)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"FlatButton"
|
||||
text = "CREATE"
|
||||
|
||||
[node name="CloseButton" type="Button" parent="VBoxContainer/HeaderContainer/MarginContainer/VContainer/SessionContainer"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(96, 0)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"FlatButton"
|
||||
text = "QUIT"
|
||||
|
||||
[node name="PeerContainer" type="HBoxContainer" parent="VBoxContainer/HeaderContainer/MarginContainer/VContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/HeaderContainer/MarginContainer/VContainer/PeerContainer"]
|
||||
custom_minimum_size = Vector2(96, 0)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Peer"
|
||||
|
||||
[node name="PeerLabel" type="Label" parent="VBoxContainer/HeaderContainer/MarginContainer/VContainer/PeerContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(208, 0)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 1
|
||||
theme_type_variation = &"HeaderLarge"
|
||||
text = "0000000000000000000"
|
||||
|
||||
[node name="SessionIndicator" type="Panel" parent="VBoxContainer/HeaderContainer/MarginContainer/VContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(4, 4)
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_type_variation = &"PanelIndicator"
|
||||
|
||||
[node name="VSplitContainer" type="VSplitContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="SocketsContainer" type="HSplitContainer" parent="VBoxContainer/VSplitContainer"]
|
||||
custom_minimum_size = Vector2(0, 160)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
size_flags_stretch_ratio = 0.35
|
||||
|
||||
[node name="TrackersPanelContainer" type="PanelContainer" parent="VBoxContainer/VSplitContainer/SocketsContainer"]
|
||||
custom_minimum_size = Vector2(0, 128)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_type_variation = &"PanelH1Container"
|
||||
|
||||
[node name="TrackersPanel" type="VBoxContainer" parent="VBoxContainer/VSplitContainer/SocketsContainer/TrackersPanelContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="LocalSignalingContainer" type="MarginContainer" parent="VBoxContainer/VSplitContainer/SocketsContainer/TrackersPanelContainer/TrackersPanel"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="LocalSignalingButton" type="Button" parent="VBoxContainer/VSplitContainer/SocketsContainer/TrackersPanelContainer/TrackersPanel/LocalSignalingContainer"]
|
||||
custom_minimum_size = Vector2(100, 30)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"ButtonFlat"
|
||||
toggle_mode = true
|
||||
button_group = ExtResource("3_v58fy")
|
||||
|
||||
[node name="Container" type="HBoxContainer" parent="VBoxContainer/VSplitContainer/SocketsContainer/TrackersPanelContainer/TrackersPanel/LocalSignalingContainer"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="LocalSignalingIndicator" type="Panel" parent="VBoxContainer/VSplitContainer/SocketsContainer/TrackersPanelContainer/TrackersPanel/LocalSignalingContainer/Container"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(16, 8)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
theme_type_variation = &"PanelIndicator"
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/VSplitContainer/SocketsContainer/TrackersPanelContainer/TrackersPanel/LocalSignalingContainer/Container"]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Local signaling"
|
||||
|
||||
[node name="TrackerHeader" type="HBoxContainer" parent="VBoxContainer/VSplitContainer/SocketsContainer/TrackersPanelContainer/TrackersPanel"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="TrackersIndicator" type="Panel" parent="VBoxContainer/VSplitContainer/SocketsContainer/TrackersPanelContainer/TrackersPanel/TrackerHeader"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(16, 8)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
size_flags_vertical = 4
|
||||
theme_type_variation = &"PanelIndicator"
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/VSplitContainer/SocketsContainer/TrackersPanelContainer/TrackersPanel/TrackerHeader"]
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Online signaling - trackers"
|
||||
|
||||
[node name="TrackersContainer" type="VBoxContainer" parent="VBoxContainer/VSplitContainer/SocketsContainer/TrackersPanelContainer/TrackersPanel"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="PeersPanelContainer" type="PanelContainer" parent="VBoxContainer/VSplitContainer/SocketsContainer"]
|
||||
custom_minimum_size = Vector2(128, 0)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_type_variation = &"PanelH1Container"
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/VSplitContainer/SocketsContainer/PeersPanelContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/VSplitContainer/SocketsContainer/PeersPanelContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer/VSplitContainer/SocketsContainer/PeersPanelContainer/VBoxContainer/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 2
|
||||
theme_type_variation = &"LabelH3"
|
||||
text = "Peers"
|
||||
|
||||
[node name="RefuseNewButton" type="Button" parent="VBoxContainer/VSplitContainer/SocketsContainer/PeersPanelContainer/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(144, 0)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"FlatButton"
|
||||
toggle_mode = true
|
||||
text = "REFUSE NEW"
|
||||
|
||||
[node name="ChatButton" type="Button" parent="VBoxContainer/VSplitContainer/SocketsContainer/PeersPanelContainer/VBoxContainer/HBoxContainer"]
|
||||
custom_minimum_size = Vector2(100, 30)
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"ButtonFlat"
|
||||
toggle_mode = true
|
||||
button_group = ExtResource("3_v58fy")
|
||||
text = "Chat"
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer/VSplitContainer/SocketsContainer/PeersPanelContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
follow_focus = true
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="PeersContainer" type="VBoxContainer" parent="VBoxContainer/VSplitContainer/SocketsContainer/PeersPanelContainer/VBoxContainer/ScrollContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="VSplitContainer" type="VSplitContainer" parent="VBoxContainer/VSplitContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer/VSplitContainer/VSplitContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_type_variation = &"PanelH1Container"
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/VSplitContainer/VSplitContainer/PanelContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="TabContainer" type="TabContainer" parent="VBoxContainer/VSplitContainer/VSplitContainer/PanelContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
current_tab = 0
|
||||
tabs_visible = false
|
||||
|
||||
[node name="ClientControl" parent="VBoxContainer/VSplitContainer/VSplitContainer/PanelContainer/VBoxContainer/TabContainer" node_paths=PackedStringArray("inspector") instance=ExtResource("4_6ede3")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
inspector = NodePath("../../../../../../..")
|
||||
|
||||
[node name="LocalSignalingControl" parent="VBoxContainer/VSplitContainer/VSplitContainer/PanelContainer/VBoxContainer/TabContainer" node_paths=PackedStringArray("messages_container") instance=ExtResource("5_v58fy")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
messages_container = NodePath("../../MessagesContainer")
|
||||
metadata/_tab_index = 1
|
||||
|
||||
[node name="TrackerControl" parent="VBoxContainer/VSplitContainer/VSplitContainer/PanelContainer/VBoxContainer/TabContainer" node_paths=PackedStringArray("messages_container") instance=ExtResource("5_2rkog")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
messages_container = NodePath("../../MessagesContainer")
|
||||
metadata/_tab_index = 2
|
||||
|
||||
[node name="PeerControl" parent="VBoxContainer/VSplitContainer/VSplitContainer/PanelContainer/VBoxContainer/TabContainer" node_paths=PackedStringArray("messages_container") instance=ExtResource("6_xyrfc")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
messages_container = NodePath("../../MessagesContainer")
|
||||
metadata/_tab_index = 3
|
||||
|
||||
[node name="ChatControl" parent="VBoxContainer/VSplitContainer/VSplitContainer/PanelContainer/VBoxContainer/TabContainer" node_paths=PackedStringArray("messages_container") instance=ExtResource("4_geh7p")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
messages_container = NodePath("../../MessagesContainer")
|
||||
metadata/_tab_index = 4
|
||||
|
||||
[node name="MessagesContainer" parent="VBoxContainer/VSplitContainer/VSplitContainer/PanelContainer/VBoxContainer" node_paths=PackedStringArray("message_control") instance=ExtResource("7_6bfdn")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
message_control = NodePath("../../../MessageControl")
|
||||
|
||||
[node name="MessageControl" parent="VBoxContainer/VSplitContainer/VSplitContainer" instance=ExtResource("8_pnoa1")]
|
||||
custom_minimum_size = Vector2(0, 96)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
size_flags_stretch_ratio = 0.7
|
||||
|
||||
[connection signal="pressed" from="VBoxContainer/HeaderContainer/HeaderButton" to="." method="_on_header_button_pressed"]
|
||||
[connection signal="pressed" from="VBoxContainer/HeaderContainer/MarginContainer/VContainer/SessionContainer/JoinButton" to="." method="_on_join_button_pressed"]
|
||||
[connection signal="pressed" from="VBoxContainer/HeaderContainer/MarginContainer/VContainer/SessionContainer/CreateButton" to="." method="_on_create_button_pressed"]
|
||||
[connection signal="pressed" from="VBoxContainer/HeaderContainer/MarginContainer/VContainer/SessionContainer/CloseButton" to="." method="_on_close_button_pressed"]
|
||||
[connection signal="pressed" from="VBoxContainer/VSplitContainer/SocketsContainer/TrackersPanelContainer/TrackersPanel/LocalSignalingContainer/LocalSignalingButton" to="VBoxContainer/VSplitContainer/VSplitContainer/PanelContainer/VBoxContainer/TabContainer/LocalSignalingControl" method="show"]
|
||||
[connection signal="toggled" from="VBoxContainer/VSplitContainer/SocketsContainer/PeersPanelContainer/VBoxContainer/HBoxContainer/RefuseNewButton" to="." method="_on_refuse_new_button_toggled"]
|
||||
[connection signal="pressed" from="VBoxContainer/VSplitContainer/SocketsContainer/PeersPanelContainer/VBoxContainer/HBoxContainer/ChatButton" to="VBoxContainer/VSplitContainer/VSplitContainer/PanelContainer/VBoxContainer/TabContainer/ChatControl" method="show"]
|
||||
300
addons/tube/tube_local_signaling_peer.gd
Normal file
300
addons/tube/tube_local_signaling_peer.gd
Normal file
@ -0,0 +1,300 @@
|
||||
class_name TubeLocalSignalingPeer extends RefCounted
|
||||
|
||||
|
||||
const BROADCAST_ADDRESS := "255.255.255.255"
|
||||
const MIN_PORT := 49152
|
||||
const MAX_PORT := 65535
|
||||
const PORT_RANGE := MAX_PORT - MIN_PORT
|
||||
|
||||
|
||||
signal received_signaling_data(data: Dictionary, address: String)
|
||||
|
||||
|
||||
signal warning_raised(message: String)
|
||||
signal data_sent(data: Dictionary, address: String, port: int)
|
||||
signal received_data(data: Variant, address: String, port: int)
|
||||
|
||||
|
||||
var udp_peer := PacketPeerUDP.new()
|
||||
var port: int
|
||||
|
||||
|
||||
static func is_local_signaling_available() -> bool:
|
||||
return OS.get_name() != "Web"
|
||||
|
||||
|
||||
func raise_warning(message: String):
|
||||
push_warning(message)
|
||||
warning_raised.emit(message)
|
||||
|
||||
|
||||
func bind(
|
||||
p_app_id: String,
|
||||
p_session_id: String,
|
||||
p_peer_id: int
|
||||
) -> Error:
|
||||
|
||||
port = get_port(
|
||||
p_app_id,
|
||||
p_session_id,
|
||||
p_peer_id
|
||||
)
|
||||
|
||||
var error := udp_peer.bind(port)
|
||||
if error:
|
||||
raise_warning(
|
||||
"Cannot set bind to port {port}: {error}".format({
|
||||
"port": port,
|
||||
"error": error_string(error)
|
||||
}))
|
||||
udp_peer.close()
|
||||
|
||||
return error
|
||||
|
||||
|
||||
func is_bound() -> bool:
|
||||
return udp_peer.is_bound()
|
||||
|
||||
|
||||
func close():
|
||||
udp_peer.close()
|
||||
|
||||
|
||||
func get_port(
|
||||
p_app_id: String,
|
||||
p_session_id: String,
|
||||
p_peer_id: int
|
||||
) -> int:
|
||||
|
||||
return (p_app_id.hash() + p_session_id.hash() + p_peer_id)%PORT_RANGE + MIN_PORT
|
||||
|
||||
|
||||
## Encodes tracker packet data as JSON string.
|
||||
func encode_data(data: Dictionary) -> PackedByteArray:
|
||||
var json := JSON.stringify(data)
|
||||
return json.to_utf8_buffer()
|
||||
|
||||
|
||||
## Decodes tracker packet data from a [PackedByteArray].
|
||||
func decode_packet(p_packet: PackedByteArray) -> Variant:
|
||||
var string := p_packet.get_string_from_utf8()
|
||||
var data = JSON.parse_string(string)
|
||||
if null == data:
|
||||
return string
|
||||
|
||||
return data
|
||||
|
||||
|
||||
func broadcast_signaling_data(
|
||||
p_app_id: String,
|
||||
p_session_id: String,
|
||||
p_peer_id: int,
|
||||
p_to_peer_id: int,
|
||||
description: Dictionary,
|
||||
ice_candidates: Array
|
||||
) -> Error:
|
||||
var data := {
|
||||
"app_id": p_app_id,
|
||||
"session_id": p_session_id,
|
||||
"peer_id": p_peer_id,
|
||||
"type": description.type,
|
||||
"sdp": description.sdp,
|
||||
"ice_candidates": ice_candidates,
|
||||
}
|
||||
|
||||
var destination_port := get_port(
|
||||
p_app_id,
|
||||
p_session_id,
|
||||
1, # Server peer_id
|
||||
)
|
||||
|
||||
udp_peer.set_broadcast_enabled(true)
|
||||
var error := udp_peer.set_dest_address(
|
||||
BROADCAST_ADDRESS,
|
||||
destination_port
|
||||
)
|
||||
if error:
|
||||
raise_warning(
|
||||
"Cannot set destination address to {address}: {error}".format({
|
||||
"address": BROADCAST_ADDRESS,
|
||||
"error": error_string(error)
|
||||
}))
|
||||
return error
|
||||
|
||||
error = udp_peer.put_packet(encode_data(data))
|
||||
if error:
|
||||
raise_warning(
|
||||
"Cannot send packet to {address}: {error}".format({
|
||||
"address": BROADCAST_ADDRESS,
|
||||
"error": error_string(error)
|
||||
}))
|
||||
return error
|
||||
|
||||
data_sent.emit(
|
||||
data,
|
||||
BROADCAST_ADDRESS,
|
||||
destination_port
|
||||
)
|
||||
return error
|
||||
|
||||
|
||||
func send_signaling_data(
|
||||
p_address: String,
|
||||
p_app_id: String,
|
||||
p_session_id: String,
|
||||
p_peer_id: int,
|
||||
p_to_peer_id: int,
|
||||
description: Dictionary,
|
||||
ice_candidates: Array
|
||||
) -> Error:
|
||||
var data := {
|
||||
"app_id": p_app_id,
|
||||
"session_id": p_session_id,
|
||||
"peer_id": p_peer_id,
|
||||
"type": description.type,
|
||||
"sdp": description.sdp,
|
||||
"ice_candidates": ice_candidates,
|
||||
}
|
||||
|
||||
var destination_port := get_port(
|
||||
p_app_id,
|
||||
p_session_id,
|
||||
p_to_peer_id,
|
||||
)
|
||||
var error := udp_peer.set_dest_address(
|
||||
p_address,
|
||||
destination_port,
|
||||
)
|
||||
if error:
|
||||
raise_warning(
|
||||
"Cannot set destination address to {address}: {error}".format({
|
||||
"address": p_address,
|
||||
"error": error_string(error)
|
||||
}))
|
||||
return error
|
||||
|
||||
error = udp_peer.put_packet(encode_data(data))
|
||||
if error:
|
||||
raise_warning(
|
||||
"Cannot send packet to {address}: {error}".format({
|
||||
"address": p_address,
|
||||
"error": error_string(error)
|
||||
}))
|
||||
return error
|
||||
|
||||
data_sent.emit(
|
||||
data,
|
||||
p_address,
|
||||
destination_port
|
||||
)
|
||||
return error
|
||||
|
||||
|
||||
func _handle_signaling_data(p_data: Variant):
|
||||
if not p_data is Dictionary:
|
||||
raise_warning("signaling data invalid data type")
|
||||
return
|
||||
|
||||
if not p_data.has("app_id"):
|
||||
raise_warning("signaling data has no app_id")
|
||||
return
|
||||
|
||||
if not p_data.app_id is String:
|
||||
raise_warning("app_id invalid data type")
|
||||
return
|
||||
|
||||
if not p_data.has("session_id"):
|
||||
raise_warning("signaling data has no session_id")
|
||||
return
|
||||
|
||||
if not p_data.session_id is String:
|
||||
raise_warning("session_id invalid data type")
|
||||
return
|
||||
|
||||
if not p_data.has("peer_id"):
|
||||
raise_warning("signaling data has no peer_id")
|
||||
return
|
||||
|
||||
if not p_data.peer_id is float:
|
||||
raise_warning("peer_id invalid data type")
|
||||
return
|
||||
|
||||
if not p_data.has("sdp"):
|
||||
raise_warning("signaling data has no sdp")
|
||||
return
|
||||
|
||||
if not p_data.sdp is String:
|
||||
raise_warning("sdp invalid data type")
|
||||
return
|
||||
|
||||
if not p_data.has("type"):
|
||||
raise_warning("signaling data has no type")
|
||||
return
|
||||
|
||||
if not p_data.type is String:
|
||||
raise_warning("type invalid data type")
|
||||
return
|
||||
|
||||
if not p_data.has("ice_candidates"):
|
||||
raise_warning("signaling data has no ice_candidates")
|
||||
return
|
||||
|
||||
if not p_data.ice_candidates is Array:
|
||||
raise_warning("ice_candidates invalid data type")
|
||||
return
|
||||
|
||||
received_signaling_data.emit(
|
||||
p_data, udp_peer.get_packet_ip()
|
||||
)
|
||||
|
||||
|
||||
static func get_app_id_from_signaling_data(p_data: Dictionary) -> String:
|
||||
return p_data.app_id
|
||||
|
||||
|
||||
static func get_session_id_from_signaling_data(p_data: Dictionary) -> String:
|
||||
return p_data.session_id
|
||||
|
||||
|
||||
static func get_peer_id_from_signaling_data(p_data: Dictionary) -> int:
|
||||
return int(p_data.peer_id)
|
||||
|
||||
|
||||
static func get_type_from_signaling_data(p_data: Dictionary) -> String:
|
||||
return p_data.type
|
||||
|
||||
|
||||
static func get_sdp_from_signaling_data(p_data: Dictionary) -> String:
|
||||
return p_data.sdp
|
||||
|
||||
|
||||
static func get_ice_candidates_from_signaling_data(p_data: Dictionary) -> Array:
|
||||
return p_data.ice_candidates
|
||||
|
||||
|
||||
static func is_ice_candidate_data_valid(p_data: Variant) -> bool:
|
||||
return TubeTracker.is_ice_candidate_data_valid(p_data)
|
||||
|
||||
|
||||
static func get_media_from_ice_candidate_data(p_data: Dictionary) -> String:
|
||||
return TubeTracker.get_media_from_ice_candidate_data(p_data)
|
||||
|
||||
|
||||
static func get_index_from_ice_candidate_data(p_data: Dictionary) -> int:
|
||||
return TubeTracker.get_index_from_ice_candidate_data(p_data)
|
||||
|
||||
|
||||
static func get_sdp_from_ice_candidate_data(p_data: Dictionary) -> String:
|
||||
return TubeTracker.get_sdp_from_ice_candidate_data(p_data)
|
||||
|
||||
|
||||
|
||||
func _process(delta):
|
||||
while 0 < udp_peer.get_available_packet_count():
|
||||
var data = decode_packet(udp_peer.get_packet())
|
||||
received_data.emit(
|
||||
data,
|
||||
udp_peer.get_packet_ip(),
|
||||
udp_peer.get_packet_port()
|
||||
)
|
||||
_handle_signaling_data(data)
|
||||
1
addons/tube/tube_local_signaling_peer.gd.uid
Normal file
1
addons/tube/tube_local_signaling_peer.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dbkvketmx6gxb
|
||||
168
addons/tube/tube_network_diagnosis_peer.gd
Normal file
168
addons/tube/tube_network_diagnosis_peer.gd
Normal file
@ -0,0 +1,168 @@
|
||||
class_name TubeNetworkDiagnosisPeer extends RefCounted
|
||||
|
||||
|
||||
signal warning_raised(message: String)
|
||||
signal nat_hole_punching_compliance_updated(compliance: Compliance)
|
||||
|
||||
|
||||
const WAITING_TIME := 10.0 #sec
|
||||
|
||||
const STUN_BINDING_REQUEST := 0x0001
|
||||
const STUN_MAGIC_COOKIE := 0x2112A442
|
||||
|
||||
# STUN attribute types
|
||||
const ATTR_XOR_MAPPED_ADDRESS := 0x0020
|
||||
const ATTR_MAPPED_ADDRESS := 0x0001
|
||||
#const ATTR_USERNAME := 0x0006
|
||||
#const ATTR_MESSAGE_INTEGRITY := 0x0008
|
||||
#const ATTR_ERROR_CODE := 0x0009
|
||||
#const ATTR_UNKNOWN_ATTRIBUTES := 0x000A
|
||||
#const ATTR_PRIORITY := 0x0024
|
||||
#const ATTR_USE_CANDIDATE := 0x0025
|
||||
#const ATTR_ICE_CONTROLLED := 0x8029
|
||||
#const ATTR_ICE_CONTROLLING := 0x802A
|
||||
|
||||
|
||||
enum Compliance {UNKNOWN, YES, NO}
|
||||
|
||||
|
||||
var peer := PacketPeerUDP.new()
|
||||
var _binding_port: int
|
||||
var _binding_time: float = 0.0
|
||||
|
||||
|
||||
var public_ports: Dictionary[String, int] = {} # destionnation address:port, public port
|
||||
|
||||
|
||||
func _u16_to_bytes(v) -> PackedByteArray:
|
||||
return PackedByteArray([v>>8 & 0xFF, v & 0xFF])
|
||||
|
||||
|
||||
func _u32_to_bytes(v) -> PackedByteArray:
|
||||
return PackedByteArray([v>>24 & 0xFF, v>>16 & 0xFF, v>>8 & 0xFF, v & 0xFF])
|
||||
|
||||
|
||||
func _bytes_to_u16(bytes: PackedByteArray) -> int:
|
||||
return (bytes[0] << 8) | bytes[1]
|
||||
|
||||
|
||||
func _init(p_port: int) -> void:
|
||||
_binding_port = p_port
|
||||
|
||||
|
||||
func raise_warning(message: String):
|
||||
push_warning(message)
|
||||
warning_raised.emit(message)
|
||||
|
||||
|
||||
# HOLE PUNCHING
|
||||
|
||||
func start_nat_hole_punching_detection(p_urls: Array[String]):
|
||||
public_ports.clear()
|
||||
|
||||
if not peer.is_bound():
|
||||
var error := peer.bind(_binding_port)
|
||||
if error:
|
||||
raise_warning("cannot bind to {port}: {error}".format({
|
||||
"port": _binding_port,
|
||||
"error": error_string(error)
|
||||
}))
|
||||
return
|
||||
|
||||
_binding_time = 0.0
|
||||
|
||||
for url in p_urls:
|
||||
var splited := url.split(":")
|
||||
var address := splited[1]
|
||||
var port := int(splited[2])
|
||||
_send_stunbinding_request(address, port)
|
||||
|
||||
|
||||
func is_nat_hole_punching_compliant() -> Compliance:
|
||||
var ports := {}
|
||||
for i_port in public_ports.values():
|
||||
ports[i_port] = 0
|
||||
|
||||
if 1 < len(ports): # multiple ports --> mapping depends on destination
|
||||
return Compliance.NO
|
||||
|
||||
if 1 == len(ports) and 1 < len(public_ports): # one port and multiple destinations --> NAT preserves mapping across destinations
|
||||
return Compliance.YES
|
||||
|
||||
return Compliance.UNKNOWN
|
||||
|
||||
|
||||
func _send_stunbinding_request(p_address: String, p_port: int) -> void:
|
||||
var error := peer.set_dest_address(p_address, p_port)
|
||||
if error:
|
||||
raise_warning("cannot set destionation address {address}:{port}: {error}".format({
|
||||
"address": p_address,
|
||||
"port": p_port,
|
||||
"error": error_string(error)
|
||||
}))
|
||||
return
|
||||
|
||||
# Build STUN Binding Request packet (20 bytes header)
|
||||
var packet = PackedByteArray()
|
||||
packet.resize(0)
|
||||
packet.append_array(_u16_to_bytes(STUN_BINDING_REQUEST)) # Type
|
||||
packet.append_array(_u16_to_bytes(0)) # Message length 0 (no attributes)
|
||||
packet.append_array(_u32_to_bytes(STUN_MAGIC_COOKIE)) # Magic Cookie
|
||||
for i in 12: # Transaction ID (random)
|
||||
packet.append(randi() & 0xFF)
|
||||
|
||||
error = peer.put_packet(packet)
|
||||
if error:
|
||||
raise_warning("cannot put packet to {address}:{port}: {error}".format({
|
||||
"address": p_address,
|
||||
"port": p_port,
|
||||
"error": error_string(error)
|
||||
}))
|
||||
return
|
||||
|
||||
|
||||
func parse_stun_response(data: PackedByteArray):
|
||||
if data.size() < 20:
|
||||
raise_warning("STUN response too short")
|
||||
return
|
||||
|
||||
var i := 20 #Skip header
|
||||
while i + 4 <= data.size():
|
||||
var attr_type := (data[i] << 8) | data[i+1]
|
||||
var attr_len := (data[i+2] << 8) | data[i+3]
|
||||
var value := data.slice(i+4, i+4+attr_len)
|
||||
i += 4 + attr_len
|
||||
# Attributes are 32-bit aligned: pad to 4 bytes
|
||||
if attr_len % 4 != 0:
|
||||
i += 4 - (attr_len % 4)
|
||||
|
||||
# Decode port in attributes
|
||||
var port: int
|
||||
if attr_type == ATTR_XOR_MAPPED_ADDRESS and attr_len >= 8:
|
||||
port = ((value[2] << 8) | value[3]) ^ (STUN_MAGIC_COOKIE >> 16)
|
||||
|
||||
elif attr_type == ATTR_MAPPED_ADDRESS and attr_len >= 8:
|
||||
port = (value[2] << 8) | value[3]
|
||||
|
||||
else:
|
||||
continue
|
||||
|
||||
var key := "{address}:{port}".format({
|
||||
"address": peer.get_packet_ip(),
|
||||
"port": peer.get_packet_port(),
|
||||
})
|
||||
public_ports[key] = port
|
||||
return
|
||||
|
||||
|
||||
# PROCESS
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_binding_time += delta
|
||||
while 0 < peer.get_available_packet_count():
|
||||
var response = peer.get_packet()
|
||||
parse_stun_response(response)
|
||||
nat_hole_punching_compliance_updated.emit(is_nat_hole_punching_compliant())
|
||||
|
||||
if WAITING_TIME < _binding_time:
|
||||
peer.close()
|
||||
1
addons/tube/tube_network_diagnosis_peer.gd.uid
Normal file
1
addons/tube/tube_network_diagnosis_peer.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://by2koobiifvrh
|
||||
459
addons/tube/tube_peer.gd
Normal file
459
addons/tube/tube_peer.gd
Normal file
@ -0,0 +1,459 @@
|
||||
class_name TubePeer extends WebRTCPeerConnectionExtension
|
||||
|
||||
|
||||
signal signaling_readied
|
||||
signal signaling_timeout
|
||||
signal connected
|
||||
signal disconnected
|
||||
signal failed
|
||||
signal closed
|
||||
|
||||
signal warning_raised(message: String)
|
||||
|
||||
signal connection_state_changed
|
||||
#signal session_description_created # local
|
||||
#signal ice_candidate_created # local
|
||||
signal remote_description_setted
|
||||
signal ice_candidate_added(ice_candidate: Dictionary) # remote
|
||||
signal port_mapped(public_port: int, local_port: int)
|
||||
|
||||
signal channel_initiated(channel: WebRTCDataChannel)
|
||||
signal channel_state_changed(channel: WebRTCDataChannel)
|
||||
|
||||
|
||||
|
||||
class WebRTCSdp extends RefCounted:
|
||||
|
||||
var foundation: String
|
||||
var component: String
|
||||
var protocol: String
|
||||
var priority: int
|
||||
var ip: String
|
||||
var port: int
|
||||
var type: String
|
||||
var related_address: String
|
||||
var related_port: int
|
||||
var tcp_type: String
|
||||
|
||||
|
||||
func _init(line: String) -> void:
|
||||
var parts: Array
|
||||
if line.begins_with("a=candidate:"):
|
||||
parts = line.substr(12, line.length()).split(" ")
|
||||
else:
|
||||
parts = line.substr(10, line.length()).split(" ")
|
||||
|
||||
var related_address: String = ""
|
||||
var related_port: int = -1
|
||||
var tcp_type: String = ""
|
||||
|
||||
var i := 8
|
||||
while i < parts.size():
|
||||
match parts[i]:
|
||||
"raddr":
|
||||
related_address = parts[i + 1]
|
||||
"rport":
|
||||
related_port = int(parts[i + 1])
|
||||
"tcptype":
|
||||
tcp_type = parts[i + 1]
|
||||
_:
|
||||
# Unknown extensions are ignored
|
||||
pass
|
||||
i += 2
|
||||
|
||||
foundation = parts[0]
|
||||
component = parts[1]
|
||||
protocol = parts[2].to_lower()
|
||||
priority = int(parts[3])
|
||||
ip = parts[4]
|
||||
port = int(parts[5])
|
||||
type = parts[7]
|
||||
related_address = related_address
|
||||
related_port = related_port
|
||||
tcp_type = tcp_type
|
||||
|
||||
|
||||
var id: int
|
||||
|
||||
var valid := true
|
||||
var error_message: String
|
||||
|
||||
var connection := WebRTCPeerConnection.new()
|
||||
var connection_state := connection.get_connection_state()
|
||||
var gathering_state := connection.get_gathering_state()
|
||||
var signaling_state := connection.get_signaling_state()
|
||||
|
||||
var signaling_time: float = -1.0
|
||||
var signaling_timeout_time: float = 1.0
|
||||
|
||||
var signaling_attempts: int = 0
|
||||
var signaling_max_attempts: int = 3
|
||||
|
||||
var connecting_time: float = 0.0
|
||||
var up_time: float = 0.0
|
||||
|
||||
var local_address: String
|
||||
var local_session_description := {}
|
||||
var remote_session_description := {}
|
||||
var ice_candidates: Array[Dictionary] = []
|
||||
var has_joined_session := false # set by client
|
||||
|
||||
var pending_public_ports: Array[int] = []
|
||||
var local_ports: Array[int] = []
|
||||
var mapped_ports: Dictionary[int, int] = {} # public to local
|
||||
|
||||
var channel_states: Dictionary[WebRTCDataChannel, WebRTCDataChannel.ChannelState] = {}
|
||||
|
||||
|
||||
func _init(p_peer_id: int) -> void:
|
||||
id = p_peer_id
|
||||
#client = p_client
|
||||
connection_state = connection.get_connection_state()
|
||||
gathering_state = connection.get_gathering_state()
|
||||
signaling_state = connection.get_signaling_state()
|
||||
|
||||
connection.session_description_created.connect(
|
||||
set_local_description
|
||||
)
|
||||
connection.ice_candidate_created.connect(
|
||||
_on_ice_candidate_created
|
||||
)
|
||||
|
||||
|
||||
func raise_warning(message: String):
|
||||
push_warning(message)
|
||||
warning_raised.emit(message)
|
||||
|
||||
|
||||
func _initialize(p_config: Dictionary) -> Error:
|
||||
var error := connection.initialize(p_config)
|
||||
if error:
|
||||
valid = false
|
||||
error_message = "cannot initialize peer: {error}".format({
|
||||
"error": error_string(error)
|
||||
})
|
||||
failed.emit()
|
||||
|
||||
return error
|
||||
|
||||
|
||||
func _get_connection_state() -> WebRTCPeerConnection.ConnectionState:
|
||||
var state := connection.get_connection_state()
|
||||
if state == WebRTCPeerConnection.STATE_DISCONNECTED:
|
||||
# WebRTC Connection can be temporary disconnected and will automaticaly reconnect quickly. But for godot, disconnected is putting a end to the connection. We don't want that so we tell godot that it is still connected. If does not reconnect, state will be FAILED and handle as real disconnection by Tube and Godot.
|
||||
# It looks like when using reliable channel, while DISCONNECTED, message will be received on reconnection.
|
||||
return WebRTCPeerConnection.STATE_CONNECTED
|
||||
|
||||
return state
|
||||
|
||||
|
||||
func _get_gathering_state() -> WebRTCPeerConnection.GatheringState:
|
||||
return connection.get_gathering_state()
|
||||
|
||||
|
||||
func _get_signaling_state() -> WebRTCPeerConnection.SignalingState:
|
||||
return connection.get_signaling_state()
|
||||
|
||||
|
||||
func _create_data_channel(p_label: String, p_config: Dictionary) -> WebRTCDataChannel:
|
||||
var channel := connection.create_data_channel(p_label, p_config)
|
||||
channel_states[channel] = channel.get_ready_state()
|
||||
channel_initiated.emit(channel)
|
||||
return channel
|
||||
|
||||
|
||||
func _create_offer() -> Error:
|
||||
var error = connection.create_offer()
|
||||
if error:
|
||||
valid = false
|
||||
error_message = "cannot create offer: {error}".format({
|
||||
"error": error_string(error)
|
||||
})
|
||||
failed.emit()
|
||||
|
||||
return error
|
||||
|
||||
|
||||
func _set_local_description(p_type: String, p_sdp: String):
|
||||
var error := connection.set_local_description(p_type, p_sdp)
|
||||
if error:
|
||||
error_message = "cannot set local description: {error}".format({
|
||||
"error": error_string(error)
|
||||
})
|
||||
failed.emit()
|
||||
return error
|
||||
|
||||
local_session_description = {
|
||||
"type": p_type,
|
||||
"sdp": p_sdp,
|
||||
}
|
||||
session_description_created.emit()
|
||||
|
||||
if is_signaling_ready() and not is_attempting_connection():
|
||||
match_remaining_ports()
|
||||
signaling_readied.emit()
|
||||
|
||||
return error
|
||||
|
||||
|
||||
func _on_ice_candidate_created(p_media: String, p_index: int, p_sdp: String):
|
||||
ice_candidates.append({
|
||||
"media": p_media,
|
||||
"index": p_index,
|
||||
"sdp": p_sdp,
|
||||
})
|
||||
|
||||
var sdp_parsed := WebRTCSdp.new(p_sdp)
|
||||
|
||||
if "udp" == sdp_parsed.protocol:
|
||||
if "host" == sdp_parsed.type:
|
||||
if not local_ports.has(sdp_parsed.port):
|
||||
local_ports.append(sdp_parsed.port)
|
||||
|
||||
elif "srflx" == sdp_parsed.type:
|
||||
if not mapped_ports.has(sdp_parsed.port):
|
||||
if not pending_public_ports.has(sdp_parsed.port):
|
||||
pending_public_ports.append(sdp_parsed.port)
|
||||
|
||||
if sdp_parsed.related_port != 0:
|
||||
mapped_ports[sdp_parsed.port] = sdp_parsed.related_port
|
||||
port_mapped.emit(sdp_parsed.port, sdp_parsed.related_port)
|
||||
pending_public_ports.erase(sdp_parsed.port)
|
||||
|
||||
match_ports()
|
||||
|
||||
|
||||
ice_candidate_created.emit()
|
||||
|
||||
if is_signaling_ready() and not is_attempting_connection():
|
||||
match_remaining_ports()
|
||||
signaling_readied.emit()
|
||||
|
||||
|
||||
func _set_remote_description(p_type: String, p_sdp: String) -> Error:
|
||||
var error := connection.set_remote_description(p_type, p_sdp)
|
||||
if error:
|
||||
raise_warning(
|
||||
"Cannot set remote description: {error}".format({
|
||||
"error": error_string(error)
|
||||
}))
|
||||
return error
|
||||
|
||||
remote_session_description = {
|
||||
"type": p_type,
|
||||
"sdp": p_sdp,
|
||||
}
|
||||
remote_description_setted.emit()
|
||||
return error
|
||||
|
||||
|
||||
func _add_ice_candidate(media: String, index: int, name: String) -> Error:
|
||||
var error = connection.add_ice_candidate(
|
||||
media,
|
||||
index,
|
||||
name,
|
||||
)
|
||||
|
||||
if error:
|
||||
raise_warning(
|
||||
"Cannot add ice candidate: {error}".format({
|
||||
"error": error_string(error)
|
||||
}))
|
||||
return error
|
||||
|
||||
ice_candidate_added.emit({
|
||||
"media": media,
|
||||
"index": index,
|
||||
"name": name,
|
||||
})
|
||||
|
||||
return error
|
||||
|
||||
|
||||
func _poll() -> Error:
|
||||
return connection.poll()
|
||||
|
||||
|
||||
func _close() -> void:
|
||||
valid = false
|
||||
if WebRTCPeerConnection.STATE_CLOSED != connection.get_connection_state():
|
||||
connection.close()
|
||||
|
||||
|
||||
func is_signaling_ready() -> bool:
|
||||
if WebRTCPeerConnection.STATE_CONNECTING != connection.get_connection_state(): # already connected, do nothing
|
||||
return false
|
||||
|
||||
#if is_attempting_connection(): # already signaling
|
||||
#return false
|
||||
|
||||
if local_session_description.is_empty():
|
||||
return false
|
||||
|
||||
if ice_candidates.is_empty():
|
||||
return false
|
||||
|
||||
return WebRTCPeerConnection.GATHERING_STATE_COMPLETE == connection.get_gathering_state()
|
||||
|
||||
|
||||
func start_connection_attempt():
|
||||
if is_attempting_connection(): # already started
|
||||
return
|
||||
|
||||
signaling_time = 0.0
|
||||
signaling_attempts += 1
|
||||
|
||||
|
||||
func is_attempting_connection():
|
||||
return 0.0 <= signaling_time
|
||||
|
||||
|
||||
func _signaling_timeout():
|
||||
if signaling_max_attempts <= signaling_attempts:
|
||||
stop_connection_attempts()
|
||||
error_message = "max connection attempts reached"
|
||||
failed.emit()
|
||||
return
|
||||
|
||||
signaling_time = -1.0
|
||||
signaling_timeout.emit()
|
||||
|
||||
|
||||
func stop_connection_attempts():
|
||||
signaling_time = -1.0
|
||||
|
||||
|
||||
func is_peer_connected() -> bool: # is_connected is use for signals
|
||||
return WebRTCPeerConnection.STATE_CONNECTED == connection_state
|
||||
|
||||
|
||||
func update_connection_state() -> bool: #changed
|
||||
var previous := connection_state
|
||||
connection_state = connection.get_connection_state()
|
||||
return previous != connection_state
|
||||
|
||||
|
||||
func update_gathering_state() -> bool: #changed
|
||||
var previous := gathering_state
|
||||
gathering_state = connection.get_gathering_state()
|
||||
return previous != gathering_state
|
||||
|
||||
|
||||
func update_signaling_state() -> bool: #changed
|
||||
var previous := signaling_state
|
||||
signaling_state = connection.get_signaling_state()
|
||||
return previous != signaling_state
|
||||
|
||||
|
||||
func match_ports():
|
||||
for port in pending_public_ports:
|
||||
if local_ports.has(port):
|
||||
mapped_ports[port] = port
|
||||
port_mapped.emit(port, port)
|
||||
|
||||
for port in mapped_ports:
|
||||
if pending_public_ports.has(port):
|
||||
pending_public_ports.erase(port)
|
||||
|
||||
|
||||
func match_remaining_ports():
|
||||
match_ports()
|
||||
if local_ports.is_empty():
|
||||
return
|
||||
|
||||
var port := local_ports[0]
|
||||
for i_port in pending_public_ports:
|
||||
mapped_ports[i_port] = port
|
||||
port_mapped.emit(i_port, port)
|
||||
|
||||
for i_port in mapped_ports:
|
||||
if pending_public_ports.has(port):
|
||||
pending_public_ports.erase(port)
|
||||
|
||||
|
||||
func _connected():
|
||||
stop_connection_attempts()
|
||||
connected.emit()
|
||||
|
||||
|
||||
func _disconnected():
|
||||
stop_connection_attempts()
|
||||
disconnected.emit()
|
||||
|
||||
|
||||
func _connection_failed():
|
||||
stop_connection_attempts()
|
||||
error_message = "connection failed"
|
||||
failed.emit()
|
||||
|
||||
|
||||
func _connection_closed():
|
||||
stop_connection_attempts()
|
||||
closed.emit()
|
||||
|
||||
|
||||
func _process(delta: float):
|
||||
|
||||
# State
|
||||
var connection_changed := update_connection_state()
|
||||
var gathering_changed := update_gathering_state()
|
||||
var signaling_changed := update_signaling_state()
|
||||
if connection_changed or gathering_changed or signaling_changed:
|
||||
connection_state_changed.emit()
|
||||
|
||||
# Channel
|
||||
for channel in channel_states:
|
||||
_process_channel(channel)
|
||||
|
||||
|
||||
# Connections
|
||||
if connection_changed:
|
||||
|
||||
if WebRTCPeerConnection.STATE_NEW == connection_state:
|
||||
pass
|
||||
|
||||
if WebRTCPeerConnection.STATE_CONNECTING == connection_state:
|
||||
pass
|
||||
|
||||
if WebRTCPeerConnection.STATE_CONNECTED == connection_state:
|
||||
_connected()
|
||||
|
||||
if WebRTCPeerConnection.STATE_DISCONNECTED == connection_state:
|
||||
_disconnected()
|
||||
|
||||
if WebRTCPeerConnection.STATE_FAILED == connection_state:
|
||||
_connection_failed()
|
||||
|
||||
if WebRTCPeerConnection.STATE_CLOSED == connection_state:
|
||||
_connection_closed()
|
||||
|
||||
|
||||
if gathering_changed or signaling_changed:
|
||||
if is_signaling_ready() and not is_attempting_connection():
|
||||
match_remaining_ports()
|
||||
signaling_readied.emit()
|
||||
|
||||
|
||||
# Times
|
||||
if is_attempting_connection():
|
||||
signaling_time += delta
|
||||
if signaling_timeout_time < signaling_time:
|
||||
_signaling_timeout()
|
||||
|
||||
if WebRTCPeerConnection.STATE_CONNECTING == connection_state:
|
||||
connecting_time += delta
|
||||
|
||||
if WebRTCPeerConnection.STATE_CONNECTED == connection_state:
|
||||
up_time += delta
|
||||
|
||||
|
||||
func _process_channel(p_channel: WebRTCDataChannel) -> WebRTCDataChannel.ChannelState:
|
||||
var current_state:= p_channel.get_ready_state()
|
||||
var old_state := channel_states[p_channel]
|
||||
channel_states[p_channel] = current_state
|
||||
if old_state != current_state:
|
||||
channel_state_changed.emit(
|
||||
p_channel
|
||||
)
|
||||
|
||||
return current_state
|
||||
1
addons/tube/tube_peer.gd.uid
Normal file
1
addons/tube/tube_peer.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://pqifv3ichp2f
|
||||
361
addons/tube/tube_tracker.gd
Normal file
361
addons/tube/tube_tracker.gd
Normal file
@ -0,0 +1,361 @@
|
||||
class_name TubeTracker extends RefCounted
|
||||
|
||||
|
||||
const MAX_INTERVAL := 120.0 #sec
|
||||
|
||||
|
||||
signal failed
|
||||
signal connected
|
||||
signal disconnected
|
||||
signal received_answer(data: Dictionary)
|
||||
signal interval_timeout
|
||||
|
||||
|
||||
signal warning_raised(message: String)
|
||||
signal state_changed
|
||||
|
||||
signal data_sent(data: Dictionary)
|
||||
signal received_data(data: Dictionary)
|
||||
|
||||
|
||||
const CLOSE_CODE_CLIENT: int = 3001
|
||||
const CLOSE_CODE_FAILED: int = 3002
|
||||
# https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close, custom code 3000-4999
|
||||
# https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1
|
||||
|
||||
|
||||
var error_message: String
|
||||
var socket := WebSocketPeer.new()
|
||||
var state := socket.get_ready_state()
|
||||
|
||||
var connecting_time: float = 0.0 #sec
|
||||
var up_time: float = 0.0 #sec
|
||||
var interval_time: float = 0.0 #sec
|
||||
var interval_time_left: float = -1.0
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
var string := socket.get_requested_url()
|
||||
|
||||
if "Web" != OS.get_name():
|
||||
if WebSocketPeer.STATE_OPEN == socket.get_ready_state():
|
||||
string += "({protocol}://{address}:{port})".format({
|
||||
"protocol": socket.get_selected_protocol(),
|
||||
"address": socket.get_connected_host(),
|
||||
"port": socket.get_connected_port(),
|
||||
})
|
||||
|
||||
return string
|
||||
|
||||
|
||||
func raise_warning(message: String):
|
||||
push_warning(message)
|
||||
warning_raised.emit(message)
|
||||
|
||||
|
||||
func connect_to_url(p_url: String) -> Error:
|
||||
var error := socket.connect_to_url(p_url)
|
||||
if error:
|
||||
error_message = "connection failed: {error}".format({
|
||||
"error": error_string(error)
|
||||
})
|
||||
failed.emit()
|
||||
|
||||
return error
|
||||
|
||||
|
||||
func is_open() -> bool:
|
||||
return WebSocketPeer.STATE_OPEN == socket.get_ready_state()
|
||||
|
||||
|
||||
func is_close() -> bool:
|
||||
return WebSocketPeer.STATE_CLOSED == socket.get_ready_state()
|
||||
|
||||
|
||||
func close(p_info_hash: String, p_peer_id_hash: String):
|
||||
if is_open():
|
||||
send_stop(
|
||||
p_info_hash,
|
||||
p_peer_id_hash
|
||||
)
|
||||
|
||||
if not is_close():
|
||||
socket.close(
|
||||
CLOSE_CODE_CLIENT,
|
||||
"Close by client",
|
||||
)
|
||||
|
||||
|
||||
func _socket_connection_opened():
|
||||
connected.emit()
|
||||
|
||||
|
||||
func _socket_connection_closed(p_code: int, p_reason: String):
|
||||
#if -1 == p_code: # error
|
||||
|
||||
if WebSocketPeer.State.STATE_CONNECTING == state:
|
||||
error_message = "connection impossible"
|
||||
|
||||
p_reason = p_reason if p_reason else "Closed unexpectedly, code: {code}".format({
|
||||
"code": p_code,
|
||||
})
|
||||
|
||||
if WebSocketPeer.State.STATE_OPEN == state:
|
||||
error_message = "connection failed: {reason}".format({
|
||||
"reason": p_reason,
|
||||
})
|
||||
|
||||
disconnected.emit()
|
||||
|
||||
|
||||
## Encodes tracker packet data as JSON string.
|
||||
func encode_data(data: Dictionary) -> String:
|
||||
var json := JSON.stringify(data)
|
||||
return json
|
||||
|
||||
|
||||
## Decodes tracker packet data from a [PackedByteArray].
|
||||
func decode_packet(p_packet: PackedByteArray) -> Variant:
|
||||
var string := p_packet.get_string_from_utf8()
|
||||
var data = JSON.parse_string(string)
|
||||
return data
|
||||
|
||||
|
||||
func send_data(p_data: Dictionary) -> Error:
|
||||
var text := encode_data(p_data)
|
||||
var error := socket.send_text(
|
||||
text
|
||||
)
|
||||
|
||||
if error:
|
||||
raise_warning(
|
||||
"Cannot send text: {error}".format({
|
||||
"error": error_string(error)
|
||||
}))
|
||||
|
||||
else:
|
||||
data_sent.emit(p_data)
|
||||
|
||||
return error
|
||||
|
||||
|
||||
func send_announce(p_info_hash: String, p_peer_id_hash: String) -> Error:
|
||||
return send_data({
|
||||
"action": "announce",
|
||||
"info_hash": p_info_hash,
|
||||
"peer_id": p_peer_id_hash,
|
||||
|
||||
"uploaded": 0,
|
||||
"downloaded": 0,
|
||||
})
|
||||
|
||||
|
||||
func send_answer(
|
||||
p_info_hash: String,
|
||||
p_peer_id_hash: String,
|
||||
p_to_peer_id_hash: String,
|
||||
description: Dictionary,
|
||||
ice_candidates: Array
|
||||
) -> Error:
|
||||
return send_data({
|
||||
"action": "announce",
|
||||
"info_hash": p_info_hash,
|
||||
"peer_id": p_peer_id_hash,
|
||||
|
||||
"to_peer_id": p_to_peer_id_hash,
|
||||
"answer": {
|
||||
"type": description.type,
|
||||
"sdp": description.sdp,
|
||||
"ice_candidates": ice_candidates,
|
||||
},
|
||||
"offer_id": "0",
|
||||
})
|
||||
|
||||
|
||||
func send_stop(p_info_hash: String, p_peer_id_hash: String) -> Error:
|
||||
return send_data({
|
||||
"action": "announce",
|
||||
"info_hash": p_info_hash,
|
||||
"peer_id": p_peer_id_hash,
|
||||
"event": "stopped"
|
||||
})
|
||||
|
||||
|
||||
func _received_packet(p_packet: PackedByteArray):
|
||||
var data = decode_packet(p_packet)
|
||||
if not data is Dictionary:
|
||||
raise_warning("Received invalid packet: {packet}".format({
|
||||
"packet": str(p_packet)
|
||||
}))
|
||||
return
|
||||
|
||||
received_data.emit(data)
|
||||
if data.has("answer"):
|
||||
_handle_answer(data)
|
||||
return
|
||||
|
||||
_handle_announce(data)
|
||||
|
||||
|
||||
func _handle_announce(p_data: Dictionary):
|
||||
if not p_data.has("interval"):
|
||||
raise_warning("announce data has no interval")
|
||||
return
|
||||
|
||||
if not p_data.interval is float:
|
||||
raise_warning("interval invalid data type")
|
||||
return
|
||||
|
||||
interval_time = min(p_data.interval, MAX_INTERVAL)
|
||||
interval_time_left = interval_time
|
||||
|
||||
|
||||
func _handle_answer(p_data: Dictionary):
|
||||
if not p_data is Dictionary:
|
||||
raise_warning("answer data invalid data type")
|
||||
return
|
||||
|
||||
if not p_data.has("peer_id"):
|
||||
raise_warning("answer data has no peer_id")
|
||||
return
|
||||
|
||||
if not p_data.peer_id is String:
|
||||
raise_warning("peer_id invalid data type")
|
||||
return
|
||||
|
||||
if not p_data.has("answer"):
|
||||
raise_warning("answer data has no answer")
|
||||
return
|
||||
|
||||
if not p_data.answer is Dictionary:
|
||||
raise_warning("answer invalid data type")
|
||||
return
|
||||
|
||||
var answer: Dictionary = p_data.answer
|
||||
if not answer.has("sdp"):
|
||||
raise_warning("answer data has no sdp")
|
||||
|
||||
if not answer.sdp is String:
|
||||
raise_warning("sdp invalid data type")
|
||||
return
|
||||
|
||||
if not answer.has("type"):
|
||||
raise_warning("answer data has no type")
|
||||
return
|
||||
|
||||
if not answer.type is String:
|
||||
raise_warning("type invalid data type")
|
||||
return
|
||||
|
||||
if not answer.has("ice_candidates"):
|
||||
raise_warning("answer data has no ice_candidates")
|
||||
return
|
||||
|
||||
if not answer.ice_candidates is Array:
|
||||
raise_warning("ice_candidates invalid data type")
|
||||
return
|
||||
|
||||
received_answer.emit(p_data)
|
||||
|
||||
|
||||
static func get_peer_id_hash_from_answer_data(p_data: Dictionary) -> String:
|
||||
return p_data.peer_id
|
||||
|
||||
|
||||
static func get_type_from_answer_data(p_data: Dictionary) -> String:
|
||||
return p_data.answer.type
|
||||
|
||||
|
||||
static func get_sdp_from_answer_data(p_data: Dictionary) -> String:
|
||||
return p_data.answer.sdp
|
||||
|
||||
|
||||
static func get_ice_candidates_from_answer_data(p_data: Dictionary) -> Array:
|
||||
return p_data.answer.ice_candidates
|
||||
|
||||
|
||||
static func is_ice_candidate_data_valid(p_data: Variant) -> bool:
|
||||
if not p_data is Dictionary:
|
||||
push_error("Ice candidate data invalid data type")
|
||||
return false
|
||||
|
||||
if not p_data.has("media"):
|
||||
push_error("Ice candidate data has no media")
|
||||
return false
|
||||
|
||||
if not p_data.media is String:
|
||||
push_error("media invalid data type")
|
||||
return false
|
||||
|
||||
if not p_data.has("index"):
|
||||
push_error("Ice candidate has no index")
|
||||
return false
|
||||
|
||||
if not (typeof(p_data.index) in [TYPE_INT, TYPE_FLOAT]):
|
||||
push_error("index invalid data type")
|
||||
return false
|
||||
|
||||
if not p_data.has("sdp"):
|
||||
push_error("Ice candidate has no sdp")
|
||||
return false
|
||||
|
||||
if not p_data.sdp is String:
|
||||
push_error("Ice candidate sdp invalid data type")
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
static func get_media_from_ice_candidate_data(p_data: Dictionary) -> String:
|
||||
return p_data.media
|
||||
|
||||
|
||||
static func get_index_from_ice_candidate_data(p_data: Dictionary) -> int:
|
||||
return int(p_data.index)
|
||||
|
||||
|
||||
static func get_sdp_from_ice_candidate_data(p_data: Dictionary) -> String:
|
||||
return p_data.sdp
|
||||
|
||||
|
||||
func _process(delta: float):
|
||||
socket.poll() # push error when 502 bad gateway, doesn't block anything
|
||||
|
||||
var old_state := state
|
||||
state = socket.get_ready_state()
|
||||
if state != old_state:
|
||||
state_changed.emit()
|
||||
|
||||
|
||||
if WebSocketPeer.STATE_CONNECTING == state:
|
||||
connecting_time += delta
|
||||
|
||||
if WebSocketPeer.STATE_OPEN == state:
|
||||
if WebSocketPeer.STATE_OPEN != old_state:
|
||||
_socket_connection_opened()
|
||||
|
||||
while socket.get_available_packet_count():
|
||||
var packet := socket.get_packet()
|
||||
_received_packet(packet)
|
||||
|
||||
up_time += delta
|
||||
|
||||
if 0.0 < interval_time:
|
||||
interval_time_left -= delta
|
||||
if interval_time_left < 0.0:
|
||||
interval_time_left = interval_time
|
||||
interval_timeout.emit()
|
||||
|
||||
|
||||
elif WebSocketPeer.STATE_CLOSING == state:
|
||||
# Keep polling to achieve proper close.
|
||||
pass
|
||||
|
||||
elif WebSocketPeer.STATE_CLOSED == state:
|
||||
var code = socket.get_close_code()
|
||||
var reason = socket.get_close_reason()
|
||||
_socket_connection_closed(code, reason)
|
||||
|
||||
#
|
||||
#if WebRTCPeerConnection.STATE_CONNECTED == connection_state:
|
||||
#
|
||||
1
addons/tube/tube_tracker.gd.uid
Normal file
1
addons/tube/tube_tracker.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b81mnu14j3uxw
|
||||
213
addons/tube/tube_upnp.gd
Normal file
213
addons/tube/tube_upnp.gd
Normal file
@ -0,0 +1,213 @@
|
||||
class_name TubeUPNP extends RefCounted
|
||||
|
||||
|
||||
signal warning_raised(message: String)
|
||||
signal port_mapping_ready
|
||||
signal port_mapped(public_port: int, local_port: int)
|
||||
|
||||
|
||||
const MAPPING_DURATION := 60*2 #sec, 2 minutes
|
||||
const MAPPING_RENEW_TIME := 0.75*MAPPING_DURATION
|
||||
|
||||
|
||||
var mapped_ports: Dictionary[int, int] = {}
|
||||
var mapped_times: Dictionary[int, float] = {}
|
||||
|
||||
var task_ids: Array[int] = []
|
||||
var upnp := UPNP.new()
|
||||
|
||||
var is_port_mapping_ready := false
|
||||
var mutex := Mutex.new()
|
||||
var mapping_queue: Array[Callable] = []
|
||||
|
||||
|
||||
func raise_warning(message: String):
|
||||
push_warning(message)
|
||||
warning_raised.emit(message)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
if OS.get_name() == "Web":
|
||||
return
|
||||
|
||||
port_mapping_ready.connect(_on_port_mapping_ready)
|
||||
task_ids.append(WorkerThreadPool.add_task(_upnp_init_task))
|
||||
|
||||
|
||||
func _upnp_init_task() -> void:
|
||||
var error := upnp.discover()
|
||||
|
||||
if error:
|
||||
raise_warning.call_deferred(
|
||||
"cannot map port, upnp discover error: {error}".format({
|
||||
"error": ClassDB.class_get_enum_constants("UPNP", "UPNPResult")[int(error)], # UPNPResult
|
||||
}))
|
||||
return
|
||||
|
||||
var gateway := upnp.get_gateway()
|
||||
if null == gateway:
|
||||
raise_warning.call_deferred(
|
||||
"cannot map port, no gateway found".format({
|
||||
}))
|
||||
return
|
||||
|
||||
if not gateway.is_valid_gateway():
|
||||
raise_warning.call_deferred(
|
||||
"cannot map port, gateway not valid".format({
|
||||
}))
|
||||
return
|
||||
|
||||
mutex.lock()
|
||||
is_port_mapping_ready = true
|
||||
mutex.unlock()
|
||||
port_mapping_ready.emit.call_deferred()
|
||||
|
||||
|
||||
func _on_port_mapping_ready():
|
||||
for callable in mapping_queue:
|
||||
WorkerThreadPool.add_task(callable, true)
|
||||
|
||||
|
||||
func _process(delta: float):
|
||||
for port in mapped_times:
|
||||
if not mapped_ports.has(port):
|
||||
continue
|
||||
|
||||
mapped_times[port] += delta
|
||||
if MAPPING_RENEW_TIME < mapped_times[port]:
|
||||
var local_port := mapped_ports[port]
|
||||
_add_port_mapping(port, local_port)
|
||||
|
||||
var tmp_ids := Array(task_ids)
|
||||
task_ids.clear()
|
||||
while not tmp_ids.is_empty():
|
||||
var id := tmp_ids.pop_back()
|
||||
if WorkerThreadPool.is_task_completed(id):
|
||||
var error := WorkerThreadPool.wait_for_task_completion(id)
|
||||
if error:
|
||||
raise_warning("cannot wait for port mapping task completion: {error}".format({
|
||||
"error": error_string(error)
|
||||
}))
|
||||
else:
|
||||
task_ids.append(id)
|
||||
|
||||
|
||||
func add_port_mapping(p_public_port: int, p_local_port: int) -> void:
|
||||
if mapped_ports.has(p_public_port):
|
||||
return
|
||||
|
||||
_add_port_mapping(p_public_port, p_local_port)
|
||||
|
||||
|
||||
func _add_port_mapping(p_public_port: int, p_local_port: int) -> void:
|
||||
mapped_ports[p_public_port] = p_local_port
|
||||
mapped_times[p_public_port] = 0.0
|
||||
|
||||
var callable := _add_port_mapping_task.bind(
|
||||
p_public_port,
|
||||
p_local_port
|
||||
)
|
||||
mutex.lock()
|
||||
if is_port_mapping_ready:
|
||||
task_ids.append(WorkerThreadPool.add_task(callable, true))
|
||||
else:
|
||||
mapping_queue.append(callable)
|
||||
|
||||
mutex.unlock()
|
||||
|
||||
|
||||
func _add_port_mapping_task(p_public_port: int, p_local_port: int) -> void:
|
||||
mutex.lock()
|
||||
if not is_port_mapping_ready:
|
||||
raise_warning.call_deferred(
|
||||
"cannot map port {port} to internal port {internal_port}, upnp not ready".format({
|
||||
"port": p_public_port,
|
||||
"internal_port": p_local_port,
|
||||
}))
|
||||
return
|
||||
mutex.unlock()
|
||||
|
||||
var result := upnp.add_port_mapping(
|
||||
p_public_port,
|
||||
p_local_port,
|
||||
"Tube", #ProjectSettings.get_setting("application/config/name"),
|
||||
"UDP",
|
||||
MAPPING_DURATION
|
||||
)
|
||||
if result:
|
||||
raise_warning.call_deferred(
|
||||
"cannot map port {port} to internal port {internal_port}, error: {error}".format({
|
||||
"port": p_public_port,
|
||||
"internal_port": p_local_port,
|
||||
"error": ClassDB.class_get_enum_constants("UPNP", "UPNPResult")[int(result)]
|
||||
}))
|
||||
|
||||
else:
|
||||
port_mapped.emit.call_deferred(
|
||||
p_public_port,
|
||||
p_local_port
|
||||
)
|
||||
|
||||
|
||||
func delete_port_mapping(p_public_port: int):
|
||||
if not mapped_ports.has(p_public_port):
|
||||
return
|
||||
|
||||
mapped_ports.erase(p_public_port)
|
||||
mapped_times.erase(p_public_port)
|
||||
|
||||
var callable := _delete_port_mapping_task.bind(
|
||||
p_public_port,
|
||||
)
|
||||
mutex.lock()
|
||||
if is_port_mapping_ready:
|
||||
task_ids.append(WorkerThreadPool.add_task(callable, true))
|
||||
else:
|
||||
mapping_queue.append(callable)
|
||||
mutex.unlock()
|
||||
|
||||
|
||||
func _delete_port_mapping_task(p_public_port: int) -> void:
|
||||
mutex.lock()
|
||||
if not is_port_mapping_ready:
|
||||
raise_warning.call_deferred(
|
||||
"cannot delete port mapping {port}, upnp not ready".format({
|
||||
"port": p_public_port,
|
||||
}))
|
||||
return
|
||||
mutex.unlock()
|
||||
|
||||
var result := upnp.delete_port_mapping(p_public_port, "UDP")
|
||||
if result:
|
||||
raise_warning.call_deferred(
|
||||
"cannot delete port mapping {port}, error: {error}".format({
|
||||
"port": p_public_port,
|
||||
"error": ClassDB.class_get_enum_constants("UPNP", "UPNPResult")[int(result)]
|
||||
}))
|
||||
|
||||
|
||||
func clear_port_mapping() -> void:
|
||||
for port in mapped_ports:
|
||||
delete_port_mapping(port)
|
||||
|
||||
mapped_ports.clear()
|
||||
mapped_times.clear()
|
||||
|
||||
|
||||
func _notification(what):
|
||||
if what == NOTIFICATION_PREDELETE:
|
||||
for port in mapped_ports:
|
||||
delete_port_mapping(port)
|
||||
|
||||
mapped_ports.clear()
|
||||
mapped_times.clear()
|
||||
|
||||
for id in task_ids:
|
||||
var error := WorkerThreadPool.wait_for_task_completion(id)
|
||||
if error:
|
||||
raise_warning("cannot wait for port mapping task completion: {error}".format({
|
||||
"error": error_string(error)
|
||||
}))
|
||||
return
|
||||
|
||||
task_ids.clear()
|
||||
1
addons/tube/tube_upnp.gd.uid
Normal file
1
addons/tube/tube_upnp.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b7atm4ie65ntq
|
||||
BIN
assets/Assets/Extra/static_shadow.png
(Stored with Git LFS)
Normal file
BIN
assets/Assets/Extra/static_shadow.png
(Stored with Git LFS)
Normal file
Binary file not shown.
40
assets/Assets/Extra/static_shadow.png.import
Normal file
40
assets/Assets/Extra/static_shadow.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b7cma8jcljou7"
|
||||
path="res://.godot/imported/static_shadow.png-1b98f78cddf6e18349e81ec4c7928421.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/Assets/Extra/static_shadow.png"
|
||||
dest_files=["res://.godot/imported/static_shadow.png-1b98f78cddf6e18349e81ec4c7928421.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
assets/Assets/Prototype_Character/_prototype_character.aseprite
Normal file
BIN
assets/Assets/Prototype_Character/_prototype_character.aseprite
Normal file
Binary file not shown.
BIN
assets/Assets/Prototype_Character/prototype_character.png
(Stored with Git LFS)
Normal file
BIN
assets/Assets/Prototype_Character/prototype_character.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://mp8uhhjx1orc"
|
||||
path="res://.godot/imported/prototype_character.png-1c49162889488572260123552355c566.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/Assets/Prototype_Character/prototype_character.png"
|
||||
dest_files=["res://.godot/imported/prototype_character.png-1c49162889488572260123552355c566.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
assets/Assets/Prototype_Character/prototype_character_blue.png
(Stored with Git LFS)
Normal file
BIN
assets/Assets/Prototype_Character/prototype_character_blue.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bap7oggpwdvqs"
|
||||
path="res://.godot/imported/prototype_character_blue.png-8f4797f794496f4660a8a57b622e59e7.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/Assets/Prototype_Character/prototype_character_blue.png"
|
||||
dest_files=["res://.godot/imported/prototype_character_blue.png-8f4797f794496f4660a8a57b622e59e7.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
assets/Assets/Prototype_Character/prototype_character_green.png
(Stored with Git LFS)
Normal file
BIN
assets/Assets/Prototype_Character/prototype_character_green.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dy1ls3gbu74pw"
|
||||
path="res://.godot/imported/prototype_character_green.png-256684b5480019ec9f154ea1a1c40b3d.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/Assets/Prototype_Character/prototype_character_green.png"
|
||||
dest_files=["res://.godot/imported/prototype_character_green.png-256684b5480019ec9f154ea1a1c40b3d.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
assets/Assets/Prototype_Character/prototype_character_red.png
(Stored with Git LFS)
Normal file
BIN
assets/Assets/Prototype_Character/prototype_character_red.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bnxunneqi1d01"
|
||||
path="res://.godot/imported/prototype_character_red.png-e2c7157cfaaa27a5548f802a98124527.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/Assets/Prototype_Character/prototype_character_red.png"
|
||||
dest_files=["res://.godot/imported/prototype_character_red.png-e2c7157cfaaa27a5548f802a98124527.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
assets/Assets/Prototype_Character/prototype_character_shadow.png
(Stored with Git LFS)
Normal file
BIN
assets/Assets/Prototype_Character/prototype_character_shadow.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://lub3bl8xi147"
|
||||
path="res://.godot/imported/prototype_character_shadow.png-2d3c58479e73f8f95450d52ca424722c.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/Assets/Prototype_Character/prototype_character_shadow.png"
|
||||
dest_files=["res://.godot/imported/prototype_character_shadow.png-2d3c58479e73f8f95450d52ca424722c.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
assets/Assets/Prototype_Character/prototype_character_yellow.png
(Stored with Git LFS)
Normal file
BIN
assets/Assets/Prototype_Character/prototype_character_yellow.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://rq1mddd0hgj3"
|
||||
path="res://.godot/imported/prototype_character_yellow.png-f8e7f36765641a155a312967fd98b1ae.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/Assets/Prototype_Character/prototype_character_yellow.png"
|
||||
dest_files=["res://.godot/imported/prototype_character_yellow.png-f8e7f36765641a155a312967fd98b1ae.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
assets/Pattern-Panic-10x10/Circles.png
(Stored with Git LFS)
Normal file
BIN
assets/Pattern-Panic-10x10/Circles.png
(Stored with Git LFS)
Normal file
Binary file not shown.
40
assets/Pattern-Panic-10x10/Circles.png.import
Normal file
40
assets/Pattern-Panic-10x10/Circles.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dmm6upjnwahu8"
|
||||
path="res://.godot/imported/Circles.png-10f706c140f293279b5d2fd9f2674218.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/Pattern-Panic-10x10/Circles.png"
|
||||
dest_files=["res://.godot/imported/Circles.png-10f706c140f293279b5d2fd9f2674218.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
assets/Pattern-Panic-10x10/Cross-and-Petals.png
(Stored with Git LFS)
Normal file
BIN
assets/Pattern-Panic-10x10/Cross-and-Petals.png
(Stored with Git LFS)
Normal file
Binary file not shown.
40
assets/Pattern-Panic-10x10/Cross-and-Petals.png.import
Normal file
40
assets/Pattern-Panic-10x10/Cross-and-Petals.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cmdsg0h4i4n1x"
|
||||
path="res://.godot/imported/Cross-and-Petals.png-b5d1fee2d129685e87710835b27fb29a.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/Pattern-Panic-10x10/Cross-and-Petals.png"
|
||||
dest_files=["res://.godot/imported/Cross-and-Petals.png-b5d1fee2d129685e87710835b27fb29a.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
assets/Pattern-Panic-10x10/Misc.png
(Stored with Git LFS)
Normal file
BIN
assets/Pattern-Panic-10x10/Misc.png
(Stored with Git LFS)
Normal file
Binary file not shown.
40
assets/Pattern-Panic-10x10/Misc.png.import
Normal file
40
assets/Pattern-Panic-10x10/Misc.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://jjiaybhtopy"
|
||||
path="res://.godot/imported/Misc.png-13825b07612f9d6c157e76a73f83d8a5.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/Pattern-Panic-10x10/Misc.png"
|
||||
dest_files=["res://.godot/imported/Misc.png-13825b07612f9d6c157e76a73f83d8a5.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
assets/Pattern-Panic-10x10/Points-and-Pulses.png
(Stored with Git LFS)
Normal file
BIN
assets/Pattern-Panic-10x10/Points-and-Pulses.png
(Stored with Git LFS)
Normal file
Binary file not shown.
40
assets/Pattern-Panic-10x10/Points-and-Pulses.png.import
Normal file
40
assets/Pattern-Panic-10x10/Points-and-Pulses.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://lud2f8x4hx54"
|
||||
path="res://.godot/imported/Points-and-Pulses.png-1f420e7fc3d1e69127dd375ab2641b2e.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/Pattern-Panic-10x10/Points-and-Pulses.png"
|
||||
dest_files=["res://.godot/imported/Points-and-Pulses.png-1f420e7fc3d1e69127dd375ab2641b2e.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
16
assets/Pattern-Panic-10x10/README.txt
Normal file
16
assets/Pattern-Panic-10x10/README.txt
Normal file
@ -0,0 +1,16 @@
|
||||
Pattern Panic
|
||||
-------------
|
||||
|
||||
Enjoy my assets? Keep up to date by following me on itch or twitter.
|
||||
|
||||
https://v3x3d.itch.io/
|
||||
https://twitter.com/_V3X3D
|
||||
|
||||
You can also support me on Patreon for just a $1 each month.
|
||||
Your support helps me keep making assets and provides you rewards.
|
||||
|
||||
https://www.patreon.com/V3X3D
|
||||
|
||||
|
||||
|
||||
-- VEXED
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user