123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527 |
- /**
- * [CSRD] Custom Achievements
- *
- * The custom achievement implementation for Pikachu's Canadian Server of Romance and Drama.
- */
- #pragma semicolon 1
- #include <sourcemod>
- #include <tf2_stocks>
- #include <sdktools_sound>
- #pragma newdecls required
- #include <stocksoup/log_server>
- #include <stocksoup/tf/tempents_stocks>
- #define PLUGIN_VERSION "0.1.2"
- public Plugin myinfo = {
- name = "[CSRD] Custom Achievements",
- author = "nosoop",
- description = "Provides a lightweight API for custom achievements.",
- version = PLUGIN_VERSION,
- url = "https://git.csrd.science/"
- }
- // Maximum language shortcode length (2-3 characters usually, though 'pt_p' is a thing).
- #define MAX_LANG_SC_LENGTH 8
- // Head attachment point index for player.
- #define ATTACHMENT_HEAD 1
- public APLRes AskPluginLoad2(Handle hPlugin, bool late, char[] error, int err_max) {
- RegPluginLibrary("custom-achievements");
-
- CreateNative("CustomAchievement.CustomAchievement", Native_RegisterAchievement);
- CreateNative("CustomAchievement.AwardToAccountID", Native_AwardAchievement);
- CreateNative("CustomAchievement.FetchMetadataByAccountID", Native_FetchAchievementMetadata);
- CreateNative("CustomAchievement.StoreMetadataByAccountID", Native_StoreAchievementMetadata);
- CreateNative("CustomAchievement.ResetByAccountID", Native_ResetAchievement);
-
- return APLRes_Success;
- }
- ConVar g_ConVarDebug;
- Database g_Database;
- DBStatement g_QueryRegisterAchievement;
- DBStatement g_QueryGetAchievementIdentifier;
- Handle g_ForwardOnCustomAchievementAwarded;
- float g_flLastEarnedAchievementTime[MAXPLAYERS+1];
- public void OnPluginStart() {
- g_ConVarDebug = CreateConVar("custom_achievement_debug", "0", "Generate debug spew?");
-
- char error[64];
-
- if ((g_Database = SQLite_UseDatabase("custom-achievements", error, sizeof(error)))
- == INVALID_HANDLE) {
- SetFailState("Could not use database custom-achievements for custom achievements: %s",
- error);
- }
-
- g_QueryRegisterAchievement = SQL_PrepareQuery(g_Database,
- "INSERT OR IGNORE INTO achievements (name, displaytype) VALUES (?, ?)",
- error, sizeof(error));
-
- if (!g_QueryRegisterAchievement) {
- SetFailState("Could not create achievement instantiation query: %s", error);
- }
-
- g_QueryGetAchievementIdentifier = SQL_PrepareQuery(g_Database,
- "SELECT achievement_id FROM achievements WHERE name = ?",
- error, sizeof(error));
-
- if (!g_QueryGetAchievementIdentifier) {
- SetFailState("Could not create achievement identifier query: %s", error);
- }
-
- g_ForwardOnCustomAchievementAwarded = CreateGlobalForward("OnCustomAchievementAwarded",
- ET_Ignore, Param_Cell, Param_Cell);
-
- HookEvent("achievement_earned", OnAchievementEarned);
- }
- public void OnClientConnected(int client) {
- g_flLastEarnedAchievementTime[client] = 0.0;
- }
- public void OnAchievementEarned(Event event, const char[] name, bool dontBroadcast) {
- int recipient = event.GetInt("player");
-
- if (recipient && recipient <= MaxClients) {
- g_flLastEarnedAchievementTime[recipient] = GetGameTime();
- }
- }
- public int Native_RegisterAchievement(Handle hPlugin, int argc) {
- int internalNameLength;
- GetNativeStringLength(1, internalNameLength);
- internalNameLength++;
-
- char[] internalName = new char[internalNameLength];
- GetNativeString(1, internalName, internalNameLength);
-
- int achievementStyle = GetNativeCell(2);
-
- // Attempt to fetch achievement first, creating it if it doesn't exist.
- g_QueryGetAchievementIdentifier.BindString(0, internalName, false);
- DBResultSet achievementResults = SQL_ExecuteStatement(g_QueryGetAchievementIdentifier);
-
- int iAchievement;
- if (achievementResults.FetchRow()) {
- iAchievement = achievementResults.FetchInt(0);
- } else {
- g_QueryRegisterAchievement.BindString(0, internalName, false);
- g_QueryRegisterAchievement.BindInt(1, achievementStyle, false);
-
- DBResultSet registerResults = SQL_ExecuteStatement(g_QueryRegisterAchievement);
-
- iAchievement = registerResults.InsertId;
- }
-
- LogDebug("Achievement '%s' registered under identifier %d", internalName, iAchievement);
- return iAchievement;
- }
- public int Native_AwardAchievement(Handle hPlugin, int argc) {
- int iAchievement = GetNativeCell(1);
- int steamid3 = GetNativeCell(2);
- bool notify = GetNativeCell(3) != 0;
-
- char query[1024];
-
- Format(query, sizeof(query), "SELECT achieved FROM achievement_status "
- ... "WHERE steamid3 = '%d' AND achievement_id = '%d'",
- steamid3, iAchievement);
-
- DataPack recipientData = new DataPack();
- recipientData.WriteCell(iAchievement);
- recipientData.WriteCell(steamid3);
- recipientData.WriteCell(notify);
-
- LogDebug("Awarding achievement %d to steamid %d", iAchievement, steamid3);
- SQL_TQuery(g_Database, SQLT_OnPreAchievementAwarded, query, recipientData);
- }
- public void SQLT_OnPreAchievementAwarded(Handle hOwner, Handle hChild, const char[] error,
- DataPack recipientData) {
- recipientData.Reset();
-
- int iAchievement = recipientData.ReadCell();
- int steamid3 = recipientData.ReadCell();
- bool notify = recipientData.ReadCell() != 0;
- delete recipientData;
-
- DBResultSet resultSet = view_as<DBResultSet>(hChild);
-
- if (!resultSet || !resultSet.FetchRow() || resultSet.FetchInt(0) == 0) {
- LogDebug("Account %d does not have achievement yet. Granting.", steamid3);
-
- char query[1024];
-
- Transaction transaction = new Transaction();
-
- // update player row to indicate achievement
- Format(query, sizeof(query), "UPDATE OR IGNORE achievement_status SET achieved=%d "
- ... "WHERE steamid3 = '%d' AND achievement_id = '%d'",
- GetTime(), steamid3, iAchievement);
- transaction.AddQuery(query);
-
- // create new row for player achievement if it doesn't exist
- Format(query, sizeof(query), "INSERT OR IGNORE INTO achievement_status "
- ..."(achievement_id, steamid3, achieved) VALUES (%d, %d, %d)",
- iAchievement, steamid3, GetTime());
- transaction.AddQuery(query);
-
- // transaction is autoclosed
- SQL_ExecuteTransaction(g_Database, transaction);
-
- if (notify) {
- // perform query to get all localized strings, then fetch each and send to clients as appropriate
- CallOnCustomAchievementAwarded(steamid3, iAchievement);
- }
- }
- }
- public int Native_FetchAchievementMetadata(Handle hPlugin, int argc) {
- int iAchievement = GetNativeCell(1);
- int steamid3 = GetNativeCell(2);
-
- DataPack fetchPack = new DataPack();
- fetchPack.WriteCell(iAchievement);
- fetchPack.WriteCell(hPlugin);
- fetchPack.WriteFunction(GetNativeFunction(3));
- fetchPack.WriteCell(GetNativeCell(4));
-
- char query[1024];
- Format(query, sizeof(query), "SELECT metadata FROM achievement_status "
- ... "WHERE achievement_id = %d AND steamid3 = %d", iAchievement, steamid3);
-
- SQL_TQuery(g_Database, SQLT_OnMetadataReceived, query, fetchPack);
- }
- public void SQLT_OnMetadataReceived(Handle owner, Handle child, const char[] error,
- DataPack fetchPack) {
- fetchPack.Reset();
-
- int iAchievement = fetchPack.ReadCell();
- Handle hPlugin = fetchPack.ReadCell();
- Function callback = fetchPack.ReadFunction();
- any data = fetchPack.ReadCell();
- delete fetchPack;
-
- if (GetPluginStatus(hPlugin) != Plugin_Running) {
- ThrowError("Calling plugin was unloaded before a query callback");
- return;
- }
-
- Call_StartFunction(hPlugin, callback);
- Call_PushCell(iAchievement);
-
- DBResultSet resultSet = view_as<DBResultSet>(child);
- if (resultSet && resultSet.FetchRow()) {
- int metadataLength = resultSet.FetchSize(0) + 1;
- char[] metadata = new char[metadataLength];
- resultSet.FetchString(0, metadata, metadataLength);
-
- Call_PushString(metadata);
-
- /**
- * quirk: we have to finish the call before metadata goes out of scope, otherwise the
- * callback gets an empty string???
- */
- Call_PushCell(data);
- Call_Finish();
- } else {
- Call_PushString("");
- Call_PushCell(data);
- Call_Finish();
- }
- }
- public int Native_StoreAchievementMetadata(Handle hPlugin, int argc) {
- int iAchievement = GetNativeCell(1);
- int steamid3 = GetNativeCell(2);
- int metadataLength;
-
- GetNativeStringLength(3, metadataLength);
- metadataLength++;
- metadataLength *= 2; // buffer must be at least 2*strlen(string)+1 [when SQL escaping]
-
- char[] metadata = new char[metadataLength];
- GetNativeString(3, metadata, metadataLength);
-
- SQL_EscapeString(g_Database, metadata, metadata, metadataLength);
-
- int queryLength = metadataLength + 1024;
- char[] query = new char[queryLength];
-
- // attempt to update an existing row
- Transaction transaction = new Transaction();
- Format(query, queryLength, "UPDATE OR IGNORE achievement_status SET metadata='%s' "
- ... "WHERE steamid3 = '%d' AND achievement_id = '%d'",
- metadata, steamid3, iAchievement);
- transaction.AddQuery(query);
-
- // create row if it doesn't
- Format(query, queryLength, "INSERT OR IGNORE INTO achievement_status "
- ..."(achievement_id, steamid3, metadata) VALUES (%d, %d, '%s')",
- iAchievement, steamid3, metadata);
- transaction.AddQuery(query);
-
- // transaction is autoclosed
- SQL_ExecuteTransaction(g_Database, transaction);
-
- LogDebug("Wrote metadata for achievement %d under steamid3 %d (%s)", iAchievement, steamid3,
- metadata);
- }
- public int Native_ResetAchievement(Handle hPlugin, int argc) {
- int iAchievement = GetNativeCell(1);
- int steamid3 = GetNativeCell(2);
-
- char query[256];
-
- // We update the entries instead of deleting the row in case the row is regenerated anyways
- Transaction transaction = new Transaction();
- Format(query, sizeof(query), "UPDATE OR IGNORE achievement_status "
- ... "SET metadata='', achieved=0 "
- ... "WHERE steamid3 = '%d' AND achievement_id = '%d'",
- steamid3, iAchievement);
- transaction.AddQuery(query);
-
- SQL_ExecuteTransaction(g_Database, transaction);
-
- LogDebug("Reset achievement %d for steamid3 %d", iAchievement, steamid3);
- }
- void CallOnCustomAchievementAwarded(int steamid3, int iAchievement) {
- int recipient = FindClientByAccountID(steamid3);
-
- if (recipient) {
- char recipientName[MAX_NAME_LENGTH];
- GetClientName(recipient, recipientName, sizeof(recipientName));
-
- // int nClients;
- // int clients[MAXPLAYERS+1];
-
- TransmitAchievementEvent(recipient, iAchievement);
-
- Call_StartForward(g_ForwardOnCustomAchievementAwarded);
- Call_PushCell(recipient);
- Call_PushCell(iAchievement);
- Call_Finish();
-
- LogDebug("%N was awarded achievement %d", recipient, iAchievement);
- }
- }
- /**
- * Prepares the achievement event by pulling localization strings.
- * If no localization strings are available, the achievement effects (particles and sound) are
- * not played.
- */
- void TransmitAchievementEvent(int recipient, int iAchievement) {
- char query[512];
-
- Format(query, sizeof(query),
- "SELECT language_shortcode, achievement_local_name, achievement_local_description "
- ... "FROM achievement_languages JOIN achievements ON achievement_name = name "
- ... "WHERE achievement_id = '%d'", iAchievement);
-
- DataPack pack = new DataPack();
- pack.WriteCell(GetClientUserId(recipient));
- pack.WriteCell(iAchievement);
-
- SQL_TQuery(g_Database, SQLT_OnLocalizedAchievementStringsReceived, query, pack);
- }
- public void SQLT_OnLocalizedAchievementStringsReceived(Handle hOwner, Handle hChild,
- const char[] error, DataPack pack) {
- pack.Reset();
- int recipient = GetClientOfUserId(pack.ReadCell());
- int iAchievement = pack.ReadCell();
-
- if (recipient) {
- DBResultSet resultSet = view_as<DBResultSet>(hChild);
-
- if (resultSet && resultSet.RowCount > 0) {
- KeyValues localizedStrings = new KeyValues("Achievement");
-
- // Put all name / descriptions into sections corresponding to language shortcodes.
- while (resultSet.FetchRow()) {
- int nNameLength, nDescriptionLength;
-
- char languageShortCode[MAX_LANG_SC_LENGTH];
- resultSet.FetchString(0, languageShortCode, sizeof(languageShortCode));
-
- nNameLength = resultSet.FetchSize(1) + 1;
- char[] achievementName = new char[nNameLength];
- resultSet.FetchString(1, achievementName, nNameLength);
-
- nDescriptionLength = resultSet.FetchSize(2) + 1;
- char[] achievementDescription = new char[nDescriptionLength];
- resultSet.FetchString(2, achievementDescription, nDescriptionLength);
-
- if (localizedStrings.JumpToKey(languageShortCode, true)) {
- localizedStrings.SetString("name", achievementName);
- localizedStrings.SetString("description", achievementDescription);
-
- localizedStrings.GoBack();
- }
- }
- localizedStrings.Rewind();
-
- char serverLanguageShortCode[MAX_LANG_SC_LENGTH];
- GetLanguageInfo(GetServerLanguage(), serverLanguageShortCode,
- sizeof(serverLanguageShortCode));
-
- // Ensure the server's language is localized as a fallback measure.
- if (localizedStrings.JumpToKey(serverLanguageShortCode)) {
- localizedStrings.GoBack();
-
- // Identify and send localized translations to each client, if possible.
- for (int i = 1; i <= MaxClients; i++) {
- if (IsClientInGame(i) && !IsFakeClient(i)) {
- char languageShortCode[MAX_LANG_SC_LENGTH];
-
- GetLanguageInfo(GetClientLanguage(i), languageShortCode,
- sizeof(languageShortCode));
-
- // Use server (default) language if client language isn't available.
- if (localizedStrings.JumpToKey(languageShortCode)
- || localizedStrings.JumpToKey(serverLanguageShortCode)) {
- char localName[64], localDescription[256];
-
- localizedStrings.GetString("name", localName, sizeof(localName));
- localizedStrings.GetString("description", localDescription,
- sizeof(localDescription));
-
- SendAchievementMessageToOne(recipient, i, localName,
- localDescription);
-
- localizedStrings.GoBack();
- }
- }
- }
-
- TransmitAchievementEffects(recipient);
-
- g_flLastEarnedAchievementTime[recipient] = GetGameTime();
- } else {
- LogMessage("Achievement %d is missing server / fallback localization entries "
- ... "in server's language '%s'. Achievement will not be displayed.",
- iAchievement, serverLanguageShortCode);
- }
-
- delete localizedStrings;
- } else {
- LogMessage("Achievement %d does not have any localized strings.", iAchievement);
- }
- }
-
- delete pack;
- }
- /**
- * Plays the achievement sound and displays the related particle on the recipient.
- */
- void TransmitAchievementEffects(int recipient) {
- /**
- * TODO add temporal filter if event `achievement_earned` or this function was called within
- * the last second so the effect is only used once.
- */
- if (GetGameTime() - g_flLastEarnedAchievementTime[recipient] > 1.0) {
- TFTeam recipientTeam = TF2_GetClientTeam(recipient);
- if (recipientTeam != TFTeam_Unassigned && recipientTeam != TFTeam_Spectator) {
- EmitGameSoundToAll("Achievement.Earned", recipient);
-
- // TODO prevent custom models from allowing this?
- if (IsPlayerAlive(recipient)) {
- TE_SetupTFParticleEffect("achieved", NULL_VECTOR, NULL_VECTOR, NULL_VECTOR,
- recipient, PATTACH_POINT_FOLLOW, ATTACHMENT_HEAD);
- TE_SendToAll();
- }
- }
- }
- }
- /**
- * Returns a client with the specified Steam account ID, or 0 if none.
- */
- stock int FindClientByAccountID(int steamid3) {
- if (steamid3) {
- for (int i = 1; i <= MaxClients; i++) {
- if (IsClientConnected(i) && !IsFakeClient(i) && IsClientAuthorized(i)) {
- if (steamid3 == GetSteamAccountID(i)) {
- return i;
- }
- }
- }
- }
- return 0;
- }
- /**
- * Sends the "<recipient> has earned the achievement <achievement>" message to the specified
- * client.
- *
- * If a description is specified, the description is enclosed in parentheses.
- */
- void SendAchievementMessageToOne(int recipient, int client, const char[] achievementName,
- const char[] achievementDescription = "") {
- char recipientName[MAX_NAME_LENGTH];
- GetClientName(recipient, recipientName, sizeof(recipientName));
-
- int clients[1];
- clients[0] = client;
-
- if (!strlen(achievementDescription)) {
- // no description provided
- SendSayText2Message(recipient, clients, 1, "#Achievement_Earned", recipientName,
- achievementName);
- } else {
- int bufferLength = strlen(achievementName) + strlen(achievementDescription) + 16;
- char[] achievementBuffer = new char[bufferLength];
- Format(achievementBuffer, bufferLength, "%s \x01(%s)", achievementName,
- achievementDescription);
-
- SendSayText2Message(recipient, clients, 1, "#Achievement_Earned", recipientName,
- achievementBuffer);
- }
- }
- void SendSayText2Message(int author, int[] clients, int nClients,
- const char[] localizationToken, const char[] name, const char[] message) {
- Handle buffer = StartMessage("SayText2", clients, nClients,
- USERMSG_RELIABLE | USERMSG_BLOCKHOOKS);
-
- BfWrite bitbuf = view_as<BfWrite>(buffer);
-
- bitbuf.WriteByte(author);
- bitbuf.WriteByte(true);
- bitbuf.WriteString(localizationToken);
- bitbuf.WriteString(name);
- bitbuf.WriteString(message);
-
- EndMessage();
- }
- void LogDebug(const char[] format, any ...) {
- if (g_ConVarDebug.BoolValue) {
- char message[256];
- VFormat(message, sizeof(message), format, 2);
- LogServer(message);
- }
- }
- /**
- * Executes a prepared statement and returns it as a result set view.
- * The handle should not be closed.
- */
- DBResultSet SQL_ExecuteStatement(DBStatement query) {
- SQL_Execute(query);
- return view_as<DBResultSet>(query);
- }
|