OverviewFilesCommitsIssuesDocsReleasesPeers

hypergodot commits / fa5502c

Progress on multiplayer game demo
fa5502cbea75c3c0e7cf4f397fb29451bed92144
by mauve <ranger@mauve.moe> · 2021-09-23T23:06:52.000Z

Changed files (19) +523 -17

.gitignore +1 -0
……@@ -2,3 +2,4 @@
22*.translation
33export_presets.cfg
44.mono/
5addons/
README.md +10 -2
……@@ -2,7 +2,7 @@
22
33A Godot plugin and example project for loading assets from the [hypercore-protocol](https://hypercore-protocol.org/).
44
5*WIP:* This is a work an progress and isn't ready for use yet. Feel free to follow for progress, though.
5*WIP:* This is a work an progress and may have breaking changes. Feel free to follow for progress, though.
66
77## How It Works:
88
……@@ -22,8 +22,16 @@ A Godot plugin and example project for loading assets from the [hypercore-protoc
2222- [ ] Examples for all the methods exposed by the gateway.
2323 - [x] GET/PUT text files
2424 - [x] Watch for changes
25 - [ ] Basic chat app (WIP
25 - [x] Basic chat app
26- [x] Load latest hyper-gateway dynamically on boot
2627- [ ] Basic Multiplayer demo using extension messages
28 - [x] Refactor chat system to be more general purpose (not just texts)
29 - [x] Basic demo moving blobs around on a map
30 - [ ] Place usernames in a hyperdrive's json file along with an image for your blob
31 - [ ] Create custom maps from an image and a JSON file
32 - [ ] Auto-discover which maps are active
33- [ ] Enable code to work in Godot Web in Agregore
34- [ ] Enable code to work in Godot Web in regular browsers
2735- [ ] Load gltf scene from hyper
2836- [ ] Load VRM avatar from hyper (social multiplayer) [VRM stuff](https://github.com/V-Sekai/godot-vrm/blob/godot3/addons/vrm/import_vrm.gd)
2937- [ ] Folder sync (From `hyper://` to fs, from fs to `hyper://`)
metadata.json +5 -0
……@@ -0,0 +1,5 @@
1{
2 "failed": [
3
4 ]
5}
project.godot +67 -1
……@@ -24,18 +24,47 @@ _global_script_classes=[ {
2424"language": "GDScript",
2525"path": "res://src/HyperGateway/HyperGateway.gd"
2626}, {
27"base": "Node",
28"class": "HyperGossip",
29"language": "GDScript",
30"path": "res://src/multiplayer/HyperGossip.gd"
31}, {
2732"base": "HTTPRequest",
2833"class": "HyperRequest",
2934"language": "GDScript",
3035"path": "res://src/HyperRequest/HyperRequest.gd"
36}, {
37"base": "Reference",
38"class": "WAT",
39"language": "GDScript",
40"path": "res://addons/WAT/namespace.gd"
41}, {
42"base": "Node",
43"class": "WATTest",
44"language": "GDScript",
45"path": "res://addons/WAT/test/test.gd"
3146} ]
3247_global_script_class_icons={
3348"HTTPEventSource": "",
3449"HyperEventSource": "",
3550"HyperGateway": "",
36"HyperRequest": ""
51"HyperGossip": "",
52"HyperRequest": "",
53"WAT": "",
54"WATTest": ""
3755}
3856
57[WAT]
58
59Test_Directory="res://"
60Results_Directory="res://"
61Test_Metadata_Directory="res://"
62Tags=PoolStringArray( )
63Window_Size=Vector2( 1280, 720 )
64Minimize_Window_When_Running_Tests=false
65Port=6008
66Display=8
67
3968[application]
4069
4170config/name="HyperGodot"
……@@ -48,6 +77,43 @@ window/size/width=666
4877window/size/height=666
4978window/dpi/allow_hidpi=true
5079
80[editor_plugins]
81
82enabled=PoolStringArray( "res://addons/WAT/plugin.cfg" )
83
84[input]
85
86left={
87"deadzone": 0.5,
88"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":65,"unicode":0,"echo":false,"script":null)
89, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777231,"unicode":0,"echo":false,"script":null)
90 ]
91}
92right={
93"deadzone": 0.5,
94"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":68,"unicode":0,"echo":false,"script":null)
95, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777233,"unicode":0,"echo":false,"script":null)
96 ]
97}
98up={
99"deadzone": 0.5,
100"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":87,"unicode":0,"echo":false,"script":null)
101, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777232,"unicode":0,"echo":false,"script":null)
102 ]
103}
104down={
105"deadzone": 0.5,
106"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":83,"unicode":0,"echo":false,"script":null)
107, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777234,"unicode":0,"echo":false,"script":null)
108 ]
109}
110click={
111"deadzone": 0.5,
112"events": [ Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"button_mask":0,"position":Vector2( 0, 0 ),"global_position":Vector2( 0, 0 ),"factor":1.0,"button_index":1,"pressed":false,"doubleclick":false,"script":null)
113, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":32,"unicode":0,"echo":false,"script":null)
114 ]
115}
116
51117[physics]
52118
53119common/enable_pause_aware_picking=true
results.xml +7 -0
……@@ -0,0 +1,7 @@
1<?xml version="1.0" ?>
2<testsuites failures="0" name="TestXML" tests="1" time="0.019">
3 <testsuite name="Hyper Gossip" failures="0" tests="1" time="0">
4 <testcase name=" create gateway and connect" time="0"></testcase>
5 </testsuite>
6
7</testsuites>
src/HTTPEventSource.gd +23 -5
……@@ -5,6 +5,7 @@ class_name HTTPEventSource
55const URLParser = preload('./utils/URLParser.gd')
66
77signal event(data, event, id)
8signal response_started(statusCode, responseHeaders)
89
910var http = HTTPClient.new()
1011var urlParser = URLParser.new()
……@@ -36,7 +37,7 @@ func request(
3637 "request_data": request_data
3738 }
3839
39 print('About to request', parsedURL, params)
40 print('About to request', parsedURL)
4041
4142 var thread = Thread.new()
4243
……@@ -44,14 +45,24 @@ func request(
4445
4546 pass
4647
48func _emit_started(statusCode, responseHeaders):
49 print("Emitting started")
50 emit_signal("response_started", statusCode, responseHeaders)
51
4752func _runRequest(params):
4853 var err = http.connect_to_host(params.host, params.port, params.use_ssl, params.verify_host)
4954
55 if err != OK:
56 printerr('Error connecting to EventSource server')
57
5058 print('Connected to host')
5159
5260 # Wait to establish a connection to the host
5361 while http.get_status() == HTTPClient.STATUS_CONNECTING or http.get_status() == HTTPClient.STATUS_RESOLVING:
54 http.poll()
62 err = http.poll()
63 if err != OK:
64 printerr('Unable to connect to EventSource')
65 return
5566 OS.delay_msec(100)
5667
5768 print('Able to connect, requesting')
……@@ -59,9 +70,16 @@ func _runRequest(params):
5970 # Start the request
6071 err = http.request(params.method, params.path, params.headers, params.request_data)
6172
73 if err != OK:
74 printerr('Unable to request EventSource')
75 return
76
6277 # Wait for request to get processed and to get the body
6378 while (http.get_status() == HTTPClient.STATUS_REQUESTING):
64 http.poll()
79 err = http.poll()
80 if err != OK:
81 printerr('Unable to request to EventSource')
82 return
6583 OS.delay_msec(100)
6684
6785 if !http.has_response():
……@@ -72,10 +90,10 @@ func _runRequest(params):
7290
7391 var statusCode = http.get_response_code()
7492 var responseHeaders = http.get_response_headers()
75
93
7694 print('Got response ', statusCode)
7795
78 call_deferred("emit_signal", "response_started", statusCode, responseHeaders)
96 call_deferred("_emit_started", statusCode, responseHeaders)
7997
8098 var buffer = ""
8199 var lastChunkEmpty = false
src/HyperGateway/HyperGateway.gd +15 -8
……@@ -10,16 +10,19 @@ signal started_gateway(pid)
1010signal started_download(version)
1111signal finished_download(version)
1212
13export var writable = true
14export var persist = true
13export var writable: bool = true
14export var persist: bool = true
1515
16export var autoStart = true
17export var autoDownload = true
18export var autoUpdate = false
16export var autoStart: bool = true
17export var autoDownload: bool = true
18export var autoUpdate: bool = false
1919
20export var storageDirectory = "user://gateway-data/"
21export var repositoryName = "rangermauve/hyper-gateway"
22export var tagStart = "v2"
20const DEFAULT_PORT = 4973
21
22export var port: int = DEFAULT_PORT
23export var storageDirectory: String = "user://gateway-data/"
24export var repositoryName: String = "rangermauve/hyper-gateway"
25export var tagStart: String = "v2"
2326
2427## TODO: Use this tag if the `latest` release doesn't match the tag?
2528# export var defaultTag = "v2.1.3"
……@@ -69,6 +72,10 @@ func setupGateway():
6972 if !persist: args.append('--no-persist')
7073 if writable: args.append('--writable')
7174
75 if port != DEFAULT_PORT:
76 args.append('--port')
77 args.append(String(port))
78
7279 var resolvedStorageDirectory = ProjectSettings.globalize_path(storageDirectory)
7380 args.append('--storage-location')
7481 args.append(resolvedStorageDirectory)
src/HyperRequest/HyperRequest.gd +0 -1
……@@ -18,5 +18,4 @@ func request(
1818 var toLoad = url.replace("hyper://", serverPrefix)
1919 emit_signal('loading_data', url)
2020
21 print(method, ' ', toLoad)
2221 return .request(toLoad, custom_headers, ssl_validate_domain, method, request_data)
src/MultiplayerDemo/Bullet.gd +11 -0
……@@ -0,0 +1,11 @@
1extends Area2D
2
3var initialSpeed = 600
4var velocity = Vector2(initialSpeed, 0)
5
6func _physics_process(delta):
7 position += velocity * delta
8
9# Destroy the bullet once it leaves the screen
10func _on_VisibilityNotifier2D_screen_exited():
11 queue_free()
src/MultiplayerDemo/Bullet.tscn +22 -0
……@@ -0,0 +1,22 @@
1[gd_scene load_steps=4 format=2]
2
3[ext_resource path="res://src/MultiplayerDemo/icon.png" type="Texture" id=1]
4[ext_resource path="res://src/MultiplayerDemo/Bullet.gd" type="Script" id=2]
5
6[sub_resource type="CircleShape2D" id=1]
7radius = 4.0
8
9[node name="Bullet" type="Area2D"]
10script = ExtResource( 2 )
11
12[node name="Sprite" type="Sprite" parent="."]
13scale = Vector2( 0.01, 0.01 )
14texture = ExtResource( 1 )
15
16[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
17shape = SubResource( 1 )
18
19[node name="VisibilityNotifier2D" type="VisibilityNotifier2D" parent="."]
20rect = Rect2( -4, -4, 8, 8 )
21
22[connection signal="screen_exited" from="VisibilityNotifier2D" to="." method="_on_VisibilityNotifier2D_screen_exited"]
src/MultiplayerDemo/FlyBalls.gd +70 -0
……@@ -0,0 +1,70 @@
1extends Node2D
2
3var PlayerSprite = preload("res://src/MultiplayerDemo/PlayerSprite.tscn")
4
5export var url: String = "hyper://blog.mauve.moe/"
6
7onready var gossip = $HyperGossip
8onready var player = $Player
9onready var gateway = $HyperGateway
10
11const POSITION = 'position'
12
13const knownPlayers = {}
14
15func _ready():
16 gateway.start()
17
18func get_player_object(id):
19 if knownPlayers.has(id): return knownPlayers[id]
20
21 var playerSprite = PlayerSprite.instance()
22 knownPlayers[id] = playerSprite
23
24 _broadcast_player()
25
26 return playerSprite
27
28func _broadcast_player():
29 var position = player.position
30 var rotation = player.rotation_degrees
31 var data = {
32 "position": {
33 "x": position.x,
34 "y": position.y
35 },
36 "rotation": rotation
37 }
38 gossip.broadcast_event(POSITION, data)
39
40func _on_remote_player_moved(positionData, id):
41 var remotePlayer = get_player_object(id)
42 var rotation_degrees = positionData.rotation
43 var raw_position = positionData.position
44 remotePlayer.rotation_degrees = rotation_degrees
45 remotePlayer.position.x = raw_position.x
46 remotePlayer.position.y = raw_position.y
47
48func _on_Player_shoot(Bullet, direction, location):
49 var bullet = Bullet.instance()
50 add_child(bullet)
51 bullet.rotation = direction
52 bullet.position = location
53 bullet.velocity = bullet.velocity.rotated(direction)
54
55func _on_HyperGateway_started_gateway(_pid):
56 gossip.url = url
57 gossip.start_listening()
58
59func _on_HyperGossip_listening(_extension_name):
60 print('Started listening on events')
61 # Tell everyone we exist!
62 _broadcast_player()
63
64func _on_HyperGossip_event(type, data, from):
65 if type == POSITION:
66 _on_remote_player_moved(data, from)
67
68func _on_Player_moved(_position, _rotation, _velocity):
69 # TODO: Don't broadcast too often to avoid flooding the system
70 _broadcast_player()
src/MultiplayerDemo/FlyBalls.tscn +28 -0
……@@ -0,0 +1,28 @@
1[gd_scene load_steps=6 format=2]
2
3[ext_resource path="res://src/MultiplayerDemo/PlayerSprite.tscn" type="PackedScene" id=1]
4[ext_resource path="res://src/MultiplayerDemo/Player.gd" type="Script" id=2]
5[ext_resource path="res://src/MultiplayerDemo/FlyBalls.gd" type="Script" id=3]
6[ext_resource path="res://src/HyperGateway/HyperGateway.gd" type="Script" id=4]
7[ext_resource path="res://src/multiplayer/HyperGossip.gd" type="Script" id=5]
8
9[node name="FlyBalls" type="Node2D"]
10script = ExtResource( 3 )
11
12[node name="Player" parent="." instance=ExtResource( 1 )]
13position = Vector2( 333, 333 )
14script = ExtResource( 2 )
15
16[node name="HyperGossip" type="Node" parent="."]
17script = ExtResource( 5 )
18extension_name = "flyballs-v1"
19
20[node name="HyperGateway" type="Node" parent="."]
21script = ExtResource( 4 )
22autoStart = false
23
24[connection signal="moved" from="Player" to="." method="_on_Player_moved"]
25[connection signal="shoot" from="Player" to="." method="_on_Player_shoot"]
26[connection signal="event" from="HyperGossip" to="." method="_on_HyperGossip_event"]
27[connection signal="listening" from="HyperGossip" to="." method="_on_HyperGossip_listening"]
28[connection signal="started_gateway" from="HyperGateway" to="." method="_on_HyperGateway_started_gateway"]
src/MultiplayerDemo/Player.gd +39 -0
……@@ -0,0 +1,39 @@
1extends "./PlayerSprite.gd"
2
3var Bullet = preload("res://src/MultiplayerDemo/Bullet.tscn")
4
5export (int) var speed = 200
6
7signal shoot(bullet, direction, location)
8signal moved(position, rotation, velocity)
9
10var velocity = Vector2()
11
12func proccess_input():
13 var hasMoved = false
14 look_at(get_global_mouse_position())
15 velocity = Vector2()
16 if Input.is_action_pressed("right"):
17 hasMoved = true
18 velocity.x += 1
19 if Input.is_action_pressed("left"):
20 hasMoved = true
21 velocity.x -= 1
22 if Input.is_action_pressed("down"):
23 hasMoved = true
24 velocity.y += 1
25 if Input.is_action_pressed("up"):
26 hasMoved = true
27 velocity.y -= 1
28
29 velocity = velocity.normalized() * speed
30
31 if Input.is_action_just_pressed("click"):
32 emit_signal("shoot", Bullet, rotation, position)
33
34 return hasMoved
35
36func _physics_process(_delta):
37 var hasMoved = proccess_input()
38 velocity = move_and_slide(velocity)
39 if hasMoved: emit_signal("moved", position, rotation, velocity)
src/MultiplayerDemo/PlayerSprite.gd +6 -0
……@@ -0,0 +1,6 @@
1extends KinematicBody2D
2
3func _on_VisibilityNotifier2D_screen_exited():
4 var viewport = get_viewport_rect().size
5 position.x = viewport.x / 2
6 position.y = viewport.y / 2
src/MultiplayerDemo/PlayerSprite.tscn +42 -0
……@@ -0,0 +1,42 @@
1[gd_scene load_steps=5 format=2]
2
3[ext_resource path="res://src/MultiplayerDemo/icon.png" type="Texture" id=1]
4[ext_resource path="res://src/MultiplayerDemo/PlayerSprite.gd" type="Script" id=2]
5
6[sub_resource type="ParticlesMaterial" id=1]
7flag_disable_z = true
8direction = Vector3( -1, 0, 0 )
9gravity = Vector3( 0, 0, 0 )
10initial_velocity = 60.0
11orbit_velocity = 0.0
12orbit_velocity_random = 0.0
13radial_accel_random = 0.14
14angle = 45.0
15angle_random = 1.0
16scale = 0.02
17scale_random = 0.01
18
19[sub_resource type="CircleShape2D" id=2]
20radius = 30.0
21
22[node name="PlayerSprite" type="KinematicBody2D"]
23script = ExtResource( 2 )
24
25[node name="Particles2D" type="Particles2D" parent="."]
26amount = 32
27lifetime = 3.0
28local_coords = false
29process_material = SubResource( 1 )
30texture = ExtResource( 1 )
31
32[node name="Sprite" type="Sprite" parent="."]
33scale = Vector2( 0.1, 0.1 )
34texture = ExtResource( 1 )
35
36[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
37shape = SubResource( 2 )
38
39[node name="VisibilityNotifier2D" type="VisibilityNotifier2D" parent="."]
40rect = Rect2( -40, -40, 80, 80 )
41
42[connection signal="screen_exited" from="VisibilityNotifier2D" to="." method="_on_VisibilityNotifier2D_screen_exited"]
src/MultiplayerDemo/icon.png +0 -0
(no content changes — binary or rename)
src/MultiplayerDemo/icon.png.import +34 -0
……@@ -0,0 +1,34 @@
1[remap]
2
3importer="texture"
4type="StreamTexture"
5path="res://.import/icon.png-f5f6ad6f85377ad8c63e3aa6b4372897.stex"
6metadata={
7"vram_texture": false
8}
9
10[deps]
11
12source_file="res://src/MultiplayerDemo/icon.png"
13dest_files=[ "res://.import/icon.png-f5f6ad6f85377ad8c63e3aa6b4372897.stex" ]
14
15[params]
16
17compress/mode=0
18compress/lossy_quality=0.7
19compress/hdr_mode=0
20compress/bptc_ldr=0
21compress/normal_map=0
22flags/repeat=0
23flags/filter=true
24flags/mipmaps=false
25flags/anisotropic=false
26flags/srgb=2
27process/fix_alpha_border=true
28process/premult_alpha=false
29process/HDR_as_SRGB=false
30process/invert_color=false
31stream=false
32size_limit=0
33detect_3d=true
34svg/scale=1.0
src/multiplayer/HyperGossip.gd +139 -0
……@@ -0,0 +1,139 @@
1extends Node
2
3class_name HyperGossip
4
5const LRU = preload('res://src/utils/LRU.gd')
6
7signal listening(extension_name)
8signal event(type, data, from)
9signal peers(peers)
10
11export var url = "hyper://blog.mauve.moe/"
12export var extension_name = "hyper-gossip-v1"
13
14var peerListRequest = HyperRequest.new()
15var eventSource = HyperEventSource.new()
16var broadcastRequest = HyperRequest.new()
17
18var seen_messages = LRU.new()
19var requestQueue = []
20var isRequesting = false
21
22func _ready():
23 add_child(peerListRequest)
24 peerListRequest.connect("request_completed", self, "_on_peer_list")
25
26 add_child(broadcastRequest)
27 broadcastRequest.connect("request_completed", self, "_on_broadcast_completed")
28
29 eventSource.connect("event", self, "_on_extesnion_event")
30 eventSource.connect("response_started", self, "_on_events_started")
31
32func start_listening():
33 load_peers()
34 eventSource.request(_get_extension_listen_url())
35
36func _on_events_started(_statusCode, _responseHeaders):
37 emit_signal("listening", extension_name)
38
39func _enqueue_broadcast_request(reqURL, body):
40 if isRequesting:
41 requestQueue.push_back({"reqURL": reqURL, "body": body})
42 else: _perform_broadcast_request(reqURL, body)
43
44func _perform_broadcast_request(reqURL, body):
45 isRequesting = true
46 broadcastRequest.request(reqURL, [], false, HTTPClient.METHOD_POST, body)
47
48func _on_broadcast_completed(_result, _response_code, _headers, _body):
49 isRequesting = false
50 if requestQueue.size() == 0: return
51 var next = requestQueue.pop_front()
52 _perform_broadcast_request(next.reqURL, next.body)
53
54func rebroadcast(event):
55 var reqURL = _get_extension_url()
56 var body = JSON.print(event)
57 _enqueue_broadcast_request(reqURL, body)
58
59func broadcast_event(type: String, data):
60 var reqURL = _get_extension_url()
61 var body = JSON.print(_generateEvent(type, data))
62 _enqueue_broadcast_request(reqURL, body)
63
64func send_to_peer(peer, type, data):
65 var reqURL = _get_extension_url_to_peer(peer)
66 var body = JSON.print(_generateEvent(type, data))
67 _enqueue_broadcast_request(reqURL, body)
68
69func load_peers():
70 var reqURL = _get_extension_url()
71 peerListRequest.request(reqURL)
72
73func _generateEvent(type, data):
74 var id = _make_id()
75 seen_messages.track(id)
76 return {
77 "id": id,
78 "type": type,
79 "data": data
80 }
81
82func _handle_event(event):
83 var type = event.type
84 var data = event.data
85 var id = event.id
86 var from = event.from
87
88 if seen_messages.has(id):
89 return
90
91 seen_messages.track(id)
92
93 emit_signal("event", type, data, from)
94 rebroadcast(event)
95
96func _on_event(data, event, id):
97 # Ignore events for other extensions
98 if event != extension_name: return
99
100 var parsed = JSON.parse(data)
101
102 if parsed.error != OK:
103 printerr("Unable to parse EventSource content " + parsed.error_line + "\n" + data)
104 return
105
106 var result = parsed.result
107 if !("from" in result):
108 result.from = id
109
110 _handle_event(result)
111
112
113func _on_peer_list(_result, response_code, _headers, body):
114 var text = body.get_string_from_utf8()
115 if response_code != 200:
116 printerr('Error listing peers ', response_code, " ", text)
117 return
118
119 var parsed = JSON.parse(text)
120
121 if parsed.error != OK:
122 printerr("Unable to parse Peer List" + String(parsed.error_line))
123 return
124
125 var peers = parsed.result
126
127 emit_signal("peers", peers)
128
129func _get_extension_url_to_peer(peer):
130 return url + '$/extensions/' + extension_name + '/' + peer
131
132func _get_extension_url():
133 return url + '$/extensions/' + extension_name
134
135func _get_extension_listen_url():
136 return url + '$/extensions/'
137
138func _make_id():
139 return str(randi()) + str(randi())
tests/multiplayer/HyperGossip.test.gd +4 -0
……@@ -0,0 +1,4 @@
1extends WAT.Test
2
3func test_create_gateway_and_connect() -> void:
4 asserts.auto_pass("TODO")