From eb1ba01812a53e7fa13793e0d24745844ce10efd Mon Sep 17 00:00:00 2001 From: leca Date: Tue, 5 Mar 2024 18:58:11 +0300 Subject: [PATCH] Add hud functionality, decomposition of Networking.gd --- project.godot | 2 + scenes/HUD/hud.tscn | 86 ++++++-- scenes/models/pistol.tscn | 2 +- scenes/models/player.tscn | 9 +- scripts/Networking.gd | 330 ++++++----------------------- scripts/Player.gd | 83 ++++++-- scripts/Select.gd | 1 - scripts/utils/NetUtils.gd | 40 ++++ scripts/utils/ServerUtils.gd | 214 +++++++++++++++++++ textures/knife_textures.png.import | 35 +++ textures/materials/black.material | Bin 1087 -> 1088 bytes textures/materials/black90%.res | Bin 1029 -> 1028 bytes 12 files changed, 503 insertions(+), 299 deletions(-) create mode 100644 scripts/utils/NetUtils.gd create mode 100644 scripts/utils/ServerUtils.gd create mode 100644 textures/knife_textures.png.import diff --git a/project.godot b/project.godot index fc5be74..0155a8c 100644 --- a/project.godot +++ b/project.godot @@ -21,6 +21,8 @@ config/icon="res://icon.svg" Networking="*res://scripts/Networking.gd" GameData="*res://scripts/GameData.gd" Anticheat="*res://scripts/Anticheat.gd" +ServerUtils="*res://scripts/utils/ServerUtils.gd" +NetUtils="*res://scripts/utils/NetUtils.gd" [input] diff --git a/scenes/HUD/hud.tscn b/scenes/HUD/hud.tscn index a7e76c2..2221134 100644 --- a/scenes/HUD/hud.tscn +++ b/scenes/HUD/hud.tscn @@ -1,6 +1,6 @@ -[gd_scene load_steps=15 format=3 uid="uid://gxfhitfre2fj"] +[gd_scene load_steps=17 format=3 uid="uid://gxfhitfre2fj"] -[ext_resource type="Texture2D" uid="uid://vnk3r1p4ao3n" path="res://textures/prototype-textures/Prototype_symbol_cross_32x32px.png" id="1_g1v70"] +[ext_resource type="Texture2D" uid="uid://c4pah1vj0aa4x" path="res://textures/prototype-textures/Prototype_symbol_cross_32x32px.png" id="1_g1v70"] [ext_resource type="Texture2D" uid="uid://na1y6c7osyj2" path="res://textures/logo/health.svg" id="1_ts1uf"] [ext_resource type="Texture2D" uid="uid://cxpljra1na1p8" path="res://textures/logo/shield.svg" id="2_ijndi"] [ext_resource type="Texture2D" uid="uid://d1i1fgjbpwped" path="res://textures/logo/magazine.svg" id="3_ssnuf"] @@ -34,6 +34,15 @@ border_width_right = 1 border_width_bottom = 1 border_blend = true +[sub_resource type="LabelSettings" id="LabelSettings_l41k6"] +font_size = 30 + +[sub_resource type="LabelSettings" id="LabelSettings_ky32e"] +font_size = 64 +font_color = Color(0.992157, 0.415686, 0.631373, 1) +shadow_size = 9 +shadow_color = Color(0, 0, 0, 0.772549) + [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ouyds"] bg_color = Color(0.117647, 0.117647, 0.117647, 0.368627) border_width_left = 1 @@ -287,29 +296,70 @@ offset_right = 20.0 offset_bottom = 30.0 grow_horizontal = 2 -[node name="cs_won" type="Label" parent="round_status"] +[node name="bg" type="Panel" parent="round_status"] layout_mode = 0 -offset_top = 3.0 -offset_right = 40.0 -offset_bottom = 26.0 - -[node name="Panel" type="Panel" parent="round_status"] -layout_mode = 0 -offset_right = 40.0 -offset_bottom = 30.0 +offset_left = -66.0 +offset_right = 106.0 +offset_bottom = 66.0 theme_override_styles/panel = SubResource("StyleBoxFlat_pxi8e") -[node name="os_won" type="Label" parent="round_status"] +[node name="cs_score_game" type="Label" parent="round_status"] +offset_left = -52.0 +offset_top = -2.0 +offset_right = -35.0 +offset_bottom = 37.0 +text = "0" +label_settings = SubResource("LabelSettings_l41k6") + +[node name="cs_score_round" type="Label" parent="round_status"] +offset_left = -48.0 +offset_top = 39.0 +offset_right = -38.0 +offset_bottom = 62.0 +text = "0" + +[node name="os_score_round" type="Label" parent="round_status"] +offset_left = 83.0 +offset_top = 39.0 +offset_right = 93.0 +offset_bottom = 62.0 +text = "0" + +[node name="os_score_game" type="Label" parent="round_status"] +offset_left = 79.0 +offset_top = -2.0 +offset_right = 96.0 +offset_bottom = 40.0 +text = "0" +label_settings = SubResource("LabelSettings_l41k6") + +[node name="round_number" type="Label" parent="round_status"] layout_mode = 0 -offset_top = 3.0 -offset_right = 40.0 -offset_bottom = 26.0 +offset_left = -13.0 +offset_top = 5.0 +offset_right = 53.0 +offset_bottom = 28.0 +text = "Round N" [node name="time" type="Label" parent="round_status"] layout_mode = 0 -offset_top = 3.0 -offset_right = 40.0 -offset_bottom = 26.0 +offset_left = -5.0 +offset_top = 39.0 +offset_right = 46.0 +offset_bottom = 62.0 +text = "mm:ss" + +[node name="winner" type="Label" parent="round_status"] +visible = false +layout_mode = 0 +offset_left = -104.0 +offset_top = 46.0 +offset_right = 159.0 +offset_bottom = 135.0 +text = "CS WON" +label_settings = SubResource("LabelSettings_ky32e") +horizontal_alignment = 1 +vertical_alignment = 1 [node name="kill_log" type="Label" parent="."] layout_mode = 0 diff --git a/scenes/models/pistol.tscn b/scenes/models/pistol.tscn index 15d694d..d0800ef 100644 --- a/scenes/models/pistol.tscn +++ b/scenes/models/pistol.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://cnu1vf8k5i3tv"] -[ext_resource type="PackedScene" uid="uid://hf47u08p80fo" path="res://scenes/models/pistoletto.dae" id="1_hdcb6"] +[ext_resource type="PackedScene" uid="uid://hf47u08p80fo" path="res://models/pistoletto.dae" id="1_hdcb6"] [node name="pistol" type="Node3D"] diff --git a/scenes/models/player.tscn b/scenes/models/player.tscn index 236d367..2ba9d82 100644 --- a/scenes/models/player.tscn +++ b/scenes/models/player.tscn @@ -3,17 +3,16 @@ [ext_resource type="Script" path="res://scripts/Player.gd" id="1_o6o4b"] [ext_resource type="ArrayMesh" uid="uid://xh7mm0ldn6yw" path="res://models/knife.obj" id="2_7hcoq"] [ext_resource type="PackedScene" uid="uid://cnu1vf8k5i3tv" path="res://scenes/models/pistol.tscn" id="2_nxbij"] -[ext_resource type="Texture2D" path="res://knife_texturesv3.png" id="3_8p34k"] +[ext_resource type="Texture2D" uid="uid://cbf6w7ativli8" path="res://textures/knife_textures.png" id="2_y1ydh"] [ext_resource type="PackedScene" uid="uid://caos4gg5cd6f6" path="res://scenes/models/ak_47.tscn" id="3_r56e3"] -[ext_resource type="Material" uid="uid://btj7xxav4d6l0" path="res://textures/materials/orange.res" id="6_md2fv"] -[ext_resource type="Material" uid="uid://ojceh78w7jp0" path="res://textures/materials/black90%.res" id="7_2k5kn"] +[ext_resource type="Material" path="res://textures/materials/orange.res" id="6_md2fv"] +[ext_resource type="Material" uid="uid://bfvkovv6sevfw" path="res://textures/materials/black90%.res" id="7_2k5kn"] [ext_resource type="ArrayMesh" uid="uid://dpdsmpuycacsx" path="res://models/playerv2.obj" id="7_jg2rj"] [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_jqoeg"] -albedo_texture = ExtResource("3_8p34k") +albedo_texture = ExtResource("2_y1ydh") [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_0suq1"] -albedo_texture = ExtResource("3_8p34k") [sub_resource type="BoxShape3D" id="BoxShape3D_04dhp"] size = Vector3(0.379028, 0.376923, 0.371674) diff --git a/scripts/Networking.gd b/scripts/Networking.gd index f947855..4f33e53 100644 --- a/scripts/Networking.gd +++ b/scripts/Networking.gd @@ -1,152 +1,16 @@ extends Node var player_script := load("res://scripts/Player.gd") -var server_map -@onready var Weapons = GameData.Weapons var player_model = load("res://scenes/models/player.tscn") var peer = ENetMultiplayerPeer.new() -var settings var clients:Dictionary = {} var last_client_id = 1 -var map_path -var map_root_name -var team_CS = { - "spawnpoints": [], - "members": [], - "round_score": 0, - "game_score": 0 -} -var team_OS = { - "spawnpoints": [], - "members": [], - "round_score": 0, - "game_score": 0 -} -var round_number = 0 -var gamemode - -func parse_arguments(): - var arguments = {} - for argument in OS.get_cmdline_args(): - if argument.find("=") > -1: - var key_value = argument.split("=") - arguments[key_value[0].lstrip("--")] = key_value[1] - else: - # Options without an argument will be present in the dictionary, - # with the value set to an empty string. - arguments[argument.lstrip("--")] = "" - return arguments - -func check_map_availability(path): - var maps = DirAccess.open("res://scenes/maps").get_files() - var map_name = (path.split("/")[-1]).split(".")[0] - for map in maps: - var checking_map_name = str(map.split(".")[0]) - if(checking_map_name == map_name): - return true - return false - -func check_gamemode_availability(gamemode): - var gamemodes = settings["game"]["gamemodes"].keys() - for gm in gamemodes: - if gm == gamemode: - return true - return false - -func _ready(): - var arguments = parse_arguments() - if "--server" in OS.get_cmdline_args(): - GameData.read_settings() - settings = GameData.server_settings - - ################ parsing map - var path = "res://scenes/maps/%s.tscn" % arguments["map"] if arguments.has("map") else "res://scenes/maps/%s.tscn" % settings["defaults"]["map"] - if (check_map_availability(path)): - map_path = path - else: - print("Unknown map %s Available maps:" % path) - for map in DirAccess.open("res://scenes/maps").get_files(): - print(str(map.split(".")[0])) - get_tree().quit() - return - ################ parsing gamemode - var gm = str(arguments["gamemode"]) if (arguments.has("gamemode")) else "TDM" - if(check_gamemode_availability(gm)): - print("Gamemode exists") - else: - print("No") - StartServer(map_path, gm) #######################################SERVER#################################### -func check_gamemode_end_conditions(): - match gamemode: - "TDM": - var kills_amount_to_win = settings["game"]["gamemodes"]["TDM"]["kills"] - if team_OS["round_score"] >= kills_amount_to_win: - team_OS["game_score"] += 1 - print("OS won") - send_everyone([find_playermodel_by_internal_id(clients[clients.keys().pick_random()]["internal_id"]).end_round, 1]) # 1 = os is win - new_round() - - elif team_CS["round_score"] >= kills_amount_to_win: - team_CS["game_score"] += 1 - send_everyone([find_playermodel_by_internal_id(clients[clients.keys().pick_random()]["internal_id"]).end_round, -1]) # -1 = cs is win - print("CS won") - new_round() -func switch_map(new_map_path): - print("Switching map to %s" % new_map_path) - if (not check_map_availability(new_map_path)): - var default_map = settings["defaults"]["map"] - print("Error. No map found. Loading default map %s" % default_map) - new_map_path = "res://scenes/maps/%s.tscn" % default_map - if (not check_map_availability(new_map_path)): - print("Error. Default map is not valid. Please, specify a valid map in a config file.") - return - map_path = new_map_path - get_tree().change_scene_to_file(map_path) - await get_tree().create_timer(0.1).timeout #I know that this isn't a good practice, but I didn't find anything better - map_root_name = (map_path.split("/")[-1]).split(".")[0] - server_map = get_tree().root.get_node(map_root_name) - - var spawnpoints = server_map.find_children("spawnpoint*", "" ,true) - team_OS["spawnpoints"] = [] - team_CS["spawnpoints"] = [] - for spawnpoint in spawnpoints: - if spawnpoint.team == 0: # cs - team_CS["spawnpoints"].push_back(spawnpoint) - elif spawnpoint.team > 0: # os - team_OS["spawnpoints"].push_back(spawnpoint) - -func switch_gamemode(new_gamemode): - if not check_gamemode_availability(new_gamemode): - print("No gamemode found") - return - gamemode = new_gamemode - -func new_game(new_map_path, new_gamemode): - team_OS["game_score"] = 0 - team_OS["round_score"] = 0 - team_CS["game_score"] = 0 - team_CS["round_score"] = 0 - - round_number = 0 - await switch_map(new_map_path) - await switch_gamemode(new_gamemode) - -func new_round(): - round_number += 1 - team_OS["round_score"] = 0 - team_CS["round_score"] = 0 - -func find_playermodel_by_internal_id(internal_id): - return server_map.get_node("player" + str(internal_id)) - -func StartServer(map_path, gm): - await new_game(map_path, gm) - - var port = int(settings["port"]) - var maxclients = int(settings["maxclients"]) +func StartServer(): + var port = int(GameData.server_settings["port"]) + var maxclients = int(GameData.server_settings["maxclients"]) if (peer.create_server(port, maxclients) != OK): print("Couldn't create server. Check if another proccess binds port %s" % str(port)) return @@ -157,56 +21,6 @@ func StartServer(map_path, gm): peer.connect("peer_connected", _Peer_Connected) peer.connect("peer_disconnected", _Peer_Disconnected) - var spectator = preload("res://scenes/models/spectator.tscn").instantiate() - server_map.add_child(spectator) - -func send_everyone(args): - if (typeof(args[0]) == 4): #string - for current_client_id in clients.keys(): - if (args.size() == 1): rpc_id(int(current_client_id), args[0]) - elif (args.size() == 2): rpc_id(int(current_client_id), args[0], args[1]) - elif (args.size() == 3): rpc_id(int(current_client_id), args[0], args[1], args[2]) - else: rpc_id(int(current_client_id), args[0], args[1], args[2], args[3]) - else: - for current_client_id in clients.keys(): - if (args.size() == 1): args[0].rpc_id(int(current_client_id)) - elif (args.size() == 2): args[0].rpc_id(int(current_client_id), args[1]) - elif (args.size() == 3): args[0].rpc_id(int(current_client_id), args[1], args[2]) - else: args[0].rpc_id(int(current_client_id), args[1], args[2], args[3]) - -func send_everyone_except(client_id, args): - if (typeof(args[0]) == 4): #string - for current_client_id in clients.keys(): - if (str(current_client_id) == str(client_id)): continue - if (args.size() == 1): rpc_id(int(current_client_id), args[0]) - elif (args.size() == 2): rpc_id(int(current_client_id), args[0], args[1]) - elif (args.size() == 3): rpc_id(int(current_client_id), args[0], args[1], args[2]) - else: rpc_id(int(current_client_id), args[0], args[1], args[2], args[3]) - else: - for current_client_id in clients.keys(): - if (str(current_client_id) == str(client_id)): continue - if (args.size() == 1): args[0].rpc_id(int(current_client_id)) - elif (args.size() == 2): args[0].rpc_id(int(current_client_id), args[1]) - elif (args.size() == 3): args[0].rpc_id(int(current_client_id), args[1], args[2]) - else: args[0].rpc_id(int(current_client_id), args[1], args[2], args[3]) - -func find_weapon_by_number(number): - var found_weapon - for weapon in settings["game"]["weapons"].keys(): - if (settings["game"]["weapons"][weapon]["number"] == number): - found_weapon = settings["game"]["weapons"][weapon].duplicate() - break - return found_weapon - -func find_class_type_by_number(number): - var found_class - var classtypes = settings["game"]["classTypes"] - for classtype in classtypes: - if classtypes[classtype]["number"] == number: - found_class = classtypes[classtype].duplicate() - break - return found_class - func _Peer_Connected(client_id): print("User " + str(client_id) + " has conected") var internal_id = last_client_id + 1 @@ -217,31 +31,31 @@ func _Peer_Connected(client_id): client["position"] = Vector3(0, 10, 0) client["internal_id"] = internal_id client["is_playable"] = false - client["current_weapon"] = settings["game"]["weapons"]["knife"].duplicate() + client["current_weapon"] = GameData.server_settings["game"]["weapons"]["knife"].duplicate() var puppet = player_model.instantiate() puppet.set_properties(clients[client_id]) - server_map.add_child(puppet) + ServerUtils.server_map.add_child(puppet) @rpc("any_peer", "reliable", "call_remote") func client_ready(client_id): var client = clients[client_id] var internal_id = client["internal_id"] - - send_everyone_except(client_id, ["spawn_puppet", clients[client_id]]) + + NetUtils.send_everyone_except(client_id, [Networking.spawn_puppet, clients[client_id]]) client["ready"] = true - var client_playermodel = find_playermodel_by_internal_id(internal_id) + var client_playermodel = ServerUtils.find_playermodel_by_internal_id(internal_id) if (client["class_type"] == 0): - var class_spawnpoint = find_class_type_by_number(client["class_type"])["spawnpoint"] + var class_spawnpoint = ServerUtils.find_class_type_by_number(client["class_type"])["spawnpoint"] client["position"] = Vector3(class_spawnpoint[0], class_spawnpoint[1], class_spawnpoint[2]) else: var index = abs(client["class_type"]) if (client["class_type"] > 0): - client["position"] = team_OS["spawnpoints"].pick_random().get_class_spawnpoint(index) + client["position"] = ServerUtils.team_OS["spawnpoints"].pick_random().get_class_spawnpoint(index) elif (client["class_type"] < 0): - client["position"] = team_CS["spawnpoints"].pick_random().get_class_spawnpoint(index) + client["position"] = ServerUtils.team_CS["spawnpoints"].pick_random().get_class_spawnpoint(index) client_playermodel.set_properties(client) client_playermodel.teleport.rpc_id(client_id, Vector3(client["position"].x, client["position"].y, client["position"].z)) @@ -250,14 +64,16 @@ func _Peer_Disconnected(client_id): var client = clients[client_id] var internal_id = client["internal_id"] if (client["class_type"] < 0): - team_CS["members"].erase(client) + ServerUtils.team_CS["members"].erase(client) elif (client["class_type"] > 0): - team_OS["members"].erase(client) + ServerUtils.team_OS["members"].erase(client) rpc("despawn_puppet", internal_id) - var puppet = server_map.get_node("player" + str(internal_id)) - server_map.remove_child(puppet) + var puppet = ServerUtils.server_map.get_node("player" + str(internal_id)) + ServerUtils.server_map.remove_child(puppet) clients.erase(client_id) + if (clients.size() == 0): + ServerUtils.new_game(null, null) #Not changing anything @rpc ("any_peer", "reliable", "call_remote") func get_character_properties(client_id): @@ -270,26 +86,29 @@ func get_character_properties(client_id): func sync_client(client_id, position, rotation): var client = clients[client_id] var internal_id = str(client["internal_id"]) - var client_playermodel = find_playermodel_by_internal_id(internal_id) + var client_playermodel = ServerUtils.find_playermodel_by_internal_id(internal_id) client_playermodel.position = position client_playermodel.find_child("Head").rotation.y = rotation.y client_playermodel.find_child("Head").find_child("Camera").rotation.x = rotation.x client["position"] = position client["rotation"] = rotation - send_everyone_except(client_id, [client_playermodel.sync_puppet, internal_id, position, rotation]) + #NetUtils.send_everyone_except(client_id, ["sync_puppet", internal_id, position, rotation]) + NetUtils.send_everyone_except(client_id, [client_playermodel.sync_puppet, internal_id, position, rotation]) @rpc ("any_peer", "call_remote", "reliable") func get_client_list(client_id): + #var playermodel = ServerUtils.find_playermodel_by_internal_id(clients[client_id]["internal_id"]) for current_client_id in clients.keys(): if (current_client_id == client_id): continue rpc_id(client_id, "spawn_puppet", clients[current_client_id]) + #rpc_id(client_id, playermodel.spawn_puppet, clients[current_client_id]) @rpc ("any_peer", "call_remote", "reliable") func get_server_settings(client_id): var client = clients[client_id] var internal_id = str(client["internal_id"]) - var client_playermodel = find_playermodel_by_internal_id(internal_id) - client_playermodel.set_game_settings.rpc_id(client_id, settings["game"]) + var client_playermodel = ServerUtils.find_playermodel_by_internal_id(internal_id) + client_playermodel.set_game_settings.rpc_id(client_id, GameData.server_settings["game"]) @rpc ("any_peer", "call_remote", "reliable") func set_nickname(client_id, nickname): @@ -299,10 +118,10 @@ func set_nickname(client_id, nickname): func shot(client_id): var client = clients[client_id] var internal_id = client["internal_id"] - var current_weapon = settings["game"]["weapons"].find_key(find_weapon_by_number(client["current_weapon"]["number"])) - var current_weapon_settings = settings["game"]["weapons"][current_weapon] + var current_weapon = GameData.server_settings["game"]["weapons"].find_key(ServerUtils.find_weapon_by_number(client["current_weapon"]["number"])) + var current_weapon_settings = GameData.server_settings["game"]["weapons"][current_weapon] - var client_playermodel = find_playermodel_by_internal_id(internal_id) + var client_playermodel = ServerUtils.find_playermodel_by_internal_id(internal_id) var raycast:RayCast3D = client_playermodel.get_node("Head/Camera/viewRaycast") var weapon_raycast:RayCast3D = client_playermodel.get_node("Head/Camera/Hand/" + str(current_weapon) + "/raycast") raycast.target_position.z = -current_weapon_settings["range"] @@ -332,7 +151,7 @@ func shot(client_id): var shape_num = weapon_raycast.get_collider_shape() var shapes = ["head", "body", "hand", "leg"] - var shape = choose_collision_shape(target, shapes, shape_num) + var shape = ServerUtils.choose_collision_shape(target, shape_num) var damage for s in shapes: if s in shape.name: @@ -351,7 +170,7 @@ func shot(client_id): var client_team = 1 if client["class_type"] > 0 else -1 if (target_client_team == client_team): - if (settings["game"]["gamemodes"][gamemode]["firendlyfile"]): + if (GameData.server_settings["game"]["gamemodes"][ServerUtils.gamemode]["firendlyfile"]): target_client["HP"] -= damage else: target_client["HP"] -= damage @@ -360,82 +179,53 @@ func shot(client_id): var index = abs(client["class_type"]) var respawn = Vector3(0, 10 ,0) if (target_client_team == 1): - team_CS["round_score"] += 1 - respawn = team_OS["spawnpoints"].pick_random().get_class_spawnpoint(index) + ServerUtils.team_CS["round_score"] += 1 + respawn = ServerUtils.team_OS["spawnpoints"].pick_random().get_class_spawnpoint(index) elif (target_client_team == -1): - team_OS["round_score"] += 1 - respawn = team_CS["spawnpoints"].pick_random().get_class_spawnpoint(index) + ServerUtils.team_OS["round_score"] += 1 + respawn = ServerUtils.team_CS["spawnpoints"].pick_random().get_class_spawnpoint(index) - print("Score(OS-CS): %s - %s" % [str(team_OS["round_score"]), str(team_CS["round_score"])]) + print("Score(OS-CS): %s - %s" % [str(ServerUtils.team_OS["round_score"]), str(ServerUtils.team_CS["round_score"])]) target_client["position"] = respawn target_client["HP"] = 100 target.teleport.rpc_id(target_client_id, target_client["position"]) - check_gamemode_end_conditions() + ServerUtils.check_gamemode_end_conditions() target.set_hp.rpc_id(target_client_id, target_client["HP"]) elif (target is StaticBody3D): - var shapes = ["head", "body"] - var shape_num = weapon_raycast.get_collider_shape() - var shape = choose_collision_shape(target, shapes, shape_num) + #var shapes = ["head", "body"] + #var shape_num = weapon_raycast.get_collider_shape() + #var shape = ServerUtils.choose_collision_shape(target, shape_num) target.shot.rpc_id(client_id, weapon_raycast.get_collision_point()) - send_everyone_except(client_id, [target.shot, weapon_raycast.get_collision_point()]) - -func choose_collision_shape(target, shapes, shape_num): - var collision_shapes:Array - for s in target.get_children(): - if s is CollisionShape3D: - collision_shapes.push_back(s) - var shape - for i in range(0, collision_shapes.size()): - if i == shape_num: - shape = collision_shapes[i] - break - return shape + NetUtils.send_everyone_except(client_id, [target.shot, weapon_raycast.get_collision_point()]) @rpc("reliable", "call_remote", "any_peer") func change_weapon(client_id, new_weapon_number): var client = clients[client_id] client["reloading"] = false var internal_id = str(client["internal_id"]) - for weapon in Weapons.keys(): - if (settings["game"]["weapons"][weapon]["number"] == new_weapon_number): - client["current_weapon"] = settings["game"]["weapons"][weapon].duplicate() + for weapon in GameData.Weapons.keys(): + if (GameData.server_settings["game"]["weapons"][weapon]["number"] == new_weapon_number): + client["current_weapon"] = GameData.server_settings["game"]["weapons"][weapon].duplicate() break - var client_playermodel = find_playermodel_by_internal_id(internal_id) + var client_playermodel = ServerUtils.find_playermodel_by_internal_id(internal_id) client_playermodel.change_weapon(new_weapon_number) - send_everyone_except(client_id, [client_playermodel.change_weapon_puppet, internal_id, new_weapon_number]) + #NetUtils.send_everyone_except(client_id, ["change_weapon_puppet", internal_id, new_weapon_number]) + NetUtils.send_everyone_except(client_id, [client_playermodel.change_weapon_puppet, internal_id, new_weapon_number]) @rpc("any_peer", "call_remote", "reliable") func client_reloading(client_id): var client = clients[client_id] - var weapons_list = settings["game"]["weapons"] + var weapons_list = GameData.server_settings["game"]["weapons"] var current_client_weapon = client["current_weapon"] - var current_weapon_example = weapons_list.find_key(find_weapon_by_number(current_client_weapon["number"])) + var current_weapon_example = weapons_list.find_key(ServerUtils.find_weapon_by_number(current_client_weapon["number"])) var current_weapon_settings = weapons_list[current_weapon_example] var reload_time = current_weapon_settings["reload"] client["reloading"] = true - reloading_complete(client_id, reload_time) - -func reloading_complete(client_id, reloading_time): - await get_tree().create_timer(reloading_time).timeout - var client = clients[client_id] - if (!client["reloading"]): # if a client has interrupted the reloading - return - - var weapons_list = settings["game"]["weapons"] - var current_client_weapon = client["current_weapon"] - var current_weapon_example = weapons_list.find_key(find_weapon_by_number(current_client_weapon["number"])) - var current_weapon_settings = weapons_list[current_weapon_example] - var to_reload = current_weapon_settings["magazine"] - current_client_weapon["magazine"] - if (to_reload <= current_client_weapon["ammo"]): - current_client_weapon["magazine"] += to_reload - current_client_weapon["ammo"] -= to_reload - else: - current_client_weapon["magazine"] += current_client_weapon["ammo"] - current_client_weapon["ammo"] = 0 + ServerUtils.reloading_complete(client_id, reload_time) @rpc("reliable", "any_peer", "call_remote") func get_map(client_id): - rpc_id(client_id, "receive_map", map_path) + rpc_id(client_id, "receive_map", ServerUtils.map_path) @rpc("reliable", "any_peer", "call_remote") func choose_class(client_id, class_id): @@ -443,9 +233,9 @@ func choose_class(client_id, class_id): client["class_type"] = class_id #here are must be checks if the teams are balanced. WIP. if class_id > 0: - team_OS["members"].push_back(client) + ServerUtils.team_OS["members"].push_back(client) elif class_id < 0: - team_CS["members"].push_back(client) + ServerUtils.team_CS["members"].push_back(client) switch_class.rpc_id(client_id, class_id) @@ -456,6 +246,9 @@ var menu = preload("res://scenes/HUD/menu.tscn") var current_map_instance var choose_team_hud var playermodel +var puppets_to_spawn = [] +var map_path +var map_root_name @rpc ("reliable", "call_remote") func set_character_properties(p): @@ -463,6 +256,16 @@ func set_character_properties(p): @rpc("authority", "reliable", "call_remote") func spawn_puppet(properties): + if (current_map_instance == null): # Player's not ready + puppets_to_spawn.push_back(properties) + return + var puppet = player_model.instantiate() + properties["ready"] = true + puppet.set_properties(properties.duplicate()) + + current_map_instance.add_child(puppet) + +func spawn_puppet_onready(properties): var puppet = player_model.instantiate() properties["ready"] = true puppet.set_properties(properties.duplicate()) @@ -471,6 +274,8 @@ func spawn_puppet(properties): @rpc("authority", "reliable", "call_remote") func despawn_puppet(internal_id): + if (current_map_instance == null): + return var puppet = current_map_instance.get_node("player" + str(internal_id)) current_map_instance.remove_child(puppet) @@ -541,6 +346,9 @@ func _Connection_Succseeded(): choose_team_hud = preload("res://scenes/HUD/choose_team.tscn").instantiate() current_map_instance.add_child(choose_team_hud) + + for puppet in puppets_to_spawn: + spawn_puppet_onready(puppet) func _Server_Disconnected(): print("Server has disconnected") diff --git a/scripts/Player.gd b/scripts/Player.gd index fa1b4da..f28becb 100644 --- a/scripts/Player.gd +++ b/scripts/Player.gd @@ -27,7 +27,7 @@ var properties = { internal_id = 0, nickname = "Unnamed", ready = false, - current_weapon = null, #game_settings["weapons"]["knife"].duplicate(), + current_weapon = null, current_weapon_number = 0, last_shot = 0, reloading = false, @@ -49,16 +49,31 @@ var client_settings var can_shoot = true var time_since_last_shot = 0 +var round_status = { + "cs_round_score" = 0, + "os_round_score" = 0, + "os_game_score" = 0, + "cs_game_score" = 0, + "round_number" = 0, +} + @onready var head = $Head @onready var camera: Camera3D = $Head/Camera @onready var playerCharacterBody = $"." -var HUD +var HUD var healthLabel var armorLabel var ammoLabel var magazineLabel var statusLabel +var csScoreRoundLabel +var csScoreGameLabel +var osScoreRoundLabel +var osScoreGameLabel +var roundNumberLabel +var timeLabel +var winnerLabel func set_properties(props): properties = props @@ -72,6 +87,24 @@ func set_property(key, value): properties[key] = value update_state() +func init_hud(): + HUD = $HUD + + healthLabel = $"HUD/health_and_ammo_display/healthdisplay/text" + armorLabel = $"HUD/health_and_ammo_display/armordisplay/text" + ammoLabel = $"HUD/health_and_ammo_display/ammodisplay/text" + magazineLabel = $"HUD/health_and_ammo_display/magazinedisplay/text" + statusLabel = $"HUD/Status" + + csScoreRoundLabel = $"HUD/round_status/cs_score_round" + csScoreGameLabel = $"HUD/round_status/cs_score_game" + osScoreRoundLabel = $"HUD/round_status/os_score_round" + osScoreGameLabel = $"HUD/round_status/os_score_game" + + roundNumberLabel = $"HUD/round_status/round_number" + timeLabel = $"HUD/round_status/time" + winnerLabel = $"HUD/round_status/winner" + func update_hud(): healthLabel.text = str(properties["HP"]) armorLabel.text = str(properties["AP"]) @@ -82,6 +115,13 @@ func update_hud(): else: statusLabel.text = "Reloaded" + csScoreRoundLabel.text = str(round_status["cs_round_score"]) + csScoreGameLabel.text = str(round_status["cs_game_score"]) + + osScoreRoundLabel.text = str(round_status["os_round_score"]) + osScoreGameLabel.text = str(round_status["os_game_score"]) + + roundNumberLabel.text = "Round " + str(round_status["round_number"]) func change_weapon(new_weapon_number): properties["reloading"] = false var weapons = $"Head/Camera/Hand".get_children() @@ -108,20 +148,14 @@ func find_current_weapon_by_number(number): func update_state(): camera.set_current(false) $"Head/Nickname".text = properties["nickname"] + if (!properties["is_playable"]): return camera.set_current(true) if (HUD == null): var hud = load("res://scenes/HUD/hud.tscn").instantiate() add_child(hud) - - HUD = $"HUD" - var healthAndAmmoDisplay = HUD.get_node("health_and_ammo_display") - - healthLabel = healthAndAmmoDisplay.get_node("healthdisplay/text") - armorLabel = healthAndAmmoDisplay.get_node("armordisplay/text") - ammoLabel = healthAndAmmoDisplay.get_node("ammodisplay/text") - magazineLabel = healthAndAmmoDisplay.get_node("magazinedisplay/text") - statusLabel = HUD.get_node("Status") + init_hud() + update_hud() playerCharacterBody.up_direction = Vector3.UP; Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) @@ -167,7 +201,8 @@ func reload(): current_weapon_instance["magazine"] += current_weapon_instance["ammo"] current_weapon_instance["ammo"] = 0 properties["reloading"] = false - update_hud() + if (HUD != null): + update_hud() func _unhandled_input(event): if (!properties["is_playable"]): return @@ -189,6 +224,10 @@ func _unhandled_input(event): camera.rotation.x = clamp(camera.rotation.x, deg_to_rad(-50), deg_to_rad(60)) func _physics_process(delta): + #var collision_shapes = find_children("collision*","",false) + #print(str(collision_shapes)) + #for shape:CollisionShape3D in collision_shapes: + #shape.rotation = rotation if (!properties["is_playable"]): return if (game_settings == null): return if (multiplayer.multiplayer_peer == null): @@ -197,7 +236,9 @@ func _physics_process(delta): if Input.is_action_pressed("shoot"): try_shoot() - update_hud() + if (HUD != null): + update_hud() + update_weapon_raycast() if Input.is_action_just_pressed("reload"): @@ -280,6 +321,22 @@ func change_weapon_puppet(i_id, new_weapon_number): if (int(i_id) == properties["internal_id"]): change_weapon(new_weapon_number) +@rpc("authority", "call_remote", "reliable") +func update_round_status(s): + print("Updated round status: " + str(s)) + round_status = s + if(HUD != null): + update_hud() + @rpc("authority", "call_remote", "reliable") func end_round(result): print("Result: %s" % str(result)) + winnerLabel.visible = true + if (result > 0): + winnerLabel.text = "OS WON" + elif (result < 0 ): + winnerLabel.text = "CS WON" + await get_tree().create_timer(1).timeout + winnerLabel.visible = false + winnerLabel.text = "" + diff --git a/scripts/Select.gd b/scripts/Select.gd index 5181fa1..4641f58 100644 --- a/scripts/Select.gd +++ b/scripts/Select.gd @@ -9,7 +9,6 @@ func _ready(): button.pressed.connect(self._button_pressed) func _button_pressed(): - var team match name: "Select_CS": choose_class_cs.visible = true diff --git a/scripts/utils/NetUtils.gd b/scripts/utils/NetUtils.gd new file mode 100644 index 0000000..39e454c --- /dev/null +++ b/scripts/utils/NetUtils.gd @@ -0,0 +1,40 @@ +extends Node + +func send_everyone(args): + for current_client_id in Networking.clients.keys(): + if (args.size() == 1): + var playermodel = ServerUtils.find_playermodel_by_internal_id(Networking.clients[current_client_id]["internal_id"]) + playermodel.rpc_id(int(current_client_id), args[0]) + elif (args.size() == 2): + var playermodel = ServerUtils.find_playermodel_by_internal_id(Networking.clients[current_client_id]["internal_id"]) + playermodel.rpc_id(int(current_client_id), args[0], args[1]) + elif (args.size() == 3): + var playermodel = ServerUtils.find_playermodel_by_internal_id(Networking.clients[current_client_id]["internal_id"]) + playermodel.rpc_id(int(current_client_id), args[0], args[1], args[2]) + else: + var playermodel = ServerUtils.find_playermodel_by_internal_id(Networking.clients[current_client_id]["internal_id"]) + playermodel.rpc_id(int(current_client_id), args[0], args[1], args[2], args[3]) + +func send_everyone_except(client_id, args): + if (typeof(args[0]) == 4): #string + for current_client_id in Networking.clients.keys(): + if (str(current_client_id) == str(client_id)): continue + if (args.size() == 1): + var playermodel = ServerUtils.find_playermodel_by_internal_id(Networking.clients[current_client_id]["internal_id"]) + playermodel.rpc_id(int(current_client_id), args[0]) + elif (args.size() == 2): + var playermodel = ServerUtils.find_playermodel_by_internal_id(Networking.clients[current_client_id]["internal_id"]) + playermodel.rpc_id(int(current_client_id), args[0], args[1]) + elif (args.size() == 3): + var playermodel = ServerUtils.find_playermodel_by_internal_id(Networking.clients[current_client_id]["internal_id"]) + playermodel.rpc_id(int(current_client_id), args[0], args[1], args[2]) + else: + var playermodel = ServerUtils.find_playermodel_by_internal_id(Networking.clients[current_client_id]["internal_id"]) + playermodel.rpc_id(int(current_client_id), args[0], args[1], args[2], args[3]) + else: + for current_client_id in Networking.clients.keys(): + if (str(current_client_id) == str(client_id)): continue + if (args.size() == 1): args[0].rpc_id(int(current_client_id)) + elif (args.size() == 2): args[0].rpc_id(int(current_client_id), args[1]) + elif (args.size() == 3): args[0].rpc_id(int(current_client_id), args[1], args[2]) + else: args[0].rpc_id(int(current_client_id), args[1], args[2], args[3]) diff --git a/scripts/utils/ServerUtils.gd b/scripts/utils/ServerUtils.gd new file mode 100644 index 0000000..525db21 --- /dev/null +++ b/scripts/utils/ServerUtils.gd @@ -0,0 +1,214 @@ +extends Node + +var team_CS = { + "spawnpoints": [], + "members": [], + "round_score": 0, + "game_score": 0 +} +var team_OS = { + "spawnpoints": [], + "members": [], + "round_score": 0, + "game_score": 0 +} + +var round_number = 0 +var gamemode +var map_path +var map_root_name +var server_map + +func find_class_type_by_number(number): + var found_class + var classtypes = GameData.server_settings["game"]["classTypes"] + for classtype in classtypes: + if classtypes[classtype]["number"] == number: + found_class = classtypes[classtype].duplicate() + break + return found_class + +func check_map_availability(path): + var maps = DirAccess.open("res://scenes/maps").get_files() + var map_name = (path.split("/")[-1]).split(".")[0] + for map in maps: + var checking_map_name = str(map.split(".")[0]) + if(checking_map_name == map_name): + return true + return false + +func check_gamemode_availability(gm): + var gamemodes = GameData.server_settings["game"]["gamemodes"].keys() + for g in gamemodes: + if gm == g: + return true + return false + +func find_weapon_by_number(number): + var found_weapon + for weapon in GameData.server_settings["game"]["weapons"].keys(): + if (GameData.server_settings["game"]["weapons"][weapon]["number"] == number): + found_weapon = GameData.server_settings["game"]["weapons"][weapon].duplicate() + break + return found_weapon + +func find_playermodel_by_internal_id(internal_id): + return server_map.get_node("player" + str(internal_id)) + +func reloading_complete(client_id, reloading_time): + await get_tree().create_timer(reloading_time).timeout + var client = Networking.clients[client_id] + if (!client["reloading"]): # if a client has interrupted the reloading + return + + var weapons_list = GameData.server_settings["game"]["weapons"] + var current_client_weapon = client["current_weapon"] + var current_weapon_example = weapons_list.find_key(ServerUtils.find_weapon_by_number(current_client_weapon["number"])) + var current_weapon_settings = weapons_list[current_weapon_example] + var to_reload = current_weapon_settings["magazine"] - current_client_weapon["magazine"] + if (to_reload <= current_client_weapon["ammo"]): + current_client_weapon["magazine"] += to_reload + current_client_weapon["ammo"] -= to_reload + else: + current_client_weapon["magazine"] += current_client_weapon["ammo"] + current_client_weapon["ammo"] = 0 + +func choose_collision_shape(target, shape_num): + var collision_shapes:Array = [] + for s in target.get_children(): + if s is CollisionShape3D: + collision_shapes.push_back(s) + var shape + for i in range(0, collision_shapes.size()): + if i == shape_num: + shape = collision_shapes[i] + break + return shape + +func new_round(): + round_number += 1 + team_OS["round_score"] = 0 + team_CS["round_score"] = 0 + print("CS members " + str(team_CS["members"])) + print("OS members " + str(team_OS["members"])) + for member in team_CS["members"]: + member["position"] = team_CS["spawnpoints"].pick_random().get_class_spawnpoint(abs(member["class_type"])) + ServerUtils.find_playermodel_by_internal_id(member["internal_id"]).teleport.rpc_id(Networking.clients.find_key(member), member["position"]) + for member in team_OS["members"]: + member["position"] = team_OS["spawnpoints"].pick_random().get_class_spawnpoint(abs(member["class_type"])) + ServerUtils.find_playermodel_by_internal_id(member["internal_id"]).teleport.rpc_id(Networking.clients.find_key(member), member["position"]) + send_scores() + +func new_game(new_map_path, new_gamemode): + team_OS["game_score"] = 0 + team_OS["round_score"] = 0 + team_CS["game_score"] = 0 + team_CS["round_score"] = 0 + + round_number = 0 + send_scores() + new_map_path = map_path if (new_map_path == null) else new_map_path + new_gamemode = gamemode if (new_gamemode == null) else new_gamemode + await switch_map(new_map_path) + await switch_gamemode(new_gamemode) + +func check_gamemode_end_conditions(): + match gamemode: + "TDM": + var kills_amount_to_win = GameData.server_settings["game"]["gamemodes"]["TDM"]["kills"] + if team_OS["round_score"] >= kills_amount_to_win: + team_OS["game_score"] += 1 + print("OS won") + NetUtils.send_everyone(["end_round", 1]) # 1 = os is win + new_round() + + elif team_CS["round_score"] >= kills_amount_to_win: + team_CS["game_score"] += 1 + NetUtils.send_everyone(["end_round", -1]) # -1 = cs is win + print("CS won") + new_round() + send_scores() + +func switch_map(new_map_path): + print("Switching map to %s" % new_map_path) + if (not ServerUtils.check_map_availability(new_map_path)): + var default_map = GameData.server_settings["defaults"]["map"] + print("Error. No map found. Loading default map %s" % default_map) + new_map_path = "res://scenes/maps/%s.tscn" % default_map + if (not ServerUtils.check_map_availability(new_map_path)): + print("Error. Default map is not valid. Please, specify a valid map in a config file.") + return + map_path = new_map_path + get_tree().change_scene_to_file(map_path) + await get_tree().create_timer(0.1).timeout #I know that this isn't a good practice, but I didn't find anything better + map_root_name = (map_path.split("/")[-1]).split(".")[0] + server_map = get_tree().root.get_node(map_root_name) + + var spawnpoints = server_map.find_children("spawnpoint*", "" ,true) + team_OS["spawnpoints"] = [] + team_CS["spawnpoints"] = [] + for spawnpoint in spawnpoints: + if spawnpoint.team == 0: # cs + team_CS["spawnpoints"].push_back(spawnpoint) + elif spawnpoint.team > 0: # os + team_OS["spawnpoints"].push_back(spawnpoint) + +func send_scores(): + NetUtils.send_everyone(["update_round_status", { + "cs_round_score" = team_CS["round_score"], + "os_round_score" = team_OS["round_score"], + "os_game_score" = team_OS["game_score"], + "cs_game_score" = team_CS["game_score"], + "round_number" = round_number, + }]) + +func switch_gamemode(new_gamemode): + if not ServerUtils.check_gamemode_availability(new_gamemode): + print("No gamemode found") + return + gamemode = new_gamemode + +func parse_arguments(): + var arguments = {} + for argument in OS.get_cmdline_args(): + if argument.find("=") > -1: + var key_value = argument.split("=") + arguments[key_value[0].lstrip("--")] = key_value[1] + else: + # Options without an argument will be present in the dictionary, + # with the value set to an empty string. + arguments[argument.lstrip("--")] = "" + return arguments + +func setup_server(): + var arguments = parse_arguments() + + GameData.read_settings() + + ################ parsing map + var path = "res://scenes/maps/%s.tscn" % arguments["map"] if arguments.has("map") else "res://scenes/maps/%s.tscn" % GameData.server_settings["defaults"]["map"] + if (ServerUtils.check_map_availability(path)): + map_path = path + else: + print("Unknown map %s Available maps:" % path) + for map in DirAccess.open("res://scenes/maps").get_files(): + print(str(map.split(".")[0])) + get_tree().quit() + return + ################ parsing gamemode + var gm = str(arguments["gamemode"]) if (arguments.has("gamemode")) else "TDM" + if (ServerUtils.check_gamemode_availability(gm)): + print("Gamemode exists") + else: + print("No") + + await new_game(map_path, gm) + + var spectator = preload("res://scenes/models/spectator.tscn").instantiate() + server_map.add_child(spectator) + + Networking.StartServer() + +func _ready(): + if "--server" in OS.get_cmdline_args(): + setup_server() diff --git a/textures/knife_textures.png.import b/textures/knife_textures.png.import new file mode 100644 index 0000000..24f6bc2 --- /dev/null +++ b/textures/knife_textures.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cbf6w7ativli8" +path.s3tc="res://.godot/imported/knife_textures.png-bfa45f2fa32fc252fea8eb5aaf3203bc.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="res://textures/knife_textures.png" +dest_files=["res://.godot/imported/knife_textures.png-bfa45f2fa32fc252fea8eb5aaf3203bc.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/textures/materials/black.material b/textures/materials/black.material index cbb1481d9fe33809280ca0df5408eb71b3d94e61..fea1339fc4c0e303027d5847850361228353980d 100644 GIT binary patch delta 1067 zcmV+`1l0S#2*3z`Qd2`i0ssI201yBGzX|{VCSeWGO8cRVE2Mm)iHZwROcQkqb^vn# zcK}dQ*0ezkv#MFE(rMrHv6m!Wq4P`?Gn7>IgK8-+cX!}_CTXcBYb*WlF2S{y3_sF! zcN;hP^a3MT6*Y$IR z@>DDT#N9c#I|6qX;O++81*x3Y{ssj~q`=^?!KG!+*XQmG9HZKU51PRWrw)Cjzu=a}8@7dTt#skU zmgvo$Fw%GZxVw&Pnn7^yG|Tu)cX<+(muwL{IZR!jmr)c&Q8pBNOPHtA&~qiT^?z*) z(;Ge@*GV#dKpc^l&*rP@VDKrOQQwH9T)uTL7BsDY{NLxo+KBVbQvc@q&p6Z;sO|Dg zo0X4jnUQc&GN&g)rm7~eeAKzS1`awuDZ?Z-N3(n+&CG!bYyWP$a^tg;2OvzGFtJW_ zkRfBnT?ywjK-z-=#R85OD`I?bam`{HBBppmiU&qg@`5BFErYVj_qQ`hgFrYm^>85+ z6!&aKJr#%&*LLVyeyFus>Bt%_!?sE#gOlH9qjuJKmD9fGgJ0WBRcu40(J~9_CsESg zFx3bI&t8~!Ny%E$l2-b$n$%MRfa{Y(0w#Y77+zE{-AOz!3qe?SbA*&>bH>q+e2!=<}(5M9=bLx0w5IlOsWS3 zu>L?_>t<&wGIuV-Edg5n3y_1U)zyTFR~X3CER)0J#F1#HL(oEwTPb+^J)`2zNLXwM z{`{S%@`eF2hXGxYJqOoA+{3&`U*hr3az0QVn++8fE@E%#VcOA zJGux$ViT!>u32*UrY5Guk|V=B*Ct?w&h%J-*tSTt@S%EhIa3hEO>y_L18Qd)TGcG> lH2TOi=89d>Jjo84(i||~c-0as^J1%9ry-QUg#l7iLqiQ8Vtt8_F+K{w-HeVITf=}s42KjYExr z+A_b?S^3D8n+O*rb9ypls!9UOMVMtqD| z9~)S(XmKZ^ISfi2(jE&QC{DO=V0cJy&SDrMwsb^G2TD>hgCQX8g0jl@w=+nCKr}Sv za2^XsiF>x53Pg!(Idm;Q)atBsWQ~?#TcxtW%I~vLJ8QhkY2VYquWhC#wxQB!n+5fg zNNIPNs^o!ZGtA4RWKC&GE1j$+4S?#CLINg#0vH}tDS9#AktxqoxTVtm{4r6eQZfn) z3*1#va*pour@zJHW;Co5Ksmq05^Og!xtmM;0WnNwi!^etC*ga*)is720V2tZ)C5te zb*|~+37gk27Em!1L9rxFA(&0NX5>qilt_{jUF^rMZn z*Alr-qE1StNtma{CP2+QqPvn&N1lxaH6$N93v26?`Zt0Y-(YtPoe_8SB*cxm=vb2` z?F{}GiAov_O>I}NEP)fwaE~)WxOQKE3qpgDwe|mUe}#5)JVV@nxeOBCl?5QIW7#3W z`U8Efo1Lx5+_@091ZedyKn|u>R}&^)VIWVlOb(M1N1~k$K?^x-2;nJ*D77#BU`+`X9{{%OellQB(g6bi z00000j7E@(KOuzdl)OxG6QB0pNy>?R2s4yb8#KdbP*KY#ny9!C#WYc;kyR=`3CtbW zy(E>HVc6ugS$kb)QW=&GDg~A*kstppBHl~X4|}$mO)xXI3@lK1XklT+o0$M3(P2?L zA4{ahr0`FXzGHthjNt`=eZ%P9t5n6o%T>@LH130Pd47!wcWM8pFAH;kJe?EVgFG1z zM@R89X)t(%y<}rN5GwD?%7sJ<|JBK$^wB({vfn%ZmwsG%T$NYJDEwQxiv*M4E;g=P znQ6j8CmS|q^bCG}2ceEYt4I@mPFrh@KFbmHfpXDD2ym=sMSwaJk8=qZ01r ztGP(O@xd$Ql_}IOr9>C?u!9%jW>)3{LL)1?svuk(;RvUMXrgS)m7Tx3I2;a#10!v7 kWHmMcfZwwp0?`2#81LW1sRF3yQBr}TGgQqD15#5%Lp)5waR2}S delta 443 zcmV;s0Yv_U2!#k$Qd2`i0ssI201yBGO$q=2?E?S+D77#BU`+`f9{{#2e=}fD(g7<) z5JUh0004lZ`dC5;*(rIM z%U5%ee&d5z$}3N(UrLEC>R|^j!p*GA2ZTmec2z;RIKmN53DHE^m@7Mfb#XWx4o61X l=E!Pn0)XGM9s