summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorStar Rauchenberger <fefferburbia@gmail.com>2024-02-08 16:29:48 -0500
committerStar Rauchenberger <fefferburbia@gmail.com>2024-02-08 16:29:48 -0500
commit60a0a573f0d0329e8d5c73878165ba80fa2d2628 (patch)
treebe2c9519c47ca3aa28f539587aae4ef9274b9e2d
downloadlingo-race-60a0a573f0d0329e8d5c73878165ba80fa2d2628.tar.gz
lingo-race-60a0a573f0d0329e8d5c73878165ba80fa2d2628.tar.bz2
lingo-race-60a0a573f0d0329e8d5c73878165ba80fa2d2628.zip
Initial commit
-rw-r--r--level_race.tscn58
-rw-r--r--racing/load.gd70
-rw-r--r--racing/lobby.gd407
-rw-r--r--racing/manager.gd28
-rw-r--r--racing/multiplayer.gd293
5 files changed, 856 insertions, 0 deletions
diff --git a/level_race.tscn b/level_race.tscn new file mode 100644 index 0000000..365f57e --- /dev/null +++ b/level_race.tscn
@@ -0,0 +1,58 @@
1[gd_scene load_steps=6 format=2]
2
3[ext_resource path="res://nodes/label.tscn" type="PackedScene" id=1]
4[ext_resource path="res://fonts/Lingo.tres" type="DynamicFont" id=2]
5[ext_resource path="res://nodes/button.tscn" type="PackedScene" id=3]
6[ext_resource path="user://maps/racing/lobby.gd" type="Script" id=4]
7[ext_resource path="res://lingo_3.png" type="Texture" id=5]
8
9[node name="racing" type="Spatial"]
10script = ExtResource( 4 )
11
12[node name="lingo_3" type="Sprite" parent="."]
13position = Vector2( 960, 540 )
14texture = ExtResource( 5 )
15
16[node name="Panel" type="Panel" parent="."]
17margin_right = 1920.0
18margin_bottom = 1080.0
19
20[node name="ItemList" type="ItemList" parent="Panel"]
21margin_left = 642.0
22margin_top = 275.0
23margin_right = 1298.0
24margin_bottom = 804.0
25auto_height = true
26
27[node name="ScrollContainer" type="ScrollContainer" parent="Panel"]
28margin_left = -70.0
29margin_top = 251.0
30margin_right = 572.0
31margin_bottom = 805.0
32
33[node name="title" parent="Panel" instance=ExtResource( 1 )]
34margin_left = 0.0
35margin_top = 75.0
36margin_right = 1920.0
37margin_bottom = 225.0
38custom_fonts/font = ExtResource( 2 )
39text = "LINGO RACING LOBBY"
40valign = 1
41
42[node name="return_button" parent="Panel" instance=ExtResource( 3 )]
43margin_left = 1002.0
44margin_top = 879.0
45margin_right = 1640.0
46margin_bottom = 1029.0
47custom_colors/font_color_hover = Color( 1, 0, 0, 1 )
48custom_fonts/font = ExtResource( 2 )
49text = "RETURN"
50
51[node name="main_button" parent="Panel" instance=ExtResource( 3 )]
52margin_left = 254.0
53margin_top = 879.0
54margin_right = 892.0
55margin_bottom = 1029.0
56custom_colors/font_color_hover = Color( 1, 0, 0, 1 )
57custom_fonts/font = ExtResource( 2 )
58text = "READY"
diff --git a/racing/load.gd b/racing/load.gd new file mode 100644 index 0000000..cd40e42 --- /dev/null +++ b/racing/load.gd
@@ -0,0 +1,70 @@
1extends "res://scripts/load.gd"
2
3
4func _load():
5 disable_saving = true
6
7 var player = get_node("player")
8 player.playable = false
9
10 var blindfold = ColorRect.new()
11 blindfold.color = Color.black
12 blindfold.name = "blindfold"
13 add_child(blindfold)
14
15 var race_manager = global.get_node("RaceManager")
16 var multiplayer = race_manager.SCRIPT_multiplayer.new()
17 multiplayer.name = "Multiplayer"
18 multiplayer.active_lobby_id = race_manager.lobby_id
19 add_child(multiplayer)
20
21 var label = Label.new()
22 label.set_name("label")
23 label.margin_right = 1920.0 - 20.0
24 label.margin_top = 20.0
25 label.align = Label.ALIGN_RIGHT
26 label.valign = Label.VALIGN_TOP
27
28 var dynamic_font = DynamicFont.new()
29 dynamic_font.font_data = load("res://fonts/Lingo.ttf")
30 dynamic_font.size = 36
31 dynamic_font.outline_color = Color(0, 0, 0, 1)
32 dynamic_font.outline_size = 2
33 label.add_font_override("font", dynamic_font)
34
35 add_child(label)
36
37 var panel_script = load("res://nodes/panel_1.tscn")
38 var victory_panel = panel_script.instance()
39 victory_panel.name = "Victory"
40 victory_panel.text = "i win"
41 victory_panel.answer = "i win"
42 victory_panel.translation.x = race_manager.end_pos[1] + 0.5
43 victory_panel.translation.y = race_manager.end_pos[2] + 1.5
44 victory_panel.translation.z = race_manager.end_pos[3] + 0.01
45 add_child(victory_panel)
46 victory_panel.get_node("Viewport/GUI/Panel/TextEdit").connect("answer_correct", self, "victory")
47
48 set_gridmap_tile(
49 race_manager.end_pos[1],
50 race_manager.end_pos[2] + 1.5,
51 race_manager.end_pos[3] - 0.5,
52 "MeshInstance5"
53 )
54
55 ._load()
56
57 multiplayer.player_loaded()
58
59
60func victory():
61 get_node("Multiplayer").player_victory()
62 ingame_achieve("Victory!")
63
64
65func set_gridmap_tile(x, y, z, tile):
66 var gridmap = self.get_node("GridMap")
67 var mesh_library = gridmap.mesh_library
68 var mapvec = gridmap.world_to_map(gridmap.to_local(Vector3(x, y, z)))
69
70 gridmap.set_cell_item(mapvec.x, mapvec.y, mapvec.z, mesh_library.find_item_by_name(tile))
diff --git a/racing/lobby.gd b/racing/lobby.gd new file mode 100644 index 0000000..87647bb --- /dev/null +++ b/racing/lobby.gd
@@ -0,0 +1,407 @@
1extends Node
2
3var player_steam_id
4var active_lobby_id = 0
5var active_lobby_members = []
6var next_message_id = 0
7var messages_needing_ack = {}
8var is_vip = false
9var is_ready = false
10var everyone_ready = false
11var members_to_join = []
12var is_starting = false
13
14const MAX_PLAYERS = 250
15const PROTOCOL_VERSION = 2
16const RECIPIENT_BROADCAST_ALL = -1
17const LOBBY_MAP_NAME = "ll1_racing"
18
19const LL1_AREAS = [
20 ["Starting Room", 0, 0, 0],
21 ["Second Room", 0, 0, -15],
22 ["The Traveled", 34, 0, -18],
23 ["The Agreeable", 30, 0, -45],
24 ["The Colorful", 10, 0, -83],
25 ["Suits Area", 0, 0, -78, true],
26 ["Arrow Garden", -93, 1, -93],
27 ["The Wondrous (Table)", -108, 1, -78],
28 ["Courtyard", -64, 0, -71],
29 ["Yellow Backside Nine", -38, 0, -58],
30 ["Hot Crusts Area", -20, 0, -81],
31 ["Crossroads Corner", -28, 0, -54],
32 ["The Discerning", -54, 0, -34, true],
33 ["Green Backside", 22, 0, -94],
34 ["Observant Upstairs", 40, 9, -92, true],
35 ["Eight Room", 95, 15, -28],
36 ["The Perceptive", 60, 9, -57],
37 ["The Tenacious", 0, 0, -43],
38 ["Rainbow", -96, 0, -41],
39 ["The Undeterred", -87, 0, 25, true],
40 ["Directional Gallery", -57, 0, 0],
41 ["The Eyes They See", -54, 0, -23],
42 ["Tower First Floor", -27, 0, -23],
43 ["The Optimistic", 76, 0, -17],
44 ["The Initiated", 63, 0, -0, true],
45 ["Art Gallery", 92, 0, 15],
46 ["Art Gallery Top", 80, 30, 15],
47 ["Lookout", 75, 18, 51],
48 ["Knight Night Room", 37, 0, 7],
49 ["The Seeker", 9, 0, 16, true],
50 ["Hidden Room", 13, 0, 4],
51 ["Owl Hallway", 44, 0, -26],
52 ["Challenge Room", -9, 6, 13],
53 ["Pilgrim Room", -22, 0, 24, true],
54 ["Cellar Replica", -44, 0, 30],
55 ["Elements Area", -61, 0, 40],
56 ["The Artistic", -25, 0, 54, true],
57 ["Outside The Wise", -44, 0, 71],
58 ["The Wise", -72, 0, 72, true],
59 ["The Scientific", -18, 0, 89],
60 ["The Wanderer", 0, 0, 80],
61 ["The Fearless", 18, 10, 90],
62 ["Champion's Rest", 23, 0, 62, true],
63 ["The Steady", 31, 0, 77, true],
64 ["The Bold", 67, 0, 77, true],
65 ["Color Hunt", 45, 0, 69],
66 ["Room Room", 95, 6, 84],
67 ["The Bearer", 61, 0, 51],
68 ["Tower Third Floor", 18, 0, 33],
69 ["Rhyme Room (Cross)", 0, 9, 42],
70 ["Tower Seventh Floor", 0, 37, 64],
71]
72
73
74func _ready():
75 installScriptExtension(ResourceLoader.load("user://maps/racing/load.gd"))
76
77 # P2P solve messages should still be received while paused.
78 set_pause_mode(Node.PAUSE_MODE_PROCESS)
79
80 # Undo the load screen removing our cursor
81 get_tree().get_root().set_disable_input(false)
82 Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
83
84 if global.get_node_or_null("RaceManager") == null:
85 var race_manager_script = load("user://maps/racing/manager.gd")
86 var race_manager = race_manager_script.new()
87 race_manager.name = "RaceManager"
88
89 race_manager.SCRIPT_multiplayer = load("user://maps/racing/multiplayer.gd")
90
91 global.add_child(race_manager)
92
93 var _ignore = get_node("Panel/main_button").connect("pressed", self, "_main_button_pressed")
94 _ignore = get_node("Panel/return_button").connect("pressed", self, "_return_button_pressed")
95
96 var dynamic_font = DynamicFont.new()
97 dynamic_font.font_data = load("res://fonts/Lingo.ttf")
98 dynamic_font.size = 36
99 dynamic_font.outline_color = Color(0, 0, 0, 1)
100 dynamic_font.outline_size = 2
101 get_node("Panel/ItemList").add_font_override("font", dynamic_font)
102
103 get_node("Panel/title").text = "LINGO RACING LOBBY (%s)" % global.save_file
104
105 player_steam_id = Steam.getSteamID()
106
107 _ignore = Steam.connect("lobby_match_list", self, "_on_lobby_match_list")
108 _ignore = Steam.connect("lobby_created", self, "_on_lobby_created")
109 _ignore = Steam.connect("lobby_joined", self, "_on_lobby_joined")
110 _ignore = Steam.connect("lobby_data_update", self, "_on_lobby_data_update")
111 _ignore = Steam.connect("persona_state_change", self, "_on_persona_state_change")
112 _ignore = Steam.connect("p2p_session_request", self, "_on_p2p_session_request")
113
114 _setup_recurring_task(5.0, "_resend_messages_needing_ack")
115 _setup_recurring_task(10.0, "_request_lobby_list")
116
117 _request_lobby_list()
118
119
120func _process(_delta: float) -> void:
121 _read_p2p_packet()
122
123
124func _exit_tree():
125 if active_lobby_id != 0 and !is_starting:
126 Steam.leaveLobby(active_lobby_id)
127 active_lobby_id = 0
128
129
130func _setup_recurring_task(wait_time, function):
131 var timer = Timer.new()
132 timer.set_wait_time(wait_time)
133 timer.set_one_shot(false)
134 timer.connect("timeout", self, function)
135 add_child(timer)
136 timer.start()
137
138
139func _resend_messages_needing_ack():
140 for message_id in messages_needing_ack:
141 var message = messages_needing_ack[message_id]
142 if message["recipient_id"] in active_lobby_members:
143 _send_p2p_packet(message["data"], message["recipient_id"], message["mode"], true)
144 else:
145 messages_needing_ack.erase(message_id)
146
147
148func _request_lobby_list():
149 Steam.addRequestLobbyListDistanceFilter(Steam.LOBBY_DISTANCE_FILTER_WORLDWIDE)
150 Steam.addRequestLobbyListNumericalFilter(
151 "protocol_version", PROTOCOL_VERSION, Steam.LOBBY_COMPARISON_EQUAL
152 )
153 Steam.addRequestLobbyListStringFilter("map", LOBBY_MAP_NAME, Steam.LOBBY_COMPARISON_EQUAL)
154 Steam.addRequestLobbyListStringFilter(
155 "save_file", global.save_file.to_lower(), Steam.LOBBY_COMPARISON_EQUAL
156 )
157 Steam.requestLobbyList()
158
159
160func _on_lobby_match_list(lobbies: Array) -> void:
161 if active_lobby_id != 0 && not active_lobby_id in lobbies:
162 # Not sure why this happens, but it seems to sometimes.
163 lobbies.append(active_lobby_id)
164 var best_lobby_id = 0
165 var best_lobby_size = -1
166 for lobby_id in lobbies:
167 var lobby_size = Steam.getNumLobbyMembers(lobby_id)
168 if (
169 lobby_size > best_lobby_size
170 || (lobby_size == best_lobby_size && lobby_id < best_lobby_id)
171 ):
172 best_lobby_id = lobby_id
173 best_lobby_size = lobby_size
174 if best_lobby_id == 0:
175 Steam.createLobby(Steam.LOBBY_TYPE_INVISIBLE, MAX_PLAYERS)
176 elif best_lobby_id != active_lobby_id:
177 Steam.joinLobby(best_lobby_id)
178 elif best_lobby_size <= 1:
179 _request_lobby_list()
180
181
182func _on_lobby_created(result: int, lobby_id: int) -> void:
183 if result != Steam.RESULT_OK:
184 return
185 var _ignore = Steam.setLobbyData(lobby_id, "map", LOBBY_MAP_NAME)
186 _ignore = Steam.setLobbyData(lobby_id, "protocol_version", str(PROTOCOL_VERSION))
187 _ignore = Steam.setLobbyData(lobby_id, "save_file", global.save_file.to_lower())
188 _ignore = Steam.setLobbyData(lobby_id, "racing_vip", str(player_steam_id))
189 is_vip = true
190 _request_lobby_list()
191
192
193func _on_lobby_joined(lobby_id: int, _permissions: int, _locked: bool, result: int) -> void:
194 if result != Steam.RESULT_OK:
195 return
196 if active_lobby_id != 0 && active_lobby_id != lobby_id:
197 Steam.leaveLobby(active_lobby_id)
198 active_lobby_id = lobby_id
199 if Steam.getLobbyData(lobby_id, "racing_vip") == str(player_steam_id):
200 is_vip = true
201 _update_lobby_members()
202
203
204func _on_lobby_data_update(_lobby_id: int, _member_id: int, _key: int) -> void:
205 _update_lobby_members()
206
207
208func _on_persona_state_change(_persona_id: int, _flag: int) -> void:
209 _update_lobby_members()
210
211
212func _update_lobby_members():
213 if active_lobby_id == 0:
214 return
215 var lobby_size: int = Steam.getNumLobbyMembers(active_lobby_id)
216 var itemlist = get_node("Panel/ItemList")
217 itemlist.clear()
218 active_lobby_members.clear()
219 var temp_everyone_ready = true
220 for i in range(0, lobby_size):
221 var member_id: int = Steam.getLobbyMemberByIndex(active_lobby_id, i)
222 var steam_name: String = Steam.getFriendPersonaName(member_id)
223 itemlist.add_item(steam_name, null, false)
224 active_lobby_members.append(member_id)
225
226 var mem_is_ready = Steam.getLobbyMemberData(active_lobby_id, member_id, "ready") == "true"
227 if mem_is_ready:
228 itemlist.set_item_custom_fg_color(itemlist.get_item_count() - 1, Color.green)
229 else:
230 temp_everyone_ready = false
231 everyone_ready = temp_everyone_ready
232 var main_button = get_node("Panel/main_button")
233 if everyone_ready and is_vip:
234 main_button.text = "START GAME"
235 main_button.disabled = false
236 elif is_ready and is_vip:
237 main_button.text = "START GAME"
238 main_button.disabled = true
239 elif is_ready:
240 main_button.text = "READY"
241 main_button.disabled = true
242 else:
243 main_button.text = "READY"
244 main_button.disabled = false
245
246
247func _on_p2p_session_request(remote_id: int) -> void:
248 if remote_id in active_lobby_members:
249 var _ignore = Steam.acceptP2PSessionWithUser(remote_id)
250
251
252func _read_p2p_packet() -> void:
253 var packet_size: int = Steam.getAvailableP2PPacketSize(0)
254 if packet_size > 0:
255 var packet: Dictionary = Steam.readP2PPacket(packet_size, 0)
256 var remote_id = packet["steam_id_remote"]
257 if remote_id in active_lobby_members:
258 var serialized: PoolByteArray = packet["data"]
259 var data: Dictionary = bytes2var(serialized, false)
260 if "message_id" in data:
261 _send_p2p_packet(
262 {
263 "ack": data["message_id"],
264 },
265 remote_id,
266 Steam.P2P_SEND_RELIABLE_WITH_BUFFERING,
267 false
268 )
269 if "start_x" in data:
270 var race_manager = global.get_node("RaceManager")
271 race_manager.start_pos = [
272 data["start_name"],
273 int(data["start_x"]),
274 int(data["start_y"]),
275 int(data["start_z"])
276 ]
277 race_manager.end_pos = [
278 data["end_name"], int(data["end_x"]), int(data["end_y"]), int(data["end_z"])
279 ]
280
281 is_starting = true
282 _start_game()
283 if "ack" in data:
284 messages_needing_ack.erase(data["ack"])
285
286 if is_starting:
287 members_to_join.erase(remote_id)
288
289 if members_to_join.empty():
290 _start_game()
291
292
293func _send_p2p_packet(data: Dictionary, recipient_id: int, mode: int, needs_ack: bool) -> void:
294 if recipient_id == RECIPIENT_BROADCAST_ALL:
295 for member_id in active_lobby_members:
296 _send_p2p_packet(data.duplicate(), member_id, mode, needs_ack)
297 return
298
299 if needs_ack:
300 var message_id
301 if "message_id" in data:
302 message_id = data["message_id"]
303 else:
304 message_id = next_message_id
305 next_message_id += 1
306 data["message_id"] = message_id
307 if not message_id in messages_needing_ack:
308 messages_needing_ack[message_id] = {
309 "data": data,
310 "recipient_id": recipient_id,
311 "mode": mode,
312 }
313 var serialized: PoolByteArray = []
314 serialized.append_array(var2bytes(data))
315 var _ignore = Steam.sendP2PPacket(recipient_id, serialized, mode)
316
317
318func _main_button_pressed():
319 if everyone_ready and is_vip:
320 get_node("Panel/main_button").disabled = true
321
322 var rng = RandomNumberGenerator.new()
323 rng.randomize()
324
325 var start_pos
326 var end_pos
327 var found = false
328 while !found:
329 var areas_dupe = LL1_AREAS.duplicate()
330 var i = rng.randi_range(0, areas_dupe.size() - 1)
331 start_pos = areas_dupe[i]
332 areas_dupe.remove(i)
333 i = rng.randi_range(0, areas_dupe.size() - 1)
334 end_pos = areas_dupe[i]
335
336 var start_vec = Vector3(start_pos[1], start_pos[2], start_pos[3])
337 var end_vec = Vector3(end_pos[1], end_pos[2], end_pos[3])
338 if start_vec.distance_to(end_vec) > 50 and not (start_pos.size() >= 5 and start_pos[4]):
339 found = true
340
341 members_to_join = active_lobby_members.duplicate()
342 members_to_join.erase(player_steam_id)
343 is_starting = true
344
345 var race_manager = global.get_node("RaceManager")
346 race_manager.start_pos = start_pos
347 race_manager.end_pos = end_pos
348
349 if active_lobby_members.size() == 1:
350 _start_game()
351 else:
352 _send_p2p_packet(
353 {
354 "start_name": start_pos[0],
355 "start_x": str(start_pos[1]),
356 "start_y": str(start_pos[2]),
357 "start_z": str(start_pos[3]),
358 "end_name": end_pos[0],
359 "end_x": str(end_pos[1]),
360 "end_y": str(end_pos[2]),
361 "end_z": str(end_pos[3]),
362 },
363 RECIPIENT_BROADCAST_ALL,
364 Steam.P2P_SEND_RELIABLE,
365 true
366 )
367 else:
368 Steam.setLobbyMemberData(active_lobby_id, "ready", "true")
369 is_ready = true
370 _update_lobby_members()
371
372
373func _return_button_pressed():
374 _exit_tree()
375 fader._fade_start("main_menu")
376
377
378func _start_game():
379 var race_manager = global.get_node("RaceManager")
380 race_manager.lobby_id = active_lobby_id
381
382 # Switch to LL1
383 Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
384 global.map = "level1"
385 global.entry_point = Vector3(
386 race_manager.start_pos[1], race_manager.start_pos[2] + 0.25, race_manager.start_pos[3]
387 )
388 global.entry_rotate = Vector3(0, 0, 0)
389 global.sets_entry_point = true
390 var _discard = get_tree().change_scene("res://scenes/load_screen.tscn")
391
392
393# Adapted from https://gitlab.com/Delta-V-Modding/Mods/-/blob/main/game/ModLoader.gd
394func installScriptExtension(childScript: Resource):
395 # Force Godot to compile the script now.
396 # We need to do this here to ensure that the inheritance chain is
397 # properly set up, and multiple mods can chain-extend the same
398 # class multiple times.
399 # This is also needed to make Godot instantiate the extended class
400 # when creating singletons.
401 # The actual instance is thrown away.
402 childScript.new()
403
404 var parentScript = childScript.get_base_script()
405 var parentScriptPath = parentScript.resource_path
406 global._print("ModLoader: Installing script extension over %s" % parentScriptPath)
407 childScript.take_over_path(parentScriptPath)
diff --git a/racing/manager.gd b/racing/manager.gd new file mode 100644 index 0000000..e2cd728 --- /dev/null +++ b/racing/manager.gd
@@ -0,0 +1,28 @@
1extends Node
2
3var SCRIPT_multiplayer
4
5var start_pos
6var end_pos
7var lobby_id
8
9
10func everyone_ready():
11 var player = get_tree().get_root().get_node("Spatial/player")
12 get_node("/root/Spatial").remove_child(get_node("/root/Spatial/blindfold"))
13 var indicator = player.get_node("pivot/camera/achievement_label")
14 indicator.visible = true
15 indicator.text = "Destination:\n%s" % end_pos[0]
16
17 yield(get_tree().create_timer(10), "timeout")
18
19 indicator.text = "3..."
20 yield(get_tree().create_timer(1), "timeout")
21
22 indicator.text = "2..."
23 yield(get_tree().create_timer(1), "timeout")
24
25 indicator.text = "1..."
26 yield(get_tree().create_timer(0.5), "timeout")
27
28 get_node("/root/Spatial/Multiplayer").send_timeout()
diff --git a/racing/multiplayer.gd b/racing/multiplayer.gd new file mode 100644 index 0000000..1cd3b26 --- /dev/null +++ b/racing/multiplayer.gd
@@ -0,0 +1,293 @@
1extends Node
2
3var player_steam_id
4var player_node
5var active_lobby_id = 0
6var active_lobby_members = {}
7var is_remote_signal = false
8var next_message_id = 0
9var messages_needing_ack = {}
10var panel_nodes
11var is_ready = false
12var is_loaded = false
13var is_activating = false
14var letsgo = false
15var is_victory = false
16var victorious_players = []
17
18const MAX_PLAYERS = 250
19const PROTOCOL_VERSION = 2
20const RECIPIENT_BROADCAST_ALL = -1
21
22
23func _ready():
24 # P2P solve messages should still be received while paused.
25 set_pause_mode(Node.PAUSE_MODE_PROCESS)
26
27 player_steam_id = Steam.getSteamID()
28 player_node = get_node("/root/Spatial/player")
29
30 var _ignore = Steam.connect("lobby_match_list", self, "_on_lobby_match_list")
31 _ignore = Steam.connect("lobby_created", self, "_on_lobby_created")
32 _ignore = Steam.connect("lobby_joined", self, "_on_lobby_joined")
33 _ignore = Steam.connect("lobby_chat_update", self, "_on_lobby_chat_update")
34 _ignore = Steam.connect("lobby_data_update", self, "_on_lobby_data_update")
35 _ignore = Steam.connect("persona_state_change", self, "_on_persona_state_change")
36 _ignore = Steam.connect("p2p_session_request", self, "_on_p2p_session_request")
37
38 _setup_recurring_task(0.1, "_broadcast_player_location")
39 _setup_recurring_task(5.0, "_resend_messages_needing_ack")
40 _setup_recurring_task(10.0, "_request_lobby_list")
41
42 _request_lobby_list()
43
44
45func _process(_delta: float) -> void:
46 _read_p2p_packet()
47
48
49func _exit_tree():
50 if active_lobby_id != 0:
51 Steam.leaveLobby(active_lobby_id)
52
53
54func _setup_recurring_task(wait_time, function):
55 var timer = Timer.new()
56 timer.set_wait_time(wait_time)
57 timer.set_one_shot(false)
58 timer.connect("timeout", self, function)
59 add_child(timer)
60 timer.start()
61
62
63func _broadcast_player_location():
64 if active_lobby_id == 0:
65 return
66 _send_p2p_packet(
67 {
68 "global_transform": player_node.global_transform,
69 },
70 RECIPIENT_BROADCAST_ALL,
71 Steam.P2P_SEND_UNRELIABLE_NO_DELAY,
72 false
73 )
74
75
76func _resend_messages_needing_ack():
77 for message_id in messages_needing_ack:
78 var message = messages_needing_ack[message_id]
79 if message["recipient_id"] in active_lobby_members:
80 _send_p2p_packet(message["data"], message["recipient_id"], message["mode"], true)
81 else:
82 messages_needing_ack.erase(message_id)
83
84
85func _request_lobby_list():
86 Steam.addRequestLobbyListDistanceFilter(Steam.LOBBY_DISTANCE_FILTER_WORLDWIDE)
87 Steam.addRequestLobbyListNumericalFilter(
88 "protocol_version", PROTOCOL_VERSION, Steam.LOBBY_COMPARISON_EQUAL
89 )
90 Steam.addRequestLobbyListStringFilter("map", global.map, Steam.LOBBY_COMPARISON_EQUAL)
91 Steam.addRequestLobbyListStringFilter(
92 "save_file", global.save_file.to_lower(), Steam.LOBBY_COMPARISON_EQUAL
93 )
94 Steam.requestLobbyList()
95
96
97func _on_lobby_match_list(lobbies: Array) -> void:
98 if active_lobby_id != 0 && not active_lobby_id in lobbies:
99 # Not sure why this happens, but it seems to sometimes.
100 lobbies.append(active_lobby_id)
101 var best_lobby_id = 0
102 var best_lobby_size = -1
103 for lobby_id in lobbies:
104 var lobby_size = Steam.getNumLobbyMembers(lobby_id)
105 if (
106 lobby_size > best_lobby_size
107 || (lobby_size == best_lobby_size && lobby_id < best_lobby_id)
108 ):
109 best_lobby_id = lobby_id
110 best_lobby_size = lobby_size
111 if best_lobby_id == 0:
112 Steam.createLobby(Steam.LOBBY_TYPE_INVISIBLE, MAX_PLAYERS)
113 elif best_lobby_id != active_lobby_id:
114 Steam.joinLobby(best_lobby_id)
115 elif best_lobby_size <= 1:
116 _request_lobby_list()
117
118
119func _on_lobby_created(result: int, lobby_id: int) -> void:
120 if result != Steam.RESULT_OK:
121 return
122 var _ignore = Steam.setLobbyData(lobby_id, "map", global.map)
123 _ignore = Steam.setLobbyData(lobby_id, "protocol_version", str(PROTOCOL_VERSION))
124 _ignore = Steam.setLobbyData(lobby_id, "save_file", global.save_file.to_lower())
125 _request_lobby_list()
126
127
128func _on_lobby_joined(lobby_id: int, _permissions: int, _locked: bool, result: int) -> void:
129 if result != Steam.RESULT_OK:
130 return
131 if active_lobby_id != 0 && active_lobby_id != lobby_id:
132 Steam.leaveLobby(active_lobby_id)
133 active_lobby_id = lobby_id
134 if is_loaded:
135 Steam.setLobbyMemberData(active_lobby_id, "loaded", "true")
136 _update_lobby_members()
137
138
139func _on_lobby_chat_update(
140 _lobby_id: int, _member_id: int, _making_change_id: int, _chat_state: int
141) -> void:
142 _update_lobby_members()
143
144
145func _on_lobby_data_update(_lobby_id: int, _member_id: int, _key: int) -> void:
146 _update_lobby_members()
147
148
149func _on_persona_state_change(_persona_id: int, _flag: int) -> void:
150 _update_lobby_members()
151
152
153func _update_lobby_members():
154 if active_lobby_id == 0:
155 return
156 var lobby_size: int = Steam.getNumLobbyMembers(active_lobby_id)
157 var prior_lobby_members = active_lobby_members
158 active_lobby_members = {}
159 var everyone_ready = is_loaded
160 var everyone_activated = is_activating
161 for i in range(0, lobby_size):
162 var member_id: int = Steam.getLobbyMemberByIndex(active_lobby_id, i)
163 if member_id != player_steam_id:
164 var steam_name: String = Steam.getFriendPersonaName(member_id)
165 if member_id in prior_lobby_members:
166 active_lobby_members[member_id] = prior_lobby_members[member_id]
167 else:
168 active_lobby_members[member_id] = (
169 load("res://nodes/multiplayer_avatar.tscn").instance()
170 )
171 add_child(active_lobby_members[member_id])
172 active_lobby_members[member_id].steam_id = member_id
173 active_lobby_members[member_id].steam_name = steam_name
174 if Steam.getLobbyMemberData(active_lobby_id, member_id, "loaded") != "true":
175 everyone_ready = false
176 if Steam.getLobbyMemberData(active_lobby_id, member_id, "timeout") != "true":
177 everyone_activated = false
178 for member in prior_lobby_members.values():
179 if not member.steam_id in active_lobby_members:
180 member.queue_free()
181 if !is_ready and everyone_ready:
182 is_ready = everyone_ready
183 global.get_node("RaceManager").everyone_ready()
184 if !letsgo and everyone_activated:
185 letsgo = true
186 time_to_start()
187
188
189func _on_p2p_session_request(remote_id: int) -> void:
190 if remote_id in active_lobby_members:
191 var _ignore = Steam.acceptP2PSessionWithUser(remote_id)
192
193
194func _read_p2p_packet() -> void:
195 var packet_size: int = Steam.getAvailableP2PPacketSize(0)
196 if packet_size > 0:
197 var packet: Dictionary = Steam.readP2PPacket(packet_size, 0)
198 var remote_id = packet["steam_id_remote"]
199 if remote_id in active_lobby_members:
200 var serialized: PoolByteArray = packet["data"]
201 var data: Dictionary = bytes2var(serialized, false)
202 if "global_transform" in data:
203 _receive_member_location(remote_id, data["global_transform"])
204 if "message_id" in data:
205 _send_p2p_packet(
206 {
207 "ack": data["message_id"],
208 },
209 remote_id,
210 Steam.P2P_SEND_RELIABLE_WITH_BUFFERING,
211 false
212 )
213 if "loaded" in data:
214 _update_lobby_members()
215 if "timeout" in data:
216 _update_lobby_members()
217 if "victory" in data:
218 _someone_victory(Steam.getFriendPersonaName(remote_id))
219 if "ack" in data:
220 messages_needing_ack.erase(data["ack"])
221
222
223func _receive_member_location(member_id: int, global_transform) -> void:
224 var member = active_lobby_members[member_id]
225 member.global_move_to(global_transform)
226 if !member.visible:
227 member.show()
228
229
230func _send_p2p_packet(data: Dictionary, recipient_id: int, mode: int, needs_ack: bool) -> void:
231 if recipient_id == RECIPIENT_BROADCAST_ALL:
232 for member_id in active_lobby_members.keys():
233 _send_p2p_packet(data.duplicate(), member_id, mode, needs_ack)
234 return
235
236 if needs_ack:
237 var message_id
238 if "message_id" in data:
239 message_id = data["message_id"]
240 else:
241 message_id = next_message_id
242 next_message_id += 1
243 data["message_id"] = message_id
244 if not message_id in messages_needing_ack:
245 messages_needing_ack[message_id] = {
246 "data": data,
247 "recipient_id": recipient_id,
248 "mode": mode,
249 }
250 var serialized: PoolByteArray = []
251 serialized.append_array(var2bytes(data))
252 var _ignore = Steam.sendP2PPacket(recipient_id, serialized, mode)
253
254
255func player_loaded():
256 is_loaded = true
257 if active_lobby_id != 0:
258 Steam.setLobbyMemberData(active_lobby_id, "loaded", "true")
259 _send_p2p_packet({"loaded": "true"}, RECIPIENT_BROADCAST_ALL, Steam.P2P_SEND_RELIABLE, true)
260 _update_lobby_members()
261
262
263func send_timeout():
264 is_activating = true
265 Steam.setLobbyMemberData(active_lobby_id, "timeout", "true")
266 _send_p2p_packet({"timeout": "true"}, RECIPIENT_BROADCAST_ALL, Steam.P2P_SEND_RELIABLE, true)
267 _update_lobby_members()
268
269
270func time_to_start():
271 var player = get_tree().get_root().get_node("Spatial/player")
272 player.playable = true
273 get_node("/root/Spatial/label").text = (
274 "Destination: %s" % global.get_node("RaceManager").end_pos[0]
275 )
276 get_node("/root/Spatial").ingame_achieve("Go!")
277
278
279func player_victory():
280 if !is_victory:
281 is_victory = true
282 _send_p2p_packet(
283 {"victory": "true"}, RECIPIENT_BROADCAST_ALL, Steam.P2P_SEND_RELIABLE, true
284 )
285 _someone_victory(Steam.getFriendPersonaName(player_steam_id))
286
287
288func _someone_victory(name):
289 if !victorious_players.has(name):
290 victorious_players.append(name)
291
292 var label = get_node("/root/Spatial/label")
293 label.text = "%s\n%s is #%d" % [label.text, name, victorious_players.size()]