// Set of client commands to collect and format feedback for judging // ----------------------------------------------------------------- /* Commands ----------------------------------------- !fb_whitelist [target] !fb_whitelist_toggle !fb (wave) (feedback) !fbg (feedback) !fb_list [wave] !fb_clear (target) [wave] [index] !fb_clear_last !fb_nuke !fb_compile */ // todo make function to get string representation of table for putting into savefile local tf_objective_resource = Entities.FindByClassname(null, "tf_objective_resource"); local MaxPlayers = MaxClients().tointeger(); local classes = ["", "Scout", "Sniper", "Soldier", "Demo", "Medic", "Heavy", "Pyro", "Spy", "Engineer", "Civilian"]; // These are local to keep them private and prevent other scripts from meddling local whitelist_enabled = false; // SteamID3 (compared against m_szNetworkIDString) local whitelist = { "[U:1:28266263]" : "Braindawg", // ----------------------------------------- "[U:1:64599352]" : "Rafradek", // ----------------------------------------- "[U:1:340368223]" : "Lite", // Rurin "[U:1:126071843]" : "Seelpit", // ----------------------------------------- "[U:1:401162912]" : "Conga Dispenser", "[U:1:362716168]" : "Damno", "[U:1:74595670]" : "DrCactus", "[U:1:112896383]" : "Lemonee", "[U:1:83176584]" : "Mince", "[U:1:453975608]" : "Mudun", "[U:1:96114934]" : "Randomguy", "[U:1:875137829]" : "Royal", "[U:1:1086491858]" : "Sergeant Table", "[U:1:285143208]" : "Skin King", "[U:1:86356942]" : "Washy", "[U:1:55891323]" : "gettysburg", "[U:1:295552082]" : "flurbury", "[U:1:99590462]" : "UltimentM", }; local whitespace = {[9]=null, [10]=null, [11]=null, [12]=null, [13]=null, [32]=null}; local punctuation = {[33]=null, [44]=null, [46]=null, [63]=null}; // . , ? ! local max_waves = 32; /* list of wave tables containing steam ids that map to feedback lists [ { [steamid] = [ "feedback 1", "feedback 2", "feedback 3" ] } ] */ local player_wave_feedback = []; local player_general_feedback = {}; local finishing_class_lineup = {}; local demo_name = ""; ::PotatoFeedback <- { TARGETFLAGS_NOSELF = 1 << 0, TARGETFLAGS_NOMULTIPLE = 1 << 1, TARGETFLAGS_NOBOTS = 1 << 2, SECONDS_IN_DAY = 86400, SECONDS_IN_YEAR = 31536000, SECONDS_IN_LEAP_YEAR = 31622400, MONTH_DAYS = [null, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], MONTH_DAYS_LEAP = [null, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], EPOCH = { year = 1970, month = 1, day = 1, hour = 0, minute = 0, second = 0, }, function localTimeToTimestamp(time) { local timestamp = 0; // Years for (local i = EPOCH.year; i < time.year; ++i) timestamp += ((i % 4 == 0) ? SECONDS_IN_LEAP_YEAR : SECONDS_IN_YEAR); // Months for (local i = EPOCH.month; i < time.month; ++i) { if (time.year % 4 == 0) timestamp += (MONTH_DAYS_LEAP[i] * SECONDS_IN_DAY); else timestamp += (MONTH_DAYS[i] * SECONDS_IN_DAY); } // Days timestamp += ((time.day - EPOCH.day) * SECONDS_IN_DAY); // The rest timestamp += (time.hour * 3600) + (time.minute * 60) + time.second; return timestamp; }, // todo refactor if necessary and make use of this in parseCommand, they both pull from the same code, // this one just handles brackets as well (probably just add a bool param to see if we handle brackets or not) // todo also see if i can make this logic cleaner function parseSimpleKV(str) { if (!str || str.len() == "") return; local table = {}; local table_path = [table]; local start = null; local in_str = false; local key = null; local val = null; local strlen = str.len(); for (local i = 0; i < strlen; ++i) { local ch = str[i]; if (ch in whitespace) { if (start != null) { if (in_str) continue; // End of token local token = str.slice(start, i); if (key != null) val = token; else key = token; start = null; } } else { if (start == null) { if (ch != 123 && ch != 125) start = i; if (ch == 34) // " { in_str = true; } else if (ch == 123) // { { if (key == null) return; local t = {}; table_path.top()[key] <- t; table_path.append(t); key = null; } else if (ch == 125) // } { if (key != null) return; table_path.pop(); } } else { // End of string if (ch == 34) // " { in_str = false; local token = str.slice(start+1, i); if (key != null) val = token; else key = token; start = null; } else if (ch == 123 && !in_str) // { { if (key != null) return; key = str.slice(start, i); local t = {}; table_path.top()[key] <- t; table_path.append(t); start = null; key = null; } else if (ch == 125 && !in_str) // } { if (key == null) return; val = str.slice(start, i); table_path.top()[key] <- val; table_path.pop(); start = null; key = null; val = null; } } } // Add slot and reset if (key != null && val != null) { table_path.top()[key] <- val; start = null; key = null; val = null; } // Ensure the last slot at EOF is added if (i == str.len() - 1 && key != null && start != null) { table_path.top()[key] <- str.slice(start); break; } } return table; }, function saveFeedbackToFiles() { local charlimit = 15000; // Down from 16384 just to be safe StringToFile("_feedbackgeneral.log", ""); local write = ""; write += format("Demo \"%s\"\n", demo_name); write += "General\n{\n"; foreach (steamid, feedback in player_general_feedback) { if (!feedback.len()) continue; write += format("\t\"%s\"\n\t{\n", steamid); foreach (i, fb in feedback) write += format("\t\tfb%d \"%s\"\n", i, fb); write += "\t}\n"; } write += "}\n"; write += "Classes\n{\n"; foreach (steamid, classname in finishing_class_lineup) { if (classname == "") continue; write += format("\t\"%s\" \"%s\"\n", steamid, classname); } write += "}"; if (write.len() > charlimit) { ClientPrint(null, 2, format("[FB] ERROR: General feedback too large to store in file (%d) characters), printing to console instead", write.len())); foreach (p in resolveTargetString("@humans")) { ClientPrint(p, 3, "[FB] See console for output"); printToConsoleFragmented(p, write); } return; } StringToFile("_feedbackgeneral.log", write); foreach (index, table in player_wave_feedback) { StringToFile(format("_feedbackwave%d.log", index+1), ""); if (!table.len()) continue; local write = format("wave%d\n{\n", index+1); foreach (steamid, feedback in table) { if (!feedback.len()) continue; write += format("\t\"%s\"\n\t{\n", steamid); foreach (i, fb in feedback) write += format("\t\tfb%d \"%s\"\n", i, fb); write += "\t}\n"; } write += "}"; if (write.len() > charlimit) { ClientPrint(null, 2, format("[FB] ERROR: Wave %d feedback too large to store in file (%d) characters), printing to console instead", index+1, write.len())); foreach (p in resolveTargetString("@humans")) { ClientPrint(p, 3, "[FB] See console for output"); printToConsoleFragmented(p, write); } return; } StringToFile(format("_feedbackwave%d.log", index+1), write); } }, function parseFeedbackFromFiles() { local found_feedback = false; local raw = FileToString("_feedbackgeneral.log"); if (raw && raw.len()) { local table = parseSimpleKV(raw); if (table && "General" in table) { found_feedback = true; foreach (steamid, feedback in table.General) { local arr = []; for (local i = 0; i < feedback.len(); ++i) { local key = format("fb%d", i); if (key in feedback) arr.append(feedback[key]); } table.General[steamid] <- arr; } player_general_feedback = table.General; } if (table && "Classes" in table) { finishing_class_lineup = table.Classes; } } for (local i = 1; i < max_waves + 1; ++i) { local raw = FileToString(format("_feedbackwave%d.log", i)); if (!raw || !raw.len()) continue local table = parseSimpleKV(raw); local wavekey = format("wave%d", i); if (table && wavekey in table) { found_feedback = true; foreach (steamid, feedback in table[wavekey]) { local arr = []; for (local i = 0; i < feedback.len(); ++i) { local key = format("fb%d", i); if (key in feedback) arr.append(feedback[key]); } table[wavekey][steamid] <- arr; } // Todo maybe make adding to the feedback table a util function since we use this code in !fb as well // If there's a gap between the wave specified and our storage then fill it in the array if (player_wave_feedback.len() < i) { local diff = i - player_wave_feedback.len(); if (diff > 1) for (local j = 1; j < diff; ++j) player_wave_feedback.append( {} ); player_wave_feedback.append( table[wavekey] ); } } } if (found_feedback) ClientPrint(null, 3, "[FB] Loaded saved feedback from server, type !fb_nuke to clear"); else ClientPrint(null, 3, "[FB] No saved feedback found on server, proceeding normally"); }, function parseCommand(string, cmdstart="!", strchar='`') { local cmd = { start = null, name = null, args = [], error = null } if (string == cmdstart) return cmd; // Make sure our string actually starts with cmdstart if (typeof(cmdstart) == "string" && !startswith(string, cmdstart)) return cmd; else if (typeof(cmdstart) == "array") { local found = false; foreach (s in cmdstart) { if (startswith(string, s)) { found = true; cmdstart = s; break; } } if (!found) return cmd; } cmd.start = cmdstart; // Get rid of cmdstart from string if (cmdstart) string = string.slice(cmdstart.len()); // Parse tokens local tokens = []; local in_str = false; local start = null local strlen = string.len(); for (local i = 0; i < strlen; ++i) { local ch = string[i]; if (ch in whitespace) { if (start != null) { if (in_str) continue; // End of token tokens.append(string.slice(start, i)); start = null; } } else { if (start == null) { start = i; if (ch == strchar) in_str = true; } else { if (ch == strchar) { in_str = false; tokens.append(string.slice(start+1, i)); start = null; } } } // Ensure we detect the last token if (i == string.len() - 1 && start != null) { if (in_str) { cmd.error <- "[CMD] Invalid arguments: String token was not closed before EOL."; return cmd; } tokens.append(string.slice(start)); break; } } cmd.name <- tokens[0]; if (tokens.len() > 1) cmd.args <- tokens.slice(1); return cmd; }, function handleArgs(player, cmd, argformat) { // Collect amount of required args local required_args = 0; foreach (arg in argformat) if ("required" in arg && arg.required) ++required_args; local arglen = cmd.args.len(); local formatlen = null; // We don't care about going over length if our last arg is a vararg local last = argformat.top(); if (!("vararg" in last) || !last.vararg) formatlen = argformat.len(); // Check number of args in cmd if (arglen < required_args || (formatlen && (arglen > formatlen))) { local output = format("[CMD] Usage: !%s", cmd.name); foreach (arg in argformat) { if ("required" in arg && arg.required) output += format(" (%s)", arg.name); else output += format(" [%s]", arg.name); } return output; // Caller handles error msg display } foreach (index, arg in argformat) { local cmparg = null; if (index < arglen) cmparg = cmd.args[index]; if (cmparg != null) { // Check type if (!("type" in arg)) arg.type <- "string"; try { switch (arg.type) { case "integer": cmd.args[index] = cmparg.tointeger(); break; case "float": cmd.args[index] = cmparg.tofloat(); break; } } catch (err) { return format("[CMD] Invalid type for argument <%s>, expected <%s>", arg.name, arg.type); } // Target type is a special case if (arg.type == "target") { local targets = resolveTargetString(cmparg, player); if (!targets || !targets.len()) return "[CMD] Could not find a valid target"; else { if ("flags" in arg) { if ((arg.flags & TARGETFLAGS_NOSELF) && targets.find(player) != null) return "[CMD] Command does not support targeting yourself"; else if ((arg.flags & TARGETFLAGS_NOMULTIPLE) && targets.len() > 1) return "[CMD] Command does not support multiple targets"; else if ((arg.flags & TARGETFLAGS_NOBOTS)) { foreach (t in targets) if (t.IsBotOfType(1337)) return "[CMD] Command does not support targeting bots"; } } cmd.args[index] = targets; } } // Update this with the new typed value cmparg = cmd.args[index]; // Check bounds if (arg.type == "integer" || arg.type == "float") { local f = (arg.type == "integer") ? "%d" : "%.2f" if ("min_value" in arg && cmparg < arg.min_value) return format("[CMD] Argument <%s> below minimum value <" + f + ">", arg.name, arg.min_value); if ("max_value" in arg && cmparg > arg.max_value) return format("[CMD] Argument <%s> above maximum value <" + f + ">", arg.name, arg.max_value); } } else { cmd.args.append(null); } } }, function printToConsoleFragmented(player, string) { local charlimit = 200; if (string.len() < charlimit) { ClientPrint(player, 2, string); return; } // We use these in order as delimiters; rather than splitting the string exactly into 200 characters, // we go as far as we can go and then check these to get a split point below 200 local last_newline = null local last_punctuation = null local last_whitespace = null local start = 0; local strlen = string.len(); for (local i = 0; i < strlen; ++i) { local ch = string[i]; if (i == start + charlimit || i == strlen - 1) { local end = i; if (i != strlen - 1) { if (last_newline) end = last_newline; else if (last_punctuation) end = last_punctuation; else if (last_whitespace) end = last_whitespace; } end += 1; ClientPrint(player, 2, string.slice(start, end)); start = end; last_newline = null last_punctuation = null last_whitespace = null } if (ch == '\n') last_newline = i; else if (ch in punctuation) last_punctuation = i; else if (ch in whitespace) last_whitespace = i; } }, function getAllPlayers(filter=null) { local players = [] for (local i = 1; i <= MaxPlayers; ++i) { local player = PlayerInstanceFromIndex(i) if (player == null) continue if (filter) { if (filter(player)) players.append(player); } else players.append(player) } return players; }, function getPlayerFromSteamID(steamid) { local player = resolveTargetString(format("#%s", steamid)); if (player && player.len()) return player[0]; } function resolveTargetString(string, player=null) { switch (string) { case "@all": return getAllPlayers(); case "@humans": return getAllPlayers( @(p) !p.IsBotOfType(1337) ); case "@bots": return getAllPlayers( @(p) p.IsBotOfType(1337) ); case "@alive": return getAllPlayers( @(p) NetProps.GetPropInt(p, "m_lifeState") == 0 ); case "@dead": return getAllPlayers( @(p) NetProps.GetPropInt(p, "m_lifeState") != 0 ); case "@aim": if (NetProps.GetPropInt(player, "m_lifeState") != 0) return [] local eyepos = player.EyePosition(); local trace = { start = eyepos, end = eyepos + player.EyeAngles().Forward() * 8192, mask = 33554433, // CONTENTS_SOLID|CONTENTS_MONSTER ignore = player, }; TraceLineEx(trace); if (trace.enthit && trace.enthit.IsPlayer()) return [trace.enthit]; else return []; case "@me": return [player] case "@!me": return getAllPlayers( @(p) p != player ); case "@red": return getAllPlayers( @(p) p.GetTeam() == 2 ); case "@blue": return getAllPlayers( @(p) p.GetTeam() == 3 ); default: if (startswith(string, "#[U:")) { local steamid = string.slice(1); for (local i = 1; i <= MaxPlayers; ++i) { local p = PlayerInstanceFromIndex(i) if (p == null) continue if (steamid == NetProps.GetPropString(p, "m_szNetworkIDString")) return [p]; } } else if (startswith(string, "#")) { local userid = null; try { userid = string.slice(1).tointeger(); } catch (err) {} if (userid) { local player = GetPlayerFromUserID(userid); if (player) return [player]; } else { local name = string.slice(1); for (local i = 1; i <= MaxPlayers; ++i) { local p = PlayerInstanceFromIndex(i) if (p == null) continue if (name == NetProps.GetPropString(p, "m_szNetname")) return [p]; } } } else { local t = null for (local i = 1; i <= MaxPlayers; ++i) { local p = PlayerInstanceFromIndex(i) if (p == null) continue local n = NetProps.GetPropString(p, "m_szNetname"); if (startswith(n, string)) { if (t == null) t = p; else return []; } } if (t != null) return [t]; } return []; } }, function joinArray(arr) { local output = ""; foreach (index, elem in arr) { output += elem; if (index != arr.len() - 1) output += " "; } return output; }, function OnGameEvent_player_say(params) { local player = GetPlayerFromUserID(params.userid); if (!player) return; local steamid = NetProps.GetPropString(player, "m_szNetworkIDString"); if (whitelist_enabled && !(steamid in whitelist)) return; local cmd = parseCommand(params.text); if (cmd.error) { ClientPrint(player, 3, cmd.error); return; } if (!cmd || !cmd.name) return; switch (cmd.name) { // Print the whitelist in console, or optionally add a target to the whitelist // Usage: !fb_whitelist [target] case "fb_whitelist": local err = handleArgs(player, cmd, [{name="target",type="target",flags=7}]); if (err) { ClientPrint(player, 3, err); break; } local targets = cmd.args[0]; if (targets) { local target = targets[0]; steamid = NetProps.GetPropString(target, "m_szNetworkIDString"); local name = NetProps.GetPropString(target, "m_szNetname"); whitelist[steamid] <- name; ClientPrint(player, 3, format("[CMD] Added %s to the whitelist", name)); } else { ClientPrint(player, 3, "[CMD] See console for output"); local string = "\nFEEDBACK WHITELIST\n------------------\n"; foreach (steamid, name in whitelist) string += format("\t%s %s\n", name, steamid); printToConsoleFragmented(player, string) } break; // Toggle whether the whitelist is enabled or not // Usage: !fb_whitelist_toggle case "fb_whitelist_toggle": whitelist_enabled = !whitelist_enabled; ClientPrint(player, 3, format("[CMD] Turned %s the whitelist", (whitelist_enabled ? "on" : "off"))); break; // Give some feedback for a specific wave // Usage: !fb [wave] (feedback) case "fb": local err = handleArgs(player, cmd, [{name="feedback",required=true,vararg=true}]); if (err) { ClientPrint(player, 3, err); break; } local wave = cmd.args[0]; local feedback = ""; try { // We handle this manually to allow wave to be optional wave = wave.tointeger() if (wave < 1) { ClientPrint(player, 3, "[CMD] Argument below minimum value <1>"); break; } else if (wave > max_waves) { ClientPrint(player, 3, format("[CMD] Argument above maximum value %d", max_waves)); break; } feedback = joinArray(cmd.args.slice(1)); } catch (err) { feedback = joinArray(cmd.args); local current_wave = NetProps.GetPropInt(tf_objective_resource, "m_nMannVsMachineWaveCount"); if (GetRoundState() != 4 && current_wave > 1) wave = current_wave - 1; else wave = current_wave; } // If there's a gap between the wave specified and our storage then fill it in the array // E.g. user specified wave 5 but we only have 3 waves stored, we need to add an empty wave 4 if (player_wave_feedback.len() < wave) { local diff = wave - player_wave_feedback.len(); if (diff > 1) for (local i = 1; i < diff; ++i) player_wave_feedback.append( {} ); player_wave_feedback.append( {[steamid] = [feedback]} ); } else { local table = player_wave_feedback[wave-1]; if (steamid in table) player_wave_feedback[wave-1][steamid].append(feedback); else table[steamid] <- [feedback]; } ClientPrint(player, 3, format("[CMD] Added your feedback to wave %d", wave)); break; // Give some general feedback for the mission // Usage: !fb (feedback) case "fbg": // check hello there ;) local err = handleArgs(player, cmd, [{name="feedback",required=true,vararg=true}]); if (err) { ClientPrint(player, 3, err); break; } local feedback = joinArray(cmd.args); if (steamid in player_general_feedback) player_general_feedback[steamid].append(feedback); else player_general_feedback[steamid] <- [feedback]; ClientPrint(player, 3, "[CMD] Added your general feedback"); break; // Print the feedback you've given for the mission in console, optionally limited to specific wave // Usage: !fb_list [wave] case "fb_list": local err = handleArgs(player, cmd, [{name="wave",type="integer",min_value=1,max_value=max_waves}]); if (err) { ClientPrint(player, 3, err); break; } local output = "\nFEEDBACK LIST\n--------------\n"; local wave = cmd.args[0]; if (wave != null) { local table = player_wave_feedback[wave-1]; if (wave > player_wave_feedback.len() || !(steamid in table) || !table[steamid].len()) { ClientPrint(player, 3, format("[CMD] No feedback stored for wave %d", wave)); break; } else { output += format("\tWave %d:\n", wave); foreach (index, feedback in player_wave_feedback[wave-1][steamid]) { output += format("\t\t%d - %s\n", index, feedback); } } } else { local no_feedback = !(steamid in player_general_feedback && player_general_feedback[steamid].len()); if (no_feedback) { foreach (index, table in player_wave_feedback) { if (steamid in table && table[steamid].len()) { no_feedback = false; break; } } } if (no_feedback) { ClientPrint(player, 3, "[CMD] No feedback stored for this mission"); break; } else { local general_feedback = ""; if (steamid in player_general_feedback) { foreach (i, feedback in player_general_feedback[steamid]) { general_feedback += format("\t\t%d - %s\n", i, feedback); } } if (general_feedback != "") { output += "\tGeneral feedback:\n"; output += general_feedback; } foreach (index, table in player_wave_feedback) { if (!(steamid in table) || !table[steamid].len()) continue; output += format("\tWave %d:\n", index+1); foreach (i, feedback in table[steamid]) { output += format("\t\t%d - %s\n", i, feedback); } } } } ClientPrint(player, 3, "[CMD] See console for output"); printToConsoleFragmented(player, output); break; // Clears the target's feedback, optionally for a specific wave or index // To obtain a feedback index, use fb_list // To clear general feedback, provide wave value of 0 // Note: This command's use AGAINST OTHERS is limited to users within the whitelist, regardless of the value of whitelist_enabled // Usage: !fb_clear (target) [wave] [index] case "fb_clear": local err = handleArgs(player, cmd, [{name="target",type="target",required=true,flags=TARGETFLAGS_NOBOTS}, {name="wave",type="integer",min_value=0,max_value=max_waves}, {name="index",type="integer",min_value=0} ]); if (err) { ClientPrint(player, 3, err); break; } local targets = cmd.args[0]; local wave = cmd.args[1]; local index = cmd.args[2]; // Don't proceed if we're trying to clear other people's feedback and we aren't in the whitelist if (targets.len() > 1 || targets.len() == 1 && targets[0] != player) { if (!(steamid in whitelist)) { ClientPrint(player, 3, "[CMD] You are not allowed to clear other people's feedback"); break; } } foreach (t in targets) { local steamid = NetProps.GetPropString(t, "m_szNetworkIDString"); // Clear a specific wave if (wave != null && wave != 0) { local table = player_wave_feedback[wave-1]; if (steamid in table) { if (index != null) { if (table[steamid].len() > index) table[steamid].remove(index); } else delete table[steamid]; } } // Clear everything else if (wave == null) { if (steamid in player_general_feedback) { if (index != null) { if (player_general_feedback[steamid].len() > index) player_general_feedback[steamid].remove(index); } else delete player_general_feedback[steamid]; } foreach (i, table in player_wave_feedback) { if (!(steamid in table) || !table[steamid].len()) continue; if (index != null) { if (table[steamid].len() > index) table[steamid].remove(index); } else delete table[steamid]; } } // Clear general feedback else if (wave == 0) { if (steamid in player_general_feedback) { if (index != null) { if (player_general_feedback[steamid].len() > index) player_general_feedback[steamid].remove(index); } else delete player_general_feedback[steamid]; } } } if (wave != null && wave != 0) ClientPrint(player, 3, format("[CMD] Cleared feedback for wave %d", wave)); else { local name = null; if (targets.len() != 1) name = "multiple targets" else name = NetProps.GetPropString(targets[0], "m_szNetname"); ClientPrint(player, 3, format("[CMD] Cleared %sfeedback for %s", ((wave == 0) ? "general ": ""), name)); } break; // Clear all feedback for the entire mission // Note: This command's use is limited to users within the whitelist, regardless of the value of whitelist_enabled // Usage: !fb_nuke case "fb_nuke": if (!(steamid in whitelist)) { ClientPrint(player, 3, "[CMD] You are not allowed to use this command"); break; } // SQL local query = "INSERT INTO playerinfo.player_feedback(steamid, timestamp, map, difficulty, mission, wave, feedback)\nVALUES\n"; local values = ""; local raw = NetProps.GetPropString(tf_objective_resource, "m_iszMvMPopfileName"); local map = null; local difficulty = null; local mission = null; if (raw && startswith(raw, "scripts/population/")) { raw = raw.slice(23); local index = null; difficulty = null; foreach (s in ["_int_", "_adv_", "_exp_"]) { index = raw.find(s); if (index != null) { difficulty = s.slice(1, s.len() - 1); break; } } if (index != null) { map = raw.slice(0, index); mission = raw.slice(index + difficulty.len() + 2, raw.len() - 4); } } if (map && difficulty && mission) { local t = {}; LocalTime(t); local datetime = format("%04d-%02d-%02d %02d:%02d:%02d", t.year, t.month, t.day, t.hour, t.minute, t.second); foreach (steamid, feedback in player_general_feedback) { if (!feedback.len()) continue; foreach (i, fb in feedback) { values += format("('%s', '%s', '%s', '%s', '%s', %d, '%s')", steamid, datetime, map, difficulty, mission, 0, fb); if (i != feedback.len() - 1) values += ",\n" } } local wave_feedback = "" foreach (index, table in player_wave_feedback) { if (!table.len()) continue; foreach (steamid, feedback in table) { if (!feedback.len()) continue; foreach (i, fb in feedback) { wave_feedback += format("('%s', '%s', '%s', '%s', '%s', %d, '%s')", steamid, datetime, map, difficulty, mission, index+1, fb); if (i != feedback.len() - 1) wave_feedback += ",\n"; } } if (index != player_wave_feedback.len() - 1) wave_feedback += ",\n"; } if (values.len() > 0) { if (wave_feedback.len() > 0) { values += ",\n"; values += wave_feedback; } } else values += wave_feedback; query += values; if (values != "") StringToFile("_feedback.sql", query); } player_wave_feedback = []; player_general_feedback = {}; finishing_class_lineup = {}; demo_name = ""; StringToFile("_feedbackgeneral.log", ""); for (local i = 1; i < max_waves + 1; ++i) StringToFile(format("_feedbackwave%d.log", i), ""); ClientPrint(player, 3, "[CMD] Nuked all feedback stored in the script and saved on the server"); break; // Compile all gathered feedback and print a formatted response to console // Usage: !fb_compile [verdict] case "fb_compile": local err = handleArgs(player, cmd, [{name="verdict",type="string",vararg=true}]); if (err) { ClientPrint(player, 3, err); break; } local verdict = joinArray(cmd.args); local output = ""; if (verdict && verdict != "" && verdict != "null") output = format("```fix\nVerdict: %s\n```\n# Feedback\n", verdict.toupper()); else output = "# Feedback\n"; local class_lineup = ""; foreach (steamid, classname in finishing_class_lineup) { if (classname == "") continue; local player = getPlayerFromSteamID(steamid); local name = (player) ? NetProps.GetPropString(player, "m_szNetname") : steamid; class_lineup += format("- %s [%s]\n", name, classname); } if (class_lineup != "") { output += "## Finishing Class Lineup\n"; output += class_lineup; } local general_feedback = ""; foreach (steamid, feedback in player_general_feedback) { if (!feedback.len()) continue; local player = getPlayerFromSteamID(steamid); local name = (player) ? NetProps.GetPropString(player, "m_szNetname") : steamid; foreach (i, fb in feedback) { if (player) general_feedback += format("- `%s %s`: %s\n", name, steamid, fb); else general_feedback += format("- `%s`: %s\n", name, fb); } } if (general_feedback != "") { output += "## General\n"; output += general_feedback; } foreach (index, table in player_wave_feedback) { if (!table.len()) continue; local wave_feedback = format("## Wave %d\n", index + 1); foreach (steamid, feedback in table) { if (!feedback.len()) continue; local player = getPlayerFromSteamID(steamid); local name = (player) ? NetProps.GetPropString(player, "m_szNetname") : steamid; foreach (i, fb in feedback) { if (player) wave_feedback += format("- `%s %s`: %s\n", name, steamid, fb); else wave_feedback += format("- `%s`: %s\n", name, fb); } } output += wave_feedback; } // We do this here instead of grabbing it in parseFeedbackFromFiles so we don't override demo_name local raw = FileToString("_feedbackgeneral.log"); if ("RAW " + (raw && raw.len())) { local table = parseSimpleKV(raw); if (table && "Demo" in table) { local demo = table.Demo; if (demo.len()) { output += "## Demo\n"; output += format("Search for: **%s**\n", demo); output += "https://testing.potato.tf/demos.html" } } } printToConsoleFragmented(player, output); ClientPrint(player, 3, "[CMD] See console for output"); break; } }, function OnGameEvent_teamplay_round_start(params) { local time = {}; LocalTime(time); local timestamp = localTimeToTimestamp(time); local raw = NetProps.GetPropString(tf_objective_resource, "m_iszMvMPopfileName"); if (raw && startswith(raw, "scripts/population/")) raw = raw.slice(19, raw.len() - 4); demo_name = format("%s-%d", raw, timestamp); ClientPrint(null, 3, format("\x07ae78f0Demo file:\n%s", demo_name)); } function OnGameEvent_mvm_mission_complete(params) { local team = getAllPlayers( @(p) !p.IsBotOfType(1337) && p.GetTeam() == 2 ); foreach (i, player in team) { local steamid = NetProps.GetPropString(player, "m_szNetworkIDString"); ClientPrint(null, 2, format("\n\n%s - %s\n\n", steamid, classes[player.GetPlayerClass()])); finishing_class_lineup[steamid] <- classes[player.GetPlayerClass()]; } if ((!player_wave_feedback || !player_wave_feedback.len()) && (!player_general_feedback || !player_general_feedback.len())) { ClientPrint(null, 3, "[FB] No feedback was received for the mission, ignoring"); return; } else { ClientPrint(null, 3, "[FB] Saving feedback to server, please reload FB on map reset to continue giving feedback."); ClientPrint(null, 3, "[FB] sm_ent_fire !self RunScriptFile fb"); saveFeedbackToFiles() } } } __CollectGameEventCallbacks(PotatoFeedback) PotatoFeedback.parseFeedbackFromFiles()