Removed most networking related things. I will keep some, also I will still use clientside and serverside methods properly as they will make some more complex features easier to implement.

This commit is contained in:
Relintai 2020-07-16 15:39:25 +02:00
parent 25bd3cf9a5
commit 81b8a73a80
10 changed files with 116 additions and 669 deletions

View File

@ -1,220 +0,0 @@
extends Node
# Copyright (c) 2019-2020 Péter Magyar
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
export (int) var port : int = 23223
signal cplayer_master_created(player_master)
signal cplayer_master_destroyed(player_master)
signal splayer_master_created(player_master)
signal splayer_master_destroyed(player_master)
var splayers_dict : Dictionary = {}
var splayers_array : Array = []
var cplayers_dict : Dictionary = {}
var cplayers_array : Array = []
var _sseed : int
var _cseed : int
var local_player_master : PlayerMaster = PlayerMaster.new()
func _ready() -> void:
#Temporary! REMOVE!
get_multiplayer().allow_object_decoding = true
get_tree().connect("network_peer_connected", self, "_network_peer_connected")
get_tree().connect("network_peer_disconnected", self, "_network_peer_disconnected")
get_tree().connect("connected_to_server", self, "_connected_to_server")
get_tree().connect("connection_failed", self, "_connection_failed")
get_tree().connect("server_disconnected", self, "_server_disconnected")
func start_hosting(p_port : int = 0) -> int:
if p_port == 0:
p_port = port
var peer : NetworkedMultiplayerENet = NetworkedMultiplayerENet.new()
var err : int = peer.create_server(p_port, 32)
get_tree().set_network_peer(peer)
_connected_to_server()
return err
func start_hosting_websocket(p_port : int = 0) -> int:
if p_port == 0:
p_port = port
var peer : WebSocketServer = WebSocketServer.new()
var err : int = peer.listen(p_port, [], true)
get_tree().set_network_peer(peer)
_connected_to_server()
return err
func connect_to_server(address : String = "127.0.0.1", p_port : int = 0) -> int:
if p_port == 0:
p_port = port
var peer = NetworkedMultiplayerENet.new()
var err : int = peer.create_client(address, p_port)
get_tree().set_network_peer(peer)
return err
func connect_to_server_websocket(address : String = "127.0.0.1", p_port : int = 0) -> int:
if p_port == 0:
p_port = port
var peer = WebSocketClient.new()
var err : int = peer.connect_to_url(address + ":" + str(p_port), [], true)
get_tree().set_network_peer(peer)
return err
func _network_peer_connected(id : int) -> void:
# Logger.verbose("NetworkManager peer connected " + str(id))
# for p in splayers_array:
# rpc_id(id, "cspawn_player", p.my_info, p.sid, p.player.translation)
var pm : PlayerMaster = PlayerMaster.new()
pm.sid = id
splayers_array.append(pm)
splayers_dict[id] = pm
emit_signal("splayer_master_created", pm)
rpc_id(id, "cset_seed", _sseed)
func _network_peer_disconnected(id : int) -> void:
# Logger.verbose("NetworkManager peer disconnected " + str(id))
var player : PlayerMaster = splayers_dict[id]
splayers_dict.erase(id)
for pi in range(len(splayers_array)):
if (splayers_array[pi] as PlayerMaster) == player:
splayers_array.remove(pi)
break
if player:
emit_signal("splayer_master_destroyed", player)
func _connected_to_server() -> void:
# Logger.verbose("NetworkManager _connected_to_server")
var pm : PlayerMaster = PlayerMaster.new()
pm.sid = get_tree().get_network_unique_id()
local_player_master = pm
emit_signal("cplayer_master_created", pm)
func _server_disconnected() -> void:
# Logger.verbose("_server_disconnected")
# Server kicked us; show error and abort.
for player in get_children():
emit_signal("NetworkManager cplayer_master_destroyed", player)
player.queue_free()
func _connection_failed() -> void:
# Logger.verbose("NetworkManager _connection_failed")
pass # Could not even connect to server; abort.
func sset_seed(pseed):
_sseed = pseed
if multiplayer.has_network_peer() and multiplayer.is_network_server():
rpc("cset_seed", _sseed)
remote func cset_seed(pseed):
_cseed = pseed
print("clientseed set")
func set_class():
# Logger.verbose("set_class")
if not get_tree().is_network_server():
rpc_id(1, "crequest_select_class", local_player_master.my_info)
else:
crequest_select_class(local_player_master.my_info)
remote func crequest_select_class(info : Dictionary) -> void:
# Logger.verbose("NetworkManager crequest_select_class")
if get_tree().is_network_server():
var sid : int = get_tree().multiplayer.get_rpc_sender_id()
if sid == 0:
sid = 1
rpc("cspawn_player", info, sid, Vector3(10, 10, 10))
remotesync func cspawn_player(info : Dictionary, sid : int, pos : Vector3):
# Logger.verbose("NetworkManager cspawn_player")
if sid == get_tree().get_network_unique_id():
local_player_master.player = ESS.get_ess_entity_spawner().spawn_player(info["selected_class"] as int, pos, info["name"] as String, str(sid), sid)
call_deferred("set_terrarin_player")
if get_tree().is_network_server() and not splayers_dict.has(sid):
splayers_dict[sid] = local_player_master
splayers_array.append(local_player_master)
else:
var pm : PlayerMaster = PlayerMaster.new()
pm.sid = sid
pm.player = ESS.get_ess_entity_spawner().spawn_networked_player(info["selected_class"] as int, pos, info["name"] as String, str(sid), sid)
if get_tree().is_network_server() and not splayers_dict.has(sid):
splayers_dict[sid] = pm
splayers_array.append(pm)
cplayers_dict[sid] = pm
cplayers_array.append(pm)
func upload_character(data : String) -> void:
rpc_id(1, "sreceive_upload_character", data)
master func sreceive_upload_character(data: String) -> void:
ESS.get_ess_entity_spawner().spawn_networked_player_from_data(data, Vector3(0, 10, 0), multiplayer.get_rpc_sender_id())
func set_terrarin_player():
# Logger.verbose("NetworkManager cspawn_player")
var terrarin : Node = get_node("/root/GameScene/VoxelWorld")
if terrarin.has_method("set_player"):
terrarin.set_player(local_player_master.player.get_body())

View File

@ -1,6 +0,0 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://autoload/Server.gd" type="Script" id=1]
[node name="Server" type="Node"]
script = ExtResource( 1 )

View File

@ -24,99 +24,69 @@ class_name PlayerGD
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#func _initialize():
# print("dadad")
func _from_dict(dict):
._from_dict(dict)
randomize()
sseed = randi()
#PlayerGDBase
var _timer : float = randf() * 2.0
var _query : Physics2DShapeQueryParameters
var _shape : SphereShape
func _ready():
_shape = SphereShape.new()
_shape.radius = 50
_query = Physics2DShapeQueryParameters.new()
_query.exclude = [ self ]
_query.shape_rid = _shape.get_rid()
set_physics_process(true)
func _physics_process(delta):
# if (multiplayer.has_network_peer() and multiplayer.is_network_server()) or not multiplayer.has_network_peer():
if multiplayer.has_network_peer() and multiplayer.is_network_server():
_timer += delta
if _timer > 3:
_timer -= 3
update_visibility()
func update_visibility() -> void:
_query.collision_layer = get_body().get_collision_layer()
_query.transform = Transform2D(0, get_body().position)
var res : Array = get_body().get_world_2d().direct_space_state.intersect_shape(_query)
#warning-ignore:unassigned_variable
var currenty_sees : Array = Array()
for collision in res:
var collider = collision["collider"]
if collider is Entity and not currenty_sees.has(collider):
currenty_sees.append(collider)
#warning-ignore:unassigned_variable
var used_to_see : Array = Array()
for i in range(sees_gets_count()):
var ent : Entity = sees_gets(i)
used_to_see.append(ent)
#warning-ignore:unassigned_variable
var currenty_sees_filtered : Array = Array()
for e in currenty_sees:
currenty_sees_filtered.append(e)
for e in currenty_sees:
if used_to_see.has(e):
used_to_see.erase(e)
currenty_sees_filtered.erase(e)
for e in used_to_see:
var ent : Entity = e as Entity
if self.get_network_master() != 1:
ESS.entity_spawner.despawn_for(self, ent)
sees_removes(ent)
for e in currenty_sees_filtered:
var ent : Entity = e as Entity
if self.get_network_master() != 1:
ESS.entity_spawner.spawn_for(self, ent)
sees_adds(ent)
#The world will propbably need to set these later
#func update_visibility() -> void:
# _query.collision_layer = get_body().get_collision_layer()
#
# _query.transform = Transform2D(0, get_body().position)
# var res : Array = get_body().get_world_2d().direct_space_state.intersect_shape(_query)
#
# #warning-ignore:unassigned_variable
# var currenty_sees : Array = Array()
#
# for collision in res:
# var collider = collision["collider"]
#
# if collider is Entity and not currenty_sees.has(collider):
# currenty_sees.append(collider)
#
#
# #warning-ignore:unassigned_variable
# var used_to_see : Array = Array()
#
# for i in range(sees_gets_count()):
# var ent : Entity = sees_gets(i)
#
# used_to_see.append(ent)
#
#
# #warning-ignore:unassigned_variable
# var currenty_sees_filtered : Array = Array()
#
# for e in currenty_sees:
# currenty_sees_filtered.append(e)
#
# for e in currenty_sees:
# if used_to_see.has(e):
# used_to_see.erase(e)
# currenty_sees_filtered.erase(e)
#
# for e in used_to_see:
# var ent : Entity = e as Entity
#
# if self.get_network_master() != 1:
# ESS.entity_spawner.despawn_for(self, ent)
#
# sees_removes(ent)
#
# for e in currenty_sees_filtered:
# var ent : Entity = e as Entity
#
# if self.get_network_master() != 1:
# ESS.entity_spawner.spawn_for(self, ent)
#
# sees_adds(ent)
remote func set_position_remote(pos : Vector2) -> void:
if get_tree().is_network_server():
rpc("set_position_remote", pos)
#print(position)
get_body().position = pos
#remote func set_position_remote(pos : Vector2) -> void:
# if get_tree().is_network_server():
# rpc("set_position_remote", pos)
# #print(position)
# get_body().position = pos

View File

@ -23,87 +23,63 @@ extends Entity
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
var _timer : float = randf() * 2.0
var _query : Physics2DShapeQueryParameters
var _shape : SphereShape
func _ready():
_shape = SphereShape.new()
_shape.radius = 50
_query = Physics2DShapeQueryParameters.new()
_query.exclude = [ self ]
_query.shape_rid = _shape.get_rid()
set_physics_process(true)
func _physics_process(delta):
# if (multiplayer.has_network_peer() and multiplayer.is_network_server()) or not multiplayer.has_network_peer():
if multiplayer.has_network_peer() and multiplayer.is_network_server():
_timer += delta
if _timer > 3:
_timer -= 3
update_visibility()
func update_visibility() -> void:
_query.collision_layer = get_body().get_collision_layer()
_query.transform = Transform2D(0, get_body().position)
var res : Array = get_body().get_world_2d().direct_space_state.intersect_shape(_query)
#warning-ignore:unassigned_variable
var currenty_sees : Array = Array()
for collision in res:
var collider = collision["collider"]
if collider is Entity and not currenty_sees.has(collider):
currenty_sees.append(collider)
#warning-ignore:unassigned_variable
var used_to_see : Array = Array()
for i in range(sees_gets_count()):
var ent : Entity = sees_gets(i)
used_to_see.append(ent)
#warning-ignore:unassigned_variable
var currenty_sees_filtered : Array = Array()
for e in currenty_sees:
currenty_sees_filtered.append(e)
for e in currenty_sees:
if used_to_see.has(e):
used_to_see.erase(e)
currenty_sees_filtered.erase(e)
for e in used_to_see:
var ent : Entity = e as Entity
if self.get_network_master() != 1:
ESS.entity_spawner.despawn_for(self, ent)
sees_removes(ent)
for e in currenty_sees_filtered:
var ent : Entity = e as Entity
if self.get_network_master() != 1:
ESS.entity_spawner.spawn_for(self, ent)
sees_adds(ent)
#The world will need to set these
#func update_visibility() -> void:
# _query.collision_layer = get_body().get_collision_layer()
#
# _query.transform = Transform2D(0, get_body().position)
# var res : Array = get_body().get_world_2d().direct_space_state.intersect_shape(_query)
#
# #warning-ignore:unassigned_variable
# var currenty_sees : Array = Array()
#
# for collision in res:
# var collider = collision["collider"]
#
# if collider is Entity and not currenty_sees.has(collider):
# currenty_sees.append(collider)
#
#
# #warning-ignore:unassigned_variable
# var used_to_see : Array = Array()
#
# for i in range(sees_gets_count()):
# var ent : Entity = sees_gets(i)
#
# used_to_see.append(ent)
#
#
# #warning-ignore:unassigned_variable
# var currenty_sees_filtered : Array = Array()
#
# for e in currenty_sees:
# currenty_sees_filtered.append(e)
#
# for e in currenty_sees:
# if used_to_see.has(e):
# used_to_see.erase(e)
# currenty_sees_filtered.erase(e)
#
# for e in used_to_see:
# var ent : Entity = e as Entity
#
# if self.get_network_master() != 1:
# ESS.entity_spawner.despawn_for(self, ent)
#
# sees_removes(ent)
#
# for e in currenty_sees_filtered:
# var ent : Entity = e as Entity
#
# if self.get_network_master() != 1:
# ESS.entity_spawner.spawn_for(self, ent)
#
# sees_adds(ent)
remote func set_position_remote(pos : Vector2) -> void:
if get_tree().is_network_server():
rpc("set_position_remote", pos)
#print(position)
get_body().position = pos
#remote func set_position_remote(pos : Vector2) -> void:
# if get_tree().is_network_server():
# rpc("set_position_remote", pos)
# #print(position)
# get_body().position = pos

View File

@ -173,7 +173,6 @@ config/version="0.2"
[autoload]
Server="*res://autoload/Server.tscn"
ThemeAtlas="*res://ui/autoload/ThemeAtlas.tscn"
WorldNumbers="*res://ui/world_numbers_2d/WorldNumbers.tscn"
CursorManager="*res://cursors/autoload/CursorManager.tscn"

View File

@ -192,20 +192,6 @@ func load_character() -> void:
if b == null:
return
# if multiplayer.has_network_peer():
# var file_name : String = "user://" + character_folder + "/" + b.file_name
#
# var f : File = File.new()
#
# if f.open(file_name, File.READ) == OK:
# var data : String = f.get_as_text()
#
# f.close()
#
# Server.upload_character(data)
#
# get_node("/root/Main").load_character(b.file_name)
# else:
get_node("/root/Main").load_character(b.file_name)
func visibility_changed() -> void:

View File

@ -139,29 +139,7 @@ func switch_scene(scene : int) -> void:
current_scene = gs
if multiplayer.has_network_peer():# and get_tree().network_peer.get_connection_status() == NetworkedMultiplayerPeer.CONNECTION_CONNECTED:
if multiplayer.is_network_server():
gs.load_character(current_character_file_name)
else:
# var dc = debug_camera_scene.instance()
#
# gs.add_child(dc)
# dc.owner = gs
#
gs.setup_client_seed(Server._cseed)
var file_name : String = "user://characters/" + current_character_file_name
var f : File = File.new()
if f.open(file_name, File.READ) == OK:
var data : String = f.get_as_text()
f.close()
Server.upload_character(data)
else:
gs.load_character(current_character_file_name)
gs.load_character(current_character_file_name)
if current_scene.has_method("needs_loading_screen"):
if current_scene.needs_loading_screen():

View File

@ -67,7 +67,7 @@ func load_character(file_name: String) -> void:
var player_y = start_room.position.y + 1 + randi() % int(start_room.size.y - 2)
var pos : Vector3 = Vector3(player_x * tile_size + tile_size / 2, player_y * tile_size + tile_size / 2, 0)
_player = ESS.entity_spawner.load_player(_player_file_name, pos, 1) as Entity
Server.sset_seed(_player.sseed)
#Server.sset_seed(_player.sseed)
#Place enemies
for i in range(enemy_count):

View File

@ -1,134 +0,0 @@
extends Spatial
# Copyright (c) 2019-2020 Péter Magyar
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
export (bool) var use_gui : bool = false
export (bool) var multi_player : bool = false
export (NodePath) var gui_path : NodePath
export (NodePath) var host_button_path : NodePath
export (NodePath) var address_line_edit_path : NodePath
export (NodePath) var port_line_edit_path : NodePath
export (NodePath) var connect_button_path : NodePath
export (NodePath) var naturalist_button_path : NodePath
export (NodePath) var terrarin_path : NodePath
var gui : Node
var host_button : Button
var address_line_edit : LineEdit
var port_line_edit : LineEdit
var connect_button : Button
var naturalist_button : Button
var player : Entity
var terrarin : VoxelWorld
var spawned : bool = false
var player_master : PlayerMaster
func _ready():
gui = get_node(gui_path)
host_button = get_node(host_button_path)
host_button.connect("pressed", self, "_on_host_button_clicked")
address_line_edit = get_node(address_line_edit_path)
port_line_edit = get_node(port_line_edit_path)
connect_button = get_node(connect_button_path)
connect_button.connect("pressed", self, "_on_client_button_clicked")
naturalist_button = get_node(naturalist_button_path)
naturalist_button.connect("pressed", self, "_on_client_naturalist_button_clicked")
terrarin = get_node(terrarin_path) as VoxelWorld
Server.connect("cplayer_master_created", self, "_cplayer_master_created")
if not multi_player:
set_process(true)
else:
set_process(false)
if use_gui:
gui.visible = true
func _process(delta):
set_process(false)
spawn()
func spawn():
if not spawned:
spawned = true
if get_tree().network_peer == null:
player = ESS.entity_spawner.spawn_player(1, Vector3(10, 20, 10), "Player", "1", 1)
call_deferred("set_terrarin_player")
#
# ESS.entity_spawner.spawn_mob(1, 50, Vector3(20, 6, 20))
# ESS.entity_spawner.spawn_mob(1, 50, Vector3(54, 6, 22))
# ESS.entity_spawner.spawn_mob(1, 50, Vector3(76, 6, 54))
func set_terrarin_player():
terrarin.set_player(player.get_body() as Spatial)
func _on_host_button_clicked():
get_tree().connect("network_peer_connected", self, "_network_peer_connected")
get_tree().connect("network_peer_disconnected", self, "_network_peer_disconnected")
Server.start_hosting()
spawn()
func _on_client_button_clicked():
var addr : String = "127.0.0.1" if address_line_edit.text == "" else address_line_edit.text
var port : int = 0 if port_line_edit.text == "" else int(port_line_edit.text)
Server.connect_to_server(addr, port)
func _network_peer_connected(id : int):
print(id)
func _network_peer_disconnected(id : int):
print(id)
func _cplayer_master_created(pplayer_master):
player_master = pplayer_master as PlayerMaster
func _on_client_naturalist_button_clicked():
#Network.is
#set_class
gui.visible = false
if get_tree().network_peer != null:
Server.set_class()
else:
spawn()

View File

@ -1,102 +0,0 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://ui/theme/ui_theme.tres" type="Theme" id=1]
[ext_resource path="res://scripts/networking/SpawnPoint.gd" type="Script" id=2]
[node name="SpawnPoint" type="Spatial"]
script = ExtResource( 2 )
multi_player = true
gui_path = NodePath("PanelContainer")
host_button_path = NodePath("PanelContainer/VBoxContainer/host")
address_line_edit_path = NodePath("PanelContainer/VBoxContainer/VBoxContainer/address")
port_line_edit_path = NodePath("PanelContainer/VBoxContainer/VBoxContainer/port")
connect_button_path = NodePath("PanelContainer/VBoxContainer/VBoxContainer/connect")
naturalist_button_path = NodePath("PanelContainer/VBoxContainer/select naturalist")
terrarin_path = NodePath("..")
[node name="PanelContainer" type="PanelContainer" parent="."]
visible = false
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
margin_left = -126.0
margin_top = -169.5
margin_right = 126.0
margin_bottom = 169.5
theme = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer"]
margin_left = 4.0
margin_top = 4.0
margin_right = 248.0
margin_bottom = 335.0
size_flags_horizontal = 3
size_flags_vertical = 3
custom_constants/separation = 35
[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/VBoxContainer"]
margin_right = 244.0
margin_bottom = 140.0
size_flags_horizontal = 3
size_flags_vertical = 3
size_flags_stretch_ratio = 4.0
[node name="Label" type="Label" parent="PanelContainer/VBoxContainer/VBoxContainer"]
margin_right = 244.0
margin_bottom = 15.0
text = "Ip:"
[node name="address" type="LineEdit" parent="PanelContainer/VBoxContainer/VBoxContainer"]
margin_top = 23.0
margin_right = 244.0
margin_bottom = 47.3413
placeholder_text = "127.0.0.1"
[node name="Label2" type="Label" parent="PanelContainer/VBoxContainer/VBoxContainer"]
margin_top = 55.0
margin_right = 244.0
margin_bottom = 70.0
text = "Port:"
[node name="port" type="LineEdit" parent="PanelContainer/VBoxContainer/VBoxContainer"]
margin_top = 78.0
margin_right = 244.0
margin_bottom = 102.341
placeholder_text = "23223"
[node name="connect" type="Button" parent="PanelContainer/VBoxContainer/VBoxContainer"]
margin_top = 110.0
margin_right = 244.0
margin_bottom = 140.0
size_flags_horizontal = 3
size_flags_vertical = 3
text = "Connect"
[node name="host" type="Button" parent="PanelContainer/VBoxContainer"]
margin_top = 175.0
margin_right = 244.0
margin_bottom = 210.0
size_flags_horizontal = 3
size_flags_vertical = 3
text = "Host"
[node name="Label" type="Label" parent="PanelContainer/VBoxContainer"]
margin_top = 245.0
margin_right = 244.0
margin_bottom = 260.0
size_flags_vertical = 1
text = "Class: (Just select for offline play):"
[node name="select naturalist" type="Button" parent="PanelContainer/VBoxContainer"]
margin_top = 295.0
margin_right = 244.0
margin_bottom = 331.0
size_flags_horizontal = 3
size_flags_vertical = 3
text = "Naturalist"