/* todo add interface functions for getting a copy of player_info, getting and setting player data change bulk damage to include weapon */ local debug = true; local dev_present = false; local dev_steamid = "[U:1:83176584]"; local defined_contracts = {}; local contract_whitelist = { "contracts.nut": null, "serverinit.nut": null, "custom_contracts.nut": {"custom_monkey":null, "custom_bison_damage":null, "custom_team_father_son":null, "custom_allclass_deaths":null}, }; ::ROOT <- getroottable(); if (!("ConstantNamingConvention" in ROOT)) { foreach(a, b in Constants) foreach(k, v in b) ROOT[k] <- v != null ? v : 0; } foreach(k, v in ::NetProps.getclass()) if (k != "IsValid" && !(k in ROOT)) ROOT[k] <- ::NetProps[k].bindenv(::NetProps); foreach(k, v in ::Entities.getclass()) if (k != "IsValid" && !(k in ROOT)) ROOT[k] <- ::Entities[k].bindenv(::Entities); // Weapon slots local SLOT_PRIMARY = 0; local SLOT_SECONDARY = 1; local SLOT_MELEE = 2; local SLOT_UTILITY = 3; local SLOT_BUILDING = 4; local SLOT_PDA = 5; local SLOT_PDA2 = 6; local SLOT_COUNT = 7; local tf_objective_resource = FindByClassname(null, "tf_objective_resource"); local tf_mann_vs_machine_stats = FindByClassname(null, "tf_mann_vs_machine_stats"); local tf_player_manager = FindByClassname(null, "tf_player_manager"); local wave_started = false; if (tf_objective_resource) wave_started = !GetPropBool(tf_objective_resource, "m_bMannVsMachineBetweenWaves"); local game_event_callbacks = {}; local fast_write_string = ""; local wave_bonus_count = 0; local end_of_mission = false; local tank_damage = {}; local flag_pickuptimes = {}; local contract_stock_blacklist = { [45]=null,[220]=null,[448]=null,[772]=null,[1078]=null,[1103]=null, [46]=null,[163]=null,[222]=null,[449]=null,[773]=null,[812]=null, [833]=null,[1121]=null,[1145]=null,[44]=null,[317]=null,[325]=null, [349]=null,[355]=null,[450]=null,[452]=null,[648]=null, [127]=null,[228]=null,[237]=null,[414]=null,[441]=null,[730]=null, [1085]=null,[1104]=null,[129]=null,[133]=null,[226]=null,[354]=null, [415]=null,[442]=null,[444]=null,[1001]=null,[1101]=null,[1153]=null, [128]=null,[154]=null,[357]=null,[416]=null,[447]=null,[775]=null, [40]=null,[215]=null,[594]=null,[1146]=null,[1178]=null,[39]=null, [351]=null,[595]=null,[740]=null,[1081]=null, [1179]=null,[1180]=null,[38]=null,[153]=null,[214]=null,[326]=null, [348]=null,[457]=null,[466]=null,[593]=null,[813]=null,[834]=null, [1000]=null,[1181]=null, [308]=null,[405]=null,[608]=null,[996]=null,[1151]=null, [130]=null,[131]=null,[265]=null,[406]=null,[1099]=null,[1144]=null, [1150]=null,[132]=null,[154]=null,[172]=null,[266]=null,[307]=null, [327]=null,[357]=null,[404]=null,[482]=null,[1082]=null, [41]=null,[312]=null,[424]=null,[811]=null,[832]=null,[850]=null, [42]=null,[159]=null,[311]=null,[425]=null,[433]=null,[863]=null, [1002]=null,[1190]=null,[43]=null,[239]=null,[310]=null, [331]=null,[426]=null,[656]=null,[1084]=null,[1100]=null,[1184]=null, [141]=null,[527]=null,[588]=null,[997]=null,[1004]=null, [140]=null,[528]=null,[1086]=null,[30668]=null,[142]=null,[155]=null, [329]=null,[589]=null, [36]=null,[305]=null,[412]=null,[1079]=null,[35]=null,[411]=null, [44]=null,[998]=null,[37]=null,[173]=null,[304]=null,[413]=null, [1003]=null, [56]=null,[230]=null,[402]=null,[526]=null,[752]=null,[1005]=null, [1092]=null,[1098]=null,[57]=null,[58]=null,[231]=null,[642]=null, [751]=null,[1083]=null,[1105]=null,[171]=null,[232]=null,[401]=null, [61]=null,[224]=null,[460]=null,[525]=null,[1006]=null,[810]=null, [831]=null,[225]=null,[356]=null,[461]=null,[574]=null,[649]=null, }; local wearable_weapons = { [133] = SLOT_SECONDARY, // Gunboats [444] = SLOT_SECONDARY, // Mantreads [405] = SLOT_PRIMARY, // Booties [608] = SLOT_PRIMARY, // Bootlegger [131] = SLOT_SECONDARY, // Chargin' Targe [406] = SLOT_SECONDARY, // Splendid Screen [1099] = SLOT_SECONDARY, // Tide Turner [1144] = SLOT_SECONDARY, // Festive Targe [57] = SLOT_SECONDARY, // Razorback [231] = SLOT_SECONDARY, // Danger Shield [642] = SLOT_SECONDARY, // Cozy Camper }; local hostname = null; local savefilename = ""; try { hostname = Convars.GetStr("hostname"); foreach (code in hostname) { if (code < 33 && !endswith(hostname, "_")) { savefilename += "_"; continue; } if (code < 48 || (code > 57 && code < 65) || (code > 90 && code < 97) || code > 122) continue; savefilename += code.tochar(); } savefilename = savefilename.tolower(); } catch (e) {} local popfile_name = null; if (tf_objective_resource) popfile_name = GetPropString(tf_objective_resource, "m_iszMvMPopfileName"); local classes = ["", "scout", "sniper", "soldier", "demo", "medic", "heavy", "pyro", "spy", "engineer"]; local MaxPlayers = MaxClients().tointeger(); local DeepCopy = null; // Need this for recursion DeepCopy = function(target) { local targettype = typeof(target); if (targettype != "table" && targettype != "array") return target; local dest = null; foreach (k, v in target) { if (targettype == "table") { if (!dest) dest = {}; if (typeof(v) == "table" || typeof(v) == "array") dest[k] <- DeepCopy(v); else dest[k] <- v; } // array else { if (!dest) dest = []; if (typeof(v) == "table" || typeof(v) == "array") dest.append(DeepCopy(v)); else dest.append(v); } } if (!dest) return (targettype == "table") ? {} : []; return dest; }; local default_player_table = { player = null, active = false, last_money = null, recent_kills = {}, recent_dmg = 0, allclass_credits = 0, medic_healing = 0, uber_starttime = null, yer_kills = 0, sentry_kill_contract = false, damage_taken_life = 0, heads_collected = 0, stab_chain_starttime = null, stab_chain_target = null, stab_chain_count = 0, mission_contracts = { "allclass_mission_no_deaths": {init={enabled=true}}, "scout_mission_money_all": {init={enabled=true}, tfclass_whitelist=[1]}, "scout_untouchable": {init={enabled=true, deaths=0}, deathlimit=3, tfclass_whitelist=[1]}, "scout_topdmg": {init={enabled=true}, tfclass_whitelist=[1]}, "scout_support": {init={enabled=true}, supportreq=40000, tfclass_whitelist=[1]}, "soldier_support": {init={enabled=true}, supportreq=40000, tfclass_whitelist=[3]}, "soldier_topdmg": {init={enabled=true}, tfclass_whitelist=[3]}, "demo_topdmg_pills": {init={enabled=true}, tfclass_whitelist=[4]}, "demo_topdmg_knight": {init={enabled=true}, tfclass_whitelist=[4]}, "heavy_topdmg": {init={enabled=true}, tfclass_whitelist=[6]}, "engineer_no_building_deaths": {init={enabled=true}, tfclass_whitelist=[9]}, }, wave_contracts = { "scout_wave_money_most": {init={enabled=true}, tfclass_whitelist=[1]}, "engineer_wave_no_canteens": {init={enabled=true}, tfclass_whitelist=[9]}, }, }; local cumulative_contracts = [ "allclass_kill_ubers", "scout_markfordeath", "scout_kill_snipers", "soldier_rjump_kills", "soldier_heal_bbox", "pyro_kill_spies", "pyro_dmg_giant_backburn", "pyro_dmg_phlog", "heavy_kills_kgb", "engineer_dispenser_heal", "engineer_kill_revenge", "engineer_repair_building_remote", "engineer_wrangle_defense", "medic_shield_block_dmg", "medic_shield_kills", "medic_shield_dmg_tank", "sniper_kills", "spy_kill_sapped", "spy_steal_health", "soldier_assists", "pyro_reflect_projs", "pyro_dmg_df", "demo_dmg_crit", "heavy_dmg_giants", "heavy_dmg_giant_scout_nat", "heavy_dmg_critonkill", "soldier_dmg_tank", "scout_dmg_tank", "scout_dmg_soda", "scout_dmg_giants", "pyro_dmg_tank", "engineer_dmg_tank", "sniper_dmg_bow", "allclass_dmg", "allclass_dmg_crit", "demo_dmg_grenades", "spy_dmg_tank", ]; foreach (index, contract in cumulative_contracts) default_player_table[contract] <- 0; foreach (contract, data in default_player_table.mission_contracts) { data.wave <- DeepCopy(data.init); data.mission <- DeepCopy(data.init); } foreach (contract, data in default_player_table.wave_contracts) data.wave <- DeepCopy(data.init); local player_info = {}; // Populate player_info on late load for (local i = 1; i <= MaxPlayers; ++i) { local player = PlayerInstanceFromIndex(i); if (player == null || player.IsBotOfType(1337)) continue; local steamid = GetPropString(player, "m_szNetworkIDString"); local t = DeepCopy(default_player_table); t.player = player; local team = player.GetTeam(); if (team == 2 || team == 3) t.active = true; if (tf_player_manager) { t.allclass_credits = GetPropIntArray(tf_player_manager, "m_iCurrencyCollected", i); t.medic_healing = GetPropIntArray(tf_player_manager, "m_iHealing", i); } player_info[steamid] <- t; } local team_mission_contracts = { "team_comp_usa" : {init={enabled=true}, tfclass_whitelist=[1,3,9]}, "team_comp_international" : {init={enabled=true}, tfclass_whitelist=[2,4,5,6,7,8]}, "team_comp_demoknight" : {init={enabled=true}, tfclass_whitelist=[4]}, "team_lowman_2" : {init={enabled=true}, failtime=null, count=2, difficulty="int"}, "team_lowman_3" : {init={enabled=true}, failtime=null, count=3, difficulty="adv"}, "team_lowman_4" : {init={enabled=true}, failtime=null, count=4, difficulty="exp"}, "team_defense" : {init={enabled=true}}, "team_stock" : {init={enabled=true}}, "team_no_refunds" : {init={enabled=true}}, "team_no_failure" : {init={enabled=true}}, "team_no_buybacks" : {init={enabled=true}}, }; foreach (i, cls in classes) if (cls != "") team_mission_contracts[format("team_comp_%s", cls)] <- {init={enabled=true}, tfclass_whitelist=[i]}; foreach (contract, data in team_mission_contracts) { data.wave <- DeepCopy(data.init); data.mission <- DeepCopy(data.init); } local team_wave_contracts = { }; foreach (contract, data in team_wave_contracts) data.wave <- DeepCopy(data.init); local max_file_size = 16000; // map, mission, difficulty local function GetMissionInfo() { local raw = NetProps.GetPropString(tf_objective_resource, "m_iszMvMPopfileName"); local t = {}; if (raw && startswith(raw, "scripts/population/")) { raw = raw.slice(23); local index = null; foreach (s in ["_int_", "_adv_", "_exp_"]) { index = raw.find(s); if (index != null) { t.difficulty <- s.slice(1, s.len() - 1); break; } } if (index != null) { t.map <- raw.slice(0, index); t.mission <- raw.slice(index + t.difficulty.len() + 2, raw.len() - 4); } } if (t.len()) return t; } local function GetItemInSlot(player, slot) { for (local i = 0; i < SLOT_COUNT; ++i) { local wep = GetPropEntityArray(player, "m_hMyWeapons", i); if ( wep == null || wep.GetSlot() != slot) continue; return wep; } } local function round(num, decimals) { if (!decimals || decimals <= 0) return floor(num + 0.5); else { local mod = pow(10, decimals); return floor((num * mod) + 0.5) / mod; } } local function GetPlayerSupportPoints(i) { local damageassist = GetPropIntArray(tf_player_manager, "m_iDamageAssist", i); local healingassist = GetPropIntArray(tf_player_manager, "m_iHealingAssist", i); local damageblocked = GetPropIntArray(tf_player_manager, "m_iDamageBlocked", i); local bonuspoints = GetPropIntArray(tf_player_manager, "m_iBonusPoints", i); return damageassist + healingassist + damageblocked + (bonuspoints * 25); } local function GetPlayerStatRankings(netprops) { local results = {}; foreach (netprop, func in netprops) results[netprop] <- []; foreach (steamid, info in player_info) { if (!info.active) continue; local player = info.player; foreach (netprop, func in netprops) { local val = (netprop == "_support") ? GetPlayerSupportPoints(player.entindex()) : func(player, netprop); results[netprop].append([player, val]); } } foreach (netprop, list in results) { list.sort(function(first, second) { if (first[1] > second[1]) return -1; else if (second[1] > first[1]) return 1; return 0; }); } // results = { netprop = [[player, val], ...], } return results; } local function GetItemByID(player, id) { for (local i = 0; i < SLOT_COUNT; ++i) { local wep = GetPropEntityArray(player, "m_hMyWeapons", i); if (wep == null) continue; if (id == GetPropInt(wep, "m_AttributeManager.m_Item.m_iItemDefinitionIndex")) return wep; } } local write_entity = FindByName(null, "__contracts_write_entity"); if (!write_entity) write_entity = SpawnEntityFromTable("move_rope", { targetname = "__contracts_write_entity" }); local function WriteContractData(thinker, override_string) { local filename = "contracts.contract"; if (hostname != "Team Fortress") filename = format("contracts_%s.contract", savefilename); local contents = FileToString(filename); local scope = thinker.GetScriptScope(); if ((!override_string || override_string == "") && scope.write_string == "") return; if (debug && dev_present) printl(scope.write_string); // The server should be wiping the contents after parsing the file; a non-empty file indicates it has not been handled // In this case the file is about full and we can't add any more to it // (The real limit is 16384) if (contents && contents.len() >= max_file_size) { return 0.2; // Check again after a short wait } // Write as much of our payload as possible else { local str = (override_string) ? override_string : scope.write_string; local lines = split(str, "\n", true); // Obtain index of first line we are unable to store local index = 0; local filesize = (contents) ? contents.len() : 0; foreach (line in lines) { filesize += line.len()+1; if (filesize > max_file_size) break; ++index; } if (!contents) contents = ""; local partial_write_string = ""; foreach (line in lines.slice(0,index)) partial_write_string += format("%s\n", line); // Write to file and set new write_string if necessary if (partial_write_string != "") { if (str == scope.write_string) { local new_write_string = ""; foreach (line in lines.slice(index)) new_write_string += format("%s\n", line); scope.write_string = new_write_string; } if (debug && !dev_present) return; StringToFile(filename, contents + partial_write_string); } } } // Write think write_entity.ValidateScriptScope(); local script_scope = write_entity.GetScriptScope(); script_scope.write_string <- ""; script_scope.write_interval <- 3.0; script_scope.Think <- function() { foreach (steamid, info in player_info) { if (!info.active) continue; foreach (index, contract in cumulative_contracts) { if (info[contract] > 0) Contracts.AddInterfaceCall("CTI_IncrementPlayerData", steamid, contract, info[contract]); info[contract] = 0; } } local w = WriteContractData(write_entity, null); return (w == null) ? write_interval : w; }; AddThinkToEnt(write_entity, "Think"); local detection_entity = FindByName(null, "__contracts_detection_entity"); if (!detection_entity) detection_entity = SpawnEntityFromTable("move_rope", { targetname = "__contracts_detection_entity" }); local tickrate = 66; detection_entity.ValidateScriptScope(); detection_entity.GetScriptScope().Think <- function() { if (fast_write_string != "") { WriteContractData(write_entity, fast_write_string) fast_write_string = ""; } if (!tf_player_manager) return -1; // Too early local tickcount = Time() / 0.015; // Scout money contracts // The mvm_creditbonus_wave event doesn't apply to the last wave so we need to do this manually if (end_of_mission) { local dropped = GetPropInt(tf_mann_vs_machine_stats, "m_previousWaveStats.nCreditsDropped"); local acquired = GetPropInt(tf_mann_vs_machine_stats, "m_previousWaveStats.nCreditsAcquired"); if (dropped == acquired) { foreach (steamid, info in player_info) if (info.active && info.mission_contracts.scout_mission_money_all.mission.enabled) Contracts.AddInterfaceCall("CTI_IncrementPlayerData", steamid, "scout_mission_money_all", 1); end_of_mission = false; } if (acquired / dropped.tofloat() >= 0.90) { foreach (steamid, info in player_info) { if (info.active && info.wave_contracts.scout_wave_money_most.wave.enabled) { Contracts.AddInterfaceCall("CTI_IncrementPlayerData", steamid, "scout_wave_money_most", 1); info.wave_contracts.scout_wave_money_most.wave.enabled = false; } } } } if (tickcount % 11 == 0) { dev_present = false; foreach (steamid, info in player_info) { if (steamid == dev_steamid) dev_present = true; local playercls = info.player.GetPlayerClass(); // Bulk damage contracts if (info.recent_dmg > 0) { if (playercls == TF_CLASS_SOLDIER && info.recent_dmg >= 750) Contracts.AddInterfaceCall("CTI_IncrementPlayerData", steamid, "soldier_dmg_bulk", 1); else if (playercls == TF_CLASS_DEMOMAN && info.recent_dmg >= 1500) Contracts.AddInterfaceCall("CTI_IncrementPlayerData", steamid, "demo_dmg_bulk", 1); else if (playercls == TF_CLASS_SNIPER && info.recent_dmg >= 750) Contracts.AddInterfaceCall("CTI_IncrementPlayerData", steamid, "sniper_dmg_bulk", 1); } info.recent_dmg = 0; if (info.stab_chain_starttime && Time() >= info.stab_chain_starttime + 4.0) { info.stab_chain_starttime = null; info.stab_chain_target = null; info.stab_chain_count = 0; } if (playercls == TF_CLASS_SPY) { if (info.stab_chain_count >= 5) { info.stab_chain_count = 0; Contracts.AddInterfaceCall("CTI_IncrementPlayerData", steamid, "spy_chain_stab_giant", 1); } if (!info.player.InCond(TF_COND_DISGUISED)) info.yer_kills = 0; if (info.yer_kills >= 5) { Contracts.AddInterfaceCall("CTI_IncrementPlayerData", steamid, "spy_yer_disguise", 1); info.yer_kills = 0; } } else if (playercls == TF_CLASS_MEDIC) { local wep = GetItemInSlot(info.player, SLOT_SECONDARY); if (wep && wep.GetClassname() == "tf_weapon_medigun") { local charge_active = GetPropBool(wep, "m_bChargeRelease") if (charge_active && !info.uber_starttime) info.uber_starttime = Time(); else if (!charge_active && info.uber_starttime) { if (Time() >= info.uber_starttime + 20.0) Contracts.AddInterfaceCall("CTI_IncrementPlayerData", steamid, "medic_uber_extended", 1); info.uber_starttime = null; } } } else if (playercls == TF_CLASS_DEMOMAN && info.active && info.player.IsAlive()) { local heads = GetPropInt(info.player, "m_Shared.m_iDecapitations"); if (heads < 4) info.heads_collected = heads; else if (info.heads_collected != null) { Contracts.AddInterfaceCall("CTI_IncrementPlayerData", steamid, "demo_collect_heads", 1); info.heads_collected = null; } } } } if (tickcount % 198 == 0) // 3 sec { local human_count = 0; foreach (steamid, info in player_info) { local player = info.player; if (!player || !player.IsValid()) continue; local player_class = player.GetPlayerClass(); local money_collected = GetPropIntArray(tf_player_manager, "m_iCurrencyCollected", player.entindex()); local dif = money_collected - info.allclass_credits; if (dif > 0) Contracts.AddInterfaceCall("CTI_IncrementPlayerData", steamid, "allclass_credits", dif); info.allclass_credits = money_collected; local healing = GetPropIntArray(tf_player_manager, "m_iHealing", player.entindex()); dif = healing - info.medic_healing; if (dif > 0 && player_class == TF_CLASS_MEDIC) Contracts.AddInterfaceCall("CTI_IncrementPlayerData", steamid, "medic_heal", dif); info.medic_healing = healing; if (!info.active) continue; ++human_count // Bulk kill Contracts // todo remove the tick based part of this, follow what recent_dmg does and just clear it every 11 ticks foreach (tick, weps in info.recent_kills) { foreach (wepid, classes in weps) { foreach (tfclass, kills in classes) { if (player_class == TF_CLASS_PYRO && tfclass == TF_CLASS_MEDIC && kills >= 2 && wepid == 593) Contracts.AddInterfaceCall("CTI_IncrementPlayerData", steamid, "pyro_chainkill_medics", round(kills / 2, 0)); else if (player_class == TF_CLASS_DEMOMAN && kills >= 5) { local wep = GetItemByID(player, wepid); if (wep && wep.GetClassname() == "tf_weapon_pipebomblauncher") Contracts.AddInterfaceCall("CTI_IncrementPlayerData", steamid, "demo_kill_bulk_detonate", round(kills / 5, 0)); } else if (player_class == TF_CLASS_SNIPER) { if (tfclass == TF_CLASS_MEDIC && kills >= 2) Contracts.AddInterfaceCall("CTI_IncrementPlayerData", steamid, "sniper_kill_medics_bulk", round(kills / 2, 0)); else if (kills >= 3) Contracts.AddInterfaceCall("CTI_IncrementPlayerData", steamid, "sniper_kills_bulk", round(kills / 3, 0)); } } } } info.recent_kills = {}; if (!wave_started) continue; // Team comp mission contracts foreach (contract, d in team_mission_contracts) { if (!d.wave.enabled || !d.mission.enabled) continue; if ("tfclass_whitelist" in d && d.tfclass_whitelist.find(player.GetPlayerClass()) == null) { d.wave.enabled = false; continue; } } // Loadout contracts // ...Weapons local stock_contract = team_mission_contracts.team_stock; local pills_contract = info.mission_contracts.demo_topdmg_pills; for (local i = 0; i < SLOT_UTILITY; ++i) { local wpn = GetPropEntityArray(player, "m_hMyWeapons", i); if (wpn == null) continue local cls = wpn.GetClassname(); if (!wpn.IsMeleeWeapon() && cls != "tf_weapon_parachute_primary") { team_mission_contracts.team_comp_demoknight.wave.enabled = false; info.mission_contracts.demo_topdmg_knight.wave.enabled = false; } local id = GetPropInt(wpn, "m_AttributeManager.m_Item.m_iItemDefinitionIndex"); if (id in contract_stock_blacklist) stock_contract.wave.enabled = false; if (cls == "tf_weapon_pipebomblauncher" || cls == "tf_weapon_parachute_primary") pills_contract.wave.enabled = false; } // ...Wearable weapons for (local child = player.FirstMoveChild(); child != null; child = child.NextMovePeer()) { local id = GetPropInt(child, "m_AttributeManager.m_Item.m_iItemDefinitionIndex"); if (id in wearable_weapons) { stock_contract.wave.enabled = false; if (child.GetClassname() != "tf_wearable_demoshield") pills_contract.wave.enabled = false; } } // Personal contracts foreach (contract, d in info.mission_contracts) { if (!d.mission.enabled || !d.wave.enabled) continue; if ("tfclass_whitelist" in d && d.tfclass_whitelist.find(player.GetPlayerClass()) == null) d.wave.enabled = false; } foreach (contract, d in info.wave_contracts) { if (!d.wave.enabled) continue; if ("tfclass_whitelist" in d && d.tfclass_whitelist.find(player.GetPlayerClass()) == null) d.wave.enabled = false; } // check attribute resistances local attributes = ["dmg taken from fire reduced", "dmg taken from crit reduced", "dmg taken from blast reduced", "dmg taken from bullets reduced"] foreach (attr in attributes) if (player.GetCustomAttribute(attr, 1.0) != 1.0) info.mission_contracts.scout_untouchable.wave.enabled = false; } // Team low-man contracts if (!wave_started || human_count == 0) return -1; foreach (contract, data in team_mission_contracts) { if (!data.wave.enabled || !data.mission.enabled) continue; if (!("difficulty" in data)) continue; local difficulty = GetMissionInfo(); if (difficulty) difficulty = difficulty.difficulty; // lol if (difficulty != data.difficulty) { data.wave.enabled = false; continue; } if (human_count <= data.count) { if (data.failtime != null) data.failtime = null; } else { if (data.failtime != null) { if (Time() >= data.failtime) data.wave.enabled = false; } else data.failtime = Time() + 15; } } } return -1; }; AddThinkToEnt(detection_entity, "Think"); local function ValidateCaller(callinfo) { // Do not allow anonymous callers if (!callinfo || !endswith(callinfo.src, ".nut")) return false; // Do not allow untrustworthy source files if (!(callinfo.src in contract_whitelist)) return false; return true; } local function ExecuteGameEventCallbacks(params, pre=true) { local callinfo = getstackinfos(2); local event = callinfo.func; if (!(event in game_event_callbacks)) return; local key = (pre) ? "pre" : "post"; if (key in game_event_callbacks[event]) { local callback = game_event_callbacks[event][key]; try { callback(params) } catch (e) { printl(e) } } } ::Contracts <- { // Note: any events that happen BEFORE InitWaveOutput will not work properly // E.g. the first player spawning into the server on map load will be missed function AddGameEventCallback(event, callback, pre=true) { local key = (pre) ? "pre" : "post"; if (!(event in game_event_callbacks)) game_event_callbacks[event] <- {}; game_event_callbacks[event][key] <- callback; }, function IsWaveStarted() { return wave_started; }, function GetPlayerInfo() { return DeepCopy(player_info); }, function DefineContract(columns, debug=false) { local c = columns; if (typeof(c) != "table" || !c.len()) return; local callinfo = getstackinfos(2); if (!ValidateCaller(callinfo)) return; if (!("contract_id" in c) || typeof(c.contract_id) != "string" || !startswith(c.contract_id, "custom_")) return; if (!("name" in c) || !("description" in c)) return; if (typeof(c.name) != "string" || c.name == "") return; if (typeof(c.description) != "string" || c.description == "") return; local allowed_contracts = contract_whitelist[callinfo.src]; if (allowed_contracts && !(c.contract_id in allowed_contracts)) return; local default_values = { completion_value = 1, overflow = 0, difficulty = 3, category = "personal", tfclass = "NULL", type = null, }; foreach (k, v in default_values) if (!(k in c)) c[k] <- v; if (typeof(c.completion_value) != "integer" || c.completion_value < 0) c.completion_value = default_values.completion_value; if (c.overflow != 0 && c.overflow != 1) c.overflow = default_values.overflow; if (typeof(c.difficulty) != "integer" || c.difficulty < 1 || c.difficulty > 6) c.difficulty = default_values.difficulty; if (typeof(c.category) != "string" || !(c.category in {"personal":null, "team":null})) c.category = default_values.category; if (typeof(c.tfclass) != "string" || (!classes.find(c.tfclass) && c.tfclass != "civilian" && c.tfclass != "allclass")) c.tfclass = default_values.tfclass; foreach (key in ["contract_id", "name", "description", "category", "tfclass"]) if (c[key].find(";") != null || c[key].find(",") != null) return; local info = GetMissionInfo(); if (!info) return; if (!("type" in c) || !(["", null, "mission", "wave", "cumulative"].find(c.type))) return; if (c.type == "cumulative" && c.category != "personal") return; if (c.type == "cumulative") { default_player_table[c.contract_id] <- 0; foreach (steamid, info in player_info) info[c.contract_id] <- 0; cumulative_contracts.append(c.contract_id); } defined_contracts[c.contract_id] <- { category = c.category type = c.type }; AddInterfaceCall("CTI_DefineCustomContract", c.contract_id, c.completion_value, c.overflow, c.difficulty, 0, c.name, c.description, c.category, c.tfclass, format("%s_%s", info.difficulty, info.mission), 0); if (debug) printl("Accepted contract: " + c.contract_id); }, function AddContractData(contract_id, init_data=null, misc_data=null) { local callinfo = getstackinfos(2); if (!ValidateCaller(callinfo)) return; if (typeof(contract_id) != "string" || !startswith(contract_id, "custom_")) return; if (!(contract_id in defined_contracts)) return; local type = defined_contracts[contract_id].type; local category = defined_contracts[contract_id].category; local allowed_contracts = contract_whitelist[callinfo.src]; if (allowed_contracts && !(contract_id in allowed_contracts)) return; // These don't need to be handled if (type == "cumulative" || !type) return; ///////// End Validation ///////// local t = {}; if (typeof(misc_data) == "table") t = DeepCopy(misc_data); t.init <- (typeof(init_data) == "table") ? DeepCopy(init_data) : {enabled=true}; if (!("enabled" in t.init)) t.init.enabled <- true; if (type == "mission") { t.wave <- DeepCopy(t.init); t.mission <- DeepCopy(t.init); } else t.wave <- DeepCopy(t.init); if (category == "team") type == "mission" ? team_mission_contracts[contract_id] <- t : team_wave_contracts[contract_id] <- t; else { local s = (type == "mission") ? "mission_contracts" : "wave_contracts"; default_player_table[s][contract_id] <- t; foreach (steamid, info in player_info) info[s][contract_id] <- t; } }, function GetContractData(contract_id, player=null) { local callinfo = getstackinfos(2); if (!ValidateCaller(callinfo)) return; if (typeof(contract_id) != "string" || !startswith(contract_id, "custom_")) return; local type = defined_contracts[contract_id].type; local category = defined_contracts[contract_id].category; local allowed_contracts = contract_whitelist[callinfo.src]; if (allowed_contracts && !(contract_id in allowed_contracts)) return; if (category == "personal" && (!player || !player.IsValid())) return; ///////// End Validation ///////// if (category == "team") return type == "mission" ? team_mission_contracts[contract_id] : team_wave_contracts[contract_id]; local s = (type == "mission") ? "mission_contracts" : "wave_contracts"; foreach (steamid, info in player_info) { if (info.player != player) continue; if (contract_id in info[s]) return info[s][contract_id]; } }, function IncrementCumulativeContract(contract_id, player, val) { local callinfo = getstackinfos(2); if (!ValidateCaller(callinfo)) return; if (typeof(contract_id) != "string" || !startswith(contract_id, "custom_")) return; local allowed_contracts = contract_whitelist[callinfo.src]; if (allowed_contracts && !(contract_id in allowed_contracts)) return; foreach (steamid, info in player_info) if (info.player == player && contract_id in info) info[contract_id] += val; }, function AddInterfaceCall(func_name, ...) { if (!func_name || typeof(func_name) != "string") return; local callinfo = getstackinfos(2); if (!ValidateCaller(callinfo)) return; try { local arg_string = ""; { foreach (i, v in vargv) { v = v.tostring(); arg_string += (i < vargv.len() - 1) ? format("%s,", v) : format("%s", v); } } if (func_name == "CTI_IncrementPlayerData") { if (vargv.len() != 3) return; // Creators may only modify their own custom contracts local allowed_contracts = contract_whitelist[callinfo.src]; local contract_id = vargv[1]; if (allowed_contracts && !(contract_id in allowed_contracts)) return; } else if (func_name == "CTI_PlayerJoinedServer") { // We already handle this, creators may not use it if (callinfo.src != "contracts.nut") return; } else if (func_name == "CTI_DefineCustomContract") { // Creators may not use this directly instead of DefineContract if (callinfo.src != "contracts.nut") return; } else if (func_name == "CTI_FetchScriptFileOwner") { // Miscellaneous for StringToFile validation in serverinit if (callinfo.src != "serverinit.nut") return; fast_write_string += format("%s,%s\n", func_name, arg_string); return; } script_scope.write_string += format("%s,%s\n", func_name, arg_string); } catch (err) printl(err); }, // Mission change, wave fail, wave jump function OnGameEvent_teamplay_round_start(params) { ExecuteGameEventCallbacks(params); try { tank_damage = {}; tf_objective_resource = FindByClassname(null, "tf_objective_resource"); tf_mann_vs_machine_stats = FindByClassname(null, "tf_mann_vs_machine_stats"); tf_player_manager = FindByClassname(null, "tf_player_manager"); local wavenum = GetPropInt(tf_objective_resource, "m_nMannVsMachineWaveCount"); if (wavenum == 1) { end_of_mission = false; game_event_callbacks = {}; } // Reset contracts // ..Team mission foreach (contract, data in team_mission_contracts) { if (wavenum == 1) { data.mission <- DeepCopy(data.init); data.wave <- DeepCopy(data.init); } else data.wave <- DeepCopy(data.mission); if ("failtime" in data) data.failtime = null; } // ..Team wave foreach (contract, data in team_wave_contracts) data.wave <- DeepCopy(data.init); // ..Personal foreach (steamid, info in player_info) { info.damage_taken_life = 0; info.heads_collected = 0; // mission foreach (contract, data in info.mission_contracts) { if (wavenum == 1) { data.mission <- DeepCopy(data.init); data.wave <- DeepCopy(data.init); } else data.wave <- DeepCopy(data.mission); } // wave foreach (contract, data in info.wave_contracts) { // This is special since we need to handle it on wave begin if (contract == "scout_wave_money_most" && wavenum > 1) continue; data.wave <- DeepCopy(data.init); } } } catch (e) { if (e) throw e } ExecuteGameEventCallbacks(params, false); }, function OnGameEvent_mvm_wave_complete(params) { ExecuteGameEventCallbacks(params); try { wave_started = false; wave_bonus_count = 0; tank_damage = {}; // Lock in wave data of mission contracts by setting mission data // ..Team foreach (contract, data in team_mission_contracts) { if (!data.mission.enabled) continue; data.mission <- DeepCopy(data.wave); } // ..Personal foreach (steamid, info in player_info) { foreach (contract, data in info.mission_contracts) { if (!data.mission.enabled) continue; data.mission <- DeepCopy(data.wave); } } // Send completed wave contracts to server // ..Team foreach (contract, data in team_wave_contracts) { if (data.wave.enabled) foreach (steamid, info in player_info) if (info.active) AddInterfaceCall("CTI_IncrementPlayerData", steamid, contract, 1); data.wave <- DeepCopy(data.init); } // ..Personal foreach (steamid, info in player_info) { if (info.active) AddInterfaceCall("CTI_IncrementPlayerData", steamid, "allclass_complete_waves", 1); foreach (contract, data in info.wave_contracts) { // This is special since we need to handle it on wave begin if (contract == "scout_wave_money_most") continue; if (info.active && data.wave.enabled) AddInterfaceCall("CTI_IncrementPlayerData", steamid, contract, 1); data.wave <- DeepCopy(data.init); } } } catch (e) { if (e) throw e } ExecuteGameEventCallbacks(params, false); }, function OnGameEvent_mvm_mission_complete(params) { ExecuteGameEventCallbacks(params); try { end_of_mission = true; // Mission stat contracts // stats = { netprop = [[player, val], ...] } local stats = GetPlayerStatRankings({"m_Shared.tfsharedlocaldata.m_ScoreData.m_iDamageDone": GetPropInt, "_support": null}); foreach (netprop, results in stats) { if (!results || !results.len()) continue; if (netprop == "m_Shared.tfsharedlocaldata.m_ScoreData.m_iDamageDone") { local topdmg_player = results[0][0]; local steamid = GetPropString(topdmg_player, "m_szNetworkIDString"); local data = player_info[steamid]; foreach (contract in ["scout_topdmg", "soldier_topdmg", "demo_topdmg_pills", "demo_topdmg_knight"]) { local table = data.mission_contracts[contract]; if (table.mission.enabled) AddInterfaceCall("CTI_IncrementPlayerData", steamid, contract, 1); } try { local topdmg = results[0][1]; local nextdmg = results[1][1]; if (topdmg >= nextdmg * 1.5 && topdmg_player.GetPlayerClass() == TF_CLASS_HEAVYWEAPONS) AddInterfaceCall("CTI_IncrementPlayerData", steamid, "heavy_topdmg", 1); } catch (e) {} } else if (netprop == "_support") { foreach (list in results) { local player = list[0]; local val = list[1]; local steamid = GetPropString(player, "m_szNetworkIDString"); local data = player_info[steamid]; foreach (contract in ["scout_support", "soldier_support"]) { local table = data.mission_contracts[contract]; if (table.mission.enabled) if (val >= table.supportreq) AddInterfaceCall("CTI_IncrementPlayerData", steamid, contract, 1); table.mission.enabled = false; } } } } // Send completed mission contracts to server // ..Team foreach (contract, data in team_mission_contracts) { if (!data.mission.enabled) continue; foreach (steamid, info in player_info) if (info.active) AddInterfaceCall("CTI_IncrementPlayerData", steamid, contract, 1); } // ..Personal local ignore = { "scout_mission_money_all":null, "scout_support":null, "soldier_support":null, "scout_topdmg":null, "soldier_topdmg":null, "demo_topdmg_pills":null, "demo_topdmg_knight":null, "heavy_topdmg":null, }; foreach (steamid, info in player_info) { if (!info.active) continue; foreach (contract, data in info.mission_contracts) { if (!data.mission.enabled) continue; if (contract in ignore) continue; AddInterfaceCall("CTI_IncrementPlayerData", steamid, contract, 1); } } } catch (e) { if (e) throw e } ExecuteGameEventCallbacks(params, false); }, // These don't need custom pre/post callbacks function OnGameEvent_player_activate(params) { local player = GetPlayerFromUserID(params.userid); if (!player || player.IsBotOfType(1337)) return; local steamid = GetPropString(player, "m_szNetworkIDString"); if (!startswith(steamid, "[U:")) return; if (steamid == dev_steamid) dev_present = true; AddInterfaceCall("CTI_PlayerJoinedServer", steamid); if (!(steamid in player_info)) player_info[steamid] <- DeepCopy(default_player_table); player_info[steamid].player = player; // If we're joining after the first wave invalidate all our mission contracts local wavenum = GetPropInt(tf_objective_resource, "m_nMannVsMachineWaveCount"); if (wavenum > 1) { foreach (contract, data in player_info[steamid].mission_contracts) { data.wave.enabled = false; data.mission.enabled = false; } } }, function OnGameEvent_player_disconnect(params) { if (params.bot) return; local steamid = params.networkid; if (steamid in player_info) delete player_info[steamid]; }, // ---- function OnGameEvent_player_team(params) { ExecuteGameEventCallbacks(params); try { local player = GetPlayerFromUserID(params.userid); if (!player || player.IsBotOfType(1337) || params.disconnect) throw null; local steamid = GetPropString(player, "m_szNetworkIDString"); local data = player_info[steamid]; if (params.team == 2 || params.team == 3) { // Prevent players from sitting in spec and joining in at the end of a wave if (!data.active && wave_started) foreach (contract, d in data.wave_contracts) d.wave.enabled = false; data.active = true; } else data.active = false; } catch (e) { if (e) throw e } ExecuteGameEventCallbacks(params, false); } function OnGameEvent_post_inventory_application(params) { ExecuteGameEventCallbacks(params); try { local player = GetPlayerFromUserID(params.userid); if (!player || player.IsBotOfType(1337)) throw null; local wavenum = GetPropInt(tf_objective_resource, "m_nMannVsMachineWaveCount"); if (wavenum == 1) throw null; local money = player.GetCurrency(); local steamid = GetPropString(player, "m_szNetworkIDString"); local data = player_info[steamid]; // todo look at m_nRespecsAwardedInWave in tf_mann_vs_machine_stats to replace this? if (!team_mission_contracts.team_no_refunds.wave.enabled) { data.last_money = money; throw null; } if (data.last_money == null) data.last_money = money; else if (!wave_started && data.last_money < money) team_mission_contracts.team_no_refunds.wave.enabled = false; data.last_money = money; } catch (e) { if (e) throw e } ExecuteGameEventCallbacks(params, false); }, function OnGameEvent_npc_hurt(params) { ExecuteGameEventCallbacks(params); try { local victim = EntIndexToHScript(params.entindex); local player = GetPlayerFromUserID(params.attacker_player); if (!victim || !player || !player.IsValid()) throw null; local victimcls = victim.GetClassname(); if (victimcls == "obj_sentrygun" && GetPropBool(victim, "m_bPlayerControlled")) { local builder = GetPropEntity(victim, "m_hBuilder"); if (!builder || builder.IsBotOfType(1337)) throw null; local steamid = GetPropString(builder, "m_szNetworkIDString"); local old_dmg = params.damageamount * 3.0; local dmg_resist = old_dmg - params.damageamount; local data = player_info[steamid]; data.engineer_wrangle_defense += dmg_resist; } else if (victimcls == "tank_boss" && !player.IsBotOfType(1337)) { local steamid = GetPropString(player, "m_szNetworkIDString"); if (!(victim in tank_damage)) tank_damage[victim] <- {}; local killed = false; local damage = params.damageamount; // Unlike in player_hurt, we can see the tank's health before it dies from damage if (damage > params.health) { damage = params.health; killed = true; } local data = player_info[steamid]; local playercls = player.GetPlayerClass(); local wep = player.GetActiveWeapon(); local id = GetPropInt(wep, "m_AttributeManager.m_Item.m_iItemDefinitionIndex"); local weptype = ("weaponid" in params) ? params.weaponid : null; // TF_WEAPON_MEDIGUN if (playercls == TF_CLASS_MEDIC && weptype == 50) { data.medic_shield_dmg_tank += damage; } // Air strike else if (playercls == TF_CLASS_SOLDIER && weptype == 22) { if (id == 1104 && params.crit && player.InCond(TF_COND_BLASTJUMPING)) data.soldier_dmg_tank += damage; } else if (playercls == TF_CLASS_SCOUT) { data.scout_dmg_tank += damage; if (id == 448 && weptype == 76) data.scout_dmg_soda += damage; } else if (playercls == TF_CLASS_PYRO) { data.pyro_dmg_tank += damage; } // Widowmaker else if (playercls == TF_CLASS_ENGINEER) { if (id == 527 && weptype == 12) data.engineer_dmg_tank += damage; } // Diamondback else if (playercls == TF_CLASS_SPY) { if (id == 525 && weptype == 43 && params.crit) data.spy_dmg_tank += damage; } local table = tank_damage[victim]; if (steamid in table) table[steamid] <- table[steamid] + damage; else table[steamid] <- damage; if (killed) { local winner = null; local max = 0; foreach (s, damage in table) { if (damage > max) { max = damage; winner = s; // steamid } } // Chicken dinner if (winner) AddInterfaceCall("CTI_IncrementPlayerData", winner, "allclass_tank_buster", 1); delete tank_damage[victim]; } } } catch (e) { if (e) throw e } ExecuteGameEventCallbacks(params, false); }, function OnGameEvent_player_hurt(params) { ExecuteGameEventCallbacks(params); try { local victim = GetPlayerFromUserID(params.userid.tointeger()); local attacker = GetPlayerFromUserID(params.attacker.tointeger()); if (!victim || !attacker || !attacker.IsPlayer()) throw null; local is_giant = (victim.IsMiniBoss() && victim.GetMaxHealth() > 1000); local wep = GetItemInSlot(attacker, SLOT_PRIMARY); local id = GetPropInt(wep, "m_AttributeManager.m_Item.m_iItemDefinitionIndex"); local weptype = ("weaponid" in params) ? params.weaponid.tointeger() : null; local custom = params.custom.tointeger(); local crit = params.crit.tointeger(); local minicrit = params.minicrit.tointeger(); local dmg = params.damageamount.tointeger(); // We use this instead of params.health because params.health is already 0 when this runs // and it makes it easier to get how much damage we actually did local health = victim.GetHealth(); if (health < 0) dmg += health; if (victim.IsBotOfType(1337) && !attacker.IsBotOfType(1337)) { local steamid = GetPropString(attacker, "m_szNetworkIDString"); local data = player_info[steamid]; data.recent_dmg += dmg; data.allclass_dmg += dmg; if (crit) data.allclass_dmg_crit += dmg; local attackercls = attacker.GetPlayerClass(); if (attackercls == TF_CLASS_SCOUT) { if (is_giant) data.scout_dmg_giants += dmg; if (custom == TF_DMG_CUSTOM_BASEBALL) { // Yes I know it's based on travel time but this is much faster and simpler local dist = (victim.GetOrigin() - attacker.GetOrigin()).Length(); if (dist >= 1250.0) AddInterfaceCall("CTI_IncrementPlayerData", steamid, "scout_longshot", 1); } else if (id == 448 && weptype == 76) { data.scout_dmg_soda += dmg; } } else if (attackercls == TF_CLASS_PYRO) { // Phlog if (id == 594 && weptype == 25) { if (attacker.InCond(TF_COND_CRITBOOSTED_RAGE_BUFF) && crit) data.pyro_dmg_phlog += dmg; } // Dragon's Fury else if (id == 1178 && weptype == 109) { data.pyro_dmg_df += dmg; } // Backburner crits else if (!attacker.IsCritBoosted() && crit && is_giant) { if (id == 40 || id == 1146) data.pyro_dmg_giant_backburn += dmg; } } else if (attackercls == TF_CLASS_DEMOMAN) { if (crit) data.demo_dmg_crit += dmg; if (weptype == 23 && wep && wep.GetClassname() == "tf_weapon_grenadelauncher") data.demo_dmg_grenades += dmg; } else if (attackercls == TF_CLASS_HEAVYWEAPONS) { if (is_giant) { data.heavy_dmg_giants += dmg; // Natascha if (victim.GetPlayerClass() == TF_CLASS_SCOUT && id == 41) data.heavy_dmg_giant_scout_nat += dmg; } if (attacker.InCond(TF_COND_CRITBOOSTED_ON_KILL) && crit) data.heavy_dmg_critonkill += dmg; } else if (attackercls == TF_CLASS_SNIPER && wep && wep.GetClassname() == "tf_weapon_compound_bow") { data.sniper_dmg_bow += dmg; } else if (attackercls == TF_CLASS_SPY && custom == TF_DMG_CUSTOM_BACKSTAB && is_giant) { if (data.stab_chain_starttime == null) data.stab_chain_starttime = Time(); if (data.stab_chain_target != victim) { data.stab_chain_target = victim; data.stab_chain_count = 1; } else ++data.stab_chain_count; } } else if (!victim.IsBotOfType(1337) && attacker.IsBotOfType(1337)) { if (victim.GetPlayerClass() == TF_CLASS_HEAVYWEAPONS && health > 0) { local steamid = GetPropString(victim, "m_szNetworkIDString"); local data = player_info[steamid]; if (data.damage_taken_life != null) { data.damage_taken_life += dmg; if (data.damage_taken_life >= 5000) { data.damage_taken_life = null; AddInterfaceCall("CTI_IncrementPlayerData", steamid, "heavy_hurt_life", 1); } } } } } catch (e) { if (e) throw e } ExecuteGameEventCallbacks(params, false); }, function OnScriptHook_OnTakeDamage(params) { ExecuteGameEventCallbacks(params); try { local victim = params.const_entity; local attacker = params.attacker; local weapon = params.weapon; if (!victim || !attacker || !victim.IsPlayer() || !attacker.IsPlayer() || !weapon) throw null; // This is in OnTakeDamage instead of player_hurt because we need to check TF_COND_MARKEDFORDEATH if (victim.IsPlayer() && victim.IsBotOfType(1337) && attacker.IsPlayer() && !attacker.IsBotOfType(1337)) { local attackercls = attacker.GetPlayerClass(); if (attackercls == TF_CLASS_SCOUT && weapon && weapon.GetAttribute("mark for death", 0) && !victim.InCond(TF_COND_MARKEDFORDEATH)) { local steamid = GetPropString(attacker, "m_szNetworkIDString"); local data = player_info[steamid]; ++data.scout_markfordeath; } } } catch (e) { if (e) throw e } ExecuteGameEventCallbacks(params, false); }, function OnGameEvent_mvm_bomb_alarm_triggered(params) { team_mission_contracts.team_defense.wave.enabled = false; }, function OnGameEvent_teamplay_flag_event(params) { ExecuteGameEventCallbacks(params); try { // Pick up if (params.eventtype == 1) { local carrier = PlayerInstanceFromIndex(params.player); if (!carrier) throw null; local flag = GetPropEntity(carrier, "m_hItem"); flag_pickuptimes[flag] <- { pickup=Time(), carrier=carrier }; } // Defense else if (params.eventtype == 3) { foreach (flag, data in flag_pickuptimes) { local carrier = PlayerInstanceFromIndex(params.carrier); if (carrier == data.carrier) { local defender = PlayerInstanceFromIndex(params.player); if (!defender || defender.IsBotOfType(1337)) break; if (defender.GetPlayerClass() == TF_CLASS_DEMOMAN && Time() <= data.pickup + 2.0) { local steamid = GetPropString(defender, "m_szNetworkIDString"); AddInterfaceCall("CTI_IncrementPlayerData", steamid, "demo_bomb_guard", 1); } break; } } } } catch (e) { if (e) throw e } ExecuteGameEventCallbacks(params, false); }, function OnGameEvent_player_used_powerup_bottle(params) { ExecuteGameEventCallbacks(params); try { if (!wave_started) throw null; local player = PlayerInstanceFromIndex(params.player); if (!player || player.IsBotOfType(1337)) throw null; local steamid = GetPropString(player, "m_szNetworkIDString"); local data = player_info[steamid]; // Building upgrade if (params.type == 5) data.wave_contracts.engineer_wave_no_canteens.wave.enabled = false; } catch (e) { if (e) throw e } ExecuteGameEventCallbacks(params, false); }, function OnGameEvent_object_destroyed(params) { ExecuteGameEventCallbacks(params); try { if (!wave_started || !("userid" in params)) throw null; local player = GetPlayerFromUserID(params.userid); if (!player || player.IsBotOfType(1337) || player.GetPlayerClass() != TF_CLASS_ENGINEER) throw null; local object = EntIndexToHScript(params.index); if (!object || object.GetClassname() == "obj_attachment_sapper") throw null; local steamid = GetPropString(player, "m_szNetworkIDString"); local data = player_info[steamid]; data.mission_contracts.engineer_no_building_deaths.wave.enabled = false; } catch (e) { if (e) throw e } ExecuteGameEventCallbacks(params, false); }, function OnGameEvent_building_healed(params) { ExecuteGameEventCallbacks(params); try { if (!wave_started) throw null; local player = PlayerInstanceFromIndex(params.healer); if (!player || player.IsBotOfType(1337)) throw null; // I hate to check active weapon here since you can always switch away // before the bolt reaches the dispenser but weapon is not provided in params local wep = player.GetActiveWeapon(); if (!wep || wep.GetClassname() != "tf_weapon_shotgun_building_rescue") throw null; local building = EntIndexToHScript(params.building); if (!building || GetPropEntity(building, "m_hBuilder") != player) throw null; local steamid = GetPropString(player, "m_szNetworkIDString"); local data = player_info[steamid]; data.engineer_repair_building_remote += params.amount; } catch (e) { if (e) throw e } ExecuteGameEventCallbacks(params, false); }, function OnGameEvent_player_death(params) { ExecuteGameEventCallbacks(params); try { // Rafmod bug with reverse where userid is passed as string local victim = GetPlayerFromUserID(params.userid.tointeger()); if (victim.IsBotOfType(1337)) { local attacker = GetPlayerFromUserID(params.attacker.tointeger()); if (!attacker || !attacker.IsPlayer() || attacker.IsBotOfType(1337)) throw null; local id = params.weapon_def_index.tointeger(); local is_giant = (victim.IsMiniBoss() && victim.GetMaxHealth() > 1000); local victimcls = victim.GetPlayerClass(); local attackercls = attacker.GetPlayerClass(); local steamid = GetPropString(attacker, "m_szNetworkIDString"); local data = player_info[steamid]; // Bulk kill contracts local time = Time() if (!(time in data.recent_kills)) data.recent_kills[time] <- {}; if (!(id in data.recent_kills[time])) data.recent_kills[time][id] <- {}; if (!(victimcls in data.recent_kills[time][id])) data.recent_kills[time][id][victimcls] <- 1; else ++data.recent_kills[time][id][victimcls]; if (victimcls == TF_CLASS_MEDIC) { local wep = GetItemInSlot(victim, SLOT_SECONDARY); local id = GetPropInt(wep, "m_AttributeManager.m_Item.m_iItemDefinitionIndex"); if (wep && id != 35 && id != 411 && id != 998) { local charge = GetPropFloat(wep, "NonLocalTFWeaponMedigunData.m_flChargeLevel"); if (charge >= 1.0) ++data.allclass_kill_ubers; } } else if (victimcls == TF_CLASS_SNIPER && attackercls == TF_CLASS_SCOUT) ++data.scout_kill_snipers; else if (victimcls == TF_CLASS_SPY && attackercls == TF_CLASS_PYRO) ++data.pyro_kill_spies; if (attackercls == TF_CLASS_SOLDIER && attacker.InCond(TF_COND_BLASTJUMPING)) ++data.soldier_rjump_kills; else if (attackercls == TF_CLASS_HEAVYWEAPONS) { if (id == 43) ++data.heavy_kills_kgb; } else if (attackercls == TF_CLASS_ENGINEER && params.customkill.tointeger() == TF_DMG_CUSTOM_SHOTGUN_REVENGE_CRIT) ++data.engineer_kill_revenge; else if (attackercls == TF_CLASS_MEDIC && params.customkill.tointeger() == TF_DMG_CUSTOM_PLASMA) ++data.medic_shield_kills; else if (attackercls == TF_CLASS_SNIPER) { ++data.sniper_kills; if (is_giant) { AddInterfaceCall("CTI_IncrementPlayerData", steamid, "sniper_kill_giants", 1); if (victimcls == TF_CLASS_MEDIC) AddInterfaceCall("CTI_IncrementPlayerData", steamid, "sniper_kill_giant_medics", 1); } } else if (attackercls == TF_CLASS_SPY) { if (is_giant) { AddInterfaceCall("CTI_IncrementPlayerData", steamid, "spy_kill_giants", 1); if (victimcls == TF_CLASS_MEDIC) AddInterfaceCall("CTI_IncrementPlayerData", steamid, "spy_kill_giant_medics", 1); } if (victim.InCond(TF_COND_SAPPED)) ++data.spy_kill_sapped; // Your Eternal Reward if (id == 225 || id == 574) ++data.yer_kills; } local assister = GetPlayerFromUserID(params.assister.tointeger()); if (assister && assister.IsPlayer() && !assister.IsBotOfType(1337) && assister.GetPlayerClass() == TF_CLASS_SOLDIER) { local ass_id = GetPropString(assister, "m_szNetworkIDString"); local ass_data = player_info[ass_id]; ++ass_data.soldier_assists; } // Sentry gun local inflictor = EntIndexToHScript(params.inflictor_entindex.tointeger()); if (!inflictor || inflictor.GetClassname() != "obj_sentrygun" || attackercls != TF_CLASS_ENGINEER) throw null; local kills = GetPropInt(inflictor, "SentrygunLocalData.m_iKills"); if (kills < 50 && data.sentry_kill_contract) data.sentry_kill_contract = false; else if (kills >= 50 && !data.sentry_kill_contract) { data.sentry_kill_contract = true; AddInterfaceCall("CTI_IncrementPlayerData", steamid, "engineer_sentry_masskills", 1); } } else { if (!wave_started) throw null; local steamid = GetPropString(victim, "m_szNetworkIDString"); local data = player_info[steamid]; data.damage_taken_life = 0; data.heads_collected = 0; data.mission_contracts.allclass_mission_no_deaths.wave.enabled = false; local contract = data.mission_contracts.scout_untouchable; ++contract.wave.deaths; if (contract.wave.deaths > contract.deathlimit) contract.wave.enabled = false; } } catch (e) { if (e) throw e } ExecuteGameEventCallbacks(params, false); }, function OnGameEvent_deploy_buff_banner(params) { local player = GetPlayerFromUserID(params.buff_owner); if (!player || player.IsBotOfType(1337) || player.GetPlayerClass() != TF_CLASS_SOLDIER) return; local type = params.buff_type; if (type < 1 || type > 3) return; local steamid = GetPropString(player, "m_szNetworkIDString"); local banners = ["buff", "batts", "conch"]; AddInterfaceCall("CTI_IncrementPlayerData", steamid, format("soldier_deploy_%s", banners[type-1]), 1); }, function OnGameEvent_object_deflected(params) { local player = GetPlayerFromUserID(params.userid); if (!player || player.IsBotOfType(1337) || player.GetPlayerClass() != TF_CLASS_PYRO) return; local object = EntIndexToHScript(params.object_entindex); if (object && !object.IsPlayer() && startswith(object.GetClassname(), "tf_projectile")) { local steamid = GetPropString(player, "m_szNetworkIDString"); local data = player_info[steamid]; ++data.pyro_reflect_projs; } }, function OnGameEvent_mvm_bomb_reset_by_player(params) { local player = PlayerInstanceFromIndex(params.player); if (!player || player.IsBotOfType(1337) || player.GetPlayerClass() != TF_CLASS_PYRO) return; local steamid = GetPropString(player, "m_szNetworkIDString"); AddInterfaceCall("CTI_IncrementPlayerData", steamid, "pyro_reset_bomb", 1); }, function OnGameEvent_revive_player_complete(params) { local player = PlayerInstanceFromIndex(params.entindex); if (!player || player.IsBotOfType(1337) || player.GetPlayerClass() != TF_CLASS_MEDIC) return; local steamid = GetPropString(player, "m_szNetworkIDString"); AddInterfaceCall("CTI_IncrementPlayerData", steamid, "medic_revive_teammates", 1); }, function OnGameEvent_player_chargedeployed(params) { local player = GetPlayerFromUserID(params.userid); if (!player || player.IsBotOfType(1337) || player.GetPlayerClass() != TF_CLASS_MEDIC) return; local steamid = GetPropString(player, "m_szNetworkIDString"); local data = player_info[steamid]; local wep = GetItemInSlot(player, SLOT_SECONDARY); local id = GetPropInt(wep, "m_AttributeManager.m_Item.m_iItemDefinitionIndex"); // No Kritzkrieg if (id == 35) return; AddInterfaceCall("CTI_IncrementPlayerData", steamid, "medic_deploy_ubers", 1); }, function OnGameEvent_medigun_shield_blocked_damage(params) { local player = GetPlayerFromUserID(params.userid); if (!player || player.IsBotOfType(1337) || player.GetPlayerClass() != TF_CLASS_MEDIC) return; local steamid = GetPropString(player, "m_szNetworkIDString"); local data = player_info[steamid]; data.medic_shield_block_dmg += params.damage; }, function OnGameEvent_player_healonhit(params) { ExecuteGameEventCallbacks(params); try { local player = PlayerInstanceFromIndex(params.entindex); if (!player || player.IsBotOfType(1337)) throw null; local steamid = GetPropString(player, "m_szNetworkIDString"); local data = player_info[steamid]; local id = params.weapon_def_index; // Black Box if (id == 228 || id == 1085) data.soldier_heal_bbox += params.amount; // ID doesn't get set to kunai for some reason local wep = player.GetActiveWeapon(); local wepid = GetPropInt(wep, "m_AttributeManager.m_Item.m_iItemDefinitionIndex"); if (id == 65535 && wepid == 356) data.spy_steal_health += params.amount; } catch (e) { if (e) throw e } ExecuteGameEventCallbacks(params, false); }, function OnGameEvent_player_healed(params) { ExecuteGameEventCallbacks(params); try { local patient = GetPlayerFromUserID(params.patient); local healer = GetPlayerFromUserID(params.healer); if (!patient || !healer || !patient.IsPlayer() || !healer.IsPlayer()) throw null; if (!healer.IsBotOfType(1337) && patient.GetTeam() == healer.GetTeam()) { local healercls = healer.GetPlayerClass(); local steamid = GetPropString(healer, "m_szNetworkIDString"); local data = player_info[steamid]; if (healercls == TF_CLASS_HEAVYWEAPONS) { local secondary = GetItemInSlot(healer, SLOT_SECONDARY); if (secondary && secondary.GetClassname() == "tf_weapon_lunchbox") AddInterfaceCall("CTI_IncrementPlayerData", steamid, "heavy_heal_team", params.amount); } else if (healercls == TF_CLASS_ENGINEER) data.engineer_dispenser_heal += params.amount; } } catch (e) { if (e) throw e } ExecuteGameEventCallbacks(params, false); }, function OnGameEvent_player_buyback(params) { local player = PlayerInstanceFromIndex(params.player); if (!player || player.IsBotOfType(1337)) return; team_mission_contracts.team_no_buybacks.wave.enabled = false; }, function OnGameEvent_mvm_begin_wave(params) { wave_started = true; local wavenum = GetPropInt(tf_objective_resource, "m_nMannVsMachineWaveCount"); if (wavenum == 1) return; if (wave_bonus_count < 2) foreach (steamid, info in player_info) info.mission_contracts.scout_mission_money_all.mission.enabled = false; if (wave_bonus_count > 0) { foreach (steamid, info in player_info) { // Contract completion for previous wave if (info.active && info.wave_contracts.scout_wave_money_most.wave.enabled) AddInterfaceCall("CTI_IncrementPlayerData", steamid, "scout_wave_money_most", 1); // Reset for this wave info.wave_contracts.scout_wave_money_most.wave.enabled = true; } } }, function OnGameEvent_mvm_creditbonus_wave(params) { ++wave_bonus_count; }, function OnGameEvent_mvm_wave_failed(params) { wave_started = false team_mission_contracts.team_no_failure.mission.enabled = false; }, }; __CollectGameEventCallbacks(Contracts); ::CT_Debug_Join <- function() { foreach (steamid, info in player_info) { Contracts.AddInterfaceCall("CTI_PlayerJoinedServer", steamid); } } ::CT_Debug_Contracts <- function() { foreach (contract, data in team_mission_contracts) { printl(format("%s - wave - %s", contract, data.wave.enabled.tostring())) printl(format("%s - mission - %s", contract, data.mission.enabled.tostring())) } }