From 0ff0445442cf01e42cad1ff05d40f3c1e6989af5 Mon Sep 17 00:00:00 2001 From: Chris Bell Date: Sat, 11 Jan 2025 20:29:20 -0600 Subject: [PATCH] Added GodotSteamSync addon --- addons/godot_steam_sync/Lobby/lobby.gd | 260 ++++++ addons/godot_steam_sync/Lobby/lobby_member.gd | 34 + .../godot_steam_sync/Lobby/lobby_member.tscn | 55 ++ addons/godot_steam_sync/Lobby/lobby_menu.tscn | 174 ++++ addons/godot_steam_sync/Lobby/main_menu.gd | 24 + addons/godot_steam_sync/Lobby/main_menu.tscn | 83 ++ addons/godot_steam_sync/Media/1.png | Bin 0 -> 4544 bytes addons/godot_steam_sync/Media/1.png.import | 34 + addons/godot_steam_sync/Media/2.png | Bin 0 -> 6433 bytes addons/godot_steam_sync/Media/2.png.import | 34 + addons/godot_steam_sync/Media/image.png | Bin 0 -> 23659 bytes .../godot_steam_sync/Media/image.png.import | 34 + .../Network/RadomeSteamSync/NetworkManager.gd | 55 ++ .../RadomeSteamSync/NetworkPlayerSpawner.gd | 85 ++ .../Network/RadomeSteamSync/P2P.gd | 123 +++ .../RadomeSteamSync/SyncNode/CommandSync.gd | 14 + .../RadomeSteamSync/SyncNode/FuncSync.gd | 15 + .../RadomeSteamSync/SyncNode/PropertySync.gd | 71 ++ .../RadomeSteamSync/SyncNode/RagDollSync.gd | 76 ++ .../SyncNode/RigidBodySync2D.gd | 50 + .../SyncNode/RigidBodySync3D.gd | 54 ++ .../RadomeSteamSync/SyncNode/Synchronizer.gd | 1 + .../SyncNode/TransformSync2D.gd | 127 +++ .../SyncNode/TransformSync3D.gd | 126 +++ .../Network/RadomeSteamSync/SyncNode/Voice.gd | 118 +++ .../RadomeSteamSync/SyncNode/funcSyncIcon.png | Bin 0 -> 15182 bytes .../SyncNode/funcSyncIcon.png.import | 34 + .../SyncNode/propertySyncIcon.png | Bin 0 -> 11505 bytes .../SyncNode/propertySyncIcon.png.import | 34 + .../SyncNode/transformSyncIcon.png | Bin 0 -> 12231 bytes .../SyncNode/transformSyncIcon.png.import | 34 + addons/godot_steam_sync/godot_steam_sync.gd | 20 + addons/godot_steam_sync/plugin.cfg | 7 + godotsteam_sync_example/Test.tscn | 176 ++++ godotsteam_sync_example/fpc/EditorModule.gd | 49 + godotsteam_sync_example/fpc/character.tscn | 854 ++++++++++++++++++ godotsteam_sync_example/fpc/debug.gd | 18 + .../fpc/reticles/reticle_0.tscn | 37 + .../fpc/reticles/reticle_1.tscn | 104 +++ godotsteam_sync_example/test.gd | 1 + project.godot | 5 +- 41 files changed, 3018 insertions(+), 2 deletions(-) create mode 100644 addons/godot_steam_sync/Lobby/lobby.gd create mode 100644 addons/godot_steam_sync/Lobby/lobby_member.gd create mode 100644 addons/godot_steam_sync/Lobby/lobby_member.tscn create mode 100644 addons/godot_steam_sync/Lobby/lobby_menu.tscn create mode 100644 addons/godot_steam_sync/Lobby/main_menu.gd create mode 100644 addons/godot_steam_sync/Lobby/main_menu.tscn create mode 100644 addons/godot_steam_sync/Media/1.png create mode 100644 addons/godot_steam_sync/Media/1.png.import create mode 100644 addons/godot_steam_sync/Media/2.png create mode 100644 addons/godot_steam_sync/Media/2.png.import create mode 100644 addons/godot_steam_sync/Media/image.png create mode 100644 addons/godot_steam_sync/Media/image.png.import create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/NetworkManager.gd create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/NetworkPlayerSpawner.gd create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/P2P.gd create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/CommandSync.gd create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/FuncSync.gd create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/PropertySync.gd create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/RagDollSync.gd create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/RigidBodySync2D.gd create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/RigidBodySync3D.gd create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/Synchronizer.gd create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/TransformSync2D.gd create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/TransformSync3D.gd create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/Voice.gd create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/funcSyncIcon.png create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/funcSyncIcon.png.import create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/propertySyncIcon.png create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/propertySyncIcon.png.import create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/transformSyncIcon.png create mode 100644 addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/transformSyncIcon.png.import create mode 100644 addons/godot_steam_sync/godot_steam_sync.gd create mode 100644 addons/godot_steam_sync/plugin.cfg create mode 100644 godotsteam_sync_example/Test.tscn create mode 100644 godotsteam_sync_example/fpc/EditorModule.gd create mode 100644 godotsteam_sync_example/fpc/character.tscn create mode 100644 godotsteam_sync_example/fpc/debug.gd create mode 100644 godotsteam_sync_example/fpc/reticles/reticle_0.tscn create mode 100644 godotsteam_sync_example/fpc/reticles/reticle_1.tscn create mode 100644 godotsteam_sync_example/test.gd diff --git a/addons/godot_steam_sync/Lobby/lobby.gd b/addons/godot_steam_sync/Lobby/lobby.gd new file mode 100644 index 0000000..afa2fa1 --- /dev/null +++ b/addons/godot_steam_sync/Lobby/lobby.gd @@ -0,0 +1,260 @@ +extends Control + +@onready var lobby_name_txt = $CreateLobbyCont/LobbyNameTxt +@onready var lobby_visibility = $CreateLobbyCont/LobbyVisibilityBtn +@onready var max_members = $CreateLobbyCont/MaxMembersBtn +@onready var lobby_member = preload("res://addons/godot_steam_sync/Lobby/lobby_member.tscn") +@onready var lobby_name_lbl = $LobbyPanel/MarginContainer/LobbyCont/Panel/LobbyNameLbl +@onready var start_btn = $LobbyPanel/MarginContainer/LobbyCont/StartBtn +var im_ready : bool = false +func _ready() -> void: + Steam.join_requested.connect(_on_lobby_join_requested) + #Steam.lobby_chat_update.connect(_on_lobby_chat_update) + Steam.lobby_created.connect(_on_lobby_created) + #Steam.lobby_data_update.connect(_on_lobby_data_update) + #Steam.lobby_invite.connect(_on_lobby_invite) + Steam.lobby_joined.connect(_on_lobby_joined) + #Steam.lobby_match_list.connect(_on_lobby_match_list) + #Steam.lobby_message.connect(_on_lobby_message) + Steam.persona_state_change.connect(_on_persona_change) + Steam.p2p_session_request.connect(_on_p2p_session_request) + Steam.p2p_session_connect_fail.connect(_on_p2p_session_connect_fail) + + + # Check for command line arguments + check_command_line() + +func check_command_line() -> void: + var these_arguments: Array = OS.get_cmdline_args() + + # There are arguments to process + if these_arguments.size() > 0: + + # A Steam connection argument exists + if these_arguments[0] == "+connect_lobby": + + # Lobby invite exists so try to connect to it + if int(these_arguments[1]) > 0: + + # At this point, you'll probably want to change scenes + # Something like a loading into lobby screen + print("Command line lobby ID: %s" % these_arguments[1]) + join_lobby(int(these_arguments[1])) + +#region JoinLobbyRegion +func join_lobby(this_lobby_id: int) -> void: + print("Attempting to join lobby %s" % this_lobby_id) + + # Clear any previous lobby members lists, if you were in a previous lobby + NetworkManager.LOBBY_MEMBERS.clear() + + # Make the lobby join request to Steam + Steam.joinLobby(this_lobby_id) + +func _on_lobby_joined(lobby_id: int, _permissions: int, _locked: bool, response: int): + # If joining succeed, this will be 1 + if response == 1: + # Set this lobby ID as your lobby ID + NetworkManager.LOBBY_ID = lobby_id + # Print the lobby ID to a label + lobby_name_lbl.set_text(Steam.getLobbyData(NetworkManager.LOBBY_ID,"name")) + # Append to output + print("[STEAM] Joined lobby "+str(NetworkManager.LOBBY_ID)+".\n") + # Get the lobby members + get_lobby_members() + # Make the initial handshake + make_p2p_handshake() + change_lobby_ui(true) + + # Else it failed for some reason + else: + # Get the failure reason + var FAIL_REASON: String + match response: + 2: FAIL_REASON = "This lobby no longer exists." + 3: FAIL_REASON = "You don't have permission to join this lobby." + 4: FAIL_REASON = "The lobby is now full." + 5: FAIL_REASON = "Uh... something unexpected happened!" + 6: FAIL_REASON = "You are banned from this lobby." + 7: FAIL_REASON = "You cannot join due to having a limited account." + 8: FAIL_REASON = "This lobby is locked or disabled." + 9: FAIL_REASON = "This lobby is community locked." + 10: FAIL_REASON = "A user in the lobby has blocked you from joining." + 11: FAIL_REASON = "A user you have blocked is in the lobby." + $Frame/Main/Displays/Outputs/Output.append_text("[STEAM] Failed joining lobby "+str(lobby_id)+": "+str(FAIL_REASON)+"\n") + # Reopen the server list + #_on_Open_Lobby_List_pressed() + +func _on_lobby_join_requested(lobby_id: int, friend_id: int): + # Get the lobby owner's name + var OWNER_NAME = Steam.getFriendPersonaName(friend_id) + print("[STEAM] Joining "+str(OWNER_NAME)+"'s lobby...\n") + # Attempt to join the lobby + join_lobby(lobby_id) +#endregion + +func change_lobby_ui(current : bool): + if current: + $CreateLobbyCont.hide() + $LobbyPanel.show() + $MembersPnl.show() + else: + $CreateLobbyCont.show() + $LobbyPanel.hide() + $MembersPnl.hide() + +#region CreateLobbyRegion +func _on_create_lobby_btn_pressed(): + if NetworkManager.LOBBY_ID == 0: + Steam.createLobby(lobby_visibility.get_selected_id(), int(max_members.get_item_text(max_members.get_selected_id()))) + +func _on_lobby_created(connect: int, this_lobby_id: int) -> void: + if connect == 1: + # Set the lobby ID + NetworkManager.LOBBY_ID = this_lobby_id + print("Created a lobby: %s" % NetworkManager.LOBBY_ID ) + # Set this lobby as joinable, just in case, though this should be done by default + Steam.setLobbyJoinable(NetworkManager.LOBBY_ID, true) + # Set some lobby data + Steam.setLobbyData(NetworkManager.LOBBY_ID, "name", lobby_name_txt.text) + Steam.setLobbyData(NetworkManager.LOBBY_ID, "mode", "Mechatronauts") + + # Allow P2P connections to fallback to being relayed through Steam if needed + var set_relay: bool = Steam.allowP2PPacketRelay(true) + + get_lobby_members() + change_lobby_ui(true) +#endregion +func get_lobby_members() -> void: + # Clear your previous lobby list + NetworkManager.LOBBY_MEMBERS.clear() + for MEMBER in $MembersPnl/MarginContainer/ScrollContainer/VBoxContainer.get_children(): + MEMBER.hide() + MEMBER.queue_free() + # Get the number of members from this lobby from Steam + var num_of_members: int = Steam.getNumLobbyMembers(NetworkManager.LOBBY_ID) + + # Get the data of these players from Steam + for this_member in range(0, num_of_members): + # Get the member's Steam ID + var member_steam_id: int = Steam.getLobbyMemberByIndex(NetworkManager.LOBBY_ID, this_member) + + # Get the member's Steam name + var member_steam_name: String = Steam.getFriendPersonaName(member_steam_id) + + # Add them to the list + add_member_to_list(member_steam_id,member_steam_name) + if NetworkManager.STEAM_ID == Steam.getLobbyOwner(NetworkManager.LOBBY_ID): + NetworkManager.IS_READY[member_steam_id] = false + + else: + start_btn.hide() + start_btn.disabled = true + +# A user's information has changed +func _on_persona_change(this_steam_id: int, _flag: int) -> void: + # Make sure you're in a lobby and this user is valid or Steam might spam your console log + if NetworkManager.LOBBY_ID > 0: + print("A user (%s) had information change, update the lobby list" % this_steam_id) + # Update the player list + get_lobby_members() + +func add_member_to_list(steam_id: int, steam_name: String): + print("Adding new player to the list: "+str(steam_id)+" / "+str(steam_name)) + # Add them to the list + NetworkManager.LOBBY_MEMBERS.append({"steam_id":steam_id, "steam_name":steam_name }) + # Instance the lobby member object + var THIS_MEMBER: Object = lobby_member.instantiate() + # Add their Steam name and ID + THIS_MEMBER.name = str(steam_id) + THIS_MEMBER._set_Member(steam_id, steam_name) + # Add the child node + $MembersPnl/MarginContainer/ScrollContainer/VBoxContainer.add_child(THIS_MEMBER) + + +#region P2PHandshake +func make_p2p_handshake() -> void: + print("Sending P2P handshake to the lobby") + P2P._send_P2P_Packet(0,0, {"message": "handshake", "from": NetworkManager.STEAM_ID},Steam.P2P_SEND_RELIABLE) + +func _on_p2p_session_request(remote_id: int) -> void: + # Get the requester's name + var this_requester: String = Steam.getFriendPersonaName(remote_id) + print("%s is requesting a P2P session" % this_requester) + # Accept the P2P session; can apply logic to deny this request if needed + Steam.acceptP2PSessionWithUser(remote_id) + # Make the initial handshake + make_p2p_handshake() +#endregion + +#region LobbyEvents +func _on_p2p_session_connect_fail(lobby_id: int, session_error: int) -> void: + # Note the session errors are: 0 - none, 1 - target user not running the same game, 2 - local user doesn't own app, 3 - target user isn't connected to Steam, 4 - connection timed out, 5 - unused + # If no error was given + if session_error == 0: + print("[WARNING] Session failure with "+str(lobby_id)+" [no error given].") + # Else if target user was not running the same game + elif session_error == 1: + print("[WARNING] Session failure with "+str(lobby_id)+" [target user not running the same game].") + # Else if local user doesn't own app / game + elif session_error == 2: + print("[WARNING] Session failure with "+str(lobby_id)+" [local user doesn't own app / game].") + # Else if target user isn't connected to Steam + elif session_error == 3: + print("[WARNING] Session failure with "+str(lobby_id)+" [target user isn't connected to Steam].") + # Else if connection timed out + elif session_error == 4: + print("[WARNING] Session failure with "+str(lobby_id)+" [connection timed out].") + # Else if unused + elif session_error == 5: + print("[WARNING] Session failure with "+str(lobby_id)+" [unused].") + # Else no known error + else: + print("[WARNING] Session failure with "+str(lobby_id)+" [unknown error "+str(session_error)+"].") + +func _on_lobby_chat_update(this_lobby_id: int, change_id: int, making_change_id: int, chat_state: int) -> void: + # Get the user who has made the lobby change + var changer_name: String = Steam.getFriendPersonaName(change_id) + + # If a player has joined the lobby + if chat_state == Steam.CHAT_MEMBER_STATE_CHANGE_ENTERED: + print("%s has joined the lobby." % changer_name) + + # Else if a player has left the lobby + elif chat_state == Steam.CHAT_MEMBER_STATE_CHANGE_LEFT: + print("%s has left the lobby." % changer_name) + + # Else if a player has been kicked + elif chat_state == Steam.CHAT_MEMBER_STATE_CHANGE_KICKED: + print("%s has been kicked from the lobby." % changer_name) + + # Else if a player has been banned + elif chat_state == Steam.CHAT_MEMBER_STATE_CHANGE_BANNED: + print("%s has been banned from the lobby." % changer_name) + + # Else there was some unknown change + else: + print("%s did... something." % changer_name) + + # Update the lobby now that a change has occurred + get_lobby_members() +#endregion + +func _on_ready_btn_pressed(): + + if NetworkManager.STEAM_ID == Steam.getLobbyOwner(NetworkManager.LOBBY_ID): + im_ready = !im_ready + #for ready in NetworkManager.IS_READY.keys(): + #if ready == NetworkManager.STEAM_ID: + NetworkManager.IS_READY[NetworkManager.STEAM_ID] = im_ready + else: + im_ready = !im_ready + P2P._send_P2P_Packet(0,Steam.getLobbyOwner(NetworkManager.LOBBY_ID),{"TYPE":NetworkManager.TYPES.READY,"steam_id":NetworkManager.STEAM_ID,"ready":im_ready},Steam.P2P_SEND_RELIABLE) + +func _on_start_btn_pressed(): + if NetworkManager.STEAM_ID == Steam.getLobbyOwner(NetworkManager.LOBBY_ID): + var result = NetworkManager.IS_READY.values().all(func(number): return number == true) + if result: + pass + #SceneManager.change_scene("res://godotsteam_sync_example/Test.tscn") + diff --git a/addons/godot_steam_sync/Lobby/lobby_member.gd b/addons/godot_steam_sync/Lobby/lobby_member.gd new file mode 100644 index 0000000..5a4c81d --- /dev/null +++ b/addons/godot_steam_sync/Lobby/lobby_member.gd @@ -0,0 +1,34 @@ +extends Panel + +var STEAM_ID: int = 0 + +# Stored at the class level to enable comparisons when helper functions are called +var AVATAR: Image + +# Is it ready? Do stuff! +func _ready(): + # connect some signals + var SIGNAL_CONNECT: int = Steam.connect("avatar_loaded", Callable(self, "_loaded_Avatar")) + +# Set this player up +func _set_Member(steam_id: int, steam_name: String) -> void: + # Set the ID and username + STEAM_ID = steam_id + $MarginContainer/HBoxContainer/ScrollContainer/UsernameLbl.set_text(steam_name) + # Get the avatar and show it + Steam.getPlayerAvatar(Steam.AVATAR_MEDIUM, STEAM_ID) + +# Load an avatar +func _loaded_Avatar(id: int, this_size: int, buffer: PackedByteArray) -> void: + # Check we're only triggering a load for the right player, and check the data has actually changed + if id == STEAM_ID and (not AVATAR or not buffer == AVATAR.get_data()): + # Create the image and texture for loading + AVATAR = Image.create_from_data(this_size, this_size, false, Image.FORMAT_RGBA8, buffer) + # Apply it to the texture + var AVATAR_TEXTURE: ImageTexture = ImageTexture.create_from_image(AVATAR) + # Set it + $MarginContainer/HBoxContainer/Avatar.set_texture(AVATAR_TEXTURE) + + +func _on_view_btn_pressed(): + Steam.activateGameOverlayToUser("steamid", STEAM_ID) diff --git a/addons/godot_steam_sync/Lobby/lobby_member.tscn b/addons/godot_steam_sync/Lobby/lobby_member.tscn new file mode 100644 index 0000000..96b26dd --- /dev/null +++ b/addons/godot_steam_sync/Lobby/lobby_member.tscn @@ -0,0 +1,55 @@ +[gd_scene load_steps=3 format=3 uid="uid://d26bfvwtqaeqw"] + +[ext_resource type="Script" path="res://addons/godot_steam_sync/Lobby/lobby_member.gd" id="1_vi8x1"] + +[sub_resource type="LabelSettings" id="LabelSettings_x51ve"] +font_size = 10 + +[node name="LobbyMember" type="Panel"] +custom_minimum_size = Vector2(236, 36) +offset_right = 236.0 +offset_bottom = 36.0 +script = ExtResource("1_vi8x1") +metadata/_edit_use_anchors_ = true + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 3 +theme_override_constants/margin_top = 3 +theme_override_constants/margin_right = 3 +theme_override_constants/margin_bottom = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"] +layout_mode = 2 + +[node name="Avatar" type="TextureRect" parent="MarginContainer/HBoxContainer"] +custom_minimum_size = Vector2(30, 30) +layout_mode = 2 +expand_mode = 1 + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="UsernameLbl" type="Label" parent="MarginContainer/HBoxContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +text = "Space War Name" +label_settings = SubResource("LabelSettings_x51ve") +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="ViewBtn" type="Button" parent="MarginContainer/HBoxContainer"] +custom_minimum_size = Vector2(30, 30) +layout_mode = 2 +theme_override_colors/font_color = Color(0, 1, 0, 1) +theme_override_font_sizes/font_size = 14 +text = "View" + +[connection signal="pressed" from="MarginContainer/HBoxContainer/ViewBtn" to="." method="_on_view_btn_pressed"] diff --git a/addons/godot_steam_sync/Lobby/lobby_menu.tscn b/addons/godot_steam_sync/Lobby/lobby_menu.tscn new file mode 100644 index 0000000..09a5952 --- /dev/null +++ b/addons/godot_steam_sync/Lobby/lobby_menu.tscn @@ -0,0 +1,174 @@ +[gd_scene load_steps=2 format=3 uid="uid://cliutsom3dxmu"] + +[ext_resource type="Script" path="res://addons/godot_steam_sync/Lobby/lobby.gd" id="1_2lhov"] + +[node name="LobbyMenu" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_2lhov") + +[node name="Panel" type="Panel" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="MembersPnl" type="Panel" parent="."] +visible = false +layout_mode = 1 +anchors_preset = -1 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -276.0 +offset_top = -262.0 +offset_right = -10.0 +offset_bottom = -12.0 +grow_horizontal = 0 +grow_vertical = 0 +metadata/_edit_use_anchors_ = true + +[node name="MarginContainer" type="MarginContainer" parent="MembersPnl"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="ScrollContainer" type="ScrollContainer" parent="MembersPnl/MarginContainer"] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MembersPnl/MarginContainer/ScrollContainer"] +layout_mode = 2 +alignment = 1 + +[node name="CreateLobbyCont" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -143.0 +offset_top = -162.0 +offset_right = 143.0 +offset_bottom = 162.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="LobbyNameTxt" type="TextEdit" parent="CreateLobbyCont"] +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 +placeholder_text = "Lobby Name" +scroll_smooth = true + +[node name="LobbyVisibilityBtn" type="OptionButton" parent="CreateLobbyCont"] +custom_minimum_size = Vector2(300, 35) +layout_mode = 2 +item_count = 4 +selected = 2 +popup/item_0/text = "PRIVATE" +popup/item_0/id = 0 +popup/item_1/text = "FRIENDS ONLY" +popup/item_1/id = 1 +popup/item_2/text = "PUBLIC" +popup/item_2/id = 2 +popup/item_3/text = "INVISIBLE" +popup/item_3/id = 3 + +[node name="MaxMembersBtn" type="OptionButton" parent="CreateLobbyCont"] +custom_minimum_size = Vector2(300, 35) +layout_mode = 2 +item_count = 4 +selected = 1 +popup/item_0/text = "2" +popup/item_0/id = 0 +popup/item_1/text = "4" +popup/item_1/id = 1 +popup/item_2/text = "8" +popup/item_2/id = 2 +popup/item_3/text = "16" +popup/item_3/id = 3 + +[node name="CreateLobbyBtn" type="Button" parent="CreateLobbyCont"] +custom_minimum_size = Vector2(0, 70) +layout_mode = 2 +text = "Create Lobby" + +[node name="LobbyPanel" type="Panel" parent="."] +visible = false +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -171.0 +offset_top = -148.0 +offset_right = 171.0 +offset_bottom = 172.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="MarginContainer" type="MarginContainer" parent="LobbyPanel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="LobbyCont" type="VBoxContainer" parent="LobbyPanel/MarginContainer"] +layout_mode = 2 + +[node name="Panel" type="Panel" parent="LobbyPanel/MarginContainer/LobbyCont"] +custom_minimum_size = Vector2(0, 60) +layout_mode = 2 + +[node name="LobbyNameLbl" type="Label" parent="LobbyPanel/MarginContainer/LobbyCont/Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_font_sizes/font_size = 27 +text = "Lobby Name" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="Space" type="Control" parent="LobbyPanel/MarginContainer/LobbyCont"] +custom_minimum_size = Vector2(0, 55.06) +layout_mode = 2 + +[node name="ReadyBtn" type="Button" parent="LobbyPanel/MarginContainer/LobbyCont"] +custom_minimum_size = Vector2(0, 80) +layout_mode = 2 +theme_override_font_sizes/font_size = 33 +text = "READY" + +[node name="StartBtn" type="Button" parent="LobbyPanel/MarginContainer/LobbyCont"] +custom_minimum_size = Vector2(0, 80) +layout_mode = 2 +theme_override_font_sizes/font_size = 33 +text = "Start" + +[connection signal="pressed" from="CreateLobbyCont/CreateLobbyBtn" to="." method="_on_create_lobby_btn_pressed"] +[connection signal="pressed" from="LobbyPanel/MarginContainer/LobbyCont/ReadyBtn" to="." method="_on_ready_btn_pressed"] +[connection signal="pressed" from="LobbyPanel/MarginContainer/LobbyCont/StartBtn" to="." method="_on_start_btn_pressed"] diff --git a/addons/godot_steam_sync/Lobby/main_menu.gd b/addons/godot_steam_sync/Lobby/main_menu.gd new file mode 100644 index 0000000..1bfdab5 --- /dev/null +++ b/addons/godot_steam_sync/Lobby/main_menu.gd @@ -0,0 +1,24 @@ +extends Control + +@onready var online_text = $OnlineLbl + + + + + +func _ready(): + + if NetworkManager.IS_ONLINE: + online_text.text = "online" + else: + online_text.text = "offline" + + +func _on_play_btn_pressed(): + get_tree().change_scene_to_file("res://addons/godot_steam_sync/Lobby/lobby_menu.tscn") + +func _on_exit_btn_pressed(): + get_tree().quit() + + + diff --git a/addons/godot_steam_sync/Lobby/main_menu.tscn b/addons/godot_steam_sync/Lobby/main_menu.tscn new file mode 100644 index 0000000..d892083 --- /dev/null +++ b/addons/godot_steam_sync/Lobby/main_menu.tscn @@ -0,0 +1,83 @@ +[gd_scene load_steps=3 format=3 uid="uid://1d2bxfqc4m8x"] + +[ext_resource type="Script" path="res://addons/godot_steam_sync/Lobby/main_menu.gd" id="1_6wvtt"] + +[sub_resource type="SystemFont" id="SystemFont_j72ml"] +font_names = PackedStringArray("Sylfaen") + +[node name="MainMenu" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_6wvtt") + +[node name="Panel" type="Panel" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="OnlineLbl" type="Label" parent="."] +layout_mode = 1 +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -49.0 +offset_top = -23.0 +grow_horizontal = 0 +grow_vertical = 0 +theme_override_font_sizes/font_size = 0 +text = "online" + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -175.0 +offset_top = -35.0 +offset_right = 175.0 +offset_bottom = 35.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="PlayBtn" type="Button" parent="VBoxContainer"] +custom_minimum_size = Vector2(350, 70) +layout_mode = 2 +text = "Play Online +" + +[node name="ExitBtn" type="Button" parent="VBoxContainer"] +custom_minimum_size = Vector2(350, 70) +layout_mode = 2 +text = "Quit" + +[node name="Titel" type="Label" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -277.0 +offset_top = -201.0 +offset_right = 277.0 +offset_bottom = -143.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_fonts/font = SubResource("SystemFont_j72ml") +theme_override_font_sizes/font_size = 56 +text = "GODOT STEAM SYNC" +uppercase = true + +[connection signal="pressed" from="VBoxContainer/PlayBtn" to="." method="_on_play_btn_pressed"] +[connection signal="pressed" from="VBoxContainer/ExitBtn" to="." method="_on_exit_btn_pressed"] diff --git a/addons/godot_steam_sync/Media/1.png b/addons/godot_steam_sync/Media/1.png new file mode 100644 index 0000000000000000000000000000000000000000..a33d446483fb0b6d1b8614f48e5fb755044d18dc GIT binary patch literal 4544 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 z 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 void: + _initialize_Steam() + + if IS_ON_STEAM_DECK: + get_window().mode = Window.MODE_EXCLUSIVE_FULLSCREEN +func _initialize_Steam() -> void: + if Engine.has_singleton("Steam"): + var INIT: Dictionary = Steam.steamInitEx(true,480) + + # If the status isn't one, print out the possible error and quit the program + if INIT['status'] != 0: + print("[STEAM] Failed to initialize: "+str(INIT)+" Shutting down...") + get_tree().quit() + + # Is the user actually using Steam; if false, the app assumes this is a non-Steam version + IS_ON_STEAM = true + # Checking if the app is on Steam Deck to modify certain behaviors + IS_ON_STEAM_DECK = Steam.isSteamRunningOnSteamDeck() + # Acquire information about the user + IS_ONLINE = Steam.loggedOn() + IS_OWNED = Steam.isSubscribed() + STEAM_ID = Steam.getSteamID() + STEAM_USERNAME = Steam.getPersonaName() + + # Check if account owns the game + if IS_OWNED == false: + print("[STEAM] User does not own this game") + # Uncomment this line to close the game if the user does not own the game + get_tree().quit() +func _process(_delta: float) -> void: + if IS_ON_STEAM: + Steam.run_callbacks() + +#endregion diff --git a/addons/godot_steam_sync/Network/RadomeSteamSync/NetworkPlayerSpawner.gd b/addons/godot_steam_sync/Network/RadomeSteamSync/NetworkPlayerSpawner.gd new file mode 100644 index 0000000..925e587 --- /dev/null +++ b/addons/godot_steam_sync/Network/RadomeSteamSync/NetworkPlayerSpawner.gd @@ -0,0 +1,85 @@ +class_name NetworkPlayerSpawner extends Node + +## Where the players will be spawned. +@export var spawn_pos : Node +## The node the players will be in after they are spawned. It has to be at the center of the scene. It has to be named 'Players'. +@export var players_parent_node : Node +## Here you can choose which way each spawned player will move. If you choose an axis, the player born will automatically move one unit away. If the game is 2D, choosing the Z axis means the center. +@export_flags("X" ,"Y" ,"Z") var spawn_distance_axis = 1 + +@export var increase_spawn_distance : Vector3 + +func _ready(): + players_parent_node.name = "Players" + if spawn_pos is Node3D: + for player in NetworkManager.LOBBY_MEMBERS.size(): + var instance_player : Node = NetworkManager.player.instantiate() + instance_player.name = str(NetworkManager.LOBBY_MEMBERS[player]["steam_id"]) + var direction = spawn_check() + match direction: + "CENTER": + instance_player.transform.origin = spawn_pos.transform.origin + Vector3(0, 0, 0) + "X": + instance_player.transform.origin = spawn_pos.transform.origin + Vector3(player + increase_spawn_distance.x, 0, 0) + "Y": + instance_player.transform.origin = spawn_pos.transform.origin + Vector3(0, player + increase_spawn_distance.y, 0) + "Z": + instance_player.transform.origin = spawn_pos.transform.origin + Vector3(0, 0, player + increase_spawn_distance.z) + "XY": + instance_player.transform.origin = spawn_pos.transform.origin + Vector3(player + increase_spawn_distance.x, player + increase_spawn_distance.y, 0) + "XZ": + instance_player.transform.origin = spawn_pos.transform.origin + Vector3(player + increase_spawn_distance.x, 0, player + increase_spawn_distance.z) + "YZ": + instance_player.transform.origin = spawn_pos.transform.origin + Vector3(0, player + increase_spawn_distance.y, player + increase_spawn_distance.z) + "XYZ": + instance_player.transform.origin = spawn_pos.transform.origin + Vector3(player + increase_spawn_distance.x, player + increase_spawn_distance.y, player + increase_spawn_distance.z) + _: + instance_player.transform.origin = spawn_pos.transform.origin + Vector3(0, 0, 0) + players_parent_node.add_child(instance_player) + + if instance_player.name == str(NetworkManager.STEAM_ID): + instance_player.make_owner() + NetworkManager.GAME_STARTED = true + if spawn_pos is Node2D: + for player in NetworkManager.LOBBY_MEMBERS.size(): + var instance_player : Node = NetworkManager.player.instantiate() + instance_player.name = str(NetworkManager.LOBBY_MEMBERS[player]["steam_id"]) + var direction = spawn_check() + + match direction: + "CENTER": + instance_player.position = spawn_pos.position + Vector2(0, 0) + "X": + instance_player.position = spawn_pos.position + Vector2(player + increase_spawn_distance.x, 0) + "Y": + instance_player.position = spawn_pos.position + Vector2(0, player + increase_spawn_distance.y) + "XY": + instance_player.position = spawn_pos.position + Vector2(player + increase_spawn_distance.x, player + increase_spawn_distance.y) + _: + instance_player.position = spawn_pos.position + Vector2(0, 0) + players_parent_node.add_child(instance_player) + if instance_player.name == str(NetworkManager.STEAM_ID): + instance_player.make_owner() + NetworkManager.GAME_STARTED = true + +func spawn_check() -> String: + match spawn_distance_axis: + 0: + return "CENTER" + 1: + return "X" + 2: + return "Y" + 3: + return "XY" + 4: + return "Z" + 5: + return "XZ" + 6: + return "YZ" + 7: + return "XYZ" + _: + return "CENTER" + return "CENTER" diff --git a/addons/godot_steam_sync/Network/RadomeSteamSync/P2P.gd b/addons/godot_steam_sync/Network/RadomeSteamSync/P2P.gd new file mode 100644 index 0000000..a04446f --- /dev/null +++ b/addons/godot_steam_sync/Network/RadomeSteamSync/P2P.gd @@ -0,0 +1,123 @@ +extends Node + +const PACKET_READ_LIMIT: int = 32 + +func _process(_delta: float) -> void: + # Get packets only if lobby is joined + if NetworkManager.LOBBY_ID > 0: + _read_All_P2P_Packets() + + +func _read_P2P_Packet() -> void: + var packet_size0 : int = Steam.getAvailableP2PPacketSize(0) + + + #region Channel0 + if packet_size0 > 0: + var this_packet0 : Dictionary = Steam.readP2PPacket(packet_size0, 0) + + if this_packet0.is_empty() or this_packet0 == null: + print("WARNING: read an empty packet with non-zero size!") + + # Get the remote user's ID + var packet_sender: int = this_packet0['remote_steam_id'] + + # Make the packet data readablev + var packet_code: PackedByteArray = this_packet0['data'] + + # Decompress the array before turning it into a useable dictionary + var READABLE: Dictionary = bytes_to_var(packet_code.decompress_dynamic(-1, FileAccess.COMPRESSION_GZIP)) + + # Append logic here to deal with packet data + if READABLE.has("TYPE"): + handle_packets(READABLE) + + #endregion + + +func _read_All_P2P_Packets(read_count: int = 0) -> void: + if read_count >= PACKET_READ_LIMIT: + return + if Steam.getAvailableP2PPacketSize(0) > 0: + _read_P2P_Packet() + _read_All_P2P_Packets(read_count + 1) +var packet_size : float +func _send_P2P_Packet(channel: int,target: int, packet_data: Dictionary,send_type: int) -> bool: + # Create a data array to send the data through + var this_data: PackedByteArray + + # Compress the PackedByteArray we create from our dictionary using the GZIP compression method + var compressed_data: PackedByteArray = var_to_bytes(packet_data).compress(FileAccess.COMPRESSION_GZIP) + this_data.append_array(compressed_data) + packet_size = this_data.size() + # If sending a packet to everyone + if target == 0: + # If there is more than one user, send packets + if NetworkManager.LOBBY_MEMBERS.size() > 1: + # Loop through all members that aren't you + for this_member in NetworkManager.LOBBY_MEMBERS: + if this_member['steam_id'] != NetworkManager.STEAM_ID: + return Steam.sendP2PPacket(this_member['steam_id'], this_data, send_type, channel) + else: + return Steam.sendP2PPacket(target, this_data, send_type, channel) + return false +func handle_start_packet(READABLE): + # This packet reading when someone ready + if READABLE["TYPE"] == NetworkManager.TYPES.READY: + NetworkManager.IS_READY[READABLE["steam_id"]] = READABLE["ready"] + # This packet reading when lobby leader change_scene + #if READABLE["TYPE"] == NetworkManager.TYPES.START_SCENE: + #SceneManager.change_scene(READABLE["scene"]) + + # + +func handle_event_packets(READABLE): + #if READABLE["TYPE"] == NetworkManager.TYPES.COMMAND: + #print("COMMAND:" + READABLE["method"] + str(READABLE["args"])) + #if READABLE["args"] != null: + #Command.callv(READABLE["method"],READABLE["args"]) + #else: + #Command.call(READABLE["method"]) + + if READABLE['TYPE'] == NetworkManager.TYPES.EVENT: + if READABLE["args"] != null: + get_tree().root.get_node(READABLE["node_path"]).callv(READABLE["method"],READABLE["args"]) + else: + get_tree().root.get_node(READABLE["node_path"]).call(READABLE["method"]) + + +func handle_property_packets(READABLE): + + if READABLE['TYPE'] == NetworkManager.TYPES.TRANFORM_SYNC and NetworkManager.GAME_STARTED: + if READABLE["property"] == "global_position": + get_tree().root.get_node(READABLE["node_path"]).transform_buffer[0] = READABLE + if READABLE["property"] == "rotation": + get_tree().root.get_node(READABLE["node_path"]).transform_buffer[1] = READABLE + if READABLE["property"] == "scale": + get_tree().root.get_node(READABLE["node_path"]).transform_buffer[2] = READABLE + + if READABLE["TYPE"] == NetworkManager.TYPES.RAGDOLL and NetworkManager.GAME_STARTED: + get_tree().root.get_node(READABLE["node_path"]).transform_buffer = READABLE + + if READABLE["TYPE"] == NetworkManager.TYPES.RIGIDBODY_SYNC and NetworkManager.GAME_STARTED: + get_tree().root.get_node(READABLE["node_path"]).sync_data = READABLE + + if READABLE["TYPE"] == NetworkManager.TYPES.PROPERTY and NetworkManager.GAME_STARTED: + if !READABLE["interpolated"]: + get_tree().root.get_node(READABLE["node_path"]).set(READABLE["property"],READABLE["value"]) + else: + var DATA :Array = [READABLE["property"],READABLE["value"]] + get_tree().root.get_node(READABLE["node_path"]).DATA = DATA + ## + + +func handle_voice(READABLE): + if READABLE["TYPE"] == NetworkManager.TYPES.VOICE and NetworkManager.GAME_STARTED: + get_tree().root.get_node(READABLE["node_path"]).process_voice_data(READABLE["voice_data"]) + #await get_tree().create_timer(0.1).timeout + +func handle_packets(READABLE): + handle_start_packet(READABLE) + handle_event_packets(READABLE) + handle_property_packets(READABLE) + handle_voice(READABLE) diff --git a/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/CommandSync.gd b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/CommandSync.gd new file mode 100644 index 0000000..806e28a --- /dev/null +++ b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/CommandSync.gd @@ -0,0 +1,14 @@ +extends Node + +#@onready var loadingScreen = preload("res://addons/godot_steam_sync/SceneChanger/LoadingScreen.tscn") +#func send(method : String,args = null): + #var DATA : Dictionary = {"player_id":NetworkManager.STEAM_ID,"TYPE":NetworkManager.TYPES.COMMAND,"args":args,"method":method} + #P2P._send_P2P_Packet(0,0, DATA,Steam.P2P_SEND_RELIABLE) + # + # +#func start_scene(scene : String): + #NetworkManager.GAME_STARTED = false + #var instance = loadingScreen.instantiate() + #get_parent().add_child(instance) + #instance.change(scene) + # diff --git a/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/FuncSync.gd b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/FuncSync.gd new file mode 100644 index 0000000..0f35acd --- /dev/null +++ b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/FuncSync.gd @@ -0,0 +1,15 @@ +@icon("res://addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/funcSyncIcon.png") +class_name FuncSync extends Synchronizer + +signal FuncCalled(method : String,args) + +func _ready() -> void: + connect("FuncCalled",_on_func_called) + +func call_f(method : String,args = null): + emit_signal("FuncCalled",method,args) + +func _on_func_called(method, args): + if NetworkManager.GAME_STARTED: + var DATA : Dictionary = {"player_id":NetworkManager.STEAM_ID,"TYPE":NetworkManager.TYPES.EVENT,"args":args,"node_path":get_parent().get_path(),"method":method} + P2P._send_P2P_Packet(0,0, DATA,Steam.P2P_SEND_RELIABLE) diff --git a/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/PropertySync.gd b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/PropertySync.gd new file mode 100644 index 0000000..029c1a7 --- /dev/null +++ b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/PropertySync.gd @@ -0,0 +1,71 @@ +@icon("res://addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/propertySyncIcon.png") +class_name PropertySync extends Synchronizer + + +@export_group("SETTINGS","is") +## If true, onlu lobby owner will send the packet. +@export var is_only_lobby_owner : bool = false + + +@export_group("NODES","object") +## Select player if its not player or not inside player you can make 'is_only_lobby_owner' true. +@export var object_player : Node + +@export_group("INTERPOLATION") +@export var is_interpolated :bool = false +@export var interpolation_value : float = 0.1 + +@export_group("") +@export var property_list : PackedStringArray## Hangi ozellikler yollayacaksak + + + +var property_type : Array +var DATA : Array +var path : NodePath +var timer : Timer + +func init_timer(): + timer = Timer.new() + timer.process_callback = Timer.TIMER_PROCESS_PHYSICS + timer.wait_time = 0.1 + add_child(timer) + timer.autostart = true + timer.start() + timer.connect("timeout",_on_timer_timeout) + +func _ready(): + init_timer() + + if !is_interpolated: + path = get_parent().get_path() + set_process(false) + else: + path = get_path() + if is_only_lobby_owner == false and object_player.name != str(NetworkManager.STEAM_ID): + timer.stop() + elif is_only_lobby_owner: + if Steam.getLobbyOwner(NetworkManager.LOBBY_ID) != NetworkManager.STEAM_ID: + timer.stop() + + for property in property_list: + var v = get_parent().get(property) + property_type.append(v) + +func _process(delta): + # Data[0] property Data[1] value + if (!DATA.is_empty()): + get_parent().set(DATA[0],lerp(get_parent().get(DATA[0]),DATA[1],interpolation_value)) + + + +func _on_timer_timeout(): + for property in property_list.size(): + if property_type[property] != get_parent().get(property_list[property]) and NetworkManager.GAME_STARTED: + var DATA : Dictionary = {"player_id":NetworkManager.STEAM_ID,"TYPE":NetworkManager.TYPES.PROPERTY,"value":get_parent().get(property_list[property]),"node_path":path,"property":property_list[property],"interpolated":is_interpolated} + P2P._send_P2P_Packet(0,0, DATA,Steam.P2P_SEND_RELIABLE) + property_type[property] = get_parent().get(property_list[property]) + +func abort(): + timer.stop() + set_process(false) diff --git a/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/RagDollSync.gd b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/RagDollSync.gd new file mode 100644 index 0000000..8416a85 --- /dev/null +++ b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/RagDollSync.gd @@ -0,0 +1,76 @@ +@icon("res://Network/RadomeSteamSync/SyncNode/transformSyncIcon.png") +class_name RagDollSync extends Synchronizer + +@export_group("SETTINGS","is") +## If true, onlu lobby owner will send the packet. +@export var is_only_lobby_owner : bool = false + +@export_group("NODES","object") +## Select player if its not player or not inside player you can make 'is_only_lobby_owner' true. +@export var object_player : Node + +@export var interpolation : float = 0.35 + +signal simulating(status : bool) + +var bones : Array[PhysicalBone3D] +var packet_index_pos : int = 0 +var last_pos : Array[Vector3] +var transform_buffer : Dictionary = {} +var last_index_buffer : int +var timer : Timer + +func init_timer(): + timer = Timer.new() + timer.process_callback = Timer.TIMER_PROCESS_PHYSICS + timer.wait_time = 0.1 + add_child(timer) + timer.connect("timeout",_on_pos_timer_timeout) + +func _ready(): + init_timer() + get_all_bones() + +func get_pos_bone() -> Array[Vector3]: + var pos : Array[Vector3] + for bone in bones: + pos.append(bone.global_transform.origin) + + return pos + +func get_all_bones(): + for i in get_parent().get_children(): + if is_instance_of(i,PhysicalBone3D): + bones.append(i) + +func _on_pos_timer_timeout(): + + if get_pos_bone() != last_pos and NetworkManager.GAME_STARTED: + var pos = get_pos_bone() + var DATA : Dictionary = {"Idx":packet_index_pos + 1,"TYPE":NetworkManager.TYPES.RAGDOLL,"value":pos,"node_path":get_path()} + P2P._send_P2P_Packet(0,0, DATA,Steam.P2P_SEND_UNRELIABLE) + packet_index_pos = packet_index_pos + 1 + last_pos = pos +func _physics_process(delta): + if transform_buffer.has("Idx") and NetworkManager.GAME_STARTED: + if transform_buffer["Idx"] >= last_index_buffer : + set_pos_bone(transform_buffer["value"]) + +func set_pos_bone(DATA): + for i in range(bones.size()): + var lerped_value = lerp(bones[i].global_transform.origin,DATA[i],interpolation) + bones[i].global_transform.origin = lerped_value + last_index_buffer = transform_buffer["Idx"] + +func abort(): + set_process(false) + timer.stop() + +func _on_simulating(status : bool): + if status== true: + if is_only_lobby_owner and Steam.getLobbyOwner(NetworkManager.LOBBY_ID) == NetworkManager.STEAM_ID: + timer.start() + elif !is_only_lobby_owner and str(NetworkManager.STEAM_ID) == object_player.name: + timer.start() + else: + timer.stop() diff --git a/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/RigidBodySync2D.gd b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/RigidBodySync2D.gd new file mode 100644 index 0000000..5d8f398 --- /dev/null +++ b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/RigidBodySync2D.gd @@ -0,0 +1,50 @@ +class_name RigidBodySync2D extends Synchronizer + +@export var interpolation_value = 0.3 + +var packet_index: int = 0 +var state_data = null +var last_index : int = 0 +var last_pos : Vector3 = Vector3.ZERO +var pTimer : Timer + +func init_timer(): + pTimer = Timer.new() + pTimer.process_callback = Timer.TIMER_PROCESS_PHYSICS + pTimer.wait_time = 1.0 / 30.0 + add_child(pTimer) + pTimer.autostart = true + pTimer.start() + pTimer.connect("timeout",_on_timeout) + +func _ready(): + init_timer() + if Steam.getLobbyOwner(NetworkManager.LOBBY_ID) != NetworkManager.STEAM_ID: + pTimer.stop() + +func _on_timeout(): + if get_parent().position != last_pos and NetworkManager.GAME_STARTED: + var DATA : Dictionary = {"Idx":packet_index,"player_id":NetworkManager.STEAM_ID,"TYPE":NetworkManager.TYPES.RIGIDBODY_SYNC,"value":get_parent().linear_velocity,"node_path":get_path(),} + P2P._send_P2P_Packet(0,0, DATA,Steam.P2P_SEND_UNRELIABLE) + packet_index = packet_index + 1 + last_pos = get_parent().linear_velocity + +func update_physics_values(): + var target_linear_velocity = state_data["value"][0] + var target_angular_velocity = state_data["value"][1] + var target_position = state_data["value"][2] + var target_rotation = state_data["value"][3] + # Linear velocity için Lerp + get_parent().linear_velocity = lerp(get_parent().linear_velocity, target_linear_velocity, interpolation_value) + # Angular velocity için Lerp + get_parent().angular_velocity = lerp(get_parent().angular_velocity, target_angular_velocity, interpolation_value) + # Position için Lerp + get_parent().position = lerp(get_parent().position, target_position, interpolation_value) + # Rotation için Lerp (Quaternion için) + get_parent().rotation = lerp(get_parent().rotation , target_rotation, interpolation_value) + +func _physics_process(delta: float) -> void: + if state_data != null and NetworkManager.GAME_STARTED: + if state_data["Idx"] >= last_index : + update_physics_values() + last_index = state_data["Idx"] diff --git a/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/RigidBodySync3D.gd b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/RigidBodySync3D.gd new file mode 100644 index 0000000..40effcc --- /dev/null +++ b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/RigidBodySync3D.gd @@ -0,0 +1,54 @@ +class_name RigidBodySync3D extends Synchronizer + +@export var interpolation_value = 0.3 + +var packet_index: int = 0 +var state_data = null +var last_index : int = 0 +var last_pos : Vector3 = Vector3.ZERO +var pTimer : Timer + +func init_timer(): + pTimer = Timer.new() + pTimer.process_callback = Timer.TIMER_PROCESS_PHYSICS + pTimer.wait_time = 1.0 /30.0 + add_child(pTimer) + pTimer.autostart = true + pTimer.start() + pTimer.connect("timeout",_on_timer_timeout) + +func _ready(): + init_timer() + if Steam.getLobbyOwner(NetworkManager.LOBBY_ID) != NetworkManager.STEAM_ID: + pTimer.stop() + +func _on_timer_timeout(): + if get_parent().position != last_pos and NetworkManager.GAME_STARTED: + var DATA : Dictionary = {"Idx":packet_index,"player_id":NetworkManager.STEAM_ID,"TYPE":NetworkManager.TYPES.RIGIDBODY_SYNC,"value":[get_parent().linear_velocity,get_parent().angular_velocity,get_parent().position,get_parent().rotation],"node_path":get_path()} + P2P._send_P2P_Packet(0,0, DATA,Steam.P2P_SEND_UNRELIABLE) + packet_index = packet_index + 1 + last_pos = get_parent().position + + + +func update_physics_values(): + var target_linear_velocity = state_data["value"][0] + var target_angular_velocity = state_data["value"][1] + var target_position = state_data["value"][2] + var target_rotation = state_data["value"][3] + # Linear velocity için Lerp + get_parent().linear_velocity = lerp(get_parent().linear_velocity, target_linear_velocity, interpolation_value) + # Angular velocity için Lerp + get_parent().angular_velocity = lerp(get_parent().angular_velocity, target_angular_velocity, interpolation_value) + # Position için Lerp + get_parent().position = lerp(get_parent().position, target_position, interpolation_value) + # Rotation için Lerp (Quaternion için) + get_parent().rotation = lerp(get_parent().rotation , target_rotation, interpolation_value) + +func _physics_process(delta: float) -> void: + await get_tree().create_timer(0.1).timeout + if state_data != null and NetworkManager.GAME_STARTED: + if state_data["Idx"] >= last_index : + update_physics_values() + + last_index = state_data["Idx"] diff --git a/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/Synchronizer.gd b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/Synchronizer.gd new file mode 100644 index 0000000..52d8ad2 --- /dev/null +++ b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/Synchronizer.gd @@ -0,0 +1 @@ +class_name Synchronizer extends Node diff --git a/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/TransformSync2D.gd b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/TransformSync2D.gd new file mode 100644 index 0000000..662267e --- /dev/null +++ b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/TransformSync2D.gd @@ -0,0 +1,127 @@ +@icon("res://addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/transformSyncIcon.png") +class_name TransformSync2D extends Synchronizer + +@export_group("SETTINGS","is") +## If true, onlu lobby owner will send the packet. +@export var is_only_lobby_owner : bool = false + +@export_group("NODES","object") +## Select player if its not player or not inside player you can make 'is_only_lobby_owner' true. +@export var object_player : Node + +@export_group("") +@export var Position : bool = true +@export var Rotation : bool = false +@export var Scale : bool = false + + +var packet_index_pos : int = 0 +var packet_index_rot : int = 0 +var packet_index_scale : int = 0 + + + +var last_pos : Vector2 = Vector2.ZERO +var last_rot : float = 0 +var last_scale : Vector2 = Vector2.ZERO + +var transform_buffer : Array = [null,null,null] +var last_index_buffer : PackedInt32Array = [0,0,0] + +var posTimer : Timer +var rotTimer : Timer +var sclTimer : Timer + + +func init_pos_timer(): + posTimer = Timer.new() + posTimer.process_callback = Timer.TIMER_PROCESS_PHYSICS + posTimer.wait_time = 0.1 + add_child(posTimer) + posTimer.autostart = true + posTimer.start() + posTimer.connect("timeout",_on_pos_timer_timeout) +func init_rot_timer(): + rotTimer = Timer.new() + rotTimer.process_callback = Timer.TIMER_PROCESS_PHYSICS + rotTimer.wait_time = 0.1 + add_child(rotTimer) + rotTimer.autostart = true + rotTimer.start() + rotTimer.connect("timeout",_on_rot_timer_timeout) +func init_scl_timer(): + sclTimer = Timer.new() + sclTimer.process_callback = Timer.TIMER_PROCESS_PHYSICS + sclTimer.wait_time = 0.1 + add_child(sclTimer) + sclTimer.autostart = true + sclTimer.start() + sclTimer.connect("timeout",_on_scale_timer_timeout) + +func _ready(): + init_pos_timer() + init_rot_timer() + init_scl_timer() + + if is_only_lobby_owner == false and object_player.name != str(NetworkManager.STEAM_ID): + posTimer.stop() + rotTimer.stop() + sclTimer.stop() + elif is_only_lobby_owner: + if Steam.getLobbyOwner(NetworkManager.LOBBY_ID) != NetworkManager.STEAM_ID: + posTimer.stop() + rotTimer.stop() + sclTimer.stop() + + + if !Position: + posTimer.stop() + if !Rotation: + rotTimer.stop() + if !Scale: + sclTimer.stop() + +func _on_pos_timer_timeout(): + if get_parent().global_position != last_pos and NetworkManager.GAME_STARTED: + var DATA : Dictionary = {"Idx":packet_index_pos + 1,"player_id":NetworkManager.STEAM_ID,"TYPE":NetworkManager.TYPES.TRANFORM_SYNC,"value":get_parent().global_position,"node_path":get_path(),"property":"global_position"} + P2P._send_P2P_Packet(0,0, DATA,Steam.P2P_SEND_UNRELIABLE) + packet_index_pos = packet_index_pos + 1 + last_pos = get_parent().global_position + +func _on_rot_timer_timeout(): + if get_parent().rotation != last_rot and NetworkManager.GAME_STARTED: + var DATA : Dictionary = {"Idx":packet_index_rot + 1,"player_id":NetworkManager.STEAM_ID,"TYPE":NetworkManager.TYPES.TRANFORM_SYNC,"value":get_parent().rotation,"node_path":get_path(),"property":"rotation"} + P2P._send_P2P_Packet(0,0, DATA,Steam.P2P_SEND_UNRELIABLE) + packet_index_rot = packet_index_rot + 1 + last_rot = get_parent().rotation + +func _on_scale_timer_timeout(): + if get_parent().scale != last_scale and NetworkManager.GAME_STARTED: + var DATA : Dictionary = {"Idx":packet_index_scale + 1,"player_id":NetworkManager.STEAM_ID,"TYPE":NetworkManager.TYPES.TRANFORM_SYNC,"value":get_parent().scale,"node_path":get_path(),"property":"scale"} + P2P._send_P2P_Packet(0,0, DATA,Steam.P2P_SEND_UNRELIABLE) + packet_index_scale = packet_index_scale + 1 + last_scale = get_parent().scale + +func _process(delta): + if transform_buffer[0] != null and NetworkManager.GAME_STARTED: + if transform_buffer[0]["Idx"] >= last_index_buffer[0] : + var lerped_value = lerp(get_parent().get(transform_buffer[0]["property"]),transform_buffer[0]["value"],0.1) + get_parent().set(transform_buffer[0]["property"],lerped_value) + last_index_buffer[0] = transform_buffer[0]["Idx"] + # Rotation + if transform_buffer[1] != null and NetworkManager.GAME_STARTED: + if transform_buffer[1]["Idx"] >= last_index_buffer[1]: + var lerped_value = lerp(get_parent().get(transform_buffer[1]["property"]),transform_buffer[1]["value"],0.1) + get_parent().set(transform_buffer[1]["property"],lerped_value) + last_index_buffer[1] = transform_buffer[1]["Idx"] + # Scale + if transform_buffer[2] != null and NetworkManager.GAME_STARTED: + if transform_buffer[2]["Idx"] >= last_index_buffer[2]: + var lerped_value = lerp(get_parent().get(transform_buffer[2]["property"]),transform_buffer[2]["value"],0.1) + get_parent().set(transform_buffer[2]["property"],lerped_value) + last_index_buffer[2] = transform_buffer[2]["Idx"] +func abort(): + posTimer.stop() + rotTimer.stop() + sclTimer.stop() + set_process(false) diff --git a/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/TransformSync3D.gd b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/TransformSync3D.gd new file mode 100644 index 0000000..265d3e7 --- /dev/null +++ b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/TransformSync3D.gd @@ -0,0 +1,126 @@ +@icon("res://addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/transformSyncIcon.png") +class_name TransformSync3D extends Synchronizer + +@export_group("SETTINGS","is") +## If true, onlu lobby owner will send the packet. +@export var is_only_lobby_owner : bool = false + +@export_group("NODES","object") +@export var object_player : Node ## Select player if its not player or not inside player you can make 'is_only_lobby_owner' true. + +@export_group("") +@export var Position : bool = true +@export var Rotation : bool = false +@export var Scale : bool = false + + +var packet_index_pos : int = 0 +var packet_index_rot : int = 0 +var packet_index_scale : int = 0 + + + +var last_pos : Vector3 = Vector3.ZERO +var last_rot : Vector3 = Vector3.ZERO +var last_scale : Vector3 = Vector3.ZERO + +var transform_buffer : Array = [null,null,null] +var last_index_buffer : PackedInt32Array = [0,0,0] + +var posTimer : Timer +var rotTimer : Timer +var sclTimer : Timer + + +func init_pos_timer(): + posTimer = Timer.new() + posTimer.process_callback = Timer.TIMER_PROCESS_PHYSICS + posTimer.wait_time = 0.1 + add_child(posTimer) + posTimer.autostart = true + posTimer.start() + posTimer.connect("timeout",_on_pos_timer_timeout) +func init_rot_timer(): + rotTimer = Timer.new() + rotTimer.process_callback = Timer.TIMER_PROCESS_PHYSICS + rotTimer.wait_time = 0.1 + add_child(rotTimer) + rotTimer.autostart = true + rotTimer.start() + rotTimer.connect("timeout",_on_rot_timer_timeout) +func init_scl_timer(): + sclTimer = Timer.new() + sclTimer.process_callback = Timer.TIMER_PROCESS_PHYSICS + sclTimer.wait_time = 0.1 + add_child(sclTimer) + sclTimer.autostart = true + sclTimer.start() + sclTimer.connect("timeout",_on_scale_timer_timeout) + +func _ready(): + init_pos_timer() + init_rot_timer() + init_scl_timer() + + if is_only_lobby_owner == false and object_player.name != str(NetworkManager.STEAM_ID): + posTimer.stop() + rotTimer.stop() + sclTimer.stop() + elif is_only_lobby_owner: + if Steam.getLobbyOwner(NetworkManager.LOBBY_ID) != NetworkManager.STEAM_ID: + posTimer.stop() + rotTimer.stop() + sclTimer.stop() + + + if !Position: + posTimer.stop() + if !Rotation: + rotTimer.stop() + if !Scale: + sclTimer.stop() + +func _on_pos_timer_timeout(): + if get_parent().global_position != last_pos and NetworkManager.GAME_STARTED: + var DATA : Dictionary = {"Idx":packet_index_pos + 1,"player_id":NetworkManager.STEAM_ID,"TYPE":NetworkManager.TYPES.TRANFORM_SYNC,"value":get_parent().global_position,"node_path":get_path(),"property":"global_position"} + P2P._send_P2P_Packet(0,0, DATA,Steam.P2P_SEND_UNRELIABLE) + packet_index_pos = packet_index_pos + 1 + last_pos = get_parent().global_position + +func _on_rot_timer_timeout(): + if get_parent().rotation != last_rot and NetworkManager.GAME_STARTED: + var DATA : Dictionary = {"Idx":packet_index_rot + 1,"player_id":NetworkManager.STEAM_ID,"TYPE":NetworkManager.TYPES.TRANFORM_SYNC,"value":get_parent().rotation,"node_path":get_path(),"property":"rotation"} + P2P._send_P2P_Packet(0,0, DATA,Steam.P2P_SEND_UNRELIABLE) + packet_index_rot = packet_index_rot + 1 + last_rot = get_parent().rotation + +func _on_scale_timer_timeout(): + if get_parent().scale != last_scale and NetworkManager.GAME_STARTED: + var DATA : Dictionary = {"Idx":packet_index_scale + 1,"player_id":NetworkManager.STEAM_ID,"TYPE":NetworkManager.TYPES.TRANFORM_SYNC,"value":get_parent().scale,"node_path":get_path(),"property":"scale"} + P2P._send_P2P_Packet(0,0, DATA,Steam.P2P_SEND_UNRELIABLE) + packet_index_scale = packet_index_scale + 1 + last_scale = get_parent().scale + +func _process(delta): + if transform_buffer[0] != null and NetworkManager.GAME_STARTED: + if transform_buffer[0]["Idx"] >= last_index_buffer[0] : + var lerped_value = lerp(get_parent().get(transform_buffer[0]["property"]),transform_buffer[0]["value"],0.1) + get_parent().set(transform_buffer[0]["property"],lerped_value) + last_index_buffer[0] = transform_buffer[0]["Idx"] + # Rotation + if transform_buffer[1] != null and NetworkManager.GAME_STARTED: + if transform_buffer[1]["Idx"] >= last_index_buffer[1]: + var lerped_value = lerp(get_parent().get(transform_buffer[1]["property"]),transform_buffer[1]["value"],0.1) + get_parent().set(transform_buffer[1]["property"],lerped_value) + last_index_buffer[1] = transform_buffer[1]["Idx"] + # Scale + if transform_buffer[2] != null and NetworkManager.GAME_STARTED: + if transform_buffer[2]["Idx"] >= last_index_buffer[2]: + var lerped_value = lerp(get_parent().get(transform_buffer[2]["property"]),transform_buffer[2]["value"],0.1) + get_parent().set(transform_buffer[2]["property"],lerped_value) + last_index_buffer[2] = transform_buffer[2]["Idx"] +func abort(): + posTimer.stop() + rotTimer.stop() + sclTimer.stop() + set_process(false) diff --git a/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/Voice.gd b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/Voice.gd new file mode 100644 index 0000000..b9c91f7 --- /dev/null +++ b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/Voice.gd @@ -0,0 +1,118 @@ +extends Synchronizer +class_name VoiceSync + +var current_sample_rate: int = 48000 +var local_playback : AudioStreamGeneratorPlayback = null +var local_voice_buffer: PackedByteArray = PackedByteArray() +var use_optimal_sample_rate: bool = false +var DATA : Dictionary +const REF_SAMPLE_RATE : int = 48000 + +@export var loopback_enabled : bool = false +@export var audio_node : AudioStreamPlayer +@export var voice_key : String = "push_to_talk" + +var streamplay : AudioStreamGenerator +var playback_stream : AudioStreamGeneratorPlayback + +func _ready(): + change_voice_settings(true) + DATA = { + "steam_id": NetworkManager.STEAM_ID, + "TYPE": NetworkManager.TYPES.VOICE, + "node_path": get_path(), + "voice_data": {} + } + + if NetworkManager.LOBBY_MEMBERS.size() > 1: + loopback_enabled = false + if audio_node != null: + audio_node.stream.mix_rate = current_sample_rate + audio_node.play() + local_playback = audio_node.get_stream_playback() + + +func change_voice_settings(push_to_talk :bool): + + if push_to_talk: + Steam.setInGameVoiceSpeaking(NetworkManager.STEAM_ID, false) + Steam.stopVoiceRecording() + set_process_input(true) + else: + Steam.setInGameVoiceSpeaking(NetworkManager.STEAM_ID, true) + Steam.startVoiceRecording() + set_process_input(false) + +func record(voice : bool): + Steam.setInGameVoiceSpeaking(NetworkManager.STEAM_ID, voice) + + if voice: + Steam.startVoiceRecording() + else: + Steam.stopVoiceRecording() + +func check_for_voice() -> void: + var available_voice: Dictionary = Steam.getAvailableVoice() + if available_voice['result'] == Steam.VOICE_RESULT_OK and available_voice['buffer'] > 0: + + var voice_data: Dictionary = Steam.getVoice() + if voice_data['result'] == Steam.VOICE_RESULT_OK: + + DATA["voice_data"] = voice_data + + P2P._send_P2P_Packet(0,0, DATA,Steam.P2P_SEND_RELIABLE) + if loopback_enabled: + process_voice_data(voice_data) + + +func _process(_delta: float) -> void: + + + if get_parent().name == str(NetworkManager.STEAM_ID): + check_for_voice() + if Input.is_action_pressed(voice_key): + record(true) + + if Input.is_action_just_released(voice_key): + record(false) + +func get_sample_rate() -> void: + var optimal_sample_rate: int = Steam.getVoiceOptimalSampleRate() + # SpaceWar uses 11000 for sample rate?! + # If are using Steam's "optimal" rate, set it; otherwise we default to 48000 + if use_optimal_sample_rate: + current_sample_rate = optimal_sample_rate + else: + current_sample_rate = 48000 + + +func process_voice_data(voice_data: Dictionary) -> void: + get_sample_rate() + var pitch : float = float(current_sample_rate)/REF_SAMPLE_RATE + audio_node.set_pitch_scale(pitch) + + var decompressed_voice: Dictionary = Steam.decompressVoice( + voice_data['buffer'], + current_sample_rate) + + if ( + not decompressed_voice['result'] == Steam.VOICE_RESULT_OK + + ): + return + + if local_playback != null: + if local_playback.get_frames_available() <= 0: + return + + local_voice_buffer = decompressed_voice['uncompressed'] + + + for i: int in range(0, mini(local_playback.get_frames_available() * 2, local_voice_buffer.size()), 2): + var raw_value = local_voice_buffer.decode_s16(i) + # Convert the 16-bit integer to a float on from -1 to 1 + var amplitude: float = float(raw_value) / 32768.0 + local_playback.push_frame(Vector2(amplitude, amplitude)) + #local_voice_buffer.remove_at(0) + #local_voice_buffer.remove_at(0) + diff --git a/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/funcSyncIcon.png b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/funcSyncIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..2796fc08ea34c4db21b70f77a9e14f37a61c4e61 GIT binary patch literal 15182 zcmdVBi9eKI^f*42kYp)}>>81>WMmmxB56pLM6wry>`W$P%@WBn_I)P_WsR{E6*3s( z&15IZ2&1eqmhT<){ro<^KjG{3>eVyPbMLw5oO||rPn5we?c>a6m?03zam0;lMi2-s zJ@p?G19*d^l}`cx(R*tkjG4gS6DGT75Qq>2aqX({5b2WrrcqE3dQ3-yn}%p+?4jA2&cVfe?MQk*^XLo4x=+nVzbndq*Fxly4J2Hz&(WLIN5UXkq%^Na<4LCf7Ge@*Rc ztvecw>Tw>F@XX4i{B-ZdpI~#-T=&KbuogzvoC~k*%Za*Yl9JvvOFefJG+Fo@C&~9t z+Tgxj%msSK%+b|5E1nHON3-eFeQw^HBP1i6jQmwRG+jT0&fwE!lvwB;%K;`+sD(%Q z-jJ>w1Y6Iey6>irYi_y^OV`yn;U_=Gg+N?4`YuHHjQDqa&a})p2|pHWp(3$;r-U@7 zQ`qzHxkD8jKa+B&dT_n6bN8r&S=qKXdYK1|lJ^Ygw>z*>w(Von(Ebp6iCyT~=<1Mk z(qgGpvW5c<-b4Yrdv^X}#?~JG>aWKv^h^;LVSLZ-{Eq?chtFAeVsTHFR`YdS1E0+XUK!VrY)_NUCj9~9NrKk*ds{*ECDLUKLL zH`X>XNVp)nCyY7;nR&OgF<998IlafE#ge>he;E=uXvidJRlW04oKOd-7Y{SfNNAZE zpPgrkU?u3j(+Jl1)Ag-U&3mEc@T(@psI9L4$p#|->H3LE_B?~Wd-Xdf5WCutU}$$y zP>@Xn&r0+3*Zto78k92i`~I7;n1;04b0}8Gr)k2}a*OS3ui(AvpTT&uCVn~^W_$B) z{7trqKH5BPi;Th3Bd7A*_$^LY%7rFU~{zl>?^MQtvV8pgB)E$3idO zT$569@BBy&kT`>jZYIhOGg;u7bgJ1t_?s>z_M{jOt)nfu4whQshbHk&gkGR0G1bkL zTazlCKe8Xo3-8f~I}9`*uw)kw{uU(i{g+Y&2izDc%lAeljeRVnPY1MJQvT_4idYNeLURUr!Ng<4tPzDC($4NFW zJC$ka*6+dV9J@PH#y4x@)0>V1w75T^6xy1>S};w<1!ER^o>9IH$x9;-=z_a*>Xm+) zSJx12?mxDW4bFZXec_%J_D4o5{wkbeicP> zKpu|F2EN;Wv(=dY042f@%0i{*4ze(nsaKo_8G{iHHf-GgjRPCP2ftpLZ>*>_mdezr z(JkYj#6q(aKeAH6(_1E)vp0q}gdZ_wD#@$6rg%yxVx-WH$tH3<_tneoWc(%gQMwPD zwJHBIFXnUcgU9f1e=f1#B$(+EV@yQ&dUg|tB4+{2m~*n09y=IbWLV$8QEsMlw$4#_ zG3yg@rFLm=sqE#6(`CT77w_fpEq|bk{~Tg8L$6o#KWpYCBYJ24bI8R$wmJdVP4Eo2 zp*?p`gzuPW$C64?<$-ykNq8Sko}z`sM!F?CTzddu@cx=^*n@n#w3Z;VC*QB@>sFl- z)r5?B4i41WJF_0oF6`oF`s~Ck+Wyu+7RL=fDSul9vT9(*LO&}rcVgx)U{fq$lgT-L zQ|$Wu99}6pI>d&huK2<{)|?#l6y0#lS=Y3$-#^#N$Xsp}{SLp=i@xV2*cKZN3D z{WL3n>~0f7r{ej$S`K-U4){NPsiX}*J|umdztB;!?2T567{@IxF&<=9y-P3l`+^ma zQmwfX710)hYKO=^j96k2w%&2lI54X0EDzOZxm-tM8*?!i@JzeZtNIpNxdcwugiH-{8JLdzvH z{LNekvQkS=@w=^JHRiiRF9c|jShU*J^JtT(!o;qA^@-igRR#C7`X0W-AZ1iaSXfLF=I^KmVz?LEdT7{g&wFYbGE(gcot#vSq?uFvjuhM zG$G+e=DSN=KYYAMe2KOvR6;pal-N02|B}I60&KLdXn-gxHpj?J>EGks9;d3a9Bp{A`3sDgOXQAB$a{av^-m_l`-;+*AtaJ`U5E zXR0V?40Zd5QM*i=JSr^e@oFs(C9IGAo+%ss1km9_RtylEg~2z+om#O-@QKJuZ}|_K z4@0ra-xV;!k$sn}L@#f#W((I=@-a32<5RTlBNAkGI40)=T*J{)fqh-)FQ#G0Rjj51 zcN?k-e^g`fRY)FDhpMhjLQphT8Wr(-73il1W0z4?@?VKC7*=xNco#Fqs>#@;!**z^ z*da0{mE25z|5ggVgxXbCkMei|oCCzCq>zzTC+wtVHyD)>E5Ibqaf=0P>3c`Gk5Jw< zMJFIa5Pf_C9gS%F`2ss8h~t1K_1Oj3Ziq`&Ap@8^LVf?Wtnm!9QhIVmAB04WPc~r@lu!EmFs0562G-Q{kWw;S{=3;a6%= z;j5cdDYraaw|Sh(+7>F#sCtXTXDZI9%K!a5P93j8#Tk`n!4NvSY7N%5|4}-eau9m3 zN0ax|PLkyo6ffd9Hq zL<8}b;-66io}U{#*6%1+i;bMNkTs3koLX2cg^byZuf?V>lga*RBV>$EYHcJ|jl71e zzoa*)hpZpK$v08XA>#I$Hx|eCcH=O^#ZFCRguWjEA4pklxb)u&UGQY>V94|X-H4p} zZuyIEeTuK@kKL2y%Lw@VZJ#n$H$PhE9QUk%lqMc+CAGi=OT=D?uD7}Z^WQz7Xj3-o z1t$U`(tG1;OR;L30b_w=@>tNyY+$Qv_HkZ$aRkP{*QINMm_8EEeG2m3rIhZ&5682j z8nwN{n3>O_m{Z?dh+mQ~k(k%dpwvQD=v5AQP`rzSw$-j*^9w_ z3P07~)RE@a#?p?&1D-hK%FNd`%inEqn+Tnmndxt&WHoPgjXdaRuh)knsz2c>Q&BBN zyrufNfs6Z;l_2Yv!4P;qDqa;`A7g70Uo2*1WOVLDuDu@CJ+QRN z!1fcn;@|kLA%_+>tkXu0W8Cxx*M}e0`8jt5L!y_D0+H3=yec5talOzp4?FKOm+y#M z5<)N{j5xJ)FyB>S*y0hF$x~VbbDn%te2OB$w7%o6k%H#39}!{VF$ox3B!5bR?R1%0 z1uw1&Bl?G8uY|WZC_Rxmmpqf%zvC>Fb_xkvpRX{;lX>!lu;ndbE<529B2O zyt9M(n?!i}MPS=-6KW2~hI5G3xB2<`>G?34E^X^qj;-VUto%^EO!tyPjZ*GX=ZQUs ze9f?L(LaJ*y78mkS#~5c#Ub}4`W;0TqZ(hb;Do^JYS)bHetG}?y|l(WZG3jC_pLP3 zWBejTql7z`Qy+a)pGFdMY8>6z*xYYd2azC_-}<510$KygXI^{`O42DlNK^9uA(^j9 zlh)*^w$DxSNDXJj>)~87ux+l7AOHCQMYN_syN_u&(8^q9p(k;rMzZ2RO^Poxfe)7z z75@wmVFXz(jLH#MyKe}mXD)SL za;Iq;%#Rdkx@zR==t#n1UXk}NA}Hx2Fwy0oeGHy?N;cZ+> z)y9{$)Rjzm*U_HEjh)U5$zMeCJ0LBUru!XqF|XR%f{LqyDAO|uan}0l;cdHf(g$7u zIvs`qdZ*Xr{RAUaMQJwXgX;x;AWP$lu~QsyHhd2^Q*fx#ulIB-7*uM}g@%JC7-+vt z&8wmiIXy<4i@MPr!fm>bxKSWJEJaQ`;Jwik*nFm0ri<6f)XY(H<#t~C$`6nx6O!laD{&VqLatFbCL55GQ0UT?!xCa<>w zkq$xDY%|0BlZ$TolklpBoR&saL`I3^FOeFNG=4|XJP^y0wzh*!o#NGQ#aQdR63IDS zS_4z>C`O1(4F_In7{#V=;I06i5f74H2oM8oX{Fr ziHwM_8lIX8xeF62m92t*WGt@zat_Sm+^8#GIG!8w;s}wO_w} zeIW1mV;D3Y9@-<1@xJ(c8ASUN&q5&YXK^_yl!j!Txm~Zu-6c7_sh?zpu)bOqbZ)-^ z#10fW-m|8U6b?Pym;#Igwg>(J7sw7DlxI1jzRAr5i75lpVFu7hE<(Q);-`zC6*3?9 zd6whT$&bF$IO5WJ|N96823>xLgpCJ86!DN%&qfzmP6T}3%yr*x1%hLPWV_+_e&Rr8 zb(U_dwSIZgkW=N&VkK7f%~T-8m1RBdil`@__1!9Bm3$2w0xpWman-!qXko<;KZal6 zX2N@uTPb87UIn*ZbF>-~> z#FT*SF-;m7P?90>ytqF57=56Co&u>!wm8<`jL`1-RHJaThIDTy!=_G_P>w&|TnbzYp?i~UB%>cD~d&MH}O1?E>4fzhurZ>W?B z`t`|kmmjV61*gg(%c!MkvkF+W0nBWOA!qg}xON0<{Q4vi+CjhptY6E1@%6&D9&=xk z^^fwrGAg@bA(eXI^nC_F4FB@M;LxNtTlU!H`Y{y*=83a?zxWT;o zcur2w-5l$)Y$d$O016K@O&RGb7f=zjO7Q3cT@1AG?SNzRBS*)VuBNxXDYkx{U`s@~ zV>ibqYn=uI`R-fiCCaepl{ZrM^I@EL4)T7noSv!tnHO7Z$B(O2@MoGxBoBG=sof^@ zkG-AR9N&uvs7fzSo+g2undC!>WHqq4gFXnuP5}qzKUU|&*U6YUa%XweofjRX-(nSH zj@>!}KXQnbU z*lDK)&jSxnZyCo00_bLBt1td-z?KENO7hkxW2K$s&J6ji9ODH3scAxAlaVANil_tV z)jV1pZn?733#e7p8yw#n$)QG?rzgF7B(Zhg)_Ei#GDFt|TGz7iN^tyIM(>Ygu-iM8 zSn?>W|HZ4JpK$~^6{UU^ADa&X{!5;=nOpSCFG3)eRmKu6=0+#r+L3-Mz<~yuR1r_l zHzMlp?IANtR=#-0@jo5}B3#kyPaFP8J-Cu;t+AQCKa@eW&BhzIGzJ72gs)TkslKj8 z6(-X0#y!q^{80s@m=7O5%-4JC=e-1&on&BO2)r1TZ}V`!-dMsi8`}NW*Ok5V_XyJG zs@3Ak`&#j6)vurTg4?OC7r5qSJW!kHK7`8r@e0#nliU{xFFX|5O7 zpW1S$06V858auo4(R23%!ab>01eJ4Zj+|RTTx?8U{W7=Ojod739(JE23-a;tU2Jex z%XBiR9)Y*lc^A~^r z>gufOTjk6x%=y-PrZ6*#nZ8~f3|>y zI!+F3ETXHn@aJ@Q1QDb}YR~c$EUh>T18v@m8~~^)6*b${`GA3uaZ4&$XN)22blOPy zM9Xc~zLB8EkKZLgyPanhTQBy6_sp#V2l_Y>8)Qw$wQt;veMezTgf>dV&Q8s@oHmd{ zF$L3tV3#eaXKoP)qj>BrF}Y_>>~dyrs?JVm5ms}aw{!)H@VY+yJOvEFElHYHDM!;F z(hp#e?BEcH_{)%x5JA9_KE>Evd!)9O;rb=S1Dj=?&Y4y5cK+shI4D_& z+`Niz@L4*-2~-qsyF;P4zcwka16%EoIWi_X0URMK-HN)%5bW~m9Y`AiTv2+>?@y!k zULAr8NuW!O)gN5qS0!+H%&GjUfh_sud~9sY01^xUYn#l{Kj#l z@gCj*ImLbE3>?~V*}IKvVB+AFRaKj0ze+Hk8YSHhEady@9*zZp6)$;QE;zf7*}g=li1MvK)!sZOz1;Pu6TQQWO~xwFqVd_LlC@$>QR27yQ@q7DT`qOC%?ilwqJFuMc?7R3@S(KrE(L zH^za^RWg#< zyb)?X`#r}mWUf*Qsn z?OE~xNE-Che!>A|Mu=1-yOS}}htTwBF(+qUKphJxAznF)3civCT!rc=k%$h!K*f+P zfqb2%b!#slz?7w*o68K!^-?50sFDMXa{M97?Ev}v$RHlLW*=+vdIdrn#O+lnG-eU&~ELC#)C-PA@%UEgzSPt(CM@nr83z*p6eXo zBE>;BHa6_xb}=*SR9}7T5eK?Wo$#4uHRi_~pNUh<-=jhxEhU_AbzZ$x)MpSJRDvx_ zPntsZWoka$;_oq?-E?!d5*K?YoCcF{yC zwJNsYrR3Ih+!ct^mm``$e$)4)p5IjdMn1OkZZ_7(N`*!ioVfx5O+hJIC!@pxiokR? z((di;S);b<$78v*29l~yeDr)$g|)_Ln)mRMo_Xp?lGZ|P>^5osk(m8ORiV!(t>Tn2d`R2ib*<5aVt-AT9Bp3nZ# zfhnt%-&DRdH^yE*^m9ve4}IO&C(N9E;4P4`M8MDu3^#MVpDPABmGOlZ#2fG_+%S^M zQH}~8-Dof#`izw-&s?3q9YB6Zz^(4n6`afhDOZ2?()0PgY>JI?A8l87I;o z>v2p34vt#=?ga%M902EiyFlD89XNTT4xD^=A6IC4fCoqo^b!LCz<^K46p9kt?Uv(j zL4JXgeD?n2MmQ^k^D6js2U9t^xd^im$q@EQl1jRoY7^OU6X1Wo>yqyOWIJMHeft&`a0ec<=RTU=Xpn~SrT6Ax- zdtbk1G`CfXoS3P3#0gwEXKBXfYtwSGGKs)k?&3Sgts(z#z^;V-vGW& zHP5Z`&-;A{W%(X4Jz8JjfLrDJz9uH$=6jtE4^3a~%&Libt2Al5K`w-ATRiWo?&!QeM3iL}biAgXviR+yNl8 z5z@xI0wQu}K;5`-AkAZ&8mDc_>8%AWPA5(6Z#L*>EImIhZ8iQM=a;Bik0n58?%X@o zcpebjOoIfm3KVjjknXo*8GkOtV1rQ0qjhvA11tf7)J z?91RoG}=0Ca8$j78n79H^mk|?cZHnG_GYg&ARr*oZxNIxtcPvu6^0s)gNjRCAFi@X zqMx-{q_Zvr2f{D@BBQe15~H%_FU(5{P^r?`eW= z4$2YB=`nz5?o*Jj@}0pO^%$$@&mdw9Ipzuby>;T^7z5I06`U<>Wh^Z=xQuue7xnU7ADvtW`2DV@p=ODs695Zt^a0UlS;33< z`#TWu=g#{A;%5wQs|0s59y7HABZmXzF+P| z!dARyiWoyVd?y1Rk7Go2b(AXEZi4I{Zx4ik5wZM}^Cn5+DhOXq2=?385}^Dwpwc@b zM)}4ALt4y`eN{9?G8EheOphAeWuaF;9XKaaK4U9o8l_yu?X8(YqhGzw~zyCbpQbzmQQ7}WK zYKt-vaByY#NcDRzS|Pd@nQm|=(vQrbT2Z~ALh+rZqCqIX=z^J7u2eU4S)>f8tIm#k zMv2;EuME{%w@=YkmrY(jwjE9fTk{a%-sLyJuL!Ced@2d2zmJP5{>X5yOkcP5+1c5t zj9)j1@U7qao;7@=1UQzo&_@?cvfhz@WSVA4ftV-sMQJ>R-)%JdioC(5t}{jyy~*v<`-Sn ze*wrC^K*b%p0den#n+AYJ$jg1UmgUc_L()@|FjC6hGq4=L7b{kS^AJ);nJo2a(X2- zTaPadf*B)E)1f1=*aP_D<&f7vMNb;EEtH%tCiGiK%s0mAb}oKk4Dy=?HLvdbEx;p; z;g;Gjg7_cX1`-lMc*spt#2MV#=jqEJC=v@u2Ig_inB?@kbeGZL$J=*LovU zae+ck*E8#{c!4^5&%?TV=MHi}fSlmf%5^Ea;Aq`iq4<}zQ*Gx}47w)T((j}!gFx*K zP?N`g>8QpwM$?)zz>r)+>l#GB>nUXCe9|~;dX(IIzrqj)>PBL5TbZE# z6@@(zi4#KB6G8H5N8bH9d0O^p&CoVa5s6m4jOvkul{Vc3 zIbbZyk;Y3kQTtY0o32k4V-L&;ZTgoZu|c3FFRlNGUr3*Y{yq2j5)RMvUBE0OQ2cpu z2{@0-FM>X=yY4woO*c9zy=1GeA?g^QivBQVG_$USS()*^(<`OmW>DQ%9(cD<(tv== zpjZ)J3wqth6^u)o`f%y6gYV=Is1UqXbZ&wneLH zos_n>5?)v-X;Qlms;2sGsZp7Q z?3F9Kvgo#tpwj%g;h>?Y4OBb0Jo)BFMj)~sfTj0IpLrQ?!mprWJG)8w%E;lQS}H|` zpo%sdsH&Oc&I=NTn+&vw3>E)PJz&I=fY`eWqJv)4ZypPLSs`Y!UycnjB20mLM5tnH zO|vFV7eB~NuiFL$NN>s==b!zJLmr=aJga_Sy*IRc+fqVrP`6MVPQ-a_uLNV;%m|NW zwU*iFR1uo3q12^xad7EZ`5( zBnR;PU2-%qNXqmm2p(3}?w%&e1J;ljG#I~fpsSGY##dR0exb_bcMrycl=~M)7>bP+ zw0&P6L-7KEmP5|AKB}x3-|Bf-Mpb1abpPC?j~KZtSFVsl@+_pLpbN@8+V7~2ct3_n z-*keGM&`^`X?X0Lv*es^y11gw$z%P?u1^uoLAeV9Uyz%#7m0L-XB4&HrpP`XD00jd)@~w=mq7+#Z(Z)BGGNY3^aIdDWCgYeY5h!HACv* zY}2{tb-`93EQ|Tv@{;k3-DC!O%VJVqJXz-eHbDI7*({PV)wDYM< z%Baj(AZ6VR(X_WY#s>VJ^yQk=Mv7qXQXDuuf-VTd%ujXxepNpuTn#aOifkOPB+jC2fXlJj17C1pJd znDpZ2>PHyZDM7Cw*M%?6;F4ol--(-%EOcjoWRC3Og*7g@OXV3eEnVAwc2)|#AeQfa z!>D^HF-X;Y^6c_gXB(U8PU_YS4BuwSoPFGV8f3|9z>;Y64ZwA%HzhBx<+SPqe}kcZEgFcd9tBBuO3(9m_J`8jk_7V=qc@l8ji`|G>EITBU* zBsA@O{9d*&GBA)6*Xt74Znlqz;gpxbEn`M#FI)DJuYGmk_QJ~B3wJGqA)oV~{F!n1 zvRAD>mV1c`g{4+Owpth7{B`b%FGu+FDg}**JnepvQz*^5+674}Nv1 zP4=lI#2vEl`@*-kZ6~{|!T;d*>N}cbV7E)@PF?)^ABep#$o-y0iG3>S?}hLF+NymZ zUCSt{$@!uFgze%y4^!|_#p&s%2a1b$O>5`^Z=>7y{eBiYaIhjAI$C+{Ycz1JLa?>{ z4z{3xpZm8Eyf7>OM|M7_0}k!1+_@(4lmrnXxcr+pf92ocqCd~hw#KuvZS^d*msQ#m z0?xGr4ckWcX8L}$HFZc}`*gjVeYv*qgzW`D6!W*Uam@_}Z#pIio~#cz+}H_5DCkem zk;$fMluRUl;2&!oz!@QUI;Ia?wE8=(PZ%O=;Jd#TR9L|_p|rAT?lywJ@B4Wythf{)S z^X#5od{KJHBnWdbMz~ro=fuoOt3*?7$or2T;J3^uBJ6>!5G0s(2C8|p`_?UR*^Q0{ z!U=AQ-6VwRgaP$~3L_2Jl@BEts zc;V0W_TLx4i%p69|K^D6L@XQrcPPIQl5G-;x5@OsAD-aHZPMQ#5Ydj9NB?#Ttp6GQ zZ+)RCjI9a0_%GmJ;JV)J|E~V!YH(-1ouU)}KLj4r>O`CzjQaOUEYD8yQuaSezXJZu z4wn7I+<-gNEjt9iyyxoG*wJ{hZXdTiM=>h1-0e;!W&jts5Q zUH!iAL1LNxu|Up03nA$xu?SZ20$KD66uL2_1YE^3KO9^P)9UspcRSBf|8qM;X!Rwy zn21Qm(?HhibEJ8E45QxM>1U&l-p$8zeD1Lv!t+4#B6TAy50MwL7KDGGUotVT|AEH~ zL%_w$h>1hgr#CzC9Oph>RzuN4NOc%n1t2sVyXwE*yTQmT*|s9%yA}B`NbrBkq8)Bv zBf-Mcp${kux;uBk9aWjdzn97BAvaVEWS733ur1FKWV$Y=!I;LBxTi^CfxOW>oyvj* z#7BdWvJr#lIrnjWbrh2;C;(@GxD2|Ac36p16*oTVx;!VmBTQ=`RsU{v5hwV1W5QH+ zDg1Ef5UWV-7JDItRBze6xfswLV3X-~o@et=VnT`}2c*C)U>P1He7>W{gr5V>2q6C! z?GTXQsILZBm%mZlNp8T!QL%`}d1lpT`vR7;lr

f?cW24rj_{`}7(&m$bZ3hp}=n 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 SU<5hLFry2X^{RE8WBw0YP(d*O literal 0 HcmV?d00001 diff --git a/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/propertySyncIcon.png.import b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/propertySyncIcon.png.import new file mode 100644 index 0000000..5ec1948 --- /dev/null +++ b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/propertySyncIcon.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dvqalcb5gle3l" +path="res://.godot/imported/propertySyncIcon.png-7230c83490a15b0c58480c84d77bc8ba.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/propertySyncIcon.png" +dest_files=["res://.godot/imported/propertySyncIcon.png-7230c83490a15b0c58480c84d77bc8ba.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +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/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 diff --git a/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/transformSyncIcon.png b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/transformSyncIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..325a40f65ed3392934da3f5e2775d4f5b6569aef GIT binary patch literal 12231 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

yr-G-~yy^F~u7X3ORM|azw*N@* zSc*3rXeN=SGi&@v$5?$hKb3qF_l=$Kj?54{cls%~eCR~S?*E~`rnU4FGWyNP-#%Bd zKJq zaAcyXoVa6)FTa`cCwP$(gA%@<(TOD5zA7-?L#Ex4Y-8Ph!&+&RWAark$MOrRm;CIu_?9Zj7}`k*0#X>-}qXNo{~y=Dm!4yn(n$vNptc78}6Gr#Y=ctzD5N` z1Sf~4ReQ`F+=^~Pcx_TcdHwEe$WqGPd7F0AC+Ksz{lI{FZqTu)qtpT!|N9`LgzP3g?eD;Vrzwg;j`rqId zo+EEBm`!c_5puYCT`>-gIHK(sxPRTv*=Sp`4RG;*7{?ery47i)_+Dg)pCrP|zo4j2 zDU6HJL|aR{e<<0m#Ia-_{QgO@q)Az-7q@fh`?eN@Zhg^axCi+OjbNK(5|0?iK_E@O z&FEiiCw3ufs+0b{DZj`e?FhnoaBE9j&`#XPF65Kqinr(JQW%-RQJ2)ky`^MRO9{y; z&T%*M7vU$U2Po&(*-h9W5*Gr95YUboxc77q22Bd#`*~dI{a*x1zI|G(>A!)3JP3bd zYyKD6zX%>cKvmR#+Bp$H73+&FYAK9TVMJS(e2G1Of7i2j1>z{b^apc((ZztCs>MBz zJX%P_e3?z=p`G-&51~H+pk>N?%k5pK2^m%pu|x9IXcD8^Z%}%0;p)GI<$`9ky}XDw=gCi8>;5G}j~J5Gn*aMS{Uo^VSMd|u zGfc`H^+ha&F&;dof)@Yffh`GpyNE<@#*Z}@;dw4pxwmp3i(m}N`$hPxzkdBO!H_+P zHKW6Qk7ALHG$_=>I8fJ;ST&QU_uD?AFou{HRH2ty%RMN6dSp-mLJBw)<3PXf<|&Lw z7`l~@D^KIg<7Gmw-KyFokGaD$EA=@1i~^Zq1kOf7&_cyYV%LvHuInkCAgi-q#BKzBQ%A-8T1>3G| z>19?3m0V4CXws+|$q$1_`;%B%EO+%wIjK+5-IcGquigzo z>dcnXVqP?iutXWmet!l@{dVIk=l(?WT!UKT^OIRScR3JmT>2swXuDi|*Wt&zWUIV( z2rBkPw@UBwK#PUsqO^n@-cZ z1=rB6`vK4Edg$BioDD|&ah0HQ=x8?oq=%cOJEbfqk{=F5h|N+PuDly=I(Z{SJT1@| zoYDSZdUZ;yGDlM7SwPENqs#S?vYt?2=SyJcE-|>r#=!njy+L=d$mnA*tssH~%FXae z%|u(CUHYrLSdFk?)FQ9`dOv45*t z=1tHU-r}6MH!aCRWKh{LSluD)FP~D@gOiP0bM`V>FKdeCnH1LiO8d86|Grk(IpQx0 z{gTQ1)I9l1`?I?Kt+<|Qt(E;h+6J+=6xq#e$hoSZ4ldAPJ+a4=M8@r$tPW1CIk_x` zt+D)-J{4k?!;ZO-V0@)K!?EJjJy>lu;n5@;c;)J`!5NG&W-W8%O+d(OvV>`qP1i_R z_9HD&9M~=ASsRx5NhNRsAjK4(HF;s`^Hb2T*~d*!;`I zmWTZrpoM&R z(#ews#tdYDFgMOpnLZ?3PAdqVe3EF(d{-wT38AEka!U4;%N;!VGc*pGiOFI;Fm6H9 zq5vaAmLBF^j90jGixvcGpG)_8To}O5T%5<~xGn=g(5TI^U*0 zA$SiPj|a7j6(0JX>Y&V&Rzd)(Hx{Bu1rf zmEIc?e;a%aqP-e8Y}zL0gUZh$`8vsPP&28K8St*y$H{v;Q&=qy#@bF)y_X`Hw)E?v zizj;P__7#u-1d1xdP7DQE9X&|>br`ow>#=nZ&)~XdKu8l2SY(SUka;q4XJ4U^}S{H z#JAzu`>?(<_EBj_g!E;r;f;2Ox%>&Ox@jAGZvt@`Tuj-IL&t1 ar5!q>QA=zK>!o4Zciee>jS4lJkpBUfX+z@x literal 0 HcmV?d00001 diff --git a/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/transformSyncIcon.png.import b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/transformSyncIcon.png.import new file mode 100644 index 0000000..58c64fb --- /dev/null +++ b/addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/transformSyncIcon.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://choidi1eihrtd" +path="res://.godot/imported/transformSyncIcon.png-70197aa309a4df07b6cd61ec5f7effd6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/transformSyncIcon.png" +dest_files=["res://.godot/imported/transformSyncIcon.png-70197aa309a4df07b6cd61ec5f7effd6.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +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/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 diff --git a/addons/godot_steam_sync/godot_steam_sync.gd b/addons/godot_steam_sync/godot_steam_sync.gd new file mode 100644 index 0000000..73f7519 --- /dev/null +++ b/addons/godot_steam_sync/godot_steam_sync.gd @@ -0,0 +1,20 @@ +@tool +extends EditorPlugin + + +func _enter_tree() -> void: + + add_autoload_singleton("NetworkManager","res://addons/godot_steam_sync/Network/RadomeSteamSync/NetworkManager.gd") + add_autoload_singleton("P2P","res://addons/godot_steam_sync/Network/RadomeSteamSync/P2P.gd") +# add_autoload_singleton("Command","res://addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/CommandSync.gd") + #add_autoload_singleton("SceneManager","res://addons/godot_steam_sync/SceneChanger/SceneManager.gd") + + + +func _exit_tree() -> void: + remove_autoload_singleton("NetworkManager") + remove_autoload_singleton("P2P") +# remove_autoload_singleton("Command") + #remove_autoload_singleton("SceneManager") + + diff --git a/addons/godot_steam_sync/plugin.cfg b/addons/godot_steam_sync/plugin.cfg new file mode 100644 index 0000000..d7124f2 --- /dev/null +++ b/addons/godot_steam_sync/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="GodotSteamSync" +description="Advanced GodotSteam Synchornizer and Lobby System." +author="Radome" +version="0.1" +script="godot_steam_sync.gd" diff --git a/godotsteam_sync_example/Test.tscn b/godotsteam_sync_example/Test.tscn new file mode 100644 index 0000000..2a87177 --- /dev/null +++ b/godotsteam_sync_example/Test.tscn @@ -0,0 +1,176 @@ +[gd_scene load_steps=13 format=3 uid="uid://dhi5myb4u2mij"] + +[ext_resource type="Script" path="res://addons/godot_steam_sync/Network/RadomeSteamSync/NetworkPlayerSpawner.gd" id="1_a26tm"] +[ext_resource type="Script" path="res://addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/RigidBodySync3D.gd" id="2_io2do"] + +[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_ilft6"] +sky_horizon_color = Color(0.64625, 0.65575, 0.67075, 1) +ground_horizon_color = Color(0.64625, 0.65575, 0.67075, 1) + +[sub_resource type="Sky" id="Sky_ryjhm"] +sky_material = SubResource("ProceduralSkyMaterial_ilft6") + +[sub_resource type="Environment" id="Environment_lmp12"] +background_mode = 2 +sky = SubResource("Sky_ryjhm") +tonemap_mode = 2 +glow_enabled = true + +[sub_resource type="BoxMesh" id="BoxMesh_li3io"] + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_r06tq"] +albedo_color = Color(0.321728, 0.321728, 0.321728, 1) + +[sub_resource type="ConcavePolygonShape3D" id="ConcavePolygonShape3D_io4rt"] +data = PackedVector3Array(-0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5, -0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, -0.5, -0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, -0.5, -0.5, -0.5) + +[sub_resource type="PhysicsMaterial" id="PhysicsMaterial_iltko"] +bounce = 1.0 + +[sub_resource type="SphereMesh" id="SphereMesh_jxxnq"] + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_cdfuw"] +albedo_color = Color(0.415686, 1, 1, 1) + +[sub_resource type="SphereShape3D" id="SphereShape3D_ktxia"] + +[node name="Test" type="Node"] + +[node name="NetworkPlayerSpawner" type="Node" parent="." node_paths=PackedStringArray("spawn_pos", "players_parent_node")] +script = ExtResource("1_a26tm") +spawn_pos = NodePath("../SpawnPos") +players_parent_node = NodePath("../Players") + +[node name="WorldEnvironment" type="WorldEnvironment" parent="."] +environment = SubResource("Environment_lmp12") + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] +transform = Transform3D(-0.866023, -0.433016, 0.250001, 0, 0.499998, 0.866027, -0.500003, 0.749999, -0.43301, 0, 0, 0) +shadow_enabled = true + +[node name="Players" type="Node3D" parent="."] + +[node name="SpawnPos" type="Marker3D" parent="."] + +[node name="Level" type="Node3D" parent="."] + +[node name="MeshInstance3D" type="MeshInstance3D" parent="Level"] +transform = Transform3D(49.9588, 0, 0, 0, 1, 0, 0, 0, 61.4249, 0, -3.98574, 0) +mesh = SubResource("BoxMesh_li3io") +skeleton = NodePath("../..") +surface_material_override/0 = SubResource("StandardMaterial3D_r06tq") + +[node name="StaticBody3D" type="StaticBody3D" parent="Level/MeshInstance3D"] + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Level/MeshInstance3D/StaticBody3D"] +shape = SubResource("ConcavePolygonShape3D_io4rt") + +[node name="MeshInstance3D6" type="MeshInstance3D" parent="Level"] +transform = Transform3D(-0.506593, 0.999949, 0, -49.9562, -0.0101402, 0, 0, 0, 61.4249, -18.7014, -3.98574, 0) +mesh = SubResource("BoxMesh_li3io") +skeleton = NodePath("../..") + +[node name="StaticBody3D" type="StaticBody3D" parent="Level/MeshInstance3D6"] + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Level/MeshInstance3D6/StaticBody3D"] +shape = SubResource("ConcavePolygonShape3D_io4rt") + +[node name="MeshInstance3D8" type="MeshInstance3D" parent="Level"] +transform = Transform3D(-0.0344473, 0.0679944, 61.2827, -49.9562, -0.0101402, 0, 0.505421, -0.997635, 4.17676, -0.748235, -3.98574, -26.874) +mesh = SubResource("BoxMesh_li3io") +skeleton = NodePath("../..") + +[node name="StaticBody3D" type="StaticBody3D" parent="Level/MeshInstance3D8"] + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Level/MeshInstance3D8/StaticBody3D"] +shape = SubResource("ConcavePolygonShape3D_io4rt") + +[node name="MeshInstance3D9" type="MeshInstance3D" parent="Level"] +transform = Transform3D(-0.0344473, 0.0679944, 61.2827, -49.9562, -0.0101402, 0, 0.505421, -0.997635, 4.17676, -6.63549, -3.98574, 19.7729) +mesh = SubResource("BoxMesh_li3io") +skeleton = NodePath("../..") + +[node name="StaticBody3D" type="StaticBody3D" parent="Level/MeshInstance3D9"] + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Level/MeshInstance3D9/StaticBody3D"] +shape = SubResource("ConcavePolygonShape3D_io4rt") + +[node name="MeshInstance3D7" type="MeshInstance3D" parent="Level"] +transform = Transform3D(-0.506593, 0.999949, 0, -49.9562, -0.0101402, 0, 0, 0, 61.4249, 12.2278, -3.98574, 0) +mesh = SubResource("BoxMesh_li3io") +skeleton = NodePath("../..") + +[node name="StaticBody3D" type="StaticBody3D" parent="Level/MeshInstance3D7"] + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Level/MeshInstance3D7/StaticBody3D"] +shape = SubResource("ConcavePolygonShape3D_io4rt") + +[node name="Physics" type="Node3D" parent="."] + +[node name="RigidBody3D" type="RigidBody3D" parent="Physics"] +physics_interpolation_mode = 1 +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.20984, 0) +collision_layer = 4 +collision_mask = 7 +physics_material_override = SubResource("PhysicsMaterial_iltko") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="Physics/RigidBody3D"] +mesh = SubResource("SphereMesh_jxxnq") +surface_material_override/0 = SubResource("StandardMaterial3D_cdfuw") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Physics/RigidBody3D"] +shape = SubResource("SphereShape3D_ktxia") + +[node name="RigidBodySync3D" type="Node" parent="Physics/RigidBody3D"] +script = ExtResource("2_io2do") + +[node name="RigidBody3D2" type="RigidBody3D" parent="Physics"] +physics_interpolation_mode = 1 +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3.5851, 1.20984, -2.08695) +collision_layer = 4 +collision_mask = 7 +physics_material_override = SubResource("PhysicsMaterial_iltko") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="Physics/RigidBody3D2"] +mesh = SubResource("SphereMesh_jxxnq") +surface_material_override/0 = SubResource("StandardMaterial3D_cdfuw") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Physics/RigidBody3D2"] +shape = SubResource("SphereShape3D_ktxia") + +[node name="RigidBodySync3D" type="Node" parent="Physics/RigidBody3D2"] +script = ExtResource("2_io2do") + +[node name="RigidBody3D3" type="RigidBody3D" parent="Physics"] +physics_interpolation_mode = 1 +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.84777, 1.20984, 4.17028) +collision_layer = 4 +collision_mask = 7 +physics_material_override = SubResource("PhysicsMaterial_iltko") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="Physics/RigidBody3D3"] +mesh = SubResource("SphereMesh_jxxnq") +surface_material_override/0 = SubResource("StandardMaterial3D_cdfuw") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Physics/RigidBody3D3"] +shape = SubResource("SphereShape3D_ktxia") + +[node name="RigidBodySync3D" type="Node" parent="Physics/RigidBody3D3"] +script = ExtResource("2_io2do") + +[node name="RigidBody3D4" type="RigidBody3D" parent="Physics"] +physics_interpolation_mode = 1 +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.84777, 1.8173, -2.41229) +collision_layer = 4 +collision_mask = 7 +physics_material_override = SubResource("PhysicsMaterial_iltko") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="Physics/RigidBody3D4"] +mesh = SubResource("SphereMesh_jxxnq") +surface_material_override/0 = SubResource("StandardMaterial3D_cdfuw") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Physics/RigidBody3D4"] +shape = SubResource("SphereShape3D_ktxia") + +[node name="RigidBodySync3D" type="Node" parent="Physics/RigidBody3D4"] +script = ExtResource("2_io2do") diff --git a/godotsteam_sync_example/fpc/EditorModule.gd b/godotsteam_sync_example/fpc/EditorModule.gd new file mode 100644 index 0000000..fa1320b --- /dev/null +++ b/godotsteam_sync_example/fpc/EditorModule.gd @@ -0,0 +1,49 @@ +@tool +extends Node + +# This does not effect runtime yet but will in the future. + +@export_category("Controller Editor Module") +@export_range(-360.0, 360.0, 0.01, "or_greater", "or_less") var head_y_rotation : float = 0.0: + set(new_rotation): + if HEAD: + head_y_rotation = new_rotation + HEAD.rotation.y = deg_to_rad(head_y_rotation) + update_configuration_warnings() +@export_range(-90.0, 90.0, 0.01, "or_greater", "or_less") var head_x_rotation : float = 0.0: + set(new_rotation): + if HEAD: + head_x_rotation = new_rotation + HEAD.rotation.x = deg_to_rad(head_x_rotation) + update_configuration_warnings() + +@export_group("Nodes") +@export var CHARACTER : CharacterBody3D +@export var head_path : String = "Head" # Relative to the parent node +#@export var CAMERA : Camera3D +#@export var HEADBOB_ANIMATION : AnimationPlayer +#@export var JUMP_ANIMATION : AnimationPlayer +#@export var CROUCH_ANIMATION : AnimationPlayer +#@export var COLLISION_MESH : CollisionShape3D + +@onready var HEAD = get_node("../" + head_path) + + +func _ready(): + if !Engine.is_editor_hint(): + print("not editor") + HEAD.rotation.y = deg_to_rad(head_y_rotation) + HEAD.rotation.x = deg_to_rad(head_x_rotation) + + +func _get_configuration_warnings(): + var warnings = [] + + if head_y_rotation > 360: + warnings.append("The head rotation is greater than 360") + + if head_y_rotation < -360: + warnings.append("The head rotation is less than -360") + + # Returning an empty array gives no warnings + return warnings diff --git a/godotsteam_sync_example/fpc/character.tscn b/godotsteam_sync_example/fpc/character.tscn new file mode 100644 index 0000000..2062108 --- /dev/null +++ b/godotsteam_sync_example/fpc/character.tscn @@ -0,0 +1,854 @@ +[gd_scene load_steps=22 format=3 uid="uid://cc1m2a1obsyn4"] + +[ext_resource type="Script" path="res://godotsteam_sync_example/fpc/EditorModule.gd" id="3_v3ckk"] +[ext_resource type="Script" path="res://godotsteam_sync_example/fpc/debug.gd" id="3_x1wcc"] +[ext_resource type="Script" path="res://addons/godot_steam_sync/Network/RadomeSteamSync/SyncNode/TransformSync3D.gd" id="4_ch187"] + +[sub_resource type="GDScript" id="GDScript_r0lhh"] +script/source = " +# COPYRIGHT Colormatic Studios +# MIT licence +# Quality Godot First Person Controller v2 + + +extends CharacterBody3D + +var IS_OWNER : bool = false + +func make_owner(): + IS_OWNER = true + $Head/Camera.current = true + +## The settings for the character's movement and feel. +@export_category(\"Character\") +## The speed that the character moves at without crouching or sprinting. +@export var base_speed : float = 3.0 +## The speed that the character moves at when sprinting. +@export var sprint_speed : float = 6.0 +## The speed that the character moves at when crouching. +@export var crouch_speed : float = 1.0 + +## How fast the character speeds up and slows down when Motion Smoothing is on. +@export var acceleration : float = 10.0 +## How high the player jumps. +@export var jump_velocity : float = 4.5 +## How far the player turns when the mouse is moved. +@export var mouse_sensitivity : float = 0.1 +## Invert the Y input for mouse and joystick +@export var invert_mouse_y : bool = false # Possibly add an invert mouse X in the future +## Wether the player can use movement inputs. Does not stop outside forces or jumping. See Jumping Enabled. +@export var immobile : bool = false +## The reticle file to import at runtime. By default are in res://addons/fpc/reticles/. Set to an empty string to remove. +@export_file var default_reticle + +@export_group(\"Nodes\") +## The node that holds the camera. This is rotated instead of the camera for mouse input. +@export var HEAD : Node3D +@export var CAMERA : Camera3D +@export var HEADBOB_ANIMATION : AnimationPlayer +@export var JUMP_ANIMATION : AnimationPlayer +@export var CROUCH_ANIMATION : AnimationPlayer +@export var COLLISION_MESH : CollisionShape3D + +@export_group(\"Controls\") +# We are using UI controls because they are built into Godot Engine so they can be used right away +@export var JUMP : String = \"ui_accept\" +@export var LEFT : String = \"ui_left\" +@export var RIGHT : String = \"ui_right\" +@export var FORWARD : String = \"ui_up\" +@export var BACKWARD : String = \"ui_down\" +## By default this does not pause the game, but that can be changed in _process. +@export var PAUSE : String = \"ui_cancel\" +@export var CROUCH : String = \"crouch\" +@export var SPRINT : String = \"sprint\" + +# Uncomment if you want controller support +#@export var controller_sensitivity : float = 0.035 +#@export var LOOK_LEFT : String = \"look_left\" +#@export var LOOK_RIGHT : String = \"look_right\" +#@export var LOOK_UP : String = \"look_up\" +#@export var LOOK_DOWN : String = \"look_down\" + +@export_group(\"Feature Settings\") +## Enable or disable jumping. Useful for restrictive storytelling environments. +@export var jumping_enabled : bool = true +## Wether the player can move in the air or not. +@export var in_air_momentum : bool = true +## Smooths the feel of walking. +@export var motion_smoothing : bool = true +@export var sprint_enabled : bool = true +@export var crouch_enabled : bool = true +@export_enum(\"Hold to Crouch\", \"Toggle Crouch\") var crouch_mode : int = 0 +@export_enum(\"Hold to Sprint\", \"Toggle Sprint\") var sprint_mode : int = 0 +## Wether sprinting should effect FOV. +@export var dynamic_fov : bool = true +## If the player holds down the jump button, should the player keep hopping. +@export var continuous_jumping : bool = true +## Enables the view bobbing animation. +@export var view_bobbing : bool = true +## Enables an immersive animation when the player jumps and hits the ground. +@export var jump_animation : bool = true +## This determines wether the player can use the pause button, not wether the game will actually pause. +@export var pausing_enabled : bool = true +## Use with caution. +@export var gravity_enabled : bool = true + + +# Member variables +var speed : float = base_speed +var current_speed : float = 0.0 +# States: normal, crouching, sprinting +var state : String = \"normal\" +var low_ceiling : bool = false # This is for when the cieling is too low and the player needs to crouch. +var was_on_floor : bool = true # Was the player on the floor last frame (for landing animation) + +# The reticle should always have a Control node as the root +var RETICLE : Control + +# Get the gravity from the project settings to be synced with RigidBody nodes +var gravity : float = ProjectSettings.get_setting(\"physics/3d/default_gravity\") # Don't set this as a const, see the gravity section in _physics_process + +# Stores mouse input for rotating the camera in the phyhsics process +var mouseInput : Vector2 = Vector2(0,0) + +func _ready(): + if IS_OWNER: + #It is safe to comment this line if your game doesn't start with the mouse captured + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED + + # If the controller is rotated in a certain direction for game design purposes, redirect this rotation into the head. + HEAD.rotation.y = rotation.y + rotation.y = 0 + + if default_reticle: + change_reticle(default_reticle) + + # Reset the camera position + # If you want to change the default head height, change these animations. + HEADBOB_ANIMATION.play(\"RESET\") + JUMP_ANIMATION.play(\"RESET\") + CROUCH_ANIMATION.play(\"RESET\") + + check_controls() + +func check_controls(): # If you add a control, you might want to add a check for it here. + # The actions are being disabled so the engine doesn't halt the entire project in debug mode + if IS_OWNER: + if !InputMap.has_action(JUMP): + push_error(\"No control mapped for jumping. Please add an input map control. Disabling jump.\") + jumping_enabled = false + if !InputMap.has_action(LEFT): + push_error(\"No control mapped for move left. Please add an input map control. Disabling movement.\") + immobile = true + if !InputMap.has_action(RIGHT): + push_error(\"No control mapped for move right. Please add an input map control. Disabling movement.\") + immobile = true + if !InputMap.has_action(FORWARD): + push_error(\"No control mapped for move forward. Please add an input map control. Disabling movement.\") + immobile = true + if !InputMap.has_action(BACKWARD): + push_error(\"No control mapped for move backward. Please add an input map control. Disabling movement.\") + immobile = true + if !InputMap.has_action(PAUSE): + push_error(\"No control mapped for pause. Please add an input map control. Disabling pausing.\") + pausing_enabled = false + if !InputMap.has_action(CROUCH): + push_error(\"No control mapped for crouch. Please add an input map control. Disabling crouching.\") + crouch_enabled = false + if !InputMap.has_action(SPRINT): + push_error(\"No control mapped for sprint. Please add an input map control. Disabling sprinting.\") + sprint_enabled = false + + +func change_reticle(reticle): # Yup, this function is kinda strange + if IS_OWNER: + if RETICLE: + RETICLE.queue_free() + + RETICLE = load(reticle).instantiate() + RETICLE.character = self + $UserInterface.add_child(RETICLE) + + +func _physics_process(delta): + if IS_OWNER: + # Big thanks to github.com/LorenzoAncora for the concept of the improved debug values + current_speed = Vector3.ZERO.distance_to(get_real_velocity()) + $UserInterface/DebugPanel.add_property(\"Speed\", snappedf(current_speed, 0.001), 1) + $UserInterface/DebugPanel.add_property(\"Target speed\", speed, 2) + var cv : Vector3 = get_real_velocity() + var vd : Array[float] = [ + snappedf(cv.x, 0.001), + snappedf(cv.y, 0.001), + snappedf(cv.z, 0.001) + ] + var readable_velocity : String = \"X: \" + str(vd[0]) + \" Y: \" + str(vd[1]) + \" Z: \" + str(vd[2]) + $UserInterface/DebugPanel.add_property(\"Velocity\", readable_velocity, 3) + + # Gravity + #gravity = ProjectSettings.get_setting(\"physics/3d/default_gravity\") # If the gravity changes during your game, uncomment this code + if not is_on_floor() and gravity and gravity_enabled: + velocity.y -= gravity * delta + + handle_jumping() + + var input_dir = Vector2.ZERO + if !immobile: # Immobility works by interrupting user input, so other forces can still be applied to the player + input_dir = Input.get_vector(LEFT, RIGHT, FORWARD, BACKWARD) + handle_movement(delta, input_dir) + + handle_head_rotation() + + # The player is not able to stand up if the ceiling is too low + low_ceiling = $CrouchCeilingDetection.is_colliding() + + handle_state(input_dir) + if dynamic_fov: # This may be changed to an AnimationPlayer + update_camera_fov() + + if view_bobbing: + headbob_animation(input_dir) + + if jump_animation: + if !was_on_floor and is_on_floor(): # The player just landed + match randi() % 2: #TODO: Change this to detecting velocity direction + 0: + JUMP_ANIMATION.play(\"land_left\", 0.25) + 1: + JUMP_ANIMATION.play(\"land_right\", 0.25) + + was_on_floor = is_on_floor() # This must always be at the end of physics_process + + +func handle_jumping(): + if IS_OWNER: + if jumping_enabled: + if continuous_jumping: # Hold down the jump button + if Input.is_action_pressed(JUMP) and is_on_floor() and !low_ceiling: + if jump_animation: + JUMP_ANIMATION.play(\"jump\", 0.25) + velocity.y += jump_velocity # Adding instead of setting so jumping on slopes works properly + else: + if Input.is_action_just_pressed(JUMP) and is_on_floor() and !low_ceiling: + if jump_animation: + JUMP_ANIMATION.play(\"jump\", 0.25) + velocity.y += jump_velocity + + +func handle_movement(delta, input_dir): + if IS_OWNER: + var direction = input_dir.rotated(-HEAD.rotation.y) + direction = Vector3(direction.x, 0, direction.y) + move_and_slide() + + if in_air_momentum: + if is_on_floor(): + if motion_smoothing: + velocity.x = lerp(velocity.x, direction.x * speed, acceleration * delta) + velocity.z = lerp(velocity.z, direction.z * speed, acceleration * delta) + else: + velocity.x = direction.x * speed + velocity.z = direction.z * speed + else: + if motion_smoothing: + velocity.x = lerp(velocity.x, direction.x * speed, acceleration * delta) + velocity.z = lerp(velocity.z, direction.z * speed, acceleration * delta) + else: + velocity.x = direction.x * speed + velocity.z = direction.z * speed + +func handle_head_rotation(): + if IS_OWNER: + HEAD.rotation_degrees.y -= mouseInput.x * mouse_sensitivity + if invert_mouse_y: + HEAD.rotation_degrees.x -= mouseInput.y * mouse_sensitivity * -1.0 + else: + HEAD.rotation_degrees.x -= mouseInput.y * mouse_sensitivity + + # Uncomment for controller support + #var controller_view_rotation = Input.get_vector(LOOK_DOWN, LOOK_UP, LOOK_RIGHT, LOOK_LEFT) * controller_sensitivity # These are inverted because of the nature of 3D rotation. + #HEAD.rotation.x += controller_view_rotation.x + #if invert_mouse_y: + #HEAD.rotation.y += controller_view_rotation.y * -1.0 + #else: + #HEAD.rotation.y += controller_view_rotation.y + + + mouseInput = Vector2(0,0) + HEAD.rotation.x = clamp(HEAD.rotation.x, deg_to_rad(-90), deg_to_rad(90)) + + +func handle_state(moving): + if IS_OWNER: + if sprint_enabled: + if sprint_mode == 0: + if Input.is_action_pressed(SPRINT) and state != \"crouching\": + if moving: + if state != \"sprinting\": + enter_sprint_state() + else: + if state == \"sprinting\": + enter_normal_state() + elif state == \"sprinting\": + enter_normal_state() + elif sprint_mode == 1: + if moving: + # If the player is holding sprint before moving, handle that cenerio + if Input.is_action_pressed(SPRINT) and state == \"normal\": + enter_sprint_state() + if Input.is_action_just_pressed(SPRINT): + match state: + \"normal\": + enter_sprint_state() + \"sprinting\": + enter_normal_state() + elif state == \"sprinting\": + enter_normal_state() + + if crouch_enabled: + if crouch_mode == 0: + if Input.is_action_pressed(CROUCH) and state != \"sprinting\": + if state != \"crouching\": + enter_crouch_state() + elif state == \"crouching\" and !$CrouchCeilingDetection.is_colliding(): + enter_normal_state() + elif crouch_mode == 1: + if Input.is_action_just_pressed(CROUCH): + match state: + \"normal\": + enter_crouch_state() + \"crouching\": + if !$CrouchCeilingDetection.is_colliding(): + enter_normal_state() + + +# Any enter state function should only be called once when you want to enter that state, not every frame. + +func enter_normal_state(): + if IS_OWNER: + #print(\"entering normal state\") + var prev_state = state + if prev_state == \"crouching\": + CROUCH_ANIMATION.play_backwards(\"crouch\") + state = \"normal\" + speed = base_speed + +func enter_crouch_state(): + if IS_OWNER: + #print(\"entering crouch state\") + var prev_state = state + state = \"crouching\" + speed = crouch_speed + CROUCH_ANIMATION.play(\"crouch\") + +func enter_sprint_state(): + if IS_OWNER: + #print(\"entering sprint state\") + var prev_state = state + if prev_state == \"crouching\": + CROUCH_ANIMATION.play_backwards(\"crouch\") + state = \"sprinting\" + speed = sprint_speed + + +func update_camera_fov(): + if IS_OWNER: + if state == \"sprinting\": + CAMERA.fov = lerp(CAMERA.fov, 85.0, 0.3) + else: + CAMERA.fov = lerp(CAMERA.fov, 75.0, 0.3) + + +func headbob_animation(moving): + if IS_OWNER: + if moving and is_on_floor(): + var use_headbob_animation : String + match state: + \"normal\",\"crouching\": + use_headbob_animation = \"walk\" + \"sprinting\": + use_headbob_animation = \"sprint\" + + var was_playing : bool = false + if HEADBOB_ANIMATION.current_animation == use_headbob_animation: + was_playing = true + + HEADBOB_ANIMATION.play(use_headbob_animation, 0.25) + HEADBOB_ANIMATION.speed_scale = (current_speed / base_speed) * 1.75 + if !was_playing: + HEADBOB_ANIMATION.seek(float(randi() % 2)) # Randomize the initial headbob direction + # Let me explain that piece of code because it looks like it does the opposite of what it actually does. + # The headbob animation has two starting positions. One is at 0 and the other is at 1. + # randi() % 2 returns either 0 or 1, and so the animation randomly starts at one of the starting positions. + # This code is extremely performant but it makes no sense. + + else: + if HEADBOB_ANIMATION.current_animation == \"sprint\" or HEADBOB_ANIMATION.current_animation == \"walk\": + HEADBOB_ANIMATION.speed_scale = 1 + HEADBOB_ANIMATION.play(\"RESET\", 1) + + +func _process(delta): + if IS_OWNER: + $UserInterface/DebugPanel.add_property(\"FPS\", Performance.get_monitor(Performance.TIME_FPS), 0) + var status : String = state + if !is_on_floor(): + status += \" in the air\" + $UserInterface/DebugPanel.add_property(\"State\", status, 4) + + if pausing_enabled: + if Input.is_action_just_pressed(PAUSE): + # You may want another node to handle pausing, because this player may get paused too. + match Input.mouse_mode: + Input.MOUSE_MODE_CAPTURED: + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + #get_tree().paused = false + Input.MOUSE_MODE_VISIBLE: + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED + #get_tree().paused = false + + +func _unhandled_input(event : InputEvent): + if IS_OWNER: + if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: + mouseInput.x += event.relative.x + mouseInput.y += event.relative.y + # Toggle debug menu + elif event is InputEventKey: + if event.is_released(): + # Where we're going, we don't need InputMap + if event.keycode == 4194338: # F7 + $UserInterface/DebugPanel.visible = !$UserInterface/DebugPanel.visible +" + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_kp17n"] +albedo_color = Color(0.909804, 0.596078, 0, 1) +clearcoat_enabled = true +clearcoat_roughness = 0.2 + +[sub_resource type="CapsuleMesh" id="CapsuleMesh_jw1de"] +material = SubResource("StandardMaterial3D_kp17n") + +[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_uy03j"] + +[sub_resource type="Animation" id="Animation_j8cx7"] +resource_name = "RESET" +length = 0.001 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Mesh:scale") +tracks/0/interp = 2 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector3(1, 1, 1)] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("Collision:scale") +tracks/1/interp = 2 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector3(1, 1, 1)] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("Mesh:position") +tracks/2/interp = 2 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector3(0, 1, 0)] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("Collision:position") +tracks/3/interp = 2 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector3(0, 1, 0)] +} +tracks/4/type = "value" +tracks/4/imported = false +tracks/4/enabled = true +tracks/4/path = NodePath("Head:position") +tracks/4/interp = 2 +tracks/4/loop_wrap = true +tracks/4/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector3(0, 1.5, 0)] +} + +[sub_resource type="Animation" id="Animation_5ec5e"] +resource_name = "crouch" +length = 0.2 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Mesh:scale") +tracks/0/interp = 2 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.2), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [Vector3(1, 1, 1), Vector3(1, 0.75, 1)] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("Collision:scale") +tracks/1/interp = 2 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 0.2), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [Vector3(1, 1, 1), Vector3(1, 0.75, 1)] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("Mesh:position") +tracks/2/interp = 2 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0, 0.2), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [Vector3(0, 1, 0), Vector3(0, 0.75, 0)] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("Collision:position") +tracks/3/interp = 2 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0, 0.2), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [Vector3(0, 1, 0), Vector3(0, 0.75, 0)] +} +tracks/4/type = "value" +tracks/4/imported = false +tracks/4/enabled = true +tracks/4/path = NodePath("Head:position") +tracks/4/interp = 2 +tracks/4/loop_wrap = true +tracks/4/keys = { +"times": PackedFloat32Array(0, 0.2), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [Vector3(0, 1.5, 0), Vector3(0, 1.12508, 0)] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_5e5t5"] +_data = { +"RESET": SubResource("Animation_j8cx7"), +"crouch": SubResource("Animation_5ec5e") +} + +[sub_resource type="Animation" id="Animation_gh776"] +resource_name = "RESET" +length = 0.001 +loop_mode = 1 +tracks/0/type = "bezier" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Camera:position:x") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"handle_modes": PackedInt32Array(0), +"points": PackedFloat32Array(0, -0.25, 0, 0.25, 0), +"times": PackedFloat32Array(0) +} +tracks/1/type = "bezier" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("Camera:position:y") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"handle_modes": PackedInt32Array(0), +"points": PackedFloat32Array(0, -0.25, 0, 0.25, 0), +"times": PackedFloat32Array(0) +} + +[sub_resource type="Animation" id="Animation_8ku67"] +resource_name = "sprint" +length = 2.0 +loop_mode = 1 +tracks/0/type = "bezier" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Camera:position:x") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"handle_modes": PackedInt32Array(0, 1, 0, 1, 0), +"points": PackedFloat32Array(0.06, -0.25, 0, 0.25, -0.01, 0, 0, 0, 0, 0, -0.06, -0.25, 0.01, 0.25, 0.01, 0, 0, 0, 0, 0, 0.06, -0.25, -0.01, 0.25, 0), +"times": PackedFloat32Array(0, 0.5, 1, 1.5, 2) +} +tracks/1/type = "bezier" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("Camera:position:y") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"handle_modes": PackedInt32Array(0, 0, 0, 0, 0), +"points": PackedFloat32Array(0.05, -0.25, 0, 0.2, -0.01, 0, -0.2, 0.000186046, 0.2, 0.000186046, 0.05, -0.2, -0.01, 0.2, -0.01, 0, -0.2, 0, 0.2, 0, 0.05, -0.2, -0.01, 0.25, 0), +"times": PackedFloat32Array(0, 0.5, 1, 1.5, 2) +} + +[sub_resource type="Animation" id="Animation_lrqmv"] +resource_name = "walk" +length = 2.0 +loop_mode = 1 +tracks/0/type = "bezier" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Camera:position:x") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"handle_modes": PackedInt32Array(0, 1, 0, 1, 0), +"points": PackedFloat32Array(0.04, -0.25, 0, 0.25, 0, 0, 0, 0, 0, 0, -0.04, -0.25, 0, 0.25, 0, 0, 0, 0, 0, 0, 0.04, -0.25, 0, 0.25, 0), +"times": PackedFloat32Array(0, 0.5, 1, 1.5, 2) +} +tracks/1/type = "bezier" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("Camera:position:y") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"handle_modes": PackedInt32Array(0, 0, 0, 0, 0), +"points": PackedFloat32Array(-0.05, -0.25, 0, 0.2, 0.005, 0, -0.2, 0.000186046, 0.2, 0.000186046, -0.05, -0.2, 0.005, 0.2, 0.005, 0, -0.2, 0, 0.2, 0, -0.05, -0.2, 0.005, 0.25, 0), +"times": PackedFloat32Array(0, 0.5, 1, 1.5, 2) +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_o0unb"] +_data = { +"RESET": SubResource("Animation_gh776"), +"sprint": SubResource("Animation_8ku67"), +"walk": SubResource("Animation_lrqmv") +} + +[sub_resource type="Animation" id="Animation_fvvjq"] +length = 0.001 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Camera:rotation") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector3(0, 0, 0)] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("Camera:position") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector3(0, 0, 0)] +} + +[sub_resource type="Animation" id="Animation_s07ye"] +resource_name = "jump" +length = 3.0 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Camera:rotation") +tracks/0/interp = 2 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.6, 3), +"transitions": PackedFloat32Array(1, 1, 1), +"update": 0, +"values": [Vector3(0, 0, 0), Vector3(0.0349066, 0, 0), Vector3(0, 0, 0)] +} + +[sub_resource type="Animation" id="Animation_l1rph"] +resource_name = "land_left" +length = 1.5 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Camera:rotation") +tracks/0/interp = 2 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.5, 1.5), +"transitions": PackedFloat32Array(1, 1, 1), +"update": 0, +"values": [Vector3(0, 0, 0), Vector3(-0.0349066, 0, 0.0174533), Vector3(0, 0, 0)] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("Camera:position") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 0.5, 1.5), +"transitions": PackedFloat32Array(1, 1, 1), +"update": 0, +"values": [Vector3(0, 0, 0), Vector3(0, -0.1, 0), Vector3(0, 0, 0)] +} + +[sub_resource type="Animation" id="Animation_vsknp"] +resource_name = "land_right" +length = 1.5 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Camera:rotation") +tracks/0/interp = 2 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.5, 1.5), +"transitions": PackedFloat32Array(1, 1, 1), +"update": 0, +"values": [Vector3(0, 0, 0), Vector3(-0.0349066, 0, -0.0174533), Vector3(0, 0, 0)] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("Camera:position") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 0.5, 1.5), +"transitions": PackedFloat32Array(1, 1, 1), +"update": 0, +"values": [Vector3(0, 0, 0), Vector3(0, -0.1, 0), Vector3(0, 0, 0)] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_qeg5r"] +_data = { +"RESET": SubResource("Animation_fvvjq"), +"jump": SubResource("Animation_s07ye"), +"land_left": SubResource("Animation_l1rph"), +"land_right": SubResource("Animation_vsknp") +} + +[sub_resource type="Theme" id="Theme_wdf0f"] +MarginContainer/constants/margin_bottom = 10 +MarginContainer/constants/margin_left = 10 +MarginContainer/constants/margin_right = 10 +MarginContainer/constants/margin_top = 10 + +[sub_resource type="SphereShape3D" id="SphereShape3D_k4wwl"] + +[node name="Character" type="CharacterBody3D" node_paths=PackedStringArray("HEAD", "CAMERA", "HEADBOB_ANIMATION", "JUMP_ANIMATION", "CROUCH_ANIMATION", "COLLISION_MESH")] +collision_layer = 2 +collision_mask = 3 +script = SubResource("GDScript_r0lhh") +default_reticle = "res://godotsteam_sync_example/fpc/reticles/reticle_1.tscn" +HEAD = NodePath("Head") +CAMERA = NodePath("Head/Camera") +HEADBOB_ANIMATION = NodePath("Head/HeadbobAnimation") +JUMP_ANIMATION = NodePath("Head/JumpAnimation") +CROUCH_ANIMATION = NodePath("CrouchAnimation") +COLLISION_MESH = NodePath("Collision") +JUMP = "jump" +LEFT = "left" +RIGHT = "right" +FORWARD = "forward" +BACKWARD = "backward" +PAUSE = "pause" + +[node name="Mesh" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +mesh = SubResource("CapsuleMesh_jw1de") + +[node name="Collision" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +shape = SubResource("CapsuleShape3D_uy03j") + +[node name="CrouchAnimation" type="AnimationPlayer" parent="."] +libraries = { +"": SubResource("AnimationLibrary_5e5t5") +} + +[node name="Head" type="Node3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) + +[node name="Camera" type="Camera3D" parent="Head"] + +[node name="HeadbobAnimation" type="AnimationPlayer" parent="Head"] +libraries = { +"": SubResource("AnimationLibrary_o0unb") +} +blend_times = [&"RESET", &"RESET", 0.5, &"RESET", &"walk", 0.5, &"walk", &"RESET", 0.5] + +[node name="JumpAnimation" type="AnimationPlayer" parent="Head"] +libraries = { +"": SubResource("AnimationLibrary_qeg5r") +} +speed_scale = 4.0 + +[node name="UserInterface" type="Control" parent="."] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 1 + +[node name="DebugPanel" type="PanelContainer" parent="UserInterface"] +visible = false +layout_mode = 0 +offset_left = 10.0 +offset_top = 10.0 +offset_right = 453.0 +offset_bottom = 50.0 +theme = SubResource("Theme_wdf0f") +script = ExtResource("3_x1wcc") + +[node name="MarginContainer" type="MarginContainer" parent="UserInterface/DebugPanel"] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="UserInterface/DebugPanel/MarginContainer"] +layout_mode = 2 + +[node name="CrouchCeilingDetection" type="ShapeCast3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +shape = SubResource("SphereShape3D_k4wwl") +target_position = Vector3(0, 0.5, 0) + +[node name="EditorModule" type="Node" parent="."] +script = ExtResource("3_v3ckk") + +[node name="TransformSync3D" type="Node" parent="." node_paths=PackedStringArray("object_player")] +script = ExtResource("4_ch187") +object_player = NodePath("..") diff --git a/godotsteam_sync_example/fpc/debug.gd b/godotsteam_sync_example/fpc/debug.gd new file mode 100644 index 0000000..efdb7a4 --- /dev/null +++ b/godotsteam_sync_example/fpc/debug.gd @@ -0,0 +1,18 @@ +extends PanelContainer + + +func _process(delta): + if visible: + pass + +func add_property(title : String, value, order : int): # This can either be called once for a static property or called every frame for a dynamic property + var target + target = $MarginContainer/VBoxContainer.find_child(title, true, false) # I have no idea what true and false does here, the function should be more specific + if !target: + target = Label.new() # Debug lines are of type Label + $MarginContainer/VBoxContainer.add_child(target) + target.name = title + target.text = title + ": " + str(value) + elif visible: + target.text = title + ": " + str(value) + $MarginContainer/VBoxContainer.move_child(target, order) diff --git a/godotsteam_sync_example/fpc/reticles/reticle_0.tscn b/godotsteam_sync_example/fpc/reticles/reticle_0.tscn new file mode 100644 index 0000000..2828124 --- /dev/null +++ b/godotsteam_sync_example/fpc/reticles/reticle_0.tscn @@ -0,0 +1,37 @@ +[gd_scene load_steps=2 format=3 uid="uid://coqpusufa8a6k"] + +[sub_resource type="GDScript" id="GDScript_10f85"] +script/source = "extends CenterContainer + + +@export_category(\"Reticle\") +@export_group(\"Nodes\") +@export var character : CharacterBody3D + +@export_group(\"Settings\") +@export var dot_size : int = 1 +@export var dot_color : Color = Color.WHITE + + +func _process(_delta): + if visible: # If the reticle is disabled (not visible), don't bother updating it + update_reticle_settings() + +func update_reticle_settings(): + $dot.scale.x = dot_size + $dot.scale.y = dot_size + $dot.color = dot_color +" + +[node name="Reticle" type="CenterContainer"] +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +grow_horizontal = 2 +grow_vertical = 2 +script = SubResource("GDScript_10f85") + +[node name="dot" type="Polygon2D" parent="."] +polygon = PackedVector2Array(-1, -1, 1, -1, 1, 1, -1, 1) diff --git a/godotsteam_sync_example/fpc/reticles/reticle_1.tscn b/godotsteam_sync_example/fpc/reticles/reticle_1.tscn new file mode 100644 index 0000000..bb83b83 --- /dev/null +++ b/godotsteam_sync_example/fpc/reticles/reticle_1.tscn @@ -0,0 +1,104 @@ +[gd_scene load_steps=2 format=3 uid="uid://3mij3cjhkwsm"] + +[sub_resource type="GDScript" id="GDScript_a8kpl"] +script/source = "extends CenterContainer + + +@export_category(\"Reticle\") +@export_group(\"Nodes\") +@export var reticle_lines : Array[Line2D] +@export var character : CharacterBody3D + +@export_group(\"Animate\") +@export var animated_reticle : bool = true +@export var reticle_speed : float = 0.5 +@export var reticle_spread : float = 4.0 + +@export_group(\"Dot Settings\") +@export var dot_size : int = 1 +@export var dot_color : Color = Color.WHITE + +@export_group(\"Line Settings\") +@export var line_color : Color = Color.WHITE +@export var line_width : int = 2 +@export var line_length : int = 10 +@export var line_distance : int = 5 +@export_enum(\"None\", \"Round\") var cap_mode : int = 0 + + +func _process(_delta): + if visible: # If the reticle is disabled (not visible), don't bother updating it + update_reticle_settings() + if animated_reticle: + animate_reticle_lines() + + +func animate_reticle_lines(): + var vel = character.get_real_velocity() + var origin = Vector3(0,0,0) + var pos = Vector2(0,0) + var speed = origin.distance_to(vel) + + reticle_lines[0].position = lerp(reticle_lines[0].position, pos + Vector2(0, -speed * reticle_spread), reticle_speed) + reticle_lines[1].position = lerp(reticle_lines[1].position, pos + Vector2(-speed * reticle_spread, 0), reticle_speed) + reticle_lines[2].position = lerp(reticle_lines[2].position, pos + Vector2(speed * reticle_spread, 0), reticle_speed) + reticle_lines[3].position = lerp(reticle_lines[3].position, pos + Vector2(0, speed * reticle_spread), reticle_speed) + + +func update_reticle_settings(): + # Dot + $dot.scale.x = dot_size + $dot.scale.y = dot_size + $dot.color = dot_color + + # Lines + for line in reticle_lines: + line.default_color = line_color + line.width = line_width + if cap_mode == 0: + line.begin_cap_mode = Line2D.LINE_CAP_NONE + line.end_cap_mode = Line2D.LINE_CAP_NONE + elif cap_mode == 1: + line.begin_cap_mode = Line2D.LINE_CAP_ROUND + line.end_cap_mode = Line2D.LINE_CAP_ROUND + + # Please someone find a better way to do this + reticle_lines[0].points[0].y = -line_distance + reticle_lines[0].points[1].y = -line_length - line_distance + reticle_lines[1].points[0].x = -line_distance + reticle_lines[1].points[1].x = -line_length - line_distance + reticle_lines[2].points[0].x = line_distance + reticle_lines[2].points[1].x = line_length + line_distance + reticle_lines[3].points[0].y = line_distance + reticle_lines[3].points[1].y = line_length + line_distance +" + +[node name="Reticle" type="CenterContainer" node_paths=PackedStringArray("reticle_lines")] +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +grow_horizontal = 2 +grow_vertical = 2 +script = SubResource("GDScript_a8kpl") +reticle_lines = [NodePath("top"), NodePath("left"), NodePath("right"), NodePath("bottom")] + +[node name="dot" type="Polygon2D" parent="."] +polygon = PackedVector2Array(-1, -1, 1, -1, 1, 1, -1, 1) + +[node name="top" type="Line2D" parent="."] +points = PackedVector2Array(0, -5, 0, -15) +width = 2.0 + +[node name="left" type="Line2D" parent="."] +points = PackedVector2Array(-5, 0, -15, 0) +width = 2.0 + +[node name="right" type="Line2D" parent="."] +points = PackedVector2Array(5, 0, 15, 0) +width = 2.0 + +[node name="bottom" type="Line2D" parent="."] +points = PackedVector2Array(0, 5, 0, 15) +width = 2.0 diff --git a/godotsteam_sync_example/test.gd b/godotsteam_sync_example/test.gd new file mode 100644 index 0000000..61510e1 --- /dev/null +++ b/godotsteam_sync_example/test.gd @@ -0,0 +1 @@ +extends Node diff --git a/project.godot b/project.godot index 0a4539f..c963913 100644 --- a/project.godot +++ b/project.godot @@ -11,13 +11,14 @@ config_version=5 [application] config/name="Steamforged Skies" -run/main_scene="res://assets/core/networking/scenes/lobby.tscn" config/features=PackedStringArray("4.3", "Forward Plus") config/icon="res://assets/icon.png" [autoload] GameConsole="*res://addons/ingameconsole/GameConsole.tscn" +NetworkManager="*res://addons/godot_steam_sync/Network/RadomeSteamSync/NetworkManager.gd" +P2P="*res://addons/godot_steam_sync/Network/RadomeSteamSync/P2P.gd" [debug] @@ -35,7 +36,7 @@ project/assembly_name="Steamforged Skies" [editor_plugins] -enabled=PackedStringArray("res://addons/ingameconsole/plugin.cfg") +enabled=PackedStringArray("res://addons/godot_steam_sync/plugin.cfg", "res://addons/ingameconsole/plugin.cfg") [input]