├── addons └── MultiplayCore │ ├── MPBase.gd │ ├── MPExtension.gd │ ├── MPIO.gd │ ├── MPPlayer.gd │ ├── MPPlayersCollection.gd │ ├── MultiPlay.gd │ ├── MultiPlayTool.gd │ ├── auths │ └── MPAuth.gd │ ├── debug_ui │ ├── MultiPlayDebugUI.gd │ └── debug_ui.tscn │ ├── dev_scripts │ └── CertMake.gd │ ├── editor │ ├── scenes │ │ └── player_debug_config.tscn │ ├── scripts │ │ ├── DebuggerPlugin.gd │ │ ├── ExportCheckout.gd │ │ ├── PlayerDebugConfig.gd │ │ └── UpdatePopup.gd │ ├── version_icon.png │ ├── version_icon.png.import │ └── window │ │ ├── debug_configs.tscn │ │ ├── first_restart.tscn │ │ ├── update_popup.tscn │ │ └── welcome_popup.tscn │ ├── fonts │ ├── VictorMono-Medium.ttf │ └── VictorMono-Medium.ttf.import │ ├── gizmos │ ├── Spawner.svg │ └── Spawner.svg.import │ ├── icons │ ├── ENetProtocol.svg │ ├── ENetProtocol.svg.import │ ├── LatencyNetProtocol.svg │ ├── LatencyNetProtocol.svg.import │ ├── MPAnimTreeSync.svg │ ├── MPAnimTreeSync.svg.import │ ├── MPAnimationSync.svg │ ├── MPAnimationSync.svg.import │ ├── MPAuth.svg │ ├── MPAuth.svg.import │ ├── MPBase.svg │ ├── MPBase.svg.import │ ├── MPDebugPlay.svg │ ├── MPDebugPlay.svg.import │ ├── MPDebugPlay_Pressed.svg │ ├── MPDebugPlay_Pressed.svg.import │ ├── MPExtension.svg │ ├── MPExtension.svg.import │ ├── MPNetProtocolBase.svg │ ├── MPNetProtocolBase.svg.import │ ├── MPPeersCollection.svg │ ├── MPPeersCollection.svg.import │ ├── MPPlayer.svg │ ├── MPPlayer.svg.import │ ├── MPStartTransform2D.svg │ ├── MPStartTransform2D.svg.import │ ├── MPStartTransform3D.svg │ ├── MPStartTransform3D.svg.import │ ├── MPSyncBase.svg │ ├── MPSyncBase.svg.import │ ├── MPTransformSync.svg │ ├── MPTransformSync.svg.import │ ├── MultiPlayCore.svg │ ├── MultiPlayCore.svg.import │ ├── MultiPlayIO.svg │ ├── MultiPlayIO.svg.import │ ├── WebSocketNetProtocol.svg │ ├── WebSocketNetProtocol.svg.import │ ├── icon.svg │ └── icon.svg.import │ ├── net_protocols │ ├── ENetProtocol.gd │ ├── LatencyNetProtocol.gd │ ├── MPNetProtocolBase.gd │ └── WebSocketNetProtocol.gd │ ├── plugin.cfg │ ├── scenes │ └── multiplay_player.tscn │ ├── spawner │ └── MPSpawnerTransform2D.gd │ └── synchronizers │ ├── MPAnimTreeSync.gd │ ├── MPAnimationSync.gd │ ├── MPSyncBase.gd │ └── MPTransformSync.gd └── script_templates ├── CharacterBody2D ├── EightWayMovement.gd └── PlatformerMovement.gd └── Node └── AuthTemplate.gd /addons/MultiplayCore/MPBase.gd: -------------------------------------------------------------------------------- 1 | @icon("res://addons/MultiplayCore/icons/MPBase.svg") 2 | @tool 3 | 4 | extends Node 5 | ## Base for MultiPlay Nodes 6 | 7 | class_name MPBase 8 | -------------------------------------------------------------------------------- /addons/MultiplayCore/MPExtension.gd: -------------------------------------------------------------------------------- 1 | @icon("res://addons/MultiplayCore/icons/MPExtension.svg") 2 | @tool 3 | 4 | extends MPBase 5 | ## Base for MultiPlay Extensions 6 | class_name MPExtension 7 | 8 | ## Main MultiPlayCore node 9 | var mpc: MultiPlayCore 10 | 11 | func _ready(): 12 | if get_parent() is MultiPlayCore: 13 | mpc = get_parent() 14 | 15 | ## Called by MultiPlay Core when it's ready 16 | func _mpc_ready(): 17 | pass 18 | 19 | func _enter_tree(): 20 | if Engine.is_editor_hint(): 21 | update_configuration_warnings() 22 | 23 | func _get_configuration_warnings(): 24 | var warns = [] 25 | if not get_parent() is MultiPlayCore: 26 | warns.append("MultiPlay extensions must be a child of MultiPlayCore") 27 | return warns 28 | -------------------------------------------------------------------------------- /addons/MultiplayCore/MPIO.gd: -------------------------------------------------------------------------------- 1 | @icon("res://addons/MultiplayCore/icons/MultiPlayIO.svg") 2 | extends MPBase 3 | ## Input/Output Singleton for MultiPlay 4 | 5 | class_name MultiPlayIO 6 | 7 | ## Main MultiPlay Core 8 | var mpc: MultiPlayCore = null 9 | var plr_id: int = 0 10 | 11 | ## Log data to the output console 12 | func logdata(data): 13 | var roles = "" 14 | 15 | roles = roles + "[" + str(plr_id) + "] " 16 | 17 | print_rich(roles + str(data)) 18 | 19 | ## Log warning to the output console 20 | func logwarn(data): 21 | logdata("[[color=yellow]WARN[/color]] " + str(data)) 22 | 23 | ## Log errpr to the output console 24 | func logerr(data): 25 | logdata("[[color=red]ERR[/color]] " + str(data)) 26 | -------------------------------------------------------------------------------- /addons/MultiplayCore/MPPlayer.gd: -------------------------------------------------------------------------------- 1 | @icon("res://addons/MultiplayCore/icons/MPPlayer.svg") 2 | 3 | extends MPBase 4 | ## MultiPlay Player Node 5 | class_name MPPlayer 6 | 7 | ## Ping in ms 8 | @export var ping_ms: int 9 | ## Handshake data 10 | @export var handshake_data = {} 11 | ## Authentication Data 12 | var auth_data = {} 13 | ## ID of the player 14 | @export var player_id: int = 0 15 | ## Get MultiPlayCore 16 | var mpc: MultiPlayCore 17 | ## The player node created from the template, see [member MultiPlayCore.player_scene] 18 | var player_node: Node 19 | ## Determines if this player is local 20 | var is_local: bool = false 21 | ## Determines if this player network is ready 22 | var is_ready: bool = false 23 | ## Determines the player index, not to be confused with player_id. 24 | var player_index: int = 0 25 | var _internal_peer: MultiplayerPeer 26 | var _initcount = 20 27 | ## Determines if swap is focusing this player, Swap mode only. 28 | var is_swap_focused: bool = false 29 | ## The resource path of the template player. 30 | var player_node_resource_path: String = "" 31 | 32 | var _local_got_handshake = false 33 | # Determine if handshake event has already been emitted 34 | var _handshake_is_ready = false 35 | 36 | ## On player ready. Only emit locally 37 | signal player_ready 38 | ## On handshake data is ready. Emit to all players 39 | signal handshake_ready(handshake_data: Dictionary) 40 | ## On swap focused, Swap mode only 41 | signal swap_focused(old_swap: MPPlayer) 42 | ## On swap unfocused, Swap mode only 43 | signal swap_unfocused(new_swap: MPPlayer) 44 | 45 | # Called when the node enters the scene tree for the first time. 46 | func _ready(): 47 | if mpc.mode != mpc.PlayMode.Online: 48 | is_ready = true 49 | player_ready.emit() 50 | _send_handshake_data(handshake_data) 51 | mpc.connected_to_server.emit(self) 52 | 53 | _internal_peer = multiplayer.multiplayer_peer 54 | #_internal_peer = multiplayer.multiplayer_peer 55 | 56 | mpc.swap_changed.connect(_on_swap_changed) 57 | 58 | if mpc.mode == mpc.PlayMode.Swap and mpc.current_swap_index == player_index: 59 | is_swap_focused = true 60 | swap_focused.emit(null) 61 | 62 | func _on_swap_changed(new, old): 63 | var new_focus = mpc.players.get_player_by_index(new) 64 | var old_focus = mpc.players.get_player_by_index(old) 65 | 66 | if new == player_index: 67 | is_swap_focused = true 68 | swap_focused.emit(old_focus) 69 | 70 | if new != player_index and is_swap_focused == true: 71 | is_swap_focused = false 72 | swap_unfocused.emit(new_focus) 73 | 74 | ## Translate input action to the intended ones. 75 | ## 76 | ## In Online/Solo, it'll return the same input action name[br] 77 | ## In One Screen, it'll return new input action, each assigned to it's own device index.[br] 78 | ## In Swap, if swap is active on this player, it'll return the same input action name. If not, it'll return the "empty" action.[br] 79 | ## 80 | func translate_action(origin_action: StringName) -> StringName: 81 | if mpc.mode == mpc.PlayMode.Online: 82 | if !is_local: 83 | return "empty" 84 | return origin_action 85 | 86 | if mpc.mode == mpc.PlayMode.Swap: 87 | if mpc.current_swap_index == player_index: 88 | return origin_action 89 | return "empty" 90 | 91 | if mpc.mode == mpc.PlayMode.OneScreen: 92 | var action_name = origin_action + "_" + str(player_index) 93 | 94 | if !InputMap.has_action(action_name): 95 | var events = InputMap.action_get_events(origin_action) 96 | 97 | for e in events: 98 | if not (e is InputEventJoypadButton or e is InputEventJoypadMotion): 99 | continue 100 | 101 | var nevent = e.duplicate(true) 102 | nevent.device = player_index 103 | 104 | if !InputMap.has_action(action_name): 105 | InputMap.add_action(action_name) 106 | InputMap.action_add_event(action_name, nevent) 107 | 108 | return action_name 109 | 110 | return origin_action 111 | 112 | ## Just like translate_action, but in shorter format 113 | func ma(action_name: StringName): 114 | return translate_action(action_name) 115 | 116 | @rpc("any_peer") 117 | func _get_handshake_data(): 118 | if is_local: 119 | rpc_id(multiplayer.get_remote_sender_id(), "_recv_handshake_data", handshake_data) 120 | 121 | @rpc("any_peer") 122 | func _recv_handshake_data(hs): 123 | handshake_data = hs 124 | _on_handshake_ready() 125 | 126 | func _on_handshake_ready(): 127 | if _handshake_is_ready: 128 | return 129 | if handshake_data.keys().has("_net_internal"): 130 | if handshake_data._net_internal.keys().has("auth_data"): 131 | auth_data = handshake_data._net_internal.auth_data 132 | handshake_ready.emit(handshake_data) 133 | _handshake_is_ready = true 134 | 135 | func _check_if_net_from_id(id): 136 | if mpc.mode != mpc.PlayMode.Online: 137 | return true 138 | return multiplayer.get_remote_sender_id() == id 139 | 140 | @rpc("authority", "call_local") 141 | func _send_handshake_data(data): 142 | handshake_data = data 143 | _handshake_is_ready = false 144 | _on_handshake_ready() 145 | 146 | @rpc("any_peer", "call_local") 147 | func _internal_ping(server_time: float): 148 | if !_check_if_net_from_id(1): 149 | return 150 | if !is_local: 151 | if not _local_got_handshake: 152 | _local_got_handshake = true 153 | rpc("_get_handshake_data") 154 | return 155 | var current_time = Time.get_unix_time_from_system() 156 | 157 | ping_ms = int(round((current_time - server_time) * 1000)) 158 | 159 | if not is_ready: 160 | if _initcount < 1: 161 | # Connnection Ready! 162 | is_ready = true 163 | player_ready.emit() 164 | 165 | mpc._on_local_player_ready() 166 | 167 | rpc("_send_handshake_data", handshake_data) 168 | else: 169 | _initcount = _initcount - 1 170 | 171 | ## Disconnect the player, this is intended for local use. 172 | func disconnect_player(): 173 | if _internal_peer: 174 | if mpc.online_connected: 175 | mpc.online_connected = false 176 | mpc.disconnected_from_server.emit("USER_REQUESTED_DISCONNECT") 177 | 178 | _internal_peer.close() 179 | 180 | ## Kick the player, Server only. 181 | func kick(reason: String = ""): 182 | rpc_id(player_id, "_net_kick", reason) 183 | 184 | ## Respawn player node, Server only. 185 | func respawn_node(): 186 | rpc("_net_despawn") 187 | rpc("_net_spawn_node") 188 | 189 | ## Despawn player node, Server only. 190 | func despawn_node(): 191 | rpc("_net_despawn") 192 | 193 | ## Spawn player node, Server only. 194 | func spawn_node(): 195 | rpc("_net_spawn_node") 196 | 197 | @rpc("any_peer", "call_local") 198 | func _net_kick(reason: String = ""): 199 | if !_check_if_net_from_id(1): 200 | return 201 | 202 | MPIO.logdata("Kicked from the server: " + str(reason)) 203 | 204 | if mpc.online_connected: 205 | mpc.online_connected = false 206 | mpc.disconnected_from_server.emit(reason) 207 | 208 | _internal_peer.close() 209 | 210 | @rpc("any_peer", "call_local") 211 | func _net_despawn(): 212 | if !_check_if_net_from_id(1): 213 | return 214 | 215 | if player_node: 216 | player_node.free() 217 | player_node = null 218 | 219 | @rpc("any_peer", "call_local") 220 | func _net_spawn_node(): 221 | if !_check_if_net_from_id(1): 222 | return 223 | if player_node and is_instance_valid(player_node): 224 | MPIO.logwarn("spawn_node: Player node already exists. Free it first with despawn_node or use respawn_node") 225 | return 226 | 227 | if player_node_resource_path: 228 | var packed_load = load(player_node_resource_path) 229 | var pscene = packed_load.instantiate() 230 | 231 | if mpc.assign_client_authority: 232 | pscene.set_multiplayer_authority(player_id, true) 233 | 234 | add_child(pscene, true) 235 | 236 | is_ready = true 237 | _send_handshake_data(handshake_data) 238 | 239 | if is_local: 240 | player_ready.emit() 241 | 242 | if mpc.mode == mpc.PlayMode.Swap: 243 | mpc.swap_to(0) 244 | 245 | player_node = pscene 246 | -------------------------------------------------------------------------------- /addons/MultiplayCore/MPPlayersCollection.gd: -------------------------------------------------------------------------------- 1 | @icon("res://addons/MultiplayCore/icons/MPPeersCollection.svg") 2 | 3 | extends MPBase 4 | ## Collection of players 5 | class_name MPPlayersCollection 6 | 7 | ## Dictionary containing [MPPlayer] 8 | var players: Dictionary = {} 9 | 10 | ## Get player by ID 11 | func get_player_by_id(player_id: int) -> MPPlayer: 12 | if players.keys().has(player_id): 13 | return players[player_id] 14 | return null 15 | 16 | ## Get player by index 17 | func get_player_by_index(player_index: int) -> MPPlayer: 18 | for p in players.values(): 19 | if p and is_instance_valid(p) and p.player_index == player_index: 20 | return p 21 | 22 | return null 23 | 24 | ## Get all players 25 | func get_players() -> Dictionary: 26 | return players 27 | 28 | func _internal_add_player(player_id, player: MPPlayer): 29 | players[player_id] = player 30 | 31 | func _internal_remove_player(player_id): 32 | players.erase(player_id) 33 | 34 | func _internal_clear_all(): 35 | players.clear() 36 | 37 | func _get_player_peer_ids(): 38 | var p = multiplayer.get_peers() 39 | p.append(1) 40 | return p 41 | 42 | func _internal_ping(): 43 | for p in players.values(): 44 | if p and is_instance_valid(p) and p.player_id in _get_player_peer_ids(): 45 | p.rpc("_internal_ping", Time.get_unix_time_from_system()) 46 | 47 | ## Despawn all player's node 48 | func despawn_node_all(): 49 | for p in players.values(): 50 | if is_instance_valid(p): 51 | p.despawn_node() 52 | 53 | ## Respawn all player's node 54 | func respawn_node_all(): 55 | for p in players.values(): 56 | if is_instance_valid(p): 57 | p.respawn_node() 58 | 59 | ## Spawn all player's node 60 | func spawn_node_all(): 61 | for p in players.values(): 62 | if is_instance_valid(p): 63 | p.spawn_node() 64 | -------------------------------------------------------------------------------- /addons/MultiplayCore/MultiPlay.gd: -------------------------------------------------------------------------------- 1 | @icon("res://addons/MultiplayCore/icons/MultiPlayCore.svg") 2 | @tool 3 | 4 | extends MPBase 5 | ## Core of everything MultiPlay 6 | class_name MultiPlayCore 7 | 8 | ## MultiPlay Core Version 9 | const MP_VERSION = "1.1.0-beta3" 10 | 11 | ## MultiPlay Core Version Name 12 | const MP_VERSION_NAME = "Packet Unit" 13 | 14 | ## On network scene loaded 15 | signal scene_loaded 16 | ## Emit when new player is connected to the server, Emit to all players in the server. 17 | signal player_connected(player: MPPlayer) 18 | ## Emit when player has disconnected from the server, Emit to all players in the server. 19 | signal player_disconnected(player: MPPlayer) 20 | ## Emit when client has connected to the server, Only emit locally. 21 | signal connected_to_server(localplayer: MPPlayer) 22 | ## Emit when client has disconnected from the server, Only emit locally. 23 | signal disconnected_from_server(reason: String) 24 | ## Emit when client faced connection error 25 | signal connection_error(reason: ConnectionError) 26 | ## Emit when swap index has changed. Only emit in Swap Play mode 27 | signal swap_changed(to_index: int, old_index: int) 28 | ## On server started 29 | signal server_started 30 | ## On server stopped 31 | signal server_stopped 32 | 33 | ## Methods to use for multiplayer game 34 | enum PlayMode { 35 | ## Network enabled multiplayer 36 | Online, 37 | ## Single screen multiplayer. User can play with multiple controllers/devices. 38 | OneScreen, 39 | ## Swap mode. Intended to be played with one player, user can switch to the peer they wanted to control. 40 | Swap, 41 | ## Solo, self explanatory 42 | Solo 43 | } 44 | 45 | ## List of connection errors 46 | enum ConnectionError { 47 | ## Unknown reason 48 | UNKNOWN, 49 | ## The server's full 50 | SERVER_FULL, 51 | ## Authentication Failure 52 | AUTH_FAILED, 53 | ## Connection timed out 54 | TIMEOUT, 55 | ## Failure during connection 56 | CONNECTION_FAILURE, 57 | ## Internal handshake data cannot be readed by the server 58 | INVALID_HANDSHAKE, 59 | ## Client's Multiplay version is not compatible with the server 60 | VERSION_MISMATCH, 61 | ## Server has been closed 62 | SERVER_CLOSED, 63 | } 64 | 65 | ## List of network connection states 66 | enum ConnectionState { 67 | ## Connection had never been active before 68 | IDLE, 69 | ## Connecting to the server 70 | CONNECTING, 71 | ## Client and server is now exchanging data. Authentication also happens here. 72 | HANDSHAKING, 73 | ## Client is waiting the server to send the player node back 74 | AWAIT_PLR_NODE, 75 | ## Client's pinging the server. and is doing first time initialization. 76 | PINGING, 77 | ## Connected and ready to go! 78 | CONNECTED, 79 | ## Connection no longer active, whatever if it's disconnection, kicked, or server close. 80 | CONNECTION_CLOSED, 81 | } 82 | 83 | @export_subgroup("Network") 84 | ## Which ip to bind on in online game host. 85 | @export var bind_address: String = "*" 86 | ## Which port to use in online game host. 87 | @export_range(0, 65535) var port: int = 4200 88 | ## Max players for the game. 89 | @export var max_players: int = 2 90 | ## Time in milliseconds before timing out the user. 91 | @export var connect_timeout_ms: int = 50000 92 | 93 | @export_subgroup("Spawn Meta") 94 | ## Your own template player scene. 95 | @export var player_scene: PackedScene 96 | ## The first scene to load 97 | @export var first_scene: PackedScene 98 | ## Should Client authority be assigned automatically? 99 | @export var assign_client_authority: bool = true 100 | ## Should the server automatically spawn the player scene? 101 | @export var auto_spawn_player_scene: bool = true 102 | 103 | @export_subgroup("Inputs") 104 | ## Which action key to use for swap mode. 105 | @export var swap_input_action: String: 106 | get: 107 | return swap_input_action 108 | set(value): 109 | swap_input_action = value 110 | if Engine.is_editor_hint(): 111 | update_configuration_warnings() 112 | 113 | @export_subgroup("Debug Options") 114 | ## Enable Debug UI 115 | @export var debug_gui_enabled: bool = true 116 | 117 | func _get_configuration_warnings(): 118 | var warns = [] 119 | if swap_input_action == "": 120 | warns.append("Swap Input action currently not set.") 121 | 122 | var net_count = 0 123 | for c in get_children(): 124 | if c is MPNetProtocolBase: 125 | net_count = net_count + 1 126 | if net_count > 1: 127 | break 128 | 129 | if net_count == 0: 130 | warns.append("No Net Protocol set for online mode, add one by Add Child Node > Type in search 'Protocol'") 131 | elif net_count > 1: 132 | warns.append("Only 1 Net Protocol can be set.") 133 | 134 | 135 | return warns 136 | 137 | var _net_data = { 138 | current_scene_path = "" 139 | } 140 | 141 | ## Current playmode 142 | var mode: PlayMode = PlayMode.Online 143 | ## MultiplayerPeer for the game 144 | var online_peer: MultiplayerPeer = null 145 | 146 | ## If connected in online mode 147 | var online_connected: bool = false 148 | 149 | ## If player node is ready 150 | var player_node_ready: bool = false 151 | 152 | ## Players Collection 153 | var players: MPPlayersCollection 154 | var _plr_spawner: MultiplayerSpawner 155 | ## Determines if MultiPlay has started 156 | var started: bool = false 157 | ## Determines if MultiPlay is running as server 158 | var is_server: bool = false 159 | ## The local player node 160 | var local_player: MPPlayer = null 161 | ## Current player count 162 | var player_count: int = 0 163 | ## Current scene node 164 | var current_scene: Node = null 165 | ## Network connection state 166 | var connection_state: ConnectionState = ConnectionState.IDLE 167 | 168 | ## Debug Status 169 | var debug_status_txt = "" 170 | 171 | ## Current swap index, Swap mode only. 172 | var current_swap_index: int = 0 173 | 174 | var _join_handshake_data = {} 175 | var _join_credentials_data = {} 176 | var _extensions = [] 177 | 178 | var _net_protocol: MPNetProtocolBase = null 179 | var _debug_join_address = "" 180 | 181 | var _debug_bootui = null 182 | 183 | func _ready(): 184 | if Engine.is_editor_hint(): 185 | child_entered_tree.connect(_tool_child_refresh_warns) 186 | child_exiting_tree.connect(_tool_child_refresh_warns) 187 | return 188 | _presetup_nodes() 189 | 190 | disconnected_from_server.connect(_on_local_disconnected) 191 | 192 | if OS.is_debug_build(): 193 | var bind_address_url = bind_address 194 | 195 | if bind_address_url == "*": 196 | bind_address_url = "127.0.0.1" 197 | 198 | _debug_join_address = bind_address_url + ":" + str(port) 199 | 200 | if OS.is_debug_build() and _net_protocol: 201 | var debug_override = _net_protocol._override_debug_url(bind_address, port) 202 | 203 | if !debug_override: 204 | var bind_address_url = bind_address 205 | 206 | if bind_address_url == "*": 207 | bind_address_url = "127.0.0.1" 208 | 209 | _debug_join_address = bind_address_url + ":" + str(port) 210 | else: 211 | _debug_join_address = debug_override 212 | 213 | if debug_gui_enabled and OS.is_debug_build(): 214 | var dgui = preload("res://addons/MultiplayCore/debug_ui/debug_ui.tscn").instantiate() 215 | _debug_bootui = dgui.get_node("Layout/BootUI") 216 | 217 | _debug_bootui.mpc = self 218 | _debug_bootui.join_address = _debug_join_address 219 | 220 | add_child(dgui) 221 | 222 | # Parse CLI arguments 223 | var arguments = {} 224 | for argument in OS.get_cmdline_args(): 225 | if argument.find("=") > -1: 226 | var key_value = argument.split("=") 227 | arguments[key_value[0].lstrip("--")] = key_value[1] 228 | else: 229 | arguments[argument.lstrip("--")] = "" 230 | 231 | if arguments.has("port"): 232 | port = int(arguments.port) 233 | 234 | if arguments.has("server"): 235 | _online_host(arguments.has("act-client")) 236 | 237 | if arguments.has("client"): 238 | var client_url = "" 239 | if arguments.has("url"): 240 | client_url = arguments.url 241 | _online_join(client_url) 242 | 243 | if OS.has_feature("debug"): 244 | EngineDebugger.register_message_capture("mpc", _debugger_msg_capture) 245 | EngineDebugger.send_message("mpc:session_ready", []) 246 | 247 | for ext in get_children(): 248 | if ext is MPExtension: 249 | _extensions.append(ext) 250 | 251 | if ext is MPNetProtocolBase: 252 | _net_protocol = ext 253 | 254 | ext.mpc = self 255 | ext._mpc_ready() 256 | 257 | ## Register Network Extension for this MPC (Extension API) 258 | func register_net_extension(ext: MPNetProtocolBase): 259 | _net_protocol = ext 260 | 261 | ## Register any extension 262 | func register_extension(ext: MPExtension): 263 | if _extensions.find(ext) == -1: 264 | _extensions.append(ext) 265 | 266 | ext.mpc = self 267 | ext._mpc_ready() 268 | 269 | func _debugger_msg_capture(msg, data): 270 | if msg.begins_with("start_"): 271 | var cred = {} 272 | var handsh = {} 273 | var session_id = data[0] 274 | 275 | # Load up debug configs 276 | var fp = FileAccess.open("user://mp_debug_configs", FileAccess.READ) 277 | if fp: 278 | var fp_data: Dictionary = JSON.parse_string(fp.get_as_text()) 279 | fp.close() 280 | 281 | if fp_data.keys().has("debug_configs"): 282 | if fp_data.debug_configs.keys().has(str(session_id)): 283 | handsh = JSON.parse_string(fp_data.debug_configs[str(session_id)].handshake) 284 | cred = JSON.parse_string(fp_data.debug_configs[str(session_id)].credentials) 285 | 286 | if msg == "start_server": 287 | start_online_host(true, handsh, cred) 288 | if _debug_bootui: 289 | _debug_bootui.boot_close() 290 | if msg == "start_client": 291 | start_online_join(_debug_join_address, handsh, cred) 292 | if _debug_bootui: 293 | _debug_bootui.boot_close() 294 | return true 295 | 296 | func _tool_child_refresh_warns(new_child): 297 | update_configuration_warnings() 298 | 299 | func _init_data(): 300 | print("MultiPlay Core v" + MP_VERSION + " - https://mpc.himaji.xyz - https://discord.gg/PXh9kZ9GzC") 301 | print("") 302 | started = true 303 | MPIO.mpc = self 304 | InputMap.add_action("empty") 305 | _setup_nodes() 306 | 307 | if mode == PlayMode.Online and _net_protocol == null: 308 | assert(false, "NetProtocol is current not set.") 309 | 310 | ## Start one screen mode 311 | func start_one_screen(): 312 | mode = PlayMode.OneScreen 313 | _online_host() 314 | 315 | for i in range(0, max_players): 316 | create_player(i, {}) 317 | 318 | ## Start solo mode 319 | func start_solo(): 320 | mode = PlayMode.Solo 321 | _online_host() 322 | 323 | create_player(1, {}) 324 | 325 | ## Start swap mode 326 | func start_swap(): 327 | mode = PlayMode.Swap 328 | _online_host() 329 | 330 | if swap_input_action == "": 331 | MPIO.logwarn("swap_input_action currently not set. Please set it first in MultiplayCore Node") 332 | 333 | for i in range(0, max_players): 334 | create_player(i, {}) 335 | 336 | func close_server(): 337 | _kick_player_request_all(MultiPlayCore.ConnectionError.SERVER_CLOSED) 338 | online_peer.close() 339 | online_connected = false 340 | server_stopped.emit() 341 | 342 | func _unhandled_input(event): 343 | if mode == PlayMode.Swap: 344 | if event.is_action_pressed(swap_input_action): 345 | swap_increment() 346 | 347 | ## Swap control to player according to index. Swap mode only 348 | func swap_increment(): 349 | if mode != PlayMode.Swap: 350 | MPIO.logwarn("swap_player: Not in swap mode") 351 | 352 | var old_index = current_swap_index 353 | current_swap_index = current_swap_index + 1 354 | if current_swap_index >= player_count: 355 | current_swap_index = 0 356 | 357 | swap_changed.emit(current_swap_index, old_index) 358 | 359 | ## Specifically Swap to index. Swap mode only 360 | func swap_to(index): 361 | if mode != PlayMode.Swap: 362 | MPIO.logwarn("swap_player: Not in swap mode") 363 | 364 | var old_index = current_swap_index 365 | current_swap_index = index 366 | if current_swap_index >= player_count or current_swap_index < 0: 367 | current_swap_index = 0 368 | 369 | swap_changed.emit(current_swap_index, old_index) 370 | 371 | func _presetup_nodes(): 372 | players = MPPlayersCollection.new() 373 | players.name = "Players" 374 | add_child(players, true) 375 | 376 | _plr_spawner = MultiplayerSpawner.new() 377 | _plr_spawner.name = "PlayerSpawner" 378 | _plr_spawner.spawn_function = _player_spawned 379 | add_child(_plr_spawner, true) 380 | 381 | func _setup_nodes(): 382 | _plr_spawner.spawn_path = players.get_path() 383 | 384 | ## Start online mode as host 385 | func start_online_host(act_client: bool = false, act_client_handshake_data: Dictionary = {}, act_client_credentials_data: Dictionary = {}): 386 | mode = PlayMode.Online 387 | _online_host(act_client, act_client_handshake_data, act_client_credentials_data) 388 | 389 | ## Start online mode as client 390 | func start_online_join(url: String, handshake_data: Dictionary = {}, credentials_data: Dictionary = {}): 391 | mode = PlayMode.Online 392 | _online_join(url, handshake_data, credentials_data) 393 | 394 | func _online_host(act_client: bool = false, act_client_handshake_data: Dictionary = {}, act_client_credentials_data: Dictionary = {}): 395 | _init_data() 396 | 397 | debug_status_txt = "Server Started!" 398 | connection_state = ConnectionState.CONNECTED 399 | 400 | is_server = true 401 | 402 | online_peer = await _net_protocol.host(port, bind_address, max_players) 403 | 404 | MPIO.logdata("Starting server at port " + str(port)) 405 | 406 | multiplayer.multiplayer_peer = online_peer 407 | multiplayer.peer_connected.connect(_network_player_connected) 408 | multiplayer.peer_disconnected.connect(_network_player_disconnected) 409 | 410 | if first_scene: 411 | load_scene(first_scene.resource_path) 412 | 413 | if act_client: 414 | _join_handshake_data = act_client_handshake_data 415 | _join_credentials_data = act_client_credentials_data 416 | #create_player(1, act_client_handshake_data) 417 | _network_player_connected(1) 418 | _client_connected() 419 | 420 | # Update debug UI if exists 421 | if _debug_bootui: 422 | _debug_bootui.boot_close() 423 | 424 | server_started.emit() 425 | 426 | func _online_join(address: String, handshake_data: Dictionary = {}, credentials_data: Dictionary = {}): 427 | _init_data() 428 | 429 | debug_status_txt = "Connecting to " + address + "..." 430 | connection_state = ConnectionState.CONNECTING 431 | 432 | _join_handshake_data = handshake_data 433 | _join_credentials_data = credentials_data 434 | 435 | var result_url = "" 436 | 437 | var ip_split = Array(address.split("/")) 438 | var hostname = ip_split[0] 439 | var real_hostname = hostname.split(":")[0] 440 | 441 | ip_split.pop_front() 442 | 443 | var url_path = "/".join(PackedStringArray(ip_split)) 444 | 445 | if url_path.length() > 0: 446 | url_path = "/" + url_path 447 | 448 | var portsplit = hostname.split(":") 449 | var port_num = port 450 | 451 | if portsplit.size() > 1: 452 | port_num = int(portsplit[1]) 453 | 454 | online_peer = await _net_protocol.join(real_hostname + url_path, port_num) 455 | 456 | # Update debug UI if exists 457 | if _debug_bootui: 458 | _debug_bootui.boot_close() 459 | 460 | multiplayer.multiplayer_peer = online_peer 461 | multiplayer.connected_to_server.connect(_client_connected) 462 | multiplayer.server_disconnected.connect(_client_disconnected) 463 | multiplayer.connection_failed.connect(_client_connect_failed) 464 | 465 | ## Create player node 466 | func create_player(player_id, handshake_data = {}): 467 | var assign_plrid = 0 468 | 469 | # Find available ID to assign 470 | for i in range(0, max_players): 471 | if players.get_player_by_index(i) == null: 472 | assign_plrid = i 473 | break 474 | 475 | _plr_spawner.spawn({player_id = player_id, handshake_data = handshake_data, pindex = assign_plrid}) 476 | 477 | @rpc("authority", "call_local", "reliable") 478 | func _net_broadcast_remove_player(peer_id: int): 479 | var target_plr = players.get_player_by_id(peer_id) 480 | 481 | if target_plr: 482 | player_count = player_count - 1 483 | 484 | player_disconnected.emit(target_plr) 485 | players._internal_remove_player(peer_id) 486 | 487 | func _player_spawned(data): 488 | MPIO.plr_id = multiplayer.get_unique_id() 489 | var player = preload("res://addons/MultiplayCore/scenes/multiplay_player.tscn").instantiate() 490 | player.name = str(data.player_id) 491 | player.player_id = data.player_id 492 | player.handshake_data = data.handshake_data 493 | player.player_index = data.pindex 494 | player.is_local = false 495 | player.mpc = self 496 | 497 | player_count = player_count + 1 498 | 499 | # If is local player 500 | if data.player_id == multiplayer.get_unique_id(): 501 | player.is_local = true 502 | local_player = player 503 | player._internal_peer = player.multiplayer 504 | 505 | if mode == PlayMode.Online: 506 | debug_status_txt = "Pinging..." 507 | connection_state = ConnectionState.PINGING 508 | else: 509 | debug_status_txt = "Ready!" 510 | connection_state = ConnectionState.CONNECTED 511 | 512 | # First time init 513 | if player_scene: 514 | player.player_node_resource_path = player_scene.resource_path 515 | 516 | if player_scene and auto_spawn_player_scene: 517 | var pscene = player_scene.instantiate() 518 | 519 | if assign_client_authority: 520 | pscene.set_multiplayer_authority(data.player_id, true) 521 | 522 | player.add_child(pscene, true) 523 | 524 | player.player_node = pscene 525 | 526 | player.set_multiplayer_authority(data.player_id, false) 527 | 528 | players._internal_add_player(data.player_id, player) 529 | 530 | if connection_state == ConnectionState.CONNECTED: 531 | player_connected.emit(player) 532 | 533 | return player 534 | 535 | func _on_local_player_ready(): 536 | EngineDebugger.send_message("mpc:connected", [local_player.player_id]) 537 | 538 | # Add node that tells what pid session is in scene session tab 539 | var viewnode = Node.new() 540 | if is_server: 541 | viewnode.name = "Server Session" 542 | else: 543 | viewnode.name = "Client ID " + str(local_player.player_id) 544 | get_node("/root").add_child(viewnode, true) 545 | 546 | if _debug_bootui: 547 | _debug_bootui.boot_close() 548 | 549 | connected_to_server.emit(local_player) 550 | player_node_ready = true 551 | debug_status_txt = "Connected!" 552 | connection_state = ConnectionState.CONNECTED 553 | 554 | func _network_player_connected(player_id): 555 | await get_tree().create_timer(connect_timeout_ms / 1000).timeout 556 | 557 | var player_node = players.get_player_by_id(player_id) 558 | 559 | if !player_node: 560 | _kick_player_request(player_id, ConnectionError.TIMEOUT) 561 | 562 | func _find_key(dictionary, value): 563 | var index = dictionary.values().find(value) 564 | return dictionary.keys()[index] 565 | 566 | func _kick_player_request(plr_id: int, reason: ConnectionError): 567 | var player_node = players.get_player_by_id(plr_id) 568 | 569 | if !player_node: 570 | players._internal_remove_player(plr_id) 571 | 572 | rpc_id(plr_id, "_request_disconnect_peer", reason) 573 | 574 | func _kick_player_request_all( reason: ConnectionError): 575 | players._internal_clear_all() 576 | 577 | rpc("_request_disconnect_peer", reason) 578 | 579 | @rpc("authority", "call_local", "reliable") 580 | func _request_disconnect_peer(reason: ConnectionError): 581 | var reason_str = str(_find_key(ConnectionError, reason)) 582 | MPIO.logerr("Disconnected: " + reason_str) 583 | connection_error.emit(reason) 584 | online_connected = false 585 | disconnected_from_server.emit(reason_str) 586 | online_peer.close() 587 | 588 | func _network_player_disconnected(player_id): 589 | var target_plr = players.get_player_by_id(player_id) 590 | 591 | if target_plr: 592 | rpc("_net_broadcast_remove_player", player_id) 593 | target_plr.queue_free() 594 | 595 | # Validate network join internal data 596 | func _check_join_internal(handshake_data): 597 | if !handshake_data.keys().has("_net_join_internal"): 598 | return false 599 | 600 | if !handshake_data._net_join_internal.keys().has("mp_version"): 601 | return false 602 | 603 | return true 604 | 605 | # Init player 606 | @rpc("any_peer", "call_local", "reliable") 607 | func _join_handshake(handshake_data: Dictionary, credentials_data): 608 | var from_id = multiplayer.get_remote_sender_id() 609 | 610 | var existing_plr = players.get_player_by_id(from_id) 611 | 612 | # Prevent existing player from init 613 | if existing_plr: 614 | return 615 | 616 | if player_count >= max_players: 617 | _kick_player_request(from_id, ConnectionError.SERVER_FULL) 618 | return 619 | 620 | # Kick if no join data present 621 | if !_check_join_internal(handshake_data): 622 | _kick_player_request(from_id, ConnectionError.INVALID_HANDSHAKE) 623 | return 624 | 625 | # Check Multiplay version, kick if mismatch 626 | if handshake_data._net_join_internal.mp_version != MP_VERSION: 627 | _kick_player_request(from_id, ConnectionError.VERSION_MISMATCH) 628 | return 629 | 630 | var auth_data = {} 631 | 632 | # Clear internal data, this is reserved 633 | if handshake_data.keys().has("_net_internal"): 634 | handshake_data._net_internal = {} 635 | 636 | handshake_data._net_internal = { 637 | auth_data = {} 638 | } 639 | 640 | # Authenticate client 641 | for ext in _extensions: 642 | if ext is MPAuth: 643 | var auth_result = await ext.authenticate(from_id, credentials_data, handshake_data) 644 | if typeof(auth_result) == TYPE_BOOL and auth_result == false: 645 | _kick_player_request(from_id, ConnectionError.AUTH_FAILED) 646 | return 647 | 648 | auth_data = auth_result 649 | 650 | break 651 | 652 | handshake_data._net_internal.auth_data = auth_data 653 | 654 | # Clear join internal, not going to be used anymore 655 | handshake_data.erase("_net_join_internal") 656 | 657 | rpc_id(from_id, "_internal_recv_net_data", _net_data) 658 | create_player(from_id, handshake_data) 659 | 660 | @rpc("any_peer", "call_local", "reliable") 661 | func _internal_recv_net_data(data): 662 | debug_status_txt = "Waiting for player node..." 663 | connection_state = ConnectionState.AWAIT_PLR_NODE 664 | 665 | MPIO.plr_id = multiplayer.get_unique_id() 666 | 667 | _net_data = data 668 | # Load current scene in the network 669 | if _net_data.current_scene_path != "" and is_server == false: 670 | _net_load_scene(_net_data.current_scene_path) 671 | 672 | func _client_connected(): 673 | debug_status_txt = "Awaiting server data..." 674 | connection_state = ConnectionState.HANDSHAKING 675 | online_connected = true 676 | 677 | # Clear net join internals, this is reserved 678 | if _join_handshake_data.keys().has("_net_join_internal"): 679 | _join_handshake_data._net_join_internal = {} 680 | 681 | _join_handshake_data._net_join_internal = { 682 | mp_version = MP_VERSION 683 | } 684 | 685 | rpc_id(1, "_join_handshake", _join_handshake_data, _join_credentials_data) 686 | 687 | 688 | func _client_disconnected(): 689 | if online_connected: 690 | disconnected_from_server.emit("Unknown") 691 | 692 | func _client_connect_failed(): 693 | disconnected_from_server.emit("Connection Failure") 694 | connection_error.emit(ConnectionError.CONNECTION_FAILURE) 695 | 696 | func _on_local_disconnected(reason): 697 | connection_state = ConnectionState.CONNECTION_CLOSED 698 | debug_status_txt = "Disconnected: " + str(reason) 699 | online_connected = false 700 | local_player = null 701 | 702 | # Ping player 703 | func _physics_process(delta): 704 | if started and is_server: 705 | players._internal_ping() 706 | 707 | ## Load scene for all players 708 | func load_scene(scene_path: String, respawn_players = true): 709 | rpc("_net_load_scene", scene_path, respawn_players) 710 | 711 | func _check_if_net_from_id(id): 712 | if mode != PlayMode.Online: 713 | return true 714 | return multiplayer.get_remote_sender_id() == id 715 | 716 | @rpc("authority", "call_local", "reliable") 717 | func _net_load_scene(scene_path: String, respawn_players = true): 718 | _net_data.current_scene_path = scene_path 719 | 720 | if current_scene: 721 | current_scene.queue_free() 722 | current_scene = null 723 | 724 | if !ResourceLoader.exists(scene_path): 725 | MPIO.logerr("Target scene doesn't exist") 726 | return 727 | 728 | var scene_pack = load(scene_path) 729 | var scene_node = scene_pack.instantiate() 730 | 731 | add_child(scene_node, true) 732 | 733 | current_scene = scene_node 734 | 735 | if respawn_players: 736 | players.respawn_node_all() 737 | 738 | scene_loaded.emit() 739 | -------------------------------------------------------------------------------- /addons/MultiplayCore/MultiPlayTool.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | const RESTART_POPUP = preload("res://addons/MultiplayCore/editor/window/first_restart.tscn") 5 | const WELCOME_POPUP = preload("res://addons/MultiplayCore/editor/window/welcome_popup.tscn") 6 | const UPDATE_POPUP = preload("res://addons/MultiplayCore/editor/window/update_popup.tscn") 7 | const ICON_RUN = preload("res://addons/MultiplayCore/icons/MPDebugPlay.svg") 8 | const ICON_RUN_PRESSED = preload("res://addons/MultiplayCore/icons/MPDebugPlay_Pressed.svg") 9 | 10 | const MULTIPLAY_ASSETLIB_URL = "https://assets.mpc.himaji.xyz" 11 | 12 | var before_export_checkout = null 13 | var mprun_btn: PanelContainer = null 14 | 15 | var icon_refresh 16 | 17 | var debugger = null 18 | 19 | func get_icon(n): 20 | return EditorInterface.get_base_control().get_theme_icon(n) 21 | 22 | func _enter_tree(): 23 | if not ProjectSettings.has_setting("autoload/MPIO"): 24 | add_autoload_singleton("MPIO", "res://addons/MultiplayCore/MPIO.gd") 25 | 26 | icon_refresh = get_icon("RotateLeft") 27 | 28 | if FileAccess.file_exists("user://mpc_tool_firstrun"): 29 | var fr = FileAccess.open("user://mpc_tool_firstrun", FileAccess.READ) 30 | var fr_val = fr.get_as_text() 31 | fr.close() 32 | 33 | if fr_val == "0": 34 | set_firstrun("1") 35 | var welcome_popup = WELCOME_POPUP.instantiate() 36 | add_child(welcome_popup) 37 | welcome_popup.popup_centered() 38 | 39 | _on_project_opened() 40 | else: 41 | # First run, require restart. 42 | 43 | var init_popup = RESTART_POPUP.instantiate() 44 | add_child(init_popup) 45 | 46 | init_popup.popup_centered() 47 | 48 | init_popup.confirmed.connect(_firstrun_restart_editor) 49 | 50 | func _set_assetlib(): 51 | var editor_settings = EditorInterface.get_editor_settings() 52 | var aburls: Dictionary = editor_settings.get_setting("asset_library/available_urls") 53 | 54 | if !aburls.keys().has("MultiPlay AssetLib"): 55 | aburls["MultiPlay AssetLib"] = MULTIPLAY_ASSETLIB_URL 56 | 57 | print("Installed MultiPlay Asset Library! If you don't see the option in the site dropdown, try to open and close editor settings menu.") 58 | else: 59 | print("MultiPlay Asset Library has been uninstalled") 60 | aburls.erase("MultiPlay AssetLib") 61 | 62 | func _on_project_opened(): 63 | mprun_btn = _add_toolbar_button(_mprun_btn, ICON_RUN, ICON_RUN_PRESSED) 64 | mprun_btn.tooltip_text = "MultiPlay Quick Run\nQuickly test online mode" 65 | mprun_btn.gui_input.connect(_mprun_gui_input) 66 | 67 | var submenu: PopupMenu = PopupMenu.new() 68 | submenu.add_item("Check for updates", 1) 69 | submenu.add_item("Create Self Signed Certificate", 2) 70 | submenu.add_item("Configure Debug Data", 3) 71 | submenu.add_item("Toggle MPC Asset Library", 6) 72 | submenu.add_separator() 73 | submenu.add_item("Open Documentation", 8) 74 | submenu.add_item("Get Support", 9) 75 | 76 | submenu.id_pressed.connect(_toolmenu_pressed) 77 | 78 | add_tool_submenu_item("MultiPlay Core", submenu) 79 | 80 | before_export_checkout = preload("res://addons/MultiplayCore/editor/scripts/ExportCheckout.gd").new() 81 | add_export_plugin(before_export_checkout) 82 | 83 | debugger = preload("res://addons/MultiplayCore/editor/scripts/DebuggerPlugin.gd").new() 84 | add_debugger_plugin(debugger) 85 | 86 | func _firstrun_restart_editor(): 87 | set_firstrun("0") 88 | 89 | EditorInterface.save_all_scenes() 90 | 91 | # Handle Plugin Saving 92 | var config = ConfigFile.new() 93 | var err = config.load("res://project.godot") 94 | 95 | if err == OK: 96 | var editor_plugins: PackedStringArray = config.get_value("editor_plugins", "enabled", PackedStringArray()) 97 | 98 | if !editor_plugins.has("res://addons/MultiplayCore/plugin.cfg"): 99 | editor_plugins.append("res://addons/MultiplayCore/plugin.cfg") 100 | 101 | var saveerr = config.save("res://project.godot") 102 | 103 | if saveerr == OK: 104 | print("MPC Plugin Saved") 105 | else: 106 | print("MPC Plugin Save Failed") 107 | 108 | print("Restarting...") 109 | await get_tree().create_timer(1).timeout 110 | 111 | EditorInterface.restart_editor(false) 112 | 113 | func _mprun_btn(): 114 | EditorInterface.save_all_scenes() 115 | 116 | debugger.send_start_auto = true 117 | 118 | EditorInterface.play_main_scene() 119 | 120 | func open_run_debug_config(): 121 | EditorInterface.popup_dialog_centered(preload("res://addons/MultiplayCore/editor/window/debug_configs.tscn").instantiate()) 122 | 123 | func _mprun_gui_input(e): 124 | if e is InputEventMouseButton: 125 | 126 | if e.button_index == 2 and e.pressed: 127 | open_run_debug_config() 128 | 129 | func _unhandled_input(event): 130 | if event is InputEventKey: 131 | if event.ctrl_pressed && event.keycode == KEY_F5: 132 | _mprun_btn() 133 | 134 | func _toolmenu_pressed(id): 135 | if id == 1: 136 | open_update_popup() 137 | 138 | if id == 2: 139 | run_devscript(preload("res://addons/MultiplayCore/dev_scripts/CertMake.gd")) 140 | 141 | if id == 3: 142 | open_run_debug_config() 143 | 144 | if id == 6: 145 | _set_assetlib() 146 | 147 | if id == 8: 148 | OS.shell_open("https://mpc.himaji.xyz/docs/") 149 | 150 | if id == 9: 151 | OS.shell_open("https://mpc.himaji.xyz/docs/community/get-support/") 152 | 153 | func set_firstrun(to): 154 | var fr = FileAccess.open("user://mpc_tool_firstrun", FileAccess.WRITE) 155 | fr.store_string(to) 156 | fr.close() 157 | 158 | func _add_toolbar_button(action: Callable, icon_normal, icon_pressed): 159 | var panel = PanelContainer.new() 160 | var b = TextureButton.new(); 161 | b.name = "tbtn" 162 | b.texture_normal = icon_normal 163 | b.texture_pressed = icon_pressed 164 | b.pressed.connect(action) 165 | panel.add_child(b) 166 | add_control_to_container(CONTAINER_TOOLBAR, panel) 167 | return panel 168 | 169 | func run_devscript(script): 170 | await script.run() 171 | 172 | func open_update_popup(): 173 | var popup = UPDATE_POPUP.instantiate() 174 | add_child(popup) 175 | popup.popup_centered() 176 | popup.check_updates() 177 | 178 | 179 | func _exit_tree(): 180 | remove_tool_menu_item("MultiPlay Core") 181 | 182 | remove_export_plugin(before_export_checkout) 183 | 184 | print("goodbye!") 185 | 186 | remove_autoload_singleton("MPIO") 187 | remove_debugger_plugin(debugger) 188 | 189 | remove_control_from_container(CONTAINER_TOOLBAR, mprun_btn) 190 | mprun_btn.free() 191 | -------------------------------------------------------------------------------- /addons/MultiplayCore/auths/MPAuth.gd: -------------------------------------------------------------------------------- 1 | @icon("res://addons/MultiplayCore/icons/MPAuth.svg") 2 | @tool 3 | 4 | extends MPExtension 5 | ## Middleware to check player's credentials before connecting. 6 | ## @experimental 7 | class_name MPAuth 8 | 9 | ## Authenticate function, this must be set. 10 | ## 11 | ## Callable will be called will the following args:[br] 12 | ## [code]plr_id[/code] The player id[br] 13 | ## [code]credentials_data[/code] Credentials data from the player[br] 14 | ## [code]handshake_data[/code] Handshake data from the player[br] 15 | ## 16 | ## Return false if fail, otherwise return the data 17 | ## 18 | var authenticate_function: Callable 19 | 20 | func authenticate(plr_id, credentials_data, handshake_data): 21 | if !authenticate_function: 22 | MPIO.logwarn("authenticate: authenticate_function has not been set. Allowing user in by default.") 23 | return true 24 | 25 | var result = await authenticate_function.call(plr_id, credentials_data, handshake_data) 26 | return result 27 | -------------------------------------------------------------------------------- /addons/MultiplayCore/debug_ui/MultiPlayDebugUI.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | var mpc: MultiPlayCore 4 | @export var connect_address: LineEdit 5 | @export var payload_input: TextEdit 6 | @export var cert_input: TextEdit 7 | @export var boot_ui: Control 8 | @export var status_ui: Control 9 | @export var status_text: Label 10 | var join_address = "ws://localhost:4200" 11 | 12 | # Used to make frequent data update slower 13 | var delta_step = 0 14 | 15 | # delta max in frames 16 | const DELTA_STEP_MAX = 100 17 | 18 | # String array line count 19 | const STRING_ARRAY_SLOT = 6 20 | 21 | var s_array = [] 22 | 23 | var worst_ping = 0 24 | 25 | func _ready(): 26 | s_array.resize(STRING_ARRAY_SLOT + 1) 27 | s_array.fill("") 28 | connect_address.text = join_address 29 | 30 | var fp = FileAccess.open("user://mp_debug_bootui", FileAccess.READ) 31 | if fp: 32 | var fp_data = JSON.parse_string(fp.get_as_text()) 33 | fp.close() 34 | payload_input.text = get_or_empty(fp_data, "payload_input") 35 | cert_input.text = get_or_empty(fp_data, "cert_input") 36 | 37 | boot_ui.visible = true 38 | status_ui.visible = false 39 | 40 | mpc.connected_to_server.connect(mpc_started_entry) 41 | 42 | func get_or_empty(data: Dictionary, field: String): 43 | if data.keys().has(field): 44 | return data[field] 45 | return "" 46 | 47 | func save_debug_cache(): 48 | var fp = FileAccess.open("user://mp_debug_bootui", FileAccess.WRITE) 49 | fp.store_string(JSON.stringify({ 50 | payload_input = payload_input.text, 51 | cert_input = cert_input.text 52 | })) 53 | fp.close() 54 | 55 | func parse_json_or_none(data): 56 | var d = JSON.parse_string(data) 57 | 58 | if d: 59 | return d 60 | 61 | if data != "": 62 | print("Got Invalid JSON data, join data ignored") 63 | 64 | return {} 65 | 66 | func _on_host_pressed(): 67 | mpc.start_online_host(false, parse_json_or_none(payload_input.text), parse_json_or_none(cert_input.text)) 68 | boot_close() 69 | 70 | func _on_host_act_pressed(): 71 | mpc.start_online_host(true, parse_json_or_none(payload_input.text), parse_json_or_none(cert_input.text)) 72 | boot_close() 73 | 74 | func _on_connect_pressed(): 75 | mpc.start_online_join(join_address, parse_json_or_none(payload_input.text), parse_json_or_none(cert_input.text)) 76 | boot_close() 77 | 78 | func _on_connect_address_text_changed(new_text): 79 | join_address = new_text 80 | 81 | func _process(delta): 82 | update_status_text() 83 | 84 | func update_status_text(): 85 | if !mpc: 86 | return 87 | 88 | s_array[0] = mpc.debug_status_txt 89 | 90 | if mpc.is_server: 91 | s_array[1] = "Running as Server at " + str(mpc.port) 92 | else: 93 | s_array[1] = "Running as client" 94 | 95 | s_array[2] = "Player Count: " + str(mpc.player_count) 96 | 97 | delta_step = delta_step + 1 98 | 99 | # Update frequent data on delta step max 100 | if delta_step >= DELTA_STEP_MAX: 101 | delta_step = 0 102 | 103 | # Update ping count 104 | if is_instance_valid(mpc.local_player) and !mpc.is_server: 105 | var ping_ms = mpc.local_player.ping_ms 106 | s_array[3] = "Ping: " + str(ping_ms) + "ms" 107 | 108 | if ping_ms > worst_ping: 109 | worst_ping = ping_ms 110 | 111 | s_array[4] = " ▸ Worst Ping: " + str(worst_ping) + "ms" 112 | 113 | var result_txt = "" 114 | 115 | for s in s_array: 116 | if s != "": 117 | result_txt = result_txt + s + "\n" 118 | 119 | status_text.text = result_txt 120 | 121 | func _on_one_screen_pressed(): 122 | mpc.start_one_screen() 123 | boot_close() 124 | 125 | 126 | func _on_solo_pressed(): 127 | mpc.start_solo() 128 | boot_close() 129 | 130 | 131 | func _on_swap_pressed(): 132 | mpc.start_swap() 133 | boot_close() 134 | 135 | func boot_close(): 136 | save_debug_cache() 137 | boot_ui.visible = false 138 | status_ui.visible = true 139 | 140 | func mpc_started_entry(player): 141 | boot_ui.visible = false 142 | status_ui.visible = true 143 | 144 | func _on_close_pressed(): 145 | visible = false 146 | -------------------------------------------------------------------------------- /addons/MultiplayCore/debug_ui/debug_ui.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=3 uid="uid://c86556b4br8c4"] 2 | 3 | [ext_resource type="FontFile" uid="uid://up2o2ylbi16n" path="res://addons/MultiplayCore/fonts/VictorMono-Medium.ttf" id="1_p1im6"] 4 | [ext_resource type="Script" path="res://addons/MultiplayCore/debug_ui/MultiPlayDebugUI.gd" id="2_wn6yf"] 5 | [ext_resource type="Texture2D" uid="uid://dupc3co7kupa3" path="res://addons/MultiplayCore/icons/MultiPlayCore.svg" id="3_vbxjx"] 6 | 7 | [sub_resource type="Theme" id="Theme_jkq4t"] 8 | default_font = ExtResource("1_p1im6") 9 | default_font_size = 12 10 | 11 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_r1q81"] 12 | content_margin_left = 12.0 13 | content_margin_top = 12.0 14 | content_margin_right = 12.0 15 | content_margin_bottom = 12.0 16 | bg_color = Color(0, 0, 0, 0.247059) 17 | border_width_left = 2 18 | border_width_top = 2 19 | border_width_right = 2 20 | border_width_bottom = 2 21 | border_color = Color(1, 1, 1, 0.188235) 22 | corner_radius_top_left = 4 23 | corner_radius_top_right = 4 24 | corner_radius_bottom_right = 4 25 | corner_radius_bottom_left = 4 26 | 27 | [node name="DebugUI" type="CanvasLayer"] 28 | 29 | [node name="Layout" type="HBoxContainer" parent="."] 30 | offset_left = 16.0 31 | offset_top = 16.0 32 | offset_right = 316.0 33 | offset_bottom = 468.0 34 | theme = SubResource("Theme_jkq4t") 35 | theme_override_constants/separation = 12 36 | 37 | [node name="BootUI" type="PanelContainer" parent="Layout" node_paths=PackedStringArray("connect_address", "payload_input", "cert_input", "boot_ui", "status_ui", "status_text")] 38 | custom_minimum_size = Vector2(300, 0) 39 | layout_mode = 2 40 | size_flags_vertical = 0 41 | theme_override_styles/panel = SubResource("StyleBoxFlat_r1q81") 42 | script = ExtResource("2_wn6yf") 43 | connect_address = NodePath("VBoxContainer/BootOptions/HBoxContainer2/ConnectAddress") 44 | payload_input = NodePath("VBoxContainer/BootOptions/HandshakeField") 45 | cert_input = NodePath("VBoxContainer/BootOptions/CredentialsField") 46 | boot_ui = NodePath("VBoxContainer/BootOptions") 47 | status_ui = NodePath("VBoxContainer/Status") 48 | status_text = NodePath("VBoxContainer/Status/StatusText") 49 | 50 | [node name="VBoxContainer" type="VBoxContainer" parent="Layout/BootUI"] 51 | layout_mode = 2 52 | size_flags_vertical = 0 53 | 54 | [node name="HBoxContainer3" type="HBoxContainer" parent="Layout/BootUI/VBoxContainer"] 55 | layout_mode = 2 56 | 57 | [node name="TextureRect" type="TextureRect" parent="Layout/BootUI/VBoxContainer/HBoxContainer3"] 58 | custom_minimum_size = Vector2(18, 0) 59 | layout_mode = 2 60 | texture = ExtResource("3_vbxjx") 61 | expand_mode = 4 62 | stretch_mode = 5 63 | 64 | [node name="Label" type="Label" parent="Layout/BootUI/VBoxContainer/HBoxContainer3"] 65 | layout_mode = 2 66 | text = "MultiPlay Core" 67 | 68 | [node name="Control" type="Control" parent="Layout/BootUI/VBoxContainer/HBoxContainer3"] 69 | layout_mode = 2 70 | size_flags_horizontal = 3 71 | 72 | [node name="Close" type="Button" parent="Layout/BootUI/VBoxContainer/HBoxContainer3"] 73 | custom_minimum_size = Vector2(30, 0) 74 | layout_mode = 2 75 | text = "X" 76 | 77 | [node name="HSeparator" type="HSeparator" parent="Layout/BootUI/VBoxContainer"] 78 | layout_mode = 2 79 | 80 | [node name="BootOptions" type="VBoxContainer" parent="Layout/BootUI/VBoxContainer"] 81 | layout_mode = 2 82 | 83 | [node name="Label2" type="Label" parent="Layout/BootUI/VBoxContainer/BootOptions"] 84 | modulate = Color(1, 1, 1, 0.596078) 85 | layout_mode = 2 86 | theme_override_font_sizes/font_size = 10 87 | text = "Online" 88 | 89 | [node name="HBoxContainer" type="HBoxContainer" parent="Layout/BootUI/VBoxContainer/BootOptions"] 90 | layout_mode = 2 91 | 92 | [node name="Host" type="Button" parent="Layout/BootUI/VBoxContainer/BootOptions/HBoxContainer"] 93 | custom_minimum_size = Vector2(80, 0) 94 | layout_mode = 2 95 | text = "Host" 96 | 97 | [node name="HostAct" type="Button" parent="Layout/BootUI/VBoxContainer/BootOptions/HBoxContainer"] 98 | layout_mode = 2 99 | size_flags_horizontal = 3 100 | text = "Host + Act Client" 101 | 102 | [node name="HBoxContainer2" type="HBoxContainer" parent="Layout/BootUI/VBoxContainer/BootOptions"] 103 | layout_mode = 2 104 | 105 | [node name="Connect" type="Button" parent="Layout/BootUI/VBoxContainer/BootOptions/HBoxContainer2"] 106 | custom_minimum_size = Vector2(80, 0) 107 | layout_mode = 2 108 | text = "Connect" 109 | 110 | [node name="ConnectAddress" type="LineEdit" parent="Layout/BootUI/VBoxContainer/BootOptions/HBoxContainer2"] 111 | layout_mode = 2 112 | size_flags_horizontal = 3 113 | text = "ws://localhost:4200" 114 | placeholder_text = "URL..." 115 | caret_blink = true 116 | 117 | [node name="Label5" type="Label" parent="Layout/BootUI/VBoxContainer/BootOptions"] 118 | modulate = Color(1, 1, 1, 0.596078) 119 | layout_mode = 2 120 | theme_override_font_sizes/font_size = 10 121 | text = "JSON Handshake Payload" 122 | 123 | [node name="HandshakeField" type="TextEdit" parent="Layout/BootUI/VBoxContainer/BootOptions"] 124 | custom_minimum_size = Vector2(0, 80) 125 | layout_mode = 2 126 | text = "{ 127 | 128 | }" 129 | placeholder_text = "JSON Data..." 130 | caret_blink = true 131 | 132 | [node name="Label6" type="Label" parent="Layout/BootUI/VBoxContainer/BootOptions"] 133 | modulate = Color(1, 1, 1, 0.596078) 134 | layout_mode = 2 135 | theme_override_font_sizes/font_size = 10 136 | text = "JSON Credentials Payload" 137 | 138 | [node name="CredentialsField" type="TextEdit" parent="Layout/BootUI/VBoxContainer/BootOptions"] 139 | custom_minimum_size = Vector2(0, 80) 140 | layout_mode = 2 141 | text = "{ 142 | 143 | }" 144 | placeholder_text = "JSON Data..." 145 | caret_blink = true 146 | 147 | [node name="HSeparator2" type="HSeparator" parent="Layout/BootUI/VBoxContainer/BootOptions"] 148 | layout_mode = 2 149 | 150 | [node name="Label3" type="Label" parent="Layout/BootUI/VBoxContainer/BootOptions"] 151 | modulate = Color(1, 1, 1, 0.596078) 152 | layout_mode = 2 153 | theme_override_font_sizes/font_size = 10 154 | text = "One Screen" 155 | 156 | [node name="OneScreen" type="Button" parent="Layout/BootUI/VBoxContainer/BootOptions"] 157 | layout_mode = 2 158 | text = "Start One Screen" 159 | 160 | [node name="HSeparator3" type="HSeparator" parent="Layout/BootUI/VBoxContainer/BootOptions"] 161 | layout_mode = 2 162 | 163 | [node name="Label4" type="Label" parent="Layout/BootUI/VBoxContainer/BootOptions"] 164 | modulate = Color(1, 1, 1, 0.596078) 165 | layout_mode = 2 166 | theme_override_font_sizes/font_size = 10 167 | text = "Swap" 168 | 169 | [node name="Swap" type="Button" parent="Layout/BootUI/VBoxContainer/BootOptions"] 170 | layout_mode = 2 171 | text = "Start Swap" 172 | 173 | [node name="HSeparator4" type="HSeparator" parent="Layout/BootUI/VBoxContainer/BootOptions"] 174 | layout_mode = 2 175 | 176 | [node name="Label7" type="Label" parent="Layout/BootUI/VBoxContainer/BootOptions"] 177 | modulate = Color(1, 1, 1, 0.596078) 178 | layout_mode = 2 179 | theme_override_font_sizes/font_size = 10 180 | text = "Solo" 181 | 182 | [node name="Solo" type="Button" parent="Layout/BootUI/VBoxContainer/BootOptions"] 183 | layout_mode = 2 184 | text = "Start Solo" 185 | 186 | [node name="Status" type="VBoxContainer" parent="Layout/BootUI/VBoxContainer"] 187 | layout_mode = 2 188 | 189 | [node name="StatusText" type="Label" parent="Layout/BootUI/VBoxContainer/Status"] 190 | layout_mode = 2 191 | text = "Running as Server at 4200" 192 | 193 | [connection signal="pressed" from="Layout/BootUI/VBoxContainer/HBoxContainer3/Close" to="Layout/BootUI" method="_on_close_pressed"] 194 | [connection signal="pressed" from="Layout/BootUI/VBoxContainer/BootOptions/HBoxContainer/Host" to="Layout/BootUI" method="_on_host_pressed"] 195 | [connection signal="pressed" from="Layout/BootUI/VBoxContainer/BootOptions/HBoxContainer/HostAct" to="Layout/BootUI" method="_on_host_act_pressed"] 196 | [connection signal="pressed" from="Layout/BootUI/VBoxContainer/BootOptions/HBoxContainer2/Connect" to="Layout/BootUI" method="_on_connect_pressed"] 197 | [connection signal="text_changed" from="Layout/BootUI/VBoxContainer/BootOptions/HBoxContainer2/ConnectAddress" to="Layout/BootUI" method="_on_connect_address_text_changed"] 198 | [connection signal="pressed" from="Layout/BootUI/VBoxContainer/BootOptions/OneScreen" to="Layout/BootUI" method="_on_one_screen_pressed"] 199 | [connection signal="pressed" from="Layout/BootUI/VBoxContainer/BootOptions/Swap" to="Layout/BootUI" method="_on_swap_pressed"] 200 | [connection signal="pressed" from="Layout/BootUI/VBoxContainer/BootOptions/Solo" to="Layout/BootUI" method="_on_solo_pressed"] 201 | -------------------------------------------------------------------------------- /addons/MultiplayCore/dev_scripts/CertMake.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | static func run(): 4 | var for_address = "127.0.0.1" 5 | 6 | print("Creating certificate for address " + str(for_address)) 7 | var crypto = Crypto.new() 8 | var key = crypto.generate_rsa(4096) 9 | var cert = crypto.generate_self_signed_certificate(key, "CN=" + str(for_address) + ",O=myorganisation,C=IT") 10 | 11 | key.save("res://private_key.key") 12 | cert.save("res://cert.crt") 13 | 14 | print("Created self signed certificate! These are for development uses only.") 15 | print("res://private_key.key") 16 | print("res://cert.crt") 17 | -------------------------------------------------------------------------------- /addons/MultiplayCore/editor/scenes/player_debug_config.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://dwuqgi8uj5n3j"] 2 | 3 | [ext_resource type="Script" path="res://addons/MultiplayCore/editor/scripts/PlayerDebugConfig.gd" id="1_154qf"] 4 | [ext_resource type="FontFile" uid="uid://up2o2ylbi16n" path="res://addons/MultiplayCore/fonts/VictorMono-Medium.ttf" id="1_ix4xy"] 5 | 6 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8ok7j"] 7 | content_margin_left = 8.0 8 | content_margin_top = 8.0 9 | content_margin_right = 8.0 10 | content_margin_bottom = 8.0 11 | bg_color = Color(0, 0, 0, 0.333333) 12 | corner_radius_top_left = 8 13 | corner_radius_top_right = 8 14 | corner_radius_bottom_right = 8 15 | corner_radius_bottom_left = 8 16 | 17 | [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_jrsfa"] 18 | 19 | [node name="Player 1" type="Panel" node_paths=PackedStringArray("handshake_edit", "credentials_edit")] 20 | script = ExtResource("1_154qf") 21 | handshake_edit = NodePath("VBoxContainer/VBoxContainer/TextEdit") 22 | credentials_edit = NodePath("VBoxContainer/VBoxContainer2/TextEdit") 23 | 24 | [node name="VBoxContainer" type="VBoxContainer" parent="."] 25 | layout_mode = 1 26 | anchors_preset = 15 27 | anchor_right = 1.0 28 | anchor_bottom = 1.0 29 | offset_left = 8.0 30 | offset_top = 8.0 31 | offset_right = -8.0 32 | offset_bottom = -8.0 33 | grow_horizontal = 2 34 | grow_vertical = 2 35 | 36 | [node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer"] 37 | layout_mode = 2 38 | size_flags_vertical = 3 39 | 40 | [node name="Label" type="Label" parent="VBoxContainer/VBoxContainer"] 41 | layout_mode = 2 42 | text = "JSON Handshake Data" 43 | 44 | [node name="TextEdit" type="TextEdit" parent="VBoxContainer/VBoxContainer"] 45 | layout_mode = 2 46 | size_flags_vertical = 3 47 | theme_override_fonts/font = ExtResource("1_ix4xy") 48 | theme_override_styles/normal = SubResource("StyleBoxFlat_8ok7j") 49 | theme_override_styles/focus = SubResource("StyleBoxEmpty_jrsfa") 50 | text = "{ 51 | 52 | }" 53 | 54 | [node name="VBoxContainer2" type="VBoxContainer" parent="VBoxContainer"] 55 | layout_mode = 2 56 | size_flags_vertical = 3 57 | 58 | [node name="Label" type="Label" parent="VBoxContainer/VBoxContainer2"] 59 | layout_mode = 2 60 | text = "JSON Credentials Data" 61 | 62 | [node name="TextEdit" type="TextEdit" parent="VBoxContainer/VBoxContainer2"] 63 | layout_mode = 2 64 | size_flags_vertical = 3 65 | theme_override_fonts/font = ExtResource("1_ix4xy") 66 | theme_override_styles/normal = SubResource("StyleBoxFlat_8ok7j") 67 | theme_override_styles/focus = SubResource("StyleBoxEmpty_jrsfa") 68 | text = "{ 69 | 70 | }" 71 | -------------------------------------------------------------------------------- /addons/MultiplayCore/editor/scripts/DebuggerPlugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorDebuggerPlugin 3 | 4 | var send_start_auto = false 5 | var start_auto_i = 0 6 | 7 | func _has_capture(prefix): 8 | return prefix == "mpc" 9 | 10 | func _capture(message, data, session_id): 11 | var session = get_session(session_id) 12 | 13 | if message == "mpc:connected": 14 | var pid = data[0] 15 | 16 | if (message == "mpc:session_ready" and send_start_auto) or message == "mpc:debug_session_ready": 17 | start_auto_i = start_auto_i + 1 18 | if session_id == 0: 19 | session.send_message("mpc:start_server", [session_id]) 20 | else: 21 | session.send_message("mpc:start_client", [session_id]) 22 | 23 | if start_auto_i == get_sessions().size(): 24 | start_auto_i = 0 25 | send_start_auto = false 26 | 27 | func _setup_session(session_id): 28 | pass 29 | -------------------------------------------------------------------------------- /addons/MultiplayCore/editor/scripts/ExportCheckout.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorExportPlugin 3 | 4 | var post_build_log = {} 5 | var is_debug = false 6 | 7 | func _export_begin(features, is_debug_build, path, flags): 8 | is_debug = is_debug_build 9 | 10 | if features.has("android"): 11 | if get_option("permissions/internet") == false: 12 | add_post_log("Android: Internet permission not enabled", [ 13 | "Internet permission is currently not enabled. This will cause multiplayer not to work.", 14 | ]) 15 | 16 | func add_post_log(title, logs): 17 | post_build_log[title] = logs 18 | 19 | func _export_file(path, type, features): 20 | # Prevent exposing private key files in client builds 21 | if path.ends_with(".key") and is_debug == false: 22 | if not features.has("headless") and not features.has("server"): 23 | skip() 24 | 25 | add_post_log("Key ignored warning", [ 26 | "Key files (.key) are ignored in this build to prevent private key exposure in client exports.", 27 | "To prevent this, Add 'headless' or 'server' to export features, or ignore the key files in export resources tab." 28 | ]) 29 | 30 | func _export_end(): 31 | if post_build_log.size() > 0: 32 | print_rich("\n[color=#FFB800]⚠️[/color] [b]Multiplay Export Warnings[/b] ----\n") 33 | for k in post_build_log.keys(): 34 | var v = post_build_log[k] 35 | 36 | print_rich("[color=#FFB800]•[/color] [b]" + k + "[/b]") 37 | 38 | for l in v: 39 | print_rich("\t", l) 40 | 41 | print("") 42 | 43 | 44 | func _get_name(): 45 | return "mpc_build_checkout" 46 | -------------------------------------------------------------------------------- /addons/MultiplayCore/editor/scripts/PlayerDebugConfig.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | 4 | @export var handshake_edit: TextEdit 5 | @export var credentials_edit: TextEdit 6 | 7 | @export var session_id = 0 8 | 9 | # Called when the node enters the scene tree for the first time. 10 | func _ready(): 11 | if not EditorPlugin: 12 | return 13 | load_data() 14 | 15 | handshake_edit.focus_exited.connect(apply_configs) 16 | credentials_edit.focus_exited.connect(apply_configs) 17 | 18 | func apply_configs(): 19 | var fp_data = read_config_file() 20 | 21 | if fp_data == null: 22 | fp_data = {} 23 | 24 | if !fp_data.keys().has("debug_configs"): 25 | fp_data.debug_configs = {} 26 | 27 | fp_data.debug_configs[str(session_id)] = { 28 | handshake = handshake_edit.text, 29 | credentials = credentials_edit.text 30 | } 31 | 32 | var fp = FileAccess.open("user://mp_debug_configs", FileAccess.WRITE) 33 | fp.store_string(JSON.stringify(fp_data)) 34 | fp.close() 35 | 36 | func load_data(): 37 | var fp_data = read_config_file() 38 | if fp_data: 39 | 40 | if fp_data.keys().has("debug_configs"): 41 | if fp_data.debug_configs.keys().has(str(session_id)): 42 | handshake_edit.text = fp_data.debug_configs[str(session_id)].handshake 43 | credentials_edit.text = fp_data.debug_configs[str(session_id)].credentials 44 | 45 | func read_config_file(): 46 | var fp = FileAccess.open("user://mp_debug_configs", FileAccess.READ) 47 | if fp: 48 | var fp_data: Dictionary = JSON.parse_string(fp.get_as_text()) 49 | fp.close() 50 | 51 | return fp_data 52 | 53 | return null 54 | 55 | # Called every frame. 'delta' is the elapsed time since the previous frame. 56 | func _process(delta): 57 | pass 58 | -------------------------------------------------------------------------------- /addons/MultiplayCore/editor/scripts/UpdatePopup.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | 4 | @export var http_request: HTTPRequest 5 | @export var slidetxt_anim_play: AnimationPlayer 6 | @export var status_label: Label 7 | @export var main_title: Label 8 | @export var current_version: Label 9 | 10 | @export var update_btn: Button 11 | 12 | @export var update_progress_bar: ProgressBar 13 | 14 | const RELEASE_INFO_JSON = "https://mpc.himaji.xyz/static/releases/release-info.json" 15 | 16 | var label_to_set = "" 17 | var label_clr_to_set = Color.WHITE 18 | 19 | var download_url = "" 20 | var download_commit_hash = "" 21 | 22 | var request_step = "" 23 | 24 | func check_updates(): 25 | current_version.text = "Current Version: " + MultiPlayCore.MP_VERSION + " - " + MultiPlayCore.MP_VERSION_NAME 26 | update_btn.disabled = true 27 | set_status("Checking for updates...") 28 | print("Update checker running...") 29 | request_step = "meta_fetch" 30 | http_request.request_completed.connect(_on_http_request_request_completed) 31 | http_request.request(RELEASE_INFO_JSON) 32 | 33 | func anim_apply_status(): 34 | status_label.text = label_to_set 35 | status_label.add_theme_color_override("font_color", label_clr_to_set) 36 | 37 | func set_status(txt, color = Color.WHITE_SMOKE): 38 | label_clr_to_set = color 39 | label_to_set = txt 40 | slidetxt_anim_play.stop() 41 | slidetxt_anim_play.play("slide") 42 | 43 | await get_tree().create_timer(0.1).timeout 44 | anim_apply_status() 45 | 46 | func _on_http_request_request_completed(result, response_code, headers, body): 47 | if request_step == "meta_fetch": 48 | if result == OK: 49 | set_status("") 50 | var data = JSON.parse_string(body.get_string_from_utf8()) 51 | 52 | download_url = data.download_url 53 | download_commit_hash = data.download_commit_hash 54 | 55 | if data.version_string == MultiPlayCore.MP_VERSION: 56 | # Latest version already 57 | main_title.text = "You're up to date!" 58 | update_btn.text = "Reinstall" 59 | update_btn.disabled = false 60 | else: 61 | # New update 62 | var engine_info = Engine.get_version_info() 63 | var engine_required = str(data.godot_version).split(".") 64 | var engine_sub = str(engine_info.major) + "." + str(engine_info.minor) 65 | 66 | if int(engine_required[0]) > engine_info.major or int(engine_required[1]) > engine_info.minor: 67 | main_title.text = "Not compatible with Godot v" + str(engine_sub) 68 | update_btn.disabled = true 69 | set_status("Please update your Godot version to " + str(data.godot_version), Color.DARK_ORANGE) 70 | else: 71 | main_title.text = "Version " + str(data.version_string) + " is available!" 72 | update_btn.text = "Update" 73 | set_status("Press update to install the latest version.") 74 | update_btn.disabled = false 75 | 76 | if request_step == "update": 77 | var updatezip = FileAccess.open("user://mpc_update.zip", FileAccess.WRITE) 78 | updatezip.store_buffer(body) 79 | updatezip.close() 80 | set_status("Extracting...") 81 | 82 | var reader := ZIPReader.new() 83 | var err := reader.open("user://mpc_update.zip") 84 | if err != OK: 85 | set_status("Extraction Failed.", Color.INDIAN_RED) 86 | var files := reader.get_files() 87 | 88 | update_progress_bar.max_value = files.size() 89 | update_progress_bar.value = 0 90 | 91 | DirAccess.remove_absolute("res://addons/MultiplayCore") 92 | 93 | for f in files: 94 | var fp = f.trim_prefix("multiplay-core-" + download_commit_hash) 95 | 96 | var res := reader.read_file(f) 97 | 98 | var user_path = "res://" + fp 99 | if res.size() == 0: 100 | DirAccess.make_dir_recursive_absolute(user_path) 101 | else: 102 | var fs = FileAccess.open(user_path, FileAccess.WRITE) 103 | fs.store_buffer(res) 104 | fs.close() 105 | 106 | update_progress_bar.value = update_progress_bar.value + 1 107 | 108 | reader.close() 109 | 110 | set_status("Completed! Restart the Editor to finish installation :D", Color.LIME_GREEN) 111 | 112 | update_btn.text = "Save + Restart" 113 | update_btn.visible = true 114 | update_progress_bar.visible = false 115 | 116 | request_step = "restart" 117 | 118 | 119 | func _on_update_button_pressed(): 120 | if request_step == "restart": 121 | EditorInterface.restart_editor(true) 122 | return 123 | request_step = "update" 124 | update_btn.visible = false 125 | update_progress_bar.visible = true 126 | set_status("Downloading the latest version...") 127 | 128 | http_request.request(download_url) 129 | -------------------------------------------------------------------------------- /addons/MultiplayCore/editor/version_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maji-git/multiplay-core/a6d496bbf2c36c59d108d2a93bc1dd184faec241/addons/MultiplayCore/editor/version_icon.png -------------------------------------------------------------------------------- /addons/MultiplayCore/editor/version_icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://caxuos5oe7cre" 6 | path="res://.godot/imported/version_icon.png-7a3cb7e7230f7aa0b02b545906c1fbf9.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/editor/version_icon.png" 14 | dest_files=["res://.godot/imported/version_icon.png-7a3cb7e7230f7aa0b02b545906c1fbf9.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /addons/MultiplayCore/editor/window/debug_configs.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://6ys58mvq55l3"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://dwuqgi8uj5n3j" path="res://addons/MultiplayCore/editor/scenes/player_debug_config.tscn" id="1_1q3wl"] 4 | 5 | [node name="Window" type="AcceptDialog"] 6 | title = "Multiplay Debug Data Config" 7 | size = Vector2i(600, 400) 8 | ok_button_text = "Close" 9 | 10 | [node name="TabContainer" type="TabContainer" parent="."] 11 | anchors_preset = 15 12 | anchor_right = 1.0 13 | anchor_bottom = 1.0 14 | grow_horizontal = 2 15 | grow_vertical = 2 16 | 17 | [node name="Player 1 (Host)" parent="TabContainer" instance=ExtResource("1_1q3wl")] 18 | layout_mode = 2 19 | 20 | [node name="Player 2" parent="TabContainer" instance=ExtResource("1_1q3wl")] 21 | visible = false 22 | layout_mode = 2 23 | session_id = 1 24 | 25 | [node name="Player 3" parent="TabContainer" instance=ExtResource("1_1q3wl")] 26 | visible = false 27 | layout_mode = 2 28 | session_id = 2 29 | 30 | [node name="Player 4" parent="TabContainer" instance=ExtResource("1_1q3wl")] 31 | visible = false 32 | layout_mode = 2 33 | session_id = 3 34 | 35 | [connection signal="close_requested" from="." to="." method="queue_free"] 36 | -------------------------------------------------------------------------------- /addons/MultiplayCore/editor/window/first_restart.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene format=3 uid="uid://dbpf25oec1ecs"] 2 | 3 | [node name="Window" type="ConfirmationDialog"] 4 | title = "MultiPlay" 5 | size = Vector2i(640, 100) 6 | visible = true 7 | ok_button_text = "Save + Restart" 8 | dialog_text = "Welcome to MultiPlay Core! MultiPlay Core requires a restart to finish initializing." 9 | cancel_button_text = "I'll do it later" 10 | -------------------------------------------------------------------------------- /addons/MultiplayCore/editor/window/update_popup.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=3 uid="uid://dxnrjskmes7tf"] 2 | 3 | [ext_resource type="Script" path="res://addons/MultiplayCore/editor/scripts/UpdatePopup.gd" id="1_1e3dt"] 4 | [ext_resource type="Texture2D" uid="uid://caxuos5oe7cre" path="res://addons/MultiplayCore/editor/version_icon.png" id="2_smiai"] 5 | 6 | [sub_resource type="Animation" id="Animation_wrshe"] 7 | length = 0.001 8 | tracks/0/type = "value" 9 | tracks/0/imported = false 10 | tracks/0/enabled = true 11 | tracks/0/path = NodePath("Title:self_modulate") 12 | tracks/0/interp = 1 13 | tracks/0/loop_wrap = true 14 | tracks/0/keys = { 15 | "times": PackedFloat32Array(0), 16 | "transitions": PackedFloat32Array(1), 17 | "update": 0, 18 | "values": [Color(1, 1, 1, 1)] 19 | } 20 | 21 | [sub_resource type="Animation" id="Animation_fjliv"] 22 | resource_name = "slide" 23 | length = 0.3 24 | tracks/0/type = "value" 25 | tracks/0/imported = false 26 | tracks/0/enabled = true 27 | tracks/0/path = NodePath("Title:self_modulate") 28 | tracks/0/interp = 1 29 | tracks/0/loop_wrap = true 30 | tracks/0/keys = { 31 | "times": PackedFloat32Array(0, 0.1, 0.3), 32 | "transitions": PackedFloat32Array(1, 1, 1), 33 | "update": 0, 34 | "values": [Color(1, 1, 1, 1), Color(1, 1, 1, 0), Color(1, 1, 1, 1)] 35 | } 36 | 37 | [sub_resource type="AnimationLibrary" id="AnimationLibrary_lxre1"] 38 | _data = { 39 | "RESET": SubResource("Animation_wrshe"), 40 | "slide": SubResource("Animation_fjliv") 41 | } 42 | 43 | [node name="UpdatePopup" type="Window" node_paths=PackedStringArray("http_request", "slidetxt_anim_play", "status_label", "main_title", "current_version", "update_btn", "update_progress_bar")] 44 | title = "Check for updates" 45 | initial_position = 2 46 | size = Vector2i(400, 350) 47 | script = ExtResource("1_1e3dt") 48 | http_request = NodePath("HTTPRequest") 49 | slidetxt_anim_play = NodePath("Panel/VBoxContainer/HBoxContainer/Control/AnimPlay") 50 | status_label = NodePath("Panel/VBoxContainer/HBoxContainer/Control/Title") 51 | main_title = NodePath("Panel/VBoxContainer/Label") 52 | current_version = NodePath("Panel/VBoxContainer/CurrentVersion") 53 | update_btn = NodePath("Panel/VBoxContainer/HBoxContainer/UpdateButton") 54 | update_progress_bar = NodePath("Panel/VBoxContainer/HBoxContainer/UpdateProgressBar") 55 | 56 | [node name="Panel" type="Panel" parent="."] 57 | self_modulate = Color(0.74902, 0.74902, 0.74902, 1) 58 | anchors_preset = 15 59 | anchor_right = 1.0 60 | anchor_bottom = 1.0 61 | grow_horizontal = 2 62 | grow_vertical = 2 63 | 64 | [node name="VBoxContainer" type="VBoxContainer" parent="Panel"] 65 | layout_mode = 0 66 | offset_left = 15.0 67 | offset_right = 385.0 68 | offset_bottom = 350.0 69 | grow_horizontal = 2 70 | grow_vertical = 2 71 | theme_override_constants/separation = 8 72 | alignment = 1 73 | 74 | [node name="VerIcon" type="TextureRect" parent="Panel/VBoxContainer"] 75 | custom_minimum_size = Vector2(0, 120) 76 | layout_mode = 2 77 | texture = ExtResource("2_smiai") 78 | expand_mode = 1 79 | stretch_mode = 5 80 | 81 | [node name="Label" type="Label" parent="Panel/VBoxContainer"] 82 | layout_mode = 2 83 | theme_override_font_sizes/font_size = 24 84 | text = "Checking Updates..." 85 | horizontal_alignment = 1 86 | 87 | [node name="HSeparator" type="HSeparator" parent="Panel/VBoxContainer"] 88 | layout_mode = 2 89 | 90 | [node name="HBoxContainer" type="VBoxContainer" parent="Panel/VBoxContainer"] 91 | layout_mode = 2 92 | theme_override_constants/separation = 7 93 | alignment = 1 94 | 95 | [node name="UpdateButton" type="Button" parent="Panel/VBoxContainer/HBoxContainer"] 96 | layout_mode = 2 97 | text = "Update" 98 | 99 | [node name="UpdateProgressBar" type="ProgressBar" parent="Panel/VBoxContainer/HBoxContainer"] 100 | visible = false 101 | layout_mode = 2 102 | show_percentage = false 103 | 104 | [node name="Control" type="Control" parent="Panel/VBoxContainer/HBoxContainer"] 105 | clip_contents = true 106 | custom_minimum_size = Vector2(0, 22) 107 | layout_mode = 2 108 | 109 | [node name="Title" type="Label" parent="Panel/VBoxContainer/HBoxContainer/Control"] 110 | layout_mode = 1 111 | anchors_preset = 15 112 | anchor_right = 1.0 113 | anchor_bottom = 1.0 114 | offset_bottom = 1.00001 115 | grow_horizontal = 2 116 | grow_vertical = 2 117 | horizontal_alignment = 1 118 | 119 | [node name="AnimPlay" type="AnimationPlayer" parent="Panel/VBoxContainer/HBoxContainer/Control"] 120 | libraries = { 121 | "": SubResource("AnimationLibrary_lxre1") 122 | } 123 | 124 | [node name="LinkButton2" type="LinkButton" parent="Panel/VBoxContainer/HBoxContainer"] 125 | layout_mode = 2 126 | size_flags_horizontal = 4 127 | text = "Read Release Notes" 128 | uri = "https://github.com/maji-git/multiplay-core/releases" 129 | 130 | [node name="CurrentVersion" type="Label" parent="Panel/VBoxContainer"] 131 | modulate = Color(1, 1, 1, 0.658824) 132 | layout_mode = 2 133 | theme_override_font_sizes/font_size = 14 134 | text = "Current Version:" 135 | horizontal_alignment = 1 136 | 137 | [node name="HTTPRequest" type="HTTPRequest" parent="."] 138 | 139 | [connection signal="close_requested" from="." to="." method="queue_free"] 140 | [connection signal="pressed" from="Panel/VBoxContainer/HBoxContainer/UpdateButton" to="." method="_on_update_button_pressed"] 141 | -------------------------------------------------------------------------------- /addons/MultiplayCore/editor/window/welcome_popup.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://dn2ag67h6kg0u"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://c01w42px0l5kw" path="res://addons/MultiplayCore/icons/icon.svg" id="1_flh7u"] 4 | 5 | [node name="WelcomePopup" type="Window"] 6 | title = "Welcome!" 7 | initial_position = 2 8 | size = Vector2i(400, 350) 9 | 10 | [node name="Panel" type="Panel" parent="."] 11 | self_modulate = Color(0.74902, 0.74902, 0.74902, 1) 12 | anchors_preset = 15 13 | anchor_right = 1.0 14 | anchor_bottom = 1.0 15 | grow_horizontal = 2 16 | grow_vertical = 2 17 | 18 | [node name="VBoxContainer" type="VBoxContainer" parent="Panel"] 19 | layout_mode = 0 20 | offset_left = 15.0 21 | offset_right = 385.0 22 | offset_bottom = 350.0 23 | grow_horizontal = 2 24 | grow_vertical = 2 25 | theme_override_constants/separation = 8 26 | alignment = 1 27 | 28 | [node name="TextureRect" type="TextureRect" parent="Panel/VBoxContainer"] 29 | custom_minimum_size = Vector2(0, 120) 30 | layout_mode = 2 31 | texture = ExtResource("1_flh7u") 32 | expand_mode = 1 33 | stretch_mode = 5 34 | 35 | [node name="Label" type="Label" parent="Panel/VBoxContainer"] 36 | layout_mode = 2 37 | theme_override_font_sizes/font_size = 24 38 | text = "Welcome to MultiPlay Core!" 39 | horizontal_alignment = 1 40 | 41 | [node name="Label2" type="Label" parent="Panel/VBoxContainer"] 42 | custom_minimum_size = Vector2(200, 0) 43 | layout_mode = 2 44 | theme_override_font_sizes/font_size = 14 45 | text = "Thank you for checking out on MultiPlay Core! MultiPlay Core is still in it's early stages, so expect bugs ahead!" 46 | horizontal_alignment = 1 47 | autowrap_mode = 2 48 | 49 | [node name="HSeparator" type="HSeparator" parent="Panel/VBoxContainer"] 50 | layout_mode = 2 51 | 52 | [node name="HBoxContainer" type="HBoxContainer" parent="Panel/VBoxContainer"] 53 | layout_mode = 2 54 | theme_override_constants/separation = 16 55 | alignment = 1 56 | 57 | [node name="LinkButton" type="LinkButton" parent="Panel/VBoxContainer/HBoxContainer"] 58 | layout_mode = 2 59 | text = "Open Documentation" 60 | uri = "https://mpc.himaji.xyz/docs/" 61 | 62 | [node name="LinkButton2" type="LinkButton" parent="Panel/VBoxContainer/HBoxContainer"] 63 | layout_mode = 2 64 | text = "Get Support" 65 | uri = "https://mpc.himaji.xyz/docs/get-support/" 66 | 67 | [node name="Close" type="Button" parent="Panel/VBoxContainer"] 68 | layout_mode = 2 69 | text = "Close" 70 | 71 | [connection signal="close_requested" from="." to="." method="set_visible" binds= [false]] 72 | [connection signal="pressed" from="Panel/VBoxContainer/Close" to="." method="set_visible" binds= [false]] 73 | -------------------------------------------------------------------------------- /addons/MultiplayCore/fonts/VictorMono-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maji-git/multiplay-core/a6d496bbf2c36c59d108d2a93bc1dd184faec241/addons/MultiplayCore/fonts/VictorMono-Medium.ttf -------------------------------------------------------------------------------- /addons/MultiplayCore/fonts/VictorMono-Medium.ttf.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="font_data_dynamic" 4 | type="FontFile" 5 | uid="uid://up2o2ylbi16n" 6 | path="res://.godot/imported/VictorMono-Medium.ttf-fe43e388f89ae07b01b654d205618d45.fontdata" 7 | 8 | [deps] 9 | 10 | source_file="res://addons/MultiplayCore/fonts/VictorMono-Medium.ttf" 11 | dest_files=["res://.godot/imported/VictorMono-Medium.ttf-fe43e388f89ae07b01b654d205618d45.fontdata"] 12 | 13 | [params] 14 | 15 | Rendering=null 16 | antialiasing=1 17 | generate_mipmaps=false 18 | disable_embedded_bitmaps=true 19 | multichannel_signed_distance_field=false 20 | msdf_pixel_range=8 21 | msdf_size=48 22 | allow_system_fallback=true 23 | force_autohinter=false 24 | hinting=1 25 | subpixel_positioning=1 26 | oversampling=0.0 27 | Fallbacks=null 28 | fallbacks=[] 29 | Compress=null 30 | compress=true 31 | preload=[] 32 | language_support={} 33 | script_support={} 34 | opentype_features={} 35 | -------------------------------------------------------------------------------- /addons/MultiplayCore/gizmos/Spawner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /addons/MultiplayCore/gizmos/Spawner.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://btpf20u8s0gy1" 6 | path="res://.godot/imported/Spawner.svg-a42c35fa2a2015c76982c1c900651fd2.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/gizmos/Spawner.svg" 14 | dest_files=["res://.godot/imported/Spawner.svg-a42c35fa2a2015c76982c1c900651fd2.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=4.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/ENetProtocol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/ENetProtocol.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bddau6vugr4ic" 6 | path="res://.godot/imported/ENetProtocol.svg-da95e9bd92b04d7004773a317bb15387.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/ENetProtocol.svg" 14 | dest_files=["res://.godot/imported/ENetProtocol.svg-da95e9bd92b04d7004773a317bb15387.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=3.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/LatencyNetProtocol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/LatencyNetProtocol.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bow4spxk57m8k" 6 | path="res://.godot/imported/LatencyNetProtocol.svg-e4b7d956a8b65dad71ab681a422b25bb.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/LatencyNetProtocol.svg" 14 | dest_files=["res://.godot/imported/LatencyNetProtocol.svg-e4b7d956a8b65dad71ab681a422b25bb.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPAnimTreeSync.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPAnimTreeSync.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://6vaw4hp8vipf" 6 | path="res://.godot/imported/MPAnimTreeSync.svg-5f9b292eab9cf45e27c17596b6590eb1.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/MPAnimTreeSync.svg" 14 | dest_files=["res://.godot/imported/MPAnimTreeSync.svg-5f9b292eab9cf45e27c17596b6590eb1.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPAnimationSync.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPAnimationSync.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://rgkywlaiolqp" 6 | path="res://.godot/imported/MPAnimationSync.svg-69cea5af27fd4636f2821efe7253f61d.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/MPAnimationSync.svg" 14 | dest_files=["res://.godot/imported/MPAnimationSync.svg-69cea5af27fd4636f2821efe7253f61d.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPAuth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPAuth.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bqrbedqykjml3" 6 | path="res://.godot/imported/MPAuth.svg-0b19f7002e685f9cd98ddfc75afa9eff.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/MPAuth.svg" 14 | dest_files=["res://.godot/imported/MPAuth.svg-0b19f7002e685f9cd98ddfc75afa9eff.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=3.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPBase.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPBase.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://7auhbxagkd3m" 6 | path="res://.godot/imported/MPBase.svg-803798640ebedf7a10cd3e53b1b2c288.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/MPBase.svg" 14 | dest_files=["res://.godot/imported/MPBase.svg-803798640ebedf7a10cd3e53b1b2c288.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=3.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPDebugPlay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPDebugPlay.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://by2ucfx05s3su" 6 | path="res://.godot/imported/MPDebugPlay.svg-86aac4619f50e83b2836c3b060797e02.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/MPDebugPlay.svg" 14 | dest_files=["res://.godot/imported/MPDebugPlay.svg-86aac4619f50e83b2836c3b060797e02.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPDebugPlay_Pressed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPDebugPlay_Pressed.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://c8r5bjd8wyfn0" 6 | path="res://.godot/imported/MPDebugPlay_Pressed.svg-093f6274fe3012a58613e4fda7981526.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/MPDebugPlay_Pressed.svg" 14 | dest_files=["res://.godot/imported/MPDebugPlay_Pressed.svg-093f6274fe3012a58613e4fda7981526.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPExtension.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPExtension.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://8tr141uujj8o" 6 | path="res://.godot/imported/MPExtension.svg-2166dda9cdfa90dd25694cd691e58011.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/MPExtension.svg" 14 | dest_files=["res://.godot/imported/MPExtension.svg-2166dda9cdfa90dd25694cd691e58011.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=3.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPNetProtocolBase.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPNetProtocolBase.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://k0ytc3st8e8q" 6 | path="res://.godot/imported/MPNetProtocolBase.svg-9c000ab6ccf9356ea22b56fdc573abc3.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/MPNetProtocolBase.svg" 14 | dest_files=["res://.godot/imported/MPNetProtocolBase.svg-9c000ab6ccf9356ea22b56fdc573abc3.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=3.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPPeersCollection.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPPeersCollection.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://u6kck42a08gh" 6 | path="res://.godot/imported/MPPeersCollection.svg-dee69bfe7ef42d77552f725d40b70b04.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/MPPeersCollection.svg" 14 | dest_files=["res://.godot/imported/MPPeersCollection.svg-dee69bfe7ef42d77552f725d40b70b04.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=3.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPPlayer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPPlayer.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://c8myk3gkwqplh" 6 | path="res://.godot/imported/MPPlayer.svg-cb591a48e2be20c308668cacafe2d4c1.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/MPPlayer.svg" 14 | dest_files=["res://.godot/imported/MPPlayer.svg-cb591a48e2be20c308668cacafe2d4c1.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=3.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPStartTransform2D.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPStartTransform2D.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://jl6s1tqi5kch" 6 | path="res://.godot/imported/MPStartTransform2D.svg-8ff318773ae13dfb4c01d6f001227e45.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/MPStartTransform2D.svg" 14 | dest_files=["res://.godot/imported/MPStartTransform2D.svg-8ff318773ae13dfb4c01d6f001227e45.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=3.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPStartTransform3D.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPStartTransform3D.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bnt63jyg1scmb" 6 | path="res://.godot/imported/MPStartTransform3D.svg-1a8611cf833f3a259d00ede09a4dcbee.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/MPStartTransform3D.svg" 14 | dest_files=["res://.godot/imported/MPStartTransform3D.svg-1a8611cf833f3a259d00ede09a4dcbee.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=3.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPSyncBase.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPSyncBase.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cltdklj3xlok3" 6 | path="res://.godot/imported/MPSyncBase.svg-00be4ef98eb70cfd7abf26811f0d271c.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/MPSyncBase.svg" 14 | dest_files=["res://.godot/imported/MPSyncBase.svg-00be4ef98eb70cfd7abf26811f0d271c.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPTransformSync.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MPTransformSync.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dsfch170g755d" 6 | path="res://.godot/imported/MPTransformSync.svg-9bdf6eda3ac0b412cd5882a8806971e3.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/MPTransformSync.svg" 14 | dest_files=["res://.godot/imported/MPTransformSync.svg-9bdf6eda3ac0b412cd5882a8806971e3.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MultiPlayCore.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MultiPlayCore.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dupc3co7kupa3" 6 | path="res://.godot/imported/MultiPlayCore.svg-35f8569a58e11cd9c8aa148e9d050f2e.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/MultiPlayCore.svg" 14 | dest_files=["res://.godot/imported/MultiPlayCore.svg-35f8569a58e11cd9c8aa148e9d050f2e.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=3.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MultiPlayIO.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/MultiPlayIO.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cc4p14xddfuxq" 6 | path="res://.godot/imported/MultiPlayIO.svg-bb46b28bd83736eaf5270e90cc362850.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/MultiPlayIO.svg" 14 | dest_files=["res://.godot/imported/MultiPlayIO.svg-bb46b28bd83736eaf5270e90cc362850.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=3.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/WebSocketNetProtocol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/WebSocketNetProtocol.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cwtb38ffgsi07" 6 | path="res://.godot/imported/WebSocketNetProtocol.svg-aa25812f3d15dd6c1d2d592024ade42d.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/WebSocketNetProtocol.svg" 14 | dest_files=["res://.godot/imported/WebSocketNetProtocol.svg-aa25812f3d15dd6c1d2d592024ade42d.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=3.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /addons/MultiplayCore/icons/icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://c01w42px0l5kw" 6 | path="res://.godot/imported/icon.svg-0e26b57068e7508dec2c10f86f7c1c40.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/MultiplayCore/icons/icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-0e26b57068e7508dec2c10f86f7c1c40.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=12.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/MultiplayCore/net_protocols/ENetProtocol.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | @icon("res://addons/MultiplayCore/icons/ENetProtocol.svg") 3 | extends MPNetProtocolBase 4 | ## Websocket Network Protocol 5 | class_name ENetProtocol 6 | 7 | ## Set ENet host compression mode 8 | @export var compression_mode: ENetConnection.CompressionMode = ENetConnection.COMPRESS_ZLIB 9 | 10 | @export_subgroup("Bandwidth") 11 | ## Bandwidth In Limit 12 | @export var bandwidth_in_limit: int = 0 13 | ## Bandwidth Out Limit 14 | @export var bandwidth_out_limit: int = 0 15 | 16 | @export_subgroup("Secure Options") 17 | ## Specify if you needs encryption in your ENet 18 | @export var secure: bool 19 | ## Secure Private key for server 20 | @export var server_private_key: CryptoKey 21 | ## Trusted SSL certificate for server & client 22 | @export var ssl_certificate: X509Certificate 23 | 24 | var role = "" 25 | 26 | func _ready(): 27 | super() 28 | 29 | func _apply_peer_config(peer: ENetMultiplayerPeer, address: String): 30 | peer.host.compress(compression_mode) 31 | peer.host.bandwidth_limit(bandwidth_in_limit, bandwidth_out_limit) 32 | 33 | if secure: 34 | if role == "server": 35 | # Server setup 36 | peer.host.dtls_server_setup(TLSOptions.server(server_private_key, ssl_certificate)) 37 | else: 38 | # Client setup 39 | peer.host.dtls_client_setup(address, TLSOptions.client(ssl_certificate)) 40 | 41 | ## Host function 42 | func host(port, bind_ip, max_players) -> MultiplayerPeer: 43 | role = "server" 44 | var peer = ENetMultiplayerPeer.new() 45 | peer.set_bind_ip(bind_ip) 46 | var err = peer.create_server(port, max_players) 47 | 48 | _apply_peer_config(peer, bind_ip) 49 | 50 | if err != OK: 51 | MPIO.logerr("Server host failed") 52 | 53 | return peer 54 | 55 | func join(address, port) -> MultiplayerPeer: 56 | role = "client" 57 | var peer = ENetMultiplayerPeer.new() 58 | var err = peer.create_client(address, port) 59 | 60 | _apply_peer_config(peer, address) 61 | 62 | if err != OK: 63 | MPIO.logerr("Client connect failed") 64 | 65 | return peer 66 | -------------------------------------------------------------------------------- /addons/MultiplayCore/net_protocols/LatencyNetProtocol.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | @icon("res://addons/MultiplayCore/icons/LatencyNetProtocol.svg") 3 | extends ENetProtocol 4 | ## Network Protocol for simulating network latency, based on ENet/UDP. Intended for debuging purposes. 5 | class_name LatencyNetProtocol 6 | 7 | # Thanks to kraybit for the UDP latency simulation 8 | # https://forum.godotengine.org/t/how-i-can-simulate-a-network-latency-and-packet-loss-between-client-and-server-peers/25012 9 | 10 | ## Simulate Network Latency in Microseconds 11 | @export_range(0, 5000) var simulate_latency_ms : int = 100 12 | ## Simulate Chance of Network Loss 13 | @export_range(0, 0.5) var simulate_loss : float = 0 14 | 15 | var _virtual_port = -1 16 | var _true_server_port = -1 17 | 18 | var _vserver_peer : PacketPeerUDP 19 | var _vserver_has_dest_address : bool = false 20 | var _vserver_first_client_port : int = -1 21 | var _vclient_peer : PacketPeerUDP 22 | 23 | func _ready(): 24 | super() 25 | if Engine.is_editor_hint(): 26 | return 27 | 28 | _true_server_port = mpc.port 29 | _virtual_port = mpc.port + 1 30 | 31 | _vserver_peer = PacketPeerUDP.new() 32 | _vserver_peer.bind(_virtual_port, "127.0.0.1") 33 | 34 | _vclient_peer = PacketPeerUDP.new() 35 | _vclient_peer.set_dest_address("127.0.0.1", mpc.port) 36 | 37 | if not OS.is_debug_build(): 38 | MPIO.logwarn("LatencyNetProtocol is currently in use! Please change network protocol in the production build!") 39 | simulate_latency_ms = 0 40 | simulate_loss = 0 41 | 42 | ## Host function 43 | func host(port, bind_ip, max_players) -> MultiplayerPeer: 44 | return super(port, bind_ip, max_players) 45 | 46 | func join(address, port) -> MultiplayerPeer: 47 | return super("127.0.0.1", _virtual_port) 48 | 49 | class QueueEntry : 50 | var byte_array : PackedByteArray 51 | var qued_at : int 52 | 53 | func _init(packet:PackedByteArray, time_now:int) : 54 | self.byte_array = packet 55 | self.qued_at = time_now 56 | 57 | var _client_to_server_queue : Array[QueueEntry] 58 | var _server_to_client_queue : Array[QueueEntry] 59 | 60 | func _process(_delta : float) -> void : 61 | if Engine.is_editor_hint(): 62 | return 63 | var now : int = Time.get_ticks_msec() 64 | var send_at_ms = now - simulate_latency_ms 65 | 66 | # Handle packets Client -> Server 67 | while _vserver_peer.get_available_packet_count() > 0 : 68 | var packet = _vserver_peer.get_packet() 69 | var err = _vserver_peer.get_packet_error() 70 | if err != OK : 71 | push_error("DebugUDPLagger : Incoming packet error : ", err) 72 | continue 73 | 74 | var from_port = _vserver_peer.get_packet_port() 75 | 76 | if not _vserver_has_dest_address : 77 | # Assume the first who send a packet to us is the True Client 78 | _vserver_peer.set_dest_address("127.0.0.1", from_port) 79 | _vserver_first_client_port = from_port 80 | _vserver_has_dest_address = true 81 | elif _vserver_first_client_port != from_port : 82 | push_warning("DebugUDPLagger : VServer got packet from unknown port, ignored.") 83 | continue 84 | 85 | _client_to_server_queue.push_back(QueueEntry.new(packet, now)) 86 | 87 | _process_queue(_client_to_server_queue, _vclient_peer, send_at_ms) 88 | 89 | # Ignore check for any incoming packets from the true server 90 | if not _vserver_has_dest_address : 91 | return 92 | 93 | # Handle packets Server -> Client 94 | while _vclient_peer.get_available_packet_count() > 0 : 95 | var packet = _vclient_peer.get_packet() 96 | var err = _vclient_peer.get_packet_error() 97 | if err != OK : 98 | push_error("DebugUDPLagger : Incoming packet error : ", err) 99 | continue 100 | 101 | var from_port = _vclient_peer.get_packet_port() 102 | if from_port != _true_server_port : 103 | push_warning("DebugUDPLagger : VClient got packet from unknown port, ignored.") 104 | continue 105 | 106 | _server_to_client_queue.push_back(QueueEntry.new(packet, now)) 107 | 108 | _process_queue(_server_to_client_queue, _vserver_peer, send_at_ms) 109 | 110 | 111 | func _process_queue(que : Array[QueueEntry], to_peer : PacketPeerUDP, send_at_ms : int) : 112 | while not que.is_empty() : 113 | var front = que.front() 114 | if send_at_ms >= front.qued_at : 115 | if simulate_loss <= 0 || randf() >= simulate_loss: 116 | to_peer.put_packet(front.byte_array) 117 | que.pop_front() 118 | else : 119 | break 120 | -------------------------------------------------------------------------------- /addons/MultiplayCore/net_protocols/MPNetProtocolBase.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | @icon("res://addons/MultiplayCore/icons/MPNetProtocolBase.svg") 3 | extends MPExtension 4 | ## Base Class for all network protocols 5 | class_name MPNetProtocolBase 6 | 7 | var net_protocols = [] 8 | 9 | func _ready(): 10 | super() 11 | mpc.register_net_extension(self) 12 | 13 | ## Debug URL Override 14 | func _override_debug_url(bind_ip: String, port: int): 15 | return null 16 | 17 | ## Host function 18 | func host(port, bind_ip, max_players) -> MultiplayerPeer: 19 | return OfflineMultiplayerPeer.new() 20 | 21 | ## Join Function 22 | func join(address, port) -> MultiplayerPeer: 23 | return OfflineMultiplayerPeer.new() 24 | -------------------------------------------------------------------------------- /addons/MultiplayCore/net_protocols/WebSocketNetProtocol.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | @icon("res://addons/MultiplayCore/icons/WebSocketNetProtocol.svg") 3 | extends MPNetProtocolBase 4 | ## Websocket Network Protocol 5 | class_name WebSocketNetProtocol 6 | 7 | ## Specify if you needs encryption in your web socket 8 | @export var secure: bool 9 | @export_subgroup("Secure Options") 10 | 11 | ## Secure Private key for server 12 | @export var server_private_key: CryptoKey 13 | ## Trusted SSL certificate for server & client 14 | @export var ssl_certificate: X509Certificate 15 | 16 | ## Host function 17 | func host(port, bind_ip, max_players) -> MultiplayerPeer: 18 | var server_tls_options = null 19 | 20 | if secure and server_private_key and ssl_certificate: 21 | server_tls_options = TLSOptions.server(server_private_key, ssl_certificate) 22 | 23 | var peer = WebSocketMultiplayerPeer.new() 24 | peer.create_server(port, bind_ip, server_tls_options) 25 | 26 | return peer 27 | 28 | func join(address, port) -> MultiplayerPeer: 29 | var client_tls_options = null 30 | 31 | var protocol = "ws" 32 | 33 | if secure: 34 | if ssl_certificate: 35 | client_tls_options = TLSOptions.client(ssl_certificate) 36 | protocol = "wss" 37 | 38 | var portstr = "" 39 | 40 | if port: 41 | portstr = ":" + str(port) 42 | 43 | var url = protocol + "://" + address + portstr 44 | 45 | var peer = WebSocketMultiplayerPeer.new() 46 | peer.create_client(url, client_tls_options) 47 | 48 | return peer 49 | -------------------------------------------------------------------------------- /addons/MultiplayCore/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="MultiPlayCore" 4 | description="Develop Online, Multiple Controller and solo multiplayer game at once!" 5 | author="maji" 6 | version="1.1.0-beta2" 7 | script="MultiPlayTool.gd" 8 | -------------------------------------------------------------------------------- /addons/MultiplayCore/scenes/multiplay_player.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://djdmom34ja7x0"] 2 | 3 | [ext_resource type="Script" path="res://addons/MultiplayCore/MPPlayer.gd" id="1_ba24x"] 4 | 5 | [sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_a8er7"] 6 | properties/0/path = NodePath(".:ping_ms") 7 | properties/0/spawn = true 8 | properties/0/replication_mode = 2 9 | properties/1/path = NodePath(".:handshake_data") 10 | properties/1/spawn = true 11 | properties/1/replication_mode = 2 12 | properties/2/path = NodePath(".:player_id") 13 | properties/2/spawn = true 14 | properties/2/replication_mode = 2 15 | 16 | [node name="MultiPlayPeer" type="Node"] 17 | script = ExtResource("1_ba24x") 18 | 19 | [node name="MultiSync" type="MultiplayerSynchronizer" parent="."] 20 | replication_config = SubResource("SceneReplicationConfig_a8er7") 21 | -------------------------------------------------------------------------------- /addons/MultiplayCore/spawner/MPSpawnerTransform2D.gd: -------------------------------------------------------------------------------- 1 | @icon("res://addons/MultiplayCore/icons/MPStartTransform2D.svg") 2 | @tool 3 | extends Node2D 4 | ## Start Transform of 2D [b]NOT READY TO BE USED[b] 5 | ## @experimental 6 | class_name MPStartTransform2D 7 | 8 | 9 | # Called when the node enters the scene tree for the first time. 10 | func _ready(): 11 | if Engine.is_editor_hint(): 12 | pass 13 | #print(EditorInterface.get_editor_viewport_2d()) 14 | 15 | 16 | # Called every frame. 'delta' is the elapsed time since the previous frame. 17 | func _process(delta): 18 | pass 19 | 20 | func _draw(): 21 | if Engine.is_editor_hint(): 22 | 23 | draw_set_transform_matrix(Transform2D.IDENTITY) 24 | 25 | draw_texture(preload("res://addons/MultiplayCore/gizmos/Spawner.svg"), Vector2(0,0)) 26 | 27 | -------------------------------------------------------------------------------- /addons/MultiplayCore/synchronizers/MPAnimTreeSync.gd: -------------------------------------------------------------------------------- 1 | @icon("res://addons/MultiplayCore/icons/MPAnimTreeSync.svg") 2 | extends MPSyncBase 3 | ## Network Animation Tree Synchronizer 4 | class_name MPAnimTreeSync 5 | 6 | var _parent: AnimationTree = null 7 | 8 | var property_list = [] 9 | 10 | func _ready(): 11 | super() 12 | _parent = get_parent() 13 | 14 | if not _parent is AnimationTree: 15 | MPIO.logwarn("MPAnimationSync: Need to be parented to AnimationTree.") 16 | set_process(false) 17 | return 18 | 19 | for p in _parent.get_property_list(): 20 | if p.name.begins_with("parameters/"): 21 | property_list.append({ 22 | data_type = p.class_name, 23 | path = p.name, 24 | obj = _parent.get(p.name), 25 | data = { 26 | temp_current_node = "", 27 | one_shot_active = false, 28 | old_val = null 29 | } 30 | }) 31 | 32 | func _physics_process(delta): 33 | if !should_sync(): 34 | return 35 | 36 | # Only watch for changes if is authority or server 37 | if check_send_permission(): 38 | for p in property_list: 39 | var obj = p.obj 40 | var path: String = p.path 41 | 42 | if obj is AnimationNodeStateMachinePlayback: 43 | # Sync state machine 44 | var current_anim = obj.get_current_node() 45 | 46 | if p.data.temp_current_node != current_anim: 47 | p.data.temp_current_node = current_anim 48 | 49 | rpc("_recv_play_anim_nsmp", p.path, current_anim) 50 | elif path.ends_with("/active"): 51 | # Sync one shot animations 52 | var val = _parent.get(path) 53 | 54 | if val: 55 | if !p.data.one_shot_active: 56 | p.data.one_shot_active = true 57 | rpc("_recv_oneshot_anim", path.trim_suffix("/active")) 58 | else: 59 | if p.data.one_shot_active: 60 | p.data.one_shot_active = false 61 | else: 62 | # Ignore time parameters 63 | if path.ends_with("/time"): 64 | continue 65 | 66 | # Sync any properties 67 | var val = _parent.get(path) 68 | 69 | var do_sync = false 70 | # Check for types 71 | if p.data.old_val == null: 72 | do_sync = true 73 | elif typeof(val) == typeof(p.data.old_val): 74 | if val != p.data.old_val: 75 | do_sync = true 76 | 77 | if do_sync: 78 | p.data.old_val = val 79 | 80 | rpc("_recv_prop_sync", path, val) 81 | 82 | 83 | # Handle play animation for AnimationNodeStateMachinePlayback 84 | @rpc("any_peer", "call_local", "unreliable_ordered") 85 | func _recv_play_anim_nsmp(property_path: String, travel_to: String): 86 | # Allow animation change from authority & server 87 | if !check_recv_permission(): 88 | return 89 | 90 | if !check_is_local(): 91 | var anim_playback = _parent.get(property_path) 92 | 93 | if anim_playback is AnimationNodeStateMachinePlayback: 94 | if travel_to == "": 95 | anim_playback.stop() 96 | else: 97 | anim_playback.travel(travel_to) 98 | 99 | # Handle play animation for One Shot properties 100 | @rpc("any_peer", "call_local", "unreliable_ordered") 101 | func _recv_oneshot_anim(property_path: String): 102 | # Allow animation change from authority & server 103 | if !check_recv_permission(): 104 | return 105 | 106 | if !check_is_local(): 107 | _parent.set(property_path + "/request", true) 108 | 109 | # Handle property sync 110 | @rpc("any_peer", "call_local", "unreliable_ordered") 111 | func _recv_prop_sync(property_path: String, value): 112 | # Allow animation change from authority & server 113 | if !check_recv_permission(): 114 | return 115 | 116 | if !check_is_local(): 117 | _parent.set(property_path, value) 118 | -------------------------------------------------------------------------------- /addons/MultiplayCore/synchronizers/MPAnimationSync.gd: -------------------------------------------------------------------------------- 1 | @icon("res://addons/MultiplayCore/icons/MPAnimationSync.svg") 2 | extends MPSyncBase 3 | ## Network Animation Synchronizer 4 | class_name MPAnimationSync 5 | 6 | var _net_current_animation = "" 7 | 8 | var _parent: AnimationPlayer = null 9 | 10 | func _ready(): 11 | super() 12 | _parent = get_parent() 13 | 14 | if not _parent is AnimationPlayer: 15 | MPIO.logwarn("MPAnimationSync: Need to be parented to AnimationPlayer.") 16 | set_process(false) 17 | return 18 | 19 | if should_sync() and check_send_permission(): 20 | _parent.animation_started.connect(_on_animation_started) 21 | 22 | func _on_animation_started(new_anim): 23 | _net_current_animation = new_anim 24 | 25 | rpc("_recv_play_anim", _net_current_animation, _parent.get_playing_speed()) 26 | 27 | @rpc("any_peer", "call_local", "unreliable_ordered") 28 | func _recv_play_anim(anim_name: String, anim_speed = 1.0): 29 | # Allow animation change from authority & server 30 | if !check_recv_permission(): 31 | return 32 | 33 | _net_current_animation = anim_name 34 | 35 | if !check_is_local(): 36 | _parent.speed_scale = 1 37 | if anim_name == "": 38 | _parent.stop(true) 39 | else: 40 | _parent.play(anim_name, -1, anim_speed) 41 | -------------------------------------------------------------------------------- /addons/MultiplayCore/synchronizers/MPSyncBase.gd: -------------------------------------------------------------------------------- 1 | @icon("res://addons/MultiplayCore/icons/MPSyncBase.svg") 2 | extends MPBase 3 | ## Base class of Network Synchronizers 4 | class_name MPSyncBase 5 | 6 | func _ready(): 7 | pass 8 | 9 | func should_sync(): 10 | return MPIO.mpc.mode == MPIO.mpc.PlayMode.Online 11 | 12 | ## If this node is local to current multiplayer peer 13 | func check_is_local(): 14 | # currently use the same implementation as check_send_permission 15 | return check_send_permission() 16 | 17 | ## Check network send permission 18 | func check_send_permission(): 19 | if !MPIO.mpc.online_connected: 20 | return 21 | return multiplayer.get_unique_id() == get_multiplayer_authority() 22 | 23 | ## Check receive permission 24 | func check_recv_permission(is_server_cmd: bool = false): 25 | var sender_id = multiplayer.get_remote_sender_id() 26 | if sender_id != get_multiplayer_authority(): 27 | if is_server_cmd and sender_id == 1: 28 | return true 29 | else: 30 | return false 31 | return true 32 | -------------------------------------------------------------------------------- /addons/MultiplayCore/synchronizers/MPTransformSync.gd: -------------------------------------------------------------------------------- 1 | @icon("res://addons/MultiplayCore/icons/MPTransformSync.svg") 2 | extends MPSyncBase 3 | ## Network Transform Synchronizer 4 | class_name MPTransformSync 5 | 6 | ## Enable lerp for sync? 7 | @export var lerp_enabled: bool = true 8 | ## Determines lerp speed 9 | @export var lerp_speed: int = 20 10 | 11 | @export_subgroup("Sync Transform") 12 | ## Determines if position will be sync 13 | @export var sync_position: bool = true 14 | ## Determines if rotation will be sync 15 | @export var sync_rotation: bool = true 16 | ## Determines if scale will be sync 17 | @export var sync_scale: bool = false 18 | 19 | @export_subgroup("Sync Sensitivity") 20 | ## Determines the sync sensitivity of position 21 | @export var position_sensitivity: float = 0.01 22 | ## Determines the sync sensitivity of rotation 23 | @export var rotation_sensitivity: float = 0.01 24 | ## Determines the sync sensitivity of scale 25 | @export var scale_sensitivity: float = 0.01 26 | 27 | var _net_position = null 28 | var _net_rotation = null 29 | var _net_scale = null 30 | 31 | var _old_position = null 32 | var _old_rotation = null 33 | var _old_scale = null 34 | 35 | var _parent = null 36 | var _sync_type = "" 37 | 38 | func _ready(): 39 | super() 40 | _parent = get_parent() 41 | 42 | if _parent is Node2D: 43 | _sync_type = "2d" 44 | elif _parent is Node3D: 45 | _sync_type = "3d" 46 | 47 | _net_position = _parent.position 48 | _net_rotation = _parent.rotation 49 | _net_scale = _parent.scale 50 | 51 | # Sync when new player joins 52 | if should_sync() and check_send_permission(): 53 | MPIO.mpc.player_connected.connect(_on_player_connected) 54 | 55 | func _on_player_connected(plr: MPPlayer): 56 | if sync_position: 57 | rpc_id(plr.player_id, "_recv_transform_reliable", "pos", _parent.position) 58 | 59 | if sync_rotation: 60 | rpc_id(plr.player_id, "_recv_transform_reliable", "rot", _parent.rotation) 61 | 62 | if sync_scale: 63 | rpc_id(plr.player_id, "_recv_transform_reliable", "scl", _parent.scale) 64 | 65 | func _physics_process(delta): 66 | if !should_sync(): 67 | return 68 | # Only watch for changes if is authority or server 69 | if check_send_permission(): 70 | # Sync Position 71 | if sync_position and (_parent.position - _net_position).length() > position_sensitivity: 72 | rpc("_recv_transform", "pos", _parent.position) 73 | 74 | # Sync Rotation 75 | if sync_rotation: 76 | if _sync_type == "2d" and _parent.rotation - _net_rotation > rotation_sensitivity: 77 | rpc("_recv_transform", "rot", _parent.rotation) 78 | 79 | if _sync_type == "3d" and (_parent.rotation - _net_rotation).length() > rotation_sensitivity: 80 | rpc("_recv_transform", "rot", _parent.rotation) 81 | 82 | # Sync Scale 83 | if sync_scale and (_parent.scale - _net_scale).length() > scale_sensitivity: 84 | rpc("_recv_transform", "scl", _parent.scale) 85 | else: 86 | if lerp_enabled: 87 | # Sync all transforms w/lerp 88 | if sync_position: 89 | _parent.position = _parent.position.lerp(_net_position, delta * lerp_speed) 90 | if sync_rotation: 91 | _parent.rotation = lerp(_parent.rotation, _net_rotation, delta * lerp_speed) 92 | if sync_scale: 93 | _parent.scale = _parent.scale.lerp(_net_scale, delta * lerp_speed) 94 | else: 95 | # Sync all transforms 96 | if sync_position: 97 | _parent.position = _net_position 98 | if sync_rotation: 99 | _parent.rotation = _net_rotation 100 | if sync_scale: 101 | _parent.scale = _net_scale 102 | 103 | ## Set position of the 2D node, Server only. 104 | func set_position_2d(to: Vector2): 105 | rpc("_recv_transform", "pos", to, true) 106 | 107 | ## Set rotation of the 2D node, Server only. 108 | func set_rotation_2d(to: float): 109 | rpc("_recv_transform", "rot", to, true) 110 | 111 | ## Set scale of the 2D node, Server only. 112 | func set_scale_2d(to: Vector2): 113 | rpc("_recv_transform", "scl", to, true) 114 | 115 | ## Set position of the 3D node, Server only. 116 | func set_position_3d(to: Vector3): 117 | rpc("_recv_transform", "pos", to, true) 118 | 119 | ## Set rotation of the 3D node, Server only. 120 | func set_rotation_3d(to: Vector3): 121 | rpc("_recv_transform", "rot", to, true) 122 | 123 | ## Set scale of the 3D node, Server only. 124 | func set_scale_3d(to: Vector3): 125 | rpc("_recv_transform", "scl", to, true) 126 | 127 | @rpc("any_peer", "call_local", "unreliable_ordered") 128 | func _recv_transform(field: String, set_to = null, is_server_cmd = false): 129 | # Allow transform change from authority & server 130 | if !check_recv_permission(is_server_cmd): 131 | return 132 | 133 | if !is_server_cmd: 134 | if field == "pos": 135 | _net_position = set_to 136 | elif field == "rot": 137 | _net_rotation = set_to 138 | elif field == "scl": 139 | _net_scale = set_to 140 | else: 141 | if field == "pos": 142 | _parent.position = set_to 143 | elif field == "rot": 144 | _parent.rotation = set_to 145 | elif field == "scl": 146 | _parent.scale = set_to 147 | 148 | @rpc("any_peer", "call_local", "reliable") 149 | func _recv_transform_reliable(field: String, set_to = null, is_server_cmd = false): 150 | _recv_transform(field, set_to, is_server_cmd) 151 | -------------------------------------------------------------------------------- /script_templates/CharacterBody2D/EightWayMovement.gd: -------------------------------------------------------------------------------- 1 | # meta-name: MultiPlay 8-way Movement 2 | # meta-description: Prebuilt 8-way Movement script that supports MultiPlay inputs. 3 | # meta-default: true 4 | 5 | extends CharacterBody2D 6 | 7 | const SPEED = 300.0 8 | 9 | # Get the MultiPlay Player node, It's the parent of this node! 10 | @onready var mpp: MPPlayer = get_parent() 11 | 12 | func _ready(): 13 | # Listen to MultiPlay Player Signals 14 | mpp.player_ready.connect(_on_player_ready) 15 | mpp.handshake_ready.connect(_on_handshake_ready) 16 | mpp.swap_focused.connect(_on_swap_focused) 17 | mpp.swap_unfocused.connect(_on_swap_unfocused) 18 | 19 | # When swap mode focused on this player 20 | func _on_swap_focused(_old_focus): 21 | pass 22 | 23 | # When swap mode unfocused on this player 24 | func _on_swap_unfocused(_new_focus): 25 | pass 26 | 27 | 28 | # When player node is ready, this only emit locally. 29 | func _on_player_ready(): 30 | print("Player's now ready!") 31 | 32 | # On handshake data is ready. This emits to everyone in the server. You can also use it to init something for all players. 33 | func _on_handshake_ready(hs): 34 | pass 35 | 36 | func _physics_process(delta): 37 | if !mpp.is_ready: 38 | return 39 | 40 | # Get the input vector 41 | # Using UI input actions because it's built-in. 42 | var input_direction = Input.get_vector(mpp.ma("ui_left"), mpp.ma("ui_right"), mpp.ma("ui_up"), mpp.ma("ui_down")) 43 | velocity = input_direction * SPEED 44 | 45 | move_and_slide() 46 | -------------------------------------------------------------------------------- /script_templates/CharacterBody2D/PlatformerMovement.gd: -------------------------------------------------------------------------------- 1 | # meta-name: MultiPlay Platformer Movement 2 | # meta-description: Prebuilt Platformer Movement script that supports MultiPlay inputs. 3 | # meta-default: true 4 | 5 | extends CharacterBody2D 6 | 7 | const SPEED = 300.0 8 | const JUMP_VELOCITY = -400.0 9 | 10 | # Get the gravity from the project settings to be synced with RigidBody nodes. 11 | var gravity = ProjectSettings.get_setting("physics/2d/default_gravity") 12 | 13 | # Get the MultiPlay Player node, It's the parent of this node! 14 | @onready var mpp: MPPlayer = get_parent() 15 | 16 | func _ready(): 17 | # Listen to MultiPlay Player Signals 18 | mpp.player_ready.connect(_on_player_ready) 19 | mpp.handshake_ready.connect(_on_handshake_ready) 20 | mpp.swap_focused.connect(_on_swap_focused) 21 | mpp.swap_unfocused.connect(_on_swap_unfocused) 22 | 23 | # When swap mode focused on this player 24 | func _on_swap_focused(_old_focus): 25 | pass 26 | 27 | # When swap mode unfocused on this player 28 | func _on_swap_unfocused(_new_focus): 29 | pass 30 | 31 | # When player node is ready, this only emit locally. 32 | func _on_player_ready(): 33 | print("Player's now ready!") 34 | 35 | # On handshake data is ready. This emits to everyone in the server. You can also use it to init something for all players. 36 | func _on_handshake_ready(hs): 37 | print(mpp.player_index) 38 | 39 | func _physics_process(delta): 40 | if !mpp.is_ready: 41 | return 42 | 43 | # Add the gravity. 44 | if not is_on_floor(): 45 | velocity.y += gravity * delta 46 | 47 | # Handle jump. 48 | if Input.is_action_just_pressed(mpp.ma("ui_accept")) and is_on_floor(): 49 | velocity.y = JUMP_VELOCITY 50 | 51 | # Get the input direction and handle the movement/deceleration 52 | # Using UI input actions because it's built-in. 53 | var direction = Input.get_axis(mpp.ma("ui_left"), mpp.ma("ui_right")) 54 | if direction: 55 | velocity.x = direction * SPEED 56 | else: 57 | velocity.x = move_toward(velocity.x, 0, SPEED) 58 | 59 | move_and_slide() 60 | -------------------------------------------------------------------------------- /script_templates/Node/AuthTemplate.gd: -------------------------------------------------------------------------------- 1 | # meta-name: MultiPlay Authenticator 2 | # meta-description: Empty MultiPlay Authenticator 3 | 4 | extends Node 5 | 6 | # Assign this variable to MPAuth node 7 | @export var auth: MPAuth 8 | 9 | func _ready(): 10 | auth.authenticate_function = _auth_check 11 | 12 | func _auth_check(plr_id: int, credentials_data: Dictionary, handshake_data: Dictionary): 13 | # Return authentication data, otherwise if failed, return false. 14 | return {} 15 | --------------------------------------------------------------------------------