├── 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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------