diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5263c1d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,49 @@ +name: Demo Check Autobuild +# shamelessly stolen from https://github.com/sapphonie/StAC-tf2/blob/master/.github/workflows/blank.yml - thanks sapph! + +on: + push: + tags: + - 'v*' + +jobs: + run: + name: Run action + runs-on: ubuntu-latest + + # skip build on '[ci skip]' + if: "!contains(github.event.head_commit.message, '[ci skip]')" + # this angers the ~linter~ + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Setup SourcePawn Compiler + uses: rumblefrog/setup-sp@master + with: + version: '1.12.x' + + - name: Compile Plugins + run: | + cd ./scripting + pwd + spcomp -i"./include/" demo_check.sp -o ../plugins/demo_check.smx + ls -la + + - name: Zip packages + run: | + mkdir build + 7za a -r build/demo_check.zip scripting/ plugins/ extensions/ translations/ + ls -la + pwd + + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + ./build/demo_check.zip + fail_on_unmatched_files: true + generate_release_notes: true diff --git a/.gitignore b/.gitignore index a054f3e..957a7da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ -plugins +# ignore my ide .vscode -demo_check.zip diff --git a/README.md b/README.md index 6344027..d3f7ae2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This plugin is used to check if players are recording demos or not. ## Compiling -Requires [morecolors.inc](https://github.com/DoctorMcKay/sourcemod-plugins/blob/master/scripting/include/morecolors.inc) +All includes and extensions are bundled with this repository. Special thanks to [Dr. McKay](https://github.com/DoctorMcKay/sourcemod-plugins/blob/master/scripting/include/morecolors.inc) and [Sapphonie](https://github.com/sapphonie/StAC-tf2) from whom I shamelessly stole morecolors.inc and SteamWorks.inc/discord.inc (plus extensions) from. ## Installation @@ -19,12 +19,37 @@ The plugin has a few cvars that can be configured: - `sm_democheck_enabled <0/1>` - Enable or disable the plugin. Default: `1` - `sm_democheck_onreadyup <0/1>` - Performs an additional check at ready up. Requires SoapDM to be running. Default: `0` +- `sm_democheck_warn <0/1>` - Set the plugin into warning only mode. Default: `0`. If enabled, players will be warned if they are not recording demos, but will not be kicked. Additionally if your use case requires different languages or links to documentation, you can modify the `demo_check.phrases.txt` file in the `translations` directory. Currently only English is supported, and existing documentation links are for ozfortress. +We've also included Discord Webhook support! Starting from version 1.1.0, you can now configure the plugin to send a message to a Discord webhook when a player is kicked for not recording demos. To enable this feature, you will need to set the following cvars: + +- `sm_democheck_announce_discord <0/1>` - Enable or disable the Discord webhook feature. Default: `0` + +Additionally, modify `/tf/addons/sourcemod/configs/discord.cfg` with the following: + +```cfg +"Discord" +{ + "democheck" + { + "url" "discord webhook url" + } +} +``` + +Trust me, it'll be a riot to watch. We don't set the avatar by the way, just the message. Set the avatar and username in the webhook settings on Discord. + +Starting from 1.1.0, we've also enabled silencing of the check messages. This is useful if you want to run the plugin in the background without notifying players. To enable this feature, you will need to set the following cvars: + +- `sm_democheck_announce <0/1>` - Enable or disable the announce feature. Default: `1` (enabled) + +Players will still be told they're being kicked, and why. But they won't be alerted if the check passed. + ## Commands -All commands are server side only. +All commands are server side only. Yes, they can be used with RCON, and honestly it'd be funnier that way. - `sm_democheck <#userid>` - Check if a given player is recording demos. - `sm_democheck_all` - Check if all players are recording demos. @@ -45,3 +70,5 @@ At this stage, the plugin only performs this check when manually triggered, when ## License This project is licensed under the GNU GPL 3.0 License - see the [LICENSE](LICENSE) file for details. + +Parts of this project may be licensed under different licenses. See their sources for more information. diff --git a/extensions/SteamWorks.ext.dll b/extensions/SteamWorks.ext.dll new file mode 100644 index 0000000..1681977 Binary files /dev/null and b/extensions/SteamWorks.ext.dll differ diff --git a/extensions/SteamWorks.ext.so b/extensions/SteamWorks.ext.so new file mode 100644 index 0000000..4ded487 Binary files /dev/null and b/extensions/SteamWorks.ext.so differ diff --git a/plugins/demo_check.smx b/plugins/demo_check.smx new file mode 100644 index 0000000..3c286fe Binary files /dev/null and b/plugins/demo_check.smx differ diff --git a/plugins/discord.smx b/plugins/discord.smx new file mode 100644 index 0000000..c3d65ba Binary files /dev/null and b/plugins/discord.smx differ diff --git a/scripting/demo_check.sp b/scripting/demo_check.sp index 6492dc0..c9ecca6 100644 --- a/scripting/demo_check.sp +++ b/scripting/demo_check.sp @@ -17,6 +17,8 @@ #include #include #include +#include +#include #define DEMOCHECK_TAG "{lime}[{red}Demo Check{lime}]{white} " @@ -25,12 +27,17 @@ public Plugin:myinfo = name = "Demo Check", author = "Shigbeard", description = "Checks if a player is recording a demo", - version = "1.0.1", + version = "1.1.0", url = "https://ozfortress.com/" }; ConVar g_bDemoCheckEnabled; ConVar g_bDemoCheckOnReadyUp; // Requires SoapDM +ConVar g_bDemoCheckWarn; +ConVar g_bDemoCheckAnnounce; +ConVar g_bDemoCheckAnnounceDiscord; // Requires Discord +ConVar g_HostName; +ConVar g_HostPort; public void OnPluginStart() { @@ -39,6 +46,10 @@ public void OnPluginStart() g_bDemoCheckEnabled = CreateConVar("sm_democheck_enabled", "1", "Enable demo check", FCVAR_NOTIFY, true, 0.0, true, 1.0); g_bDemoCheckOnReadyUp = CreateConVar("sm_democheck_onreadyup", "0", "Check if all players are recording a demo when both teams ready up - requires SoapDM", FCVAR_NOTIFY, true, 0.0, true, 1.0); + g_bDemoCheckWarn = CreateConVar("sm_democheck_warn", "0", " Set the plugin into warning only mode.", FCVAR_NOTIFY, true, 0.0, true, 1.0); + g_bDemoCheckAnnounce = CreateConVar("sm_democheck_announce", "1", "Announce passed demo checks to chat", FCVAR_NOTIFY, true, 0.0, true, 1.0); + g_bDemoCheckAnnounceDiscord = CreateConVar("sm_democheck_announce_discord", "0", "Announce failed demo checks to discord", FCVAR_NOTIFY, true, 0.0, true, 1.0); + RegServerCmd("sm_democheck", Cmd_DemoCheck_Console, "Check if a player is recording a demo", 0); RegServerCmd("sm_democheck_enable", Cmd_DemoCheckEnable_Console, "Enable demo check", 0); RegServerCmd("sm_democheck_disable", Cmd_DemoCheckDisable_Console, "Disable demo check", 0); @@ -46,6 +57,8 @@ public void OnPluginStart() HookConVarChange(g_bDemoCheckEnabled, OnDemoCheckEnabledChange) + g_HostName = FindConVar("hostname"); + g_HostPort = FindConVar("hostport"); } public void SOAP_StopDeathMatching() @@ -77,7 +90,10 @@ public void OnDemoCheckEnabledChange(ConVar convar, const char[] oldValue, const { if (GetConVarBool(g_bDemoCheckEnabled)) { - CPrintToChatAll(DEMOCHECK_TAG ... "%t", "enabled"); + if (GetConVarBool(g_bDemoCheckAnnounce)) + { + CPrintToChatAll(DEMOCHECK_TAG ... "%t", "enabled"); + } for (int i = 1; i <= MaxClients; i++) { if (IsClientInGame(i)) @@ -88,7 +104,10 @@ public void OnDemoCheckEnabledChange(ConVar convar, const char[] oldValue, const } else { - CPrintToChatAll(DEMOCHECK_TAG ... "%t", "disabled"); + if (GetConVarBool(g_bDemoCheckAnnounce)) + { + CPrintToChatAll(DEMOCHECK_TAG ... "%t", "disabled"); + } } } @@ -171,7 +190,10 @@ public void OnDSEnableCheck(QueryCookie cookie, int client, ConVarQueryResult re { if (StrEqual(value, "3")) { - CPrintToChat(client, DEMOCHECK_TAG ... "%t", "ds_enabled 3"); + if (GetConVarBool(g_bDemoCheckAnnounce)) + { + CPrintToChat(client, DEMOCHECK_TAG ... "%t", "ds_enabled 3"); + } } else if(StrEqual(value, "0")) { @@ -179,17 +201,33 @@ public void OnDSEnableCheck(QueryCookie cookie, int client, ConVarQueryResult re PrintToConsole(client, "[Demo Check] %t", "docs"); char sName[64]; GetClientName(client, sName, sizeof(sName)); - CreateTimer(2.0, Timer_KickClient, client); - CPrintToChatAll(DEMOCHECK_TAG ... "%t", "kicked_announce", sName); + + if (GetConVarBool(g_bDemoCheckWarn)) + { + if (GetConVarBool(g_bDemoCheckAnnounce)) + { + CPrintToChatAll(DEMOCHECK_TAG ... "%t", "kicked_announce_disabled", sName); + } + } else { + CreateTimer(2.0, Timer_KickClient, client); + CPrintToChatAll(DEMOCHECK_TAG ... "%t", "kicked_announce", sName); + } } else { PrintToConsole(client, "[Demo Check] %t", "ds_enabled 0"); PrintToConsole(client, "[Demo Check] %t", "docs"); char sName[64]; - GetClientName(client, sName, sizeof(sName)); - CreateTimer(2.0, Timer_KickClient, client); - CPrintToChatAll(DEMOCHECK_TAG ... "%t", "kicked_announce", sName); + if (GetConVarBool(g_bDemoCheckWarn)) + { + if (GetConVarBool(g_bDemoCheckAnnounce)) + { + CPrintToChatAll(DEMOCHECK_TAG ... "%t", "kicked_announce_disabled", sName); + } + } else { + CreateTimer(2.0, Timer_KickClient, client); + CPrintToChatAll(DEMOCHECK_TAG ... "%t", "kicked_announce", sName); + } } } @@ -201,17 +239,69 @@ public void OnDSAutoDeleteCheck(QueryCookie cookie, int client, ConVarQueryResul PrintToConsole(client, "[Demo Check] %t", "docs"); char sName[64]; GetClientName(client, sName, sizeof(sName)); - CreateTimer(2.0, Timer_KickClient, client); - CPrintToChatAll(DEMOCHECK_TAG ... "%t", "kicked_announce", sName); + if (GetConVarBool(g_bDemoCheckWarn)) + { + if (GetConVarBool(g_bDemoCheckAnnounce)) + { + CPrintToChatAll(DEMOCHECK_TAG ... "%t", "kicked_announce_disabled", sName); + } + } else { + CreateTimer(2.0, Timer_KickClient, client); + CPrintToChatAll(DEMOCHECK_TAG ... "%t", "kicked_announce", sName); + } } else { - CPrintToChat(client, DEMOCHECK_TAG ... "%t", "ds_autodelete 0"); + if (GetConVarBool(g_bDemoCheckAnnounce)) + { + CPrintToChat(client, DEMOCHECK_TAG ... "%t", "ds_autodelete 0"); + } } } public Action Timer_KickClient(Handle timer, int client) { + if (!IsClientInGame(client)) + { + return Plugin_Stop; + } + if (GetConVarBool(g_bDemoCheckAnnounceDiscord)) + { + char sName[64]; + char sSteamID[64]; + char sProfileURL[64]; + char sServerName[64]; + int iServerIP[4]; + int iServerPort; + char sServerIP[64]; + GetClientName(client, sName, sizeof(sName)); + GetClientAuthId(client, AuthId_Steam2, sSteamID, sizeof(sSteamID)); + GetClientAuthId(client, AuthId_SteamID64, sProfileURL, sizeof(sProfileURL)); + Format(sProfileURL, sizeof(sProfileURL), "https://steamcommunity.com/profiles/%s", sProfileURL); + char sMsg[512]; + if (g_HostName == INVALID_HANDLE) + { + g_HostName = FindConVar("hostname"); + if (g_HostName == INVALID_HANDLE) + { + Format(sServerName, sizeof(sServerName), "Unknown Server"); + } + } + if (g_HostPort == INVALID_HANDLE) + { + g_HostPort = FindConVar("hostport"); + if (g_HostPort == INVALID_HANDLE) + { + iServerPort = 27015; + } + } + GetConVarString(g_HostName, sServerName, sizeof(sServerName)); + iServerPort = GetConVarInt(g_HostPort); + SteamWorks_GetPublicIP(iServerIP); + Format(sServerIP, sizeof(sServerIP), "%i.%i.%i.%i:%i", iServerIP[0], iServerIP[1], iServerIP[2], iServerIP[3], iServerPort); + Format(sMsg, sizeof(sMsg), "[Demo Check] %t", "discord_democheck", sName, sSteamID, sProfileURL, sServerName, sServerIP); + Discord_SendMessage("democheck", sMsg); + } KickClient(client, "[Demo Check] %t", "kicked"); return Plugin_Stop; } diff --git a/scripting/include/SteamWorks.inc b/scripting/include/SteamWorks.inc new file mode 100644 index 0000000..9df23c6 --- /dev/null +++ b/scripting/include/SteamWorks.inc @@ -0,0 +1,383 @@ +#if defined _SteamWorks_Included + #endinput +#endif +#define _SteamWorks_Included + +#pragma semicolon 1 +#pragma newdecls required + +// https://partner.steamgames.com/doc/api + +/* results from UserHasLicenseForApp */ +enum EUserHasLicenseForAppResult +{ + k_EUserHasLicenseResultHasLicense = 0, // User has a license for specified app + k_EUserHasLicenseResultDoesNotHaveLicense = 1, // User does not have a license for the specified app + k_EUserHasLicenseResultNoAuth = 2, // User has not been authenticated +}; + +/* General result codes */ +enum EResult +{ + k_EResultOK = 1, // success + k_EResultFail = 2, // generic failure + k_EResultNoConnection = 3, // no/failed network connection +// k_EResultNoConnectionRetry = 4, // OBSOLETE - removed + k_EResultInvalidPassword = 5, // password/ticket is invalid + k_EResultLoggedInElsewhere = 6, // same user logged in elsewhere + k_EResultInvalidProtocolVer = 7, // protocol version is incorrect + k_EResultInvalidParam = 8, // a parameter is incorrect + k_EResultFileNotFound = 9, // file was not found + k_EResultBusy = 10, // called method busy - action not taken + k_EResultInvalidState = 11, // called object was in an invalid state + k_EResultInvalidName = 12, // name is invalid + k_EResultInvalidEmail = 13, // email is invalid + k_EResultDuplicateName = 14, // name is not unique + k_EResultAccessDenied = 15, // access is denied + k_EResultTimeout = 16, // operation timed out + k_EResultBanned = 17, // VAC2 banned + k_EResultAccountNotFound = 18, // account not found + k_EResultInvalidSteamID = 19, // steamID is invalid + k_EResultServiceUnavailable = 20, // The requested service is currently unavailable + k_EResultNotLoggedOn = 21, // The user is not logged on + k_EResultPending = 22, // Request is pending (may be in process, or waiting on third party) + k_EResultEncryptionFailure = 23, // Encryption or Decryption failed + k_EResultInsufficientPrivilege = 24, // Insufficient privilege + k_EResultLimitExceeded = 25, // Too much of a good thing + k_EResultRevoked = 26, // Access has been revoked (used for revoked guest passes) + k_EResultExpired = 27, // License/Guest pass the user is trying to access is expired + k_EResultAlreadyRedeemed = 28, // Guest pass has already been redeemed by account, cannot be acked again + k_EResultDuplicateRequest = 29, // The request is a duplicate and the action has already occurred in the past, ignored this time + k_EResultAlreadyOwned = 30, // All the games in this guest pass redemption request are already owned by the user + k_EResultIPNotFound = 31, // IP address not found + k_EResultPersistFailed = 32, // failed to write change to the data store + k_EResultLockingFailed = 33, // failed to acquire access lock for this operation + k_EResultLogonSessionReplaced = 34, + k_EResultConnectFailed = 35, + k_EResultHandshakeFailed = 36, + k_EResultIOFailure = 37, + k_EResultRemoteDisconnect = 38, + k_EResultShoppingCartNotFound = 39, // failed to find the shopping cart requested + k_EResultBlocked = 40, // a user didn't allow it + k_EResultIgnored = 41, // target is ignoring sender + k_EResultNoMatch = 42, // nothing matching the request found + k_EResultAccountDisabled = 43, + k_EResultServiceReadOnly = 44, // this service is not accepting content changes right now + k_EResultAccountNotFeatured = 45, // account doesn't have value, so this feature isn't available + k_EResultAdministratorOK = 46, // allowed to take this action, but only because requester is admin + k_EResultContentVersion = 47, // A Version mismatch in content transmitted within the Steam protocol. + k_EResultTryAnotherCM = 48, // The current CM can't service the user making a request, user should try another. + k_EResultPasswordRequiredToKickSession = 49, // You are already logged in elsewhere, this cached credential login has failed. + k_EResultAlreadyLoggedInElsewhere = 50, // You are already logged in elsewhere, you must wait + k_EResultSuspended = 51, // Long running operation (content download) suspended/paused + k_EResultCancelled = 52, // Operation canceled (typically by user: content download) + k_EResultDataCorruption = 53, // Operation canceled because data is ill formed or unrecoverable + k_EResultDiskFull = 54, // Operation canceled - not enough disk space. + k_EResultRemoteCallFailed = 55, // an remote call or IPC call failed + k_EResultPasswordUnset = 56, // Password could not be verified as it's unset server side + k_EResultExternalAccountUnlinked = 57, // External account (int PSN, Facebook...) is not linked to a Steam account + k_EResultPSNTicketInvalid = 58, // PSN ticket was invalid + k_EResultExternalAccountAlreadyLinked = 59, // External account (int PSN, Facebook...) is already linked to some other account, must explicitly request to replace/delete the link first + k_EResultRemoteFileConflict = 60, // The sync cannot resume due to a conflict between the local and remote files + k_EResultIllegalPassword = 61, // The requested new password is not legal + k_EResultSameAsPreviousValue = 62, // new value is the same as the old one ( secret question and answer ) + k_EResultAccountLogonDenied = 63, // account login denied due to 2nd factor authentication failure + k_EResultCannotUseOldPassword = 64, // The requested new password is not legal + k_EResultInvalidLoginAuthCode = 65, // account login denied due to auth code invalid + k_EResultAccountLogonDeniedNoMail = 66, // account login denied due to 2nd factor auth failure - and no mail has been sent + k_EResultHardwareNotCapableOfIPT = 67, + k_EResultIPTInitError = 68, + k_EResultParentalControlRestricted = 69, // operation failed due to parental control restrictions for current user + k_EResultFacebookQueryError = 70, // Facebook query returned an error + k_EResultExpiredLoginAuthCode = 71, // account login denied due to auth code expired + k_EResultIPLoginRestrictionFailed = 72, + k_EResultAccountLockedDown = 73, + k_EResultAccountLogonDeniedVerifiedEmailRequired = 74, + k_EResultNoMatchingURL = 75, + k_EResultBadResponse = 76, // parse failure, missing field, etc. + k_EResultRequirePasswordReEntry = 77, // The user cannot complete the action until they re-enter their password + k_EResultValueOutOfRange = 78, // the value entered is outside the acceptable range + k_EResultUnexpectedError = 79, // something happened that we didn't expect to ever happen + k_EResultDisabled = 80, // The requested service has been configured to be unavailable + k_EResultInvalidCEGSubmission = 81, // The set of files submitted to the CEG server are not valid ! + k_EResultRestrictedDevice = 82, // The device being used is not allowed to perform this action + k_EResultRegionLocked = 83, // The action could not be complete because it is region restricted + k_EResultRateLimitExceeded = 84, // Temporary rate limit exceeded, try again later, different from k_EResultLimitExceeded which may be permanent + k_EResultAccountLoginDeniedNeedTwoFactor = 85, // Need two-factor code to login + k_EResultItemDeleted = 86, // The thing we're trying to access has been deleted + k_EResultAccountLoginDeniedThrottle = 87, // login attempt failed, try to throttle response to possible attacker + k_EResultTwoFactorCodeMismatch = 88, // two factor code mismatch + k_EResultTwoFactorActivationCodeMismatch = 89, // activation code for two-factor didn't match + k_EResultAccountAssociatedToMultiplePartners = 90, // account has been associated with multiple partners + k_EResultNotModified = 91, // data not modified + k_EResultNoMobileDevice = 92, // the account does not have a mobile device associated with it + k_EResultTimeNotSynced = 93, // the time presented is out of range or tolerance + k_EResultSmsCodeFailed = 94, // SMS code failure (no match, none pending, etc.) + k_EResultAccountLimitExceeded = 95, // Too many accounts access this resource + k_EResultAccountActivityLimitExceeded = 96, // Too many changes to this account + k_EResultPhoneActivityLimitExceeded = 97, // Too many changes to this phone + k_EResultRefundToWallet = 98, // Cannot refund to payment method, must use wallet + k_EResultEmailSendFailure = 99, // Cannot send an email + k_EResultNotSettled = 100, // Can't perform operation till payment has settled + k_EResultNeedCaptcha = 101, // Needs to provide a valid captcha + k_EResultGSLTDenied = 102, // a game server login token owned by this token's owner has been banned + k_EResultGSOwnerDenied = 103, // game server owner is denied for other reason (account lock, community ban, vac ban, missing phone) + k_EResultInvalidItemType = 104, // the type of thing we were requested to act on is invalid +}; + +/* This enum is used in client API methods, do not re-number existing values. */ +enum EHTTPMethod +{ + k_EHTTPMethodInvalid = 0, + k_EHTTPMethodGET, + k_EHTTPMethodHEAD, + k_EHTTPMethodPOST, + k_EHTTPMethodPUT, + k_EHTTPMethodDELETE, + k_EHTTPMethodOPTIONS, + + // The remaining HTTP methods are not yet supported, per rfc2616 section 5.1.1 only GET and HEAD are required for + // a compliant general purpose server. We'll likely add more as we find uses for them. + +// k_EHTTPMethodTRACE, +// k_EHTTPMethodCONNECT +}; + + +/* HTTP Status codes that the server can send in response to a request, see rfc2616 section 10.3 for descriptions + of each of these. */ +enum EHTTPStatusCode +{ + // Invalid status code (this isn't defined in HTTP, used to indicate unset in our code) + k_EHTTPStatusCodeInvalid = 0, + + // Informational codes + k_EHTTPStatusCode100Continue = 100, + k_EHTTPStatusCode101SwitchingProtocols = 101, + + // Success codes + k_EHTTPStatusCode200OK = 200, + k_EHTTPStatusCode201Created = 201, + k_EHTTPStatusCode202Accepted = 202, + k_EHTTPStatusCode203NonAuthoritative = 203, + k_EHTTPStatusCode204NoContent = 204, + k_EHTTPStatusCode205ResetContent = 205, + k_EHTTPStatusCode206PartialContent = 206, + + // Redirection codes + k_EHTTPStatusCode300MultipleChoices = 300, + k_EHTTPStatusCode301MovedPermanently = 301, + k_EHTTPStatusCode302Found = 302, + k_EHTTPStatusCode303SeeOther = 303, + k_EHTTPStatusCode304NotModified = 304, + k_EHTTPStatusCode305UseProxy = 305, +// k_EHTTPStatusCode306Unused = 306, // used in old HTTP spec, now unused in 1.1 + k_EHTTPStatusCode307TemporaryRedirect = 307, + + // Error codes + k_EHTTPStatusCode400BadRequest = 400, + k_EHTTPStatusCode401Unauthorized = 401, // You probably want 403 or something else. 401 implies you're sending a WWW-Authenticate header and the client can sent an Authorization header in response. + k_EHTTPStatusCode402PaymentRequired = 402, // This is reserved for future HTTP specs, not really supported by clients + k_EHTTPStatusCode403Forbidden = 403, + k_EHTTPStatusCode404NotFound = 404, + k_EHTTPStatusCode405MethodNotAllowed = 405, + k_EHTTPStatusCode406NotAcceptable = 406, + k_EHTTPStatusCode407ProxyAuthRequired = 407, + k_EHTTPStatusCode408RequestTimeout = 408, + k_EHTTPStatusCode409Conflict = 409, + k_EHTTPStatusCode410Gone = 410, + k_EHTTPStatusCode411LengthRequired = 411, + k_EHTTPStatusCode412PreconditionFailed= 412, + k_EHTTPStatusCode413RequestEntityTooLarge = 413, + k_EHTTPStatusCode414RequestURITooLong = 414, + k_EHTTPStatusCode415UnsupportedMediaType = 415, + k_EHTTPStatusCode416RequestedRangeNotSatisfiable = 416, + k_EHTTPStatusCode417ExpectationFailed = 417, + k_EHTTPStatusCode4xxUnknown = 418, // 418 is reserved, so we'll use it to mean unknown + k_EHTTPStatusCode429TooManyRequests = 429, + + // Server error codes + k_EHTTPStatusCode500InternalServerError = 500, + k_EHTTPStatusCode501NotImplemented = 501, + k_EHTTPStatusCode502BadGateway = 502, + k_EHTTPStatusCode503ServiceUnavailable = 503, + k_EHTTPStatusCode504GatewayTimeout = 504, + k_EHTTPStatusCode505HTTPVersionNotSupported = 505, + k_EHTTPStatusCode5xxUnknown = 599, +}; + +/* list of possible return values from the ISteamGameCoordinator API */ +enum EGCResults +{ + k_EGCResultOK = 0, + k_EGCResultNoMessage = 1, // There is no message in the queue + k_EGCResultBufferTooSmall = 2, // The buffer is too small for the requested message + k_EGCResultNotLoggedOn = 3, // The client is not logged onto Steam + k_EGCResultInvalidMessage = 4, // Something was wrong with the message being sent with SendMessage +}; + +native bool SteamWorks_IsVACEnabled(); +native bool SteamWorks_GetPublicIP(int ipaddr[4]); +native int SteamWorks_GetPublicIPCell(); +native bool SteamWorks_IsLoaded(); +native bool SteamWorks_SetGameDescription(const char[] sDesc); +native bool SteamWorks_SetMapName(const char[] sMapName); +native bool SteamWorks_IsConnected(); +native bool SteamWorks_SetRule(const char[] sKey, const char[] sValue); +native bool SteamWorks_ClearRules(); +native bool SteamWorks_ForceHeartbeat(); +native bool SteamWorks_GetUserGroupStatus(int client, int groupid); +native bool SteamWorks_GetUserGroupStatusAuthID(int authid, int groupid); + +native EUserHasLicenseForAppResult SteamWorks_HasLicenseForApp(int client, int app); +native EUserHasLicenseForAppResult SteamWorks_HasLicenseForAppId(int authid, int app); +native int SteamWorks_GetClientSteamID(int client, char[] sSteamID, int length); + +native bool SteamWorks_RequestStatsAuthID(int authid, int appid); +native bool SteamWorks_RequestStats(int client, int appid); +native bool SteamWorks_GetStatCell(int client, const char[] sKey, int &value); +native bool SteamWorks_GetStatAuthIDCell(int authid, const char[] sKey, int &value); +native bool SteamWorks_GetStatFloat(int client, const char[] sKey, float &value); +native bool SteamWorks_GetStatAuthIDFloat(int authid, const char[] sKey, float &value); + +native Handle SteamWorks_CreateHTTPRequest(EHTTPMethod method, const char[] sURL); +native bool SteamWorks_SetHTTPRequestContextValue(Handle hHandle, any data1, any data2=0); +native bool SteamWorks_SetHTTPRequestNetworkActivityTimeout(Handle hHandle, int timeout); +native bool SteamWorks_SetHTTPRequestHeaderValue(Handle hHandle, const char[] sName, const char[] sValue); +native bool SteamWorks_SetHTTPRequestGetOrPostParameter(Handle hHandle, const char[] sName, const char[] sValue); +native bool SteamWorks_SetHTTPRequestUserAgentInfo(Handle hHandle, const char[] sUserAgentInfo); +native bool SteamWorks_SetHTTPRequestRequiresVerifiedCertificate(Handle hHandle, bool bRequireVerifiedCertificate); +native bool SteamWorks_SetHTTPRequestAbsoluteTimeoutMS(Handle hHandle, int unMilliseconds); + + +typeset SteamWorksHTTPRequestCompleted +{ + function void (Handle hRequest, bool bFailure, bool bRequestSuccessful, EHTTPStatusCode eStatusCode); + function void (Handle hRequest, bool bFailure, bool bRequestSuccessful, EHTTPStatusCode eStatusCode, any data1); + function void (Handle hRequest, bool bFailure, bool bRequestSuccessful, EHTTPStatusCode eStatusCode, any data1, any data2); +}; + +typeset SteamWorksHTTPHeadersReceived +{ + function void (Handle hRequest, bool bFailure); + function void (Handle hRequest, bool bFailure, any data1); + function void (Handle hRequest, bool bFailure, any data1, any data2); +}; + +typeset SteamWorksHTTPDataReceived +{ + function void (Handle hRequest, bool bFailure, int offset, int bytesreceived); + function void (Handle hRequest, bool bFailure, int offset, int bytesreceived, any data1); + function void (Handle hRequest, bool bFailure, int offset, int bytesreceived, any data1, any data2); +}; + +native bool SteamWorks_SetHTTPCallbacks(Handle hHandle, SteamWorksHTTPRequestCompleted fCompleted = INVALID_FUNCTION, SteamWorksHTTPHeadersReceived fHeaders = INVALID_FUNCTION, SteamWorksHTTPDataReceived fData = INVALID_FUNCTION, Handle hCalling = INVALID_HANDLE); +native bool SteamWorks_SendHTTPRequest(Handle hRequest); +native bool SteamWorks_SendHTTPRequestAndStreamResponse(Handle hRequest); +native bool SteamWorks_DeferHTTPRequest(Handle hRequest); +native bool SteamWorks_PrioritizeHTTPRequest(Handle hRequest); +native bool SteamWorks_GetHTTPResponseHeaderSize(Handle hRequest, const char[] sHeader, int &size); +native bool SteamWorks_GetHTTPResponseHeaderValue(Handle hRequest, const char[] sHeader, char[] sValue, int size); +native bool SteamWorks_GetHTTPResponseBodySize(Handle hRequest, int &size); +native bool SteamWorks_GetHTTPResponseBodyData(Handle hRequest, char[] sBody, int length); +native bool SteamWorks_GetHTTPStreamingResponseBodyData(Handle hRequest, int cOffset, char[] sBody, int length); +native bool SteamWorks_GetHTTPDownloadProgressPct(Handle hRequest, float &percent); +native bool SteamWorks_GetHTTPRequestWasTimedOut(Handle hRequest, bool &bWasTimedOut); +native bool SteamWorks_SetHTTPRequestRawPostBody(Handle hRequest, const char[] sContentType, const char[] sBody, int bodylen); +native bool SteamWorks_SetHTTPRequestRawPostBodyFromFile(Handle hRequest, const char[] sContentType, const char[] sFileName); + +typeset SteamWorksHTTPBodyCallback +{ + function void (const char[] sData); + function void (const char[] sData, any value); + function void (const int[] data, any value, int datalen); +}; + +native bool SteamWorks_GetHTTPResponseBodyCallback(Handle hRequest, SteamWorksHTTPBodyCallback fCallback, any data = 0, Handle hPlugin = INVALID_HANDLE); +native bool SteamWorks_WriteHTTPResponseBodyToFile(Handle hRequest, const char[] sFileName); + +forward void SW_OnValidateClient(int ownerauthid, int authid); +forward void SteamWorks_OnValidateClient(int ownerauthid, int authid); +forward void SteamWorks_SteamServersConnected(); +forward void SteamWorks_SteamServersConnectFailure(EResult result); +forward void SteamWorks_SteamServersDisconnected(EResult result); + +forward void SteamWorks_RestartRequested(); +forward void SteamWorks_TokenRequested(char[] sToken, int maxlen); + +forward void SteamWorks_OnClientGroupStatus(int authid, int groupid, bool isMember, bool isOfficer); + +forward EGCResults SteamWorks_GCSendMessage(int unMsgType, const char[] pubData, int cubData); +forward void SteamWorks_GCMsgAvailable(int cubData); +forward EGCResults SteamWorks_GCRetrieveMessage(int punMsgType, const char[] pubDest, int cubDest, int pcubMsgSize); + +native EGCResults SteamWorks_SendMessageToGC(int unMsgType, const char[] pubData, int cubData); + +public Extension __ext_SteamWorks = +{ + name = "SteamWorks", + file = "SteamWorks.ext", +#if defined AUTOLOAD_EXTENSIONS + autoload = 1, +#else + autoload = 0, +#endif +#if defined REQUIRE_EXTENSIONS + required = 1, +#else + required = 0, +#endif +}; + +#if !defined REQUIRE_EXTENSIONS +public void __ext_SteamWorks_SetNTVOptional() +{ + MarkNativeAsOptional("SteamWorks_IsVACEnabled"); + MarkNativeAsOptional("SteamWorks_GetPublicIP"); + MarkNativeAsOptional("SteamWorks_GetPublicIPCell"); + MarkNativeAsOptional("SteamWorks_IsLoaded"); + MarkNativeAsOptional("SteamWorks_SetGameDescription"); + MarkNativeAsOptional("SteamWorks_IsConnected"); + MarkNativeAsOptional("SteamWorks_SetRule"); + MarkNativeAsOptional("SteamWorks_ClearRules"); + MarkNativeAsOptional("SteamWorks_ForceHeartbeat"); + MarkNativeAsOptional("SteamWorks_GetUserGroupStatus"); + MarkNativeAsOptional("SteamWorks_GetUserGroupStatusAuthID"); + + MarkNativeAsOptional("SteamWorks_HasLicenseForApp"); + MarkNativeAsOptional("SteamWorks_HasLicenseForAppId"); + MarkNativeAsOptional("SteamWorks_GetClientSteamID"); + + MarkNativeAsOptional("SteamWorks_RequestStatsAuthID"); + MarkNativeAsOptional("SteamWorks_RequestStats"); + MarkNativeAsOptional("SteamWorks_GetStatCell"); + MarkNativeAsOptional("SteamWorks_GetStatAuthIDCell"); + MarkNativeAsOptional("SteamWorks_GetStatFloat"); + MarkNativeAsOptional("SteamWorks_GetStatAuthIDFloat"); + + MarkNativeAsOptional("SteamWorks_SendMessageToGC"); + + MarkNativeAsOptional("SteamWorks_CreateHTTPRequest"); + MarkNativeAsOptional("SteamWorks_SetHTTPRequestContextValue"); + MarkNativeAsOptional("SteamWorks_SetHTTPRequestNetworkActivityTimeout"); + MarkNativeAsOptional("SteamWorks_SetHTTPRequestHeaderValue"); + MarkNativeAsOptional("SteamWorks_SetHTTPRequestGetOrPostParameter"); + + MarkNativeAsOptional("SteamWorks_SetHTTPCallbacks"); + MarkNativeAsOptional("SteamWorks_SendHTTPRequest"); + MarkNativeAsOptional("SteamWorks_SendHTTPRequestAndStreamResponse"); + MarkNativeAsOptional("SteamWorks_DeferHTTPRequest"); + MarkNativeAsOptional("SteamWorks_PrioritizeHTTPRequest"); + MarkNativeAsOptional("SteamWorks_GetHTTPResponseHeaderSize"); + MarkNativeAsOptional("SteamWorks_GetHTTPResponseHeaderValue"); + MarkNativeAsOptional("SteamWorks_GetHTTPResponseBodySize"); + MarkNativeAsOptional("SteamWorks_GetHTTPResponseBodyData"); + MarkNativeAsOptional("SteamWorks_GetHTTPStreamingResponseBodyData"); + MarkNativeAsOptional("SteamWorks_GetHTTPDownloadProgressPct"); + MarkNativeAsOptional("SteamWorks_SetHTTPRequestRawPostBody"); + MarkNativeAsOptional("SteamWorks_SetHTTPRequestRawPostBodyFromFile"); + + MarkNativeAsOptional("SteamWorks_GetHTTPResponseBodyCallback"); + MarkNativeAsOptional("SteamWorks_WriteHTTPResponseBodyToFile"); +} +#endif diff --git a/scripting/include/discord.inc b/scripting/include/discord.inc new file mode 100644 index 0000000..8993fff --- /dev/null +++ b/scripting/include/discord.inc @@ -0,0 +1,36 @@ +#if defined _discord_included + #endinput +#endif +#define _discord_included + +#pragma semicolon 1 +#pragma newdecls required + +native void Discord_SendMessage(const char[] webhook, const char[] message); + +public SharedPlugin __pl_discord = +{ + name = "discord", + file = "discord.smx", +#if defined REQUIRE_PLUGIN + required = 1, +#else + required = 0, +#endif +}; + +#if !defined REQUIRE_PLUGIN + +public void __pl_discord_SetNTVOptional() +{ + MarkNativeAsOptional("Discord_SendMessage"); +} + +#endif + +stock void Discord_EscapeString(char[] string, int maxlen) +{ + ReplaceString(string, maxlen, "@", "@"); + ReplaceString(string, maxlen, "'", "'"); + ReplaceString(string, maxlen, "\"", """); +} diff --git a/scripting/include/morecolors.inc b/scripting/include/morecolors.inc new file mode 100644 index 0000000..77d4942 --- /dev/null +++ b/scripting/include/morecolors.inc @@ -0,0 +1,674 @@ +// MOAR COLORS +// By Dr. McKay +// Inspired by: https://forums.alliedmods.net/showthread.php?t=96831 + +#if defined _colors_included + #endinput +#endif +#define _colors_included + +#include + +#define MORE_COLORS_VERSION "1.9.1" +#define MAX_MESSAGE_LENGTH 256 +#define MAX_BUFFER_LENGTH (MAX_MESSAGE_LENGTH * 4) + +#define COLOR_RED 0xFF4040 +#define COLOR_BLUE 0x99CCFF +#define COLOR_GRAY 0xCCCCCC +#define COLOR_GREEN 0x3EFF3E + +#define GAME_DODS 0 + +new bool:CSkipList[MAXPLAYERS + 1]; +new Handle:CTrie; +new CTeamColors[][] = {{0xCCCCCC, 0x4D7942, 0xFF4040}}; // Multi-dimensional array for games that don't support SayText2. First index is the game index (as defined by the GAME_ defines), second index is team. 0 = spectator, 1 = team1, 2 = team2 + +/** + * Prints a message to a specific client in the chat area. + * Supports color tags. + * + * @param client Client index. + * @param message Message (formatting rules). + * @noreturn + * + * On error/Errors: If the client is not connected an error will be thrown. + */ +stock CPrintToChat(client, const String:message[], any:...) { + CCheckTrie(); + if(client <= 0 || client > MaxClients) { + ThrowError("Invalid client index %i", client); + } + if(!IsClientInGame(client)) { + ThrowError("Client %i is not in game", client); + } + decl String:buffer[MAX_BUFFER_LENGTH], String:buffer2[MAX_BUFFER_LENGTH]; + SetGlobalTransTarget(client); + Format(buffer, sizeof(buffer), "\x01%s", message); + VFormat(buffer2, sizeof(buffer2), buffer, 3); + CReplaceColorCodes(buffer2); + CSendMessage(client, buffer2); +} + +/** + * Prints a message to all clients in the chat area. + * Supports color tags. + * + * @param client Client index. + * @param message Message (formatting rules). + * @noreturn + */ +stock CPrintToChatAll(const String:message[], any:...) { + CCheckTrie(); + decl String:buffer[MAX_BUFFER_LENGTH], String:buffer2[MAX_BUFFER_LENGTH]; + for(new i = 1; i <= MaxClients; i++) { + if(!IsClientInGame(i) || CSkipList[i]) { + CSkipList[i] = false; + continue; + } + SetGlobalTransTarget(i); + Format(buffer, sizeof(buffer), "\x01%s", message); + VFormat(buffer2, sizeof(buffer2), buffer, 2); + CReplaceColorCodes(buffer2); + CSendMessage(i, buffer2); + } +} + +/** + * Prints a message to a specific client in the chat area. + * Supports color tags and teamcolor tag. + * + * @param client Client index. + * @param author Author index whose color will be used for teamcolor tag. + * @param message Message (formatting rules). + * @noreturn + * + * On error/Errors: If the client or author are not connected an error will be thrown + */ +stock CPrintToChatEx(client, author, const String:message[], any:...) { + CCheckTrie(); + if(client <= 0 || client > MaxClients) { + ThrowError("Invalid client index %i", client); + } + if(!IsClientInGame(client)) { + ThrowError("Client %i is not in game", client); + } + if(author <= 0 || author > MaxClients) { + ThrowError("Invalid client index %i", author); + } + if(!IsClientInGame(author)) { + ThrowError("Client %i is not in game", author); + } + decl String:buffer[MAX_BUFFER_LENGTH], String:buffer2[MAX_BUFFER_LENGTH]; + SetGlobalTransTarget(client); + Format(buffer, sizeof(buffer), "\x01%s", message); + VFormat(buffer2, sizeof(buffer2), buffer, 4); + CReplaceColorCodes(buffer2, author); + CSendMessage(client, buffer2, author); +} + +/** + * Prints a message to all clients in the chat area. + * Supports color tags and teamcolor tag. + * + * @param author Author index whose color will be used for teamcolor tag. + * @param message Message (formatting rules). + * @noreturn + * + * On error/Errors: If the author is not connected an error will be thrown. + */ +stock CPrintToChatAllEx(author, const String:message[], any:...) { + CCheckTrie(); + if(author <= 0 || author > MaxClients) { + ThrowError("Invalid client index %i", author); + } + if(!IsClientInGame(author)) { + ThrowError("Client %i is not in game", author); + } + decl String:buffer[MAX_BUFFER_LENGTH], String:buffer2[MAX_BUFFER_LENGTH]; + for(new i = 1; i <= MaxClients; i++) { + if(!IsClientInGame(i) || CSkipList[i]) { + CSkipList[i] = false; + continue; + } + SetGlobalTransTarget(i); + Format(buffer, sizeof(buffer), "\x01%s", message); + VFormat(buffer2, sizeof(buffer2), buffer, 3); + CReplaceColorCodes(buffer2, author); + CSendMessage(i, buffer2, author); + } +} + +/** + * Sends a SayText2 usermessage + * + * @param client Client to send usermessage to + * @param message Message to send + * @noreturn + */ +stock CSendMessage(client, const String:message[], author=0) { + if(author == 0) { + author = client; + } + decl String:buffer[MAX_MESSAGE_LENGTH], String:game[16]; + GetGameFolderName(game, sizeof(game)); + strcopy(buffer, sizeof(buffer), message); + new UserMsg:index = GetUserMessageId("SayText2"); + if(index == INVALID_MESSAGE_ID) { + if(StrEqual(game, "dod")) { + new team = GetClientTeam(author); + if(team == 0) { + ReplaceString(buffer, sizeof(buffer), "\x03", "\x04", false); // Unassigned gets green + } else { + decl String:temp[16]; + Format(temp, sizeof(temp), "\x07%06X", CTeamColors[GAME_DODS][team - 1]); + ReplaceString(buffer, sizeof(buffer), "\x03", temp, false); + } + } + PrintToChat(client, "%s", buffer); + return; + } + new Handle:buf = StartMessageOne("SayText2", client, USERMSG_RELIABLE|USERMSG_BLOCKHOOKS); + if(GetFeatureStatus(FeatureType_Native, "GetUserMessageType") == FeatureStatus_Available && GetUserMessageType() == UM_Protobuf) { + PbSetInt(buf, "ent_idx", author); + PbSetBool(buf, "chat", true); + PbSetString(buf, "msg_name", buffer); + PbAddString(buf, "params", ""); + PbAddString(buf, "params", ""); + PbAddString(buf, "params", ""); + PbAddString(buf, "params", ""); + } else { + BfWriteByte(buf, author); // Message author + BfWriteByte(buf, true); // Chat message + BfWriteString(buf, buffer); // Message text + } + EndMessage(); +} + +/** + * This function should only be used right in front of + * CPrintToChatAll or CPrintToChatAllEx. It causes those functions + * to skip the specified client when printing the message. + * After printing the message, the client will no longer be skipped. + * + * @param client Client index + * @noreturn + */ +stock CSkipNextClient(client) { + if(client <= 0 || client > MaxClients) { + ThrowError("Invalid client index %i", client); + } + CSkipList[client] = true; +} + +/** + * Checks if the colors trie is initialized and initializes it if it's not (used internally) + * + * @return No return + */ +stock CCheckTrie() { + if(CTrie == INVALID_HANDLE) { + CTrie = InitColorTrie(); + } +} + +/** + * Replaces color tags in a string with color codes (used internally by CPrintToChat, CPrintToChatAll, CPrintToChatEx, and CPrintToChatAllEx + * + * @param buffer String. + * @param author Optional client index to use for {teamcolor} tags, or 0 for none + * @param removeTags Optional boolean value to determine whether we're replacing tags with colors, or just removing tags, used by CRemoveTags + * @param maxlen Optional value for max buffer length, used by CRemoveTags + * @noreturn + * + * On error/Errors: If the client index passed for author is invalid or not in game. + */ +stock CReplaceColorCodes(String:buffer[], author=0, bool:removeTags=false, maxlen=MAX_BUFFER_LENGTH) { + CCheckTrie(); + if(!removeTags) { + ReplaceString(buffer, maxlen, "{default}", "\x01", false); + } else { + ReplaceString(buffer, maxlen, "{default}", "", false); + ReplaceString(buffer, maxlen, "{teamcolor}", "", false); + } + if(author != 0 && !removeTags) { + if(author < 0 || author > MaxClients) { + ThrowError("Invalid client index %i", author); + } + if(!IsClientInGame(author)) { + ThrowError("Client %i is not in game", author); + } + ReplaceString(buffer, maxlen, "{teamcolor}", "\x03", false); + } + new cursor = 0; + new value; + decl String:tag[32], String:buff[32], String:output[maxlen]; + strcopy(output, maxlen, buffer); + // Since the string's size is going to be changing, output will hold the replaced string and we'll search buffer + + new Handle:regex = CompileRegex("{[a-zA-Z0-9]+}"); + for(new i = 0; i < 1000; i++) { // The RegEx extension is quite flaky, so we have to loop here :/. This loop is supposed to be infinite and broken by return, but conditions have been added to be safe. + if(MatchRegex(regex, buffer[cursor]) < 1) { + CloseHandle(regex); + strcopy(buffer, maxlen, output); + return; + } + GetRegexSubString(regex, 0, tag, sizeof(tag)); + CStrToLower(tag); + cursor = StrContains(buffer[cursor], tag, false) + cursor + 1; + strcopy(buff, sizeof(buff), tag); + ReplaceString(buff, sizeof(buff), "{", ""); + ReplaceString(buff, sizeof(buff), "}", ""); + + if(!GetTrieValue(CTrie, buff, value)) { + continue; + } + + if(removeTags) { + ReplaceString(output, maxlen, tag, "", false); + } else { + Format(buff, sizeof(buff), "\x07%06X", value); + ReplaceString(output, maxlen, tag, buff, false); + } + } + LogError("[MORE COLORS] Infinite loop broken."); +} + +/** + * Gets a part of a string + * + * @param input String to get the part from + * @param output Buffer to write to + * @param maxlen Max length of output buffer + * @param start Position to start at + * @param numChars Number of characters to return, or 0 for the end of the string + * @noreturn + */ +stock CSubString(const String:input[], String:output[], maxlen, start, numChars=0) { + new i = 0; + for(;;) { + if(i == maxlen - 1 || i >= numChars || input[start + i] == '\0') { + output[i] = '\0'; + return; + } + output[i] = input[start + i]; + i++; + } +} + +/** + * Converts a string to lowercase + * + * @param buffer String to convert + * @noreturn + */ +stock CStrToLower(String:buffer[]) { + new len = strlen(buffer); + for(new i = 0; i < len; i++) { + buffer[i] = CharToLower(buffer[i]); + } +} + +/** + * Adds a color to the colors trie + * + * @param name Color name, without braces + * @param color Hexadecimal representation of the color (0xRRGGBB) + * @return True if color was added successfully, false if a color already exists with that name + */ +stock bool:CAddColor(const String:name[], color) { + CCheckTrie(); + new value; + if(GetTrieValue(CTrie, name, value)) { + return false; + } + decl String:newName[64]; + strcopy(newName, sizeof(newName), name); + CStrToLower(newName); + SetTrieValue(CTrie, newName, color); + return true; +} + +/** + * Removes color tags from a message + * + * @param message Message to remove tags from + * @param maxlen Maximum buffer length + * @noreturn + */ +stock CRemoveTags(String:message[], maxlen) { + CReplaceColorCodes(message, 0, true, maxlen); +} + +/** + * Replies to a command with colors + * + * @param client Client to reply to + * @param message Message (formatting rules) + * @noreturn + */ +stock CReplyToCommand(client, const String:message[], any:...) { + decl String:buffer[MAX_BUFFER_LENGTH]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 3); + if(GetCmdReplySource() == SM_REPLY_TO_CONSOLE) { + CRemoveTags(buffer, sizeof(buffer)); + PrintToConsole(client, "%s", buffer); + } else { + CPrintToChat(client, "%s", buffer); + } +} + +/** + * Replies to a command with colors + * + * @param client Client to reply to + * @param author Client to use for {teamcolor} + * @param message Message (formatting rules) + * @noreturn + */ +stock CReplyToCommandEx(client, author, const String:message[], any:...) { + decl String:buffer[MAX_BUFFER_LENGTH]; + SetGlobalTransTarget(client); + VFormat(buffer, sizeof(buffer), message, 4); + if(GetCmdReplySource() == SM_REPLY_TO_CONSOLE) { + CRemoveTags(buffer, sizeof(buffer)); + PrintToConsole(client, "%s", buffer); + } else { + CPrintToChatEx(client, author, "%s", buffer); + } +} + +/** + * Shows admin activity with colors + * + * @param client Client performing an action + * @param message Message (formatting rules) + * @noreturn + */ +stock CShowActivity(client, const String:message[], any:...) { + CCheckTrie(); + if(client < 0 || client > MaxClients) { + ThrowError("Invalid client index %d", client); + } + if(client != 0 && !IsClientInGame(client)) { + ThrowError("Client %d is not in game", client); + } + decl String:buffer[MAX_BUFFER_LENGTH], String:buffer2[MAX_BUFFER_LENGTH]; + Format(buffer, sizeof(buffer), "\x01%s", message); + VFormat(buffer2, sizeof(buffer2), buffer, 3); + CReplaceColorCodes(buffer2); + ShowActivity(client, "%s", buffer2); +} + +/** + * Shows admin activity with colors + * + * @param client Client performing an action + * @param tag Tag to prepend to the message (color tags supported) + * @param message Message (formatting rules) + * @noreturn + */ +stock CShowActivityEx(client, const String:tag[], const String:message[], any:...) { + CCheckTrie(); + if(client < 0 || client > MaxClients) { + ThrowError("Invalid client index %d", client); + } + if(client != 0 && !IsClientInGame(client)) { + ThrowError("Client %d is not in game", client); + } + decl String:buffer[MAX_BUFFER_LENGTH], String:buffer2[MAX_BUFFER_LENGTH]; + Format(buffer, sizeof(buffer), "\x01%s", message); + VFormat(buffer2, sizeof(buffer2), buffer, 4); + CReplaceColorCodes(buffer2); + strcopy(buffer, sizeof(buffer), tag); + CReplaceColorCodes(buffer); + ShowActivityEx(client, tag, "%s", buffer2); +} + +/** + * Shows admin activity with colors + * + * @param client Client performing an action + * @param tag Tag to prepend to the message (color tags supported) + * @param message Message (formatting rules) + * @noreturn + */ +stock CShowActivity2(client, const String:tag[], const String:message[], any:...) { + CCheckTrie(); + if(client < 0 || client > MaxClients) { + ThrowError("Invalid client index %d", client); + } + if(client != 0 && !IsClientInGame(client)) { + ThrowError("Client %d is not in game", client); + } + decl String:buffer[MAX_BUFFER_LENGTH], String:buffer2[MAX_BUFFER_LENGTH]; + Format(buffer, sizeof(buffer), "\x01%s", message); + VFormat(buffer2, sizeof(buffer2), buffer, 4); + CReplaceColorCodes(buffer2); + strcopy(buffer, sizeof(buffer), tag); + CReplaceColorCodes(buffer); + ShowActivity2(client, buffer, "%s", buffer2); +} + +/** + * Determines whether a color name exists + * + * @param color The color name to check + * @return True if the color exists, false otherwise + */ +stock bool:CColorExists(const String:color[]) { + CCheckTrie(); + new temp; + return GetTrieValue(CTrie, color, temp); +} + +/** + * Returns the hexadecimal representation of a client's team color (will NOT initialize the trie) + * + * @param client Client to get the team color for + * @return Client's team color in hexadecimal, or green if unknown + * On error/Errors: If the client index passed is invalid or not in game. + */ +stock CGetTeamColor(client) { + if(client <= 0 || client > MaxClients) { + ThrowError("Invalid client index %i", client); + } + if(!IsClientInGame(client)) { + ThrowError("Client %i is not in game", client); + } + new value; + switch(GetClientTeam(client)) { + case 1: { + value = COLOR_GRAY; + } + case 2: { + value = COLOR_RED; + } + case 3: { + value = COLOR_BLUE; + } + default: { + value = COLOR_GREEN; + } + } + return value; +} + +stock Handle:InitColorTrie() { + new Handle:hTrie = CreateTrie(); + SetTrieValue(hTrie, "aliceblue", 0xF0F8FF); + SetTrieValue(hTrie, "allies", 0x4D7942); // same as Allies team in DoD:S + SetTrieValue(hTrie, "ancient", 0xEB4B4B); // same as Ancient item rarity in Dota 2 + SetTrieValue(hTrie, "antiquewhite", 0xFAEBD7); + SetTrieValue(hTrie, "aqua", 0x00FFFF); + SetTrieValue(hTrie, "aquamarine", 0x7FFFD4); + SetTrieValue(hTrie, "arcana", 0xADE55C); // same as Arcana item rarity in Dota 2 + SetTrieValue(hTrie, "axis", 0xFF4040); // same as Axis team in DoD:S + SetTrieValue(hTrie, "azure", 0x007FFF); + SetTrieValue(hTrie, "beige", 0xF5F5DC); + SetTrieValue(hTrie, "bisque", 0xFFE4C4); + SetTrieValue(hTrie, "black", 0x000000); + SetTrieValue(hTrie, "blanchedalmond", 0xFFEBCD); + SetTrieValue(hTrie, "blue", 0x99CCFF); // same as BLU/Counter-Terrorist team color + SetTrieValue(hTrie, "blueviolet", 0x8A2BE2); + SetTrieValue(hTrie, "brown", 0xA52A2A); + SetTrieValue(hTrie, "burlywood", 0xDEB887); + SetTrieValue(hTrie, "cadetblue", 0x5F9EA0); + SetTrieValue(hTrie, "chartreuse", 0x7FFF00); + SetTrieValue(hTrie, "chocolate", 0xD2691E); + SetTrieValue(hTrie, "collectors", 0xAA0000); // same as Collector's item quality in TF2 + SetTrieValue(hTrie, "common", 0xB0C3D9); // same as Common item rarity in Dota 2 + SetTrieValue(hTrie, "community", 0x70B04A); // same as Community item quality in TF2 + SetTrieValue(hTrie, "coral", 0xFF7F50); + SetTrieValue(hTrie, "cornflowerblue", 0x6495ED); + SetTrieValue(hTrie, "cornsilk", 0xFFF8DC); + SetTrieValue(hTrie, "corrupted", 0xA32C2E); // same as Corrupted item quality in Dota 2 + SetTrieValue(hTrie, "crimson", 0xDC143C); + SetTrieValue(hTrie, "cyan", 0x00FFFF); + SetTrieValue(hTrie, "darkblue", 0x00008B); + SetTrieValue(hTrie, "darkcyan", 0x008B8B); + SetTrieValue(hTrie, "darkgoldenrod", 0xB8860B); + SetTrieValue(hTrie, "darkgray", 0xA9A9A9); + SetTrieValue(hTrie, "darkgrey", 0xA9A9A9); + SetTrieValue(hTrie, "darkgreen", 0x006400); + SetTrieValue(hTrie, "darkkhaki", 0xBDB76B); + SetTrieValue(hTrie, "darkmagenta", 0x8B008B); + SetTrieValue(hTrie, "darkolivegreen", 0x556B2F); + SetTrieValue(hTrie, "darkorange", 0xFF8C00); + SetTrieValue(hTrie, "darkorchid", 0x9932CC); + SetTrieValue(hTrie, "darkred", 0x8B0000); + SetTrieValue(hTrie, "darksalmon", 0xE9967A); + SetTrieValue(hTrie, "darkseagreen", 0x8FBC8F); + SetTrieValue(hTrie, "darkslateblue", 0x483D8B); + SetTrieValue(hTrie, "darkslategray", 0x2F4F4F); + SetTrieValue(hTrie, "darkslategrey", 0x2F4F4F); + SetTrieValue(hTrie, "darkturquoise", 0x00CED1); + SetTrieValue(hTrie, "darkviolet", 0x9400D3); + SetTrieValue(hTrie, "deeppink", 0xFF1493); + SetTrieValue(hTrie, "deepskyblue", 0x00BFFF); + SetTrieValue(hTrie, "dimgray", 0x696969); + SetTrieValue(hTrie, "dimgrey", 0x696969); + SetTrieValue(hTrie, "dodgerblue", 0x1E90FF); + SetTrieValue(hTrie, "exalted", 0xCCCCCD); // same as Exalted item quality in Dota 2 + SetTrieValue(hTrie, "firebrick", 0xB22222); + SetTrieValue(hTrie, "floralwhite", 0xFFFAF0); + SetTrieValue(hTrie, "forestgreen", 0x228B22); + SetTrieValue(hTrie, "frozen", 0x4983B3); // same as Frozen item quality in Dota 2 + SetTrieValue(hTrie, "fuchsia", 0xFF00FF); + SetTrieValue(hTrie, "fullblue", 0x0000FF); + SetTrieValue(hTrie, "fullred", 0xFF0000); + SetTrieValue(hTrie, "gainsboro", 0xDCDCDC); + SetTrieValue(hTrie, "genuine", 0x4D7455); // same as Genuine item quality in TF2 + SetTrieValue(hTrie, "ghostwhite", 0xF8F8FF); + SetTrieValue(hTrie, "gold", 0xFFD700); + SetTrieValue(hTrie, "goldenrod", 0xDAA520); + SetTrieValue(hTrie, "gray", 0xCCCCCC); // same as spectator team color + SetTrieValue(hTrie, "grey", 0xCCCCCC); + SetTrieValue(hTrie, "green", 0x3EFF3E); + SetTrieValue(hTrie, "greenyellow", 0xADFF2F); + SetTrieValue(hTrie, "haunted", 0x38F3AB); // same as Haunted item quality in TF2 + SetTrieValue(hTrie, "honeydew", 0xF0FFF0); + SetTrieValue(hTrie, "hotpink", 0xFF69B4); + SetTrieValue(hTrie, "immortal", 0xE4AE33); // same as Immortal item rarity in Dota 2 + SetTrieValue(hTrie, "indianred", 0xCD5C5C); + SetTrieValue(hTrie, "indigo", 0x4B0082); + SetTrieValue(hTrie, "ivory", 0xFFFFF0); + SetTrieValue(hTrie, "khaki", 0xF0E68C); + SetTrieValue(hTrie, "lavender", 0xE6E6FA); + SetTrieValue(hTrie, "lavenderblush", 0xFFF0F5); + SetTrieValue(hTrie, "lawngreen", 0x7CFC00); + SetTrieValue(hTrie, "legendary", 0xD32CE6); // same as Legendary item rarity in Dota 2 + SetTrieValue(hTrie, "lemonchiffon", 0xFFFACD); + SetTrieValue(hTrie, "lightblue", 0xADD8E6); + SetTrieValue(hTrie, "lightcoral", 0xF08080); + SetTrieValue(hTrie, "lightcyan", 0xE0FFFF); + SetTrieValue(hTrie, "lightgoldenrodyellow", 0xFAFAD2); + SetTrieValue(hTrie, "lightgray", 0xD3D3D3); + SetTrieValue(hTrie, "lightgrey", 0xD3D3D3); + SetTrieValue(hTrie, "lightgreen", 0x99FF99); + SetTrieValue(hTrie, "lightpink", 0xFFB6C1); + SetTrieValue(hTrie, "lightsalmon", 0xFFA07A); + SetTrieValue(hTrie, "lightseagreen", 0x20B2AA); + SetTrieValue(hTrie, "lightskyblue", 0x87CEFA); + SetTrieValue(hTrie, "lightslategray", 0x778899); + SetTrieValue(hTrie, "lightslategrey", 0x778899); + SetTrieValue(hTrie, "lightsteelblue", 0xB0C4DE); + SetTrieValue(hTrie, "lightyellow", 0xFFFFE0); + SetTrieValue(hTrie, "lime", 0x00FF00); + SetTrieValue(hTrie, "limegreen", 0x32CD32); + SetTrieValue(hTrie, "linen", 0xFAF0E6); + SetTrieValue(hTrie, "magenta", 0xFF00FF); + SetTrieValue(hTrie, "maroon", 0x800000); + SetTrieValue(hTrie, "mediumaquamarine", 0x66CDAA); + SetTrieValue(hTrie, "mediumblue", 0x0000CD); + SetTrieValue(hTrie, "mediumorchid", 0xBA55D3); + SetTrieValue(hTrie, "mediumpurple", 0x9370D8); + SetTrieValue(hTrie, "mediumseagreen", 0x3CB371); + SetTrieValue(hTrie, "mediumslateblue", 0x7B68EE); + SetTrieValue(hTrie, "mediumspringgreen", 0x00FA9A); + SetTrieValue(hTrie, "mediumturquoise", 0x48D1CC); + SetTrieValue(hTrie, "mediumvioletred", 0xC71585); + SetTrieValue(hTrie, "midnightblue", 0x191970); + SetTrieValue(hTrie, "mintcream", 0xF5FFFA); + SetTrieValue(hTrie, "mistyrose", 0xFFE4E1); + SetTrieValue(hTrie, "moccasin", 0xFFE4B5); + SetTrieValue(hTrie, "mythical", 0x8847FF); // same as Mythical item rarity in Dota 2 + SetTrieValue(hTrie, "navajowhite", 0xFFDEAD); + SetTrieValue(hTrie, "navy", 0x000080); + SetTrieValue(hTrie, "normal", 0xB2B2B2); // same as Normal item quality in TF2 + SetTrieValue(hTrie, "oldlace", 0xFDF5E6); + SetTrieValue(hTrie, "olive", 0x9EC34F); + SetTrieValue(hTrie, "olivedrab", 0x6B8E23); + SetTrieValue(hTrie, "orange", 0xFFA500); + SetTrieValue(hTrie, "orangered", 0xFF4500); + SetTrieValue(hTrie, "orchid", 0xDA70D6); + SetTrieValue(hTrie, "palegoldenrod", 0xEEE8AA); + SetTrieValue(hTrie, "palegreen", 0x98FB98); + SetTrieValue(hTrie, "paleturquoise", 0xAFEEEE); + SetTrieValue(hTrie, "palevioletred", 0xD87093); + SetTrieValue(hTrie, "papayawhip", 0xFFEFD5); + SetTrieValue(hTrie, "peachpuff", 0xFFDAB9); + SetTrieValue(hTrie, "peru", 0xCD853F); + SetTrieValue(hTrie, "pink", 0xFFC0CB); + SetTrieValue(hTrie, "plum", 0xDDA0DD); + SetTrieValue(hTrie, "powderblue", 0xB0E0E6); + SetTrieValue(hTrie, "purple", 0x800080); + SetTrieValue(hTrie, "rare", 0x4B69FF); // same as Rare item rarity in Dota 2 + SetTrieValue(hTrie, "red", 0xFF4040); // same as RED/Terrorist team color + SetTrieValue(hTrie, "rosybrown", 0xBC8F8F); + SetTrieValue(hTrie, "royalblue", 0x4169E1); + SetTrieValue(hTrie, "saddlebrown", 0x8B4513); + SetTrieValue(hTrie, "salmon", 0xFA8072); + SetTrieValue(hTrie, "sandybrown", 0xF4A460); + SetTrieValue(hTrie, "seagreen", 0x2E8B57); + SetTrieValue(hTrie, "seashell", 0xFFF5EE); + SetTrieValue(hTrie, "selfmade", 0x70B04A); // same as Self-Made item quality in TF2 + SetTrieValue(hTrie, "sienna", 0xA0522D); + SetTrieValue(hTrie, "silver", 0xC0C0C0); + SetTrieValue(hTrie, "skyblue", 0x87CEEB); + SetTrieValue(hTrie, "slateblue", 0x6A5ACD); + SetTrieValue(hTrie, "slategray", 0x708090); + SetTrieValue(hTrie, "slategrey", 0x708090); + SetTrieValue(hTrie, "snow", 0xFFFAFA); + SetTrieValue(hTrie, "springgreen", 0x00FF7F); + SetTrieValue(hTrie, "steelblue", 0x4682B4); + SetTrieValue(hTrie, "strange", 0xCF6A32); // same as Strange item quality in TF2 + SetTrieValue(hTrie, "tan", 0xD2B48C); + SetTrieValue(hTrie, "teal", 0x008080); + SetTrieValue(hTrie, "thistle", 0xD8BFD8); + SetTrieValue(hTrie, "tomato", 0xFF6347); + SetTrieValue(hTrie, "turquoise", 0x40E0D0); + SetTrieValue(hTrie, "uncommon", 0xB0C3D9); // same as Uncommon item rarity in Dota 2 + SetTrieValue(hTrie, "unique", 0xFFD700); // same as Unique item quality in TF2 + SetTrieValue(hTrie, "unusual", 0x8650AC); // same as Unusual item quality in TF2 + SetTrieValue(hTrie, "valve", 0xA50F79); // same as Valve item quality in TF2 + SetTrieValue(hTrie, "vintage", 0x476291); // same as Vintage item quality in TF2 + SetTrieValue(hTrie, "violet", 0xEE82EE); + SetTrieValue(hTrie, "wheat", 0xF5DEB3); + SetTrieValue(hTrie, "white", 0xFFFFFF); + SetTrieValue(hTrie, "whitesmoke", 0xF5F5F5); + SetTrieValue(hTrie, "yellow", 0xFFFF00); + SetTrieValue(hTrie, "yellowgreen", 0x9ACD32); + return hTrie; +} diff --git a/translations/demo_check.phrases.txt b/translations/demo_check.phrases.txt index a425f69..6e7a1df 100644 --- a/translations/demo_check.phrases.txt +++ b/translations/demo_check.phrases.txt @@ -32,6 +32,11 @@ { "en" "You have been kicked from the server for failing the Demo Check. See console for details." } + "kicked_announce_disabled" + { + "#format" "{1:s}" + "en" "{red}Warning! {lime}{1}{white} does not appear to be auto-recording demos correctly." + } "kicked_announce" { "#format" "{1:s}" @@ -41,4 +46,10 @@ { "en" "Your settings for recording Demos is incorrect. Please read the docs here: docs.ozfortress.com/guides/pov_demo_recording/" } + + "discord_democheck" + { + "#format" "{1:s},{2:s},{3:s},{4:s},{5:s}" + "en" "[{1} ({2})]({3}) failed the demo check at {4} - `{5}`" + } }