extends Node enum GameFocus {MENU, GAME, CHAT, AWAY} enum MultiplayerRole {NONE, CLIENT, SERVER, DEDICATED_SERVER, RELAY_SERVER} const NET_PORT = 12597 var server_message : String var version : String #const NET_SERVER = "liblast.unfa.xyz" const NET_SERVER = "localhost" var peer = ENetMultiplayerPeer.new() var role = MultiplayerRole.NONE: set(new_role): role = new_role # print("Multiplayer Role changed to ", MultiplayerRole.keys()[new_role]) var player_scene = preload("res://Assets/Characters/Player.tscn") @onready var gui = $GUI @onready var hud = $HUD @onready var chat = hud.get_node("Chat") var local_player: Node = null var local_player_focus_previous: GameFocus # to store focus that should be set after AWAY is gone var mute_previous: bool class PlayerInfo: var name: String var team: int var color: Color var focus: GameFocus var health: int var score: int var ping: int var packet_loss: int func _init():#name: String, team: int, color: Color): # var player_name = "" # for i in range(0, 4): # player_name += ['a','b','c', 'd', 'e', 'f'][randi() % 5] self.name = ""#player_name self.color = Color.WHITE #Color(randf(),randf(),randf()) self.team = 0 self.focus = 999 self.health = 100 self.score = 0 self.ping = -1 self.packet_loss = -1 func serialize(): return { 'name': self.name, 'team': str(self.team), 'color': self.color.to_html(), 'focus': self.focus, 'health': self.health, 'score': self.score, 'ping': self.ping, 'loss': self.packet_loss, } func set(name: String, team: int, color: Color, focus: int, health: int, score: int, ping: int, packet_loss: int): self.name = name self.team = team self.color = color self.focus = focus self.health = health self.score = score self.ping = ping self.packet_loss = packet_loss func deserialize(info): self.name = info['name'] self.team = info['team'].to_int() self.color = Color.html(info['color']) self.focus = info['focus'] self.health = info['health'] self.score = info['score'] self.ping = info['ping'] self.packet_loss = info['loss'] #func generate(): var uptime = 0 # seconds const respawn_delay = 3 # seconds const reset_delay = 10 # seconds var spawn_queue = {} var reset_at = -1 var game_score_limit = 10 #15 const destroy_free_player_crash_workaround = true func get_mute() -> bool: return AudioServer.is_bus_mute(0) func set_mute(mute) -> void: if mute == null: # toggle AudioServer.set_bus_mute(0, not get_mute()) else: AudioServer.set_bus_mute(0, mute) # update the HUD icon hud.get_node("MuteIcon").visible = get_mute() func _notification(what: int) -> void: match what: NOTIFICATION_APPLICATION_FOCUS_OUT: Engine.target_fps = 5 mute_previous = get_mute() set_mute(true) if local_player: local_player_focus_previous = focus focus = GameFocus.AWAY NOTIFICATION_APPLICATION_FOCUS_IN: # `0` means "unlimited". Engine.target_fps = 0 set_mute(mute_previous) if local_player: focus = local_player_focus_previous func _process(delta): uptime += delta #print($GUI.settings) $Label.text = "player_list: \n" for i in player_list.players.keys(): if player_list.players[i]: $Label.text += str(i) + " = " + str(player_list.get(i).serialize()) + "\n" else: $Label.text += str(i) + " = ???" # poll respawn queue $Label.text += "\n\nspawn_queue: \n" $Label.text += str(spawn_queue) if local_player: $Label.text += "\n\nLOCAL PLAYER DEAD: " + str(local_player.dead) $Label.text += "\n\nWEAPON SPREAD: " + str(local_player.weapon.spread) #TODO use this to alter crosshair $Label.text += "\n\nGAME RESET AT: " + str(reset_at) # respawn queue for i in spawn_queue.keys(): if spawn_queue[i] <= uptime: var is_local = true if i == get_tree().multiplayer.get_unique_id() else false create_player(i, is_local, true) spawn_queue.erase(i) if local_player: # 3 seconds left to new round if 0 < reset_at and (uptime + 3.5) >= reset_at and (uptime + 3) < reset_at and not $Announcer.is_playing(): $Announcer.speak($Announcer.getready) # starting new round if 0 < reset_at and uptime >= reset_at: hud.scoretab(false) local_player.dead = false reset_at = -1 $Announcer.speak($Announcer.go) for i in player_list.players.keys(): player_list.players[i].score = 0 class PlayerList: var players = {} func erase(pid): players.erase(pid) func set(pid: int, info: PlayerInfo): # if info is PlayerInfo: players[pid] = info func get(pid: int) -> PlayerInfo: if players.has(pid): return players[pid] else: return PlayerInfo.new() @onready var player_list = PlayerList.new() var focus:GameFocus: set(new_focus): # print("Focus set to ", new_focus) if local_player != null: assert(local_player != null, "local_player is not null but it is null, WTF?!") match new_focus: 0: # MENU Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) gui.show() hud.hide() if local_player: local_player.input_active = false local_player.rpc(&"focus_banner", true, 0) 1: # GAME Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) gui.hide() hud.show() if local_player: local_player.input_active = true local_player.rpc(&"focus_banner", false) 2: # CHAT if local_player: local_player.input_active = false local_player.rpc(&"focus_banner", true, 1) 3: #AWAY if local_player: local_player.input_active = true local_player.rpc(&"focus_banner", true, 2) focus = new_focus func _input(_event) -> void: if Input.is_action_just_pressed("ui_cancel"): if focus == GameFocus.GAME: focus = GameFocus.MENU #get_tree().root.set_input_as_handled() elif focus == GameFocus.MENU: focus = GameFocus.GAME #get_tree().root.set_input_as_handled() if Input.is_action_just_pressed("show_scoretab"): hud.scoretab(true) elif Input.is_action_just_released("show_scoretab"): hud.scoretab(false) if Input.is_action_just_pressed("screenshot"): var res = get_viewport().get_texture().get_image().save_exr('res://Screnshot.exr') chat.chat_notification("Screenshot taken: " + str(res)) if Input.is_action_just_pressed("mute_audio"): set_mute(null) @rpc(any_peer, call_local, reliable) func game_over(winner): if local_player: local_player.die(-1) hud.game_over(winner) spawn_queue.clear() for j in player_list.players.keys(): spawn_queue[j] = uptime + reset_delay reset_at = uptime + reset_delay if local_player: if winner == local_player.get_multiplayer_authority(): $Announcer.speak($Announcer.victory) elif player_list.get(local_player.get_multiplayer_authority()).score == 0: $Announcer.speak($Announcer.defeat2) # embarrasing defeat else: $Announcer.speak($Announcer.defeat) func check_game_win_condition(): for i in player_list.players.keys(): if player_list.players[i].score >= game_score_limit: chat.rpc(&'chat_notification', "Player [color=" + player_list.players[i].color.to_html() + "]" + player_list.players[i].name + "[/color] has won this round!") rpc(&'game_over', i) break func update_hud(): ### Health hud.get_node("Stats").get_node("HealthBar").value = player_list.get( get_tree().multiplayer.get_unique_id() ).health if player_list.players.size() <= 1: return # if we're the sole player in the server, stop here ### SCORE, RANK, GAP/LEAD hud.get_node("ScoreRank").text = "SCORE: " + str(player_list.get( get_tree().multiplayer.get_unique_id() ).score) var score = player_list.get( get_tree().multiplayer.get_unique_id() ).score var scores = [] for i in player_list.players.values(): scores.append(i.score) scores.sort() scores.reverse() var rank = scores.find(score) + 1 scores.remove_at(scores.find(score)) scores.sort() scores.reverse() var lead = score - scores[0] hud.get_node("ScoreRank").text = "SCORE: " + str(score) + "\nRANK: " if lead > 0: hud.get_node("ScoreRank").text += str(rank) + "\nLEAD: " + str(lead) else: hud.get_node("ScoreRank").text += str(rank) + "\nGAP: " + str(-lead) @rpc(any_peer, reliable) func player_list_update(info, pid = get_tree().get_rpc_sender_id(), erase:=false): if erase: player_list.erase(pid) spawn_queue.erase(pid) get_node("Players").get_node(str(pid)).die(-1) return var new_info = PlayerInfo.new() new_info.deserialize(info) if player_list.players.has(pid): var old_name = player_list.get(pid).name if old_name != new_info.name: chat.chat_notification("Player [b]" + old_name + "[/b] changed name to [b]" + new_info.name + "[/b]") else: create_player(pid, false) if $Players.has_node(str(pid)): var player = $Players.get_node(str(pid)) player.update_player(new_info) player_list.set(pid, new_info) # server relays other PID's data # update local HUD if local_player: update_hud() #check_game_win_condition() func push_local_player_info(): # var pid = get_tree().multiplayer.get_unique_id() assert(pid >= 1, "Another server must be running. PID is 0, that means Enet did not initialize.") rpc(&'player_list_update', player_list.get(pid).serialize(), pid) @rpc(call_local, any_peer, reliable) func destroy_player(pid: int): print("DESTROY_PLAYER called on ", get_tree().multiplayer.get_unique_id(), " by ", get_tree().multiplayer.get_remote_sender_id(), " regarding ", pid) #assert($Players.has_node(str(pid)), "Attempting to destroy a player that does not exist") var player_node if $Players.has_node(str(pid)): player_node = $Players.get_node(str(pid)) else: print("Destroying a player node that's not there") return #assert(player_node != null, "Attempting to delete a player node that does not exist") # alternative finale to this function that is a workaround to avoid Godot crashing due to an engine bug # https://github.com/godotengine/godot/issues/52853 if destroy_free_player_crash_workaround: pass #print("Setting player ", pid, " as DEAD") if not player_node.dead: # disconnected, not killed player_node.die(-1) return # skip the rest - don't add to respawn queue else: # regular method follows player_node.name = str(player_node.name) + "_dead" # avoids name collision when instancing another player scene print("before free") player_node.queue_free() print("after free") # respawn queue applies ot both implementations of death if player_list.players.has(pid): # don't respawn players that are not there, lol spawn_queue[pid] = uptime + respawn_delay if pid == get_tree().multiplayer.get_unique_id(): update_hud() func create_player(pid: int, is_local:= false, respawn:= false) -> void: if destroy_free_player_crash_workaround: var player_node if $Players.has_node(str(pid)): # if this is a respawn player_node = $Players.get_node(str(pid)) #print("Setting player ", pid, " as ALIVE") #player_node.set_dead(false) var spawnpoint = $Map/SpawnPoints.get_children()[randi() % len($Map/SpawnPoints.get_children())] player_node.spawn(spawnpoint.global_transform) # player_node.global_transform = spawnpoint.global_transform # player_list.players[pid].health = 100 # if local_player: # push_local_player_info() #focus = GameFocus.GAME # make sure the player has no menu displayed when they respawn return # avoid running the rest of this function else: pass # if this is not a respawn, let it carry on var new_player new_player = player_scene.instantiate() var spawnpoint = $Map/SpawnPoints.get_children()[randi() % len($Map/SpawnPoints.get_children())] new_player.name = str(pid) new_player.global_transform = spawnpoint.global_transform new_player.set_multiplayer_authority(pid) $Players.add_child(new_player) if not respawn and is_local: var new_info = PlayerInfo.new() new_info.name = $GUI.settings["username"] new_info.color = $GUI.settings["player color"] player_list.set(pid, new_info) push_local_player_info() if is_local: local_player = new_player # if not respawn: # first spawn # new_info = PlayerInfo.new() # if is_local: # new_info.name = $GUI.settings["username"] # new_info.color = $GUI.settings["player color"] # else: # respawn # new_info = player_list.players[pid] # reuse previous name, color etc # new_info.health = 100 # give the respawned player full health # focus = GameFocus.GAME # make sure the player has no menu displayed when they respawn # superfluous and harmful code that messes up player data: # if get_tree().multiplayer.get_multiplayer_unique_id() != 1: # if we're not the server - update the server # rpc_id(1, &'player_list_update', new_info.serialize(), pid) # send local player info to the server # else: # if we're the server, update the clients # rpc(&'player_list_update', new_info.serialize(), pid) # if is_local: # local_player = new_player # if not get_tree().multiplayer.is_server(): # push_local_player_info() func start_dedicated_server(): # start server without creating a local player role = MultiplayerRole.DEDICATED_SERVER peer.create_server(NET_PORT, 16) get_tree().multiplayer.multiplayer_peer = peer get_viewport().debug_draw = Viewport.DebugDraw.DEBUG_DRAW_OVERDRAW get_viewport().msaa = Viewport.MSAA.MSAA_DISABLED get_viewport().use_debanding = false get_viewport().screen_space_aa = 0 get_viewport().lod_threshold = 128 get_viewport().get_camera_3d().effects = null get_viewport().shadow_atlas_size = 64 $Map/Decals.hide() $Map/Lights.hide() $Map/ReflectionProbes.hide() AudioServer.set_bus_mute(0, true) # mute sound $Label.show() #get_tree().root.title = "LIBLAST DEDICATED SERVER" Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) gui.hide() hud.show() hud.get_node("ScoreRank").hide() hud.get_node("Stats").hide() hud.get_node("Crosshair").hide() hud.scoretab(true) func host_server(): _on_Host_pressed() func commit_suicide(): local_player.rpc(&"die", local_player.get_multiplayer_authority()) focus = GameFocus.GAME func connect_to_server(ip): $NetworkTesting/Host.disabled = true $NetworkTesting/Connect.disabled = true peer.create_client(ip, NET_PORT) get_tree().multiplayer.multiplayer_peer = peer func _on_Host_pressed(): # start server and create a local player role = MultiplayerRole.SERVER $NetworkTesting/Host.disabled = true $NetworkTesting/Connect.disabled = true peer.create_server(NET_PORT, 16) get_tree().multiplayer.multiplayer_peer = peer create_player(1, true) focus = GameFocus.GAME chat.chat_notification("Started server") func _on_Connect_pressed(): $NetworkTesting/Host.disabled = true $NetworkTesting/Connect.disabled = true peer.create_client(NET_SERVER, NET_PORT) get_tree().multiplayer.multiplayer_peer = peer func _player_connected(pid) -> void: print("peer connected, id: ", pid) if get_tree().multiplayer.is_server(): # if we're the server for i in player_list.players.keys(): # send the player_list to the new client #pass rpc_id(pid, &'player_list_update', player_list.get(i).serialize(), i) # send local player info to the server chat.rpc_id(pid, &'chat_notification', server_message) # if local_player: # local_player.rpc(&"update_info") # # if role in [MultiplayerRole.SERVER, MultiplayerRole.DEDICATED_SERVER]: # rpc_id(id, "player_list_update", player_list) @rpc(call_remote, any_peer, reliable) func disconnect_player(pid): print("player disconnected, id: ", pid) spawn_queue.erase(pid) if get_tree().multiplayer.is_server(): chat.rpc(&'chat_notification', "Player [b]" + player_list.get(pid).name + "[/b] left") player_list.erase(pid) rpc(&'player_list_update', null, pid, true) rpc(&'destroy_player', pid) func _player_disconnected(pid) -> void: disconnect_player(pid) func _connected_ok() -> void: print("connected to server") chat.chat_notification("Connected to server") var pid = get_tree().multiplayer.get_unique_id() create_player(pid, true) focus = GameFocus.GAME role = MultiplayerRole.CLIENT func _connected_fail() -> void: print("connection to server failed") chat.chat_notification("Connection failed") func _server_disconnected() -> void: print("server disconnected") role = MultiplayerRole.NONE chat.chat_notification("Server disconnected") func _ready() -> void: hud.hide() # start in the menu # focus = GameFocus.MENU focus = GameFocus.GAME local_player_focus_previous = focus get_tree().multiplayer.connect("peer_connected", self._player_connected) get_tree().multiplayer.connect("peer_disconnected", self._player_disconnected) get_tree().multiplayer.connect("connected_to_server", self._connected_ok) get_tree().multiplayer.connect("connection_failed", self._connected_fail) get_tree().multiplayer.connect("server_disconnected", self._server_disconnected) # update game build version, if we're running in the editor if Engine.get_version_info(): OS.execute('bash', ["./version.sh"]) var file = File.new() file.open("res://version", File.READ) version = file.get_as_text() file.close() var out = [] OS.execute('hostname', [], out) var ev = Engine.get_version_info() server_message = "Welcome to Liblast server on " + out[0] + "game version: " + version + "\nengine version: " + ev['string'] + " [" + ev['hash'].left(10) + "]" $Announcer.speak($Announcer.welcome) if OS.get_cmdline_args().has("--dedicated-server"): start_dedicated_server() print(server_message) func change_player_color(color): if get_tree().multiplayer.has_multiplayer_peer(): player_list.players[get_tree().multiplayer.get_unique_id()].color = color push_local_player_info() func change_player_name(player_name): if get_tree().multiplayer.has_multiplayer_peer(): player_list.players[get_tree().multiplayer.get_unique_id()].name = player_name push_local_player_info() func _on_Disconnect_pressed(): get_tree().multiplayer.multiplayer_peer = null