Files
SMScripts/scripting/backpack-tf.sp
2025-04-15 22:27:20 -04:00

937 lines
35 KiB
SourcePawn

#pragma semicolon 1
#include <sourcemod>
#include <sdktools>
#include <advanced_motd>
#include <SteamWorks>
#define PLUGIN_VERSION "2.11.1-skial-steamworks"
#define BACKPACK_TF_URL "http://backpack.tf/api/IGetPrices/v3/"
#define ITEM_EARBUDS 143
#define ITEM_REFINED 5002
#define ITEM_KEY 5021
#define ITEM_CRATE 5022
#define ITEM_SALVAGED_CRATE 5068
#define ITEM_HAUNTED_SCRAP 267
#define ITEM_HEADTAKER 266
#define QUALITY_UNIQUE "6"
#define QUALITY_UNUSUAL "5"
#define NOTIFICATION_SOUND "replay/downloadcomplete.wav"
public Plugin:myinfo = {
name = "[TF2] backpack.tf Price Check",
author = "Bottiger, Dr. McKay",
description = "Provides a price check command for use with backpack.tf",
version = PLUGIN_VERSION,
url = "http://www.doctormckay.com"
};
new lastCacheTime;
new cacheTime;
new Handle:backpackTFPricelist;
new Handle:qualityNameTrie;
new Handle:unusualNameTrie;
new Handle:cvarBPCommand;
new Handle:cvarDisplayUpdateNotification;
new Handle:cvarDisplayChangedPrices;
new Handle:cvarHudXPos;
new Handle:cvarHudYPos;
new Handle:cvarHudRed;
new Handle:cvarHudGreen;
new Handle:cvarHudBlue;
new Handle:cvarHudHoldTime;
new Handle:cvarMenuHoldTime;
new Handle:cvarAPIKey;
new Handle:cvarTag;
new Handle:hudText;
new Handle:sv_tags;
new Float:budsToKeys;
new Float:keysToRef;
new Float:refToUsd;
#define UPDATE_FILE "backpack-tf.txt"
#define CONVAR_PREFIX "backpack_tf"
//#include "mckayupdater.sp"
public OnPluginStart() {
cvarBPCommand = CreateConVar("backpack_tf_bp_command", "1", "Enables the !bp command for use with backpack.tf");
cvarDisplayUpdateNotification = CreateConVar("backpack_tf_display_update_notification", "0", "Display a notification to clients when the cached price list has been updated?");
cvarDisplayChangedPrices = CreateConVar("backpack_tf_display_changed_prices", "1", "If backpack_tf_display_update_notification is set to 1, display all prices that changed since the last update?");
cvarHudXPos = CreateConVar("backpack_tf_update_notification_x_pos", "-1.0", "X position for HUD text from 0.0 to 1.0, -1.0 = center", _, true, -1.0, true, 1.0);
cvarHudYPos = CreateConVar("backpack_tf_update_notification_y_pos", "0.1", "Y position for HUD text from 0.0 to 1.0, -1.0 = center", _, true, -1.0, true, 1.0);
cvarHudRed = CreateConVar("backpack_tf_update_notification_red", "0", "Red value of HUD text", _, true, 0.0, true, 255.0);
cvarHudGreen = CreateConVar("backpack_tf_update_notification_green", "255", "Green value of HUD text", _, true, 0.0, true, 255.0);
cvarHudBlue = CreateConVar("backpack_tf_update_notification_blue", "0", "Blue value of HUD text", _, true, 0.0, true, 255.0);
cvarHudHoldTime = CreateConVar("backpack_tf_update_notification_message_time", "5", "Seconds to keep each message in the update ticker on the screen", _, true, 0.0);
cvarMenuHoldTime = CreateConVar("backpack_tf_menu_open_time", "0", "Time to keep the price panel open for, 0 = forever");
cvarAPIKey = CreateConVar("backpack_tf_api_key", "", "API key obtained at http://backpack.tf/api/register/", FCVAR_PROTECTED);
cvarTag = CreateConVar("backpack_tf_add_tag", "1", "If 1, adds the backpack.tf tag to your server's sv_tags, which is required to be listed on http://backpack.tf/servers", _, true, 0.0, true, 1.0);
AutoExecConfig();
LoadTranslations("backpack-tf.phrases");
sv_tags = FindConVar("sv_tags");
RegConsoleCmd("sm_bp", Command_Backpack, "Usage: sm_bp <player>");
RegConsoleCmd("sm_backpack", Command_Backpack, "Usage: sm_backpack <player>");
RegConsoleCmd("sm_pc", Command_PriceCheck, "Usage: sm_pc <item>");
RegConsoleCmd("sm_pricecheck", Command_PriceCheck, "Usage: sm_pricecheck <item>");
RegAdminCmd("sm_updateprices", Command_UpdatePrices, ADMFLAG_ROOT, "Updates backpack.tf prices");
qualityNameTrie = CreateTrie();
SetTrieString(qualityNameTrie, "0", "Normal");
SetTrieString(qualityNameTrie, "1", "Genuine");
SetTrieString(qualityNameTrie, "2", "rarity2");
SetTrieString(qualityNameTrie, "3", "Vintage");
SetTrieString(qualityNameTrie, "4", "rarity3");
SetTrieString(qualityNameTrie, "5", "Unusual");
SetTrieString(qualityNameTrie, "6", "Unique");
SetTrieString(qualityNameTrie, "7", "Community");
SetTrieString(qualityNameTrie, "8", "Valve");
SetTrieString(qualityNameTrie, "9", "Self-Made");
SetTrieString(qualityNameTrie, "10", "Customized");
SetTrieString(qualityNameTrie, "11", "Strange");
SetTrieString(qualityNameTrie, "12", "Completed");
SetTrieString(qualityNameTrie, "13", "Haunted");
SetTrieString(qualityNameTrie, "14", "Collector's");
SetTrieString(qualityNameTrie, "300", "Uncraftable Vintage"); // custom for backpack.tf
SetTrieString(qualityNameTrie, "600", "Uncraftable"); // custom for backpack.tf
SetTrieString(qualityNameTrie, "1100", "Uncraftable Strange"); // custom for backpack.tf
SetTrieString(qualityNameTrie, "1300", "Uncraftable Haunted"); // custom for backpack.tf
unusualNameTrie = CreateTrie();
// Original effects
SetTrieString(unusualNameTrie, "6", "Green Confetti");
SetTrieString(unusualNameTrie, "7", "Purple Confetti");
SetTrieString(unusualNameTrie, "8", "Haunted Ghosts");
SetTrieString(unusualNameTrie, "9", "Green Energy");
SetTrieString(unusualNameTrie, "10", "Purple Energy");
SetTrieString(unusualNameTrie, "11", "Circling TF Logo");
SetTrieString(unusualNameTrie, "12", "Massed Flies");
SetTrieString(unusualNameTrie, "13", "Burning Flames");
SetTrieString(unusualNameTrie, "14", "Scorching Flames");
SetTrieString(unusualNameTrie, "15", "Searing Plasma");
SetTrieString(unusualNameTrie, "16", "Vivid Plasma");
SetTrieString(unusualNameTrie, "17", "Sunbeams");
SetTrieString(unusualNameTrie, "18", "Circling Peace Sign");
SetTrieString(unusualNameTrie, "19", "Circling Heart");
// Batch 2
SetTrieString(unusualNameTrie, "29", "Stormy Storm");
SetTrieString(unusualNameTrie, "30", "Blizzardy Storm");
SetTrieString(unusualNameTrie, "31", "Nuts n' Bolts");
SetTrieString(unusualNameTrie, "32", "Orbiting Planets");
SetTrieString(unusualNameTrie, "33", "Orbiting Fire");
SetTrieString(unusualNameTrie, "34", "Bubbling");
SetTrieString(unusualNameTrie, "35", "Smoking");
SetTrieString(unusualNameTrie, "36", "Steaming");
// Halloween
SetTrieString(unusualNameTrie, "37", "Flaming Lantern");
SetTrieString(unusualNameTrie, "38", "Cloudy Moon");
SetTrieString(unusualNameTrie, "39", "Cauldron Bubbles");
SetTrieString(unusualNameTrie, "40", "Eerie Orbiting Fire");
SetTrieString(unusualNameTrie, "43", "Knifestorm");
SetTrieString(unusualNameTrie, "44", "Misty Skull");
SetTrieString(unusualNameTrie, "45", "Harvest Moon");
SetTrieString(unusualNameTrie, "46", "It's A Secret To Everybody");
SetTrieString(unusualNameTrie, "47", "Stormy 13th Hour");
// Batch 3
SetTrieString(unusualNameTrie, "56", "Kill-a-Watt");
SetTrieString(unusualNameTrie, "57", "Terror-Watt");
SetTrieString(unusualNameTrie, "58", "Cloud 9");
SetTrieString(unusualNameTrie, "59", "Aces High");
SetTrieString(unusualNameTrie, "60", "Dead Presidents");
SetTrieString(unusualNameTrie, "61", "Miami Nights");
SetTrieString(unusualNameTrie, "62", "Disco Beat Down");
// Robo-effects
SetTrieString(unusualNameTrie, "63", "Phosphorous");
SetTrieString(unusualNameTrie, "64", "Sulphurous");
SetTrieString(unusualNameTrie, "65", "Memory Leak");
SetTrieString(unusualNameTrie, "66", "Overclocked");
SetTrieString(unusualNameTrie, "67", "Electrostatic");
SetTrieString(unusualNameTrie, "68", "Power Surge");
SetTrieString(unusualNameTrie, "69", "Anti-Freeze");
SetTrieString(unusualNameTrie, "70", "Time Warp");
SetTrieString(unusualNameTrie, "71", "Green Black Hole");
SetTrieString(unusualNameTrie, "72", "Roboactive");
// Halloween 2013
SetTrieString(unusualNameTrie, "73", "Arcana");
SetTrieString(unusualNameTrie, "74", "Spellbound");
SetTrieString(unusualNameTrie, "75", "Chiroptera Venenata");
SetTrieString(unusualNameTrie, "76", "Poisoned Shadows");
SetTrieString(unusualNameTrie, "77", "Something Burning This Way Comes");
SetTrieString(unusualNameTrie, "78", "Hellfire");
SetTrieString(unusualNameTrie, "79", "Darkblaze");
SetTrieString(unusualNameTrie, "80", "Demonflame");
// Halloween 2014
SetTrieString(unusualNameTrie, "81", "Bonzo The All-Gnawing");
SetTrieString(unusualNameTrie, "82", "Amaranthine");
SetTrieString(unusualNameTrie, "83", "Stare From Beyond");
SetTrieString(unusualNameTrie, "84", "The Ooze");
SetTrieString(unusualNameTrie, "85", "Ghastly Ghosts Jr");
SetTrieString(unusualNameTrie, "86", "Haunted Phantasm Jr");
// EOTL
SetTrieString(unusualNameTrie, "87", "Frostbite");
SetTrieString(unusualNameTrie, "88", "Molten Mallard");
SetTrieString(unusualNameTrie, "89", "Morning Glory");
SetTrieString(unusualNameTrie, "90", "Death at Dusk");
// Taunt effects
SetTrieString(unusualNameTrie, "3001", "Showstopper");
SetTrieString(unusualNameTrie, "3002", "Showstopper");
SetTrieString(unusualNameTrie, "3003", "Holy Grail");
SetTrieString(unusualNameTrie, "3004", "'72");
SetTrieString(unusualNameTrie, "3005", "Fountain of Delight");
SetTrieString(unusualNameTrie, "3006", "Screaming Tiger");
SetTrieString(unusualNameTrie, "3007", "Skill Gotten Gains");
SetTrieString(unusualNameTrie, "3008", "Midnight Whirlwind");
SetTrieString(unusualNameTrie, "3009", "Silver Cyclone");
SetTrieString(unusualNameTrie, "3010", "Mega Strike");
// Halloween 2014 taunt effects
SetTrieString(unusualNameTrie, "3011", "Haunted Phantasm");
SetTrieString(unusualNameTrie, "3012", "Ghastly Ghosts");
hudText = CreateHudSynchronizer();
}
public OnConfigsExecuted() {
CreateTimer(2.0, Timer_AddTag); // Let everything load first
}
public Action:Timer_AddTag(Handle:timer) {
if(!GetConVarBool(cvarTag)) {
return;
}
decl String:value[512];
GetConVarString(sv_tags, value, sizeof(value));
TrimString(value);
if(strlen(value) == 0) {
SetConVarString(sv_tags, "backpack.tf");
return;
}
decl String:tags[64][64];
new total = ExplodeString(value, ",", tags, sizeof(tags), sizeof(tags[]));
for(new i = 0; i < total; i++) {
if(StrEqual(tags[i], "backpack.tf")) {
return; // Tag found, nothing to do here
}
}
StrCat(value, sizeof(value), ",backpack.tf");
SetConVarString(sv_tags, value);
}
public OnMapStart() {
PrecacheSound(NOTIFICATION_SOUND);
}
public Steam_FullyLoaded() {
CreateTimer(1.0, Timer_Update); // In case of late-loads
}
GetCachedPricesAge() {
decl String:path[PLATFORM_MAX_PATH];
BuildPath(Path_SM, path, sizeof(path), "data/backpack-tf.txt");
if(!FileExists(path)) {
return -1;
}
new Handle:kv = CreateKeyValues("Response");
if(!FileToKeyValues(kv, path)) {
CloseHandle(kv);
return -1;
}
new offset = KvGetNum(kv, "time_offset", 1337); // The actual offset can be positive, negative, or zero, so we'll just use 1337 as a default since that's unlikely
new time = KvGetNum(kv, "current_time");
CloseHandle(kv);
if(offset == 1337 || time == 0) {
return -1;
}
return GetTime() - time;
}
public Action:Timer_Update(Handle:timer) {
new age = GetCachedPricesAge();
if(age != -1 && age < 900) { // 15 minutes
LogMessage("Locally saved pricing data is %d minutes old, bypassing backpack.tf query", age / 60);
if(backpackTFPricelist != INVALID_HANDLE) {
CloseHandle(backpackTFPricelist);
}
decl String:path[PLATFORM_MAX_PATH];
BuildPath(Path_SM, path, sizeof(path), "data/backpack-tf.txt");
backpackTFPricelist = CreateKeyValues("Response");
FileToKeyValues(backpackTFPricelist, path);
budsToKeys = GetConversion(ITEM_EARBUDS);
keysToRef = GetConversion(ITEM_KEY);
KvRewind(backpackTFPricelist);
refToUsd = KvGetFloat(backpackTFPricelist, "refined_usd_value");
CreateTimer(float(3600 - age), Timer_Update);
return;
}
decl String:key[32];
GetConVarString(cvarAPIKey, key, sizeof(key));
if(strlen(key) == 0) {
LogError("No API key set. Fill in your API key and reload the plugin.");
return;
}
Handle request = SteamWorks_CreateHTTPRequest(k_EHTTPMethodGET, BACKPACK_TF_URL);
SteamWorks_SetHTTPRequestGetOrPostParameter(request, "key", key);
SteamWorks_SetHTTPRequestGetOrPostParameter(request, "format", "vdf");
SteamWorks_SetHTTPRequestGetOrPostParameter(request, "names", "1");
SteamWorks_SetHTTPCallbacks(request, OnBackpackTFComplete);
SteamWorks_SendHTTPRequest(request);
}
public OnBackpackTFComplete(Handle request, bool failure, bool successful, EHTTPStatusCode status) {
if(status != k_EHTTPStatusCode200OK || !successful) {
if(status == k_EHTTPStatusCode400BadRequest) {
LogError("backpack.tf API failed: You have not set an API key");
CloseHandle(request);
CreateTimer(600.0, Timer_Update); // Set this for 10 minutes instead of 1 minute
return;
} else if(status == k_EHTTPStatusCode403Forbidden) {
LogError("backpack.tf API failed: Your API key is invalid");
CloseHandle(request);
CreateTimer(600.0, Timer_Update); // Set this for 10 minutes instead of 1 minute
return;
} else if(status == k_EHTTPStatusCode412PreconditionFailed) {
decl String:retry[16];
SteamWorks_GetHTTPResponseHeaderValue(request, "Retry-After", retry, sizeof(retry));
LogError("backpack.tf API failed: We are being rate-limited by backpack.tf, next request allowed in %s seconds", retry);
} else if(status >= k_EHTTPStatusCode500InternalServerError) {
LogError("backpack.tf API failed: An internal server error occurred");
} else if(status == k_EHTTPStatusCode200OK && !successful) {
LogError("backpack.tf API failed: backpack.tf returned an OK response but no data");
} else if(status != k_EHTTPStatusCodeInvalid) {
LogError("backpack.tf API failed: Unknown error (status code %d)", _:status);
} else {
LogError("backpack.tf API failed: Unable to connect to server or server returned no data");
}
CloseHandle(request);
CreateTimer(60.0, Timer_Update); // try again!
return;
}
decl String:path[256];
BuildPath(Path_SM, path, sizeof(path), "data/backpack-tf.txt");
SteamWorks_WriteHTTPResponseBodyToFile(request, path);
CloseHandle(request);
LogMessage("backpack.tf price list successfully downloaded!");
CreateTimer(3600.0, Timer_Update);
if(backpackTFPricelist != INVALID_HANDLE) {
CloseHandle(backpackTFPricelist);
}
backpackTFPricelist = CreateKeyValues("Response");
FileToKeyValues(backpackTFPricelist, path);
lastCacheTime = cacheTime;
cacheTime = KvGetNum(backpackTFPricelist, "current_time");
new offset = GetTime() - cacheTime;
KvSetNum(backpackTFPricelist, "time_offset", offset);
KeyValuesToFile(backpackTFPricelist, path);
budsToKeys = GetConversion(ITEM_EARBUDS);
keysToRef = GetConversion(ITEM_KEY);
KvRewind(backpackTFPricelist);
refToUsd = KvGetFloat(backpackTFPricelist, "refined_usd_value");
if(!GetConVarBool(cvarDisplayUpdateNotification)) {
return;
}
if(lastCacheTime == 0) { // first download
new Handle:array = CreateArray(128);
PushArrayString(array, "#Type_command");
SetHudTextParams(GetConVarFloat(cvarHudXPos), GetConVarFloat(cvarHudYPos), GetConVarFloat(cvarHudHoldTime), GetConVarInt(cvarHudRed), GetConVarInt(cvarHudGreen), GetConVarInt(cvarHudBlue), 255);
for(new i = 1; i <= MaxClients; i++) {
if(!IsClientInGame(i)) {
continue;
}
ShowSyncHudText(i, hudText, "%t", "Price list updated");
EmitSoundToClient(i, NOTIFICATION_SOUND);
}
CreateTimer(GetConVarFloat(cvarHudHoldTime), Timer_DisplayHudText, array, TIMER_REPEAT);
return;
}
PrepPriceKv();
KvGotoFirstSubKey(backpackTFPricelist);
new bool:isNegative = false;
new lastUpdate, Float:valueOld, Float:valueOldHigh, Float:value, Float:valueHigh, Float:difference;
decl String:defindex[16], String:qualityIndex[32], String:quality[32], String:name[64], String:message[128], String:currency[32], String:currencyOld[32], String:oldPrice[64], String:newPrice[64];
new Handle:array = CreateArray(128);
PushArrayString(array, "#Type_command");
if(GetConVarBool(cvarDisplayChangedPrices)) {
do {
// loop through items
KvGetSectionName(backpackTFPricelist, defindex, sizeof(defindex));
if(StringToInt(defindex) == ITEM_REFINED) {
continue; // Skip over refined price changes
}
KvGotoFirstSubKey(backpackTFPricelist);
do {
// loop through qualities
KvGetSectionName(backpackTFPricelist, qualityIndex, sizeof(qualityIndex));
if(StrEqual(qualityIndex, "item_info")) {
KvGetString(backpackTFPricelist, "item_name", name, sizeof(name));
continue;
}
KvGotoFirstSubKey(backpackTFPricelist);
do {
// loop through instances (series #s, effects)
lastUpdate = KvGetNum(backpackTFPricelist, "last_change");
if(lastUpdate == 0 || lastUpdate < lastCacheTime) {
continue; // hasn't updated
}
valueOld = KvGetFloat(backpackTFPricelist, "value_old");
valueOldHigh = KvGetFloat(backpackTFPricelist, "value_high_old");
value = KvGetFloat(backpackTFPricelist, "value");
valueHigh = KvGetFloat(backpackTFPricelist, "value_high");
KvGetString(backpackTFPricelist, "currency", currency, sizeof(currency));
KvGetString(backpackTFPricelist, "currency_old", currencyOld, sizeof(currencyOld));
if(strlen(currency) == 0 || strlen(currencyOld) == 0) {
continue;
}
FormatPriceRange(valueOld, valueOldHigh, currency, oldPrice, sizeof(oldPrice), StrEqual(qualityIndex, QUALITY_UNUSUAL));
FormatPriceRange(value, valueHigh, currency, newPrice, sizeof(newPrice), StrEqual(qualityIndex, QUALITY_UNUSUAL));
// Get an average so we can determine if it went up or down
if(valueOldHigh != 0.0) {
valueOld = (valueOld + valueOldHigh) / 2.0;
}
if(valueHigh != 0.0) {
value = (value + valueHigh) / 2.0;
}
// Get prices in terms of refined now so we can determine if it went up or down
if(StrEqual(currencyOld, "earbuds")) {
valueOld = valueOld * budsToKeys * keysToRef;
} else if(StrEqual(currencyOld, "keys")) {
valueOld = valueOld * keysToRef;
}
if(StrEqual(currency, "earbuds")) {
value = value * budsToKeys * keysToRef;
} else if(StrEqual(currency, "keys")) {
value = value * keysToRef;
}
difference = value - valueOld;
if(difference < 0.0) {
isNegative = true;
difference = difference * -1.0;
} else {
isNegative = false;
}
// Format a quality name
if(StrEqual(qualityIndex, QUALITY_UNIQUE)) {
Format(quality, sizeof(quality), ""); // if quality is unique, don't display a quality
} else if(StrEqual(qualityIndex, QUALITY_UNUSUAL) && (StringToInt(defindex) != ITEM_HAUNTED_SCRAP && StringToInt(defindex) != ITEM_HEADTAKER)) {
decl String:effect[16];
KvGetSectionName(backpackTFPricelist, effect, sizeof(effect));
if(!GetTrieString(unusualNameTrie, effect, quality, sizeof(quality))) {
LogError("Unknown unusual effect: %s in OnBackpackTFComplete. Please report this!", effect);
decl String:kvPath[PLATFORM_MAX_PATH];
BuildPath(Path_SM, kvPath, sizeof(kvPath), "data/backpack-tf.%d.txt", GetTime());
if(!FileExists(kvPath)) {
KeyValuesToFile(backpackTFPricelist, kvPath);
}
continue;
}
} else {
if(!GetTrieString(qualityNameTrie, qualityIndex, quality, sizeof(quality))) {
LogError("Unknown quality index: %s. Please report this!", qualityIndex);
continue;
}
}
Format(message, sizeof(message), "%s%s%s: %s #From %s #To %s", quality, StrEqual(quality, "") ? "" : " ", name, isNegative ? "#Down" : "#Up", oldPrice, newPrice);
PushArrayString(array, message);
} while(KvGotoNextKey(backpackTFPricelist)); // end: instances
KvGoBack(backpackTFPricelist);
} while(KvGotoNextKey(backpackTFPricelist)); // end: qualities
KvGoBack(backpackTFPricelist);
} while(KvGotoNextKey(backpackTFPricelist)); // end: items
}
SetHudTextParams(GetConVarFloat(cvarHudXPos), GetConVarFloat(cvarHudYPos), GetConVarFloat(cvarHudHoldTime), GetConVarInt(cvarHudRed), GetConVarInt(cvarHudGreen), GetConVarInt(cvarHudBlue), 255);
for(new i = 1; i <= MaxClients; i++) {
if(!IsClientInGame(i)) {
continue;
}
ShowSyncHudText(i, hudText, "%t", "Price list updated");
EmitSoundToClient(i, NOTIFICATION_SOUND);
}
CreateTimer(GetConVarFloat(cvarHudHoldTime), Timer_DisplayHudText, array, TIMER_REPEAT);
}
Float:GetConversion(defindex) {
decl String:buffer[32];
PrepPriceKv();
IntToString(defindex, buffer, sizeof(buffer));
KvJumpToKey(backpackTFPricelist, buffer);
KvJumpToKey(backpackTFPricelist, "6");
KvJumpToKey(backpackTFPricelist, "0");
new Float:value = KvGetFloat(backpackTFPricelist, "value");
new Float:valueHigh = KvGetFloat(backpackTFPricelist, "value_high");
if(valueHigh == 0.0) {
return value;
}
return (value + valueHigh) / 2.0;
}
FormatPrice(Float:price, const String:currency[], String:output[], maxlen, bool:includeCurrency = true, bool:forceBuds = false) {
new String:outputCurrency[32];
if(StrEqual(currency, "metal")) {
Format(outputCurrency, sizeof(outputCurrency), "refined");
} else if(StrEqual(currency, "keys")) {
Format(outputCurrency, sizeof(outputCurrency), "key");
} else if(StrEqual(currency, "earbuds")) {
Format(outputCurrency, sizeof(outputCurrency), "bud");
} else if(StrEqual(currency, "usd")) {
if(forceBuds) {
Format(outputCurrency, sizeof(outputCurrency), "earbuds"); // This allows us to force unusual price ranges to display buds only
}
ConvertUSD(price, outputCurrency, sizeof(outputCurrency));
} else {
ThrowError("Unknown currency: %s", currency);
}
if(FloatIsInt(price)) {
Format(output, maxlen, "%d", RoundToFloor(price));
} else {
Format(output, maxlen, "%.2f", price);
}
if(!includeCurrency) {
return;
}
if(StrEqual(output, "1") || StrEqual(currency, "metal")) {
Format(output, maxlen, "%s %s", output, outputCurrency);
} else {
Format(output, maxlen, "%s %ss", output, outputCurrency);
}
}
FormatPriceRange(Float:low, Float:high, const String:currency[], String:output[], maxlen, bool:forceBuds = false) {
if(high == 0.0) {
FormatPrice(low, currency, output, maxlen, true, forceBuds);
return;
}
decl String:buffer[32];
FormatPrice(low, currency, output, maxlen, false, forceBuds);
FormatPrice(high, currency, buffer, sizeof(buffer), true, forceBuds);
Format(output, maxlen, "%s-%s", output, buffer);
}
ConvertUSD(&Float:price, String:outputCurrency[], maxlen) {
new Float:budPrice = refToUsd * keysToRef * budsToKeys;
if(price < budPrice && !StrEqual(outputCurrency, "earbuds")) {
new Float:keyPrice = refToUsd * keysToRef;
price = price / keyPrice;
Format(outputCurrency, maxlen, "key");
} else {
price = price / budPrice;
Format(outputCurrency, maxlen, "bud");
}
}
bool:FloatIsInt(Float:input) {
return float(RoundToFloor(input)) == input;
}
public Action:Timer_DisplayHudText(Handle:timer, any:array) {
if(GetArraySize(array) == 0) {
CloseHandle(array);
return Plugin_Stop;
}
decl String:text[128], String:display[128];
GetArrayString(array, 0, text, sizeof(text));
SetHudTextParams(GetConVarFloat(cvarHudXPos), GetConVarFloat(cvarHudYPos), GetConVarFloat(cvarHudHoldTime), GetConVarInt(cvarHudRed), GetConVarInt(cvarHudGreen), GetConVarInt(cvarHudBlue), 255);
for(new i = 1; i <= MaxClients; i++) {
if(!IsClientInGame(i)) {
continue;
}
PerformTranslationTokenReplacement(i, text, display, sizeof(display));
ShowSyncHudText(i, hudText, display);
}
RemoveFromArray(array, 0);
return Plugin_Continue;
}
PerformTranslationTokenReplacement(client, const String:message[], String:output[], maxlen) {
SetGlobalTransTarget(client);
strcopy(output, maxlen, message);
decl String:buffer[64];
Format(buffer, maxlen, "%t", "Type !pc for a price check");
ReplaceString(output, maxlen, "#Type_command", buffer);
Format(buffer, maxlen, "%t", "Up");
ReplaceString(output, maxlen, "#Up", buffer);
Format(buffer, maxlen, "%t", "Down");
ReplaceString(output, maxlen, "#Down", buffer);
Format(buffer, maxlen, "%t", "From");
ReplaceString(output, maxlen, "#From", buffer);
Format(buffer, maxlen, "%t", "To");
ReplaceString(output, maxlen, "#To", buffer);
}
PrepPriceKv() {
KvRewind(backpackTFPricelist);
KvJumpToKey(backpackTFPricelist, "prices");
}
public Action:Command_PriceCheck(client, args) {
if(backpackTFPricelist == INVALID_HANDLE) {
decl String:key[32];
GetConVarString(cvarAPIKey, key, sizeof(key));
if(strlen(key) == 0) {
ReplyToCommand(client, "\x04[SM] \x01The server administrator has not filled in their API key yet. Please contact the server administrator.");
} else {
ReplyToCommand(client, "\x04[SM] \x01%t.", "The price list has not loaded yet");
}
return Plugin_Handled;
}
if(args == 0) {
new Handle:menu = CreateMenu(Handler_ItemSelection);
SetMenuTitle(menu, "Price Check");
PrepPriceKv();
KvGotoFirstSubKey(backpackTFPricelist);
decl String:name[128];
do {
if(!KvJumpToKey(backpackTFPricelist, "item_info")) {
continue;
}
KvGetString(backpackTFPricelist, "item_name", name, sizeof(name));
if(KvGetNum(backpackTFPricelist, "proper_name") == 1) {
Format(name, sizeof(name), "The %s", name);
}
AddMenuItem(menu, name, name);
KvGoBack(backpackTFPricelist);
} while(KvGotoNextKey(backpackTFPricelist));
DisplayMenu(menu, client, GetConVarInt(cvarMenuHoldTime));
return Plugin_Handled;
}
new resultDefindex = -1;
decl String:defindex[8], String:name[128], String:itemName[128];
GetCmdArgString(name, sizeof(name));
new bool:exact = StripQuotes(name);
PrepPriceKv();
KvGotoFirstSubKey(backpackTFPricelist);
new Handle:matches;
if(!exact) {
matches = CreateArray(128);
}
do {
KvGetSectionName(backpackTFPricelist, defindex, sizeof(defindex));
if(!KvJumpToKey(backpackTFPricelist, "item_info")) {
continue;
}
KvGetString(backpackTFPricelist, "item_name", itemName, sizeof(itemName));
if(KvGetNum(backpackTFPricelist, "proper_name") == 1) {
Format(itemName, sizeof(itemName), "The %s", itemName);
}
KvGoBack(backpackTFPricelist);
if(exact) {
if(StrEqual(itemName, name, false)) {
resultDefindex = StringToInt(defindex);
break;
}
} else {
if(StrContains(itemName, name, false) != -1) {
resultDefindex = StringToInt(defindex); // In case this is the only match, we store the resulting defindex here so that we don't need to search to find it again
PushArrayString(matches, itemName);
}
}
} while(KvGotoNextKey(backpackTFPricelist));
if(!exact && GetArraySize(matches) > 1) {
new Handle:menu = CreateMenu(Handler_ItemSelection);
SetMenuTitle(menu, "Search Results");
new size = GetArraySize(matches);
for(new i = 0; i < size; i++) {
GetArrayString(matches, i, itemName, sizeof(itemName));
AddMenuItem(menu, itemName, itemName);
}
DisplayMenu(menu, client, GetConVarInt(cvarMenuHoldTime));
CloseHandle(matches);
return Plugin_Handled;
}
if(!exact) {
CloseHandle(matches);
}
if(resultDefindex == -1) {
ReplyToCommand(client, "\x04[SM] \x01No matching item was found.");
return Plugin_Handled;
}
// At this point, we know that we've found our item. Its defindex is stored in resultDefindex as a cell
// defindex was used to store the defindex of every item as we searched it, so it's not reliable
if(resultDefindex == ITEM_REFINED) {
SetGlobalTransTarget(client);
new Handle:menu = CreateMenu(Handler_PriceListMenu);
SetMenuTitle(menu, "%t\n%t\n%t\n ", "Price check", itemName, "Prices are estimates only", "Prices courtesy of backpack.tf");
decl String:buffer[32];
Format(buffer, sizeof(buffer), "Unique: $%.2f USD", refToUsd);
AddMenuItem(menu, "", buffer);
DisplayMenu(menu, client, GetConVarInt(cvarMenuHoldTime));
return Plugin_Handled;
}
new bool:isCrate = (resultDefindex == ITEM_CRATE || resultDefindex == ITEM_SALVAGED_CRATE);
new bool:onlyOneUnusual = (resultDefindex == ITEM_HEADTAKER || resultDefindex == ITEM_HAUNTED_SCRAP);
PrepPriceKv();
IntToString(resultDefindex, defindex, sizeof(defindex));
KvJumpToKey(backpackTFPricelist, defindex);
KvJumpToKey(backpackTFPricelist, "item_info");
KvGetString(backpackTFPricelist, "item_name", itemName, sizeof(itemName));
if(KvGetNum(backpackTFPricelist, "proper_name") == 1) {
Format(itemName, sizeof(itemName), "The %s", itemName);
}
KvGotoNextKey(backpackTFPricelist);
SetGlobalTransTarget(client);
new Handle:menu = CreateMenu(Handler_PriceListMenu);
SetMenuTitle(menu, "%t\n%t\n%t\n ", "Price check", itemName, "Prices are estimates only", "Prices courtesy of backpack.tf");
new bool:unusualDisplayed = false;
new Float:value, Float:valueHigh;
decl String:currency[32], String:qualityIndex[16], String:quality[16], String:series[8], String:price[32], String:buffer[64];
do {
KvGetSectionName(backpackTFPricelist, qualityIndex, sizeof(qualityIndex));
if(StrEqual(qualityIndex, "item_info") || StrEqual(qualityIndex, "alt_defindex")) {
continue;
}
KvGotoFirstSubKey(backpackTFPricelist);
do {
if(StrEqual(qualityIndex, QUALITY_UNUSUAL) && !onlyOneUnusual) {
if(!unusualDisplayed) {
AddMenuItem(menu, defindex, "Unusual: View Effects");
unusualDisplayed = true;
}
} else {
value = KvGetFloat(backpackTFPricelist, "value");
valueHigh = KvGetFloat(backpackTFPricelist, "value_high");
KvGetString(backpackTFPricelist, "currency", currency, sizeof(currency));
FormatPriceRange(value, valueHigh, currency, price, sizeof(price));
if(!GetTrieString(qualityNameTrie, qualityIndex, quality, sizeof(quality))) {
LogError("Unknown quality index: %s. Please report this!", qualityIndex);
continue;
}
if(isCrate) {
KvGetSectionName(backpackTFPricelist, series, sizeof(series));
if(StrEqual(series, "0")) {
continue;
}
if(StrEqual(qualityIndex, QUALITY_UNIQUE)) {
Format(buffer, sizeof(buffer), "Series %s: %s", series, price);
} else {
Format(buffer, sizeof(buffer), "%s: Series %s: %s", quality, series, price);
}
} else {
Format(buffer, sizeof(buffer), "%s: %s", quality, price);
}
AddMenuItem(menu, "", buffer, ITEMDRAW_DISABLED);
}
} while(KvGotoNextKey(backpackTFPricelist));
KvGoBack(backpackTFPricelist);
} while(KvGotoNextKey(backpackTFPricelist));
DisplayMenu(menu, client, GetConVarInt(cvarMenuHoldTime));
return Plugin_Handled;
}
public Handler_ItemSelection(Handle:menu, MenuAction:action, client, param) {
if(action == MenuAction_End) {
CloseHandle(menu);
}
if(action != MenuAction_Select) {
return;
}
decl String:selection[128];
GetMenuItem(menu, param, selection, sizeof(selection));
FakeClientCommand(client, "sm_pricecheck \"%s\"", selection);
}
public Handler_PriceListMenu(Handle:menu, MenuAction:action, client, param) {
if(action == MenuAction_End) {
CloseHandle(menu);
}
if(action != MenuAction_Select) {
return;
}
decl String:defindex[32];
GetMenuItem(menu, param, defindex, sizeof(defindex));
decl String:name[64];
PrepPriceKv();
KvJumpToKey(backpackTFPricelist, defindex);
KvJumpToKey(backpackTFPricelist, "item_info");
KvGetString(backpackTFPricelist, "item_name", name, sizeof(name));
if(KvGetNum(backpackTFPricelist, "proper_name") == 1) {
Format(name, sizeof(name), "The Unusual %s", name);
} else {
Format(name, sizeof(name), "Unusual %s", name);
}
KvGoBack(backpackTFPricelist);
if(!KvJumpToKey(backpackTFPricelist, QUALITY_UNUSUAL)) {
return;
}
KvGotoFirstSubKey(backpackTFPricelist);
SetGlobalTransTarget(client);
new Handle:menu2 = CreateMenu(Handler_PriceListMenu);
SetMenuTitle(menu2, "%t\n%t\n%t\n ", "Price check", name, "Prices are estimates only", "Prices courtesy of backpack.tf");
decl String:effect[8], String:effectName[64], String:message[128], String:price[64], String:currency[32];
new Float:value, Float:valueHigh;
do {
KvGetSectionName(backpackTFPricelist, effect, sizeof(effect));
if(!GetTrieString(unusualNameTrie, effect, effectName, sizeof(effectName))) {
LogError("Unknown unusual effect: %s in Handler_PriceListMenu. Please report this!", effect);
decl String:path[PLATFORM_MAX_PATH];
BuildPath(Path_SM, path, sizeof(path), "data/backpack-tf.%d.txt", GetTime());
if(!FileExists(path)) {
KeyValuesToFile(backpackTFPricelist, path);
}
continue;
}
value = KvGetFloat(backpackTFPricelist, "value");
valueHigh = KvGetFloat(backpackTFPricelist, "value_high");
KvGetString(backpackTFPricelist, "currency", currency, sizeof(currency));
if(StrEqual(currency, "")) {
continue;
}
FormatPriceRange(value, valueHigh, currency, price, sizeof(price), true);
Format(message, sizeof(message), "%s: %s", effectName, price);
AddMenuItem(menu2, "", message, ITEMDRAW_DISABLED);
} while(KvGotoNextKey(backpackTFPricelist));
DisplayMenu(menu2, client, GetConVarInt(cvarMenuHoldTime));
}
public Action:Command_Backpack(client, args) {
if(!GetConVarBool(cvarBPCommand)) {
return Plugin_Continue;
}
new target;
if(args == 0) {
target = GetClientAimTarget(client);
if(target <= 0) {
DisplayClientMenu(client);
return Plugin_Handled;
}
} else {
decl String:arg1[MAX_NAME_LENGTH];
GetCmdArg(1, arg1, sizeof(arg1));
target = FindTargetEx(client, arg1, true, false, false);
if(target == -1) {
DisplayClientMenu(client);
return Plugin_Handled;
}
}
decl String:steamID[64];
//Steam_GetCSteamIDForClient(target, steamID, sizeof(steamID)); // we could use the regular Steam ID, but we already have SteamTools, so we can just bypass backpack.tf's redirect directly
GetClientAuthId(target, AuthId_SteamID64, steamID, sizeof(steamID), false);
decl String:url[256];
Format(url, sizeof(url), "http://backpack.tf/profiles/%s?t=%i", steamID, GetTime());
AdvMOTD_ShowMOTDPanel(client, "backpack.tf", url, MOTDPANEL_TYPE_URL, true, true, true, OnMOTDFailure);
return Plugin_Handled;
}
public OnMOTDFailure(client, MOTDFailureReason:reason) {
switch(reason) {
case MOTDFailure_Disabled: PrintToChat(client, "\x04[SM] \x01You cannot view backpacks with HTML MOTDs disabled.");
case MOTDFailure_Matchmaking: PrintToChat(client, "\x04[SM] \x01You cannot view backpacks after joining via Quickplay.");
case MOTDFailure_QueryFailed: PrintToChat(client, "\x04[SM] \x01Unable to open backpack.");
}
}
DisplayClientMenu(client) {
new Handle:menu = CreateMenu(Handler_ClientMenu);
SetMenuTitle(menu, "Select Player");
decl String:name[MAX_NAME_LENGTH], String:index[8];
for(new i = 1; i <= MaxClients; i++) {
if(!IsClientInGame(i) || IsFakeClient(i)) {
continue;
}
GetClientName(i, name, sizeof(name));
IntToString(GetClientUserId(i), index, sizeof(index));
AddMenuItem(menu, index, name);
}
DisplayMenu(menu, client, GetConVarInt(cvarMenuHoldTime));
}
public Handler_ClientMenu(Handle:menu, MenuAction:action, client, param) {
if(action == MenuAction_End) {
CloseHandle(menu);
}
if(action != MenuAction_Select) {
return;
}
decl String:selection[32];
GetMenuItem(menu, param, selection, sizeof(selection));
FakeClientCommand(client, "sm_backpack #%s", selection);
}
public Action:Command_UpdatePrices(client, args) {
new age = GetCachedPricesAge();
if(age != -1 && age < 900) { // 15 minutes
ReplyToCommand(client, "\x04[SM] \x01The price list cannot be updated more frequently than every 15 minutes. It is currently %d minutes old.", age / 60);
return Plugin_Handled;
}
ReplyToCommand(client, "\x04[SM] \x01Updating backpack.tf prices...");
Timer_Update(INVALID_HANDLE);
return Plugin_Handled;
}
FindTargetEx(client, const String:target[], bool:nobots = false, bool:immunity = true, bool:replyToError = true) {
decl String:target_name[MAX_TARGET_LENGTH];
decl target_list[1], target_count, bool:tn_is_ml;
new flags = COMMAND_FILTER_NO_MULTI;
if(nobots) {
flags |= COMMAND_FILTER_NO_BOTS;
}
if(!immunity) {
flags |= COMMAND_FILTER_NO_IMMUNITY;
}
if((target_count = ProcessTargetString(
target,
client,
target_list,
1,
flags,
target_name,
sizeof(target_name),
tn_is_ml)) > 0)
{
return target_list[0];
} else {
if(replyToError) {
ReplyToTargetError(client, target_count);
}
return -1;
}
}