require 'webrick' require 'net/http' require 'json' require 'uri' require 'set' #require_relative 'map_manager' #if ENV['MAP'] # MapManager.update_mod #end STDOUT.sync = true # Ensure prints show up immediately # --- CONFIGURATION --- PORT = 8080 CONFIG_FILE = "./main/server.cfg" COD2_PATH = "./cod2_lnxded" # path to your linux cod2_lnxded MAP = "mp_rust" GAME = "dm" #MOD = "vandal" PORT_GAME = 28960 #if ENV['DEV'] #TELEGRAM_BOT_TOKEN = "5471134744:AAH34WiLh2a8gcZ5L2P6Z7y43wuz_7oOyXI" #TELEGRAM_CHAT_ID = "140859376" #else TELEGRAM_BOT_TOKEN = "8319299986:AAGfN-vX0S5PsLS_eSq_0g_9aD9zOhPY4y0" TELEGRAM_CHAT_ID = "-1001277125281" #end # Translations GAMETYPE_TRANSLATIONS = { 'dm' => 'Індивідуальний', 'tdm' => 'Командний', 'ctf' => 'Захоплення прапора', 'sd' => 'Пошук та знищення', 'hq' => 'Штаб', 'htf' => 'Утримування прапору', 're' => 'Викрадення речей', 'strat' => 'Індивідуальне утримування прапору', 'dom' => 'Домінування' } last_joins = {} JOIN_COOLDOWN = 10000 # seconds last_stats_sent_at = 0 STATS_COOLDOWN = 1 # seconds @map_ended_flag = false @last_valid_scores = nil @connected_players = {} @current_map_name = MAP @current_gametype = GAME def send_telegram_notification(msg, silent: true, parse_mode: nil) return if TELEGRAM_BOT_TOKEN.empty? || TELEGRAM_CHAT_ID.empty? uri = URI("https://api.telegram.org/bot#{TELEGRAM_BOT_TOKEN}/sendMessage") payload = { chat_id: TELEGRAM_CHAT_ID, text: msg, disable_notification: silent } payload[:parse_mode] = parse_mode if parse_mode http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Post.new(uri.request_uri) request["Content-Type"] = "application/json; charset=utf-8" request.body = payload.to_json begin response = http.request(request) if response.code == "200" puts "📡 Silent Telegram notification sent: #{msg}" else puts "⚠️ Telegram response: #{response.code} #{response.body}" end rescue => e puts "❌ Failed to send Telegram notification: #{e.message}" end end def escape_tg_markdown(text) # Remove CoD color codes like ^4, ^7, etc. Then escape telegram markdown chars. clean_text = text.to_s.gsub(/\^[0-9]/, '') clean_text.gsub(/([_*\[\]`\\])/, '\\\\\1') end def send_round_stats_notification(stats) types = GAMETYPE_TRANSLATIONS msg = "📊 *Результати раунду*\n\n" msg += "🗺 Карта: `#{stats[:map]}`\n" msg += "🎮 Режим: `#{types[stats[:type]]}`\n" unless stats[:type] == 'dm' || stats[:type] == 'strat' msg += "🏆 Рахунок: `🇺🇸 #{stats[:allies]}-#{stats[:axis]} 🇩🇪`\n" end if stats[:players].empty? msg += "Гравців не знайдено" else all_players_data = stats[:players].map do |p| mvp_score = if stats[:type] == 'strat' p[:score].to_i else (p[:kills].to_i - p[:deaths].to_i) + (p[:caps].to_i * 5) + (p[:rets].to_i * 3) + (p[:drops].to_i * 2) + (p[:plnts].to_i * 5) + (p[:defs].to_i * 5) + (p[:hcaps].to_i * 5) + (p[:hdets].to_i * 5) + (p[:picks].to_i * 1) + (p[:own_picks].to_i * 1) end { player: p, mvp_score: mvp_score } end.sort_by { |d| d[:mvp_score] }.reverse best = all_players_data.first if best escaped_best_name = escape_tg_markdown(best[:player][:name]) msg += "\n🌟 *MVP матчу*: #{escaped_best_name} (`#{best[:mvp_score]}`)\n\n" end msg += "👤 *Статистика гравців:*\n" used_emojis = {} all_players_data.each do |data| p = data[:player] team_icon = (p[:team] == "allies" ? "🇺🇸" : (p[:team] == "axis" ? "🇩🇪" : "👁️")) details = [] if p[:own_picks] && p[:own_picks].to_i > 0 emoji = "🛡️" details << "#{emoji}#{p[:own_picks]}" used_emojis[emoji] = "Підбір свого прапора (+1)" end if p[:picks] && p[:picks].to_i > 0 emoji = stats[:type] == 're' ? "📦" : "🚩" details << "#{emoji}#{p[:picks]}" desc = stats[:type] == 're' ? "Підбір предмета (+1)" : "Підбір прапора (+1)" used_emojis[emoji] = desc end if p[:caps] && p[:caps].to_i > 0 if stats[:type] == 're' emoji = "🏠" desc = "Принесення предмета (+5)" elsif stats[:type] == 'htf' || stats[:type] == 'strat' || stats[:type] == 'dom' emoji = "🚩" desc = if stats[:type] == 'strat' "Захоплення прапора" elsif stats[:type] == 'dom' "Захоплення бази (+5)" else "Утримування прапора (+5)" end else emoji = "🏁" desc = "Принесення прапора (+5)" end details << "#{emoji}#{p[:caps]}" used_emojis[emoji] = desc end if p[:rets] && p[:rets].to_i > 0 details << "♻️#{p[:rets]}" desc = stats[:type] == 're' ? "Повернення предмета (+3)" : "Повернення прапора (+3)" used_emojis["♻️"] = desc end if p[:drops] && p[:drops].to_i > 0 details << "🎯#{p[:drops]}" desc = if stats[:type] == 're' "Знешкодження носія предмета (+2)" elsif stats[:type] == 'strat' "Знешкодження носія прапора" else "Знешкодження носія прапора (+2)" end used_emojis["🎯"] = desc end if p[:plnts] && p[:plnts].to_i > 0 details << "💣#{p[:plnts]}" used_emojis["💣"] = "Заложення бомби (+5)" end if p[:defs] && p[:defs].to_i > 0 details << "🔧#{p[:defs]}" used_emojis["🔧"] = "Розмінування бомби (+5)" end if p[:hcaps] && p[:hcaps].to_i > 0 details << "📻#{p[:hcaps]}" used_emojis["📻"] = "Встановлення штабу (+5)" end if p[:hdets] && p[:hdets].to_i > 0 details << "💥#{p[:hdets]}" used_emojis["💥"] = "Знищення штабу (+5)" end detail_str = details.empty? ? "" : " (#{details.join(' ')})" escaped_name = escape_tg_markdown(p[:name]) msg += "#{team_icon} *#{escaped_name}*: `#{data[:mvp_score]}` (`#{p[:kills]}`/`#{p[:deaths]}`)#{detail_str}\n" end unless used_emojis.empty? msg += "\n📖 *Пояснення:*\n" legend_parts = used_emojis.map { |emoji, desc| "#{emoji} #{desc}" } msg += legend_parts.join("\n") end end send_telegram_notification(msg, silent: true, parse_mode: 'Markdown') end # 1️⃣ Start WEBrick HTTP server for map downloads server_thread = Thread.new do mime_types = WEBrick::HTTPUtils::DefaultMimeTypes mime_types.store 'iwd', 'application/zip' server = WEBrick::HTTPServer.new(Port: PORT, BindAddress: "0.0.0.0", DocumentRoot: "./", MimeTypes: mime_types) trap('INT') { server.shutdown } puts "✅ Web server started at http://0.0.0.0:#{PORT}" server.start end # Determine appropriate IP to bind the game server to. # For Oracle Cloud and general VMs, bind to 0.0.0.0 # For Fly.io, bind to the fly-global-services interface bind_ip = "0.0.0.0" if File.exist?('/etc/hosts') fly_ip_line = File.readlines('/etc/hosts').find { |l| l.include?('fly-global-services') } if fly_ip_line bind_ip = fly_ip_line.split.first puts "🪰 Detected Fly.io environment, binding to #{bind_ip}" else puts "☁️ Using standard binding to 0.0.0.0" end end # 2️⃣ Generate server.cfg with your download URL # Use Oracle Cloud IP or environment variable public_ip = ENV['PUBLIC_IP'] || "130.162.218.15" public_url = "http://#{public_ip}:8080/" config_content = <<~CFG seta sv_cracked 1 seta sv_voice 0 seta sv_pure 0 seta sv_allowdownload 1 seta sv_wwwDownload 1 seta sv_wwwBaseURL "#{public_url}" seta sv_wwwDlDisconnected 1 seta g_needpass 0 seta sv_lan 0 seta sv_strictAuth 0 seta sv_disableClientConsole 0 seta sv_referencedIwds 0 seta scr_friendlyfire 1 seta scr_killcam 0 seta r_lodScale 1 set pam_mode "pub" // Hostname set sv_hostname "^3VAN^5DAL^7|No PRO" // Message of the Day set scr_motd "Not a PRO players. Play all gametypes in evening. DISCORD: vandal8269. DM, IHTF, TDM, CTFB, HTF, RE, SD, DOM | cod2.fly.dev" // Server password set g_password "" // Rcon password set rcon_password "3441" // Mod download url // Other set logfile 2 // Enable console logging into console_mp.log, 1=buffered, 2=continuous set g_log "games_mp.log" // If set, log game events into file (like player connect, disconnect, kill, damage, ...) set g_logsync 1 // Write data continuously // Public // Cvars below are used only if pam_mode is set to public (pam_mode pub) // Otherwise these settings are overwritten by pam_mode rules //******************************* // S&D Public server settings //******************************* // Match Style set scr_sd_timelimit 0 // Time limit for map. 0=disabled (minutes) set scr_sd_half_round 10 // Number of rounds when half-time starts. 0=ignored set scr_sd_half_score 6 // Number of score when half-time starts. 0=ignored set scr_sd_end_round 0 // Number of rounds when map ends. 0=ignored set scr_sd_end_score 10 // Number of score when map ends. 0=ignored // Round options set scr_sd_strat_time 5 // Time before round starts where players cannot move set scr_sd_roundlength 3 // Time length of each round (min) set scr_sd_end_time 5 // Time et the end of the round (after last player is killed) when you can finding weapons for the next round set scr_sd_count_draws 0 // If players are killed at same time - count this round (1) or play new round (0)? // Bomb settings set scr_sd_bombtimer_show 1 // Show bombtimer stopwatch set scr_sd_bombtimer 60 // Time untill bomb explodes. (seconds) set scr_sd_PlantTime 5 set scr_sd_DefuseTime 10 set scr_sd_plant_points 0 set scr_sd_defuse_points 0 set scr_sd_sniper_shotgun_info 0 // Show weapon info about sniper and shotgun players //******************************* // TDM Public server settings //******************************* // Match Style set scr_tdm_timelimit 30 // Time limit. When halftime is enabled, its time limit per half. 0=disabled (minutes) set scr_tdm_half_score 0 // Number of score when half-time starts. Has no effect when halftime is disabled. set scr_tdm_end_score 100 // Number of score when map ends. 0=ignored set scr_tdm_halftime 0 // Do halftime. When 1, scr_tdm_timelimit means time per half set scr_tdm_strat_time 0 // Time before round starts (sec) (0 - 10, default 5) //******************************* // DM Public server settings //******************************* // Match Style set scr_dm_timelimit 30 // Time limit. When halftime is enabled, its time limit per half. 0=disabled (minutes) set scr_dm_half_score 0 // Number of score when half-time starts. Has no effect when halftime is disabled. set scr_dm_end_score 50 // Number of score when map ends. 0=ignored set scr_dm_halftime 0 // Do halftime. When 1, scr_dm_timelimit means time per half set scr_dm_strat_time 0 // Time before round starts (sec) (0 - 10, default 5) //******************************* // STRAT Public server settings //******************************* set scr_strat_timelimit 30 // Time limit in minutes set scr_strat_scorelimit 500 // Score limit to win the map set scr_strat_strat_time 0 // Freeze time before match starts in seconds set scr_strat_respawn_time 0 // Player respawn delay in seconds set scr_strat_flag_spawndelay 10 // Initial delay before the flag spawns at the start of the match set scr_strat_flag_autoreturn 0 // How many seconds a dropped flag sits on the ground before auto-returning to spawn set scr_strat_objective_points 1 // Set to 1 to show the waypoint/star over the dropped flag or 0 to hide it //******************************* // HQ Public server settings //******************************* // Match Style set scr_hq_timelimit 30 // Time limit. When halftime is enabled, its time limit per half. 0=disabled (minutes) set scr_hq_half_score 0 // Number of score when half-time starts. Has no effect when halftime is disabled. set scr_hq_end_score 500 // Number of score when map ends. 0=ignored set scr_hq_halftime 0 // Do halftime. When 1, scr_hq_timelimit means time per half set scr_hq_strat_time 0 // Time before round starts (sec) (0 - 10, default 5) set scr_hq_respawn_time 0 // Time after wich player is respawned set scr_hq_neutralizing_points 0 set scr_hq_radio_respawn_time 15 set scr_hq_radio_hold_time 100 set scr_hq_restricted_smoke 0 // Get smoke only if radio is take by some team //******************************* // CTF Public server settings //******************************* // Match Style set scr_ctf_timelimit 30 // Time limit. When halftime is enabled, its time limit per half. 0=disabled (minutes) set scr_ctf_half_score 0 // Number of score when half-time starts. Has no effect when halftime is disabled. set scr_ctf_end_score 5 // Number of score when map ends. 0=ignored set scr_ctf_halftime 0 // Do halftime. When 1, scr_ctf_timelimit means time per half set scr_ctf_strat_time 0 // Time before round starts (sec) (0 - 10, default 5) set scr_ctf_respawn_time 10 // Time after wich player is respawned set scr_ctf_flag_autoreturn_time -1 // Flag auto return set scr_ctf_original_mode 1 //******************************* // DOM Public server settings //******************************* set scr_dom_timelimit 30 set scr_dom_scorelimit 500 set scr_dom_point_interval 2 //******************************* // HTF Public server settings //******************************* // Match Style set scr_htf_timelimit 30 // Time limit. When halftime is enabled, its time limit per half. 0=disabled (minutes) set scr_htf_half_score 0 // Number of score when half-time starts. Has no effect when halftime is disabled. set scr_htf_end_score 500 // Number of score when map ends. 0=ignored set scr_htf_halftime 0 // Do halftime. When 1, scr_htf_timelimit means time per half set scr_htf_strat_time 0 // Time before round starts (sec) (0 - 10, default 5) set scr_htf_respawn_time 0 // Time after wich player is respawned set scr_htf_respawn_time_holders 10 // Time after wich player is respawned if their team holds the flag set scr_htf_flag_spawndelay 10 set scr_htf_flag_autoreturn 0 set scr_htf_objective_points 1 //******************************* // RE Public server settings //******************************* // Match Style set scr_re_timelimit 30 // Time limit for map. 0=disabled (minutes) set scr_re_half_round 10 // Number of rounds when half-time starts. 0=ignored set scr_re_half_score 6 // Number of score when half-time starts. 0=ignored set scr_re_end_round 0 // Number of rounds when map ends. 0=ignored set scr_re_end_score 10 // Number of score when map ends. 0=ignored // Round options set scr_re_strat_time 5 // Time before round starts where players cannot move set scr_re_roundlength 2 // Time length of each round (min) set scr_re_end_time 5 // Time et the end of the round (after last player is killed) when you can finding weapons for the next round set scr_re_count_draws 0 // If players are killed at same time - count this round (1) or play new round (0)? set scr_re_sniper_shotgun_info 0 // Show weapon info about sniper and shotgun players //******************************* // Shared gametype script cvars //******************************* // Readyup set scr_readyup 0 // Enable readyup [0, 1] 0 = disbled 1 = enabled set scr_readyup_autoresume_half 1 // Minutes to auto-resume halftime [0 - 10] 0 = disabled set scr_readyup_nadetraining 0 // Enable strat grenade fly mode in readyup set scr_readyup_start_timer 5 // Count-down timer with black screen when all players are ready // Time-out set scr_timeouts 0 // Total timeouts for one team set scr_timeouts_half 5 // How many per side set scr_timeout_length 5 // Length in minutes // Are there OT Rules? set scr_overtime 0 // Hud Options set scr_show_players_left 0 set scr_show_objective_icons 1 set scr_show_hitblip 0 set scr_show_scoreboard 1 //Score in the upper left corner [0 - 1] 0=hided 1=visible (if 1, score can be still hided by player settings) set scr_show_scoreboard_limit 1 // Health Regeneration set scr_allow_health_regen 0 // 0=no regen, 1=refill health after "scr_regen_delay" set scr_allow_regen_sounds 0 set scr_regen_delay 5000 //Time before regen starts set scr_drop_medkit 1 // Ambients set scr_allow_ambient_fog 1 set scr_allow_ambient_sounds 1 set scr_allow_ambient_fire 1 set scr_allow_ambient_weather 1 set scr_allow_shellshock 1 // Create shell shock effect when player is hitted set scr_replace_russian 1 // Replace russians with Americans / Brisith set scr_blackout 0 // If match is in progress, show map background all over the screen and disable sounds for connected player set scr_recording 0 // Starts automatically recording when match starts set scr_matchinfo 2 // Show match info in menu (1 = without team names, 2 = with team names) set scr_map_vote 1 // Open voting system so players can vote about next map set scr_map_vote_replay 1 // Show option to replay this map in voting system set scr_auto_deadchat 1 // Automaticly enable / disable deadchat set scr_remove_killtriggers 1 // Remove some of the kill-triggers created in 1.3 path set scr_bash 0 // Bash mode can be called via menu in readyup set scr_drawfriend 1 set scr_teambalance 0 set scr_spectatefree 0 set scr_spectateenemy 0 set g_allowVote 1 set g_antilag 0 set g_maxDroppedWeapons 32 set sv_fps 20 set sv_minRate 25000 set sv_maxRate 25000 set sv_timeout 60 // Time after 999 player is kicked set com_maxfps 100 set scr_fast_reload_fix 1 // Prevent players from shoting faster via double-scroll bug set scr_shotgun_consistent 1 // Enable consistent shotgun to fix long shot kills and short range hits set scr_prone_peek_fix 1 // Prevent players from doing fast peeks from prone (time, after player can prone again will be increased) set scr_mg_peek_fix 1 // When mg is dropped, player is spawned right behid mg set scr_hitbox_hand_fix 1 // Damage to left hand is adjusted for rifles and scopes. set scr_hitbox_torso_fix 1 // Damage of M1, rifles, scopes and shotgun is adjusted to have less hits in game //******************************* // Weapons //******************************* // Rifles-only mode set scr_rifle_mode 0 // Nade spawn counts for each class set scr_boltaction_nades 1 set scr_semiautomatic_nades 1 set scr_smg_nades 1 set scr_sniper_nades 1 set scr_mg_nades 1 set scr_shotgun_nades 1 // Smoke spawn counts for each class set scr_boltaction_smokes 1 set scr_semiautomatic_smokes 1 set scr_smg_smokes 1 set scr_sniper_smokes 1 set scr_mg_smokes 1 set scr_shotgun_smokes 1 // Weapon Limits by class per team set scr_boltaction_limit 99 set scr_sniper_limit 99 set scr_semiautomatic_limit 99 set scr_smg_limit 99 set scr_mg_limit 99 set scr_shotgun_limit 99 // Allow weapon drop when player die set scr_boltaction_allow_drop 1 set scr_sniper_allow_drop 1 set scr_semiautomatic_allow_drop 1 set scr_smg_allow_drop 1 set scr_mg_allow_drop 1 set scr_shotgun_allow_drop 1 set scr_pistol_allow_drop 1 // Allow grenade / smoke drop when player die set scr_allow_grenade_drop 1 set scr_allow_smoke_drop 0 // Player's weapon drop set scr_allow_primary_drop 1 set scr_allow_secondary_drop 1 // Allow/Disallow Weapons set scr_allow_greasegun 1 set scr_allow_m1carbine 1 set scr_allow_m1garand 1 set scr_allow_springfield 1 set scr_allow_thompson 1 set scr_allow_bar 1 set scr_allow_sten 1 set scr_allow_enfield 1 set scr_allow_enfieldsniper 1 set scr_allow_bren 1 set scr_allow_pps42 1 set scr_allow_nagant 1 set scr_allow_svt40 1 set scr_allow_nagantsniper 1 set scr_allow_ppsh 1 set scr_allow_mp40 1 set scr_allow_kar98k 1 set scr_allow_g43 1 set scr_allow_kar98ksniper 1 set scr_allow_mp44 1 set scr_allow_shotgun 1 set scr_allow_pistols 1 set scr_allow_turrets 1 // Single Shot Kills set scr_no_oneshot_pistol_kills 0 set scr_no_oneshot_ppsh_kills 0 // PPSH Balance - Limits range of PPSH to same as Tommy set scr_balance_ppsh_distance 0 // CVARs that will change the smoke bloom type. 0 being thin original smoke. 1 being the smoke fix (thick smoke). 2 being a new consistent smoke modeled after thick smoke. set scr_smoke_type 2 // CVAR that will enable the shotgun consistent pellet spread. set g_shotgun_spread_fix 1 // CVAR that will convert all neck shots to head shots. set scr_hitbox_neck_kills 1 // CVAR that enables the buffed BAR. Bar has faster reload and fire rate. set scr_bar_buffed 1 // CVR that enables the buffed Bren LMG. Bren LMG has faster reload. set scr_bren_buffed 1 // CVAR that enables the British team in breakout. set scr_breakout_british 0 // Map rotation //set sv_maprotation "map mp_toujane map mp_burgundy map mp_dawnville map mp_matmata map mp_carentan map mp_farmhouse map mp_brecourt map mp_trainstation map mp_leningrad map mp_downtown map mp_railyard map mp_breakout" CFG File.write(CONFIG_FILE, config_content) puts "📝 server.cfg updated, sv_wwwBaseURL = #{public_url}" # 3️⃣ Start COD2 dedicated server +set fs_game "#{MOD}" #cmd = %Q(LD_PRELOAD=./libCoD2x.so "#{COD2_PATH}" +g_gametype #{GAME} +set dedicated 2 +set net_ip #{bind_ip} +set net_port #{PORT_GAME} +set fs_homepath "#{Dir.pwd}" +set fs_referencedList 1 +set sv_referencedIwds 1 +set sv_log_sync 1 +set sv_punkbuster 0 +pb_sv_disable +exec server.cfg +map #{MAP}) cmd = %Q(LD_PRELOAD=./libCoD2x.so "#{COD2_PATH}" +g_gametype #{GAME} +set dedicated 2 +set net_ip #{bind_ip} +set net_port #{PORT_GAME} +set fs_homepath "#{Dir.pwd}" +set sv_log_sync 1 +set sv_punkbuster 0 +pb_sv_disable +exec server.cfg +map #{MAP}) puts "🚀 Запуск сервера Call of Duty 2..." puts "Команда: #{cmd}" send_telegram_notification("🚀 CoD2 сервер запущено\nАдреса: #{public_ip}", silent: false) # Run cod2 server directly (not via system) spawn(cmd) # 4️⃣ Monitor games_mp.log for game events log_file = "./main/games_mp.log" puts "👀 Monitoring #{log_file} for Telegram notifications..." # Wait for log file to be created/accessible sleep 1 until File.exist?(log_file) Thread.new do loop do begin File.open(log_file, "r") do |f| f.seek(0, IO::SEEK_END) # Start at end of file loop do raw_line = f.gets if raw_line begin line = raw_line.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '') # Clean game log prefix if present (e.g. " 5:41 ") clean_line = line.sub(/^\s*\d+:\d+\s+/, "") now = Time.now.to_i # Standard Join Event: Connected;GUID;EntityID;PlayerName if clean_line =~ /^Connected;.*;.*;(.*)/ player_name = escape_tg_markdown($1.strip) # Always add them to the connected list so they are tracked correctly for Match is starting @connected_players[player_name] ||= "spectator" if last_joins[player_name] && now - last_joins[player_name] < JOIN_COOLDOWN puts "⏳ [CONSOLE MONITOR] Cooldown for #{player_name}" else last_joins[player_name] = now puts "📡 [CONSOLE MONITOR] Sending TG for #{player_name}" send_telegram_notification("🎮 #{player_name} в грі") end end # Disconnect Event: Q;GUID;EntityID;PlayerName OR Disconnect;... # We try to catch widespread formats. if clean_line =~ /^(?:Q|Disconnected);.*;.*;(.*)/ pname = escape_tg_markdown($1.strip) @connected_players.delete(pname) end # Join Team Event if clean_line =~ /^J;[^;]+;[^;]+;([^;]+);(.+)/ pname = escape_tg_markdown($1.strip) team = $2.strip @connected_players[pname] = team end # Timeout Event if clean_line =~ /^TIMEOUT_START;([^;]+);([^;]+);(.+)/ team = $1.strip player = escape_tg_markdown($2.strip) duration = $3.strip team_icon = team == "allies" ? "🇺🇸" : (team == "axis" ? "🇩🇪" : "👁️") msg = "⏸ *Тайм-аут!*\n#{team_icon} *#{player}* бере тайм-аут.\nТривалість: `#{duration}` хв або ж по готовності всіх гравців.\n⏩ Щоб продовжити гру швидше, натисніть кнопку *F* в грі" send_telegram_notification(msg, silent: false, parse_mode: 'Markdown') end # Map Init: InitGame: \sv_pure\1\gamename\Call of Duty 2\g_gametype\ctf\mapname\mp_toujane... if clean_line =~ /InitGame: (.*)/ info = $1 if info =~ /\\mapname\\([^\\]+)/ @current_map_name = $1 end if info =~ /\\g_gametype\\([^\\]+)/ @current_gametype = $1 end # Clear players list on new map load to ensure we only track # actually connected players for this new match. # The engine re-emits 'Connected' for all active players right after InitGame. @connected_players.clear end # Match Start if clean_line =~ /Match is starting/ readable_gametype = GAMETYPE_TRANSLATIONS[@current_gametype] || @current_gametype player_count = @connected_players.size # Formulate the message # "Match is starting" usually appears after warmup msg = "🔫 *Матч розпочато!*\n" msg += "🗺 Карта: `#{@current_map_name}`\n" msg += "🎮 Режим: `#{readable_gametype}`\n" if player_count > 0 msg += "👥 Гравців (#{player_count}):\n" @connected_players.each do |p_name, team| team_icon = (team == "allies" ? "🇺🇸" : (team == "axis" ? "🇩🇪" : "👁️")) msg += "#{team_icon} #{p_name}\n" end else msg += "👥 Гравців: 0\n" end msg += "🔗 Результати доступні за посиланням: https://cod2.fly.dev\n" send_telegram_notification(msg, silent: true, parse_mode: 'Markdown') end if clean_line =~ /ExitLevel: executed/ @map_ended_flag = true puts "🏁 [GAME] Map ending detected (ExitLevel)" # Check for Round Statistics elsif clean_line =~ /^ROUND_STATS;([^;]+);([^;]+);([^;]+);([^;]+)/ current_stats = { map: $1, type: $2, allies: $3.strip, axis: $4.strip, players: [] } puts "📊 Stats started for #{current_stats[:map]}" @current_round_stats = current_stats elsif clean_line.start_with?("PLAYER_STATS;") parts = clean_line.split(';') # PLAYER_STATS;Name;Team;Score;Kills;Deaths;Caps;Rets;Plnts;Defs;HCaps;HDets;Drops;Pickups;OwnPickups # Index: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 if @current_round_stats && parts.size >= 6 player_data = { name: parts[1].strip, team: parts[2].strip, score: parts[3].strip, kills: parts[4].strip, deaths: parts[5].strip, caps: parts[6] ? parts[6].strip : "0", rets: parts[7] ? parts[7].strip : "0", plnts: parts[8] ? parts[8].strip : "0", defs: parts[9] ? parts[9].strip : "0", hcaps: parts[10] ? parts[10].strip : "0", hdets: parts[11] ? parts[11].strip : "0", drops: parts[12] ? parts[12].strip : "0", picks: parts[13] ? parts[13].strip : "0", own_picks: parts[14] ? parts[14].strip : "0" } @current_round_stats[:players] << player_data end elsif clean_line =~ /STATS_DONE|AM EndMap/ if @current_round_stats if @map_ended_flag puts "🏁 Final Map Stats detected. Merging with previous data..." send_round_stats_notification(@current_round_stats) puts "📡 Sending merged round stats to Telegram..." @map_ended_flag = false # Reset flag @last_round_stats_data = nil else puts "💾 [STATS] Saving intermediate stats (Pre-ExitLevel)" @last_round_stats_data = @current_round_stats end @current_round_stats = nil end end rescue => e puts "⚠️ Error parsing line, continuing: #{e.message}" end else sleep 1 # Wait for new content end end end rescue => e puts "❌ Log monitor file error: #{e.message}, restarting loop in 5s..." sleep 5 end end end # 5️⃣ Keep WEBrick thread alive server_thread.join