commit 965453909e1d28aed3abfca7f93b6c1b27a7f75d Author: Chris Lynch Date: Wed Dec 25 18:43:29 2013 -0500 Reupload after bitbucket wipe diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..888a024 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,1764 @@ +HLstatsX Community Edition 1.6.19 +October 13, 2012 + ++ Heatmaps: Added support for NTS, L4D2 and CSS Heatmap configurations. (#1602, #844) +! GameSupport: Fixed some CS:GO actions that were incorrectly added in 1.6.18. (#1599) +! Web: Fixed adding a new server to a duplicate game returning user to original game's server listing. (#1594) +! Web: Fixed output that could be used as Cross-Site Scripting exploit (Reported by P.Whitton) (#1612) + +HLstatsX Community Edition 1.6.18 +August 31, 2012 + ++ GameSupport: Added support for Dino D-Day and Counter-Strike: Global Offensive. (#1589, #1552) ++ Web: Added additional chat logging support for Nuclear Dawn. +! Web: Fixed adding a new server to a duplicate game returning user to original game's server listing. (#1594) + + +HLstatsX Community Edition 1.6.17 +June 27, 2012 + ++ GameSupport: Added 9 new TF2 weapons from the Pyromania update. (#1579, 1583) ++ Web: Added new map image for TF2 map sd_doomsday. (#1582) +! GameSupport: Corrected TF2 award names that were incorrectly updated. (#1558) +! Web: Updated updated check URL to hlxce.com domain. (#1581) + + +HLstatsX Community Edition 1.6.16 +May 20, 2012 + ++ Daemon: Added a task to purge out old entries in hlstats_server_load table. (#1545) ++ GameSupport: Added 8 new weapons for TF2. (#1546 #1547) ++ GameSupport: Support for Nuclear Dawn added. (#1487) + SuperLogs is available for Nuclear Dawn. More info at http://wiki.hlxce.com/wiki/SuperLogs +! Daemon: Fixed awards being given to players of different games. (#1440) +! Daemon: Fixed daemon awarding no points when you kill a player with no points. (#1227) +! Daemon: Fixed regression causing the 'next' command to have garbage output. (#1479) +! Daemon: Fixed uptime regex calculation due to format change in Team Fortress 2. (#1497) +! Daemon: Updated geoip.py to use correct MaxMind URL for GeoLiteCity.CSV file. (#1521) +! Daemon: Fixed install_binary.sh not installing GeoLiteCity to correct location. (#1532) +! General: Standardized typing for server_id columns in all tables. (#1456) +! Web: Removed legacy which could be used for XSS and SQL injection attacks. +! Web: Fixed Status and Avatar not working for players with custom URLs in their Steam profile. (#1525) +! Web: Fixed various locations where role code was displayed instead of role name. (#1548) +! Web: Added missing TF2 award and ribbon images. (#1449 #1468) +! Web: Fixed a syntax issue on tools_perlcontrol.php that caused the page to not load on PHP 5.4 (#1544) +? Web: Changed the default zoom level for the EUROPE region by 1. (#1426) +? Web: Changed the table on player award history page to use the award name instead of award code. (#1556) +? Web: Changed the query on the Chat page to sort by the ID instead of Event Time, which would have required an index. (#1083) + + +HLstatsX Community Edition 1.6.15 +November 8th, 2011 + ++ GameSupport: Added many new weapons for TF2, including the Victory Pack, Manno-Technology and Halloween update (#1439, #1447, #1448) (Packhead) ++ GameSupport: Added awards and ribbons for four existing actions (#1467) (Packhead) ++ GameSupport: Captured player_penetration weapon event and changed to action for TF2 (weapon logged as machina) (Packhead) ++ Web: Awards, Maps, Ribbons, Roles and Weapon images now failback to "realgame" directory if not present in "game" directory (#1461) (Packhead) +! Daemon: Fixed FPS and Uptime tracking post-TF2 stats output fix (#1458) (Packhead) +! Daemon: Converted server load DB updates to cached queries to reduce load (Packhead) +! Daemon: Converted almost all daemon queries to use MySQL Prepared Statements to help reduce DB load (Packhead) +! GameSupport: Corrected typos and added two missing TF2 weapons (#1446) (Bluthund) +? Plugin: Removed old hack to fix cvars on a2s_rules response (psychonic) +? Plugin: Removed a large number of redundant client checks (psychonic) +? Web: Removed unnecessary joins from multiple pages (octo-dhd) +? Web: Additional index added to speed up PlayerPlayerAction transactions (octo-dhd) +? Web: Updater has been enhanced to provide more output (Packhead) + + +HLstatsX Community Edition 1.6.14 +July 15, 2011 ++ GameSupport: Added 31 new weapons for Team Fortress 2 (#1421, #1429) (Packhead, psychonic, octo-dhd, bug, Rakshot, soolshock, solarpowered) ++ GameSupport: Added new TF2 player action to log scout extinguishes with the Mad Milk (#1397) (Packhead) +! Daemon: Player position was not printing at initial release of 1.6.13 (#1425) (psychonic) +! Daemon: Updated the GeoLite_Import.sh script due to change in release cycle of Max Mind (#1438) (aubergine) +? Daemon: Chat commands are no longer reliant on player events being enabled (psychonic) +? Daemon: Lazy log line format no longer clears player's team (psychonic) +? Daemon: Removed some redundant logic in EventHandlers.plib (psychonic) +? GameSupport: Renamed 14 Team Fortress 2 weapons to align them with the TF2 wiki (#1423) (Packhead) + + +HLstatsX Community Edition 1.6.13 +January 22, 2011 ++ GameSupport: Added missing headshot action, awards, and ribbons to HL2DM (psychonic) ++ General: Added total teamkills to player tracking (psychonic) ++ General: Added teamkills and kill/death streaks to player session tracking (psychonic) ++ General: Added more new special award possibilities, "connectiontime", "killsteak", and "deathstreak" (psychonic) ++ Web: Added teamkills to ingame statsme webpage (psychonic) +? GameSupport: Updated many recently-changed TF2 weapon codes (psychonic) +? Daemon: Updated run_hlstats to now automatically change to the directory it's in (no longer do you have to CD before using script in cron!) (Packhead) +? Daemon: Updated run_hlstats to verify shebang line in hlstats.pl is formatted correctly and binary exists (Packhead) +? Daemon: Moved connect message from Connect event to EnterGame event (to prevent display when banned users connect) (psychonic) +? Heatmaps: Greatly improved output quality of heat data (msleeper) +? General: Improved speed of generating awards for "mostkill", "suicides", "teamkills" (psychonic) +? Web: Server configuration parameter names are now read-only to avoid accidental changes (psychonic) +? Web: Id column is now hidden on most admin pages with EditTables as it serves little purpose to the user (psychonic) +! Web: Fixed paging issue on search page (psychonic) +! Web: Fixed JS error when selecting an admin task (psychonic) +! Web: Fixed lack of input box when adding new row in readonly EditListColumns (psychonic) + +HLstatsX Community Edition 1.6.12 +December 19, 2010 ++ GameSupport: Added TF2 Australian Christmas support and missing weapons/actions (thorrr, psychonic, Packhead) + (requires SuperLogs:TF2 for some weapons to track independently) +? Daemon: Cleaned up some output from run_hlstats (#1336) (Packhead) +! Daemon: Fixed a local-variable being declared outside a function (#1306) (Packhead) +! Plugin: Fixed a missing handle check (#1307) (psychonic) +! Web: Fixed hidden clans appearing on ingame browser (#1309) (Packhead) +! Web: Fixed zoom out button being covered up by Google logo on map page (#1317) (Azelphur) +! Web: Fixed incorrectly named TF2 ribbon images (#1319) (Packhead) +! Web: Fixed admin login form not working correctly in certain situations (#1340) (octo-dhd) + + + +HLstatsX Community Edition 1.6.11 +October 10, 2010 + ++ GameSupport: Added PVKII support (Munra, CoZmicShReddeR, psychonic, Packhead) ++ GameSupport: Added CSPromod support (requires SuperLogs:CSPromod) (psychonic, Packhead, Unity) ++ GameSupport: Added HL1DM support (Peterson, Packhead, psychonic) ++ GameSupport: Added support for TF2 polycount additions (Packhead, psychonic, Dragonshadow) + (requires SuperLogs:TF2 for some weapons to track independently) ++ GameSupport: Added support for tracking Mini Sentries in TF2 (requires SuperLogs:TF2) (psychonic, Packhead) ++ GameSupport: Added support for CS:S round_mvp action newly added in SuperLogs: CSS (psychonic) ++ GameSupport: Chat colors enhanced in FoF now that 3.0+ supports them (psychonic) ++ GameSupport: Added missing AOC weapon codes, chivalry, Throwing Axe, and Fists (psychonic) ++ GameSupport: Added some missing award images for CS:S/CSP (Dragonshadow) ++ GameSupport: Added some missing award and ribbon images for TF2 (FernFerret, Dragonshadow, Packhead) ++ GameSupport: Added L4D1 roles to L4D2 for 'The Sacrifice' update (Packhead) ++ GameSupport: Added some missing L4D2 award images (rockerdsk) ++ Daemon: Added support for retrieving hostname and maxplayers via server query if rcon is unavailable (psychonic) ++ Daemon: Added 'CpanelHack' conf directive to optionally add $home/perl to include path before loading Geo::IP modules (psychonic) ++ Daemon: Added SIGINT handling to clean up and flush data upon interupt (ctrl-c), also on C;SHUTDOWN; and C;RELOAD; (psychonic) ++ Daemon: Added SIGHUP handling to mimic C;RELOAD; (psychonic) ++ Daemon: Added perl 5.12 compatibility (psychonic) ++ Daemon: Added much more configurability to hlstats-awards.pl (psychonic) ++ Daemon: Added support for re-evaluating player clan tags in hlstats-awards.pl, use -t option (psychonic) ++ Daemon: Added reload option to run_hlstats to allow reloading settings without actually stopping daemon (Packhead) ++ Plugin: Added workaround for Source 2009 linux rules query issue (psychonic) ++ Web: Admin IP search now supports partial matches (psychonic) ++ Web: "Perl Stats Backend" page can now reload remotely located daemons (#1266) (Packhead) ++ Web: Added new player trend graph (psychonic) ++ Web: Added links to daemon control page in Server Settings page in restart warning box and on Reset page (psychonic) ++ Other: Added AMXBans 6.0 support to hlstatsxban.php import script (Packhead) +- GameSupport: Removed FoF weapon explosive_arrow, no longer exists ingame (psychonic) +? Daemon: Chat no longer has to be >3 characters to be logged (psychonic) +? Daemon: Suicide within 2 seconds of team change is now ignored (psychonic) +? Daemon: Event inserts are now queued per table in groups of 10 (default) to reduce number of queries (psychonic) +? Daemon: Added --event-queue-size command line parameter to forcefully override the above (ex. size 100+ when importing logs) +? Daemon: Player count changes now only update player count, rather than triggering full db flush (psychonic, Matt Lawrence) +? Daemon: Changed some parsing to aid pvkii support and potentially give a minor boost in speed (psychonic) +? Daemon: Run script now sends SIGINT instead of SIGKILL when stopping daemon to allow cleanup (Packhead) +? Daemon: run_proxy now defaults to using hlstats.conf instead of the non-existant proxy.conf (psychonic) +? Daemon: Re-wrote run_hlstats to now handle start/stop/reload/restart/status single or multiple daemons (Packhead) +? Daemon: run_hlstats_multi has been deprecated (Packhead) +? Web: Chat page no longer shows server column when showing chat for a single server (psychonic) +? Web: Countries page now defaults to sorting by Avg Skill (psychonic) +? Web: After adding a new server, you now automatically get redirected to the server settings page (psychonic) +? Web: RCon password is no longer displayed on Edit Servers list (psychonic) +? Web: Update check in admin area is now async and will not hang admin page if hlx site is down (psychonic) +! GameSupport: Fixed daemon TFC object dismantle logic not working (psychonic) +! GameSupport: Fixed GES weapon image filenames not matching weapon codes (psychonic) +! GameSupport: Fixed db issue causing some TFC actions to not track properly (psychonic) +! GameSupport: Fixed FoF bug in last release causing all weapons to log as "bow" (psychonic) +! GameSupport: Updated Dystopia support to GameEngine 3 (psychonic) +! GameSupport: Fixed spelling of FoF team name 'desparados' => 'desperados' (psychonic) +! GameSupport: Fixed some issues with certain TF2 weapons/awards not triggering properly (psychonic) +! GameSupport: Re-added two L4D2 weapons (M60, Golf Club) that only existed on new installs (Packhead) +! Daemon: Fixed player flag not showing in livestats if the player was already ingame when daemon started (psychonic) +! Daemon: Fixed "stats" command parsing failing when receiving more output than expected (jocker) +! Daemon: Fixed some incompatibilities with Mani Admin, extra quotes, etc. (psychonic) +! Daemon: Fixed kill streaks persisting across map change (psychonic) +! Plugin: Fixed HLstatsX sv_tag not being applied if custom tags were specified in config (psychonic) +! Web: Fixed some pages using live activity calculation instead of cached value (psychonic) +! Web: Fixed players with hidden rank still displaying on per-session player ranking list (psychonic) +! Web: Fixed some values being html-escaped before being saved to the db (psychonic) +! Web: Fixed action points on player/clan info possibly being inaccurate due to using current point values (psychonic) +! Web: Fixed multiple idiosyncracies with weapon and role image filename and text name lookup. (psychonic) +! Web: Fixed some display glitches on actioninfo page (psychonic) +! Web: Fixed "Reset Players' Names' Counts" checkbox on Reset page not being checked/unchecked with reset all (psychonic) +! Web: Fixed incorrect bracket in default hlstats.css causing warning text to be incorrect size (Hipster) +! Web: Fixed an issue with image filenames that contained spaces (psychonic) +! Web: Fixed signature images not working when using Steam IDs to lookup player (Packhead) +! Other: Fixed hlstatsxban.php import script not properly unbanning unbanned users from Sourcebans (Rambomst) +! Other: Fixed a regex issue with heatmaps and maps with $ in name (Danfocus) + + +HLstatsX Community Edition 1.6.10 +July 13, 2010 + ++ GameSupport: TF2 - Engineer Update - Added new weapons, awards, ribbons and map screenshots (#1205, #1206,#1207) (FernFerret, octo-dhd, Packhead) ++ GameSupport: TF2 - Added missing ribbon images and database support for various weapons (#1197) (octo-dhd, FernFerret, Packhead) ++ GameSupport: CSS - Changed default and existing installs to use GameEngine 3 (Orange Box) (#1203) (Packhead) +? General: hlstats-awards now has three additional (optional) parameters and only runs award and ribbon generation when a date is specified (#1210) (psychonic) + +HLstatsX Community Edition 1.6.9 +June 19, 2010 + ++ GameSupport: Updated HL2DM/HL2CTF weapon images (Carioca, Peterson) +? Daemon: Player stats are now flushed to the database about twice as often due to feedback from users (psychonic) +? Daemon: Player stats flush timer is now per server instead of global so potentially less are flushed at once (psychonic) +? Daemon: Server stats are now flushed on a timer (slightly more frequent than player stats), rather than instantly (#1176) (psychonic) +! Daemon: Fixed an issue that could cause player stats not to save correctly and lead to increased cpu usage (psychonic) +! GameSupport: Fixed TF2 obj_teleporter_exit and _entrance being removed (now logging obj_teleporter instead) (#1177) (Packhead, psychonic) +! Web: Fixed admin pages missing images in some places (#1182) (Packhead) +! Web: Whitespace is now trimmed from the beginner and end of search input (#1134) (psychonic) +! Web: Fixed some redundancy and general silliness with sanitizing of search queries. (psychonic) + +HLstatsX Community Edition 1.6.8 +May 26, 2010 + ++ GameSupport: Added many new TF actions from SuperLogs:TF2 2.x (#1090, #1159) (Packhead) ++ GameSupport: Added 4 new TF weapons from recent TF2 updates (#1108, #1162) (Packhead) ++ GameSupport: Added many new TF weapon/award/ribbon images (psychonic, HeK, FernFerret) ++ GameSupport: Added 2 new L4D2 weapons from The Passing update (#1130) (Packhead) ++ GameSupport: Added new CSS (OB) actions provided by new SuperLogs:CSS (#1168) (Packhead) +? Daemon: Removed some old debug messages that could confuse users (psychonic) +? Web: Put 'beta' notice on perl backend control page as it is not meant to work on all installs yet (psychonic) +? SMPlugin: Removed chat notification when adding to sv_tags (psychonic) +? GameSupport: Change TF default TKPenalty to 0 (#1170) (psychonic) +! Daemon: Fixed possible crash when inserting livestats data (#1063) (psychonic) +! Daemon: Fixed 'EnablePublicCommands' option not being respected on some commands when disabled (#1116) (psychonic) +! Daemon: Fixed IP addresses not being saved for players when tracking in LAN mode (#1142) (psychonic) +! Daemon: Fixed issue causing bots (including l4d infected) to not be recognized in LAN mode (#1166) (psychonic) +! Web: Fixed loading error on player chat history page due to short tags (psychonic) +! Web: Fixed broken formatting on player chat history page (#1076) (psychonic) +! Web: Added missing padding on game page between servers and awards (psychonic) +! Web: Fixed incorrect page count on chat page (psychonic) +! Web: Fixed occurence where options.php could have a blank POST, clearing all options (#1143) (psychonic) +! Web: Fixed multibyte characters not displaying properly in some admin area fields (#1155) (psychonic) +! SMPlugin: Fixed non-default cvar values being ignored after reload (#1073) (psychonic) +! SMPlugin: Fixed sv_tags addition happening multiple times on reload (psychonic) +! GameSupport: Fixed many small issues with NeoTokyo support (#1161, #1173) (SolidSnake916, psychonic) +! GameSupport: AOC no longer defaults to attempting to use ShowMenu for 'OSD' commands (psychonic) +! GameSupport: BG2 now defaults to using ShowMenu for 'OSD' commands (psychonic) +! GameSupport: Added workaround for broken chat in FF 2.4.1 (#1099) (psychonic) +! GameSupport: Removed old NeoTokyo weapon codes and changed m41l to m41s (#1161) (psychonic) + +HLstatsX Community Edition 1.6.7 +February 13, 2010 + ++ Added missing headshot action for cstrike (#1069) (Packhead) ++ Added TF2 actions and awards for airshot_rocket and airshot_headshot (#1065) (Packhead) ++ Added TF2 weapon, award and ribbons for taunt_medic (#1064) (FernFerret, Packhead) ++ Added tf_pumpkin_bomb weapon image and TF2 ball image (FernFerret) ++ Added the ability to search server and player chat logs as well as optimizations (#677, #1034, #1054, #1055) (octo-dhd, agent86) ++ Added L4D2 forum signature background images (gH0sTy) +? Changed references of HLstatsX to HLstatsX: CE in perl scripts (#1036) (octo-dhd) +? Changed "pickaxe" to "unique_pickaxe" in TF2 after Valve update (psychonic) +? Cleaned up all known 'notice' errors in web code (#1058) (octo-dhd) +? Resolved PHP4 compatibility issues (#1035) (octo-dhd) +! Fixed double-connect announcements (octo-dhd) +! Fixed STEAM_ID argument on playerinfo page (#1061) (psychonic) +! Fixed players, clans and server counts on ingame MOTD being ignored (psychonic) +! Fixed the reset page in the admin center to now show disabled games (#1050) (Packhead) +! Fixed character display and maxlength issues in the administration center (#1053, #1062) (octo-dhd) +! Fixed a daemon crash when a player joins and ranking is set to "kills" (#996, #1044) (psychonic) +! Fixed some unpredictable behavior on map-specific plyr and plyrplyr actions (Thor_R, psychonic) +! Fixed issue with short tags being used in certain pages (Kigen, octo-dhd) (#1052) +! Fixed lastAddress, geolookup and flags not appearing on server status pages (#1010, #1011) (octo-dhd) +! Fixed map run times being incorrect if the map didn't "start" (octo-dhd) +! Fixed blank type filter causing no results to print on Admin Event History (psychonic) +! Fixed team message coloring for L4D(2) and HL2MP in hlstatsx.sp (psychonic) + +HLstatsX Community Edition 1.6.6 +January 10, 2010 ++ Added TF2 Ball weapon code (#992) (Packhead) ++ Added team color player names on ingame messages in HL2DM when doing team deathmatch and HL2CTF (psychonic) ++ Added optional (default on) adding of "HLstatsX:CE" to server tags on non-L4D/2 games that support sv_tags (psychonic) +? Changed the header to use the alt text to be more consistent with project (Packhead) +? Changed the default point value to 0 for crit kill action in TF2 (#993) (Packhead) +! Fixed a recently discovered exploit as well as other possible avenues for exploitation (psychonic) +! Fixed game page not loading on a very small number of installs (octo-dhd) +! Fixed opt_libdir being ignored in hlstats.pl and hlstats-awards.pl when loading GeoLite binary file (psychonic) +! Fixed "Can't use an undefined value as a HASH reference" error on some installs, mostly L4D and/or having global chat enabled (psychonic) +! Fixed favorite weapon calculation in claninfo page (#985) (Packhead) +! Fixed weapon list for clan including data from other games (Packhead) +! Removed quotes from playerId in HLstats_EventHandlers.plib to fix potential slowdown in MySQL (psychonic, Packhead) +! Fixed a case where L4D/L4D2 bots that connect to the server list their IP address as "none". (Packhead) +! Fixed the kick method for goldsource games (#1022) (Packhead) +! Fixed a case where lastAddress could be set to "" for players (#1010) (Packhead, psychonic) +! Fixed utf8 characters getting mangled in hlstats-awards.pl. (psychonic) +! Fixed boomer and smoker role codes being incorrect on l4d2 (issue #1013) (psychonic) +! Fixed not checking for existance of geoip data before doing geo lookups in hlstats-awards.pl (#1002) (Packhead) + +HLstatsX Community Edition 1.6.5 +December 23, 2009 + ++ Added a huge number of new TF2 images for awards, ribbons, and weapons (FernFerret) ++ Added new TF2 weapon and action codes for "War" update (Packhead, psychonic) ++ Added missing TF2 awards for Scout (#897) (trawa, Packhead) ++ Added various missing TF2 weapons, awards, and ribbons (#968) (Packhead) ++ Added ability to move servers between "Games" that share the same "realgame" (psychonic) ++ Added HLX:CE logo on hitbox flash file (BusteR) ++ Added HLX:CE donate link to admin area :3 (psychonic) +? Bonus round state now triggers at Round_End in addition to Round_Win and Mini_Round_Win (psychonic) +? Made BonusRoundIgnore be enabled by default for newly added "tf" servers (psychonic) +? Hide option for each game type now uses a checkbox instead of a drop-down box (psychonic) +? Game code on Games page in admin area is now read-only to prevent user-caused issues with changing it (psychonic) +? Daemon code tidied up slightly more along with more slight optimizations (including #823) (psychonic) +? Changed behavior for heatmap generation to only generate with valid data. (BoNzO) +? Added an ini_set for max_execution_time for heatmap generation to prevent timeouts. (BoNzO) +! Fixed a case where an administrator could accidentally lock themselves out of the admin area if not careful (ie. setting password to "(encrypted" or similar) (psychonic) +! Fixed an issue with hlxce_version and hlxce_webpage not being set at server start under certain conditions (psychonic) +! Fixed the admin center timing out when our update check server is down (#965) (psychonic, Packhead) +! Fixed "help" not being blocked by ingame plugin (#914) (Packhead) +! Fixed incorrect Favorite Weapon calculation (#908) (Steph D, Packhead) +! Fixed "to-all" messages just showing a "2" using hlx_sm_psay2 (psychonic) +! Fixed regression in 1.6.3 that caused minplayers to to be ignored for frags on all games except L4D/L4D2 (#934) (psychonic) +! Fixed multiple potential daemon crash scenarios (#880,#917) (psychonic, octo-dhd) +! Fixed an issue with plyrplyr actions not being recognized properly (psychonic) +! Fixed a typo in show_graph.php that caused the graphs not to generate when the style was changed. (#928) (Warbucks) +! Fixed update check telling admin to upgrade when showing installation as being up-to-date. (Packhead) +! Daemon now correctly uses player ip address from connect log line when able instead of doing "rcon status" to retrieve it (psychonic) + + +HLstatsX Community Edition 1.6.3 +December 03, 2009 + ++ Added "Taiwin" to gmap location selection (#818) (Owen Lyu, psychonic) ++ Added some missing weapon codes for l4d2 (psychonic) ++ Added SQL profiling for advanced debugging, disabled by default (agent86) ++ Added new L4D2 role icons (#837) (ribit) ++ Added new CSS class to provide a background for "gameslist" icons (#834) (Packhead) ++ Added a simple version check to the admin center (#825) (psychonic) ++ Added additional data-table-head classes to voicecomm server pages (#856) (Packhead) ++ Added a CSS ID "gameslist-active-game" to allow for customization to gameslist when you're on the appropriate game's page (#834) (Packhead) ++ Added actions for SuperLogs:TF2 new "Most Valuable Player" tracking (#826) (Packhead) ++ Added a new "noaward" image for Left 4 Dead 2 (Packhead) ++ Added/enhanced coloring to ingame messages on L4D, L4D2, HL2MP, and DODS (psychonic) ++ Added partial verification of Server Address and Admin Mod choice to Add Server page. (psychonic) ++ Added ability to use steam_id AND game for signatures instead of player_id (#877) (Packhead) ++ Added colored Left4Dead Roles (#890) (Packhead) +- Removed "smash-only" l4d2 awards from installations and future upgrades, will not be removed from existing sites (psychonic, Packhead) +- Removed "run_hlstats_autorestart" as it is redundant now. (Packhead) +- Removed non-existant "SMAC" weapon from NTS in install.sql (psychonic) +? Made multiple optimizations on the Chat page (#829) (agent86, Packhead) +? Player DB Updates in daemon now utilize prepared statements (#842) (agent86) +? Updated INSMOD and FOF game icons (psychonic) +? Updated the run_ scripts so they could be used as a startup script in init.d (#861) (Packhead) +? hlx_sm_csay now does light green chat-style messages on L4D/L4D2 instead of extremely small center message (psychonic) +? Various minor daemon optimizations (psychonic) +? Cleaned up code in hlstatsx.sp and minor optimizations (psychonic) +? Modified date that is printed for Last Kill on contents.php (#863) (Packhead) +? Cleaned up updater notifications and added new "warning", "warning-header" and "warning-text" CSS classes (Packhead) +? Modified the doConnect method to retry on daemon connection error (#841) (agent86) +? Changed map screenshot behavior to now fail back to a game default and then the HLXCE logo if the map screenshot cannot be found. (Packhead) +? Changed stylesheet cookie behavior (#806) (octo-dhd) +? Touched up favicon.ico (psychonic) +? Slightly darkened watermark on forum sig image (psychonic) +? Updated run_hlstats and run_proxy with variables to daemon filename, conf file and path. (Packhead) +? Updated run_hlstats_multi with variables for daemon filename and path (Packhead) +? Daemon no longer counts incaps (l4d/l4d2) where victim is on Infected team (#836) (psychonic) +? Revised logic on mapinfo.php to follow logic from status.php and utilize getImage() (Packhead) +? Changed user-settings on playerinfo_general.php to now say "Not Set" and be linked to #set on the help page (#869) (Packhead) +? Check for existance of geolite binary file before attempting to load it (#807) (BoNzO) +? Changed behavior of config file/command line -- command line option now overrides conf file settings (#862) (octo-dhd) +! Fixed Zoom support to Google Maps (psychonic) +! Fixed an issue with plyrplyr actions being ignored (#883) (psychonic) +! Fixed (attempt) for possible null error on geodata inserts (#880) (psychonic) +! Fixed a typo in updater.php (Packhead) +! Fixed "alt" text on ribbon images (#882) (Packhead) +! Fixed some bugs in display on playerinfo_general.php (#869) (Packhead) +! Fixed some ingame web issues (Packhead) +! Fixed anchor tags on some pages which made some CSS styles look bad (#835) (Packhead) +! Fixed new game code and name getting escaped twice when duplicating a game (#855) (psychonic) +! Fixed not being able to have two teamspeak servers with different connect ports but same query port (#851) (psychonic) +! Fixed "broken text" on award pages when nobody wins the award (#857) +! Fixed some issues in global and sourcebans CSS revolving around the gamelist icons (Packhead) +! Fixed status.php to also make use of default.png for missing map screenshots (#852) (Maximilian Lotz, Packhead) +! Fixed header links not correct after performing search or using admin pages (#486) (psychonic) +! Fixed display_style_selector having the wrong optype and causing an error on the daemon (Packhead) +! Fixed run_hlstats_multi not using the logs directory (#736, #802) (Packhead) +! Fixed all run scripts to use relative paths (#737) (Packhead) +! Fixed hlstatsxban.cfg not being read correctly (#603) (Valentin G., psychonic) +! Fixed error that could cause graphs not to display correctly (psychonic) +! Fixed spacing issue with player names on some ingame messages (psychonic) +! Fixed incorrect format specifier in daemon player info string causing uniqueid to not print properly (psychonic) +! Fixed possible null error on playerhistory.php and tools_adminevents.php (#553) (psychonic) +! Fixed utf8 character display issue on playerhistory.php and tools_adminevents.php (psychonic) +! Fixed slow query on claninfo_mapperformance.php (#816) (agent86) +! Fixed footer having "an Interwave Studios Community project" twice (#810) (Packhead) +! Fixed ingame forced messages not displaying if BroadCastEvents was off (#839) (psychonic) +! Fixed ingame commands requiring MOTD not working in HL2DM (psychonic) +! Fixed extinguish actions from being incorrect action type in TF2 (#729) (psychonic) +! Fixed Forums link on web site only showing if Sourcebans link was enabled. (psychonic) +! Fixed Romania coordinates being negated on gmap. (Nigh) +! Fixed Sourcebans theme table header background image not filling full cell if screen size is too small (#812) (octo-dhd) +! hlx_sm_psay2 command in hlstatsx.sp now strips color control codes from input message to combat solid message color interruption that occurred on some games (psychonic) + + + +HLstatsX Community Edition 1.6.2 +November 14, 2009 + ++ Added support for Left 4 Dead 2 (#732) (Packhead, psychonic, octo-dhd) ++ Added new update script for geoip binary file (Packhead) ++ Added misc dev tools such as fake server (agent86), log code parser (psychonic), optional web profiling code (agent86), and server query tool (BoNzO) ++ Added Team Fortress 2 tf_pumpkin_Bomb weapon logging (octo-dhd) ++ Added five additional Zombie Panic Source weapons (Packhead) ++ Added support for TF2 jarate and shield_blocked events (Packhead) ++ Created a new field to track creation date for a player and modified daemon to start filling field (psychonic, Packhead) ++ Added medic and engineer extinguish awards (#729) (Packhead) ++ Created php-based SQL updater to ease upgrading from version to version (psychonic) ++ Headshot actions and awards added for ZPS and NTS (psychonic) ++ Added new signatures to replace existing signature images (R3M) ++ Added a watermark inserted on generation for signature images (psychonic) ++ Added signature background images for The Hidden (R3M) ++ Added R3M's "Sourcebans" theme to HLX:CE as the new default theme for new installations (R3M, Packhead, psychonic, octo-dhd) ++ Added small HLXCE logo to display on accordian pages when loading Trend graphs (Packhead) ++ Added ability for end user to switch themes and for administrator to enable/disable this option, Defaults to off on new and old installs (psychonic, Packhead) ++ Added ingame pages "Go Back" link to allow for a quick return while browsing stats through the in-game browser (Packhead) ++ Added new logo in the header and footer (psychonic, Packhead) ++ Added new Favicon of new logo (psychonic) ++ Added NTS award and ribbon icons (R3M, TheTrickster, Packhead) ++ Added new game list at the top-right of the page that displays with more than one game enabled and the administrator enabling, Defaulted on for new and old installs (Packhead) ++ Added ability for 'themes' to load custom navigation icons per stylesheet. (Packhead) ++ Added new options to specify path to SourceBans and Forum pages. Icon must be present in hlstatsimg/icon// and administrator must specifiy path. (Packhead) +- Unused NeoTokyo weapon codes removed (psychonic) +- OnTakeDamage removed (psychonic) +- Removed unused "racket" weapon code for ZPS (psychonic) +- Fixed recording "other" crowbar as broom in ZPS (ViolentCrimes, psychonic) +- Removed options to set Trend Graph background image (Packhead) +? Server graph generation is now delayed until the accordion is open (#640) (agent86) +? Removed some deprecated function from the php files (psychonic) +? Site now utilizes google chrome frame if installed (psychonic) +? Moved configuration for heatmap script into a separate configuration file and added debug output (BoNzO) +? Moved configuration for hlstatsxban.php into a separate configuration file (Packhead) +? Event insertion queries in daemon now get prepared and reused to severely cut down on overall time spent in queries (#543) (agent86 +? Cleaned up output and added pruning of hlstats_Trends to hlstats-awards.pl (#773) (psychonic) +? Hlstatsx.sp now hides options from menu that require motd screen if on a game that does not support it (psychonic) +? Changed "Console Events" wording in ingame menu in hlstatsx.sp to "Toggle Point Msgs" +? Updated to Google Maps v3 API (#560) (octo-dhd) +! Fixed incorrect function return in hlstatsx.sp interfering with some chat plugins (Antithasys) +! Fixed some issues with php ban importer script (Packhead) +! Fixed miscellanous invalid html (#627) (psychonic) +! Fixed misc crashing issues in daemon from invalid players/servers (including #628) (b|afk, agent86, psychonic) +! Fixed occurrences in daemon when rank could print as 0 (octo-dhd) +! Fixed issues with changing GeoIP setting and reloading daemon config rather than restarting daemon (#598) (psychonic) +! Fixed issues where headshots could be doubled or incorrect kills being counted as headshots since 1.6.0 (#602) (psychonic) +! Fixed map-specific actions being broken since 1.6.0 (psychonic) +! Fixed "HLstats_Server->set: "map" is not a valid property name" in daemon console (#362) (agent86) +! Fixed messageAll() sub on daemon server object not checking to make sure event broadcasting is enabled before attempting to send. (psychonic) +! Fixed magic quotes issue on player and clan info weapon pages (#695) (raydan) +! Fixed trend count telling incorrect number of new players/kills in last 24 hours (#657) (psychonic) +! Fixed potential array out of bounds error in hlstatsx.sp (psychonic) +! Fixed more cases in hlstatsx.sp where names could be miscolored (TTS Oetzel & Goerz GmbH) +! Fixed game detection successful message in hlstatsx.sp not necessarily showing correct game name (psychonic) +! Fixed location logging format in LogPlyrPlyrEvent in loghelper.inc (psychonic) +! Fixed old events not being deleted (psychonic) +! Fixed a grammatical problem with TF2 pumpkin bomb weapon (Packhead) +! Fixed hlstats-awards.pl not detecting correct version (psychonic) +! Fixed the hitbox flash on ZPS to use the zombie1 file instead of the cstrike model. (psychonic) +! Fixed country flags not displaying since implementation of accordians on game page (octo-dhd) +! Fixed second level cstrike ribbons using the same filename as first level (Unity, Packhead) + + + +HLstatsX Community Edition 1.6.1 +September 23, 2009 + ++ Added many new heatmap background images (stachi, Roman Sobol, HSFighter, Zuko, Extrim25, Patalete, Packhead) ++ Added missing Ventrilo images (compuwis) ++ Added Fistful of Frags award images (Mosalar) ++ Added alternate php ban import script to extras (Peace-Maker) ++ Added FTP script in extras to allow logs to be pulled on a schedule instead of streamed (Woody) ++ Added more DODS awards (BusteR, Packhead) ++ Added missing image for NTS DetPac (R3M) ++ Added much more granularity to options on Admin Reset page (psychonic) ++ Added HLstatsX:CE Settings option to Sourcemod's !settings menu (psychonic, Packhead) ++ Added parsing for TF2's new medic_death logging to create new killed_charged_medic action (psychonic) ++ Added parsing for TF2's sawblade death logging to create new death_sawblade action (psychonic) ++ Added Romania to Google Maps country list (Alexandru Bajdechi, KingJ) ++ Added total kill count to contents.php (psychonic) ++ Added InterWave Studios link to footer.php (psychonic) ++ Added compiled ep1 linux build of stub version plugin for installs without Sourcemod to report HLX:CE version (psychonic) ++ Added new FF weapon "backstab", including award (psychonic, Rawh) ++ Added more database indexes for optimization (agent86) ++ Added new tf2 "telefrag" weapon code and award (octo-dhd) ++ Added missing tf2 award for deflect_arrow (Packhead) +! Fixed player counts issues that started with release 1.6.0 (octo-dhd, agent86, psychonic) +! Fixed hlxce_ cvars becoming unset if game server restarted without a daemon restart (psychonic) +! Fixed "Unblessed reference 'increment'" that results from players with blank names (octo-dhd, psychonic) +! Fixed "data truncated for column 'pos_x'" error on plyrplyr actions (Mave Rick) +! Fixed GlobalBan query in importbans.pl (Peace-Maker) +! Fixed heatmap generation script hitting php memory limit (Kotonoha Katsura) +! Fixed "String found where operator expected at (eval 38) line 1, near "$ = ''"" error on startup since 1.6.0 (psychonic) +! Fixed web rendering error on game.php when showing multiple servers without javascript "glider" enabled (psychonic) +! Fixed error when running hlstats-resolve.pl since 1.6.0 (psychonic) +! Fixed issue with encodings on some pages (psychonic) +! Fixed floats not being cleared on playerinfo and claninfo causing display glitches on some sections in tabbed mode (psychonic) +! Fixed tables width on "Weapon Statistics" table on Weapons section on claninfo (psychonic) +! Fixed weapon statistics logged after player disconnect being ignored (psychonic) +! Fixed AutoTeamBalancer not working in CSS since 1.6.0 (stachi) +! Fixed L4D heatmaps not generating properly (Packhead) +! Fixed daemon crashing when receiving control packets since 1.6.0 (BoNzO) +! Fixed hidden clans being able to be shown on games list (BoNzO, Packhead) +! Fixed sql error in daemon that could arise if you have a game without servers but have past data from (Mave Rick) +! Fixed cases players earning a point for suicide or teamkill if point value was set to 0 (psychonic) +! Fixed potential sql errors on Player Event History page if events referenced players, servers, or any other data that no longer exists (psychonic) +! Fixed a potential unique key issue in hlstats_Servers_VoiceComm table (psychonic) +! Fixed number formatting in General Statistics on contents.php (psychonic) +! Added missing % sign after accuracy in kdeath command output (psychonic) +! Fixed variable names being printed on ingame load page (psychonic) +! Fixed typo in sql query on ingame weapons page causing sql query error (psychonic) +! Removed link to activity gantt scheme, since we don't use it in 1.6, it only generated an broken image (BoNzO) +! Fixed circular reference in TRcon.pm (agent86) +! Fixed scope issue with g_players_temp in hlstats.pl (agent86) +! Fixed missing label for hideranking 3 on tools_editdetails_player.php (psychonic) +! Fixed shots per kill count on playerinfo_general.php to more accurately use statsme table kills to match statsme table hits/shots, rather than comparing to total kills (psychonic) +! Fixed ventrilo server join link not working properly in some scenarios (Semikolon) +! Fixed default graph color to match default style (psychonic) +! Fixed weapon/role name not displaying on Weapons/Roles page if no image was found (if weapon) and code contained on or more spaces (psychonic) +! Fixed html characters in weapon names on Weapons page being escaped twice (psychonic) +! Servers and players for hidden games are no longer counted on contents.php (psychonic) +! Fixed a case where the incorrect top player for a game on contents.php could be shown if more than one player was tied for skill/kills (psychonic) +! Fixed country tooltip and image alt text being lowercase on flag beside player name on playerinfo_general.php (psychonic) +! Fixed issues with display of ingame chat where a name had a " or ' (psychonic) +- Removed redundant "Remove Game Settings" admin page since deleting a game on the Games page does the same thing, but correctly (psychonic) +? Touched up many images (R3M, Packhead, BoNzO, Unity, Violent Crimes) +? Optimized rank queries in web and daemon (agent86, psychonic, BoNzO) +? Player table updates in daemon are now buffered and flushed to greatly reduce number of queries being ran(agent86) +? Generation of heatmaps now works for duplicated games with manual changes (BoNzO) +? Ingame plugin now displays a notice to players that have HTML MOTDs disabled if they use a command that would require one (psychonic, Packhead) +? By popular request, adjusted game total player count to not exclude inactive players, pre1.6.0 behavior (psychonic) +? Updated run_importbans to allow for user to use either importbans.pl or Peace-Maker's php version (Packhead) +? Updated included DejaVu font to version 2.30 (http://dejavu.sourceforge.net) +? When deleting a game via the Games page in the admin area, all associated data for that game is now removed. A warning was also added on that page (psychonic) +? When deleting a server via the Edit Servers page, server trend data for that server is now also deleted (psychonic) +? Moved db cleanup from hlstats.pl to hlstats-awards.pl (agent86, psychonic) + + +HLstatsX Community Edition 1.6.1 +September 23, 2009 + ++ Added many new heatmap background images (stachi, Roman Sobol, HSFighter, Zuko, Extrim25, Patalete, Packhead) ++ Added missing Ventrilo images (compuwis) ++ Added Fistful of Frags award images (Mosalar) ++ Added alternate php ban import script to extras (Peace-Maker) ++ Added FTP script in extras to allow logs to be pulled on a schedule instead of streamed (Woody) ++ Added more DODS awards (BusteR, Packhead) ++ Added missing image for NTS DetPac (R3M) ++ Added much more granularity to options on Admin Reset page (psychonic) ++ Added HLstatsX:CE Settings option to Sourcemod's !settings menu (psychonic, Packhead) ++ Added parsing for TF2's new medic_death logging to create new killed_charged_medic action (psychonic) ++ Added parsing for TF2's sawblade death logging to create new death_sawblade action (psychonic) ++ Added Romania to Google Maps country list (Alexandru Bajdechi, KingJ) ++ Added total kill count to contents.php (psychonic) ++ Added InterWave Studios link to footer.php (psychonic) ++ Added compiled ep1 linux build of stub version plugin for installs without Sourcemod to report HLX:CE version (psychonic) ++ Added new FF weapon "backstab", including award (psychonic, Rawh) ++ Added more database indexes for optimization (agent86) ++ Added new tf2 "telefrag" weapon code and award (octo-dhd) ++ Added missing tf2 award for deflect_arrow (Packhead) +! Fixed player counts issues that started with release 1.6.0 (octo-dhd, agent86, psychonic) +! Fixed hlxce_ cvars becoming unset if game server restarted without a daemon restart (psychonic) +! Fixed "Unblessed reference 'increment'" that results from players with blank names (octo-dhd, psychonic) +! Fixed "data truncated for column 'pos_x'" error on plyrplyr actions (Mave Rick) +! Fixed GlobalBan query in importbans.pl (Peace-Maker) +! Fixed heatmap generation script hitting php memory limit (Kotonoha Katsura) +! Fixed "String found where operator expected at (eval 38) line 1, near "$ = ''"" error on startup since 1.6.0 (psychonic) +! Fixed web rendering error on game.php when showing multiple servers without javascript "glider" enabled (psychonic) +! Fixed error when running hlstats-resolve.pl since 1.6.0 (psychonic) +! Fixed issue with encodings on some pages (psychonic) +! Fixed floats not being cleared on playerinfo and claninfo causing display glitches on some sections in tabbed mode (psychonic) +! Fixed tables width on "Weapon Statistics" table on Weapons section on claninfo (psychonic) +! Fixed weapon statistics logged after player disconnect being ignored (psychonic) +! Fixed AutoTeamBalancer not working in CSS since 1.6.0 (stachi) +! Fixed L4D heatmaps not generating properly (Packhead) +! Fixed daemon crashing when receiving control packets since 1.6.0 (BoNzO) +! Fixed hidden clans being able to be shown on games list (BoNzO, Packhead) +! Fixed sql error in daemon that could arise if you have a game without servers but have past data from (Mave Rick) +! Fixed cases players earning a point for suicide or teamkill if point value was set to 0 (psychonic) +! Fixed potential sql errors on Player Event History page if events referenced players, servers, or any other data that no longer exists (psychonic) +! Fixed a potential unique key issue in hlstats_Servers_VoiceComm table (psychonic) +! Fixed number formatting in General Statistics on contents.php (psychonic) +! Added missing % sign after accuracy in kdeath command output (psychonic) +! Fixed variable names being printed on ingame load page (psychonic) +! Fixed typo in sql query on ingame weapons page causing sql query error (psychonic) +! Removed link to activity gantt scheme, since we don't use it in 1.6, it only generated an broken image (BoNzO) +! Fixed circular reference in TRcon.pm (agent86) +! Fixed scope issue with g_players_temp in hlstats.pl (agent86) +! Fixed missing label for hideranking 3 on tools_editdetails_player.php (psychonic) +! Fixed shots per kill count on playerinfo_general.php to more accurately use statsme table kills to match statsme table hits/shots, rather than comparing to total kills (psychonic) +! Fixed ventrilo server join link not working properly in some scenarios (Semikolon) +! Fixed default graph color to match default style (psychonic) +! Fixed weapon/role name not displaying on Weapons/Roles page if no image was found (if weapon) and code contained on or more spaces (psychonic) +! Fixed html characters in weapon names on Weapons page being escaped twice (psychonic) +! Servers and players for hidden games are no longer counted on contents.php (psychonic) +! Fixed a case where the incorrect top player for a game on contents.php could be shown if more than one player was tied for skill/kills (psychonic) +! Fixed country tooltip and image alt text being lowercase on flag beside player name on playerinfo_general.php (psychonic) +! Fixed issues with display of ingame chat where a name had a " or ' (psychonic) +- Removed redundant "Remove Game Settings" admin page since deleting a game on the Games page does the same thing, but correctly (psychonic) +? Touched up many images (R3M, Packhead, BoNzO, Unity, Violent Crimes) +? Optimized rank queries in web and daemon (agent86, psychonic, BoNzO) +? Player table updates in daemon are now buffered and flushed to greatly reduce number of queries being ran(agent86) +? Generation of heatmaps now works for duplicated games with manual changes (BoNzO) +? Ingame plugin now displays a notice to players that have HTML MOTDs disabled if they use a command that would require one (psychonic, Packhead) +? By popular request, adjusted game total player count to not exclude inactive players, pre1.6.0 behavior (psychonic) +? Updated run_importbans to allow for user to use either importbans.pl or Peace-Maker's php version (Packhead) +? Updated included DejaVu font to version 2.30 (http://dejavu.sourceforge.net) +? When deleting a game via the Games page in the admin area, all associated data for that game is now removed. A warning was also added on that page (psychonic) +? When deleting a server via the Edit Servers page, server trend data for that server is now also deleted (psychonic) +? Moved db cleanup from hlstats.pl to hlstats-awards.pl (agent86, psychonic) + +HLstatsX Community Edition 1.6.0 +August 12, 2009 + ++ added support for NeoTokyo, including headshot support and images (R3M, gH0sTy, ViolentCrimes) ++ added first version of new proxy daemon to distribute load between multiple daemon instances (BoNzO) ++ added key for proxy and daemon. When used, allows daemon to accept control commands from ip addresses other than 127.0.0.1 (BoNzO) ++ added hlxce_version (not to be confused with hlxce_plugin_version) cvar to ingame plugins (psychonic) ++ added server query methods to get server map when rcon is unavailable and to identify new servers when allowing all servers to track (BoNzO) ++ added new logic to determine whether or not a player should could toward the minimum amount of players set (to leave out bots if IgnoreBots is set or leave out team "" and spectators) without affecting visible player count (psychonic) ++ added option to specify config file for daemon on command line (BoNzO) ++ added ability to change player hint command via server configuration page in admin area (psychonic) ++ added heartbeat daemon control command to check status (BoNzO) ++ admin event logging now also tracks amxx admin actions that are in game log (psychonic) ++ added run_ script for importbans utility (Packhead) ++ added GlobalBan support to importbans utility (R3M) ++ added heatmap generation support and many heatmap background images (msleeper, BoNzO, `Zuko) ++ added more hl2dm and l4d award images (MrXorMrY) ++ added new CSS connection time award & ribbon images (HSFighter) ++ killlocation world actions from SuperLogs plugins are now supported to catch kill/death coordinates for use with next logged kill (psychonic) ++ full headshot tracking for more mods (INS, NTS, GES) is now available in combination with new SuperLogs plugins (psychonic) ++ colored ingame messages are now supported in Age of Chivalry (psychonic) ++ hlxce_version and hlxce_webpage are now automatically set on the game server by the daemon if rcon is available (psychonic) ++ hlstats.pl and hlstats-awards.pl now support a --configfile directive (BoNzO) ++ TF2's logged player_extinguished event is now supported as four different player actions, one per class (psychonic) ++ ventrilo servers are now supported in the voice server list and viewer (compuwis, psychonic) ++ added many new ribbons for cs1.6 (Unity) ++ added beta autocomplete on search box on Players page (KingJ, BoNzO) ++ added new historical cache feature to add ability cache every page on first load to ease load on static stats archives (octodhd) ++ added l4d map thumbnails (Honk) ++ added many missing TF2 weapon, award, and ribbon images (Semikolon) ++ added two new web styles, "classic" and "nom nom nom" (psychonic) ++ added options version stub VSP plugins for people who wish to report their HLX:CE installs but aren't running the sourcemod or amxx (psychonic) +! fixed many perl warnings in daemon (psychonic) +! fixed and optimized more regular expressions in daemon (psychonic) +! fixed issue with banned players not being hidden from ranking even with global banning enabled (psychonic) +! fixed bug where rcon connection would be lost on map change on some configurations (raydan aka Ben6006) +! fixed some rank lookups in daemon returning incorrect rank when ranking by kills (psychonic) +! fixed connect announce messages saying player had 0 kills when ranking by kills (octodhd) +! fixed some cases where ingame messages would have incorrect or inconsistent coloring (psychonic) +! fixed nonexistant pages not returning 404 header (KingJ) +! added missing ribbon entries for Galil in CSS (BoNzO) +! fixed bans not being able to be sorted by date (R3M) +! fixed daemon crash when set to track any servers and a logged server has a single quote in its hostname (psychonic) +! fixed a ping of 0 sometimes getting recorded for players (psychonic) +! fixed compiler warning in HLX:CE Ingame Plugin on Sourcemod 1.3+ (psychonic) +! replaced remaining, deprecated ereg_ functions from pages that still had them (psychonic) +! servers are now properly added to db with default config for server's game when "Allow only servers set up in admin panel to be tracked" set to "no" (BoNzO) +- removed legacy support for specifying game servers in hlstats.conf (psychonic) +- removed the need for redundant AddressPort option in server configuration (psychonic) +- removed "Type help..." server advertisement as it is better left up to other server plugins for ease of enabling or disabling the message or changing the text/frequency of it (psychonic) +- removed mp_logdetail 3 requirement for tracking headshots in DOD:S (SuperLogs:DODS or equivalent plugin is now needed to track DODS headshots) (psychonic) +- removed all auxilary logging from HLstatsX:CE Ingame Plugin in favor of using new specialized SuperLogs plugin set (psychonic) +? png's are now supported for game icon images (psychonic) +? gif's and jpg's are now supported for award images (psychonic) +? normalized the names of like column headings on web interface (psychonic) +? web portion, aside from the admin area, is now XHTML 1.0 Transitional compliant. stylesheets have also been reworked. table layout has been converted to divs (psychonic, octodhd) +? many optimizations added to the daemon, large and small (octo-dhd, psychonic) +? many small optimizations added to sourcemod ingame plugin (psychonic) +? some small db changes were made to speed up loading of some pages and some daemon interactions (psychonic) +? hlx_webpage cvar in ingame plugins renamed to hlxce_webpage (psychonic) +? weapon and action info (codes/point values/modifiers) is now cached in the daemon (psychonic) +? activity is now calculated with hlstats-awards.pl instead of on-the-fly with every rank query (loses up to 3.5% accuracy for active players based on minactivity of last 28 days, but much faster) (psychonic) +? more coordinates are now stored, including victim coords on death, player and victim coords on plyrplyr actions, and victim position on teamkills (psychonic) +? log filename format is now ymd instead of dmy (Packhead) +? optimized filesize of many more of the included images (many already done in 1.5.x) (psychonic) +? edit Player/Clan Details links are now only visible when logged in as an admin (U#0) +? custom banner now supports external URLs (MadMakz) +? severely sped up Maps page by adding caching of counts of kills and headshots per map (psychonic) +? renamed remaining places that said "cheaters" to "bans" or "banned users and cheaters", including ingame command to display them (psychonic) +? hlx_sm_psay no longer colors ingame messages on insurgency. change command to hlx_sm_psay2 for this (psychonic) +? rcon broadcasts to all (with sourcemod plugin) are now sent to all users at once instead of in groups of eight (psychonic) +? nojs check on page load no longer uses redirectly upon js not being available and now does an actual check instead of relying on useragent (KingJ, psychonic) +? claninfo -> General now used shared google_maps.php include (psychonic) +? ie8 compatibility mode button is now hidden (psychonic) +? rank # no longer shows on livestats for players not on a team (psychonic) + +HLstatsX Community Edition 1.5.6 +May 27, 2009 +* added: new weapon codes for tf2 (force_a_nature, ambassador, tf_projectile_arrow (huntsman), taunt_spy, and taunt_sniper) +* added: handling of "Dead Ringer" for tf2. fake kills will show realistic point message to attacker upon fake death, but no points will be adjusted nor kill recorded +* added: pyro_extinguish and sniper_extinguish actions for tf2 to ingame plugin +* added: search by IP address can now be used in track modes other than "LAN" (admin only) +* added: Edit Player Details screen now shows IP addresses used by that player +* added: Edit Player Details screen now has more editable fields (points, kills, deaths, headshots) +* added: IP addresses in Admin Event History are now linked to IP address search results page +* added: script to import bans from a Sourcebans, AMXBANS, or Beetlesmod ban database +* changed: admin event logging for Sourcemod is now logged as one type per originating plugin, allowing filtering of messages per plugin +* changed: cheaters.php is now bans.php. name in header in now "Bans" and page name displays as "Cheaters and Banned Players" +* changed: Admin Event History page now shows nothing instead of ' password ""' when password is not recorded (as in Source games) +* changed: many post hooks in ingame plugin have been changed to post hooks (as pre hooks were unnecessary) +* bugfix: kill streak actions were not being logged for players whom had kills after their last death but before disconnecting +* bugfix: some bugs in connect message were sometimes causing an incorrect and unrelated message to be displayed +* bugfix: Admin Event History page require register_globals to be on for filter to function properly +* bugfix: headshot actions in weapon_logging plugin were not logging properly +* bugfix: minactivity was getting set to incorrect value after reloading daemon settings +* bugfix: tf2 arena first_blood action broke with spy/sniper update +* bugfix: shots were being recorded twice for most weapons for css and l4d in weapon_logging plugin + +HLstatsX Community Edition 1.5.5.2 +May 12, 2009 +* bugfix: Fixed regression in 1.5.5 causing headshots in DOD:S to no longer be counted + +HLstatsX Community Edition 1.5.5.1 +May 6, 2009 +* bugfix: Fixed daemon crashing on mapchange +* bugfix: Fixed pages ending display at first "weaponimg" column +* bugfix: Fixed bot detection (octo) +* bugfix: Fixed sql error on reset page when resetting player names history (or all) for all games + +HLstatsX Community Edition 1.5.5 +May 4, 2009 +* added: round_win and headshot actions for Insurgency that fire but were missing from default install +* added: in-game award actions for GE:S, fired but were missing from default install +* added: new weapons, roles, maps, and award images for GES (Dr.NO and the GE:S team) +* added: colored ingame messages for FOF +* added: forum sig images for dystopia, hl2ctf, hl2mp, ns, and sgtls (HO!NO!) +* added: new tf2 and css weapon images (HO!NO!) +* added: clicking forum sig text in playerinfo_general.php now highlights the text. Also, text is now read-only (MadMakz) +* added: new weapon images for bg2 (the bg2 team) +* added: new UA stylesheet (Unity) +* added: map images for FOF (Fragenstein) +* added: bot dectection for /whichbot/ bots +* added: weapon name tooltip for weapon images +* added: geoip lookup when importing logs via STDIN or when rcon password is unknown (Woody) +* added: added support for "own" configfiles in your homedir (BoNzO) +* added: new special award code allsentrykills to combine kills from all levels of sentry guns in tf2 +* added: daemon now sets the value of the hlx_webpage cvar on the amxx and sm plugins +* added: added "mapfix" to sm plugin similar to psychostats plugin to print a line to the log stating the current map when the plugin is loaded, usually when server is started. Should result in less events not having a map named. +* added: default values for PlayerEventsAdminCommand for each supported admin mod +* added: admin message in ATB to tell admins that a player was moved +* changed: removed unused code from amxx plugins +* changed: game reset tool now uses truncate instead of delete when possible when removing data for all games, much faster on large DBs +* changed: forced gameserver (log) time to be used for tracking of certain events to increase accuracy (octo) +* changed: removed closing of handle in sm plugin in OnPluginEnd (redundant as all handles are already closed when a plugin is unloaded) +* changed: moved hint command to be user-specifyable instead of hardcoded in daemon. Added mod defaults with game default overrides. +* changed: simplified some logic in global banning code +* bugfix: graph image filename no longer has "unset" in name (octo) +* bugfix: multiple issues on search page, including game filter not functioning when searching by playername +* bugfix: sql error on reset page when resetting all data for one or all games +* bugfix: potential column size error on admin event history +* bugfix: headshot action was not always firing for some games (if health ended up < 0) (Dr. NO) +* bugfix: fixed weapon logging for FOF (because of the way the game is coded, it will only log accurately on dm mode, disabled on others) +* bugfix: color hex code was printing in some ingame messages in GE:S +* bugfix: roles images were not aligned properly +* bugfix: cheaters and people with hidden ranking were able to earn awards/ribbons +* bugfix: cstrike amxx plugin creates many unnessecary log line (one per hit) +* bugfix: text of connection error in class_db.php references hlstats.php instead of config.php for settings +* bugfix: playerinfo_general.php does not close socket after fetching steam community info +* bugfix: query for favorite weapon on playerinfo is not always accurate +* bugfix: server graph showing 0 for activity and fps on some systems (octo) +* bugfix: ns individual builtstructure actions were not firing +* bugfix: bow with regular arrow in FOF sometimes logs as bow, sometimes as arrow +* bugfix: kills, deaths, headshots, etc. counts were carrying over on game copy tool +* bugfix: unicode characters do not show properly on ingame messages in insmod, issue 66 (IceMatrix) +* bugfix: rescue_survivor action in l4d was firing anytime any "survivor-door" was opened, whether or not there was a survivor to be rescued +* bugfix: Ingame clans page was showing clans marked as hidden (Issue 67) +* bugfix: Unicode characters in google map (for player, city, and country names) were not displaying properly +* bugfix: ZPS detection in ingame sm plugin was out of date on description check +* bugfix: maxplayers no longer gets reset to 0 if rcon access is lost +* bugfix: plain_uniqueid property on server object in daemon now stores actual game steamid for use in ban and kick statements (instead of incorrectly using trimmed, stored version of steamid) +* bugfix: ATB was not always showing switch message to switched players. Added fallback for switch message on games that don't support hint style message + +*** build env changes (BoNzO) +* restructured build environment to collect the scripts at one place. +* added command sudo before epm command if building for .deb since the filepermissions will be screwed if you don't run epm as root under debian like OS. +* updated man page to include how to setup userspecific config file with deb and rpm packages + + +HLstatsX Community Edition 1.5.4 +Mar 24, 2009 +* added: new full/partial reset page to admin panel. New page allows more control of exactly what gets reset +* added: map image for dod_palermo (Unity) +* added: many new (great-looking) custom forum signature background images (HO!NO!) +* added: colored message support ingame for ff (thx hlstriker), ges, and insmod (thx Fyren) +* added: support for Dystopia, and Stargate: The Last Stand, and The Battle Grounds 2 +* added: hitbox images for GE:S (thanks GE:S team for the images) +* added: victims section to actioninfo, playerinfo, and claninfo for plyrplyr actions +* added: several new l4d actions +* added: requested option in Edit Player Details to block avatar from loading (uses default image instead) +* added: requested option to hide query execution information in footer. This is on (info hidden) by default for new installs. +* added: link to countryclaninfo from country in Location field on playerinfo +* added: ability to set game defaults that override mod defaults (ie. blank PlayerEventsCommandOSD for games that don't support msay) +* added: Map region per clan can now be chosen in Edit Clan Details screen (inspired by an addon by R3M) +* changed: restructured sig directory and moved game-specific sig background images to game folders +* changed: updated text on Help page +* changed: reworked parts of logging in and out and keeping track of session (octo) +* changed: Each games's icon is now "game.gif" since game-$game.gif was redundant after splitting up game directories and would also need extra logic built into duplication tool +* changed: Backstab can now optionally be a plyrplyr action +* changed: Reset page now let's you choose from any non-hidden game, instead of any game with servers +* changed: Reworded message on stats disabled for not-min-players +* changed: Games on contents page are now orders by realgame, name +* changed: Updated DejaVu font (used for forum signatures) to version 2.29 (http://dejavu-fonts.org) +* changed: added many optimizations to the daemon including eliminating many lookups by caching data, and reducing rcon commands in some areas by 50-66% +* changed: clarified usage instructions for hlx_sm_bulkpsay that are shown when typing it without parameters +* changed: -s/--stdin option on daemon command-line now implies --norcon +* changed: Merged plyr action and plyrplyr action tables on playerinfo and claninfo. +* changed: Printed version number is now stored in the db, with separate dbversion stored for future update scripts +* changed: Clarified names of server sections in admin page +* changed: Set default PlayerEventsCommandOSD to '' for hl2mp and bg2 +* bugfix: Fixed clicking plyrplyr actions on actions page not working (octo) +* bugfix: Fixed stun award defaulting to plyr when action defaulted to plyrplyr (thx to octo for catching) +* bugfix: Fixed regression in 1.5.3 causing owner_ events not firing correctly +* bugfix: Fixed a couple GE:S weapon logging bugs (thx Dr. No) +* bugfix: Added trimming of logged rcon and admin commands to fit in the 255 character limit in db +* bugfix: multple rcon commands sent at once, delimited with semicolons are now logged as separate commands if rcon logging is enabled +* bugfix: Fixed some html errors +* bugfix: Fixed command line options not being read before connecting to db (reported by allstats.de) +* bugfix: Fixed new new instance of geoip object being created for every lookup in daemon (reported by allstats.de) +* bugfix: Fixed display of rank output when ShowMenu (msay) is not supported +* bugfix: Fixed error in game duplication tool where new code or name contained special db characters like ' +* bugfix: Connect announce message for new players now just says "New Player X connected (country)" instead of saying Pos with 1000 points (no message for player connect if new player and country not detected) +* bugfix: Reworded Connect Announce message for players with hidden rank. +* bugfix: Fixed character encoding on teamspeak server list (ServerAlex) +* bugfix: Player chat history now labels team chat as such +* bugfix: Fixed some potential "Data too long" sql errors in chat and player histories +* bugfix: Fixed bug on some systems if hlstats was not in root webdir (Woody) +* bugfix: Fixed player and victim being reversed on steal_sandwich action (octo) +* bugfix: Fixed multiple l4d actions +* bugfix: Fixed roles links not being click-able in some browsers. +* bugfix: Fixed all games not showing up in search criteria box on search page. +* bugfix: Removed unused icq field from edit player details screen +* bugfix: Fixed intermittent issue with wrong ribbon image showing for some ribbons on playerinfo (octo) +* bugfix: Informational messages (not-min-players message, connect announce (if on), round end disabled (if applicable) are now sent even if EnablePublicCommands is off. +* bugfix: PublicCommand messages (like place) are no longer shown on mods other than sm & amxx when EnablePublicCommands is off +* bugfix: Connect Announce messages are no longer sent to the person connecting as they would not have connect in time to see it in most cases anyway +* bugfix: Changed default DisplayResultsInBrowser value to 0 for GE:S and INSMOD as they don't support it currently +* bugfix: Fixed norcon option being ignored when not using stdin +* bugfix: Fixed some server graph-related issues (octo) +* bugfix: Re-added missing EUROPE option from map regions +* bugfix: fixed misc bugs with source rcon logging +* bugfix: revised weapons for FOF, including adding new Winchester Shotgun + + +HLstatsX Community Edition 1.5.3 +Mar 1, 2009 +* added: Added hlx_sm_bulkpsay command to reduce rcon messages sent during team actions and global announcements +* added: Added support for colored global announcements (only on games that support coloring on sm & amxx) +* added: Added optional support for announcing rank, points (or kills if ranked by kills), and country upon player connect +* added: new tf2 weapon codes +* added: Added roles support for GE:S +* added: Added weapon_logging support for GE:S beta 3.1+ +* added: Added headshot tracking and action for ge:s and tfc +* added: Added DefaultDisplayEvents option to control default option for players' DisplayEvents option +* added: Added first_blood (player), steal_sandvich plyr or plyrplyr), and stun (plyr or plyrplyr) actions for tf2 +* added: New insurgency forum sig image (R3M) +* added: New css forum sig image (Unity) +* added: New "bulkpsay" command in sm and amxx plugins to send psays eight at one time to minimize rcon during team rewards and global messages +* added: Added basic logout link for logged in admins +* added: dods daily awards images (GTFO.G, dannyowan) +* added: dustbowl capture actions for tfc +* added: dbversion parameter to hlstats_Options for future use +* changed: edited more db column widths for consistency, added an index on playerId in EventEntries +* changed: Awards are now sorted by name +* changed: messages said with say_team are now marked with "(Team)" on chat page +* changed: Made auto-updating of server name from hostname cvar optional +* changed: Removed constant "$playerstring is XXXs idle" from daemon console/log +* changed: Chat triggered that use public commands now use bulkpsay if available +* changed: Clarified daemon restart notice on HLstatsX:CE settings page +* changed: Changed dropdowns on award setup pages to use name/description instead of code +* changed: Games in admin panel are now in alphabetical order +* bugfix: adjusted alignment on pages missed in 1.5.2's adjustments (HO!NO!) +* bugfix: Fixed mysql_enable_utf8 not being set on the db connection after a reconnect +* bugfix: Added better method of protecting against infected TK in l4d +* bugfix: Fixed https detection bug with IIS +* bugfix: Fixed duplicate display of some options in HLstatsX:CE Settings page +* bugfix: Fixed "Admins" not working with full steamid since 1.5.1 +* bugfix: Fixed potential issue with player location lookup +* bugfix: Fixed dods headshot action occuring for wrong player or not occuring +* bugfix: Fixed skill being able to go negative, causing errors +* bugfix: Special awardcode (mostkills, bonuspoints, etc.) can now be set up in the admin panel +* bugfix: Fixed misspelling of ribbon/award for placed sappers (thx rrtaft for catching this) +* bugfix: Fixed inaccuracy on some l4d kill messages +* bugfix: Fixed kills and ppactions not being recognized if victim had a " in name +* bugfix: Fixed event properties not being read correctly if a property value had a " in the name +* bugfix: Fixed some killedobject events not firing correctly since 1.5.2 + +HLstatsX Community Edition 1.5.2 +Feb 22, 2009 +* added: game logo for sig.php, game are (aoc, ges, fof, and thx to heimer for is zps contribution) (HO!NO!) +* added: some missing cstrike awards and ribbons +* added: more cstrike award an ribbons images (Unity) +* changed: When showing daily award list on main game page, award names now link to dailyawardsinfo +* changed: Action names and role names in playerinfo and claninfo now link to the respective info pages +* changed: trailing slashes in HLStatsURL are now automatically removed +* bugfix: Fixed "plain_userid" typo +* bugfix: Fixed "Fixed "String found where operator expected at (eval 18) line 1, near "$ = '1'" (Missing operator before '1'?)" message appearing" message showing in daemon log +* bugfix: Fixed searching with full steamid no longer working on search page since 1.5.1 +* bugfix: Fixed events not tracking for players with " marks in their names +* bugfix: Fixed rcon logging for source games (and unrecognized message on rcon) +* bugfix: Fixed RconIgnoreSelf option. Will only work if rcon shows as from BindIp +* bugfix: Fixed special awards not necessarily being award to players of the correct game +* bugfix: Fixed owner_ events not firing in tf2 after switching internal steamid format in 1.5.1 +* bugfix: Fixed query in search not being fully escaped before being sent to the db +* bugfix: Fixed ip lookup happening even if no ip was found for player +* bugfix: Fixed bot steamid display in playerinfo +* bugfix: Fixed bug in hlstats-awards.pl requiring Geo::IP::PurePerl perl module to be installed even if using database lookup method for geolocation since 1.5.1 +* bugfix: Fixed a couple mostly-cosmetic typos in server setting explanations +* bugfix: Fixed some lingering alignment issues on some of the pages (HO!NO!) +* bugfix: Fixed some bugs in the rewardTeam sub causing extra rcon to happen during team actions (thx to octo for finding this) +* bugfix: Fixed infected teamkills happened in l4d +* bugfix: Removed victim kill msg in l4d and removed mention of victim from killer's kill msg +* bugfix: Put in some safeguards so if a player's ip cannot be found, the daemon only tries to find it once every 2 minutes +* bugfix: Fixed a table coloring issue in countryclansinfo +* bugfix: crit_kill action will no longer occur in tf2 on suicides + +HLstatsX Community Edition 1.5.1 +Feb 18, 2009 +* added: New TF2 award and ribbon images +* added: Better roles tracking for l4d +* added: script to convert from HLStats 1.40 to HLstatsX:CE 1.5.1 +* added: chat coloring for ZPS in ingame plugin (heimer) +* changed: STEAM_X: is now ignored on steamids. Included script to convert all stored ids to new format +* changed: Hardcoded event table names on reset pages to prevent accidental table deletion (in case someone has a non-core-hlx table with similar name in their db) +* changed: removed dailystats.php (no longer used) +* changed: removed PLATFORM constant from config.php (no longer needed) +* changed: Optimized more column length and indexing in db +* changed: Daemon will now only delete events older than DELETEDAYS if DELETEDAYS >= 0 +* bugfix: Optimized last event query in playerinfo_general +* bugfix: Fixed intermittent 100% cpu bug in TRcon (for real this time) +* bugfix: Clarified some text on teamspeak server setup page +* bugfix: Fixed issue with repeated team awarded on pointcaptured action +* bugfix: GeoIP lookup in hlstats-awards now sets country name +* bugfix: GeoIP lookup in hlstats-awards is now able to lookup using binary version of GeoIP data +* bugfix: Fixed utf-8 encoding issues with region names from GeoIP binary data +* bugfix: copied all recent changes and fixes from playerinfo_weapons to claninfo_weapons (HO!NO!) +* bugfix: Fixed kdeath and kpd commands not functioning properly + +HLstatsX Community Edition 1.5 +Feb 15, 2009 +* added: Support for Left 4 Dead, Fistful of Frags, and GoldenEye: Source +* added: Weapon logging with game-specific hitbox models for Left 4 Dead, Fistful of Frags, and Insurgency (hitgroups only) +* added: ingame plugin now generates crit_kill, force_suicide, hit_by_train, and drowned actions for tf2 +* added: many performance improvements on web pages +* added: new hlx:ce favicon (D3vilF1sh) +* added: support for using Maxmind's binary format GeoIP data (huge speed increase and less resources used on player loc lookup). Geo::IP::PurePerl module required +* added: support for awards for playerplayer actions (type P) and victims of playerplayers actions (type V) +* added: full set of ribbons for cstrike (Unity) +* added: new winchester weapon in ZPS +* added: many new actions for FF now present as of FF 2.2 +* added: admin logging support for sourcemod (requires sourcemod to be logging to game log instead of separate sm log) +* added: daily awards can now be run for a specified day (octo) +* added: Added some missing insmod awards images and added higher quality versions of rank images. (R3M) +* added: destroying own building (sentry, teleporter, etc.) longer than two minutes after it has been created does not fire the owner_killed or dismantle action (no loss of points) in tf, tfc, and ff (idea from octo) +* added: new page, dailyawardinfo, to show win history of an indiv award +* added: steam avatar image override (U#0) +* added: Broader forum support for forum sig image by adding modrewrite option (idea from MadMakz and R3M) +* added: tf2 flag_dropped_death event so that there is distinction between a player dropping the flag manually and dropping it because he died. (octo) +* added: sortorder field for server list (order is now by sortorder, name, serverid) +* added: support for automatically marking users seen kicked by steambans as cheaters. Other very minor optimizations +* added: special zps skill calc mode +* changed: Redid TF2 actions so that all action info is in actioncode now (eliminating object and event fields and enabling more possibilities for awards) +* changed: Headshot are now done in daemon if kill line has headshot property. Removed manual "triggered" line added to logs from plugin +* changed: Removed AdminContact broadcast because some user did not want and almost all are already running an admin mod with the capability; no need to duplicate. +* changed: dailystats.php is now obsolete. all functionality of it has been merged into hlstats-awards.pl +* changed: forum sig now uses realgame rather than gamecode to detect what game image to use +* changed: gliding server tabs on game page is now optional (otherwise old static display is used) +* changed: made seperate "(Join)" link for steam:// links. (whole server address was too easy to accidentally click) +* changed: mp_logdetail 3 no longer required for DODS to track headshots +* changed: Replaced many large country flag images with smaller (filesize) images(R3M) +* changed: Removed unused hlstats_server_addons table and corresponding admin page +* changed: removed class change logging for FF in ingame plugin. Added parsing for FF-style change role log lines +* changed: Global awards now use same method to show as Daily awards on awards page (so now they show even if no one has been awarded them yet.) +* changed: Default DNS resolve timeout changed from 5 to 3 for new installs +* changed: Replaced included font for forum sigs with open source DejaVuSans.ttf (http://dejavu-fonts.org) +* changed: added optimizations to database and sourcemod plugins +* changed: Improved freetype detection in sig.php +* changed: Player actions with team points listed will now reward the team of the player committing the action if team is left blank (instead of issuing no award) +* changed: Reworked image directory structure to be more modular per game +* changed: Merged hlstats_Options and hlstats_PerlSettings (and thus hlstats_Options_Choices and hlstats_PerlSettings_Choices as well as the two related configuration pages in the admin panel) since many options are shared between the daemon and the web interface. +* changed: Minactivity in config.php is now ignored. Both web interface and daemon will use minactivity value specified in Options page. +* changed: Some more admin pages, such as resets, now require full admin permission instead of only restricted admin access +* changed: Removed entries/joins column from Maps page to increase performance +* bugfix: the hlstatsx:ce daemon now works properly on Perl 5.10 +* bugfix: eliminated some more unneeded rcon status "spam" +* bugfix: Roles link in header will no longer show if game has roles but all are hidden +* bugfix: Fixed bonusround ignore message showing twice in some cases +* bugfix: kill streak now resets on round end +* bugfix: Duplicate game page in admin panel now copies 'realgame' settings and maps image folder +* bugfix: Removed more remnants of old imgpath option +* bugfix: Fixed ingame load command not necessarily showing graph for correct server +* bugfix: ingame messages no longer show surround by quotation marks when use beetlesmod or mani admin on source ep1 games +* bugfix: steam avatar now shows steam community default "?" avatar instead of broken image if image cannot be found (bot or human with no profile set up) +* bugfix: accuracy no longer mentioned on rank output in games that do not support accuracy tracking +* bugfix: many tables were not obeying table_border formatting in the stylesheet +* bugfix: fixed some cases of team round win not being picked up if BonusRoundIgnore was on +* bugfix: AoC headshot detection is now more accurate +* bugfix: Fixed point calculation bug that occurred when using skillmode 0 with XYZ saynt's calculation method +* bugfix: Fixed and weapon and team typo for insmod (R3M) +* bugfix: Fixed typo in teams admin page causing changes not to be saved +* bugfix: Round over message (on bonusroundignore) now only shows if minplayers is met +* bugfix: In some configurations, daemon recognized all data from game server as control commands +* bugfix: Roles marked as hidden are now hidden from the Roles page (U#0) +* bugfix: Top of Chat page is now consistent with other pages (U#0) +* bugfix: Fixed MALFORMED DATA message that some people were receiving (HO!NO!) +* bugfix: other various formatting issues on web +* bugfix: Users not on a defined team now show in livestats view (spectators no longer need to be defined and now people on no team or "Unassigned" will show) +* bugfix: some tables in playerinfo and claninfo were not sorting properly +* bugfix: Fixed image alignment in php pages +* bugfix: Fixed award winners copying over when duplcating game settings +* bugfix: Fixed roles names not showing in playerinfo and claninfo +* bugfix: hidden players and cheaters now cannot obtain any of the special code awards (latency, bonuspoints, etc.) +* bugfix: Players with "last_event" in the future are now included in ranking in daemon rank-related calculations (octo) +* bugfix: Daemon no longer tries to resolve ip address unless running in "Normal" mode and player is not a bot +* bugfix: Fixed bug where daemon created kill_streak_0 and kill_streak_1 actions upon player suicide +* bugfix: Fixed "Back to..." links on ribboninfo and rankinfo +* bugfix: Fixed hitbox flash not narrowing down to indiv weapon stats when clicked +* bugfix: Clan tag patterns no longer save html entities (<< was showing << and not matching) +* bugfix: SET NAMES 'UTF-8' was not being run after database reconnect in daemon +* bugfix: Fixed typo in playerinfo_general.php causing favorite weapon link to not work +* bugfix: Removed double slash occurring in forum image link on some systems +* bugfix: When rankingtype is set to kills, ingame commands (such as next, top10, rank, place, etc.) should now reflect same (correct) ranking as web interface +* bugfix: Fixed playerinfo ranking to be more consistent with player list and sig ranks +* bugfix: Fixed wrong award image being shown in some cases on playerinfo if player earned multiple levels of an award + + +HLstatsX Community Edition 1.4.1 +Dec 17, 2008 +* added: Added notice to perlsettings.php and serversettings.php to state that perl daemon must be restarted for settings to take effect +* added: Reset DB Collations page in admin panel now has option to print generated sql commands to screen instead of directly running them +* added: Added ability to do partial reset per game instead of only for all +* changed: New Server page in admin panel is now called "Quick-add Server" +* bugfix: Removed remaining mention of freetypeenabled option in sig.php +* bugfix: Flash hitbox game detection now uses realgame field +* bugfix: Forum signature image is now compatible with more forum software +* bugfix: removed remaining mentions of obsolete imgpath option replacing with IMAGE_PATH +* bugfix: Fixed numerous bugs and omissions in Game Duplication tool +* bugfix: removed blocking of "gstats" and "global_stats" in chat in SM plugin +* bugfix: removed fix for catching chat commands in zps in SM plugin(no longer needed and causes problems on latest zps version) +* bugfix: hlx_sm_team_action command now prints correct command name in usage parameters + +HLstatsX Community Edition v1.4 +Dec 8, 2008 + * added: support for CS 1.6 (cstrike), TFC, DOD, and NS (psychonic, with help from the HLStats project) + * added: amxx plugins for cstrike, tfc, dod, and ns, based on amxx plugins for hlx premium by TTS Oetzel & Goerz GmbH. Changed newline character in plugins to "\n" to match sourcemod plugin. (psychonic) + * added: support to the daemon to send psay/msay/browse/swap commands in the form the amxx plugins expect (psychonic). + * added: game, map, and weapon images for cstrike, tfc, dod, and ns. game images using vaksa's valve icon images. maps and weapon images from HLstats, used with permission from banana. + * added: header to make ingame pages no longer cached by the steam browser (psychonic) + * added: more map images for The Hidden (Cheesey) + * added: Round_Win event for AoC. the sourcemod plugin supported the event since 1.3 (psychonic) + * added: game icon for aoc (pirate555) + * added: game icon for zombie panic (WaywerdWolf : Wiff23@msn.com) + * added: award and weapon icons for zombie panic (aveneyer : aveneyer@gmail.com) + * added: large flag display on countryclansinfo.inc (R3M) + * added: ability to set team color/bgcolor (for livestats) in admin panel. livestats now also uses this value for link color rather than having to manually have every team listed in hlstats.css (psychonic) + * added: new AJAX functionality on game, playerinfo, claninfo, and awards pages ([iO]crayolacrayon) + * added: tooltip on rank history, ribbon, and global award images to show name + * added: a2s_info querying (octo) + * added: ability to parse output of HL1-style rcon status command (psychonic) + * added: Insurgency awards and ribbons icons (insurgency.pl) + * added: rank progress bar to playerinfo, similar to hlx premium (psychonic) + * added: missing latency awards for ff, hidden, aoc, & zps (psychonic) + * added: new title, background, and tab images for header (slaintrax) + * added: total count of currently playing players per game on contents page (warbucks) + * added: new map type for google maps, physical (psychonic) + * added: enhancements to web, perl, and server settings pages to show dropdown boxes with possible values where applicable (psychonic) + * added: Teamspeak viewer from ELstatsNEO 2.44, adapted to NEO by hellraiser, further adapted to HLX:CE by psychonic + * added: alternative GeoIP import script (*XYZ*SaYnt) + * added: two more ranking views "last week" and "last month" (hellraiser) + * added: server activity overview to playerinfo (hellraiser) + * added: support for more daily/global award types: suicides, kills, teamkills, bonuspoints (urmel, ELstatsNEO) + * added: added new css ribbons from ELstatsNEO 2.44 (hellraiser) + * added: new option to cap max gained points by ratio of killer to victim points (*XYZ*SaYnt, psychonic) + * added: additional security to web files ([iO]crayolacrayon) + * added: player and server blips on map are now one per location instead of one per person/server, allowing multple people/servers to be listed on one instead of hidden behind each other if they are at same location ([iO]crayolacrayon) + * added: player map on claninfo page ([iO]crayolacrayon) + * added: support for PlayerPlayerActions for actions that are logged in this format (victim loses points player gained ex: domination, sentry destruction) (psychonic) + * added: upgrade script to go from ELstatsNEO 2.45 to HLX:CE 1.2 (still need to then upgrade from 1.2 - 1.3 and 1.3 - 1.4). Will do proper NEO - 1.5 in next release (psychonic) + * changed: directory and file structure of web has changed greatly ([iO]crayolacrayon) + * changed: sourcemod plugin, in AoC, now prints same weapon name to log as would be natively logged by the game. upgrade script will fix existing and future entries (psychonic) + * changed: in run_ scripts, replaced ":" in log file names with "-" to avoid some issues when trying to download logs with certain Windows ftp clients (psychonic) + * changed: Removed nav-countryclans option and renamed show_flags to countrydata. (psychonic) + * changed: status.php and sig.php now default to countrydata setting for showing flags instead of defaulting to showing flags (can be overridden with query string parameter) (psychonic) + * changed: flags on awards page are only shown if countrydata is set to 1. (psychonic) + * changed: global awards are automatic from specified daily awards now, also allowing global awards for actions and not just weapons. hlstats_Awards_Global table is no longer used or needed. Entries in hlstats_Ribbons where special=3 are no longer used or needed. Global and daily award images now go in gawards and dawards directories respectively in game directory with filename (type O/W)_(code).png (psychonic) + * changed: if missing image for award or ribbon, defaults to gold medallion image rather than broken image link. (psychonic) + * changed: reformatted map popups a bit and added connect link to server popup. (psychonic) + * changed: hlstats-awards.pl does not give error anymore when attempting to insert award into player award history if ran more than once in one day. (psychonic) + * changed: a2s_info query now used to retrieve hostname/maxplayers/currentmap rather than parsing output of rcon status command (psychonic) + * changed: BASTARDrcon, a merge of TRcon subs with KKrcon connection for use with HL1 rcon (psychonic) + * changed: ranks are now restructured using similar structure to stats on insurgency.pl. added new rank icons. (insurgency.pl) + * changed: games marked hidden are now hidden from Game Settings menu (psychonic)\ + * changed: game detection removed in favor of setting actual game for each created "game" in admin panel (psychonic) + * changed: made map text on graph darker (psychonic) + * changed: removed player commend steamid and broadcast command steamid options since all games require the userid. (psychonic) + * changed: removed freetypeinstalled option from web settings in favor of checking of existence of imagegetfbbox function (octo) + * changed: moved google maps options from config.php to web settings in admin panel (psychonic) + * changed: removed MODE settings from config.php. now uses setting already set in perl settings. + * changed: year and month graphs now have much higher refresh interval as they do not change nearly as often as other graphs ([iO]crayolacrayon) + * changed: country dropdown on edit_player_info now shows country names of all countries in db rather than two-letter country code from list of country image files. (psychonic) + * bugfix: perl daemon now correctly encodes utf8 characters (from a post by Procyon @ lart's forum) + * bugfix: roles names were not showing on playerinfo or claninfo (psychonic) + * bugfix: flags on daily awards on game.inc were not showing (psychonic) + * bugfix: server link on server map popup was incomplete (R3M) + * bugfix: potential display issue with roles and weapons pages if role or weapon code was not all lowercase (psychonic) + * bugfix: ff headshots were not processed correctly if logs were received from stdin and database game name was not "ff" (ex. ff2, ff3) (psychonic) + * bugfix: found another spot in perl where existing bad utf8 data could cause crash when inserting clans (psychonic) + * bugfix: server connect links on map server popup and status.php now use public address if public address is not blank. (VieuxEd) + * bugfix: Insurgency team codes were incorrect (crawler163) + * bugfix: server location lookup now uses publicaddress (if not blank) for location lookup (psychonic) + * bugfix: U.K. flag was not showing up (psychonic) + * bugfix: google maps started enforcing use of newer api causing maps not to show correctly (R3M) + * bugfix: minor formatting issues in some playerinfo tables (psychonic) + * bugfix: ampersands and other special characters were being html encoded in rcon passwords and other select fields when they should not have been. (psychonic) + * bugfix: load graph had display error if not populated with enough data to fill graph (psychonic) + * bugfix: roles totals on playerinfo were inaccurate ([iO]crayolacrayon) + * bugfix: ranks were not saving in admin panel (psychonic) + * bugfix: tables in claninfo were not aligned properly (warbucks) + * bugfix: bonus round not being ignored if BonusRoundIgnore was set to 1, but BonusRoundIgnoreTime was 0 (psychonic) + * bugfix: help page now obeys styles (psychonic) + * bugfix: requirement of "http://" in siteurl in web options was ambiguous. can use with or without now (GTFO.G) + * bugfix: from ELstatsNEO 2.43 "thanks to 'trawa': fixed more " bgcolor and text color now definable in the teams admin settings + * added ability to hide clans from the clan stats (eg. if you would hide [ger] (ger) or some other country tags from clan list) + * switched perl script servers config to the database, now configurable via web interface! + -> hlstats.conf -> database import script included in the scripts/perl folder + ! Updated hlstats.pl to read the servers[] portion from the database instead from hlstats.conf + * switched perl script general config to the database, now configurable via web interface! + ! Updated hlstats.pl to read the moved parameters from the database instead from hlstats.conf + * added some useful functions to the perl ans servers[] settings in the admin interface: + -> reset to default, copy settings from other server, onmouseover-help window for each parameter! + * changed players list design: column order now switches with the selected rankingtype in options + * added .htaccess files to include folders to prevent web access - so you can place them within the webroot folder if you wish (requires "user .htaccess override" enabled in apache webserver) + * optimized tsgk_swear_words calculation query in playerinfo_general.inc (lesser server cpu load when having a lot of swear words) + * added swear-wordlist editor to admin interface + * modified font tags to span classes controlled via css file -> saves a LOT of html output garbage + (test profile playerinfo shrinked from 720kb to 202kb plain html code (72% less output) without any design changes) + * switched most database based style presets to cascading stylesheets - you still can choose the used style from the admin interface + * modify ALL scripts to use the Stylesheet classes instead the old database color codes - WHAT A MESS + * added tsgk black stylesheet + * created new perl start script "run_hlstats_multi" - now you can run multiple processes without duplicating the whole perl folder with different hlstats.conf files + * changed masterserver address/port to "master.elstatsneo.de:27801" (created own masterserver with actual hlstatsx cheater steamid's dump and GeoCityDB) + * changed statsserver address/port to "stats.elstatsneo.de:27802" (created own statsserver providing a powerful global stats on www.elstatsneo.de in the future!) + +ELstatsNEO - RELEASE v2.20 +11. May 2008 + * bugfix: ribbon admin module didn't work after last update + * bugfix: on some php installations the math functions are missing - coded a fix to playerinfo's steam community link + so the script dont crashes when functions are missing + * bugfix: dailyawards didn't build correctly when two gametypes shared same weapon code (eg. pistol on hl2mp and tf2) + * bugfix: corrected "contents.inc" -> top player & clan calculated now like options setting (kill/points rate setting) + * changed default "buddystats" to off since it consumes MUCH resources when >10k players in database + * added hits & visits counter (configurable via admin interface) + * split the rank definitions to each game, so you can define different kill count between eg. CSS and HL2MP + * added country names table to database + * re-added global chat link (hideable via admin options) + * created "CountryClans" overview - Players grouped by country (hideable via admin options) + * Added admin function: "Duplicate a whole game settings tree to split servers of same gametype" + * Added admin function: "Remove a complete game settings tree from the database" + * Added Support for OpenSource GeoLite-City database + (automatic downloader&database import script included in scripts/GeoLiteCity) + * dailystats.php now tries to update every player who don't have his country set against the GeoLite Database + * css: added all weapons, daily awards and daw images + * css: added all global awards and global awards images + * css: added all ribbons and silver ribbon images (default 5 awards needed) + * tf2: added all daily awards and daw images + * tf2: added all global awards and global awards images + * tf2: added all ribbons and silver ribbon images (default 5 awards needed) + +ELstatsNEO - UPDATE v2.10 +05. May 2008 + * bugfix: ribboninfo.inc -> ribbon image wasn't displayed + * bugfix: playerinfo.inc -> hl2mp ribbons linebreak didn't work sometimes + * bugfix: admin option "score ranking" will now work :) + * speedup: all ribbon related stuff + * speedup: global awards + * extended sig.php to easily set the ttf font in the admin options (no more font hardcoding required) + * added some ttf fonts to the package (webroot/fonts) + * joined both ribbons tables to single "hlstats_Ribbons" for better administration + * now done fixed ribbons sort order in playerinfo + * added trend graph background file (admin options parameter) + * added player buddy list to playerinfo (played at least x minutes together) + * split playerinfo.inc into several files for better development handling + * reworked playerinfo: added tabs to improve display & navigation (tabs can be disabled in the elstatsneo options) + +ELstatsNEO - FIRST RELEASE v2.00 +02. May 2008 + * final packaged release - no longer a HLstatsX patch + * added steam community link in playerinfo + * added forum signature in playerinfo + * added new admin task: partial score reset + * moved teamspeak server display to the contents page instead of the header.inc + * added favourite weapon in playerinfo + * added kills per minute in playerinfo + * added electro1337 style preset + * added clanlogo modification (by TSGK/Osiris) + * modified claninfo details (by el/gringo) + * added new config setting: rankingtype -> rank by score or by kills + * modified scripts for alternate ranking types + * added tsgk|osiris weapon ribbons for hl2mp (plain, bronze, silver, supreme image sets) + * added 'rank' informations on the page + * added complete new 'ribbons' overview with details to sections + * added global awards system to sections and playerinfo + * added player Trend Graph to playerinfo + * added "swear-o-meter" to the playerinfo (add badwords manually to the table "tsgk_swear_words" + +--------------------------------------------------------------------------------------------------------------------------------------------- +HLstatsX 1.20 RC2 PATCHES by Malte Bayer +--------------------------------------------------------------------------------------------------------------------------------------------- + +HLstatsX 1.20 RC2 ___PATCH___ v1.14 + * modified hitbox embed to support the new models + * hlstatsx bugfix: hitbox weapon select (error in javascript translation) + * added two new hitbox models (alyx for "hl2mp" and pyro for "tf" games) + * add / edit hl2mp ribbons + * added script run-time stats in footer + * added global server chat log + * added hpk, kpd and skill change graphic to player sessions + * made activity sortable + +HLstatsX 1.20 RC2 ___PATCH___ v1.12 + * modified admintask "edit player details" to let admins set the country flag by hand + * corrected improper working medal system + * cleaned up playerinfo + * added kill counter to awards (note: the hlstats-awards.pl script has changed!) + * extended player awards history + + +HLstatsX 1.20 RC2 ___PATCH___ v1.11 + * added new tables & admin forms for new Ribbons system + * added admin form to manage Ranks + * added ribbons (award based) for "Counter-Strike Source" + * added ribbons (award based) for "Half-Life 2 Deathmatch" + * added ribbons (award based) for "Team Fortress 2" + * added logo for GameType "tf" + * extended actions table to support "tf" special events (object / event) + * modified PERL parser scripts to support "tf" special events (object / event) + * added support for sniper "headshot" + * extended admin task "reset stats" to choose which games to be reset + * added teamspeak status viewer + * added new table & admin form: teamspeak + +HLstatsX 1.20 RC2 ___PATCH___ v1.10 + * fix: Player Awards history + * added new admin task: "Clean up Statistics" (Delete all inactive players, clans from the database) + * added ranks (kill based) + * added global rank statistics overview + * SQL file: Updated Settings for the Game "Team Fortress 2" (added more awards) + +HLstatsX 1.20 RC2 ___PATCH___ v1.00 + * added Player session history + * added Player Awards history + * SQL file: Settings & Weapon Images for the Game "Team Fortress 2" + +--------------------------------------------------------------------------------------------------------------------------------------------- +HLstatsX Changelog below +--------------------------------------------------------------------------------------------------------------------------------------------- + + +18. June 2007 [V1.20 RC2] +--------------------------------------------------------------------------------------------------------------------------------------------- ++ Integration of the SourceMod plugin. ++ Added player action "headshot" raised by the SourceMod plugin (see upgrade_v120RC2.sql in the upgrade directory). ++ Added compatibility for the MetaMod: Source plugin "MiniStats" to display HLstatsX events ingame. +- Reworked all webpages for higher security against exploits. +- New coloring of the ingame messages with the SourceMod plugin. +Fixed: Game detection is not working correctly. +Fixed: All unicode characters are now allowed on player search and within the admin interface. + + +22. May 2007 [V1.20 RC1] +--------------------------------------------------------------------------------------------------------------------------------------------- ++ Integrated support for SourceMod. ++ Support for the new GoogleMap api version 2. ++ Option to define the minimum kills a player must have before taken into full account. ++ Full Support of native DoD:s events. ++ Integrated the two new css models artic and gsg9 into the flash movie (by Sandman). ++ Public Commands can now be disabled. +- Rewrite of the browser commands for SourceMod and Mani v1.2. +- The victims points change is now based on killer skill changes (if SkillMode > 0). +- Victims now see how many points they lost in the broadcast messages. +- Changed HLstatsX link in the header to work correctly with more then one game. +- Server name is now restricted to 255 characters (64 characters before). +- Included security fixes for the webpage. +- Removed Raw Sockets variable from standard configuration since it is not needed. +- Updated configuration file including SourceMod default configuration. +Fixed: Crash and no country data for name tracking mode. +Fixed: Break on date/time display on smaller resolutions. +Fixed: Announce command ma_hlx_csay is not working (missing target). +Fixed: False team-kills are recognized rarely. +Fixed: Special characters in the search are not allowed. +Fixed: "Best latency" award only in css. +Fixed: Error in display long server names in player history. + + +31. Mar 2006 [V1.01] +--------------------------------------------------------------------------------------------------------------------------------------------- ++ Global realtime player tracking included. ++ Name and IP-Tracking Mode now full working e.g. for LAN sessions. ++ Option to log chat history for each player (additional option to exclude admins). ++ Global chat for players to communicate between gameservers. ++ Smoother graphics in serverload graphs. ++ Integrated new models into the flash movie (by Sandman). ++ New option "SkillMinChange". Specifies the minimum number of skill points a player will + gain for each frag. (Default is 2 skill points minimum change). ++ Integrated action events of VIP-Mod from LDuke. ++ Added dods images (weapons, maps) and de_nuke and cs_militia images for css (by Michael_Jim). ++ Kick reason if player want to connect on rank limited servers. ++ Added dods integration on webpages and dods events in sql file from various plugins. ++ New claninfo and action ingame webpages. ++ Support for CSS Deatmatch gametype. ++ Added commands for rank, session, kpd which results are displayed to all players. +- Updated function to display names on google map properly. +- Reworked ingame html files. Now all links to players and clans will work correctly. +- Safer way handle internal player array. +- Player pings are retrieved within a greater interval. +- Server status script displays noimage picture if map image is not available. +- Inactive clan members will no more displayed in detailled clan view. +- Reworked ingame AMX style menus. +- Broadcast public address to the masterserver if setup in the webinterface. +- Server graphics now displaying much smoother. +- Updated actions for Day of Defeat: Source to work with all plugins. +- Updated hlstats.sql file with larger varchar database entries. +- Default maximum skill change is now 25 points. +Fixed: Webpage Vulnerability. +Fixed: Socket functions for synchronization countries and vac banlist not working for + windows operating systems (Thx crazydew). +Fixed: Deathmatch awards not tracking the .357 magnum and wrong weapon image. +Fixed: Bot tracking on name changes. +Fixed: Statistics are displayed to other players after gameserver crash. +Fixed: Sometimes too much entries were displayed on next command. +Fixed: Smaller bugs. + + +29. Oct 2005 [V1.00 Final] +--------------------------------------------------------------------------------------------------------------------------------------------- ++ Server html page which displays the serverload on different ranges. ++ Game Images for source mods (thx to Vasili Vorotnikov). ++ Support for Day of Defeat: Source. ++ Added option that admins will not be switched from auto team-balance (SwitchAdmins in hlstats.conf). ++ Flash animation now displays different models on css games (by Sandman). +- Checking maxplayers and map every 10 minutes. +- Player connection time is now tracked on importing log files. +- Status and signature script now display flags as standard value. +- Not displaying contact if contact field is empty. +- Dynamic dns resolution of master server address (New parameter: --masterserver, to set ip address manually). +- Updated server overview page. +- Better support for non css games (status.php). +- Updated queries on masterserver communication. +Fixed: Search on "all" games and rewritten html formular part. +Fixed: Some crashes with function track_server_load. +Fixed: Server load graph for all servers showing wrong max_slots. +Fixed: Displaying bots on day ranking when ignoring bots is enabled. +Fixed: Global banning may end in an infinite loop on very rare situations (Thx DOH!Scorpion). +Fixed: Correct playercount on importing logs (Performance gain ~500%). +Fixed: Not displaying osd menu with kpd command if accuracy is 0.00. +Fixed: TRcon.pm crashing with negative strlen error. +Fixed: Minor bugs. + +22. Aug 2005 [RC10] +--------------------------------------------------------------------------------------------------------------------------------------------- ++ Added forum signature script with 10 integrated backgrounds (Thx Michael_Jim). ++ Added favicon.ico (Thx Michael_Jim). ++ New predefined regions for Google Map. Map view type is now configurable. +- Updated ctf support file (MySQL/Weapon images). +- Reworked importing old logs. +- Improved mutiple game tracking. +- Creating upgrade directory for all MySQL files which only for upgrading from older versions. +- Renamed "set" command to "hlx_set" and "hideranking" command to "hlx_hideranking". +- Improved query of clan rankings page. +- Server address in overview now link to hlsw client. +Fixed: History is not calculated after day change while playing. +Fixed: Perl daemon crashing if gameserver is restarted. +Fixed: Infinite loop while rcon password was wrong. +Fixed: Wrong percentages in team statistics. +Fixed: Javascript error on startpage if mutiple games are installed. +Fixed: Flash hitbox displayed wrong arm and leg sides. +Fixed: Smaller bugs. + + +28. July 2005 [RC9] +--------------------------------------------------------------------------------------------------------------------------------------------- ++ Integration of bettlesmod. HLstatsX development in future will mainly focus on this plugin! ++ Extended functionally to get the players country and city from the masterserver. ++ Integrated google world map on webpage. ++ Top10-Players display as amx style menu. ++ Introducing "/next" command to display amx style menu with the players ranked ahead. ++ Tracking players total connection time and day performances. ++ Setting mode of "players loosing points" for getting killed (Server[]-array variable "SkillMode"). ++ Adding Halflife 2 capture the flag support (game_ctf.sql). +- Webpage title show now the "sitename" instead of HLstatsX. +- Reworked ingame webpages with small fonts. +- Now needed 3 players for getting ranked in clan ranking. +- Just one awards date to avoid confusions. +- If servers got > 5 minutes no events, map is detected again. +- Changing weapon and action logging. Webpages are much more faster now. +- Team balancing now working also with bots. +- Touched a hostage event is set to "0" points (game_css.sql). +Fixed: X/0 slot display. +Fixed: Rank on session start (-1). New players see "N/A" until the got a rank (=> 1 kill). +Fixed: Team-Killing message sometimes not showing new killer skill. +Fixed: Link color on displaying the TopX-Players (status.php). +Fixed: Switching the hitbox players sides (hitbox.swf). +Fixed: Styles working correctly on all ingame webpages. +Fixed: Bug on showing clans in motd.inc ingame. +Fixed: Division by zero error in server graphics on setup (show_graph.php). +Fixed: Activity display on rarely situations. +Fixed: Smaller bugs. + + +08. July 2005 [RC8] +--------------------------------------------------------------------------------------------------------------------------------------------- ++ Synchronization with the global banlist in administration web interface. ++ Optional display the content auf /session, /rank, /kpd in an ingame window on CSS servers (required Mani-Admin-Plugin V1.1.0o). ++ Players now can turn off displaying console events with "hlx_display 0"(required BroadCastEventsCommandSteamid == 1, PlayerEventsCommandSteamid == 1). ++ Using cvar sv_visiblemaxplayers to display maximum server slots correctly. +- The cheaterlist starts now with 0 minimum kills to show all cheaters. +- Updated Flashanimation. Colors now fit to the styles. Size reduced (-20kbyte). +- Events were deleted also when people are playing. +- Update all output in webpage and perl daemon to display formatted numbers. +- Cheaters and hidden players are no more listed in clan rankings. +- New default value "" for "PlayerEventsAdminCommand". +- Sending now Top200-Players to the global ranking. Value can changed with new variable "MasterServerTopPlayers". +- Auto commands call has changed to "hlx_auto". +Fixed: MySQL upgrade file missing some table changes (upgrade_hlstats_v132.sql). +Fixed: Clearing player team on map change (Switching DM to TDM therefore possible). +Fixed: Actplayers could now never be greater then maxplayers (HLstats_Server.pm). +Fixed: Showing graph axis not correct or with just on value (e.g. maxplayers = 13). + + +30. Juli 2005 [RC7] +--------------------------------------------------------------------------------------------------------------------------------------------- ++ If broadcasting enabled all vac bans are reported to the masterserver and saved in a global banlist. ++ Optional show Flash Animation (by Sandman) for weapon targets (need SHOW_WEAPON_TARGET_FLASH = 1 in config.php). ++ Get the vac cheater listing ingame through "/cheaters" command. +- Response on setting auto commands. +- Server overview graphics range is now 24 hours. +- Minor changes in webpages. +Fixed: Removing margin in ingame webpages. +Fixed: Font color auf graphics are now dynamic to the style. +Fixed: On resetting statistics also resetting the hlstats_Servers tables data. +Fixed: Graph peaks on map change when bots are playing. +Fixed: Top line of graphics now never paint dotted. +Fixed: On importing logs from stdin not call rcon commands. +Fixed: Total player count on "skill" command not shown correctly on different games (Thx SoNiC). +Fixed: Shown player statistics on hl2mp on server overview. + + +26. Jun 2005 [RC6] +--------------------------------------------------------------------------------------------------------------------------------------------- ++ Detect VAC2 bans and hide automatically such players from ranking. ++ Cheater list table in the main menu (need SHOW_VAC_BANS = 1 in config.php). ++ Admin-Event History now shows Mani-Admin-Plugin and BeetlesMod. +Fixed: Load graph is displaying css graphs on hl2mp (game.inc). +Fixed: Maxplayers finally working also during map-change (HLstats_Server.pm). +Fixed: Now server resetting includes Hlstats_server_load and HLstats_Trend tables (Thx DOH!Scorpion). +Fixed: Display message on teamkills. +Fixed: Game images are now included for source engine. + + +23. Jun 2005 [RC5] +--------------------------------------------------------------------------------------------------------------------------------------------- ++ HL2 Deathmatch support with images. ++ Upgrade sql file for hlstats V1.32 users (upgrade_hlstats_v132.sql). ++ Motd ingame include file. This shows Top-Players/Clans/Maps etc on startup (by Flashman). +- Changing admin access rights on restricted accounts. Now restricted accounts can also change styles and add servers. They can now even reset the statistics so be careful with your admin accounts! +- Set MinKill limit again to 5 as default. +- Standard of delete days is lowered to 14 days as default. +- Introduce game related sql files (game_hl2mp.sql/game_css.sql). +Fixed: Styles are back to business. +Fixed: Sometimes crashing on suicide events. +Fixed: TK penalty not working (got the suicide penalty points). +Fixed: Announcing ingame stats with "say" command. +Fixed: Not displaying correct maxplayers. +Fixed: HTML page width on stats with more than one game. +Fixed: Sytax error in hlstatsx.css (Thx Michael_Jim). + + +11. Jun 2005 [RC4] +--------------------------------------------------------------------------------------------------------------------------------------------- ++ Possibility to broadcast events only to the affected players (requires mani-admin-plugin). ++ New option to disable broadcasting PlayerActions (f.e touched a hostage, dropped the bomb etc). ++ Server script to show the actual server status on your homepage (status.php). ++ New options to make individual penalty points for teamkilling and suicides. +Fixed: Rcon crashes after some hours. +Fixed: Minor bugs. + + +06. Jun 2005 [RC3] +--------------------------------------------------------------------------------------------------------------------------------------------- ++ Added possibility to ignore bots from HLstatsX. ++ New rcon features, the connection will be renewed every 100 commands. This makes it possible to fix a broken connection. +Fixed: Crash on splitted source packets (Hopefully for ev1). + + +05. Jun 2005 [RC2] +--------------------------------------------------------------------------------------------------------------------------------------------- ++ Added config.php in the web directory. Now only one file must be configured for php files. ++ Added new constant "IMAGE_PATH_URL" to avoid problems with not showing the graphs correctly. The value should point to your hlstats web directory WITHOUT "/hlstats.php" or trailing "/". ++ Added new options to the server array configuration. Now you can set MinPlayers for each server. Now it is also possible to specify admins. Those can turn on/off team-balancing (hlx_teams 0/1), aren't banned on retry (AutoBanRetry = 1) and they can play even if they don't match the positions limit (MinimumPlayersRank > 0). +- Small updates on ingame webpages. +- General easier way bringing graphics up running. +Fixed: Clanranking didn't work (execute update_rc1.sql in your database). +Fixed: On some purposes ingame displaying not working. +Fixed: Removing unnecessary hlstatsimg directory. + + +31. May 2005 [RC1] +--------------------------------------------------------------------------------------------------------------------------------------------- +- Changed all webpages. The menu is now always on top of the page and all pages are shown over the complete width of the screen. + + +15. May 2005 +--------------------------------------------------------------------------------------------------------------------------------------------- ++ Added possibility to set a required position-limit. If you wanna play on those servers you must have at least position <= limit (f.e. Top500-Servers). ++ New developing of HLstatsX-Proxy to send spoofed-packets over multiple servers. ++ Completely rewritten rcon-class since KKrcon version has a horrible way of working and performance problems (Performance gain ~500%). ++ It is now possible to display statistics ingame in an own window. ++ Complete new ingame framework. ++ Possibility to detect bans and make the ban "global" on all servers. ++ Graphical display from the server load. ++ Player activity for ranking (inactive players are not displayed anymore). +- Reworked MySQL-Queries. +Fixed: Many bugs. + + +11. Apr 2005 +--------------------------------------------------------------------------------------------------------------------------------------------- +- Changed master server protocol to submit players and top-players (HLstats_Server.pm). + + +10. Apr 2005 +--------------------------------------------------------------------------------------------------------------------------------------------- ++ Changed way of socket handling. If no logs for parsing available hlstats optimizes the database, delete old events and can communicate with the master server (hlstats.pl). +- Empty servers are now reported to master server if MasterServerData > 0 (hlstats.pl and HLstats_Server.pm) + + +08. Apr 2005 +--------------------------------------------------------------------------------------------------------------------------------------------- ++ Option to track only configurated servers. This is a BIG security thing to older versions! ++ New way of configuration in hlstats.conf. Now you can configure every single server. (Needs to change ConfigReaderSimple.pm) ++ Support for master server communication (http://www.statsme.de/master) +- Changed standard config behaviour for server. For now almost all extended options are turned off. +- Changed MySQL-Table hlstats_Servers. You have to include on more field (players int(11) NOT NULL default '0') +Fixed: Errors on htmlentities (playerinfo.inc) +Fixed: All known bugs (f.e. weblinks in navbar) + + +04. Apr 2005 +--------------------------------------------------------------------------------------------------------------------------------------------- +- Reworked table design for better display on 1024x768. (game.inc, weapons.inc, players.inc, playerinfo.inc) +Fixed: Max-Players value is set automatically in database (HLStats_Server.pm) + + +02. Apr 2005 +--------------------------------------------------------------------------------------------------------------------------------------------- ++ Add possibility to get other players rank, session or kpd-data (Players userid as argument. f.e. "session 4501") ++ Statsme-events are only tracked if actplayers >= minplayers +Fixed: Hard-coded link to statsme.de in livestats (game.inc) +Fixed: Settings in the config-file now (hopefully) working, sorry for that :( + + +30. Mar 2005 +--------------------------------------------------------------------------------------------------------------------------------------------- ++ Added configurations-options, now everybody (hello admins :D) can use this modifcation. ++ Weapon-Accuracy informations fully integrated into the web-page ++ Server-Table now includes every related server-data (f.e. All shots from CTs :D) (Serverstats-Webpage is on the way...) ++ Better player tracking. ++ UTF-8 Header support. +Fixed: All known bugs from hlstats-versions around. (STEAM_ID_PENDING-problem is fixed!) + + +20. Mar 2005 +--------------------------------------------------------------------------------------------------------------------------------------------- +- Now you can work right from the start with the new MySQL-Design. Just use the hlstats.sql from here. + + +18. Mar 2005 +--------------------------------------------------------------------------------------------------------------------------------------------- +- Initial release. diff --git a/CREATORS b/CREATORS new file mode 100644 index 0000000..91e5550 --- /dev/null +++ b/CREATORS @@ -0,0 +1,34 @@ + HLstatsX Community Edition - Real-time player and clan rankings and statistics + http://www.hlxcommunity.com + Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) + + HLstatsX is an enhanced version of ELstatsNEO + ELstatsNEO - Real-time player and clan rankings and statistics + http://ovrsized.neo-soft.org/ + Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) + + ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX + HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 + http://www.hlstatsx.com/ + Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) + + HLstatsX is an enhanced version of HLstats made by Simon Garner + HLstats - Real-time player and clan rankings and statistics for Half-Life + http://sourceforge.net/projects/hlstats/ + Copyright (C) 2001 Simon Garner + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + + For support and installation notes visit http://ovrsized.neo-soft.org! diff --git a/IMAGES b/IMAGES new file mode 100644 index 0000000..1f352db --- /dev/null +++ b/IMAGES @@ -0,0 +1,101 @@ +HLStatsX Community Edition +IMAGES Credits + +NOTE: The developers have made a good-faith effort to give credit where credit is due for images provided to the project. +All images remain the property of their respective owners. + +HLStatsX Community Edition +Classic Game Icons: vaksa (http://www.myspace.com/vaksa) +Logo: Viper +Nav-icons: Viper +Signature Images: R3M + +Hitbox Flash Animation by sandman {sandman@borgcluster.de) +Updated with new logo by BusteR + +Age of Chivalry +Game icon: pirate555 + +Counter-Strike +Maps: (jumpin) banana from HLStats Project +Ribbons: Unity +Weapons: (jumpin) banana from HLStats Project + +Counter-Strike: Source +Awards: Hellraiser from ELstatsNEO (http://forum.elstatsneo.de), HS Fighter +Heatmaps: stachi, +Ribbons: Hellraiser from ELstatsNEO (http://forum.elstatsneo.de), HS Fighter +Weapons: HO!NO! + +Day of Defeat +Heatmaps: Zuko +Maps: (jumpin) banana from HLStats Project +Weapons: (jumpin) banana from HLStats Project + +Day of Defeat: Source +Awards: Mosalar + +Fistful of Frags +Awards: Mosalar + +Goldeneye Source +Awards: Dr.NO +Maps: Dr.NO +Roles: Dr.NO +Weapons: Dr.No + +HL2DM +Awards: Osiris_TSGK +Ribbons: Osiris_TSGK + +Insurgency +Awards: Trawa +Ribbons: Trawa +Other: Zuko, _KaszpiR_ + +Left4Dead +Awards: U#O, MrXorMrY +Maps: Honk +Ribbons: U#O, MrXorMrY +Roles: MrXorMrY +Weapons: R3M + +Left4Dead 2 +Awards: U#0, MrXorMry +Ribbons: U#0, MrXorMry +Roles: ribit +Weapons: R3M +Signature images: gH0sTy (http://www.affenkaefig.com/) + +Natural-Selection +Heatmaps: Zuko +Maps: (jumpin) banana from HLStats Project from HLStats Project +Weapons: (jumpin) banana from HLStats Project + +NeoTokyo Source +Awards: R3M +Maps: R3M +Ribbons: R3M +Weapons: R3M + +Stargate: The Last Stand +Heatmaps: Zuko + +Team Fortress Classic +Heatmaps: Zuko +Maps: (jumpin) banana from HLStats Project +Weapons: (jumpin) banana from HLStats Project + +Team Fortress 2 +Awards: FernFerret, Semikolon, DragonShadow, Fluff-a-Licious, Rakshot, bug +Heatmaps: Patalete, Roman Sobol +Maps: Extrim25 +Ribbons: FernFerret, Semikolon, DragonShadow, Rakshot, bug +Sandvich Award Icon: aveneyer (aveneyer@gmail.com) +Weapons: HO!NO!, Semikolon, FernFerret, DragonShadow, Solarpowered, soolshock, bug + +Zombie Panic Source +Awards: aveneyer (aveneyer@gmail.com) +Game icon: ~WaywerdWolf (wiff23@msn.com) +Weapons: aveneyer (aveneyer@gmail.com) + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5b6e7c6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/README b/README new file mode 100644 index 0000000..bfdb5c7 --- /dev/null +++ b/README @@ -0,0 +1,54 @@ +=========================================================== +HLstatsX Community Edition +http://www.hlxce.com +=========================================================== + +HLstatsX Community Edition is an open-source project licensed +under GNU General Public License v2 and is a real-time stats +and ranking for Source engine based games. HLstatsX Community +Edition uses a Perl daemon to parse the log streamed from the +game server. The data is stored in a MySQL Database and has +a PHP frontend. + + +For installation help, please visit our web site: + + http://www.hlxce.com + http://wiki.hlxce.com + +or our forums: + + http://forums.hlxce.com + +or join us on IRC: + + irc.gamesurge.net, channel #HLXCE + + +To be notified of upcoming releases, you can join our +FreeLists group: + + http://www.freelists.org/list/hlxce + + +We are also always looking for Beta-Testers. Join our +Google Group at: + + http://groups.google.com/group/hlxce-beta-testers + + +NOTE: + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. \ No newline at end of file diff --git a/amxmodx/plugins/hlstatsx_commands_cstrike.amxx b/amxmodx/plugins/hlstatsx_commands_cstrike.amxx new file mode 100644 index 0000000..5f03f4a Binary files /dev/null and b/amxmodx/plugins/hlstatsx_commands_cstrike.amxx differ diff --git a/amxmodx/plugins/hlstatsx_commands_dod.amxx b/amxmodx/plugins/hlstatsx_commands_dod.amxx new file mode 100644 index 0000000..f3f20cc Binary files /dev/null and b/amxmodx/plugins/hlstatsx_commands_dod.amxx differ diff --git a/amxmodx/plugins/hlstatsx_commands_ns.amxx b/amxmodx/plugins/hlstatsx_commands_ns.amxx new file mode 100644 index 0000000..b08d4b9 Binary files /dev/null and b/amxmodx/plugins/hlstatsx_commands_ns.amxx differ diff --git a/amxmodx/plugins/hlstatsx_commands_tfc.amxx b/amxmodx/plugins/hlstatsx_commands_tfc.amxx new file mode 100644 index 0000000..0fc5d08 Binary files /dev/null and b/amxmodx/plugins/hlstatsx_commands_tfc.amxx differ diff --git a/amxmodx/scripting/hlstatsx_commands_cstrike.sma b/amxmodx/scripting/hlstatsx_commands_cstrike.sma new file mode 100644 index 0000000..18a3a66 --- /dev/null +++ b/amxmodx/scripting/hlstatsx_commands_cstrike.sma @@ -0,0 +1,1355 @@ +/** + * 2008 - Modified by Nicholas Hastings (psychonic) for used with HLstatsX Community Edition + * http://www.hlxcommunity.com + * + * HLstatsX - AMX Mod X plugin to display ingame messages + * http://www.hlstatsx.com/ + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma dynamic 16000 + +#include +#include + +#include +#include +#include +#include + +#define VERSION "1.6.19 (HL1)" + +new g_hlx_block_commands +new g_hlx_message_prefix +new blocked_commands[][] = { "rank", "skill", "points", "place", "session", "session_data", + "kpd", "kdratio", "kdeath", "next", "load", "status", "servers", + "top20", "top10", "top5", "clans", "cheaters", "statsme", "weapons", + "weapon", "action", "actions", "accuracy", "targets", "target", "kills", + "kill", "player_kills", "cmd", "cmds", "command", "hlx_display 0", + "hlx_display 1", "hlx_teams 0", "hlx_teams 1", "hlx_hideranking", + "hlx_chat 0", "hlx_chat 1", "hlx_menu", "servers 1", "servers 2", + "servers 3", "hlx", "hlstatsx", "help" } + + +new g_msgSayText +new g_msgTeamInfo +new g_HLstatsX_MainMenu +new g_HLstatsX_AutoMenu +new g_HLstatsX_EventsMenu +new Array:g_HLstatsX_ColorArray + + +new ct_player_color = -1 +new ts_player_color = -1 +new message_cache[192] +new parsed_message_cache[192] +new cached_color_index + +new logmessage_ignore[512] +new display_menu_keys = MENU_KEY_0|MENU_KEY_1|MENU_KEY_2|MENU_KEY_3|MENU_KEY_4|MENU_KEY_5|MENU_KEY_6|MENU_KEY_7|MENU_KEY_8|MENU_KEY_9 + +new TeamNames[][] = +{ + "", + "TERRORIST", + "CT", + "SPECTATOR" +} + + +public plugin_init() +{ + register_plugin("HLstatsX CE Ingame Plugin", VERSION, "psychonic") + register_srvcmd("hlx_amx_psay", "hlx_amx_psay", 0, " - sends private message") + register_srvcmd("hlx_amx_bulkpsay", "hlx_amx_bulkpsay", 0, " - sends private message to many") + register_srvcmd("hlx_amx_psay2", "hlx_amx_psay2", 0, " - sends green colored private message") + register_srvcmd("hlx_amx_say", "hlx_amx_say", 0, " - sends message to all players") + register_srvcmd("hlx_amx_csay", "hlx_amx_csay", 0, " - sends center hud message to all players") + register_srvcmd("hlx_amx_hint", "hlx_amx_hint", 0, " - sends hint message") + register_srvcmd("hlx_amx_msay", "hlx_amx_msay", 0, " - displays advanced information") + register_srvcmd("hlx_amx_browse", "hlx_amx_browse", 0, " - displays internal browser") + register_srvcmd("hlx_amx_swap", "hlx_amx_swap", 0, " - swaps players to the opposite team") + + register_cvar("hlxce_plugin_version", VERSION, FCVAR_SPONLY|FCVAR_SERVER) + register_cvar("hlxce_version", "", FCVAR_SPONLY|FCVAR_SERVER) + register_cvar("hlxce_webpage", "http://www.hlxcommunity.com", FCVAR_SPONLY|FCVAR_SERVER) + g_hlx_block_commands = register_cvar("hlx_block_commands", "1") + g_hlx_message_prefix = register_cvar("hlx_message_prefix", "") + + register_event("TextMsg", "client_joinedteam", "a", "2=#Game_join_ct", "2=#Game_join_terrorist") + register_event("DeathMsg", "client_kill", "a", "1>0") + + // building the menus only once + g_HLstatsX_MainMenu = menu_create("HLstatsX - Main Menu", "mainmenu_handle") + menu_additem(g_HLstatsX_MainMenu, "Display Rank", "1") + menu_additem(g_HLstatsX_MainMenu, "Next Players", "2") + menu_additem(g_HLstatsX_MainMenu, "Top10 Players", "3") + menu_additem(g_HLstatsX_MainMenu, "Clans Ranking", "4") + menu_additem(g_HLstatsX_MainMenu, "Server Status", "5") + menu_additem(g_HLstatsX_MainMenu, "Statsme", "6") + menu_additem(g_HLstatsX_MainMenu, "Auto Ranking", "7") + menu_additem(g_HLstatsX_MainMenu, "Console Events", "8") + menu_additem(g_HLstatsX_MainMenu, "Weapon Usage", "9") + menu_additem(g_HLstatsX_MainMenu, "Weapons Accuracy", "10") + menu_additem(g_HLstatsX_MainMenu, "Weapons Targets", "11") + menu_additem(g_HLstatsX_MainMenu, "Player Kills", "12") + menu_additem(g_HLstatsX_MainMenu, "Toggle Ranking Display", "13") + menu_additem(g_HLstatsX_MainMenu, "VAC Cheaterlist", "14") + menu_additem(g_HLstatsX_MainMenu, "Display Help", "15") + menu_setprop(g_HLstatsX_MainMenu, MPROP_PERPAGE, 6) + + g_HLstatsX_AutoMenu = menu_create("HLstatsX - Auto-Ranking", "automenu_handle") + menu_additem(g_HLstatsX_AutoMenu, "Enable on round-start", "1") + menu_additem(g_HLstatsX_AutoMenu, "Enable on round-end", "2") + menu_additem(g_HLstatsX_AutoMenu, "Enable on player death", "3") + menu_additem(g_HLstatsX_AutoMenu, "Disable", "4") + menu_setprop(g_HLstatsX_AutoMenu, MPROP_PERPAGE, 0) + + g_HLstatsX_EventsMenu = menu_create("HLstatsX - Console Events", "eventsmenu_handle") + menu_additem(g_HLstatsX_EventsMenu, "Enable Events", "1") + menu_additem(g_HLstatsX_EventsMenu, "Disable Events", "2") + menu_additem(g_HLstatsX_EventsMenu, "Enable Global Chat", "3") + menu_additem(g_HLstatsX_EventsMenu, "Disable Global Chat", "4") + menu_setprop(g_HLstatsX_EventsMenu, MPROP_PERPAGE, 0) + + register_menucmd(register_menuid("Display Menu"), display_menu_keys, "handle_internal_menu") + register_clcmd("say", "hlx_block_commands") + register_clcmd("say_team", "hlx_block_commands") + + clear_message_cache() + + ct_player_color = -1 + ts_player_color = -1 + find_player_team_slot("CT") + find_player_team_slot("TERRORIST") + + g_msgSayText = get_user_msgid("SayText") + g_msgTeamInfo = get_user_msgid("TeamInfo") + g_HLstatsX_ColorArray = ArrayCreate() + + register_forward(FM_AlertMessage, "game_log_hook") +} + + +public add_message_cache(message[192], parsed_message[192], color_index) { + message_cache = message + parsed_message_cache = parsed_message + cached_color_index = color_index +} + + +public is_message_cached(message[192]) { + if (strcmp(message, message_cache) == 0) { + return 1 + } + return 0 +} + + +public clear_message_cache() { + message_cache = "" + parsed_message_cache = "" + cached_color_index = -1 +} + + +public log_player_event(client, verb[32], player_event[192], display_location) +{ + if ((client > 0) && (is_user_connected(client))) { + new player_userid = get_user_userid(client) + + static player_authid[32] + get_user_authid(client, player_authid, 31) + + static player_name[32] + get_user_name(client, player_name, 31) + + static player_team[16] + get_user_team(client, player_team, 15) + + if (display_location > 0) { + new player_origin[3] + get_user_origin (client, player_origin) + + format(logmessage_ignore, 511, "^"%s<%d><%s><%s>^" %s ^"%s^"", player_name, player_userid, player_authid, player_team, verb, player_event) + log_message("^"%s<%d><%s><%s>^" %s ^"%s^" (position ^"%d %d %d^")", player_name, player_userid, player_authid, player_team, verb, player_event, player_origin[0], player_origin[1], player_origin[2]) + } else { + log_message("^"%s<%d><%s><%s>^" %s ^"%s^"", player_name, player_userid, player_authid, player_team, verb, player_event) + } + } +} + + +public game_log_hook(AlertType: type, message[]) +{ + if (type != at_logged ) { + return FMRES_IGNORED + } + if ((strcmp("", logmessage_ignore) != 0) && (contain(message, logmessage_ignore) != -1)) { + if (contain(message, "position") == -1) { + logmessage_ignore = "" + return FMRES_SUPERCEDE + } + } + return FMRES_IGNORED +} + + +stock find_player_team_slot(team[16]) +{ + + new team_index = get_team_index(team) + if (team_index > -1) { + if (strcmp(team, "CT") == 0) { + ct_player_color = -1 + } else if (strcmp(team, "TERRORIST") == 0) { + ts_player_color = -1 + } + + new max_clients = get_maxplayers() + for(new i = 1; i <= max_clients; i++) { + new player_index = i + if (is_user_connected(player_index)) { + static player_team[16] + get_user_team(player_index, player_team, 15) + + if (strcmp(player_team, team) == 0) { + if (strcmp(team, "CT") == 0) { + ct_player_color = player_index + if (ts_player_color == ct_player_color) { + ct_player_color = -1 + ts_player_color = -1 + } + break + } else if (strcmp(team, "TERRORIST") == 0) { + ts_player_color = player_index + if (ts_player_color == ct_player_color) { + ct_player_color = -1 + ts_player_color = -1 + } + break + } + } + } + } + } +} + + +public validate_team_colors() +{ + if (ct_player_color > -1) { + if (is_user_connected(ct_player_color)) { + static player_team[16] + get_user_team(ct_player_color, player_team, 15) + if (strcmp("CT", player_team) != 0) { + ct_player_color = -1 + } + } else { + ct_player_color = -1 + } + } else if (ts_player_color > -1) { + if (is_user_connected(ts_player_color)) { + static player_team[16] + get_user_team(ts_player_color, player_team, 15) + if (strcmp("TERRORIST", player_team) != 0) { + ts_player_color = -1 + } + } else { + ts_player_color = -1 + } + } + if ((ct_player_color == -1) || (ts_player_color == -1)) { + if (ct_player_color == -1) { + find_player_team_slot("CT") + } + if (ts_player_color == -1) { + find_player_team_slot("TERRORIST") + } + } +} + + +stock get_team_index(team_name[16]) +{ + if (strcmp(team_name, "TERRORIST") == 0) { + return 0 + } else if (strcmp(team_name, "CT") == 0) { + return 1 + } + return -1 +} + + +stock remove_color_entities(message[192]) +{ + replace_all(message, 192, "x04", "") + replace_all(message, 192, "x03", "") + replace_all(message, 192, "x01", "") +} + + +stock color_entities(message[192]) +{ + new replace_message[2] + replace_message[0] = 0x04 + replace_all(message, 192, "x04", replace_message) + replace_message[0] = 0x03 + replace_all(message, 192, "x03", replace_message) + replace_message[0] = 0x01 + replace_all(message, 192, "x01", replace_message) +} + + +stock color_team_entities(message[192]) +{ + if (ts_player_color > -1) { + if (contain(message, "TERRORIST") > -1) { + new replace_message[192] + replace_message[0] = 0x03 + format(replace_message[1], 191, "%s", "TERRORIST") + replace_message[strlen("TERRORIST") + 1] = 0x01 + replace_all(message, 192, "TERRORIST", replace_message) + return ts_player_color + } + } + if (ct_player_color > -1) { + if (contain(message, "CT") > -1) { + new replace_message[192] + replace_message[0] = 0x03 + format(replace_message[1], 191, "%s", "CT") + replace_message[strlen("CT") + 1] = 0x01 + replace_all(message, 192, "CT", replace_message) + return ct_player_color + } + } + + return -1 +} + +stock color_player(color_type, player_index, client_message[192]) +{ + new color_player_index = -1 + static client_name[192] + get_user_name(player_index, client_name, 191) + if (color_type == 1) { + new colored_player_name[192] + colored_player_name[0] = 0x03 + format(colored_player_name[1], 191, "%s", client_name) + colored_player_name[strlen(client_name) + 1] = 0x01 + + if (contain(client_message, client_name) > -1) { + replace_all(client_message, 192, client_name, colored_player_name) + return player_index + } + } else { + new colored_player_name[192] + colored_player_name[0] = 0x04 + format(colored_player_name[1], 191, "%s", client_name) + colored_player_name[strlen(client_name) + 1] = 0x01 + + if (contain(client_message, client_name) > -1) { + replace_all(client_message, 192, client_name, colored_player_name) + } + } + return color_player_index +} + + +stock color_all_players(message[192]) +{ + new color_index = -1 + ArrayClear(g_HLstatsX_ColorArray) + + new lowest_matching_pos = 192 + new lowest_matching_pos_client = -1 + + new max_clients = get_maxplayers() + for(new i = 1; i <= max_clients; i++) { + + new client = i + if (is_user_connected(client)) { + static client_name[192] + get_user_name(client, client_name, 191) + new message_pos = contain(message, client_name) + if (message_pos > -1) { + if (lowest_matching_pos > message_pos) { + lowest_matching_pos = message_pos + lowest_matching_pos_client = client + } + new TempPlayerColorArray[1] + TempPlayerColorArray[0] = client + ArrayPushArray(g_HLstatsX_ColorArray, TempPlayerColorArray) + } + } + } + + new size = ArraySize(g_HLstatsX_ColorArray) + for (new i = 0; i < size; i++) { + new temp_player_array[1] + ArrayGetArray(g_HLstatsX_ColorArray, i, temp_player_array) + new temp_client = temp_player_array[0] + if (temp_client == lowest_matching_pos_client) { + new temp_color_index = color_player(1, temp_client, message) + color_index = temp_color_index + } else { + color_player(0, temp_client, message) + } + } + ArrayClear(g_HLstatsX_ColorArray) + + return color_index +} + + +public client_kill() +{ + + new killer_id = read_data(1) + new victim_id = read_data(2) + new is_headshot = read_data(3) + new weapon[32] + read_data(4, weapon, 31) + + if ((killer_id > 0) && (is_user_connected(killer_id))) { + + new suicide = 0 + if (killer_id == victim_id) { + suicide = 1 + } + + static killer_name[32], killer_team[16], killer_authid[32] + new killer_userid = get_user_userid(killer_id) + + get_user_name(killer_id, killer_name, 31) + get_user_team(killer_id, killer_team, 15) + get_user_authid(killer_id, killer_authid, 31) + new killer_origin[3] + get_user_origin (killer_id, killer_origin) + new headshot_logentry[16] = "" + + if (suicide == 0) { + if ((victim_id > 0) && (is_user_connected(victim_id))) { + static victim_name[32], victim_team[16], victim_authid[32] + new victim_userid = get_user_userid(victim_id) + + get_user_name(victim_id, victim_name, 31) + get_user_team(victim_id, victim_team, 15) + get_user_authid(victim_id, victim_authid, 31) + + new victim_origin[3] + get_user_origin (victim_id, victim_origin) + + if (is_headshot > 0) { + headshot_logentry = " (headshot)" + } + format(logmessage_ignore, 511, "^"%s<%d><%s><%s>^" killed ^"%s<%d><%s><%s>^" with ^"%s^"", + killer_name, killer_userid, killer_authid, killer_team, + victim_name, victim_userid, victim_authid, victim_team, + weapon) + log_message("^"%s<%d><%s><%s>^" killed ^"%s<%d><%s><%s>^" with ^"%s^"%s (attacker_position ^"%d %d %d^") (victim_position ^"%d %d %d^")", + killer_name, killer_userid, killer_authid, killer_team, + victim_name, victim_userid, victim_authid, victim_team, + weapon, headshot_logentry, + killer_origin[0], killer_origin[1], killer_origin[2], + victim_origin[0], victim_origin[1], victim_origin[2]) + + } + } else { + format(logmessage_ignore, 511, "^"%s<%d><%s><%s>^" committed suicide with ^"%s^"", + killer_name, killer_userid, killer_authid, killer_team, weapon) + log_message("^"%s<%d><%s><%s>^" committed suicide with ^"%s^" (attacker_position ^"%d %d %d^")", + killer_name, killer_userid, killer_authid, killer_team, weapon, + killer_origin[0], killer_origin[1], killer_origin[2]) + } + } + +} + + +public client_joinedteam() +{ + new id = read_data(1) + if ((id > 0) && (is_user_connected(id))) { + if ((ct_player_color == -1) || (id == ct_player_color)) { + ct_player_color = -1 + clear_message_cache() + } else if ((ts_player_color == -1) || (id == ts_player_color)) { + ts_player_color = -1 + clear_message_cache() + } + } +} + + +public client_disconnect(id) +{ + if ((id > 0) && (is_user_connected(id))) { + if ((ct_player_color == -1) || (id == ct_player_color)) { + ct_player_color = -1 + clear_message_cache() + } else if ((ts_player_color == -1) || (id == ts_player_color)) { + ts_player_color = -1 + clear_message_cache() + } + } +} + + +public client_death(killer, victim, wpnindex, hitplace, TK) +{ + new id = victim + + if ((id > 0) && (is_user_connected(id))) { + new iStats[8], iHits[8] + static szTeam[16], szName[32], szAuthid[32], szWeapon[24] + new iUserid = get_user_userid(id) + new _max = xmod_get_maxweapons() + + get_user_team(id, szTeam, 15) + get_user_name(id, szName, 31) + get_user_authid(id, szAuthid, 31) + + for (new i = 1; i < _max; ++i) { + if (get_user_wstats(id, i, iStats, iHits)) { + xmod_get_wpnname(i, szWeapon, 23) + + log_message("^"%s<%d><%s><%s>^" triggered ^"weaponstats^" (weapon ^"%s^") (shots ^"%d^") (hits ^"%d^") (kills ^"%d^") (headshots ^"%d^") (tks ^"%d^") (damage ^"%d^") (deaths ^"%d^")", + szName, iUserid, szAuthid, szTeam, szWeapon, iStats[4], iStats[5], iStats[0], iStats[2], iStats[3], iStats[6], iStats[1]) + log_message("^"%s<%d><%s><%s>^" triggered ^"weaponstats2^" (weapon ^"%s^") (head ^"%d^") (chest ^"%d^") (stomach ^"%d^") (leftarm ^"%d^") (rightarm ^"%d^") (leftleg ^"%d^") (rightleg ^"%d^")", + szName, iUserid, szAuthid, szTeam, szWeapon, iHits[1], iHits[2], iHits[3], iHits[4], iHits[5], iHits[6], iHits[7]) + } + } + reset_user_wstats(id) + } +} + + +stock ExplodeString( Output[][], Max, Size, Input[], Delimiter ) +{ + new Idx, l = strlen(Input), Len; + do Len += (1 + copyc( Output[Idx], Size, Input[Len], Delimiter )); + while( (Len < l) && (++Idx < Max) ) + return Idx; +} + +psay (client_id[], is_colored, client_message[192]) +{ + new client = str_to_num(client_id) + + if (client > 0) { + new Players[32] + new player_count, temp_player_index, player_index + get_players(Players, player_count, "ch") + for (temp_player_index = 0; temp_player_index < player_count; temp_player_index++) { + new player = Players[temp_player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == client) { + player_index = player + break + } + } + + if ((player_index > 0) && (!is_user_bot(player_index)) && (is_user_connected(player_index))) { + new color_index = player_index + + static display_message[192] + if (is_colored > 0) { + if (is_message_cached(client_message) > 0) { + client_message = parsed_message_cache + color_index = cached_color_index + } else { + static client_message_backup[192] + copy(client_message_backup, 191, client_message) + + new player_color_index = color_all_players(client_message) + if (player_color_index > -1) { + color_index = player_color_index + } else { + validate_team_colors() + color_index = color_team_entities(client_message) + } + color_entities(client_message) + add_message_cache(client_message_backup, client_message, color_index) + } + + } + + static message_prefix[64] + get_pcvar_string(g_hlx_message_prefix, message_prefix, 64) + if (strcmp(message_prefix, "") == 0) { + display_message[0] = 0x01 + format(display_message[1], 191, "%s", client_message) + } else { + display_message[0] = 0x04 + format(display_message[1], 191, "%s", message_prefix) + display_message[strlen(message_prefix) + 1] = 0x01 + format(display_message[strlen(message_prefix) + 2], 192 - (strlen(message_prefix) + 2), " %s", client_message) + } + + new player_team = get_user_team(player_index) + new color_team = -1 + if (is_user_connected(color_index)) { + color_team = get_user_team(color_index) + } + + if ((player_team != color_team) && (color_team > -1)) { + message_begin(MSG_ONE, g_msgTeamInfo, {0,0,0}, player_index) + write_byte(player_index) + write_string(TeamNames[color_team]) + message_end() + } + + message_begin(MSG_ONE, g_msgSayText, {0,0,0}, player_index) + if (is_user_connected(color_index)) { + write_byte(color_index) + } else { + write_byte(player_index) + } + write_string(display_message) + message_end() + + if ((player_team != color_team) && (color_team > -1)) { + message_begin(MSG_ONE, g_msgTeamInfo, {0,0,0}, player_index) + write_byte(player_index) + write_string(TeamNames[player_team]) + message_end() + } + + } + } +} + +public hlx_amx_bulkpsay(id, level, cid) +{ + new argument_count = read_argc() + if (argument_count < 3) { + return PLUGIN_HANDLED + } + + new client_id_list[48] + read_argv(1, client_id_list, 47) + new client_ids[8][6]; + ExplodeString(client_ids, 7, 5, client_id_list, ','); + + static colored_param[32] + read_argv(2, colored_param, 31) + new is_colored = 0 + new ignore_param = 0 + if (strcmp(colored_param, "1") == 0) { + is_colored = 1 + ignore_param = 1 + } + if (strcmp(colored_param, "0") == 0) { + ignore_param = 1 + } + + new client_message[192] + for(new i = (1 + ignore_param); i < argument_count; i++) { + static temp_argument[192] + read_argv(i + 1, temp_argument, 191) + if (i > (1 + ignore_param)) { + if ((191 - strlen(client_message)) > strlen(temp_argument)) { + if ((temp_argument[0] == 41) || (temp_argument[0] == 125)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } else if ((strlen(client_message) > 0) && (client_message[strlen(client_message)-1] != 40) && (client_message[strlen(client_message)-1] != 123) && (client_message[strlen(client_message)-1] != 58) && (client_message[strlen(client_message)-1] != 39) && (client_message[strlen(client_message)-1] != 44)) { + if ((strcmp(temp_argument, ":") != 0) && (strcmp(temp_argument, ",") != 0) && (strcmp(temp_argument, "'") != 0)) { + client_message[strlen(client_message)] = 32 + } + copy(client_message[strlen(client_message)], 191, temp_argument) + } else { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } else { + if ((192 - strlen(client_message)) > strlen(temp_argument)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } + + for (new i = 0; i < 8; i++) + { + psay(client_ids[i], is_colored, client_message); + } + + return PLUGIN_HANDLED +} + +public hlx_amx_psay(id, level, cid) +{ + new argument_count = read_argc() + if (argument_count < 3) { + return PLUGIN_HANDLED + } + + static client_id[32] + read_argv(1, client_id, 31) + copy(client_id, 30, client_id[1]) + + static colored_param[32] + read_argv(2, colored_param, 31) + new is_colored = 0 + new ignore_param = 0 + if (strcmp(colored_param, "1") == 0) { + is_colored = 1 + ignore_param = 1 + } + if (strcmp(colored_param, "0") == 0) { + ignore_param = 1 + } + + new client_message[192] + for(new i = (1 + ignore_param); i < argument_count; i++) { + static temp_argument[192] + read_argv(i + 1, temp_argument, 191) + if (i > (1 + ignore_param)) { + if ((191 - strlen(client_message)) > strlen(temp_argument)) { + if ((temp_argument[0] == 41) || (temp_argument[0] == 125)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } else if ((strlen(client_message) > 0) && (client_message[strlen(client_message)-1] != 40) && (client_message[strlen(client_message)-1] != 123) && (client_message[strlen(client_message)-1] != 58) && (client_message[strlen(client_message)-1] != 39) && (client_message[strlen(client_message)-1] != 44)) { + if ((strcmp(temp_argument, ":") != 0) && (strcmp(temp_argument, ",") != 0) && (strcmp(temp_argument, "'") != 0)) { + client_message[strlen(client_message)] = 32 + } + copy(client_message[strlen(client_message)], 191, temp_argument) + } else { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } else { + if ((192 - strlen(client_message)) > strlen(temp_argument)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } + + psay(client_id, is_colored, client_message) + + return PLUGIN_HANDLED +} + + +public hlx_amx_psay2(id, level, cid) +{ + new argument_count = read_argc() + if (argument_count < 3) { + return PLUGIN_HANDLED + } + + static client_id[32] + read_argv(1, client_id, 31) + copy(client_id, 30, client_id[1]) + + static colored_param[32] + read_argv(2, colored_param, 31) + new ignore_param = 0 + if (strcmp(colored_param, "1") == 0) { + ignore_param = 1 + } + if (strcmp(colored_param, "0") == 0) { + ignore_param = 1 + } + + new client_message[192] + for(new i = (1 + ignore_param); i < argument_count; i++) { + static temp_argument[192] + read_argv(i + 1, temp_argument, 191) + if (i > (1 + ignore_param)) { + if ((191 - strlen(client_message)) > strlen(temp_argument)) { + if ((temp_argument[0] == 41) || (temp_argument[0] == 125)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } else if ((strlen(client_message) > 0) && (client_message[strlen(client_message)-1] != 40) && (client_message[strlen(client_message)-1] != 123) && (client_message[strlen(client_message)-1] != 58) && (client_message[strlen(client_message)-1] != 39) && (client_message[strlen(client_message)-1] != 44)) { + if ((strcmp(temp_argument, ":") != 0) && (strcmp(temp_argument, ",") != 0) && (strcmp(temp_argument, "'") != 0)) { + client_message[strlen(client_message)] = 32 + } + copy(client_message[strlen(client_message)], 191, temp_argument) + } else { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } else { + if ((192 - strlen(client_message)) > strlen(temp_argument)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } + + new client = str_to_num(client_id) + + if (client > 0) { + new Players[32] + new player_count, temp_player_index, player_index + get_players(Players, player_count, "ch") + for (temp_player_index = 0; temp_player_index < player_count; temp_player_index++) { + new player = Players[temp_player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == client) { + player_index = player + break + } + } + + if ((player_index > 0) && (!is_user_bot(player_index)) && (is_user_connected(player_index))) { + new color_index = player_index + + static display_message[192] + + static message_prefix[64] + get_pcvar_string(g_hlx_message_prefix, message_prefix, 64) + if (strcmp(message_prefix, "") == 0) { + display_message[0] = 0x04 + format(display_message[1], 191, "%s", client_message) + } else { + display_message[0] = 0x04 + format(display_message[1], 191, "%s: %s", message_prefix, client_message) + } + + message_begin(MSG_ONE, g_msgSayText, {0,0,0}, player_index) + write_byte(color_index) + write_string(display_message) + message_end() + } + } + + return PLUGIN_HANDLED +} + + +public hlx_amx_say(id, level, cid) +{ + if (!cmd_access(id, level, cid, 2)) + return PLUGIN_HANDLED + + static message[192] + read_args(message, 191) + remove_quotes(message) + + static message_prefix[64] + get_pcvar_string(g_hlx_message_prefix, message_prefix, 64) + if (strcmp(message_prefix, "") == 0) { + client_print(0, print_chat, "%s", message) + } else { + client_print(0, print_chat, "%s %s", message_prefix, message) + } + + return PLUGIN_HANDLED +} + + +public hlx_amx_csay(id, level, cid) +{ + if (!cmd_access(id, level, cid, 2)) + return PLUGIN_HANDLED + + static message[192] + read_args(message, 191) + remove_quotes(message) + + new color3[0][] = {255, 255, 255} + new Float:verpos = 0.3 + + set_hudmessage(color3[0][0], color3[0][1], color3[0][2], -1.0, verpos, 0, 6.0, 6.0, 0.5, 0.15, -1) + show_hudmessage(0, "%s", message) + + return PLUGIN_HANDLED +} + + +public hlx_amx_hint(id, level, cid) +{ + if (!cmd_access(id, level, cid, 2)) + return PLUGIN_HANDLED + + new argument_count = read_argc() + + static client_id[32] + read_argv(1, client_id, 31) + copy(client_id, 30, client_id[1]) + + new client_message[192] + for(new i = 1; i < argument_count; i++) { + static temp_argument[192] + read_argv(i + 1, temp_argument, 191) + if (i > 1) { + if ((191 - strlen(client_message)) > strlen(temp_argument)) { + if ((temp_argument[0] == 41) || (temp_argument[0] == 125)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } else if ((strlen(client_message) > 0) && (client_message[strlen(client_message)-1] != 40) && (client_message[strlen(client_message)-1] != 123) && (client_message[strlen(client_message)-1] != 58) && (client_message[strlen(client_message)-1] != 39) && (client_message[strlen(client_message)-1] != 44)) { + if ((strcmp(temp_argument, ":") != 0) && (strcmp(temp_argument, ",") != 0) && (strcmp(temp_argument, "'") != 0)) { + client_message[strlen(client_message)] = 32 + } + copy(client_message[strlen(client_message)], 191, temp_argument) + } else { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } else { + if ((192 - strlen(client_message)) > strlen(temp_argument)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } + + new client = str_to_num(client_id) + + if (client > 0) { + new Players[32] + new player_count, temp_player_index, player_index + get_players(Players, player_count, "ch") + for (temp_player_index = 0; temp_player_index < player_count; temp_player_index++) { + new player = Players[temp_player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == client) { + player_index = player + break + } + } + + if ((player_index > 0) && (!is_user_bot(player_index)) && (is_user_connected(player_index))) { + new color3[0][] = {255, 128, 0} + new Float:verpos = 0.80 + + set_hudmessage(color3[0][0], color3[0][1], color3[0][2], -1.0, verpos, 0, 6.0, 6.0, 0.5, 0.15, -1) + show_hudmessage(player_index, "%s", client_message) + } + } + + return PLUGIN_HANDLED +} + + +public hlx_amx_msay(id, level, cid) +{ + if (!cmd_access(id, level, cid, 3)) { + return PLUGIN_HANDLED + } + + static delay[8] + read_argv(1, delay, 7) + remove_quotes(delay) + + static name[32] + read_argv(2, name, 31) + copy(name, 30, name[1]) + new raw_user_id = str_to_num(name) + + static handler_param[32] + read_argv(3, handler_param, 31) + new ignore_param = 0 + new need_handler = 0 + if (strcmp(handler_param, "1") == 0) { + need_handler = 1 + ignore_param = 1 + } + if (strcmp(handler_param, "0") == 0) { + need_handler = 0 + ignore_param = 1 + } + + static message[1024] + new userid + new Players[32] + new player_count, player_index + get_players(Players, player_count, "ch") + for (player_index = 0; player_index < player_count; player_index++) { + new player = Players[player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == raw_user_id) { + userid = player + break + } + } + + read_args(message, 1023) + + new find_pattern[] = "#" + new find_pos = strfind(message, find_pattern) + new text_pos = find_pos + strlen(name) + 2 + if (ignore_param == 1) { + text_pos += 3 + } + + static menu_text[1024] + copy(menu_text, 1023, message[text_pos]) + remove_quotes(menu_text) + + new menu_display[1024] + + new i, start = 0 + new nLen = 0 + new buffer[1024] + + for(i = 0; i < strlen(menu_text); i++) { + if (i > 0) { + if ((menu_text[i-1] == '\') && (menu_text[i] == 'n')) { + buffer = "" + copy(buffer, (i - start)-1 , menu_text[start]) + nLen += format(menu_display[nLen], (1023 - nLen), "%s^n", buffer) + i += 1 + start = i + } + } + } + + if ((userid > 0) && (!is_user_bot(userid)) && (is_user_connected(userid))) { + if (need_handler == 0) { + show_menu(userid, display_menu_keys, menu_display, 15) + } else { + show_menu(userid, display_menu_keys, menu_display, 15, "Display Menu") + } + } + + return PLUGIN_HANDLED + +} + + +public handle_internal_menu(id, key) +{ + new client = id + if (is_user_connected(client)) { + if (key < 9) { + static player_event[192] + new slot = key + slot++ + num_to_str(slot, player_event, 192) + log_player_event(client, "selected", player_event, 0) + } else { + new player_event[192] = "cancel" + log_player_event(client, "selected", player_event, 0) + } + } +} + + +public hlx_amx_browse(id, level, cid) +{ + if (!cmd_access(id, level, cid, 2)) + return PLUGIN_HANDLED + + static name[32] + read_argv(1, name, 31) + copy(name, 30, name[1]) + new raw_user_id = str_to_num(name) + + static message[1024] + new userid + new Players[32] + new player_count, player_index + get_players(Players, player_count, "ch") + for (player_index = 0; player_index < player_count; player_index++) { + new player = Players[player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == raw_user_id) { + userid = player + break + } + } + + read_args(message, 1023) + + new find_pattern[] = "#" + new find_pos = strfind(message, find_pattern) + + static url[1024] + copy(url, 1023, message[find_pos + strlen(name) + 2]) + remove_quotes(url) + + if ((userid > 0) && (!is_user_bot(userid)) && (is_user_connected(userid))) { + show_motd(userid, url, "HLstatsX CE") + } + + return PLUGIN_HANDLED +} + + +public hlx_amx_swap(id, level, cid) +{ + if (!cmd_access(id, level, cid, 1)) + return PLUGIN_HANDLED + + static client_id[32] + read_argv(1, client_id, 31) + copy(client_id, 30, client_id[1]) + new client = str_to_num(client_id) + + if (client > 0) { + new userid = 0 + new Players[32] + new player_count, player_index + get_players(Players, player_count, "h") + for (player_index = 0; player_index < player_count; player_index++) { + new player = Players[player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == client) { + userid = player + break + } + } + + static player_team[32] + get_user_team(userid, player_team, 31) + + if ((userid > 0) && (is_user_connected(userid))) { + + if (strcmp(player_team, "TERRORIST") == 0) { + cs_set_user_team(userid, CS_TEAM_CT) + cs_reset_user_model(userid) + + if ((ct_player_color == -1) || (userid == ct_player_color)) { + ct_player_color = -1 + clear_message_cache() + } else if ((ts_player_color == -1) || (userid == ts_player_color)) { + ts_player_color = -1 + clear_message_cache() + } + } else if (strcmp(player_team, "CT") == 0) { + cs_set_user_team(userid, CS_TEAM_T) + cs_reset_user_model(userid) + + if ((ct_player_color == -1) || (userid == ct_player_color)) { + ct_player_color = -1 + clear_message_cache() + } else if ((ts_player_color == -1) || (userid == ts_player_color)) { + ts_player_color = -1 + clear_message_cache() + } + } + } + } + return PLUGIN_HANDLED +} + + +stock make_player_command(client, player_command[192]) +{ + if (client > 0) { + log_player_event(client, "say", player_command, 0) + } +} + + +public display_menu(menu, id) +{ + menu_display(id, menu, 0) +} + + +public display_mainmenu(id) +{ + display_menu(g_HLstatsX_MainMenu, id) + return PLUGIN_HANDLED +} + + +public display_automenu(id) +{ + display_menu(g_HLstatsX_AutoMenu, id) + return PLUGIN_HANDLED +} + + +public display_eventsmenu(id) +{ + display_menu(g_HLstatsX_EventsMenu, id) + return PLUGIN_HANDLED +} + + +public mainmenu_handle(id, menu, item) +{ + if (item < 0) { + return PLUGIN_CONTINUE + } + + static command[16], name[64] + new access, callback + menu_item_getinfo(menu, item, access, command, 15, name, 63, callback) + + new choice = str_to_num(command) + switch (choice) { + case 1 : + make_player_command(id, "/rank") + case 2 : + make_player_command(id, "/next") + case 3 : + make_player_command(id, "/top10") + case 4 : + make_player_command(id, "/clans") + case 5 : + make_player_command(id, "/status") + case 6 : + make_player_command(id, "/statsme") + case 7 : + display_automenu(id) + case 8 : + display_eventsmenu(id) + case 9 : + make_player_command(id, "/weapons") + case 10 : + make_player_command(id, "/accuracy") + case 11 : + make_player_command(id, "/targets") + case 12 : + make_player_command(id, "/kills") + case 13 : + make_player_command(id, "/hlx_hideranking") + case 14 : + make_player_command(id, "/cheaters") + case 15 : + make_player_command(id, "/help") + } + + return PLUGIN_HANDLED +} + + +public automenu_handle(id, menu, item) +{ + if (item < 0) { + return PLUGIN_CONTINUE + } + + static command[16], name[64] + new access, callback + menu_item_getinfo(menu, item, access, command, 15, name, 63, callback) + + new choice = str_to_num(command) + switch (choice) { + case 1 : + make_player_command(id, "/hlx_auto start rank") + case 2 : + make_player_command(id, "/hlx_auto end rank") + case 3 : + make_player_command(id, "/hlx_auto kill rank") + case 4 : + make_player_command(id, "/hlx_auto clear") + } + + return PLUGIN_HANDLED +} + + +public eventsmenu_handle(id, menu, item) +{ + if (item < 0) { + return PLUGIN_CONTINUE + } + + static command[16], name[64] + new access, callback + menu_item_getinfo(menu, item, access, command, 15, name, 63, callback) + + new choice = str_to_num(command) + switch (choice) { + case 1 : + make_player_command(id, "/hlx_display 1") + case 2 : + make_player_command(id, "/hlx_display 0") + case 3 : + make_player_command(id, "/hlx_chat 1") + case 4 : + make_player_command(id, "/hlx_chat 0") + } + + return PLUGIN_HANDLED +} + + +stock is_command_blocked(command[192]) +{ + new command_blocked = 0 + new command_index = 0 + while ((command_blocked == 0) && (command_index < sizeof(blocked_commands))) { + if (strcmp(command, blocked_commands[command_index]) == 0) { + command_blocked++ + } + command_index++ + } + if (command_blocked > 0) { + return 1 + } + return 0 +} + + + +public hlx_block_commands(client) +{ + if (client) { + if (client == 0) { + return PLUGIN_CONTINUE + } + + new block_chat_commands = get_pcvar_num(g_hlx_block_commands) + + static user_command[192] + read_args(user_command, 192) + static origin_command[192] + + new start_index = 0 + new command_length = strlen(user_command) + if (command_length > 0) { + if (user_command[start_index] == 34) { + start_index = start_index + 1 + if (user_command[command_length - 1] == 34) { + user_command[command_length - 1] = 0 + } + } + + if (user_command[start_index] == 47) { + start_index++ + } + + copy(origin_command, 192, user_command[start_index]) + } + + if (command_length > 0) { + if (block_chat_commands > 0) { + + new command_type[32] = "say" + new command_blocked = is_command_blocked(origin_command) + + if (command_blocked > 0) { + + if (is_user_connected(client)) { + if ((strcmp("hlx_menu", user_command[start_index]) == 0) || + (strcmp("hlx", user_command[start_index]) == 0) || + (strcmp("hlstatsx", user_command[start_index]) == 0)) { + + display_mainmenu(client) + } + log_player_event(client, command_type, origin_command, 0) + } + return PLUGIN_HANDLED + } + } else { + if (is_user_connected(client)) { + if ((strcmp("hlx_menu", user_command[start_index]) == 0) || + (strcmp("hlx", user_command[start_index]) == 0) || + (strcmp("hlstatsx", user_command[start_index]) == 0)) { + display_mainmenu(client) + } + } + return PLUGIN_CONTINUE + } + } + } + + return PLUGIN_CONTINUE +} + diff --git a/amxmodx/scripting/hlstatsx_commands_dod.sma b/amxmodx/scripting/hlstatsx_commands_dod.sma new file mode 100644 index 0000000..4cbb7be --- /dev/null +++ b/amxmodx/scripting/hlstatsx_commands_dod.sma @@ -0,0 +1,1138 @@ +/** + * 2008 - Modified by Nicholas Hastings (psychonic) for used with HLstatsX Community Edition + * http://www.hlxcommunity.com + * + * HLstatsX - AMX Mod X plugin to display ingame messages + * http://www.hlstatsx.com/ + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma dynamic 16000 + +#include +#include + +#include +#include +#include + +#include +#include + +#define VERSION "1.6.19 (HL1)" + +new g_hlx_block_commands +new g_hlx_message_prefix +new blocked_commands[][] = { "rank", "skill", "points", "place", "session", "session_data", + "kpd", "kdratio", "kdeath", "next", "load", "status", "servers", + "top20", "top10", "top5", "clans", "cheaters", "statsme", "weapons", + "weapon", "action", "actions", "accuracy", "targets", "target", "kills", + "kill", "player_kills", "cmd", "cmds", "command", "hlx_display 0", + "hlx_display 1", "hlx_teams 0", "hlx_teams 1", "hlx_hideranking", + "hlx_chat 0", "hlx_chat 1", "hlx_menu", "servers 1", "servers 2", + "servers 3", "hlx", "hlstatsx", "help" } + + +new g_msgSayText +new g_HLstatsX_MainMenu +new g_HLstatsX_AutoMenu +new g_HLstatsX_EventsMenu + +new allies_player_color = -1 +new axis_player_color = -1 + +new logmessage_ignore[512] +new display_menu_keys = MENU_KEY_0|MENU_KEY_1|MENU_KEY_2|MENU_KEY_3|MENU_KEY_4|MENU_KEY_5|MENU_KEY_6|MENU_KEY_7|MENU_KEY_8|MENU_KEY_9 + + +public plugin_init() +{ + register_plugin("HLstatsX CE Ingame Plugin", VERSION, "psychonic") + register_srvcmd("hlx_amx_psay", "hlx_amx_psay", 0, " - sends private message") + register_srvcmd("hlx_amx_bulkpsay", "hlx_amx_bulkpsay", 0, " - sends private message to many") + register_srvcmd("hlx_amx_psay2", "hlx_amx_psay2", 0, " - sends green colored private message") + register_srvcmd("hlx_amx_say", "hlx_amx_say", 0, " - sends message to all players") + register_srvcmd("hlx_amx_csay", "hlx_amx_csay", 0, " - sends center hud message to all players") + register_srvcmd("hlx_amx_hint", "hlx_amx_hint", 0, " - sends hint message") + register_srvcmd("hlx_amx_msay", "hlx_amx_msay", 0, " - displays advanced information") + register_srvcmd("hlx_amx_browse", "hlx_amx_browse", 0, " - displays internal browser") + register_srvcmd("hlx_amx_swap", "hlx_amx_swap", 0, " - swaps players to the opposite team") + + register_cvar("hlxce_plugin_version", VERSION, FCVAR_SPONLY|FCVAR_SERVER) + register_cvar("hlxce_version", "", FCVAR_SPONLY|FCVAR_SERVER) + register_cvar("hlxce_webpage", "http://www.hlxcommunity.com", FCVAR_SPONLY|FCVAR_SERVER) + g_hlx_block_commands = register_cvar("hlx_block_commands", "1") + g_hlx_message_prefix = register_cvar("hlx_message_prefix", "") + + // building the menus only once + g_HLstatsX_MainMenu = menu_create("HLstatsX - Main Menu", "mainmenu_handle") + menu_additem(g_HLstatsX_MainMenu, "Display Rank", "1") + menu_additem(g_HLstatsX_MainMenu, "Next Players", "2") + menu_additem(g_HLstatsX_MainMenu, "Top10 Players", "3") + menu_additem(g_HLstatsX_MainMenu, "Clans Ranking", "4") + menu_additem(g_HLstatsX_MainMenu, "Server Status", "5") + menu_additem(g_HLstatsX_MainMenu, "Statsme", "6") + menu_additem(g_HLstatsX_MainMenu, "Auto Ranking", "7") + menu_additem(g_HLstatsX_MainMenu, "Console Events", "8") + menu_additem(g_HLstatsX_MainMenu, "Weapon Usage", "9") + menu_additem(g_HLstatsX_MainMenu, "Weapons Accuracy", "10") + menu_additem(g_HLstatsX_MainMenu, "Weapons Targets", "11") + menu_additem(g_HLstatsX_MainMenu, "Player Kills", "12") + menu_additem(g_HLstatsX_MainMenu, "Toggle Ranking Display", "13") + menu_additem(g_HLstatsX_MainMenu, "VAC Cheaterlist", "14") + menu_additem(g_HLstatsX_MainMenu, "Display Help", "15") + menu_setprop(g_HLstatsX_MainMenu, MPROP_PERPAGE, 6) + + g_HLstatsX_AutoMenu = menu_create("HLstatsX - Auto-Ranking", "automenu_handle") + menu_additem(g_HLstatsX_AutoMenu, "Enable on round-start", "1") + menu_additem(g_HLstatsX_AutoMenu, "Enable on round-end", "2") + menu_additem(g_HLstatsX_AutoMenu, "Enable on player death", "3") + menu_additem(g_HLstatsX_AutoMenu, "Disable", "4") + menu_setprop(g_HLstatsX_AutoMenu, MPROP_PERPAGE, 0) + + g_HLstatsX_EventsMenu = menu_create("HLstatsX - Console Events", "eventsmenu_handle") + menu_additem(g_HLstatsX_EventsMenu, "Enable Events", "1") + menu_additem(g_HLstatsX_EventsMenu, "Disable Events", "2") + menu_additem(g_HLstatsX_EventsMenu, "Enable Global Chat", "3") + menu_additem(g_HLstatsX_EventsMenu, "Disable Global Chat", "4") + menu_setprop(g_HLstatsX_EventsMenu, MPROP_PERPAGE, 0) + + register_menucmd(register_menuid("Display Menu"), display_menu_keys, "handle_internal_menu") + + register_clcmd("say", "hlx_block_commands") + register_clcmd("say_team", "hlx_block_commands") + + allies_player_color = -1 + axis_player_color = -1 + find_player_team_slot("Allies") + find_player_team_slot("Axis") + + g_msgSayText = get_user_msgid("SayText") + +} + + +public log_player_event(client, verb[32], player_event[192], display_location) +{ + if ((client > 0) && (is_user_connected(client))) { + new player_userid = get_user_userid(client) + + static player_authid[32] + get_user_authid(client, player_authid, 31) + + static player_name[32] + get_user_name(client, player_name, 31) + + static player_team[16] + get_user_team(client, player_team, 15) + + if (display_location > 0) { + new player_origin[3] + get_user_origin (client, player_origin) + + format(logmessage_ignore, 511, "^"%s<%d><%s><%s>^" %s ^"%s^"", player_name, player_userid, player_authid, player_team, verb, player_event) + log_message("^"%s<%d><%s><%s>^" %s ^"%s^" (position ^"%d %d %d^")", player_name, player_userid, player_authid, player_team, verb, player_event, player_origin[0], player_origin[1], player_origin[2]) + } else { + log_message("^"%s<%d><%s><%s>^" %s ^"%s^"", player_name, player_userid, player_authid, player_team, verb, player_event) + } + } +} + + +public game_log_hook(AlertType: type, message[]) +{ + if (type != at_logged ) { + return FMRES_IGNORED + } + if ((strcmp("", logmessage_ignore) != 0) && (contain(message, logmessage_ignore) != -1)) { + if (contain(message, "position") == -1) { + logmessage_ignore = "" + return FMRES_SUPERCEDE + } + } + return FMRES_IGNORED +} + + +stock find_player_team_slot(team[16]) +{ + + new team_index = get_team_index(team) + if (team_index > -1) { + if (strcmp(team, "Allies") == 0) { + allies_player_color = -1 + } else if (strcmp(team, "Axis") == 0) { + axis_player_color = -1 + } + + new max_clients = get_maxplayers() + for(new i = 1; i <= max_clients; i++) { + new player_index = i + if (is_user_connected(player_index)) { + static player_team[16] + get_user_team(player_index, player_team, 15) + + if (strcmp(player_team, team) == 0) { + if (strcmp(team, "Allies") == 0) { + allies_player_color = player_index + if (axis_player_color == allies_player_color) { + allies_player_color = -1 + axis_player_color = -1 + } + break + } else if (strcmp(team, "Axis") == 0) { + axis_player_color = player_index + if (axis_player_color == allies_player_color) { + allies_player_color = -1 + axis_player_color = -1 + } + break + } + } + } + } + } +} + +public validate_team_colors() +{ + if (allies_player_color > -1) { + if (is_user_connected(allies_player_color)) { + static player_team[16] + get_user_team(allies_player_color, player_team, 15) + if (strcmp("Allies", player_team) != 0) { + allies_player_color = -1 + } + } else { + allies_player_color = -1 + } + } else if (axis_player_color > -1) { + if (is_user_connected(axis_player_color)) { + static player_team[16] + get_user_team(axis_player_color, player_team, 15) + if (strcmp("Axis", player_team) != 0) { + axis_player_color = -1 + } + } else { + axis_player_color = -1 + } + } + if ((allies_player_color == -1) || (axis_player_color == -1)) { + if (allies_player_color == -1) { + find_player_team_slot("Allies") + } + if (axis_player_color == -1) { + find_player_team_slot("Axis") + } + } +} + + +stock get_team_index(team_name[16]) +{ + if (strcmp(team_name, "Axis") == 0) { + return 0 + } else if (strcmp(team_name, "Allies") == 0) { + return 1 + } + return -1 +} + +stock remove_color_entities(message[192]) +{ + replace_all(message, 192, "x04", "") + replace_all(message, 192, "x03", "") + replace_all(message, 192, "x01", "") +} + +stock color_entities(message[192]) +{ + new replace_message[2] + replace_message[0] = 0x04 + replace_all(message, 192, "x04", replace_message) + replace_message[0] = 0x03 + replace_all(message, 192, "x03", replace_message) + replace_message[0] = 0x01 + replace_all(message, 192, "x01", replace_message) +} + +stock color_team_entities(message[192]) +{ + if (axis_player_color > -1) { + if (contain(message, "Axis") > -1) { + new replace_message[192] + replace_message[0] = 0x03 + format(replace_message[1], 191, "%s", "Axis") + replace_message[strlen("Axis") + 1] = 0x01 + replace_all(message, 192, "Axis", replace_message) + return axis_player_color + } + } + if (allies_player_color > -1) { + if (contain(message, "Allies") > -1) { + new replace_message[192] + replace_message[0] = 0x03 + format(replace_message[1], 191, "%s", "Allies") + replace_message[strlen("Allies") + 1] = 0x01 + replace_all(message, 192, "Allies", replace_message) + return allies_player_color + } + } + + return -1 +} + +stock color_player(color_type, player_index, client_message[192]) +{ + new color_player_index = -1 + static client_name[192] + get_user_name(player_index, client_name, 191) + if (color_type == 1) { + new colored_player_name[192] + colored_player_name[0] = 0x03 + format(colored_player_name[1], 191, "%s", client_name) + colored_player_name[strlen(client_name) + 1] = 0x01 + + if (contain(client_message, client_name) > -1) { + replace_all(client_message, 192, client_name, colored_player_name) + return player_index + } + } else { + new colored_player_name[192] + colored_player_name[0] = 0x04 + format(colored_player_name[1], 191, "%s", client_name) + colored_player_name[strlen(client_name) + 1] = 0x01 + + if (contain(client_message, client_name) > -1) { + replace_all(client_message, 192, client_name, colored_player_name) + } + } + return color_player_index +} + +public dod_client_changeteam(id, team, oldteam) +{ + if ((id > 0) && (is_user_connected(id))) { + if ((allies_player_color == -1) || (id == allies_player_color)) { + allies_player_color = -1 + } else if ((axis_player_color == -1) || (id == axis_player_color)) { + axis_player_color = -1 + } + } +} + +public client_disconnect(id) +{ + if ((id > 0) && (is_user_connected(id))) { + if ((allies_player_color == -1) || (id == allies_player_color)) { + allies_player_color = -1 + } else if ((axis_player_color == -1) || (id == axis_player_color)) { + axis_player_color = -1 + } + } +} + + +public client_death(killer, victim, wpnindex, hitplace, TK) +{ + new id = victim + + if ((id > 0) && (is_user_connected(id))) { + new iStats[9], iHits[8] + static szTeam[16], szName[32], szAuthid[32], szWeapon[24] + new iUserid = get_user_userid(id) + new _max = xmod_get_maxweapons() + + get_user_team(id, szTeam, 15) + get_user_name(id, szName, 31) + get_user_authid(id, szAuthid, 31) + + for (new i = 1; i < _max; ++i) { + if (get_user_wstats(id, i, iStats, iHits)) + { + xmod_get_wpnname(i, szWeapon, 23) + + log_message("^"%s<%d><%s><%s>^" triggered ^"weaponstats^" (weapon ^"%s^") (shots ^"%d^") (hits ^"%d^") (kills ^"%d^") (headshots ^"%d^") (tks ^"%d^") (damage ^"%d^") (deaths ^"%d^")", + szName, iUserid, szAuthid, szTeam, szWeapon, iStats[4], iStats[5], iStats[0], iStats[2], iStats[3], iStats[6], iStats[1]) + log_message("^"%s<%d><%s><%s>^" triggered ^"weaponstats2^" (weapon ^"%s^") (head ^"%d^") (chest ^"%d^") (stomach ^"%d^") (leftarm ^"%d^") (rightarm ^"%d^") (leftleg ^"%d^") (rightleg ^"%d^")", + szName, iUserid, szAuthid, szTeam, szWeapon, iHits[1], iHits[2], iHits[3], iHits[4], iHits[5], iHits[6], iHits[7]) + } + } + + reset_user_wstats(id) + } +} + + +stock ExplodeString( Output[][], Max, Size, Input[], Delimiter ) +{ + new Idx, l = strlen(Input), Len; + do Len += (1 + copyc( Output[Idx], Size, Input[Len], Delimiter )); + while( (Len < l) && (++Idx < Max) ) + return Idx; +} + +psay (client_id[], client_message[192]) +{ + new client = str_to_num(client_id) + + if (client > 0) { + new Players[32] + new player_count, temp_player_index, player_index + get_players(Players, player_count, "ch") + for (temp_player_index = 0; temp_player_index < player_count; temp_player_index++) { + new player = Players[temp_player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == client) { + player_index = player + break + } + } + + if ((player_index > 0) && (!is_user_bot(player_index)) && (is_user_connected(player_index))) { + + static display_message[192] + /* + if (is_colored > 0) { + if (is_message_cached(client_message) > 0) { + client_message = parsed_message_cache + color_index = cached_color_index + } else { + static client_message_backup[192] + copy(client_message_backup, 191, client_message) + + new player_color_index = color_all_players(client_message) + if (player_color_index > -1) { + color_index = player_color_index + } else { + validate_team_colors() + color_index = color_team_entities(client_message) + } + color_entities(client_message) + add_message_cache(client_message_backup, client_message, color_index) + } + } + */ + remove_color_entities(client_message) + static message_prefix[64] + get_pcvar_string(g_hlx_message_prefix, message_prefix, 64) + if (strcmp(message_prefix, "") == 0) { + format(display_message, 192, "%s", client_message) + } else { + format(display_message, 192, "%s %s", message_prefix, client_message) + } + + message_begin(MSG_ONE, g_msgSayText, {0,0,0}, player_index) + write_byte(player_index) + write_string(display_message) + message_end() + + } + } +} + +public hlx_amx_bulkpsay(id, level, cid) +{ + new argument_count = read_argc() + if (argument_count < 3) { + return PLUGIN_HANDLED + } + + new client_id_list[48] + read_argv(1, client_id_list, 47) + new client_ids[8][6]; + ExplodeString(client_ids, 7, 5, client_id_list, ','); + + static colored_param[32] + read_argv(2, colored_param, 31) + new ignore_param = 0 + if (strcmp(colored_param, "1") == 0) { + ignore_param = 1 + } + if (strcmp(colored_param, "0") == 0) { + ignore_param = 1 + } + + new client_message[192] + for(new i = (1 + ignore_param); i < argument_count; i++) { + static temp_argument[192] + read_argv(i + 1, temp_argument, 191) + if (i > (1 + ignore_param)) { + if ((191 - strlen(client_message)) > strlen(temp_argument)) { + if ((temp_argument[0] == 41) || (temp_argument[0] == 125)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } else if ((strlen(client_message) > 0) && (client_message[strlen(client_message)-1] != 40) && (client_message[strlen(client_message)-1] != 123) && (client_message[strlen(client_message)-1] != 58) && (client_message[strlen(client_message)-1] != 39) && (client_message[strlen(client_message)-1] != 44)) { + if ((strcmp(temp_argument, ":") != 0) && (strcmp(temp_argument, ",") != 0) && (strcmp(temp_argument, "'") != 0)) { + client_message[strlen(client_message)] = 32 + } + copy(client_message[strlen(client_message)], 191, temp_argument) + } else { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } else { + if ((192 - strlen(client_message)) > strlen(temp_argument)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } + + for (new i = 0; i < 8; i++) + { + psay(client_ids[i], client_message); + } + + return PLUGIN_HANDLED +} + +public hlx_amx_psay(id, level, cid) +{ + new argument_count = read_argc() + if (argument_count < 3) { + return PLUGIN_HANDLED + } + + static client_id[32] + read_argv(1, client_id, 31) + copy(client_id, 30, client_id[1]) + + static colored_param[32] + read_argv(2, colored_param, 31) + new ignore_param = 0 + if (strcmp(colored_param, "1") == 0) { + ignore_param = 1 + } + if (strcmp(colored_param, "0") == 0) { + ignore_param = 1 + } + + new client_message[192] + for(new i = (1 + ignore_param); i < argument_count; i++) { + static temp_argument[192] + read_argv(i + 1, temp_argument, 191) + if (i > (1 + ignore_param)) { + if ((191 - strlen(client_message)) > strlen(temp_argument)) { + if ((temp_argument[0] == 41) || (temp_argument[0] == 125)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } else if ((strlen(client_message) > 0) && (client_message[strlen(client_message)-1] != 40) && (client_message[strlen(client_message)-1] != 123) && (client_message[strlen(client_message)-1] != 58) && (client_message[strlen(client_message)-1] != 39) && (client_message[strlen(client_message)-1] != 44)) { + if ((strcmp(temp_argument, ":") != 0) && (strcmp(temp_argument, ",") != 0) && (strcmp(temp_argument, "'") != 0)) { + client_message[strlen(client_message)] = 32 + } + copy(client_message[strlen(client_message)], 191, temp_argument) + } else { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } else { + if ((192 - strlen(client_message)) > strlen(temp_argument)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } + + psay(client_id, client_message) + + return PLUGIN_HANDLED +} + +public hlx_amx_psay2(id, level, cid) +{ + new argument_count = read_argc() + if (argument_count < 3) { + return PLUGIN_HANDLED + } + + static client_id[32] + read_argv(1, client_id, 31) + copy(client_id, 30, client_id[1]) + + static colored_param[32] + read_argv(2, colored_param, 31) + new ignore_param = 0 + if (strcmp(colored_param, "1") == 0) { + ignore_param = 1 + } + if (strcmp(colored_param, "0") == 0) { + ignore_param = 1 + } + + new client_message[192] + for(new i = (1 + ignore_param); i < argument_count; i++) { + static temp_argument[192] + read_argv(i + 1, temp_argument, 191) + if (i > (1 + ignore_param)) { + if ((191 - strlen(client_message)) > strlen(temp_argument)) { + if ((temp_argument[0] == 41) || (temp_argument[0] == 125)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } else if ((strlen(client_message) > 0) && (client_message[strlen(client_message)-1] != 40) && (client_message[strlen(client_message)-1] != 123) && (client_message[strlen(client_message)-1] != 58) && (client_message[strlen(client_message)-1] != 39) && (client_message[strlen(client_message)-1] != 44)) { + if ((strcmp(temp_argument, ":") != 0) && (strcmp(temp_argument, ",") != 0) && (strcmp(temp_argument, "'") != 0)) { + client_message[strlen(client_message)] = 32 + } + copy(client_message[strlen(client_message)], 191, temp_argument) + } else { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } else { + if ((192 - strlen(client_message)) > strlen(temp_argument)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } + + new client = str_to_num(client_id) + + if (client > 0) { + new Players[32] + new player_count, temp_player_index, player_index + get_players(Players, player_count, "ch") + for (temp_player_index = 0; temp_player_index < player_count; temp_player_index++) { + new player = Players[temp_player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == client) { + player_index = player + break + } + } + + if ((player_index > 0) && (!is_user_bot(player_index)) && (is_user_connected(player_index))) { + + static display_message[192] + static message_prefix[64] + get_pcvar_string(g_hlx_message_prefix, message_prefix, 64) + if (strcmp(message_prefix, "") == 0) { + format(display_message, 192, "%s", client_message) + } else { + format(display_message, 192, "%s %s", message_prefix, client_message) + } + + message_begin(MSG_ONE, g_msgSayText, {0,0,0}, player_index) + write_byte(player_index) + write_string(display_message) + message_end() + } + } + + return PLUGIN_HANDLED +} + +public hlx_amx_say(id, level, cid) +{ + if (!cmd_access(id, level, cid, 2)) + return PLUGIN_HANDLED + + static message[192] + read_args(message, 191) + remove_quotes(message) + + static message_prefix[64] + get_pcvar_string(g_hlx_message_prefix, message_prefix, 64) + if (strcmp(message_prefix, "") == 0) { + client_print(0, print_chat, "%s", message) + } else { + client_print(0, print_chat, "%s %s", message_prefix, message) + } + + return PLUGIN_HANDLED +} + + +public hlx_amx_csay(id, level, cid) +{ + if (!cmd_access(id, level, cid, 2)) + return PLUGIN_HANDLED + + static message[192] + read_args(message, 191) + remove_quotes(message) + + new color3[0][] = {255, 255, 255} + new Float:verpos = 0.3 + + set_hudmessage(color3[0][0], color3[0][1], color3[0][2], -1.0, verpos, 0, 6.0, 6.0, 0.5, 0.15, -1) + show_hudmessage(0, "%s", message) + + return PLUGIN_HANDLED +} + + +public hlx_amx_hint(id, level, cid) +{ + if (!cmd_access(id, level, cid, 2)) + return PLUGIN_HANDLED + + new argument_count = read_argc() + + static client_id[32] + read_argv(1, client_id, 31) + copy(client_id, 30, client_id[1]) + + new client_message[192] + for(new i = 1; i < argument_count; i++) { + static temp_argument[192] + read_argv(i + 1, temp_argument, 191) + if (i > 1) { + if ((191 - strlen(client_message)) > strlen(temp_argument)) { + if ((temp_argument[0] == 41) || (temp_argument[0] == 125)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } else if ((strlen(client_message) > 0) && (client_message[strlen(client_message)-1] != 40) && (client_message[strlen(client_message)-1] != 123) && (client_message[strlen(client_message)-1] != 58) && (client_message[strlen(client_message)-1] != 39) && (client_message[strlen(client_message)-1] != 44)) { + if ((strcmp(temp_argument, ":") != 0) && (strcmp(temp_argument, ",") != 0) && (strcmp(temp_argument, "'") != 0)) { + client_message[strlen(client_message)] = 32 + } + copy(client_message[strlen(client_message)], 191, temp_argument) + } else { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } else { + if ((192 - strlen(client_message)) > strlen(temp_argument)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } + + new client = str_to_num(client_id) + + if (client > 0) { + new Players[32] + new player_count, temp_player_index, player_index + get_players(Players, player_count, "ch") + for (temp_player_index = 0; temp_player_index < player_count; temp_player_index++) { + new player = Players[temp_player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == client) { + player_index = player + break + } + } + + if ((player_index > 0) && (!is_user_bot(player_index)) && (is_user_connected(player_index))) { + new color3[0][] = {255, 128, 0} + new Float:verpos = 0.80 + + set_hudmessage(color3[0][0], color3[0][1], color3[0][2], -1.0, verpos, 0, 6.0, 6.0, 0.5, 0.15, -1) + show_hudmessage(player_index, "%s", client_message) + } + } + + return PLUGIN_HANDLED +} + + +public hlx_amx_msay(id, level, cid) +{ + if (!cmd_access(id, level, cid, 3)) + return PLUGIN_HANDLED + + static delay[8] + read_argv(1, delay, 7) + remove_quotes(delay) + + static name[32] + read_argv(2, name, 31) + copy(name, 30, name[1]) + new raw_user_id = str_to_num(name) + + static handler_param[32] + read_argv(3, handler_param, 31) + new ignore_param = 0 + new need_handler = 0 + if (strcmp(handler_param, "1") == 0) { + need_handler = 1 + ignore_param = 1 + } + if (strcmp(handler_param, "0") == 0) { + need_handler = 0 + ignore_param = 1 + } + + static message[1024] + new userid + new Players[32] + new player_count, player_index + get_players(Players, player_count, "ch") + for (player_index = 0; player_index < player_count; player_index++) { + new player = Players[player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == raw_user_id) { + userid = player + break + } + } + + read_args(message, 1023) + + new find_pattern[] = "#" + new find_pos = strfind(message, find_pattern) + new text_pos = find_pos + strlen(name) + 2 + if (ignore_param == 1) { + text_pos += 3 + } + + static menu_text[1024] + copy(menu_text, 1023, message[text_pos]) + remove_quotes(menu_text) + + new menu_display[1024] + + new i, start = 0 + new nLen = 0 + new buffer[1024] + + for(i = 0; i < strlen(menu_text); i++) { + if (i > 0) { + if ((menu_text[i-1] == '\') && (menu_text[i] == 'n')) { + buffer = "" + copy(buffer, (i - start)-1 , menu_text[start]) + nLen += format(menu_display[nLen], (1023 - nLen), "%s^n", buffer) + i += 1 + start = i + } + } + } + + if ((userid > 0) && (!is_user_bot(userid)) && (is_user_connected(userid))) { + if (need_handler == 0) { + show_menu(userid, display_menu_keys, menu_display, 15) + } else { + show_menu(userid, display_menu_keys, menu_display, 15, "Display Menu") + } + } + + return PLUGIN_HANDLED + +} + + +public handle_internal_menu(id, key) +{ + new client = id + if (is_user_connected(client)) { + if (key < 9) { + static player_event[192] + new slot = key + slot++ + num_to_str(slot, player_event, 192) + log_player_event(client, "selected", player_event, 0) + } else { + new player_event[192] = "cancel" + log_player_event(client, "selected", player_event, 0) + } + } +} + + +public hlx_amx_browse(id, level, cid) +{ + if (!cmd_access(id, level, cid, 2)) + return PLUGIN_HANDLED + + static name[32] + read_argv(1, name, 31) + copy(name, 30, name[1]) + new raw_user_id = str_to_num(name) + + static message[1024] + new userid + new Players[32] + new player_count, player_index + get_players(Players, player_count, "ch") + for (player_index = 0; player_index < player_count; player_index++) { + new player = Players[player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == raw_user_id) { + userid = player + break + } + } + + read_args(message, 1023) + + new find_pattern[] = "#" + new find_pos = strfind(message, find_pattern) + + static url[1024] + copy(url, 1023, message[find_pos + strlen(name) + 2]) + remove_quotes(url) + + if ((userid > 0) && (!is_user_bot(userid)) && (is_user_connected(userid))) { + show_motd(userid, url, "HLstatsX CE") + } + + return PLUGIN_HANDLED +} + +public hlx_amx_swap(id, level, cid) +{ + if (!cmd_access(id, level, cid, 1)) + return PLUGIN_HANDLED + + static client_id[32] + read_argv(1, client_id, 31) + copy(client_id, 30, client_id[1]) + new client = str_to_num(client_id) + + if (client > 0) { + new userid = 0 + new Players[32] + new player_count, player_index + get_players(Players, player_count, "h") + for (player_index = 0; player_index < player_count; player_index++) { + new player = Players[player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == client) { + userid = player + break + } + } + + static player_team[32] + get_user_team(userid, player_team, 31) + + + if ((userid > 0) && (is_user_connected(userid))) { + + if (strcmp(player_team, "Axis") == 0) { + dod_set_user_team(userid, ALLIES) + + if ((allies_player_color == -1) || (userid == allies_player_color)) { + allies_player_color = -1 + } else if ((axis_player_color == -1) || (userid == axis_player_color)) { + axis_player_color = -1 + } + } else if (strcmp(player_team, "Allies") == 0) { + dod_set_user_team(userid, AXIS) + + if ((allies_player_color == -1) || (userid == allies_player_color)) { + allies_player_color = -1 + } else if ((axis_player_color == -1) || (userid == axis_player_color)) { + axis_player_color = -1 + } + } + } + } + return PLUGIN_HANDLED +} + +stock make_player_command(client, player_command[192]) +{ + if (client > 0) { + log_player_event(client, "say", player_command, 0) + } +} + +public display_menu(menu, id) +{ + menu_display(id, menu, 0) +} + +public display_mainmenu(id) +{ + display_menu(g_HLstatsX_MainMenu, id) + return PLUGIN_HANDLED +} + +public display_automenu(id) +{ + display_menu(g_HLstatsX_AutoMenu, id) + return PLUGIN_HANDLED +} + +public display_eventsmenu(id) +{ + display_menu(g_HLstatsX_EventsMenu, id) + return PLUGIN_HANDLED +} + +public mainmenu_handle(id, menu, item) +{ + if (item < 0) { + return PLUGIN_CONTINUE + } + + static command[16], name[64] + new access, callback + menu_item_getinfo(menu, item, access, command, 15, name, 63, callback) + + new choice = str_to_num(command) + switch (choice) { + case 1 : + make_player_command(id, "/rank") + case 2 : + make_player_command(id, "/next") + case 3 : + make_player_command(id, "/top10") + case 4 : + make_player_command(id, "/clans") + case 5 : + make_player_command(id, "/status") + case 6 : + make_player_command(id, "/statsme") + case 7 : + display_automenu(id) + case 8 : + display_eventsmenu(id) + case 9 : + make_player_command(id, "/weapons") + case 10 : + make_player_command(id, "/accuracy") + case 11 : + make_player_command(id, "/targets") + case 12 : + make_player_command(id, "/kills") + case 13 : + make_player_command(id, "/hlx_hideranking") + case 14 : + make_player_command(id, "/cheaters") + case 15 : + make_player_command(id, "/help") + } + + return PLUGIN_HANDLED +} + +public automenu_handle(id, menu, item) +{ + if (item < 0) { + return PLUGIN_CONTINUE + } + + static command[16], name[64] + new access, callback + menu_item_getinfo(menu, item, access, command, 15, name, 63, callback) + + new choice = str_to_num(command) + switch (choice) { + case 1 : + make_player_command(id, "/hlx_auto start rank") + case 2 : + make_player_command(id, "/hlx_auto end rank") + case 3 : + make_player_command(id, "/hlx_auto kill rank") + case 4 : + make_player_command(id, "/hlx_auto clear") + } + + return PLUGIN_HANDLED +} + +public eventsmenu_handle(id, menu, item) +{ + if (item < 0) { + return PLUGIN_CONTINUE + } + + static command[16], name[64] + new access, callback + menu_item_getinfo(menu, item, access, command, 15, name, 63, callback) + + new choice = str_to_num(command) + switch (choice) { + case 1 : + make_player_command(id, "/hlx_display 1") + case 2 : + make_player_command(id, "/hlx_display 0") + case 3 : + make_player_command(id, "/hlx_chat 1") + case 4 : + make_player_command(id, "/hlx_chat 0") + } + + return PLUGIN_HANDLED +} + +stock is_command_blocked(command[192]) +{ + new command_blocked = 0 + new command_index = 0 + while ((command_blocked == 0) && (command_index < sizeof(blocked_commands))) { + if (strcmp(command, blocked_commands[command_index]) == 0) { + command_blocked++ + } + command_index++ + } + if (command_blocked > 0) { + return 1 + } + return 0 +} + + +public hlx_block_commands(client) +{ + if (client) { + if (client == 0) { + return PLUGIN_CONTINUE + } + + new block_chat_commands = get_pcvar_num(g_hlx_block_commands) + + static user_command[192] + read_args(user_command, 192) + static origin_command[192] + + new start_index = 0 + new command_length = strlen(user_command) + if (command_length > 0) { + if (user_command[start_index] == 34) { + start_index = start_index + 1 + if (user_command[command_length - 1] == 34) { + user_command[command_length - 1] = 0 + } + } + + if (user_command[start_index] == 47) { + start_index++ + } + + copy(origin_command, 192, user_command[start_index]) + } + + if (command_length > 0) { + if (block_chat_commands > 0) { + + new command_type[32] = "say" + new command_blocked = is_command_blocked(origin_command) + + if (command_blocked > 0) { + + if (is_user_connected(client)) { + if ((strcmp("hlx_menu", user_command[start_index]) == 0) || + (strcmp("hlx", user_command[start_index]) == 0) || + (strcmp("hlstatsx", user_command[start_index]) == 0)) { + + display_mainmenu(client) + } + log_player_event(client, command_type, origin_command, 0) + } + return PLUGIN_HANDLED + } + } else { + if (is_user_connected(client)) { + if ((strcmp("hlx_menu", user_command[start_index]) == 0) || + (strcmp("hlx", user_command[start_index]) == 0) || + (strcmp("hlstatsx", user_command[start_index]) == 0)) { + display_mainmenu(client) + } + } + return PLUGIN_CONTINUE + } + } + } + + return PLUGIN_CONTINUE +} \ No newline at end of file diff --git a/amxmodx/scripting/hlstatsx_commands_ns.sma b/amxmodx/scripting/hlstatsx_commands_ns.sma new file mode 100644 index 0000000..e1a2bb1 --- /dev/null +++ b/amxmodx/scripting/hlstatsx_commands_ns.sma @@ -0,0 +1,872 @@ +/** + * 2008 - Modified by Nicholas Hastings (psychonic) for used with HLstatsX Community Edition + * http://www.hlxcommunity.com + * + * HLstatsX - AMX Mod X plugin to display ingame messages + * http://www.hlstatsx.com/ + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma dynamic 16000 + +#include +#include + +#include +#include + +#include +#include + +#define VERSION "1.6.19 (HL1)" + +new g_hlx_block_commands +new g_hlx_message_prefix +new blocked_commands[][] = { "rank", "skill", "points", "place", "session", "session_data", + "kpd", "kdratio", "kdeath", "next", "load", "status", "servers", + "top20", "top10", "top5", "clans", "cheaters", "statsme", "weapons", + "weapon", "action", "actions", "accuracy", "targets", "target", "kills", + "kill", "player_kills", "cmd", "cmds", "command", "hlx_display 0", + "hlx_display 1", "hlx_teams 0", "hlx_teams 1", "hlx_hideranking", + "hlx_chat 0", "hlx_chat 1", "hlx_menu", "servers 1", "servers 2", + "servers 3", "hlx", "hlstatsx", "help" } + + +new g_msgSayText +new g_HLstatsX_MainMenu +new g_HLstatsX_AutoMenu +new g_HLstatsX_EventsMenu + +new logmessage_ignore[512] +new display_menu_keys = MENU_KEY_0|MENU_KEY_1|MENU_KEY_2|MENU_KEY_3|MENU_KEY_4|MENU_KEY_5|MENU_KEY_6|MENU_KEY_7|MENU_KEY_8|MENU_KEY_9 + + +public plugin_init() +{ + register_plugin("HLstatsX CE Ingame Plugin", VERSION, "psychonic") + register_srvcmd("hlx_amx_psay", "hlx_amx_psay", 0, " - sends private message") + register_srvcmd("hlx_amx_psay2", "hlx_amx_psay2", 0, " - sends green colored private message") + register_srvcmd("hlx_amx_bulkpsay", "hlx_amx_bulkpsay", 0, " - sends private message to many") + register_srvcmd("hlx_amx_say", "hlx_amx_say", 0, " - sends message to all players") + register_srvcmd("hlx_amx_csay", "hlx_amx_csay", 0, " - sends center hud message to all players") + register_srvcmd("hlx_amx_hint", "hlx_amx_hint", 0, " - sends hint message") + register_srvcmd("hlx_amx_msay", "hlx_amx_msay", 0, " - displays advanced information") + register_srvcmd("hlx_amx_browse", "hlx_amx_browse", 0, " - displays internal browser") + register_srvcmd("hlx_amx_swap", "hlx_amx_swap", 0, " - swaps players to the opposite team") + + register_cvar("hlxce_plugin_version", VERSION, FCVAR_SPONLY|FCVAR_SERVER) + register_cvar("hlxce_version", "", FCVAR_SPONLY|FCVAR_SERVER) + register_cvar("hlxce_webpage", "http://www.hlxcommunity.com", FCVAR_SPONLY|FCVAR_SERVER) + g_hlx_block_commands = register_cvar("hlx_block_commands", "1") + g_hlx_message_prefix = register_cvar("hlx_message_prefix", "") + + // building the menus only once + g_HLstatsX_MainMenu = menu_create("HLstatsX - Main Menu", "mainmenu_handle") + menu_additem(g_HLstatsX_MainMenu, "Display Rank", "1") + menu_additem(g_HLstatsX_MainMenu, "Next Players", "2") + menu_additem(g_HLstatsX_MainMenu, "Top10 Players", "3") + menu_additem(g_HLstatsX_MainMenu, "Auto Ranking", "7") + menu_additem(g_HLstatsX_MainMenu, "Console Events", "8") + menu_setprop(g_HLstatsX_MainMenu, MPROP_PERPAGE, 6) + + g_HLstatsX_AutoMenu = menu_create("HLstatsX - Auto-Ranking", "automenu_handle") + menu_additem(g_HLstatsX_AutoMenu, "Enable on round-start", "1") + menu_additem(g_HLstatsX_AutoMenu, "Enable on round-end", "2") + menu_additem(g_HLstatsX_AutoMenu, "Enable on player death", "3") + menu_additem(g_HLstatsX_AutoMenu, "Disable", "4") + menu_setprop(g_HLstatsX_AutoMenu, MPROP_PERPAGE, 0) + + g_HLstatsX_EventsMenu = menu_create("HLstatsX - Console Events", "eventsmenu_handle") + menu_additem(g_HLstatsX_EventsMenu, "Enable Events", "1") + menu_additem(g_HLstatsX_EventsMenu, "Disable Events", "2") + menu_additem(g_HLstatsX_EventsMenu, "Enable Global Chat", "3") + menu_additem(g_HLstatsX_EventsMenu, "Disable Global Chat", "4") + menu_setprop(g_HLstatsX_EventsMenu, MPROP_PERPAGE, 0) + + register_menucmd(register_menuid("Display Menu"), display_menu_keys, "handle_internal_menu") + + register_clcmd("say", "hlx_block_commands") + register_clcmd("say_team", "hlx_block_commands") + + g_msgSayText = get_user_msgid("SayText") + +} + + +public log_player_event(client, verb[32], player_event[192], display_location) +{ + if ((client > 0) && (is_user_connected(client))) { + new player_userid = get_user_userid(client) + + static player_authid[32] + get_user_authid(client, player_authid, 31) + + static player_name[32] + get_user_name(client, player_name, 31) + + static player_team[16] + get_user_team(client, player_team, 15) + + if (display_location > 0) { + new player_origin[3] + get_user_origin (client, player_origin) + + format(logmessage_ignore, 511, "^"%s<%d><%s><%s>^" %s ^"%s^"", player_name, player_userid, player_authid, player_team, verb, player_event) + log_message("^"%s<%d><%s><%s>^" %s ^"%s^" (position ^"%d %d %d^")", player_name, player_userid, player_authid, player_team, verb, player_event, player_origin[0], player_origin[1], player_origin[2]) + } else { + log_message("^"%s<%d><%s><%s>^" %s ^"%s^"", player_name, player_userid, player_authid, player_team, verb, player_event) + } + } +} + + +public game_log_hook(AlertType: type, message[]) +{ + if (type != at_logged ) { + return FMRES_IGNORED + } + if ((strcmp("", logmessage_ignore) != 0) && (contain(message, logmessage_ignore) != -1)) { + if (contain(message, "position") == -1) { + logmessage_ignore = "" + return FMRES_SUPERCEDE + } + } + return FMRES_IGNORED +} + +stock ExplodeString( Output[][], Max, Size, Input[], Delimiter ) +{ + new Idx, l = strlen(Input), Len; + do Len += (1 + copyc( Output[Idx], Size, Input[Len], Delimiter )); + while( (Len < l) && (++Idx < Max) ) + return Idx; +} + +psay (client_id[], client_message[192]) +{ + new client = str_to_num(client_id) + + if (client > 0) { + new Players[32] + new player_count, temp_player_index, player_index + get_players(Players, player_count, "ch") + for (temp_player_index = 0; temp_player_index < player_count; temp_player_index++) { + new player = Players[temp_player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == client) { + player_index = player + break + } + } + + if ((player_index > 0) && (!is_user_bot(player_index)) && (is_user_connected(player_index))) { + + static display_message[192] + static message_prefix[64] + get_pcvar_string(g_hlx_message_prefix, message_prefix, 64) + if (strcmp(message_prefix, "") == 0) { + format(display_message, 192, "%s", client_message) + } else { + format(display_message, 192, "%s %s", message_prefix, client_message) + } + + message_begin(MSG_ONE, g_msgSayText, {0,0,0}, player_index) + write_byte(player_index) + write_string(display_message) + message_end() + } + } +} + +public hlx_amx_bulkpsay(id, level, cid) +{ + new argument_count = read_argc() + if (argument_count < 3) { + return PLUGIN_HANDLED + } + + new client_id_list[48] + read_argv(1, client_id_list, 47) + new client_ids[8][6]; + ExplodeString(client_ids, 7, 5, client_id_list, ','); + + static colored_param[32] + read_argv(2, colored_param, 31) + + new ignore_param = 0 + if (strcmp(colored_param, "1") == 0) { + ignore_param = 1 + } + if (strcmp(colored_param, "0") == 0) { + ignore_param = 1 + } + + new client_message[192] + for(new i = (1 + ignore_param); i < argument_count; i++) { + static temp_argument[192] + read_argv(i + 1, temp_argument, 191) + if (i > (1 + ignore_param)) { + if ((191 - strlen(client_message)) > strlen(temp_argument)) { + if ((temp_argument[0] == 41) || (temp_argument[0] == 125)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } else if ((strlen(client_message) > 0) && (client_message[strlen(client_message)-1] != 40) && (client_message[strlen(client_message)-1] != 123) && (client_message[strlen(client_message)-1] != 58) && (client_message[strlen(client_message)-1] != 39) && (client_message[strlen(client_message)-1] != 44)) { + if ((strcmp(temp_argument, ":") != 0) && (strcmp(temp_argument, ",") != 0) && (strcmp(temp_argument, "'") != 0)) { + client_message[strlen(client_message)] = 32 + } + copy(client_message[strlen(client_message)], 191, temp_argument) + } else { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } else { + if ((192 - strlen(client_message)) > strlen(temp_argument)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } + + for (new i = 0; i < 8; i++) + { + psay(client_ids[i], client_message); + } + + return PLUGIN_HANDLED +} + +public hlx_amx_psay(id, level, cid) +{ + new argument_count = read_argc() + if (argument_count < 3) { + return PLUGIN_HANDLED + } + + static client_id[32] + read_argv(1, client_id, 31) + copy(client_id, 30, client_id[1]) + + static colored_param[32] + read_argv(2, colored_param, 31) + + new ignore_param = 0 + if (strcmp(colored_param, "1") == 0) { + ignore_param = 1 + } + if (strcmp(colored_param, "0") == 0) { + ignore_param = 1 + } + + new client_message[192] + for(new i = (1 + ignore_param); i < argument_count; i++) { + static temp_argument[192] + read_argv(i + 1, temp_argument, 191) + if (i > (1 + ignore_param)) { + if ((191 - strlen(client_message)) > strlen(temp_argument)) { + if ((temp_argument[0] == 41) || (temp_argument[0] == 125)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } else if ((strlen(client_message) > 0) && (client_message[strlen(client_message)-1] != 40) && (client_message[strlen(client_message)-1] != 123) && (client_message[strlen(client_message)-1] != 58) && (client_message[strlen(client_message)-1] != 39) && (client_message[strlen(client_message)-1] != 44)) { + if ((strcmp(temp_argument, ":") != 0) && (strcmp(temp_argument, ",") != 0) && (strcmp(temp_argument, "'") != 0)) { + client_message[strlen(client_message)] = 32 + } + copy(client_message[strlen(client_message)], 191, temp_argument) + } else { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } else { + if ((192 - strlen(client_message)) > strlen(temp_argument)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } + + psay(client_id, client_message) + + return PLUGIN_HANDLED +} + +public hlx_amx_psay2(id, level, cid) +{ + new argument_count = read_argc() + if (argument_count < 3) { + return PLUGIN_HANDLED + } + + static client_id[32] + read_argv(1, client_id, 31) + copy(client_id, 30, client_id[1]) + + static colored_param[32] + read_argv(2, colored_param, 31) + new ignore_param = 0 + if (strcmp(colored_param, "1") == 0) { + ignore_param = 1 + } + if (strcmp(colored_param, "0") == 0) { + ignore_param = 1 + } + + new client_message[192] + for(new i = (1 + ignore_param); i < argument_count; i++) { + static temp_argument[192] + read_argv(i + 1, temp_argument, 191) + if (i > (1 + ignore_param)) { + if ((191 - strlen(client_message)) > strlen(temp_argument)) { + if ((temp_argument[0] == 41) || (temp_argument[0] == 125)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } else if ((strlen(client_message) > 0) && (client_message[strlen(client_message)-1] != 40) && (client_message[strlen(client_message)-1] != 123) && (client_message[strlen(client_message)-1] != 58) && (client_message[strlen(client_message)-1] != 39) && (client_message[strlen(client_message)-1] != 44)) { + if ((strcmp(temp_argument, ":") != 0) && (strcmp(temp_argument, ",") != 0) && (strcmp(temp_argument, "'") != 0)) { + client_message[strlen(client_message)] = 32 + } + copy(client_message[strlen(client_message)], 191, temp_argument) + } else { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } else { + if ((192 - strlen(client_message)) > strlen(temp_argument)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } + + new client = str_to_num(client_id) + + if (client > 0) { + new Players[32] + new player_count, temp_player_index, player_index + get_players(Players, player_count, "ch") + for (temp_player_index = 0; temp_player_index < player_count; temp_player_index++) { + new player = Players[temp_player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == client) { + player_index = player + break + } + } + + if ((player_index > 0) && (!is_user_bot(player_index)) && (is_user_connected(player_index))) { + new color_index = player_index + + static display_message[192] + static message_prefix[64] + get_pcvar_string(g_hlx_message_prefix, message_prefix, 64) + if (strcmp(message_prefix, "") == 0) { + format(display_message, 192, "%s", client_message) + } else { + format(display_message, 192, "%s %s", message_prefix, client_message) + } + + message_begin(MSG_ONE, g_msgSayText, {0,0,0}, player_index) + write_byte(color_index) + write_string(display_message) + message_end() + } + } + + return PLUGIN_HANDLED +} + +public hlx_amx_say(id, level, cid) +{ + if (!cmd_access(id, level, cid, 2)) + return PLUGIN_HANDLED + + static message[192] + read_args(message, 191) + remove_quotes(message) + + static message_prefix[64] + get_pcvar_string(g_hlx_message_prefix, message_prefix, 64) + if (strcmp(message_prefix, "") == 0) { + client_print(0, print_chat, "%s", message) + } else { + client_print(0, print_chat, "%s %s", message_prefix, message) + } + + return PLUGIN_HANDLED +} + + +public hlx_amx_csay(id, level, cid) +{ + if (!cmd_access(id, level, cid, 2)) + return PLUGIN_HANDLED + + static message[192] + read_args(message, 191) + remove_quotes(message) + + new color3[0][] = {255, 255, 255} + new Float:verpos = 0.3 + + set_hudmessage(color3[0][0], color3[0][1], color3[0][2], -1.0, verpos, 0, 6.0, 6.0, 0.5, 0.15, -1) + show_hudmessage(0, "%s", message) + + return PLUGIN_HANDLED +} + + +public hlx_amx_hint(id, level, cid) +{ + if (!cmd_access(id, level, cid, 2)) + return PLUGIN_HANDLED + + new argument_count = read_argc() + + static client_id[32] + read_argv(1, client_id, 31) + copy(client_id, 30, client_id[1]) + + new client_message[192] + for(new i = 1; i < argument_count; i++) { + static temp_argument[192] + read_argv(i + 1, temp_argument, 191) + if (i > 1) { + if ((191 - strlen(client_message)) > strlen(temp_argument)) { + if ((temp_argument[0] == 41) || (temp_argument[0] == 125)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } else if ((strlen(client_message) > 0) && (client_message[strlen(client_message)-1] != 40) && (client_message[strlen(client_message)-1] != 123) && (client_message[strlen(client_message)-1] != 58) && (client_message[strlen(client_message)-1] != 39) && (client_message[strlen(client_message)-1] != 44)) { + if ((strcmp(temp_argument, ":") != 0) && (strcmp(temp_argument, ",") != 0) && (strcmp(temp_argument, "'") != 0)) { + client_message[strlen(client_message)] = 32 + } + copy(client_message[strlen(client_message)], 191, temp_argument) + } else { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } else { + if ((192 - strlen(client_message)) > strlen(temp_argument)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } + + new client = str_to_num(client_id) + + if (client > 0) { + new Players[32] + new player_count, temp_player_index, player_index + get_players(Players, player_count, "ch") + for (temp_player_index = 0; temp_player_index < player_count; temp_player_index++) { + new player = Players[temp_player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == client) { + player_index = player + break + } + } + + if ((player_index > 0) && (!is_user_bot(player_index)) && (is_user_connected(player_index))) { + new color3[0][] = {255, 128, 0} + new Float:verpos = 0.80 + + set_hudmessage(color3[0][0], color3[0][1], color3[0][2], -1.0, verpos, 0, 6.0, 6.0, 0.5, 0.15, -1) + show_hudmessage(player_index, "%s", client_message) + } + } + + return PLUGIN_HANDLED +} + + +public hlx_amx_msay(id, level, cid) +{ + if (!cmd_access(id, level, cid, 3)) + return PLUGIN_HANDLED + + static delay[8] + read_argv(1, delay, 7) + remove_quotes(delay) + + static name[32] + read_argv(2, name, 31) + copy(name, 30, name[1]) + new raw_user_id = str_to_num(name) + + static handler_param[32] + read_argv(3, handler_param, 31) + new ignore_param = 0 + new need_handler = 0 + if (strcmp(handler_param, "1") == 0) { + need_handler = 1 + ignore_param = 1 + } + if (strcmp(handler_param, "0") == 0) { + need_handler = 0 + ignore_param = 1 + } + + static message[1024] + new userid + new Players[32] + new player_count, player_index + get_players(Players, player_count, "ch") + for (player_index = 0; player_index < player_count; player_index++) { + new player = Players[player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == raw_user_id) { + userid = player + break + } + } + + read_args(message, 1023) + + new find_pattern[] = "#" + new find_pos = strfind(message, find_pattern) + new text_pos = find_pos + strlen(name) + 2 + if (ignore_param == 1) { + text_pos += 3 + } + + static menu_text[1024] + copy(menu_text, 1023, message[text_pos]) + remove_quotes(menu_text) + + new menu_display[1024] + + new i, start = 0 + new nLen = 0 + new buffer[1024] + + for(i = 0; i < strlen(menu_text); i++) { + if (i > 0) { + if ((menu_text[i-1] == '\') && (menu_text[i] == 'n')) { + buffer = "" + copy(buffer, (i - start)-1 , menu_text[start]) + nLen += format(menu_display[nLen], (1023 - nLen), "%s^n", buffer) + i += 1 + start = i + } + } + } + + if ((userid > 0) && (!is_user_bot(userid)) && (is_user_connected(userid))) { + if (need_handler == 0) { + show_menu(userid, display_menu_keys, menu_display, 15) + } else { + show_menu(userid, display_menu_keys, menu_display, 15, "Display Menu") + } + } + + return PLUGIN_HANDLED + +} + + +public handle_internal_menu(id, key) +{ + new client = id + if (is_user_connected(client)) { + if (key < 9) { + static player_event[192] + new slot = key + slot++ + num_to_str(slot, player_event, 192) + log_player_event(client, "selected", player_event, 0) + } else { + new player_event[192] = "cancel" + log_player_event(client, "selected", player_event, 0) + } + } +} + + +public hlx_amx_browse(id, level, cid) +{ + if (!cmd_access(id, level, cid, 2)) + return PLUGIN_HANDLED + + static name[32] + read_argv(1, name, 31) + copy(name, 30, name[1]) + new raw_user_id = str_to_num(name) + + static message[160] + new userid + new Players[32] + new player_count, player_index + get_players(Players, player_count, "ch") + for (player_index = 0; player_index < player_count; player_index++) { + new player = Players[player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == raw_user_id) { + userid = player + break + } + } + + read_args(message, 159) + + new find_pattern[] = "#" + new find_pos = strfind(message, find_pattern) + + static url[160] + copy(url, 159, message[find_pos + strlen(name) + 2]) + remove_quotes(url) + + if ((userid > 0) && (!is_user_bot(userid)) && (is_user_connected(userid))) { + show_motd(userid, url, "HLstatsX CE") + } + + return PLUGIN_HANDLED +} + +public hlx_amx_swap(id, level, cid) +{ + if (!cmd_access(id, level, cid, 1)) + return PLUGIN_HANDLED + + static client_id[32] + read_argv(1, client_id, 31) + copy(client_id, 30, client_id[1]) + new client = str_to_num(client_id) + + if (client > 0) { + new userid = 0 + new Players[32] + new player_count, player_index + get_players(Players, player_count, "h") + for (player_index = 0; player_index < player_count; player_index++) { + new player = Players[player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == client) { + userid = player + break + } + } + + static player_team[32] + get_user_team(userid, player_team, 31) + + if ((userid > 0) && (is_user_connected(userid))) { + + } + } + return PLUGIN_HANDLED +} + +stock make_player_command(client, player_command[192]) +{ + if (client > 0) { + log_player_event(client, "say", player_command, 0) + } +} + +public display_menu(menu, id) +{ + menu_display(id, menu, 0) +} + +public display_mainmenu(id) +{ + display_menu(g_HLstatsX_MainMenu, id) + return PLUGIN_HANDLED +} + +public display_automenu(id) +{ + display_menu(g_HLstatsX_AutoMenu, id) + return PLUGIN_HANDLED +} + +public display_eventsmenu(id) +{ + display_menu(g_HLstatsX_EventsMenu, id) + return PLUGIN_HANDLED +} + +public mainmenu_handle(id, menu, item) +{ + if (item < 0) { + return PLUGIN_CONTINUE + } + + static command[16], name[64] + new access, callback + menu_item_getinfo(menu, item, access, command, 15, name, 63, callback) + + new choice = str_to_num(command) + switch (choice) { + case 1 : + make_player_command(id, "/rank") + case 2 : + make_player_command(id, "/next") + case 3 : + make_player_command(id, "/top10") + case 4 : + make_player_command(id, "/clans") + case 5 : + make_player_command(id, "/status") + case 6 : + make_player_command(id, "/statsme") + case 7 : + display_automenu(id) + case 8 : + display_eventsmenu(id) + case 9 : + make_player_command(id, "/weapons") + case 10 : + make_player_command(id, "/accuracy") + case 11 : + make_player_command(id, "/targets") + case 12 : + make_player_command(id, "/kills") + case 13 : + make_player_command(id, "/hlx_hideranking") + case 14 : + make_player_command(id, "/cheaters") + case 15 : + make_player_command(id, "/help") + } + + return PLUGIN_HANDLED +} + +public automenu_handle(id, menu, item) +{ + if (item < 0) { + return PLUGIN_CONTINUE + } + + static command[16], name[64] + new access, callback + menu_item_getinfo(menu, item, access, command, 15, name, 63, callback) + + new choice = str_to_num(command) + switch (choice) { + case 1 : + make_player_command(id, "/hlx_auto start rank") + case 2 : + make_player_command(id, "/hlx_auto end rank") + case 3 : + make_player_command(id, "/hlx_auto kill rank") + case 4 : + make_player_command(id, "/hlx_auto clear") + } + + return PLUGIN_HANDLED +} + +public eventsmenu_handle(id, menu, item) +{ + if (item < 0) { + return PLUGIN_CONTINUE + } + + static command[16], name[64] + new access, callback + menu_item_getinfo(menu, item, access, command, 15, name, 63, callback) + + new choice = str_to_num(command) + switch (choice) { + case 1 : + make_player_command(id, "/hlx_display 1") + case 2 : + make_player_command(id, "/hlx_display 0") + case 3 : + make_player_command(id, "/hlx_chat 1") + case 4 : + make_player_command(id, "/hlx_chat 0") + } + + return PLUGIN_HANDLED +} + +stock is_command_blocked(command[192]) +{ + new command_blocked = 0 + new command_index = 0 + while ((command_blocked == 0) && (command_index < sizeof(blocked_commands))) { + if (strcmp(command, blocked_commands[command_index]) == 0) { + command_blocked++ + } + command_index++ + } + if (command_blocked > 0) { + return 1 + } + return 0 +} + + +public hlx_block_commands(client) +{ + if (client) { + if (client == 0) { + return PLUGIN_CONTINUE + } + + new block_chat_commands = get_pcvar_num(g_hlx_block_commands) + + static user_command[192] + read_args(user_command, 192) + static origin_command[192] + + new start_index = 0 + new command_length = strlen(user_command) + if (command_length > 0) { + if (user_command[start_index] == 34) { + start_index = start_index + 1 + if (user_command[command_length - 1] == 34) { + user_command[command_length - 1] = 0 + } + } + + if (user_command[start_index] == 47) { + start_index++ + } + + copy(origin_command, 192, user_command[start_index]) + } + + if (command_length > 0) { + if (block_chat_commands > 0) { + + new command_type[32] = "say" + new command_blocked = is_command_blocked(origin_command) + + if (command_blocked > 0) { + + if (is_user_connected(client)) { + if ((strcmp("hlx_menu", user_command[start_index]) == 0) || + (strcmp("hlx", user_command[start_index]) == 0) || + (strcmp("hlstatsx", user_command[start_index]) == 0)) { + + display_mainmenu(client) + } + log_player_event(client, command_type, origin_command, 0) + } + return PLUGIN_HANDLED + } + } else { + if (is_user_connected(client)) { + if ((strcmp("hlx_menu", user_command[start_index]) == 0) || + (strcmp("hlx", user_command[start_index]) == 0) || + (strcmp("hlstatsx", user_command[start_index]) == 0)) { + display_mainmenu(client) + } + } + return PLUGIN_CONTINUE + } + } + } + + return PLUGIN_CONTINUE +} + + diff --git a/amxmodx/scripting/hlstatsx_commands_tfc.sma b/amxmodx/scripting/hlstatsx_commands_tfc.sma new file mode 100644 index 0000000..3557f3e --- /dev/null +++ b/amxmodx/scripting/hlstatsx_commands_tfc.sma @@ -0,0 +1,913 @@ +/** + * 2008 - Modified by Nicholas Hastings (psychonic) for used with HLstatsX Community Edition + * http://www.hlxcommunity.com + * + * HLstatsX - AMX Mod X plugin to display ingame messages + * http://www.hlstatsx.com/ + * Copyright (C) 2007-2008 TTS Oetzel & Goerz GmbH + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma dynamic 16000 + +#include +#include + +#include +#include + +#include +#include + +#define VERSION "1.6.19 (HL1)" + + +new g_hlx_block_commands +new g_hlx_message_prefix +new blocked_commands[][] = { "rank", "skill", "points", "place", "session", "session_data", + "kpd", "kdratio", "kdeath", "next", "load", "status", "servers", + "top20", "top10", "top5", "clans", "cheaters", "statsme", "weapons", + "weapon", "action", "actions", "accuracy", "targets", "target", "kills", + "kill", "player_kills", "cmd", "cmds", "command", "hlx_display 0", + "hlx_display 1", "hlx_teams 0", "hlx_teams 1", "hlx_hideranking", + "hlx_chat 0", "hlx_chat 1", "hlx_menu", "servers 1", "servers 2", + "servers 3", "hlx", "hlstatsx", "help" } + + +new g_msgSayText +new g_HLstatsX_MainMenu +new g_HLstatsX_AutoMenu +new g_HLstatsX_EventsMenu + +new logmessage_ignore[512] +new display_menu_keys = MENU_KEY_0|MENU_KEY_1|MENU_KEY_2|MENU_KEY_3|MENU_KEY_4|MENU_KEY_5|MENU_KEY_6|MENU_KEY_7|MENU_KEY_8|MENU_KEY_9 + + +public plugin_init() +{ + register_plugin("HLstatsX CE Ingame Plugin", VERSION, "psychonic") + register_srvcmd("hlx_amx_psay", "hlx_amx_psay", 0, " - sends private message") + register_srvcmd("hlx_amx_bulkpsay", "hlx_amx_bulkpsay", 0, " - sends private message to many") + register_srvcmd("hlx_amx_psay2", "hlx_amx_psay2", 0, " - sends green colored private message") + register_srvcmd("hlx_amx_say", "hlx_amx_say", 0, " - sends message to all players") + register_srvcmd("hlx_amx_csay", "hlx_amx_csay", 0, " - sends center hud message to all players") + register_srvcmd("hlx_amx_hint", "hlx_amx_hint", 0, " - sends hint message") + register_srvcmd("hlx_amx_msay", "hlx_amx_msay", 0, " - displays advanced information") + register_srvcmd("hlx_amx_browse", "hlx_amx_browse", 0, " - displays internal browser") + register_srvcmd("hlx_amx_swap", "hlx_amx_swap", 0, " - swaps players to the opposite team") + + register_cvar("hlxce_plugin_version", VERSION, FCVAR_SPONLY|FCVAR_SERVER) + register_cvar("hlxce_version", "", FCVAR_SPONLY|FCVAR_SERVER) + register_cvar("hlxce_webpage", "http://www.hlxcommunity.com", FCVAR_SPONLY|FCVAR_SERVER) + g_hlx_block_commands = register_cvar("hlx_block_commands", "1") + g_hlx_message_prefix = register_cvar("hlx_message_prefix", "") + + // building the menus only once + g_HLstatsX_MainMenu = menu_create("HLstatsX - Main Menu", "mainmenu_handle") + menu_additem(g_HLstatsX_MainMenu, "Display Rank", "1") + menu_additem(g_HLstatsX_MainMenu, "Next Players", "2") + menu_additem(g_HLstatsX_MainMenu, "Top10 Players", "3") + menu_additem(g_HLstatsX_MainMenu, "Clans Ranking", "4") + menu_additem(g_HLstatsX_MainMenu, "Server Status", "5") + menu_additem(g_HLstatsX_MainMenu, "Statsme", "6") + menu_additem(g_HLstatsX_MainMenu, "Auto Ranking", "7") + menu_additem(g_HLstatsX_MainMenu, "Console Events", "8") + menu_additem(g_HLstatsX_MainMenu, "Weapon Usage", "9") + menu_additem(g_HLstatsX_MainMenu, "Weapons Accuracy", "10") + menu_additem(g_HLstatsX_MainMenu, "Weapons Targets", "11") + menu_additem(g_HLstatsX_MainMenu, "Player Kills", "12") + menu_additem(g_HLstatsX_MainMenu, "Toggle Ranking Display", "13") + menu_additem(g_HLstatsX_MainMenu, "VAC Cheaterlist", "14") + menu_additem(g_HLstatsX_MainMenu, "Display Help", "15") + menu_setprop(g_HLstatsX_MainMenu, MPROP_PERPAGE, 6) + + g_HLstatsX_AutoMenu = menu_create("HLstatsX - Auto-Ranking", "automenu_handle") + menu_additem(g_HLstatsX_AutoMenu, "Enable on round-start", "1") + menu_additem(g_HLstatsX_AutoMenu, "Enable on round-end", "2") + menu_additem(g_HLstatsX_AutoMenu, "Enable on player death", "3") + menu_additem(g_HLstatsX_AutoMenu, "Disable", "4") + menu_setprop(g_HLstatsX_AutoMenu, MPROP_PERPAGE, 0) + + g_HLstatsX_EventsMenu = menu_create("HLstatsX - Console Events", "eventsmenu_handle") + menu_additem(g_HLstatsX_EventsMenu, "Enable Events", "1") + menu_additem(g_HLstatsX_EventsMenu, "Disable Events", "2") + menu_additem(g_HLstatsX_EventsMenu, "Enable Global Chat", "3") + menu_additem(g_HLstatsX_EventsMenu, "Disable Global Chat", "4") + menu_setprop(g_HLstatsX_EventsMenu, MPROP_PERPAGE, 0) + + register_menucmd(register_menuid("Display Menu"), display_menu_keys, "handle_internal_menu") + + register_clcmd("say", "hlx_block_commands") + register_clcmd("say_team", "hlx_block_commands") + + g_msgSayText = get_user_msgid("SayText") + +} + + +public log_player_event(client, verb[32], player_event[192], display_location) +{ + if ((client > 0) && (is_user_connected(client))) { + new player_userid = get_user_userid(client) + + static player_authid[32] + get_user_authid(client, player_authid, 31) + + static player_name[32] + get_user_name(client, player_name, 31) + + static player_team[16] + get_user_team(client, player_team, 15) + + if (display_location > 0) { + new player_origin[3] + get_user_origin (client, player_origin) + + format(logmessage_ignore, 511, "^"%s<%d><%s><%s>^" %s ^"%s^"", player_name, player_userid, player_authid, player_team, verb, player_event) + log_message("^"%s<%d><%s><%s>^" %s ^"%s^" (position ^"%d %d %d^")", player_name, player_userid, player_authid, player_team, verb, player_event, player_origin[0], player_origin[1], player_origin[2]) + } else { + log_message("^"%s<%d><%s><%s>^" %s ^"%s^"", player_name, player_userid, player_authid, player_team, verb, player_event) + } + } +} + + +public game_log_hook(AlertType: type, message[]) +{ + if (type != at_logged ) { + return FMRES_IGNORED + } + if ((strcmp("", logmessage_ignore) != 0) && (contain(message, logmessage_ignore) != -1)) { + if (contain(message, "position") == -1) { + logmessage_ignore = "" + return FMRES_SUPERCEDE + } + } + return FMRES_IGNORED +} + + +public client_death(killer, victim, wpnindex, hitplace, TK) +{ + new id = victim + + if ((id > 0) && (is_user_connected(id))) { + new iStats[8], iHits[8] + static szTeam[16], szName[32], szAuthid[32], szWeapon[24] + new iUserid = get_user_userid(id) + new _max = TFCMAX_WEAPONS + + get_user_team(id, szTeam, 15) + get_user_name(id, szName, 31) + get_user_authid(id, szAuthid, 31) + + for (new i = 1; i < _max; ++i) { + if (get_user_wstats(id, i, iStats, iHits)) { + xmod_get_wpnlogname(i, szWeapon, 23) + + log_message("^"%s<%d><%s><%s>^" triggered ^"weaponstats^" (weapon ^"%s^") (shots ^"%d^") (hits ^"%d^") (kills ^"%d^") (headshots ^"%d^") (tks ^"%d^") (damage ^"%d^") (deaths ^"%d^")", + szName, iUserid, szAuthid, szTeam, szWeapon, iStats[4], iStats[5], iStats[0], iStats[2], iStats[3], iStats[6], iStats[1]) + log_message("^"%s<%d><%s><%s>^" triggered ^"weaponstats2^" (weapon ^"%s^") (head ^"%d^") (chest ^"%d^") (stomach ^"%d^") (leftarm ^"%d^") (rightarm ^"%d^") (leftleg ^"%d^") (rightleg ^"%d^")", + szName, iUserid, szAuthid, szTeam, szWeapon, iHits[1], iHits[2], iHits[3], iHits[4], iHits[5], iHits[6], iHits[7]) + } + } + + reset_user_wstats(id) + } +} + +stock ExplodeString( Output[][], Max, Size, Input[], Delimiter ) +{ + new Idx, l = strlen(Input), Len; + do Len += (1 + copyc( Output[Idx], Size, Input[Len], Delimiter )); + while( (Len < l) && (++Idx < Max) ) + return Idx; +} + +psay (client_id[], client_message[192]) +{ + new client = str_to_num(client_id) + + if (client > 0) { + new Players[32] + new player_count, temp_player_index, player_index + get_players(Players, player_count, "ch") + for (temp_player_index = 0; temp_player_index < player_count; temp_player_index++) { + new player = Players[temp_player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == client) { + player_index = player + break + } + } + + if ((player_index > 0) && (!is_user_bot(player_index)) && (is_user_connected(player_index))) { + + static display_message[192] + static message_prefix[64] + get_pcvar_string(g_hlx_message_prefix, message_prefix, 64) + if (strcmp(message_prefix, "") == 0) { + format(display_message, 192, "%s", client_message) + } else { + format(display_message, 192, "%s %s", message_prefix, client_message) + } + + message_begin(MSG_ONE, g_msgSayText, {0,0,0}, player_index) + write_byte(player_index) + write_string(display_message) + message_end() + } + } +} + +public hlx_amx_bulkpsay(id, level, cid) +{ + new argument_count = read_argc() + if (argument_count < 3) { + return PLUGIN_HANDLED + } + + new client_id_list[48] + read_argv(1, client_id_list, 47) + new client_ids[8][6]; + ExplodeString(client_ids, 7, 5, client_id_list, ','); + + static colored_param[32] + read_argv(2, colored_param, 31) + + new ignore_param = 0 + if (strcmp(colored_param, "1") == 0) { + ignore_param = 1 + } + if (strcmp(colored_param, "0") == 0) { + ignore_param = 1 + } + + new client_message[192] + for(new i = (1 + ignore_param); i < argument_count; i++) { + static temp_argument[192] + read_argv(i + 1, temp_argument, 191) + if (i > (1 + ignore_param)) { + if ((191 - strlen(client_message)) > strlen(temp_argument)) { + if ((temp_argument[0] == 41) || (temp_argument[0] == 125)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } else if ((strlen(client_message) > 0) && (client_message[strlen(client_message)-1] != 40) && (client_message[strlen(client_message)-1] != 123) && (client_message[strlen(client_message)-1] != 58) && (client_message[strlen(client_message)-1] != 39) && (client_message[strlen(client_message)-1] != 44)) { + if ((strcmp(temp_argument, ":") != 0) && (strcmp(temp_argument, ",") != 0) && (strcmp(temp_argument, "'") != 0)) { + client_message[strlen(client_message)] = 32 + } + copy(client_message[strlen(client_message)], 191, temp_argument) + } else { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } else { + if ((192 - strlen(client_message)) > strlen(temp_argument)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } + + for (new i = 0; i < 8; i++) + { + psay(client_ids[i], client_message); + } + + return PLUGIN_HANDLED +} + +public hlx_amx_psay(id, level, cid) +{ + new argument_count = read_argc() + if (argument_count < 3) { + return PLUGIN_HANDLED + } + + static client_id[32] + read_argv(1, client_id, 31) + copy(client_id, 30, client_id[1]) + + static colored_param[32] + read_argv(2, colored_param, 31) + + new ignore_param = 0 + if (strcmp(colored_param, "1") == 0) { + ignore_param = 1 + } + if (strcmp(colored_param, "0") == 0) { + ignore_param = 1 + } + + new client_message[192] + for(new i = (1 + ignore_param); i < argument_count; i++) { + static temp_argument[192] + read_argv(i + 1, temp_argument, 191) + if (i > (1 + ignore_param)) { + if ((191 - strlen(client_message)) > strlen(temp_argument)) { + if ((temp_argument[0] == 41) || (temp_argument[0] == 125)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } else if ((strlen(client_message) > 0) && (client_message[strlen(client_message)-1] != 40) && (client_message[strlen(client_message)-1] != 123) && (client_message[strlen(client_message)-1] != 58) && (client_message[strlen(client_message)-1] != 39) && (client_message[strlen(client_message)-1] != 44)) { + if ((strcmp(temp_argument, ":") != 0) && (strcmp(temp_argument, ",") != 0) && (strcmp(temp_argument, "'") != 0)) { + client_message[strlen(client_message)] = 32 + } + copy(client_message[strlen(client_message)], 191, temp_argument) + } else { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } else { + if ((192 - strlen(client_message)) > strlen(temp_argument)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } + + psay(client_id, client_message) + + return PLUGIN_HANDLED +} + +public hlx_amx_psay2(id, level, cid) +{ + new argument_count = read_argc() + if (argument_count < 3) { + return PLUGIN_HANDLED + } + + static client_id[32] + read_argv(1, client_id, 31) + copy(client_id, 30, client_id[1]) + + static colored_param[32] + read_argv(2, colored_param, 31) + new ignore_param = 0 + if (strcmp(colored_param, "1") == 0) { + ignore_param = 1 + } + if (strcmp(colored_param, "0") == 0) { + ignore_param = 1 + } + + new client_message[192] + for(new i = (1 + ignore_param); i < argument_count; i++) { + static temp_argument[192] + read_argv(i + 1, temp_argument, 191) + if (i > (1 + ignore_param)) { + if ((191 - strlen(client_message)) > strlen(temp_argument)) { + if ((temp_argument[0] == 41) || (temp_argument[0] == 125)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } else if ((strlen(client_message) > 0) && (client_message[strlen(client_message)-1] != 40) && (client_message[strlen(client_message)-1] != 123) && (client_message[strlen(client_message)-1] != 58) && (client_message[strlen(client_message)-1] != 39) && (client_message[strlen(client_message)-1] != 44)) { + if ((strcmp(temp_argument, ":") != 0) && (strcmp(temp_argument, ",") != 0) && (strcmp(temp_argument, "'") != 0)) { + client_message[strlen(client_message)] = 32 + } + copy(client_message[strlen(client_message)], 191, temp_argument) + } else { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } else { + if ((192 - strlen(client_message)) > strlen(temp_argument)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } + + new client = str_to_num(client_id) + + if (client > 0) { + new Players[32] + new player_count, temp_player_index, player_index + get_players(Players, player_count, "ch") + for (temp_player_index = 0; temp_player_index < player_count; temp_player_index++) { + new player = Players[temp_player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == client) { + player_index = player + break + } + } + + if ((player_index > 0) && (!is_user_bot(player_index)) && (is_user_connected(player_index))) { + new color_index = player_index + + static display_message[192] + static message_prefix[64] + get_pcvar_string(g_hlx_message_prefix, message_prefix, 64) + if (strcmp(message_prefix, "") == 0) { + format(display_message, 192, "%s", client_message) + } else { + format(display_message, 192, "%s %s", message_prefix, client_message) + } + + message_begin(MSG_ONE, g_msgSayText, {0,0,0}, player_index) + write_byte(color_index) + write_string(display_message) + message_end() + } + } + + return PLUGIN_HANDLED +} + +public hlx_amx_say(id, level, cid) +{ + if (!cmd_access(id, level, cid, 2)) + return PLUGIN_HANDLED + + static message[192] + read_args(message, 191) + remove_quotes(message) + + static message_prefix[64] + get_pcvar_string(g_hlx_message_prefix, message_prefix, 64) + if (strcmp(message_prefix, "") == 0) { + client_print(0, print_chat, "%s", message) + } else { + client_print(0, print_chat, "%s %s", message_prefix, message) + } + + return PLUGIN_HANDLED +} + + +public hlx_amx_csay(id, level, cid) +{ + if (!cmd_access(id, level, cid, 2)) + return PLUGIN_HANDLED + + static message[192] + read_args(message, 191) + remove_quotes(message) + + new color3[0][] = {255, 255, 255} + new Float:verpos = 0.3 + + set_hudmessage(color3[0][0], color3[0][1], color3[0][2], -1.0, verpos, 0, 6.0, 6.0, 0.5, 0.15, -1) + show_hudmessage(0, "%s", message) + + return PLUGIN_HANDLED +} + + +public hlx_amx_hint(id, level, cid) +{ + if (!cmd_access(id, level, cid, 2)) + return PLUGIN_HANDLED + + new argument_count = read_argc() + + static client_id[32] + read_argv(1, client_id, 31) + copy(client_id, 30, client_id[1]) + + new client_message[192] + for(new i = 1; i < argument_count; i++) { + static temp_argument[192] + read_argv(i + 1, temp_argument, 191) + if (i > 1) { + if ((191 - strlen(client_message)) > strlen(temp_argument)) { + if ((temp_argument[0] == 41) || (temp_argument[0] == 125)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } else if ((strlen(client_message) > 0) && (client_message[strlen(client_message)-1] != 40) && (client_message[strlen(client_message)-1] != 123) && (client_message[strlen(client_message)-1] != 58) && (client_message[strlen(client_message)-1] != 39) && (client_message[strlen(client_message)-1] != 44)) { + if ((strcmp(temp_argument, ":") != 0) && (strcmp(temp_argument, ",") != 0) && (strcmp(temp_argument, "'") != 0)) { + client_message[strlen(client_message)] = 32 + } + copy(client_message[strlen(client_message)], 191, temp_argument) + } else { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } else { + if ((192 - strlen(client_message)) > strlen(temp_argument)) { + copy(client_message[strlen(client_message)], 191, temp_argument) + } + } + } + + new client = str_to_num(client_id) + + if (client > 0) { + new Players[32] + new player_count, temp_player_index, player_index + get_players(Players, player_count, "ch") + for (temp_player_index = 0; temp_player_index < player_count; temp_player_index++) { + new player = Players[temp_player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == client) { + player_index = player + break + } + } + + if ((player_index > 0) && (!is_user_bot(player_index)) && (is_user_connected(player_index))) { + new color3[0][] = {255, 128, 0} + new Float:verpos = 0.80 + + set_hudmessage(color3[0][0], color3[0][1], color3[0][2], -1.0, verpos, 0, 6.0, 6.0, 0.5, 0.15, -1) + show_hudmessage(player_index, "%s", client_message) + } + } + + return PLUGIN_HANDLED +} + + +public hlx_amx_msay(id, level, cid) +{ + if (!cmd_access(id, level, cid, 3)) + return PLUGIN_HANDLED + + static delay[8] + read_argv(1, delay, 7) + remove_quotes(delay) + + static name[32] + read_argv(2, name, 31) + copy(name, 30, name[1]) + new raw_user_id = str_to_num(name) + + static handler_param[32] + read_argv(3, handler_param, 31) + new ignore_param = 0 + new need_handler = 0 + if (strcmp(handler_param, "1") == 0) { + need_handler = 1 + ignore_param = 1 + } + if (strcmp(handler_param, "0") == 0) { + need_handler = 0 + ignore_param = 1 + } + + static message[1024] + new userid + new Players[32] + new player_count, player_index + get_players(Players, player_count, "ch") + for (player_index = 0; player_index < player_count; player_index++) { + new player = Players[player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == raw_user_id) { + userid = player + break + } + } + + read_args(message, 1023) + + new find_pattern[] = "#" + new find_pos = strfind(message, find_pattern) + new text_pos = find_pos + strlen(name) + 2 + if (ignore_param == 1) { + text_pos += 3 + } + + static menu_text[1024] + copy(menu_text, 1023, message[text_pos]) + remove_quotes(menu_text) + + new menu_display[1024] + + new i, start = 0 + new nLen = 0 + new buffer[1024] + + for(i = 0; i < strlen(menu_text); i++) { + if (i > 0) { + if ((menu_text[i-1] == '\') && (menu_text[i] == 'n')) { + buffer = "" + copy(buffer, (i - start)-1 , menu_text[start]) + nLen += format(menu_display[nLen], (1023 - nLen), "%s^n", buffer) + i += 1 + start = i + } + } + } + + if ((userid > 0) && (!is_user_bot(userid)) && (is_user_connected(userid))) { + if (need_handler == 0) { + show_menu(userid, display_menu_keys, menu_display, 15) + } else { + show_menu(userid, display_menu_keys, menu_display, 15, "Display Menu") + } + } + + return PLUGIN_HANDLED + +} + + +public handle_internal_menu(id, key) +{ + new client = id + if (is_user_connected(client)) { + if (key < 9) { + static player_event[192] + new slot = key + slot++ + num_to_str(slot, player_event, 192) + log_player_event(client, "selected", player_event, 0) + } else { + new player_event[192] = "cancel" + log_player_event(client, "selected", player_event, 0) + } + } +} + + +public hlx_amx_browse(id, level, cid) +{ + if (!cmd_access(id, level, cid, 2)) + return PLUGIN_HANDLED + + static name[32] + read_argv(1, name, 31) + copy(name, 30, name[1]) + new raw_user_id = str_to_num(name) + + static message[1024] + new userid + new Players[32] + new player_count, player_index + get_players(Players, player_count, "ch") + for (player_index = 0; player_index < player_count; player_index++) { + new player = Players[player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == raw_user_id) { + userid = player + break + } + } + + read_args(message, 1023) + + new find_pattern[] = "#" + new find_pos = strfind(message, find_pattern) + + static url[1024] + copy(url, 1023, message[find_pos + strlen(name) + 2]) + remove_quotes(url) + + if ((userid > 0) && (!is_user_bot(userid)) && (is_user_connected(userid))) { + show_motd(userid, url, "HLstatsX CE") + } + + return PLUGIN_HANDLED +} + +public hlx_amx_swap(id, level, cid) +{ + if (!cmd_access(id, level, cid, 1)) + return PLUGIN_HANDLED + + static client_id[32] + read_argv(1, client_id, 31) + copy(client_id, 30, client_id[1]) + new client = str_to_num(client_id) + + if (client > 0) { + new userid = 0 + new Players[32] + new player_count, player_index + get_players(Players, player_count, "h") + for (player_index = 0; player_index < player_count; player_index++) { + new player = Players[player_index] + new temp_user_id = get_user_userid(player) + if (temp_user_id == client) { + userid = player + break + } + } + + static player_team[32] + get_user_team(userid, player_team, 31) + + if ((userid > 0) && (is_user_connected(userid))) { + + } + } + return PLUGIN_HANDLED +} + +stock make_player_command(client, player_command[192]) +{ + if (client > 0) { + log_player_event(client, "say", player_command, 0) + } +} + +public display_menu(menu, id) +{ + menu_display(id, menu, 0) +} + +public display_mainmenu(id) +{ + display_menu(g_HLstatsX_MainMenu, id) + return PLUGIN_HANDLED +} + +public display_automenu(id) +{ + display_menu(g_HLstatsX_AutoMenu, id) + return PLUGIN_HANDLED +} + +public display_eventsmenu(id) +{ + display_menu(g_HLstatsX_EventsMenu, id) + return PLUGIN_HANDLED +} + +public mainmenu_handle(id, menu, item) +{ + if (item < 0) { + return PLUGIN_CONTINUE + } + + static command[16], name[64] + new access, callback + menu_item_getinfo(menu, item, access, command, 15, name, 63, callback) + + new choice = str_to_num(command) + switch (choice) { + case 1 : + make_player_command(id, "/rank") + case 2 : + make_player_command(id, "/next") + case 3 : + make_player_command(id, "/top10") + case 4 : + make_player_command(id, "/clans") + case 5 : + make_player_command(id, "/status") + case 6 : + make_player_command(id, "/statsme") + case 7 : + display_automenu(id) + case 8 : + display_eventsmenu(id) + case 9 : + make_player_command(id, "/weapons") + case 10 : + make_player_command(id, "/accuracy") + case 11 : + make_player_command(id, "/targets") + case 12 : + make_player_command(id, "/kills") + case 13 : + make_player_command(id, "/hlx_hideranking") + case 14 : + make_player_command(id, "/cheaters") + case 15 : + make_player_command(id, "/help") + } + + return PLUGIN_HANDLED +} + +public automenu_handle(id, menu, item) +{ + if (item < 0) { + return PLUGIN_CONTINUE + } + + static command[16], name[64] + new access, callback + menu_item_getinfo(menu, item, access, command, 15, name, 63, callback) + + new choice = str_to_num(command) + switch (choice) { + case 1 : + make_player_command(id, "/hlx_auto start rank") + case 2 : + make_player_command(id, "/hlx_auto end rank") + case 3 : + make_player_command(id, "/hlx_auto kill rank") + case 4 : + make_player_command(id, "/hlx_auto clear") + } + + return PLUGIN_HANDLED +} + +public eventsmenu_handle(id, menu, item) +{ + if (item < 0) { + return PLUGIN_CONTINUE + } + + static command[16], name[64] + new access, callback + menu_item_getinfo(menu, item, access, command, 15, name, 63, callback) + + new choice = str_to_num(command) + switch (choice) { + case 1 : + make_player_command(id, "/hlx_display 1") + case 2 : + make_player_command(id, "/hlx_display 0") + case 3 : + make_player_command(id, "/hlx_chat 1") + case 4 : + make_player_command(id, "/hlx_chat 0") + } + + return PLUGIN_HANDLED +} + +stock is_command_blocked(command[192]) +{ + new command_blocked = 0 + new command_index = 0 + while ((command_blocked == 0) && (command_index < sizeof(blocked_commands))) { + if (strcmp(command, blocked_commands[command_index]) == 0) { + command_blocked++ + } + command_index++ + } + if (command_blocked > 0) { + return 1 + } + return 0 +} + + +public hlx_block_commands(client) +{ + if (client) { + if (client == 0) { + return PLUGIN_CONTINUE + } + + new block_chat_commands = get_pcvar_num(g_hlx_block_commands) + + static user_command[192] + read_args(user_command, 192) + static origin_command[192] + + new start_index = 0 + new command_length = strlen(user_command) + if (command_length > 0) { + if (user_command[start_index] == 34) { + start_index = start_index + 1 + if (user_command[command_length - 1] == 34) { + user_command[command_length - 1] = 0 + } + } + + if (user_command[start_index] == 47) { + start_index++ + } + + copy(origin_command, 192, user_command[start_index]) + } + + if (command_length > 0) { + if (block_chat_commands > 0) { + + new command_type[32] = "say" + new command_blocked = is_command_blocked(origin_command) + + if (command_blocked > 0) { + + if (is_user_connected(client)) { + if ((strcmp("hlx_menu", user_command[start_index]) == 0) || + (strcmp("hlx", user_command[start_index]) == 0) || + (strcmp("hlstatsx", user_command[start_index]) == 0)) { + + display_mainmenu(client) + } + log_player_event(client, command_type, origin_command, 0) + } + return PLUGIN_HANDLED + } + } else { + if (is_user_connected(client)) { + if ((strcmp("hlx_menu", user_command[start_index]) == 0) || + (strcmp("hlx", user_command[start_index]) == 0) || + (strcmp("hlstatsx", user_command[start_index]) == 0)) { + display_mainmenu(client) + } + } + return PLUGIN_CONTINUE + } + } + } + + return PLUGIN_CONTINUE +} + + diff --git a/heatmaps/DejaVuSans.ttf b/heatmaps/DejaVuSans.ttf new file mode 100644 index 0000000..c1b19d8 Binary files /dev/null and b/heatmaps/DejaVuSans.ttf differ diff --git a/heatmaps/README b/heatmaps/README new file mode 100644 index 0000000..949b9b1 --- /dev/null +++ b/heatmaps/README @@ -0,0 +1,14 @@ +HLstatsX Community Edition +Heatmap Generation Installation Instructions + +WARNING: Heatmap generation will consume alot of processor time. Please be sure to plan accordingly! +300k kills on one map will take ~10min to generate on one 2.4Ghz core processor. + +Install: + - change user, password, database and path to your "web" folder in config.inc.php + - download the appropriate heatmap pack from www.hlxcommunity.com and put into "src" + +To generate heatmaps: + - run: php generate.php + +report problems to hlx forums or irc diff --git a/heatmaps/config.inc.php b/heatmaps/config.inc.php new file mode 100755 index 0000000..46ad7ed --- /dev/null +++ b/heatmaps/config.inc.php @@ -0,0 +1,20 @@ +init(); + +foreach (Env::get('mapinfo') as $game => $gameconf) { + foreach ($gameconf as $map => $data) { + $heat->generate($game, $map, "kill"); + } +} + +show::Event("CREATE", "Heatmap creation done.", 1); +?> diff --git a/heatmaps/heatmap.class.php b/heatmaps/heatmap.class.php new file mode 100644 index 0000000..e07eb52 --- /dev/null +++ b/heatmaps/heatmap.class.php @@ -0,0 +1,608 @@ +value to the environmental data array + * + * @param string $key this is the identifier of the value you are adding + * @param string $value The value to add into the array + */ + public static function set($key, $value) { + self::$data[$key] = $value; + } + + /** + * Gets the current value from the data array + * + * @param string $key The key to lookup in the array + * @throws SteambansException if the variable cannot be found + * @return mixed null if the key cannot be found, or the value that was stored in the array + */ + public static function get($key) { + return array_key_exists($key, self::$data) ? self::$data[$key] : null; + } +} + +/** +* Heatmap API to generate/add/update/delete content for heatmaps. +* +*/ + +class Heatmap { + var $version = "0.1"; + + /** + * Stuff we want to fire when we start. + * + */ + public static function init () { + global $argv; + DB::connect(); + self::mapinfo(); + self::parseArguments($argv); + } + + /** + * Function that builds the maplist from db elements. + * + */ + public static function mapinfo () { + $query = 'SELECT + g.code, + hc.game, + hc.map, + hc.xoffset, + hc.yoffset, + hc.flipx, + hc.flipy, + hc.rotate, + hc.days, + hc.brush, + hc.scale, + hc.font, + hc.thumbw, + hc.thumbh, + hc.cropx1, + hc.cropx2, + hc.cropy1, + hc.cropy2 + FROM + ' . DB_PREFIX . '_Games AS g + INNER JOIN + ' . DB_PREFIX . '_Heatmap_Config AS hc + ON + hc.game = g.realgame + WHERE 1=1 + ORDER BY code ASC, game ASC, map ASC'; + + $result = DB::doQuery($query); + if (DB::numRows($result)) { + while ($row = DB::getAssoc($result)) { + foreach ($row as $key => $val) { + $mapinfo[$row['code']][$row['map']][$key] = $val; + } + } + + Env::set('mapinfo', $mapinfo); + } + } + + private static function printHeatDot($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct){ + if(!isset($pct)){ + return false; + } + $pct /= 100; + + // Get image width and height + $w = imagesx( $src_im ); + $h = imagesy( $src_im ); + + // Turn alpha blending off + imagealphablending( $src_im, false ); + + // Find the most opaque pixel in the image (the one with the smallest alpha value) + $minalpha = 127; + for( $x = 0; $x < $w; $x++ ) { + for( $y = 0; $y < $h; $y++ ){ + $alpha = ( imagecolorat( $src_im, $x, $y ) >> 24 ) & 0xFF; + if( $alpha < $minalpha ){ + $minalpha = $alpha; + } + } + } + + //loop through image pixels and modify alpha for each + for( $x = 0; $x < $w; $x++ ){ + for( $y = 0; $y < $h; $y++ ){ + //get current alpha value (represents the TANSPARENCY!) + $colorxy = imagecolorat( $src_im, $x, $y ); + $alpha = ( $colorxy >> 24 ) & 0xFF; + + //calculate new alpha + if( $minalpha !== 127 ){ + $alpha = 127 + 127 * $pct * ( $alpha - 127 ) / ( 127 - $minalpha ); + } else { + $alpha += 127 * $pct; + } + + //get the color index with new alpha + $alphacolorxy = imagecolorallocatealpha( $src_im, ( $colorxy >> 16 ) & 0xFF, ( $colorxy >> 8 ) & 0xFF, $colorxy & 0xFF, $alpha ); + + //set pixel with the new color + opacity + if( !imagesetpixel( $src_im, $x, $y, $alphacolorxy ) ){ + return false; + } + } + } + + // The image copy + imagecopy($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h); + } + + public static function generate($code, $map, $mode) { + // generatemap($map, $code, "Total Kills", "HlstatsX:CE") + self::buildQuery($code, $map); + + + $mapinfo = Env::get('mapinfo'); + $map_query = Env::get('map_query'); + $disable_cache = Env::get('disable_cache'); + + // See if we need to rotate the map or not. + $rotate = $mapinfo[$code][$map]['rotate']; + + + $timestamp = time(); + + // Fix the last part of your path. + $path = HLXCE_WEB . "/hlstatsimg/games/" . $mapinfo[$code][$map]['code'] . "/heatmaps"; + show::Event("PATH", $path, 3); + + // Does the source image exists? else there is no idea to spend resources on it. + if (!file_exists(dirname(__FILE__) . "/src/" . $mapinfo[$code][$map]['game'] . "/" . $map . ".jpg")) { + show::Event("FILE", dirname(__FILE__) . "/src/" . $mapinfo[$code][$map]['game'] . "/" . $map . ".jpg doesn't exists", 3); + return false; + } + + // Check that the dir exists, else try to create it. + if (!is_dir($path)) { + if (!@mkdir($path)) { + show::Event("PREPARE", "Couln't create outputfolder: $path", 1); + } + } + + // Check if we have cached info, then we should work from that instead. + if (is_dir(CACHE_DIR . "/$code")) { + if ($handle = opendir(CACHE_DIR. "/$code")) { + while (false !== ($file = readdir($handle))) { + if ($file != "." && $file != ".." && preg_match(str_replace("\$","\\\$","/${map}_(\d+).png/i"), $file, $matches)) { + $cache_file = CACHE_DIR . "/$code/$file"; + $oldtimestamp = $matches[1]; + + // unless it's over 30 days old cache file, then we delete it and go from 0 again. + if (floor((time() - $oldtimestamp) / 86400 > 30)) { + $obsolite_cache = true; + show::Event("CACHE", "Cache file is obsolite, " . floor((time() - $oldtimestamp) / 86400) . " days old. Generating from scratch", 1); + } + + // If we called with --disable-cache we want to clean up and then regen from our start. + if ($disable_cache || isset($obsolite_cache)) { + $disable_cache = true; + if (file_exists($cache_file)) { + unlink($cache_file); + } + } else { + show::Event("CACHE","Found cached file ($file), we will use timestamp $oldtimestamp instead", 1); + $find = '/.*AND hef.eventTime >= FROM_UNIXTIME\([0-9]+\).*/i'; + $replace = ' AND hef.eventTime > FROM_UNIXTIME(' . $oldtimestamp . ')'; + $map_query = preg_replace($find, $replace, $map_query); + } + } + } + closedir($handle); + } + } else { + if (!@mkdir(CACHE_DIR . "/$code")) { + show::Event("CACHE", "Can't create cache_dir: " . CACHE_DIR . "/$code", 1); + } + } + + $result = DB::doQuery($map_query); + $num_kills = DB::numRows($result); + + if (!$num_kills) { + show::Event("IGNORE", "Game: $code, Map: $map, Kills: $num_kills, (to few kills)", 1); + return false; + } + + $firstdata = time(); + + $img = imagecreatefromjpeg("./src/" . $mapinfo[$code][$map]['game'] . "/" . $map . ".jpg"); + imagealphablending($img, true); + imagesavealpha($img, true); + + if (isset($cache_file) && !$disable_cache) { + $overlay = imagecreatefrompng($cache_file); + } else { + $overlay = imagecreatetruecolor(imagesx($img), imagesy($img)); + } + + imagealphablending($overlay, true); + imagesavealpha($overlay, true); + + $brush = imagecreatefrompng("./src/brush_" . $mapinfo[$code][$map]['brush'] . ".png"); + $brushsize = ($mapinfo[$code][$map]['brush'] == "large") ? 33 : 17; + + $white = imagecolorallocate($overlay, 255, 255, 255); + $black = imagecolorallocate($overlay, 0, 0, 0); + + imagefill($overlay, 0, 0, $black); + imagecolortransparent($overlay, $black); + + $num_kills = ($num_kills) ? $num_kills : 1; + + show::Event("CREATE", "Game: $code, Map: $map, Kills: $num_kills", 1); + $opacity = intval((500 / $num_kills) * 100); + + + if ($opacity > 40) $opacity = 40; + if ($opacity < 1) $opacity = 2; + + + $max_red = 0; + $i = 0; + while ($row = DB::getAssoc($result)) { + if ($row['eventTime'] < $firstdata) $firstdata = $row['eventTime']; + + if ($mapinfo[$code][$map]['flipx']) $row['pos_x'] = $row['pos_x'] * -1; + if ($mapinfo[$code][$map]['flipy']) $row['pos_y'] = $row['pos_y'] * -1; + + $x = ($row['pos_x'] + $mapinfo[$code][$map]['xoffset']) / $mapinfo[$code][$map]['scale']; + $y = ($row['pos_y'] + $mapinfo[$code][$map]['yoffset']) / $mapinfo[$code][$map]['scale']; + + $rgb = imagecolorat($overlay, $x, $y); + $colors = imagecolorsforindex($overlay, $rgb); + + if ($colors['red'] > $max_red) $max_red = $colors['red']; + + if ($colors['red'] <= 200) { + // Rotate the image + if ($rotate) { + self::printHeatDot($overlay, $brush, $y - ($brushsize / 2), $x - ($brushsize / 2), 0, 0, $brushsize, $brushsize, $opacity); + } else { + self::printHeatDot($overlay, $brush, $x - ($brushsize / 2), $y - ($brushsize / 2), 0, 0, $brushsize, $brushsize, $opacity); + } + } + } + + imagedestroy($brush); + + $colorarr = array(); + $colors = array(0, 0, 255); + + for ($line = 0; $line < 128; ++$line) { + $colors = array(0, $colors[1] + 2, $colors[2] -2); + $colorarr[$line] = $colors; + } + + for ($line = 128; $line < 255; ++$line) { + $colors = array($colors[0] + 2, $colors[1] -2, 0); + $colorarr[$line] = $colors; + } + + for ($x = 0; $x < imagesx($overlay); ++$x) { + for ($y = 0; $y < imagesy($overlay); ++$y) { + $index = imagecolorat($overlay, $x, $y); + $rgb = imagecolorsforindex($overlay, $index); + $alpha = ( imagecolorat( $overlay, $x, $y ) >> 24 ) & 0xFF; + + $color = imagecolorallocatealpha($img, $colorarr[$rgb['red']][0], $colorarr[$rgb['red']][1], $colorarr[$rgb['red']][2], 127 - ($rgb['red'] / 2)); + if (!imagesetpixel($img, $x, $y, $color)) echo "."; + } + } + + if ($mapinfo[$code][$map]['cropy2'] > 0 && $mapinfo[$code][$map]['cropy2'] > 0) { + $temp = imagecreatetruecolor($mapinfo[$code][$map]['cropx2'], $mapinfo[$code][$map]['cropy2']); + imagecopy($temp, $img, 0, 0, $mapinfo[$code][$map]['cropx1'], $mapinfo[$code][$map]['cropy1'], $mapinfo[$code][$map]['cropx2'], $mapinfo[$code][$map]['cropy2']); + imagedestroy($img); + + $img = imagecreatetruecolor(imagesx($temp), imagesy($temp)); + imagecopy($img, $temp, 0, 0, 0, 0, imagesx($temp), imagesy($temp)); + imagedestroy($temp); + } + + if ($mapinfo[$code][$map]['thumbw'] > 0 && $mapinfo[$code][$map]['thumbh'] > 0) { + $thumb = imagecreatetruecolor(imagesx($img) * $mapinfo[$code][$map]['thumbw'], imagesy($img) * $mapinfo[$code][$map]['thumbh']); + imagecopyresampled($thumb, $img, 0, 0, 0, 0, imagesx($thumb), imagesy($thumb), imagesx($img), imagesy($img)); + imagejpeg($thumb, $path . "/" . $map . "-" . $mode . "-thumb.jpg", 100); + imagedestroy($thumb); + } + + $img = self::drawHud($img, $map, "HLX:CE", "Total Kills", $num_kills, $firstdata); + + if (imagejpeg($img, $path . "/" . $map . "-" . $mode . ".jpg", 100)) $return = true; + if (imagepng($overlay, CACHE_DIR . "/$code/${map}_${timestamp}.png", 9)) $return = true; + imagedestroy($overlay); + + // Clean upc cache file + if (isset($cache_file) && file_exists($cache_file)) { + unlink(CACHE_DIR . "/$code/${map}_${oldtimestamp}.png"); + } + + + imagedestroy($img); + + return $return; + } + + public static function buildQuery ($code, $map) { + $mapinfo = Env::get('mapinfo'); + Env::set('code', $code); + $ignore_infected = Env::get('ignore_infected'); + $timescope = (time() - 60*60*24*$mapinfo[$code][$map]['days']); + + $map_query = 'SELECT + "frag" AS killtype, + hef.id, + hef.map, + hs.game, + hef.eventTime, + hef.pos_x, + hef.pos_y + FROM + hlstats_Events_Frags as hef, + hlstats_Servers as hs + WHERE 1=1 + AND hef.map = "' . $map . '" + AND hs.serverId = hef.serverId + AND hs.game = "' . $code. '" + AND hef.pos_x IS NOT NULL + AND hef.pos_y IS NOT NULL + AND hef.eventTime >= FROM_UNIXTIME(' . $timescope . ') +'; + if ($ignore_infected) { + $map_query.= ' AND hef.victimRole != "infected" +'; + } + $map_query.= ' LIMIT ' . KILL_LIMIT . ' + + UNION ALL + + SELECT + "teamkill" AS killtype, + hef.id, + hef.map, + hs.game, + hef.eventTime, + hef.pos_x, + hef.pos_y + FROM + hlstats_Events_Teamkills as hef, + hlstats_Servers as hs + WHERE 1=1 + AND hef.map = "' . $map . '" + AND hs.serverId = hef.serverId + AND hs.game = "' . $code. '" + AND hef.pos_x IS NOT NULL + AND hef.pos_y IS NOT NULL + AND hef.eventTime >= FROM_UNIXTIME(' . $timescope . ') + LIMIT ' . KILL_LIMIT . ' + '; + + Env::set('map_query', $map_query); + show::Event("SQL", $map_query, 3); + + } + + private static function drawHud ($img, $map, $heatmapname, $method, $num_kills, $firstdata) { + $mapinfo = Env::get('mapinfo'); + $code = Env::get('code'); + + + // Resize the image according to your settings + $img = self::resize($img); + + $hudText = array( + strtoupper($map) . " - " . strtoupper($heatmapname) . " HEATMAP - " . strtoupper($method), + date("m/d/y", intval(time() - 60*60*24*30)) . " - " . date("m/d/y", time()), + "Generated: " . date("Y-m-d H:i:s"), + HUD_URL + ); + + show::Event("HUD", "Creating Overlay HUD", 2); + + $hudx = imagesx($img); + $hudy = intval(intval($mapinfo[$code][$map]['font'] + 4) * intval(count($hudText) + 1) + 8); + + $hud = imagecreatetruecolor(imagesx($img), imagesy($img)) or die('Cannot Initialize new GD image stream'); + imagesavealpha($hud, true); + + $trans_colour = imagecolorallocatealpha($hud, 0, 0, 0, 127); + $black = imagecolorallocatealpha($hud, 0, 0, 0, 90); + + imagefill($hud, 0, 0, $trans_colour); + imagefilledrectangle($hud, 0, 0, imagesx($img) - 1, imagesy($img) - 1, $black); + + $font = "./DejaVuSans.ttf"; + + // Copy the hud to the top of the image. + imagecopy($img, $hud, 0, 0, 0, 0, $hudx, $hudy); + + //array imagettftext ( resource $image , float $size , float $angle , int $x , int $y , int $color , string $fontfile , string $text ) + $i = 1; + foreach ($hudText as $text) { + imagettftext( $img, + $mapinfo[$code][$map]['font'], + 0, + 10, + intval(intval($mapinfo[$code][$map]['font'] + 4) * $i + 8), + imagecolorallocate($img, 255, 255, 255), + $font, + $text + ); + $i++; + } + + imagedestroy($hud); + + show::Event("HUD", "Done...", 2); + return $img; + } + + private static function resize ($img) { + switch (OUTPUT_SIZE) { + case "small": + $newwidth = 800; + $newheight = 600; + break; + case "medium": + $newwidth = 1024; + $newheight = 768; + break; + case "large": + $newwidth = 1280; + $newheight = 1024; + // As for now we don't do anything since this is default size + return $img; + break; + default: + $newwidth = 1024; + $newheight = 768; + } + + show::Event("RESIZE", "Adjusting Heatmap to current setting: " . OUTPUT_SIZE, 2); + + $resized = imagecreatetruecolor($newwidth, $newheight); + imagecopyresized($resized, $img, 0, 0, 0, 0, $newwidth, $newheight, imagesx($img), imagesy($img)); + + imagedestroy($img); + + show::Event("RESIZE", "Done...", 2); + return $resized; + } + + private static function arguments($argv) { + $_ARG = array(); + foreach ($argv as $arg) { + if (preg_match('/\-\-[a-zA-Z0-9]*=.*/', $arg)) { + $str = explode('=', $arg); + $arg = ''; + $key = preg_replace('/\-\-/', '', $str[0]); + for ( $i = 1; $i < count($str); $i++ ) { + $arg .= $str[$i]; + } + $_ARG[$key] = $arg; + } elseif(preg_match('/\-[a-zA-Z0-9]/', $arg)) { + $arg = preg_replace('/\-/', '', $arg); + $_ARG[$arg] = 'true'; + } + } + return $_ARG; + } + + public static function parseArguments($argv) { + $mapinfo = Env::get('mapinfo'); + $cache = false; + $args = self::arguments($argv); + + if (isset($args['game'])) { + if (!isset($mapinfo[$args['game']])) { + show::Event("ERROR", "Game: " . $args['game'] . " doesn't exists, escaping", 1); + exit; + } + + if (isset($args['map'])) { + if (!isset($mapinfo[$args['game']][$args['map']])) { + show::Event("ERROR", "Game: " . $args['game'] . " Map: " . $args['map'] . " doesn't exists, escaping", 1); + exit; + } + + $tmp[$args['game']][$args['map']] = $mapinfo[$args['game']][$args['map']]; + show::Event("ARGS", "--game=" . $args['game'], 2); + show::Event("ARGS", "--map=" . $args['map'], 2); + } else { + $tmp[$args['game']] = $mapinfo[$args['game']]; + show::Event("ARGS", "--game=" . $args['game'], 2); + } + } else { + $visible = ''; + $query = "SELECT code FROM hlstats_Games WHERE hidden='0'"; + $result = DB::doQuery($query); + if (DB::numRows($result)) { + while ($row = DB::getAssoc($result)) { + foreach ($row as $key => $val) { + if (isset($mapinfo[$val])) { + $visible .= "$val, "; + $tmp[$val] = $mapinfo[$val]; + } + } + } + } + show::Event("GAMES", substr($visible, 0, -2), 2); + } + + if (isset($tmp)) { + $mapinfo = $tmp; + } + + if (isset($args['disablecache'])) { + $cache = true; + show::Event("ARGS", "--disable-cache=true", 2); + } else { + $cache = false; + show::Event("ARGS", "--disable-cache=false", 2); + } + + if (isset($args['ignoreinfected'])) { + $ignore_infected = true; + show::Event("ARGS", "--ignore-infected=true", 2); + } else { + $ignore_infected = false; + show::Event("ARGS", "--ignore-infected=false", 2); + } + + Env::set('mapinfo', $mapinfo); + Env::set('disable_cache', $cache); + Env::set('ignore_infected', $ignore_infected); + } + +// End of Heat Class +} + + + +class DB { + public static function connect () { + mysql_connect(DB_HOST, DB_USER, DB_PASS); + mysql_select_db(DB_NAME); + show::Event("DB", "Connected to " . DB_NAME . " as " . DB_USER . "@" . DB_HOST, 1); + } + + public static function doQuery ($query) { + return mysql_query($query); + } + + public static function getAssoc ($result) { + return mysql_fetch_assoc($result); + } + + public static function numRows ($result) { + return mysql_num_rows($result); + } +} + +class show { + public static function Event ($type, $text, $runlevel) { + if ($runlevel <= DEBUG) { + print date("Y-m-d H:i:s") . "\t\t$type: $text\n"; + } + } +} + +?> diff --git a/heatmaps/src/README b/heatmaps/src/README new file mode 100644 index 0000000..cfe3168 --- /dev/null +++ b/heatmaps/src/README @@ -0,0 +1,5 @@ +The Heatmap source files are not included with the release packages of HLXCE. + +Heatmap Packs can be downloaded from http://www.hlxce.com. + +Heatmap Pack source files are stored in the HLX-Extras repository. \ No newline at end of file diff --git a/heatmaps/src/brush_large.png b/heatmaps/src/brush_large.png new file mode 100644 index 0000000..f579f14 Binary files /dev/null and b/heatmaps/src/brush_large.png differ diff --git a/heatmaps/src/brush_small.png b/heatmaps/src/brush_small.png new file mode 100644 index 0000000..86d78db Binary files /dev/null and b/heatmaps/src/brush_small.png differ diff --git a/scripts/BASTARDrcon.pm b/scripts/BASTARDrcon.pm new file mode 100644 index 0000000..079768e --- /dev/null +++ b/scripts/BASTARDrcon.pm @@ -0,0 +1,384 @@ +package BASTARDrcon; +# +# BASTARDrcon Perl Module - execute commands on a remote Half-Life 1 server using Rcon. +# A merge of the KKrcon library into HLstatsX +# Copyright (C) 2008-20XX Nicholas Hastings (nshastings@gmail.com) + +# KKrcon Perl Module - execute commands on a remote Half-Life server using Rcon. +# http://kkrcon.sourceforge.net +# +# TRcon Perl Module - execute commands on a remote Half-Life2 server using remote console. +# http://www.hlstatsx.com +# +# Copyright (C) 2000, 2001 Rod May +# Enhanced in 2005 by Tobi (Tobi@gameme.de) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# + +use strict; +use sigtrap; +use Socket; +use Sys::Hostname; +use bytes; + +## +## Main +## + +# +# Constructor +# +sub new +{ + my ($class_name, $server_object) = @_; + my ($self) = {}; + bless($self, $class_name); + + # Initialise properties + $self->{server_object} = $server_object; + $self->{rcon_password} = $server_object->{rcon} or die("BASTARDrcon: a Password is required\n"); + $self->{server_host} = $server_object->{address}; + $self->{server_port} = int($server_object->{port}) or die("BASTARDrcon: invalid Port \"" . $server_object->{port} . "\"\n"); + + $self->{socket} = undef; + $self->{error} = ""; + + # Set up socket parameters + $self->{_ipaddr} = gethostbyname($self->{server_host}) or die("BASTARDrcon: could not resolve Host \"" . $self->{server_host} . "\"\n"); + + return $self; +} + +# +# Execute an Rcon command and return the response +# +sub execute +{ + my ($self, $command) = @_; + my $msg; + my $ans; + + # version x.1.0.6+ HL1 server + $msg = "\xFF\xFF\xFF\xFFchallenge rcon\n\0"; + $ans = $self->_sendrecv($msg); + + if ($ans =~ /challenge +rcon +(\d+)/) + { + $msg = "\xFF\xFF\xFF\xFFrcon $1 \"" . $self->{"rcon_password"} . "\" $command\0"; + $ans = $self->_sendrecv($msg); + } + elsif (!$self->error()) + { + $ans = ""; + $self->{"error"} = "No challenge response"; + } + + if ($ans =~ /bad rcon_password/i) + { + $self->{"error"} = "Bad Password"; + } + return $ans; +} + +sub _sendrecv +{ + my ($self, $msg) = @_; + my $host = $self->{"server_host"}; + my $port = $self->{"server_port"}; + my $ipaddr = $self->{"_ipaddr"}; + my $proto = $self->{"_proto"}; + + # Open socket + socket($self->{"socket"}, PF_INET, SOCK_DGRAM, $proto) or die("BASTARDrcon(141): socket: $!\n"); + my $hispaddr = sockaddr_in($port, $ipaddr); + + die("BASTARDrcon: send $ipaddr:$port : $!") unless(defined(send($self->{"socket"}, $msg, 0, $hispaddr))); + + my $rin = ""; + vec($rin, fileno($self->{"socket"}), 1) = 1; + my $ans = "TIMEOUT"; + if (select($rin, undef, undef, 0.5)) + { + $ans = ""; + $hispaddr = recv($self->{"socket"}, $ans, 8192, 0); + $ans =~ s/\x00+$//; # trailing crap + $ans =~ s/^\xFF\xFF\xFF\xFFl//; # HL response + $ans =~ s/^\xFF\xFF\xFF\xFFn//; # QW response + $ans =~ s/^\xFF\xFF\xFF\xFF//; # Q2/Q3 response + $ans =~ s/^\xFE\xFF\xFF\xFF.....//; # old HL bug/feature + } + # Close socket + close($self->{"socket"}); + + if ($ans eq "TIMEOUT") + { + $ans = ""; + $self->{"error"} = "Rcon timeout"; + } + return $ans; +} + +# +# Send a package +# +sub send_rcon +{ + my ($self, $id, $command, $string1, $string2) = @_; + my $tmp = pack("VVZ*Z*",$id,$command,$string1,$string2); + my $size = length($tmp); + if($size > 4096) + { + $self->{error} = "Command too long to send!"; + return 1; + } + $tmp = pack("V", $size) .$tmp; + + unless(defined(send($self->{"socket"},$tmp,0))) + { + die("BASTARDrcon: send $!"); + } + return 0; +} + +# +# Recieve a package +# +sub recieve_rcon +{ + my $self = shift; + my ($size, $id, $command, $msg); + my $rin = ""; + my $tmp = ""; + + vec($rin, fileno($self->{"socket"}), 1) = 1; + if(select($rin, undef, undef, 0.5)) + { + while(length($size) < 4) + { + $tmp = ""; + recv($self->{"socket"}, $tmp, (4-length($size)), 0); + $size .= $tmp; + } + $size = unpack("V", $size); + if($size < 10 || $size > 8192) + { + close($self->{"socket"}); + $self->{error} = "illegal size $size "; + return (-1, -1, -1); + } + + while(length($id)<4) + { + $tmp = ""; + recv($self->{"socket"}, $tmp, (4-length($id)), 0); + $id .= $tmp; + } + $id = unpack("V", $id); + $size = $size - 4; + while(length($command)<4) + { + $tmp =""; + recv($self->{"socket"}, $tmp, (4-length($command)),0); + $command.=$tmp; + } + $command = unpack("V", $command); + $size = $size - 4; + my $msg = ""; + while($size >= 1) + { + $tmp = ""; + recv($self->{"socket"}, $tmp, $size, 0); + $size -= length($tmp); + $msg .= $tmp; + } + my ($string1,$string2) = unpack("Z*Z*",$msg); + $msg = $string1.$string2; + return ($id, $command, $msg); + } + else + { + return (-1, -1, -1); + } +} + +# +# Get error message +# +sub error +{ + my ($self) = @_; + return $self->{"error"}; +} + +# +# Parse "status" command output into player information +# +sub getPlayers +{ + my ($self) = @_; + my $status = $self->execute("status"); + + my @lines = split(/[\r\n]+/, $status); + + my %players; + +# HL1 +# name userid uniqueid frag time ping loss adr +# 1 "psychonic" 1 STEAM_0:1:4153990 0 00:33 13 0 192.168.5.115:27005 + + foreach my $line (@lines) + { + if ($line =~ /^\#\s*\d+\s+ + "(.+)"\s+ # name + (\d+)\s+ # userid + ([^\s]+)\s+\d+\s+ # uniqueid + ([\d:]+)\s+ # time + (\d+)\s+ # ping + (\d+)\s+ # loss + ([^:]+): # addr + (\S+) # port + $/x) + + { + my $name = $1; + my $userid = $2; + my $uniqueid = $3; + my $time = $4; + my $ping = $5; + my $loss = $6; + my $state = ""; + my $address = $7; + my $port = $8; + + $uniqueid =~ s/^STEAM_[0-9]+?\://i; + + # &::printEvent("DEBUG", "USERID: '$userid', NAME: '$name', UNIQUEID: '$uniqueid', TIME: '$time', PING: '$ping', LOSS: '$loss', ADDRESS:'$address', CLI_PORT: '$port'", 1); + + if ($::g_mode eq "NameTrack") { + $players{$name} = { + "Name" => $name, + "UserID" => $userid, + "UniqueID" => $uniqueid, + "Time" => $time, + "Ping" => $ping, + "Loss" => $loss, + "State" => $state, + "Address" => $address, + "ClientPort" => $port + }; + } elsif ($::g_mode eq "LAN") { + $players{$address} = { + "Name" => $name, + "UserID" => $userid, + "UniqueID" => $uniqueid, + "Time" => $time, + "Ping" => $ping, + "Loss" => $loss, + "State" => $state, + "Address" => $address, + "ClientPort" => $port + }; + } else { + $players{$uniqueid} = { + "Name" => $name, + "UserID" => $userid, + "UniqueID" => $uniqueid, + "Time" => $time, + "Ping" => $ping, + "Loss" => $loss, + "State" => $state, + "Address" => $address, + "ClientPort" => $port + }; + } + } + } + return %players; +} + +sub getServerData +{ + my ($self) = @_; + my $status = $self->execute("status"); + + my @lines = split(/[\r\n]+/, $status); + + my $servhostname = ""; + my $map = ""; + my $max_players = 0; + foreach my $line (@lines) + { + if ($line =~ /^\s*hostname\s*:\s*([\S].*)$/x) + { + $servhostname = $1; + } + elsif ($line =~ /^\s*map\s*:\s*([\S]+).*$/x) + { + $map = $1; + } + elsif ($line =~ /^\s*players\s*:\s*\d+.+\((\d+)\smax.*$/) + { + $max_players = $1; + } + } + return ($servhostname, $map, $max_players, 0); +} + + +sub getVisiblePlayers +{ + my ($self) = @_; + my $status = $self->execute("sv_visiblemaxplayers"); + + my @lines = split(/[\r\n]+/, $status); + + + my $max_players = -1; + foreach my $line (@lines) + { + # "sv_visiblemaxplayers" = "-1" + # - Overrides the max players reported to prospective clients + if ($line =~ /^\s*"sv_visiblemaxplayers"\s*=\s*"([-0-9]+)".*$/x) + { + $max_players = $1; + } + } + return ($max_players); +} + + +# +# Get information about a player by userID +# + +sub getPlayer +{ + my ($self, $uniqueid) = @_; + my %players = $self->getPlayers(); + + if (defined($players{$uniqueid})) + { + return $players{$uniqueid}; + } + else + { + $self->{"error"} = "No such player # $uniqueid"; + return 0; + } +} + +1; +# end diff --git a/scripts/ConfigReaderSimple.pm b/scripts/ConfigReaderSimple.pm new file mode 100644 index 0000000..a6035fb --- /dev/null +++ b/scripts/ConfigReaderSimple.pm @@ -0,0 +1,247 @@ +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + + +package ConfigReaderSimple; +# +# Simple interface to a configuration file +# +# Originally developed by Ben Oberin. +# Modified for HLstats by Simon Garner. +# Modified for HLstatsX by Tobias Oetzel. +# +# ObLegalStuff: +# Copyright (c) 2000 Bek Oberin. All rights reserved. This program is +# free software; you can redistribute it and/or modify it under the +# same terms as Perl itself. +# + +use strict; +use vars qw($VERSION @ISA @EXPORT @EXPORT_OK); + +require Exporter; + +@ISA = qw(Exporter); +@EXPORT = qw(); +@EXPORT_OK = qw(); + +$VERSION = "1.0"; + +my $DEBUG = 0; + +=head1 NAME + +ConfigReader::Simple - Simple configuration file parser + +=head1 SYNOPSIS + + use ConfigReader::Simple; + + $config = ConfigReader::Simple->new("configrc", [qw(Foo Bar Baz Quux)]); + + $config->parse(); + + $config->get("Foo"); + + +=head1 DESCRIPTION + + C reads and parses simple configuration files. It's + designed to be smaller and simpler than the C module + and is more suited to simple configuration files. + +=cut + +################################################################### +# Functions under here are member functions # +################################################################### + +=head1 CONSTRUCTOR + +=item new ( FILENAME, DIRECTIVES ) + +This is the constructor for a new ConfigReader::Simple object. + +C tells the instance where to look for the configuration +file. + +C is an optional argument and is a reference to an array. +Each member of the array should contain one valid directive. A directive +is the name of a key that must occur in the configuration file. If it +is not found, the module will die. The directive list may contain all +the keys in the configuration file, a sub set of keys or no keys at all. + +=cut + +sub new { + my $prototype = shift; + my $filename = shift; + my $keyref = shift; + + my $class = ref($prototype) || $prototype; + my $self = {}; + + $self->{"filename"} = $filename; + $self->{"validkeys"} = $keyref; + + bless($self, $class); + return $self; +} + + +# +# destructor +# +sub DESTROY { + my $self = shift; + + return 1; +} + +=pod +=item parse () + +This does the actual work. No parameters needed. + +=cut + +sub parse { + my $self = shift; + + open(CONFIG, $self->{"filename"}) || + die "Config: Can't open config file " . $self->{"filename"} . ": $!"; + + my @array_buffer; + my $ext_option = 0; + my $parsed_line = 0; + + while () { + chomp; + next if /^\s*$/; # blank + next if /^\s*#/; # comment + next if /^\s*.*\[[0-9]+\]\s*=\s*\(/; # old style server config start + next if /^\s*.*\s*=>\s*\.*".*\",/; # old style server config option + + $parsed_line = 0; + my $input_text = $_; + + if (($ext_option == 0) && ($parsed_line == 0)) { + my ($key, $value) = &parse_line($input_text); + warn "Key: '$key' Value: '$value'\n" if $DEBUG; + $self->{"config_data"}{$key} = $value; + } + } + close(CONFIG); + + return 1; + +} + +=pod +=item get ( DIRECTIVE ) + +Returns the parsed value for that directive. + +=cut + +sub get { + my $self = shift; + my $key = shift; + + unless (ref $self->{"config_data"}{$key}) { + return $self->{"config_data"}{$key}; + } else { + return %{$self->{"config_data"}{$key}}; + } +} + +# Internal methods + +sub parse_line { + my $text = shift; + + my ($key, $value); + + if ($text =~ /^\s*(\w+)\s+(['"]?)(.*?)\2\s*$/) { + $key = $1; + $value = $3; + } else { + die "Config: Can't parse line: $text\n"; + } + + return ($key, $value); +} + + +=pod + +=head1 LIMITATIONS/BUGS + +Directives are case-sensitive. + +If a directive is repeated, the first instance will silently be +ignored. + +Always die()s on errors instead of reporting them. + +C doesn't warn if used before C. + +C doesn't warn if you try to acces the value of an +unknown directive not know (ie: one that wasn't passed via C). + +All these will be addressed in future releases. + +=head1 CREDITS + +Kim Ryan adapted the module to make declaring +keys optional. Thanks Kim. + +=head1 AUTHORS + +Bek Oberin + +=head1 COPYRIGHT + +Copyright (c) 2000 Bek Oberin. All rights reserved. + +This program is free software; you can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut + +# +# End code. +# +1; diff --git a/scripts/GeoLiteCity/GeoLite_Import.sh b/scripts/GeoLiteCity/GeoLite_Import.sh new file mode 100644 index 0000000..2ce1f2e --- /dev/null +++ b/scripts/GeoLiteCity/GeoLite_Import.sh @@ -0,0 +1,86 @@ +#!/bin/sh +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + +# Configure the variables below + +# Set this value to 1 if you are running Gentoo linux, or any other linux distro where the "cal" command outputs not Sunday as the first day in every row! +LINUX_OTHER="0" + +# Login information for your MySQL server +DBHOST="localhost" +DBNAME="" +DBUSER="" +DBPASS="" + +# +# Nothing to change below here. +# + + +# database is updated every first tuesday of any month, so download it with that specific date and import it +TODAY_MONTH=$( date +%m ) +TODAY_YEAR=$( date +%Y ) +if [ $LINUX_OTHER == "1" ] + then CAL_COMMAND="cal -s" + else CAL_COMMAND="cal" +fi +FIRST_TUESDAY_MONTH=$( $CAL_COMMAND $TODAY_MONTH $TODAY_YEAR | + awk ' + NR == 1 { next } + NR == 2 { next } + NF <= 4 { next } + NF == 5 { print $1 ; exit } + NF == 6 { print $2 ; exit } + NF == 7 { print $3 ; exit } + ' ) + +DATE=""$TODAY_YEAR""$TODAY_MONTH"0"$FIRST_TUESDAY_MONTH"" +DIR="GeoLiteCity_$DATE" +FILE="GeoLiteCity_$DATE.zip" +ls *.csv &>/dev/null && rm *.csv +[ -f $FILE ] || wget http://geolite.maxmind.com/download/geoip/database/GeoLiteCity_CSV/$FILE || exit 1 +unzip -o $FILE || exit 1 +mv $DIR/GeoLiteCity-Blocks.csv geoLiteCity_Blocks.csv +mv $DIR/GeoLiteCity-Location.csv geoLiteCity_Location.csv.temp +iconv -f ISO-8859-1 -t UTF-8 geoLiteCity_Location.csv.temp > geoLiteCity_Location.csv +mysqlimport -C -d --fields-terminated-by=, --fields-enclosed-by=\" --ignore-lines=2 --default-character-set=utf8 -L -i -h $DBHOST -u $DBUSER --password=$DBPASS $DBNAME geoLiteCity_Blocks.csv +mysqlimport -C -d --fields-terminated-by=, --fields-enclosed-by=\" --ignore-lines=2 --default-character-set=utf8 -L -i -h $DBHOST -u $DBUSER --password=$DBPASS $DBNAME geoLiteCity_Location.csv + +# Cleanup +ls *.csv &>/dev/null && rm *.csv +ls *.csv.temp &>/dev/null && rm *.csv.temp +rm $FILE +rmdir $DIR diff --git a/scripts/GeoLiteCity/README b/scripts/GeoLiteCity/README new file mode 100644 index 0000000..d119887 --- /dev/null +++ b/scripts/GeoLiteCity/README @@ -0,0 +1,17 @@ +Included in this folder are two scripts to import data from the MaxMind (http://www.maxmind.com) from the GeoLiteCity database into your HLX db. + +For Linux: +Edit the GeoLite_Import.sh file to insert your database settings in the top section and then execute. + +For Windows: +Download the HLstatsX CE GeoLiteCity Importer from http://code.google.com/p/hlstatsxcommunity + + +-------------------------------------------------------------- +Alternative method by *XYZ*SaYnt: +-------------------------------------------------------------- +* For Windows or Linux. +* Requires python. +* Generates INSERTs in case LOAD DATA command is disabled + +Edit the geoip.py file to insert your database settings in the top section and then execute. \ No newline at end of file diff --git a/scripts/GeoLiteCity/geoip.py b/scripts/GeoLiteCity/geoip.py new file mode 100644 index 0000000..20aadb5 --- /dev/null +++ b/scripts/GeoLiteCity/geoip.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com + +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ + +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) + +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner + +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +# For support and installation notes visit http://www.hlxcommunity.com + + +import os,sys + +# version 1.0 alpha contributed to hlstatsx community by *XYZ*SaYnt + +DBHOST="localhost" +DBNAME="your_hlstats_db" +DBUSER="your_sql_username" +DBPASS="your_sql_password" + + +def system(cmd): + """ + executes a system call and returns the text as a string. Returns only the first line of output. + """ + print("EXECUTING: %s"%cmd) + f = os.popen(cmd) + output = f.readlines() + f.close() + if len(output) > 0: + return output[0].replace("\n","") + else: + return "" + + +def fetch_geodata(): + """ + Obtains the geoLiteCity raw data, resulting in geoLiteCity_Location.csv and geoLiteCity_Blocks.csv + """ + # database is updated on 1st every month, so download the file from the 1st of current month + DAT = system("date +%Y%m01") + FIL = "GeoLiteCity_%s"%DAT + FILE = FIL + ".zip" + system("rm *.csv > /dev/null") + if not os.path.exists(FILE): + system("wget http://geolite.maxmind.com/download/geoip/database/GeoLiteCity_CSV/" + FILE) + system("unzip -o " + FILE) + + system("mv %s/GeoLiteCity-Blocks.csv geoLiteCity_Blocks.csv"%FIL) + system("mv %s/GeoLiteCity-Location.csv geoLiteCity_Location.csv.temp"%FIL) + system("rmdir " + FIL) + system("iconv -f ISO-8859-1 -t UTF-8 geoLiteCity_Location.csv.temp > geoLiteCity_Location.csv") + + return + + +def dump_sql(fname): + """ + Dump the new sql data into our database + """ + system("mysql -u %s -p%s -h %s %s < %s"%(DBUSER,DBPASS,DBHOST,DBNAME,fname)) + + +def write_sql(fname): + """ + Write a file of sql commands so that our data can be imported into our database. + """ + try: + fout = open(fname,"w") + except: + print("ERROR: unable to open "+fname) + return 0 + + fout.write(""" + DROP TABLE IF EXISTS `geoLiteCity_Blocks`; + DROP TABLE IF EXISTS `geolitecity_blocks`; + DROP TABLE IF EXISTS `geolitecity_location`; + DROP TABLE IF EXISTS `geoLiteCity_Location`; + + CREATE TABLE `geoLiteCity_Blocks` + (`startIpNum` bigint(11) unsigned NOT NULL default '0', + `endIpNum` bigint(11) unsigned NOT NULL default '0', + `locId` bigint(11) unsigned NOT NULL default '0' + ) ENGINE=MyISAM DEFAULT CHARSET=utf8; + + CREATE TABLE `geoLiteCity_Location` ( + `locId` bigint(11) unsigned NOT NULL default '0', + `country` varchar(2) NOT NULL, + `region` varchar(50) default NULL, + `city` varchar(50) default NULL, + `postalCode` varchar(10) default NULL, + `latitude` decimal(14,4) default NULL, + `longitude` decimal(14,4) default NULL, + PRIMARY KEY (`locId`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8; + """) + + # read in the raw data + f = open("geoLiteCity_Blocks.csv") + raw = f.readlines() + f.close() + del raw[0:2] + + # chunk up the raw data + chunksize = 1000 + gblocks = [] + chunks = len(raw)/chunksize + 1 + for i in xrange(0,chunks): gblocks.append(raw[i*chunksize:(i+1)*chunksize]) + print("SQL: %d data items, %d chunks"%(len(raw),chunks)) + del raw + + for chunk in gblocks: + if chunk: + s = "insert into `geoLiteCity_Blocks`(`startIpNum`,`endIpNum`,`locId`) values" + for l in chunk: + vals = l.replace('"',"").replace('\n',"").split(',') + s += "(%s,%s,%s),"%(vals[0],vals[1],vals[2]) + s = s[0:-1] # chop the last comma + fout.write(s + ";\n"); + del gblocks + + + + + f = open("geoLiteCity_Location.csv") + raw = f.readlines() + f.close() + del raw[0:2] + + # chunk up the raw data + gblocks = [] + chunks = len(raw)/chunksize + 1 + for i in xrange(0,chunks): gblocks.append(raw[i*chunksize:(i+1)*chunksize]) + print("SQL: %d data items, %d chunks"%(len(raw),chunks)) + del raw + + for chunk in gblocks: + if chunk: + s = "insert into `geoLiteCity_Location`(`locId`,`country`,`region`,`city`,`postalCode`,`latitude`,`longitude`) values" + for l in chunk: + vals = l.replace('"',"").replace('\n',"").split(',') + for i,v in enumerate(vals): vals[i] = v.replace("'","\\'") + s += "(%s,'%s','%s','%s','%s','%s','%s'),"%(vals[0],vals[1],vals[2],vals[3],vals[4],vals[5],vals[6]) + s = s[0:-1] # chop the last comma + fout.write(s + ";\n"); + del gblocks + + return 1 + + +def main(): + sqlname = "geodata.sql" + print("DOWNLOADING GEO DATA....") + fetch_geodata() + print("WRITING DATABASE FILE....") + if write_sql(sqlname): + print("IMPORTING DATABASE FILE....") + dump_sql(sqlname) + else: + print("Fatal error; unable to finish.") + + # clean up. + system("rm "+sqlname) + + print("DONE.") + + + +main() + diff --git a/scripts/GeoLiteCity/install_binary.sh b/scripts/GeoLiteCity/install_binary.sh new file mode 100644 index 0000000..b0b3ae4 --- /dev/null +++ b/scripts/GeoLiteCity/install_binary.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + +# *** You should not need to configure anything in this script. *** +# URL is the location to download the GeoLiteCity.dat.gz file from. +URL=http://geolite.maxmind.com/download/geoip/database/ + +# File is the file to download from the server above, as well as what to store it as locally. +FILE=GeoLiteCity.dat.gz + +# ***** NOTHING TO CONFIGURE BELOW HERE ***** + +# Change to directory where installer is +cd `dirname $0` + +echo Downloading a new copy of GeoLiteCity, if needed. +wget -N -q $URL$FILE +echo Uncompressing database +gzip -dc < $FILE > $(basename $FILE .gz) +chmod 777 *.dat +echo Done diff --git a/scripts/GeoLiteCity/ip2number.php b/scripts/GeoLiteCity/ip2number.php new file mode 100644 index 0000000..42a453a --- /dev/null +++ b/scripts/GeoLiteCity/ip2number.php @@ -0,0 +1,49 @@ +=$number; + +?> diff --git a/scripts/HLStatsFTP/README b/scripts/HLStatsFTP/README new file mode 100644 index 0000000..bdb9602 --- /dev/null +++ b/scripts/HLStatsFTP/README @@ -0,0 +1,31 @@ +hlstats-ftp.pl README + +Utility written by Woody. + +This utility can be used to grab log files from a remote game server through FTP and then use the HLStatsX Daemon to enter +the data into the database. You must have perl installed to use this utility. + +The script "remembers" the modification time (mtime) of the last log file that has been processed, and when run again, +only processes files with a later mtime. + +*** NOTE *** +To use this utility you must copy it into the same folder your hlstats.pl (the daemon) is in. +You probably want to configure this as a cronjob as well. + + +Usage: + hlstats-ftp.pl --help + hlstats-ftp.pl --version + hlstats-ftp.pl --gs-ip=GSIP --gs-port=GSPORT [--ftp-ip=FTPIP] --ftp-usr=FTPUSR --ftp-pwd=FTPPWD --ftp-dir=FTPDIR [--quiet] + +Options: + --help display this help and exit + --version output version information and exit + --gs-ip=IP game server IP address + --gs-port=PORT game server port + --ftp-ip=IP ftp log server IP address, if different from + game server IP address (--gs-ip) + --ftp-usr=USERNAME ftp log server user name + --ftp-pwd=PASSWORD ftp log server password + --ftp-dir=DIRECTORY ftp log server directory + --quiet quiet operation, i.e. no output diff --git a/scripts/HLStatsFTP/hlstats-ftp.pl b/scripts/HLStatsFTP/hlstats-ftp.pl new file mode 100644 index 0000000..15c5cbb --- /dev/null +++ b/scripts/HLStatsFTP/hlstats-ftp.pl @@ -0,0 +1,235 @@ +#!/usr/bin/perl +################################################################################################################ +# hlstats-ftp.pl +################################################################################################################ +# Feed HLstatsX with log data via FTP +# (w) 2009 by Woody (http://woodystree.net) +################################################################################################################ + +use strict; +use Getopt::Long; +use Net::FTP; + +use constant VERSION => "0.42"; +use constant USAGE => < --gs-port= [--ftp-ip=] --ftp-usr= --ftp-pwd= --ftp-dir= [--quiet] + +Options: + --help display this help and exit + --version output version information and exit + --gs-ip=IP game server IP address + --gs-port=PORT game server port + --ftp-ip=IP ftp log server IP address, if different from + game server IP address (--gs-ip) + --ftp-usr=USR ftp log server user name + --ftp-pwd=PWD ftp log server password + --ftp-dir=DIR ftp log server directory + --quiet quiet operation, i.e. no output + +(w)2009 by Woody (http://woodystree.net) + + +EOT +; + + + +################################################################################################################ +# define output subroutine +################################################################################################################ +sub output { + if (!(our $opt_quiet)) { + my $text = shift; + print $text; + } +} + + + +################################################################################################################ +# get & parse command line options +################################################################################################################ +my $opt_help; my $opt_version; my $gs_ip; my $gs_port; my $ftp_ip; my $ftp_usr; my $ftp_pwd; my $ftp_dir; our $opt_quiet; +GetOptions( + "help" => \$opt_help, + "version" => \$opt_version, + "gs-ip=s" => \$gs_ip, + "gs-port=i" => \$gs_port, + "ftp-ip=s" => \$ftp_ip, + "ftp-usr=s" => \$ftp_usr, + "ftp-pwd=s" => \$ftp_pwd, + "ftp-dir=s" => \$ftp_dir, + "quiet" => \$opt_quiet +) or die(USAGE); + +if ($opt_help) { + print USAGE; + exit(0); +} + +if ($opt_version) { + print "\nhlstats-ftp.pl - Version " . VERSION . "\n"; + print "Feed HLstatsX with log data via FTP.\n"; + print "(w)2009 by Woody (http://woodystree.net)\n\n"; + exit(0); +} + +if (!(defined $gs_ip) && !(defined $gs_port) && !(defined $ftp_usr) && !(defined $ftp_pwd) && !(defined $ftp_dir)) { + die(USAGE); +} + +if (!(defined $ftp_ip)) { + $ftp_ip = $gs_ip; +} + + + +################################################################################################################ +# OK, lets go... +################################################################################################################ +output("\nStarting hlstats-ftp.pl for IP $gs_ip, Port $gs_port...\n\n"); + + + +################################################################################################################ +# create tmp directory +################################################################################################################ +output(" - creating tmp directory... "); + +my $tmp_dir = "hlstats-ftp-$gs_ip-$gs_port.tmp"; + +if (-e $tmp_dir) { + if (!(-w $tmp_dir)) { + die "Writing to tmp directory is not possible."; + } +} else { + mkdir($tmp_dir, 0775) || die "Make tmp directory \"$tmp_dir\" failed: $!"; +} + +output("OK.\n"); + + + +################################################################################################################ +# get last mtime info, if any +################################################################################################################ +output(" - getting last mtime info... "); + +my $last_mtime_filename = "hlstats-ftp-$gs_ip-$gs_port.last"; +my $last_mtime = 0; + +if (-e $last_mtime_filename) { + open(LASTMTIME, "<$last_mtime_filename") || die "Open file \"$last_mtime_filename\" failed: $!"; + $last_mtime = ; + close(LASTMTIME); + output("OK: last mtime $last_mtime.\n"); +} else { + output("none: using default 0.\n"); +} + + + +################################################################################################################ +# establish ftp connection +################################################################################################################ +output(" - establishing FTP connection... "); + +my $ftp = Net::FTP->new($ftp_ip) || die "FTP connect to \"$ftp_ip\" failed: $!"; +$ftp->login($ftp_usr,$ftp_pwd) || die "FTP login for user \"$ftp_usr\" failed: $!"; +$ftp->binary() || die "FTP binary mode failed: $!"; +$ftp->cwd($ftp_dir) || die "FTP chdir to \"$ftp_dir\" failed: $!"; + +output("OK.\n"); + + + +################################################################################################################ +# get complete list of log files +################################################################################################################ +output(" - getting complete list of log files... "); + +my @files = $ftp->ls("-t *.log"); # get list of log files sorted by mtime +$#files = $#files - 1; # skip last file, i.e. the latest file, which is possibly still in use by the game server + +output("OK.\n"); + + + +################################################################################################################ +# transfer log files to tmp directory, if todo +################################################################################################################ +output(" + transfering log files, if todo:\n"); + +my @todo_files = (); +my @todo_mtimes = (); + +foreach my $file (@files) { + output(" - \"$file\" "); + my $mtime = $ftp->mdtm($file) || die "FTP mtdm failed: $!"; + output("(mtime $mtime): "); + if ($mtime > $last_mtime) { + output("transferring... "); + $ftp->get($file, "$tmp_dir/$file") || die "FTP get with \"$file\" failed: $!"; + push(@todo_files, $file); + push(@todo_mtimes, $mtime); + output("OK.\n"); + } else { + output("skipping.\n"); + } +} + + + +################################################################################################################ +# close ftp connection +################################################################################################################ +output(" - closing FTP connection... "); + +$ftp->close() || die "FTP close failed: $!"; + +output("OK.\n"); + + + +################################################################################################################ +# process log files in tmp directory +################################################################################################################ +output(" + parsing log files:\n"); + +for (my $i = 0; $i <= $#todo_files; $i++) { + my $progress = "(" . ($i+1) . "/" . ($#todo_files+1) . ")"; + output(" - \"" . $todo_files[$i] . "\" " . $progress . ": parsing... "); + system("./hlstats.pl --stdin --server-ip $gs_ip --server-port $gs_port < $tmp_dir/" . $todo_files[$i] . " > /dev/null"); + output("updating last mtime..."); + open(LASTMTIME, ">$last_mtime_filename") || die "Open file \"$last_mtime_filename\" failed: $!"; + print LASTMTIME $todo_mtimes[$i]; + close(LASTMTIME); + output("OK.\n"); +} + + + +################################################################################################################ +# delete tmp log files and directory +################################################################################################################ +output(" - delete tmp log files and directory... "); + +foreach my $file (<$tmp_dir/*>) { + unlink($file) || die "Delete tmp log files failed: $!"; +} +rmdir($tmp_dir) || die "Delete tmp directory failed: $!"; + +output("OK.\n"); + + + +################################################################################################################ +# the end +################################################################################################################ +output("\nSo Long, and Thanks for all the Fish.\n\n"); diff --git a/scripts/HLstats.plib b/scripts/HLstats.plib new file mode 100644 index 0000000..072eeb8 --- /dev/null +++ b/scripts/HLstats.plib @@ -0,0 +1,455 @@ +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + + +# HLstatsX CE release version number + +$g_version = ""; +my %db_stmt_cache = (); + +%g_eventTables = ( + "TeamBonuses", + ["playerId", "actionId", "bonus"], + "ChangeRole", + ["playerId", "role"], + "ChangeName", + ["playerId", "oldName", "newName"], + "ChangeTeam", + ["playerId", "team"], + "Connects", + ["playerId", "ipAddress", "hostname", "hostgroup"], + "Disconnects", + ["playerId"], + "Entries", + ["playerId"], + "Frags", + ["killerId", "victimId", "weapon", "headshot", "killerRole", "victimRole", "pos_x","pos_y","pos_z", "pos_victim_x","pos_victim_y","pos_victim_z"], + "PlayerActions", + ["playerId", "actionId", "bonus", "pos_x","pos_y","pos_z"], + "PlayerPlayerActions", + ["playerId", "victimId", "actionId", "bonus", "pos_x","pos_y","pos_z", "pos_victim_x","pos_victim_y","pos_victim_z"], + "Suicides", + ["playerId", "weapon", "pos_x","pos_y","pos_z"], + "Teamkills", + ["killerId", "victimId", "weapon", "pos_x","pos_y","pos_z", "pos_victim_x","pos_victim_y","pos_victim_z"], + "Rcon", + ["type", "remoteIp", "password", "command"], + "Admin", + ["type", "message", "playerName"], + "Statsme", + ["playerId", "weapon", "shots", "hits", "headshots", "damage", "kills", "deaths"], + "Statsme2", + ["playerId", "weapon", "head", "chest", "stomach", "leftarm", "rightarm", "leftleg", "rightleg"], + "StatsmeLatency", + ["playerId", "ping"], + "StatsmeTime", + ["playerId", "time"], + "Latency", + ["playerId", "ping"], + "Chat", + ["playerId", "message_mode", "message"] +); + + +## +## Common Functions +## + +sub number_format { + local $_ = shift; + 1 while s/^(-?\d+)(\d{3})/$1,$2/; + return $_; +} + +sub date_format { + my $timestamp = shift; + return sprintf('%dd %02d:%02d:%02dh', + $timestamp / 86400, + $timestamp / 3600 % 24, + $timestamp / 60 % 60, + $timestamp % 60 + ); +} + + + +# +# void error (string errormsg) +# +# Dies, and optionally mails error messages to $g_mailto. +# + +sub error +{ + my $errormsg = $_[0]; + + if ($g_mailto && $g_mailpath) + { + system("echo \"$errormsg\" | $g_mailpath -s \"HLstatsX:CE crashed `date`\" $g_mailto"); + } + + die("$errormsg\n"); +} + + +# +# string quoteSQL (string varQuote) +# +# Escapes all quote characters in a variable, making it suitable for use in an +# SQL query. Returns the escaped version. +# + +sub quoteSQL +{ + my $varQuote = $_[0]; + + $varQuote =~ s/\\/\\\\/g; # replace \ with \\ + $varQuote =~ s/'/\\'/g; # replace ' with \' + + return $varQuote; +} + +# +# void doConnect +# +# Connects to the HLstatsX database +# + +sub doConnect +{ + $db_conn = DBI->connect( + "DBI:mysql:$db_name:$db_host", + $db_user, $db_pass, { mysql_enable_utf8 => 1 } + ); + while(!$db_conn) { + &printEvent("MYSQL", "\nCan't connect to MySQL database '$db_name' on '$db_host'\n" . + "Server error: $DBI::errstr\n"); + sleep(5); + $db_conn = DBI->connect( + "DBI:mysql:$db_name:$db_host", + $db_user, $db_pass, { mysql_enable_utf8 => 1 } + ); + } + $db_conn->do("SET NAMES 'utf8'"); + &printEvent("MYSQL", "Connecting to MySQL database '$db_name' on '$db_host' as user '$db_user' ... connected ok", 1); + %db_stmt_cache = (); +} + +# +# result doQuery (string query) +# +# Executes the SQL query 'query' and returns the result identifier. +# + +sub doQuery +{ + my ($query, $callref) = @_; + if(!$db_conn->ping()) { + &printEvent("HLSTATSX", "Lost database connection. Trying to reconnect...", 1); + &doConnect(); + } + + my $result = $db_conn->prepare($query) or die("Unable to prepare query:\n$query\n$DBI::errstr\n$callref"); + $result->execute or die("Unable to execute query:\n$query\n$DBI::errstr\n$callref"); + + return $result; +} + +sub execNonQuery +{ + my ($query) = @_; + if(!$db_conn->ping()) { + &printEvent("HLSTATSX", "Lost database connection. Trying to reconnect...", 1); + &doConnect(); + } + #&printEvent("DEBUG","execNonQuery:\n".$query); + $db_conn->do($query); +} + +sub execCached { + my ($query_id,$query, @bind_args) = @_; + + if(!$db_conn->ping()) { + &printEvent("HLSTATSX", "Lost database connection. Trying to reconnect...", 1); + &doConnect(); + } + + if(!$db_stmt_cache{$query_id}) { + $db_stmt_cache{$query_id} = $db_conn->prepare($query) or die("Unable to prepare query ($query_id):\n$query\n$DBI::errstr"); + #&printEvent("HLSTATSX", "Prepared a statement ($query_id) for the first time.", 1); + } + $db_stmt_cache{$query_id}->execute(@bind_args) or die ("Unable to execute query ($query_id):\n$query\n$DBI::errstr"); + return $db_stmt_cache{$query_id}; +} + +# +# string resolveIp (string ip, boolean quiet) +# +# Do a DNS reverse-lookup on an IP address and return the hostname, or empty +# string on error. +# + +sub resolveIp +{ + my ($ip, $quiet) = @_; + my ($host) = ""; + + unless ($g_dns_resolveip) + { + return ""; + } + + + eval + { + $SIG{ALRM} = sub { die "DNS Timeout\n" }; + alarm $g_dns_timeout; # timeout after $g_dns_timeout sec + $host = gethostbyaddr(inet_aton($ip), AF_INET); + alarm 0; + }; + + if ($@) + { + my $error = $@; + chomp($error); + printEvent("DNS", "Resolving hostname (timeout $g_dns_timeout sec) for IP \"$ip\" - $error ", 1); + $host = ""; # some error occurred + } + elsif (!defined($host)) + { + printEvent("DNS", "Resolving hostname (timeout $g_dns_timeout sec) for IP \"$ip\" - No Host ", 1); + $host = ""; # ip did not resolve to any host + } else { + $host = lc($host); # lowercase + printEvent("DNS", "Resolving hostname (timeout $g_dns_timeout sec) for IP \"$ip\" - $host ", 1); + } + chomp($host); + return $host; +} + + +# +# object queryHostGroups () +# +# Returns result identifier. +# + +sub queryHostGroups +{ + return &doQuery(" + SELECT + pattern, + name, + LENGTH(pattern) AS patternlength + FROM + hlstats_HostGroups + ORDER BY + patternlength DESC, + pattern ASC + "); +} + + +# +# string getHostGroup (string hostname[, object result]) +# +# Return host group name if any match, or last 2 or 3 parts of hostname. +# + +sub getHostGroup +{ + my ($hostname, $result) = @_; + my $hostgroup = ""; + + # User can define special named hostgroups in hlstats_HostGroups, i.e. + # '.adsl.someisp.net' => 'SomeISP ADSL' + + $result = &queryHostGroups() unless ($result); + $result->execute(); + + while (my($pattern, $name) = $result->fetchrow_array()) + { + $pattern = quotemeta($pattern); + $pattern =~ s/\\\*/[^.]*/g; # allow basic shell-style globbing in pattern + if ($hostname =~ /$pattern$/) + { + $hostgroup = $name; + last; + } + } + $result->finish; + + if (!$hostgroup) + { + # + # Group by last 2 or 3 parts of hostname, i.e. 'max1.xyz.someisp.net' as + # 'someisp.net', and 'max1.xyz.someisp.net.nz' as 'someisp.net.nz'. + # Unfortunately some countries do not have categorical SLDs, so this + # becomes more complicated. The dom_nosld array below contains a list of + # known country codes that do not use categorical second level domains. + # If a country uses SLDs and is not listed below, then it will be + # incorrectly grouped, i.e. 'max1.xyz.someisp.yz' will become + # 'xyz.someisp.yz', instead of just 'someisp.yz'. + # + # Please mail sgarner@hlstats.org with any additions. + # + + my @dom_nosld = ( + "ca", # Canada + "ch", # Switzerland + "be", # Belgium + "de", # Germany + "ee", # Estonia + "es", # Spain + "fi", # Finland + "fr", # France + "ie", # Ireland + "nl", # Netherlands + "no", # Norway + "ru", # Russia + "se", # Sweden + ); + + my $dom_nosld = join("|", @dom_nosld); + + if ($hostname =~ /([\w-]+\.(?:$dom_nosld|\w\w\w))$/) + { + $hostgroup = $1; + } + elsif ($hostname =~ /([\w-]+\.[\w-]+\.\w\w)$/) + { + $hostgroup = $1; + } + else + { + $hostgroup = $hostname; + } + } + + return $hostgroup; +} + + +# +# void doConf (object conf, hash directives) +# +# Walk through configuration directives, setting values of global variables. +# + +sub doConf +{ + my ($conf, %directives) = @_; + + while (($directive, $variable) = each(%directives)) + { + if ($directive eq "Servers") { + %$variable = $conf->get($directive); + } else { + $$variable = $conf->get($directive); + } + } + +} + +# +# void setOptionsConf (hash optionsconf) +# +# Walk through configuration directives, setting values of global variables. +# + +sub setOptionsConf +{ + my (%optionsconf) = @_; + + while (($thekey, $theval) = each(%optionsconf)) + { + if($theval) + { + $$thekey = $theval; + } + } + +} + + +# +# string abbreviate (string thestring[, int maxlength) +# +# Returns thestring abbreviated to maxlength-3 characters plus "...", unless +# thestring is shorter than maxlength. +# + +sub abbreviate +{ + my ($thestring, $maxlength) = @_; + + $maxlength = 12 unless ($maxlength); + + if (length($thestring) > $maxlength) + { + $thestring = substr($thestring, 0, $maxlength - 3); + return "$thestring..."; + } + else + { + return $thestring; + } +} + + +# +# void printEvent (int code, string description) +# +# Logs event information to stdout. +# + +sub printEvent +{ + my ($code, $description, $update_timestamp, $force_output) = @_; + if ( (($g_debug > 0) && ($g_stdin == 0))|| (($g_stdin == 1) && ($force_output == 1)) ) { + my ($sec,$min,$hour,$mday,$mon,$year) = localtime(time()); + my $timestamp = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec); + if ($update_timestamp == 0) { + $timestamp = $ev_timestamp; + } + if (is_number($code)) { + printf("%s: %21s - E%03d: %s\n", $timestamp, $s_addr, $code, $description); + } else { + printf("%s: %21s - %s: %s\n", $timestamp, $s_addr, $code, $description); + } + } +} + +1; diff --git a/scripts/HLstats_EventHandlers.plib b/scripts/HLstats_EventHandlers.plib new file mode 100644 index 0000000..3d9d4b7 --- /dev/null +++ b/scripts/HLstats_EventHandlers.plib @@ -0,0 +1,3096 @@ +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + +sub get_fav_weapon { + + my ($player) = @_; + my $p_connect_time = $player->{connect_time}; + + my $result = &doQuery(" + SELECT + hlstats_Events_Frags.weapon, + COUNT(hlstats_Events_Frags.weapon) AS kills, + SUM(hlstats_Events_Frags.headshot=1) as headshots + FROM + hlstats_Events_Frags,hlstats_Servers + WHERE + hlstats_Servers.serverId=hlstats_Events_Frags.serverId + AND hlstats_Servers.game='"."eSQL($g_servers{$s_addr}->{game})."' AND hlstats_Events_Frags.killerId=".$player->{playerid}." + GROUP BY + hlstats_Events_Frags.weapon + ORDER BY + kills desc, headshots desc + LIMIT 1 + "); + my ($fav_weapon, $fav_weapon_kills, $fav_weapon_headshots) = $result->fetchrow_array; + $result->finish; + + my $result = &doQuery(" + SELECT + IFNULL(ROUND((SUM(hlstats_Events_Statsme.hits) / SUM(hlstats_Events_Statsme.shots) * 100), 0), 0) AS acc + FROM + hlstats_Events_Statsme, hlstats_Servers + WHERE + hlstats_Servers.serverId=hlstats_Events_Statsme.serverId + AND hlstats_Servers.game='"."eSQL($g_servers{$s_addr}->{game})."' AND hlstats_Events_Statsme.PlayerId=".$player->{playerid}." + AND hlstats_Events_Statsme.weapon='"."eSQL($fav_weapon)."' + LIMIT 0,1 + "); + my ($fav_weapon_sm_acc) = $result->fetchrow_array; + $result->finish; + + my $result = &doQuery(" + SELECT + hlstats_Events_Frags.weapon, + COUNT(hlstats_Events_Frags.weapon) AS kills, + SUM(hlstats_Events_Frags.headshot=1) as headshots + FROM + hlstats_Events_Frags,hlstats_Servers + WHERE + hlstats_Servers.serverId=hlstats_Events_Frags.serverId + AND hlstats_Servers.game='"."eSQL($g_servers{$s_addr}->{game})."' AND hlstats_Events_Frags.killerId='".$player->{playerid}."' + AND (hlstats_Events_Frags.eventTime > FROM_UNIXTIME(".$p_connect_time.")) + GROUP BY + hlstats_Events_Frags.weapon + ORDER BY + kills desc, headshots desc + LIMIT 0, 1 + "); + + my ($s_fav_weapon, $s_fav_weapon_kills, $s_fav_weapon_headshots) = $result->fetchrow_array; + $result->finish; + my $s_fav_weapon_sm_acc = 0; + + if (($s_fav_weapon ne "") && ($s_fav_weapon_kills > 0)) { + my $result = &doQuery(" + SELECT + IFNULL(ROUND((SUM(hlstats_Events_Statsme.hits) / SUM(hlstats_Events_Statsme.shots) * 100), 0), 0) AS acc + FROM + hlstats_Events_Statsme, hlstats_Servers + WHERE + hlstats_Servers.serverId=hlstats_Events_Statsme.serverId + AND hlstats_Servers.game='"."eSQL($g_servers{$s_addr}->{game})."' AND hlstats_Events_Statsme.PlayerId='".$player->{playerid}."' + AND hlstats_Events_Statsme.weapon='"."eSQL($s_fav_weapon)."' + AND (hlstats_Events_Statsme.eventTime > FROM_UNIXTIME(".$p_connect_time.")) + LIMIT 0,1 + "); + ($s_fav_weapon_sm_acc) = $result->fetchrow_array; + $result->finish; + } + + return ($g_games{$g_servers{$s_addr}->{game}}{weapons}{$fav_weapon}{name}, $fav_weapon_kills, $fav_weapon_sm_acc, $g_games{$g_servers{$s_addr}->{game}}{weapons}{$s_fav_weapon}{name}, $s_fav_weapon_kills, $s_fav_weapon_sm_acc); +} + +sub get_next_ranks +{ + my ($player) = @_; + + my $result = &doQuery(" + SELECT + $g_ranktype, + kills / IF(deaths=0,1,deaths) AS kpd + FROM + hlstats_Players + WHERE + playerId=" . $player->{playerid} + ); + my ($base, $kpd) = $result->fetchrow_array; + $result->finish; + my $playerName = $player->{name}; + + if (!$base) { + $base = 0; + } + + my $ranknumber = $player->getRank(); + + my $rankword = ""; + if ($g_ranktype eq "kills") { + $rankword = " kills"; + } + my $osd_message = "->1 - Next Players\\n".sprintf(" %02d %s%s - %s", $ranknumber, &number_format($base), $rankword, $playerName)."\\n"; + + my $result = &doQuery(" + SELECT + playerId, + lastName, + $g_ranktype, + kills / IF(deaths=0,1,deaths) AS kpd + FROM + hlstats_Players + WHERE + game='"."eSQL($player->{game})."' + AND $g_ranktype = $base + AND hideranking = 0 + AND kills >= 1 + AND playerId <> " . $player->{playerid} . " + HAVING + kpd>=$kpd + ORDER BY $g_ranktype, kpd + LIMIT 0,10 + "); + + my $player_count = 0; + my $i = $ranknumber; + while (($player_count < 11) && (my ($playerId, $lastName, $p_base, $kpd) = $result->fetchrow_array)) { + $i--; + if (length($lastName) > 20) { + $lastName = substr($lastName, 0, 17)."..."; + } + $player_count++; + $osd_message .= sprintf(" %02d %s%s +%04d %s", $i, &number_format($p_base), $rankword, &number_format($p_base-$base), $lastName)."\\n"; + } + $result->finish; + + if ($player_count < 11) { + my $result = &doQuery(" + SELECT + playerId, + lastName, + $g_ranktype, + kills / IF(deaths=0,1,deaths) AS kpd + FROM + hlstats_Players + WHERE + game='"."eSQL($player->{game})."' + AND hideranking = 0 + AND $g_ranktype > $base + AND kills >= 1 + ORDER BY + $g_ranktype, + kpd + LIMIT 0,10 + "); + + while (($player_count < 11) && (my ($playerId, $lastName, $p_base, $kpd) = $result->fetchrow_array)) { + $i--; + if (length($lastName) > 20) { + $lastName = substr($lastName, 0, 17)."..."; + } + $osd_message .= sprintf(" %02d %s%s +%04d %s", $i, &number_format($p_base), $rankword, ($p_base-$base), $lastName)."\\n"; + $player_count++; + } + $result->finish; + } + + return ($ranknumber, $g_games{$player->{game}}->getTotalPlayers(), $osd_message); +} + + +sub get_player_rank +{ + my ($player) = @_; + + my $base = 0; + if ($g_ranktype ne "kills") { + $base = $player->{skill}; + } else { + $base = $player->{total_kills}; + } + + return ($base, $player->getRank(), $g_games{$player->{game}}->getTotalPlayers()); + +} + +sub get_player_data +{ + my ($player, $get_rank) = @_; + + my $result = &doQuery(" + SELECT + skill, kills, deaths, suicides, headshots, + IFNULL(ROUND((hits / shots * 100), 0), 0) AS acc, + connection_time + FROM + hlstats_Players + WHERE + playerId='" . $player->{playerid} . "' + "); + my ($skill, $kills, $deaths, $suicides, $headshots, $acc, $connection_time) = $result->fetchrow_array; + $result->finish; + + my $playerName = $player->{name}; + + if ($deaths > 0) { + $kd = sprintf("%.2f", $kills/$deaths); + } else { + $kd = sprintf("%.2f", $kills); + } + if ($kills > 0) { + $hpk = sprintf("%.0f", (100/$kills) * $headshots); + } else { + $hpk = sprintf("%.0f", $kills); + } + + if ($kills > 0) { + ($fav_weapon, $fav_weapon_kills, $fav_weapon_acc, $s_fav_weapon, $s_fav_weapon_kills, $s_fav_weapon_acc) = get_fav_weapon($player); + } else { + $fav_weapon = "-"; + $s_fav_weapon = "-"; + } + + if ($get_rank > 0) { + my ($rang_skill, $rank_number, $total_players) = get_player_rank($player); + return ($skill, $kills, $deaths, $kd, $suicides, $headshots, $hpk, $acc, $connection_time,$fav_weapon, $fav_weapon_kills, $fav_weapon_acc, $s_fav_weapon, $s_fav_weapon_kills, $s_fav_weapon_acc, $rank_number, $total_players); + } else { + return ($skill, $kills, $deaths, $kd, $suicides, $headshots, $hpk, $acc, $connection_time, $fav_weapon, $fav_weapon_kills, $fav_weapon_acc, $s_fav_weapon, $s_fav_weapon_kills, $s_fav_weapon_acc); + } + +} + +sub get_menu_text +{ + my ($player, $skill, $kills, $deaths, $kd, $suicides, $headshots, $hpk, $acc, $connection_time, $fav_weapon, $fav_weapon_kills, $fav_weapon_acc, $s_fav_weapon, $s_fav_weapon_kills, $s_fav_weapon_acc, $rank_number, $total_players) = @_; + + my $p_name = $player->{name}; + my $p_connect_time = $player->{connect_time}; + + my $s_pos_change = "N/A"; + if ($player->{session_start_pos} > 0) { + $s_pos_change = ($player->{session_start_pos} - $rank_number); + if ($s_pos_change > 0) { + $s_pos_change = "+".$s_pos_change; + } + } + + my $s_skill_change = $player->{session_skill}; + if ($s_skill_change > 0) { + $s_skill_change = "+".$s_skill_change; + } + + my $s_kills = $player->{session_kills}; + my $s_deaths = $player->{session_deaths}; + my $s_headshots = $player->{session_headshots}; + my $s_hits = $player->{session_hits}; + my $s_shots = $player->{session_shots}; + + if ($s_deaths > 0) { + $s_kd = sprintf("%.2f", $s_kills/$s_deaths); + } else { + if ($s_kills > 0) { + $s_kd = sprintf("%.2f", $s_kills); + } else { + $s_kd = sprintf("%.2f", 0); + } + } + + if ($s_kills > 0) { + $s_hpk = sprintf("%.0f", (100/$s_kills) * $s_headshots); + } else { + if ($s_headshots > 0) { + $s_hpk = sprintf("%.0f", $s_headshots); + } else { + $s_hpk = sprintf("%.0f", 0); + } + } + + if ($s_shots > 0) { + $s_acc = sprintf("%.0f", (100/$s_shots) * $s_hits); + } else { + if ($s_hits > 0) { + $s_acc = sprintf("%.0f", $s_hits); + } else { + $s_acc = sprintf("%.0f", 0); + } + } + + my $weapon_str = " No Fav Weapon\\n"; + my $acc_str = " $acc% Accuracy\\n"; + if ($fav_weapon_kills > 0) { + $weapon_str = " ".$fav_weapon_kills." kills with ".$fav_weapon."\\n"; + $acc_str = " $acc% Accuracy (".$fav_weapon." ".$fav_weapon_acc."%)\\n"; + } + if ($acc == 0) { + $acc_str = ""; + } + + $s_weapon_str = " No Fav Weapon\\n"; + $s_acc_str = " $s_acc% Accuracy\\n"; + if ($s_fav_weapon_kills > 0) { + $s_weapon_str = " ".$s_fav_weapon_kills." kills with ".$s_fav_weapon."\\n"; + $s_acc_str = " $s_acc% Accuracy (".$s_fav_weapon." ".$s_fav_weapon_acc."%)\\n"; + } + + if ($s_acc == 0) { + $s_acc_str = ""; + } + my $cmd_text = ""; + + if ($g_ranktype eq "kills") { + $cmd_text = "->1 - Total\\n". + " Position ".&number_format($rank_number)." of ".&number_format($total_players)."\\n". + " ".&number_format($kills).":".&number_format($deaths)." Frags ($kd)\\n". + " ".&number_format($headshots)." Headshots ($hpk%)\\n". + " ".&number_format($skill)." Points\\n". + $weapon_str. + $acc_str. + " Time ".&date_format($connection_time)."\\n \\n". + "->2 - Session\\n". + " ".&number_format($s_pos_change)." Positions\\n". + " ".&number_format($s_kills).":".&number_format($s_deaths)." Frags ($s_kd)\\n". + " ".&number_format($s_headshots)." Headshots ($s_hpk%)\\n". + " ".&number_format($s_skill_change)." Points\\n". + $s_weapon_str. + $s_acc_str. + " Time ".&date_format(time() - $p_connect_time)."\\n"; + } else { + $cmd_text = "->1 - Total\\n". + " Position ".&number_format($rank_number)." of ".&number_format($total_players)."\\n". + " ".&number_format($skill)." Points\\n". + " ".&number_format($kills).":".&number_format($deaths)." Frags ($kd)\\n". + " ".&number_format($headshots)." Headshots ($hpk%)\\n". + $weapon_str. + $acc_str. + " Time ".&date_format($connection_time)."\\n \\n". + "->2 - Session\\n". + " ".&number_format($s_pos_change)." Positions\\n". + " ".&number_format($s_skill_change)." Points\\n". + " ".&number_format($s_kills).":".&number_format($s_deaths)." Frags ($s_kd)\\n". + " ".&number_format($s_headshots)." Headshots ($s_hpk%)\\n". + $s_weapon_str. + $s_acc_str. + " Time ".&date_format(time() - $p_connect_time)."\\n"; + } + + $points = " ".&number_format($skill)." Points\\n"; + $s_points = " ".&number_format($s_skill_change)." Points\\n"; + + return $cmd_text; +} + +sub endKillStreak +{ + my ($player) = @_; + + my $killtotal = $player->{kills_per_life}; + if ($killtotal > $player->{kill_streak}) { + $player->{kill_streak} = $killtotal; + } + if ($killtotal > 12) { + $killtotal = 12; + } + # octo: I don't think suicides should count as deaths in a row + if ($killtotal > 1) { + &doEvent_PlayerAction( + $player->{userid}, + $player->{uniqueid}, + "kill_streak_" . $killtotal + ); + } + $player->{kills_per_life} = 0; +} + +# +# 001. Connect +# + +sub doEvent_Connect +{ + my ($playerId, $playerUniqueId, $ipAddr) = @_; + + my $desc; + my $player = lookupPlayer($s_addr, $playerId, $playerUniqueId); + my $playerstr = &getPlayerInfoString($player, $playerId); + my $min_players_rank = $g_servers{$s_addr}->{min_players_rank}; + my $hostname; + my $hostgroup; + + if ($player) + { + if (($g_servers{$s_addr}->{ignore_bots} == 1) && ($player->{is_bot} == 1)) + { + $player->set("connect_time", time()); + $player->updateDB(); + $desc = "(IGNORED) BOT: "; + } else { + $player->set("connect_time", time()); + my $player_rank = 0; + $player_rank = $player->getRank(); + if ($min_players_rank > 0 || $player_rank == 0) { + if ($player_rank > $min_players_rank) { + my $steamid = $player->{uniqueid}; + my $p_steamid = $player->{plain_uniqueid}; + if ($g_servers{$s_addr}->is_admin($steamid) == 0) { + if ($g_servers{$s_addr}->{game_engine} == 1) { + $g_servers{$s_addr}->dorcon("kick #$playerId Not a Top $min_players_rank-Player"); + } else { + $g_servers{$s_addr}->dorcon("kickid $p_steamid Not a Top $min_players_rank-Player"); + } + } + } + } + + if ($g_mode eq "LAN") + { + $player->set("uniqueid", $ipAddr); + } + + &recordEvent( + "Connects", 0, + $player->{playerid}, + $ipAddr, + $hostname, + $hostgroup + ); + } + } + else + { + $desc = "(IGNORED) NOPLAYERINFO: "; + } + + return $desc . $playerstr . " connected, address \"$ipAddr\"," + . " hostname \"$hostname\", hostgroup \"$hostgroup\""; +} + + +# +# 002. Enter Game +# + +sub doEvent_EnterGame +{ + my ($playerId, $playerUniqueId) = @_; + + my $desc; + my $player = lookupPlayer($s_addr, $playerId, $playerUniqueId); + my $playerstr = &getPlayerInfoString($player, $playerId); + my $isbot = $player->{is_bot}; + + my $min_players_rank = $g_servers{$s_addr}->{min_players_rank}; + + if ($player) + { + if ($g_mode eq "Normal" && $isbot == 0 && $player->{userid} > 0) + { + $hostname = &resolveIp($player->{address}); + $hostgroup = ""; + + if ($hostname) + { + $hostgroup = &getHostGroup($hostname); + } + if ($g_stdin == 0 && $g_servers{$s_addr}->{connect_announce} > 0) + { + my $player_rank = $player->getRank(); + if ($player->{country} ne "") + { + my $msg = ""; + if ($player_rank == 0) + { + $msg = sprintf("Player %s has connected from %s", $player->{name},$player->{country}); + } + elsif ($player->{skill} == 1000) + { + $msg = sprintf("New player %s has connected from %s",$player->{name},$player->{country}); + } + elsif ($g_ranktype ne "kills") + { + $msg = sprintf("%s (Pos %s with %s points) has connected from %s",$player->{name},$player_rank,$player->{skill},$player->{country}); + } + else + { + $msg = sprintf("%s (Pos %s with %s kills) has connected from %s",$player->{name},$player_rank,$player->{total_kills},$player->{country}); + } + $g_servers{$s_addr}->messageAll($msg, $player->{playerid}, 1); + } + else + { + if ($g_ranktype ne "kills") + { + $msg = sprintf("%s (Pos %s with %s points) has connected",$player->{name},$player_rank,$player->{skill}); + } + else + { + $msg = sprintf("%s (Pos %s with %s kills) has connected",$player->{name},$player_rank,$player->{total_kills}); + } + $g_servers{$s_addr}->messageAll($msg, $player->{playerid}, 1); + } + } + } + + if (($g_servers{$s_addr}->{ignore_bots} == 1) && ($isbot == 1)) + { + $connect_time = $player->{connect_time}; + if ($connect_time == 0) + { + $player->set("connect_time", time()); + } + $player->updateDB(); + $desc = "(IGNORED) BOT: "; + } + else + { + $connect_time = $player->{connect_time}; + if ($connect_time == 0) + { + $player->set("connect_time", time()); + if (($min_players_rank > 0) && ($isbot == 0)) + { + my $player_rank = $player->getRank(); + if ($player_rank > $min_players_rank || $player_rank == 0) + { + my $steamid = $player->{uniqueid}; + my $p_steamid = $player->{plain_uniqueid}; + if ($g_servers{$s_addr}->is_admin($steamid) == 0) + { + $g_servers{$s_addr}->dorcon("kickid $p_steamid Not a Top".$min_players_rank."-Player"); + } + } + } + } + if ($isbot == 0) + { + $player->updateDB(); + recordEvent( + "Entries", 0, + $player->{playerid} + ); + } + } + } + else + { + $desc = "(IGNORED) NOPLAYERINFO: "; + } + + return $desc . $playerstr . " entered the game"; +} + + +# +# 003. Disconnect +# + +sub doEvent_Disconnect +{ + my ($pUserId, $playerUniqueId, $ev_properties) = @_; + + my $desc; + my $player = lookupPlayer($s_addr, $pUserId, $playerUniqueId); + my $playerstr = &getPlayerInfoString($player, $pUserId); + + if ($player) + { + if (($g_servers{$s_addr}->{ignore_bots} == 1) && (($player->{is_bot} == 1) || ($pUserId < 0))) + { + $player->updateDB(); + removePlayer($s_addr, $pUserId, $playerUniqueId); + $desc = "(IGNORED) BOT: "; + } else { + &recordEvent( + "Disconnects", 0, + $player->{playerid} + ); + + &endKillStreak($player); + + if (($g_global_chat > 0)) { + my $p_name = $player->{name}; + my $b_message = $p_name." (".$g_servers{$s_addr}->{name}.") disconnected"; + if ($player->{is_dead} == 1) { + $b_message = "*DEAD* ".$p_name." (".$g_servers{$s_addr}->{name}.") disconnected"; + } + send_global_chat($b_message); + } + + my $p_steamid = $player->{plain_uniqueid}; + my $p_connect_time = $player->{connect_time}; + my $p_is_banned = $player->{is_banned}; + + if ($p_connect_time == 0) { + $p_connect_time = time(); + } + + my $auto_ban = $g_servers{$s_addr}->{auto_ban}; + # time()-$p_connect_time > 30 to avoid permanent bans changes in 5 min bans + # $p_is_banned > 0 for not converting bans to "just" 5 minutes bans + if (($auto_ban > 0) && ((time() - $p_connect_time) > 30) && ($p_is_banned == 0) && ($player->{is_bot} == 0)) + { + if ($g_servers{$s_addr}->is_admin($playerUniqueId) == 0) { + $g_servers{$s_addr}->dorcon("banid 5 $p_steamid"); + } + } + $player->updateDB(); + removePlayer($s_addr, $pUserId, $playerUniqueId); + } + } + else + { + $desc = "(IGNORED) NOPLAYERINFO: "; + if ($playerUniqueId ne "") { + if ($ev_properties eq "(reason \"VAC banned from secure server\")") { + $desc = "(IGNORED) VAC BANNED PLAYER [".$playerUniqueId."]: "; + my $playerid = &getPlayerId($playerUniqueId); + if ($playerid) { + my $query = "SELECT lastName, hideranking FROM hlstats_Players WHERE playerId=$playerid"; + my $result = &doQuery($query); + ($lastName, $hideranking) = $result->fetchrow_array; + $result->finish; + if ($hideranking < 2) { + my $query = "UPDATE hlstats_Players SET last_event=UNIX_TIMESTAMP(), hideranking=2 WHERE playerId=$playerid"; + &execNonQuery($query); + $desc = "HIDING VAC BANNED PLAYER [".$playerUniqueId.", ".$lastName."]: "; + } else { + $desc = "VAC BANNED PLAYER [".$playerUniqueId.", ".$lastName."] ALREADY HIDDEN: "; + } + } + } elsif ($ev_properties eq "(reason \"Kicked by Console : You have been banned, visit www.steambans.com for information.\")") { + $desc = "(IGNORED) STEAMBANS BANNED PLAYER [".$playerUniqueId."]: "; + my $playerid = &getPlayerId($playerUniqueId); + if ($playerid) { + my $query = "SELECT lastName, hideranking FROM hlstats_Players WHERE playerId=$playerid"; + my $result = &doQuery($query); + ($lastName, $hideranking) = $result->fetchrow_array; + $result->finish; + if ($hideranking < 2) { + my $query = "UPDATE hlstats_Players SET last_event=UNIX_TIMESTAMP(), hideranking=2 WHERE playerId=$playerid"; + &execNonQuery($query); + $desc = "HIDING STEAMBANS BANNED PLAYER [".$playerUniqueId.", ".$lastName."]: "; + } else { + $desc = "STEANBANS BANNED PLAYER [".$playerUniqueId.", ".$lastName."] ALREADY HIDDEN: "; + } + } + } + } + } + + return $desc . $playerstr . " disconnected ".$ev_properties; +} + + +# +# 004. Suicide +# + +sub doEvent_Suicide +{ + my ($playerId, $playerUniqueId, $weapon, %properties) = @_; + + $x = undef; + $y = undef; + $z = undef; + + if (defined($properties{attacker_position})) { + $coords = $properties{attacker_position}; + # print "\nCoords SUICIDE: ".$coords."\n\n"; + ($x,$y,$z) = split(/ /,$coords); + } + + my $desc; + my $player = lookupPlayer($s_addr, $playerId, $playerUniqueId); + my $playerstr = &getPlayerInfoString($player, $playerId); + + if ($g_servers{$s_addr}->{num_trackable_players} < $g_servers{$s_addr}->{minplayers}) + { + $desc = "(IGNORED) NOTMINPLAYERS: "; + } + elsif (checkBonusRound()) + { + $desc = "(IGNORED) BonusRound: "; + } + elsif (!$player) + { + $desc = "(IGNORED) NOPLAYERINFO: "; + } + elsif ($player->{last_team_change}+2 > $ev_remotetime) + { + $desc = "(IGNORED) TEAMSWITCH: "; + } + else + { + if (($g_servers{$s_addr}->{ignore_bots} == 1) && ($player->{is_bot} == 1)) + { + $desc = "(IGNORED) BOT: "; + } else { + + &endKillStreak($player); + + if ($g_servers{$s_addr}->{play_game} == TF() && defined($properties{customkill})) { + if ($properties{customkill} eq "train") { + &doEvent_PlayerAction( + $player->{userid}, + $player->{uniqueid}, + "hit_by_train", + $x, + $y, + $z + ); + } elsif ($properties{customkill} eq "saw") { + &doEvent_PlayerAction( + $player->{userid}, + $player->{uniqueid}, + "death_sawblade", + $x, + $y, + $z + ); + } + } + + &recordEvent( + "Suicides", 0, + $player->{playerid}, + $weapon, + $x, + $y, + $z + ); + + my $suicide_penalty = (-1) * $g_servers{$s_addr}->{suicide_penalty}; + $player->increment("suicides"); + $player->increment("skill", $suicide_penalty); + $player->increment("session_suicides"); + $player->increment("session_skill", $suicide_penalty); + $player->increment("map_suicides"); + $g_servers{$s_addr}->increment("suicides"); + $g_servers{$s_addr}->increment("total_suicides"); + $player->updateDB(); + } + } + + return $desc . $playerstr . " committed suicide with \"$weapon\""; +} + + +# +# 005. Team Selection +# + +sub doEvent_TeamSelection +{ + my ($playerId, $playerUniqueId, $team) = @_; + + my $desc; + my $player = lookupPlayer($s_addr, $playerId, $playerUniqueId); + my $playerstr = &getPlayerInfoString($player, $playerId); + + if ($player) + { + if (($g_servers{$s_addr}->{ignore_bots} == 1) && ($player->{is_bot} == 1)) + { + $player->set("team", $team); + $desc = "(IGNORED) BOT: "; + } else { + $old_team = $player->{team}; + if (($old_team eq "CT") || ($old_team eq "TERRORIST")) + { + $g_servers{$s_addr}->increment("ba_player_switch"); + } + $player->set("last_team_change",$ev_remotetime); + $player->set("team", $team); + &recordEvent( + "ChangeTeam", 0, + $player->{playerid}, + $team + ); + + $player->updateTrackable(); + $player->updateDB(); + $g_servers{$s_addr}->updatePlayerCount(); + } + } + else + { + $desc = "(IGNORED) NOPLAYERINFO: "; + } + + return $desc . $playerstr . " joined team \"$team\""; +} + + +# +# 006. Role Selection +# + +sub doEvent_RoleSelection +{ + my ($playerId, $playerUniqueId, $role) = @_; + + my $desc; + my $player = lookupPlayer($s_addr, $playerId, $playerUniqueId); + my $playerstr = &getPlayerInfoString($player, $playerId); + + if ($player) + { + if (($g_servers{$s_addr}->{ignore_bots} == 1) && ($player->{is_bot} == 1)) + { + $player->set("role", $role); + $desc = "(IGNORED) BOT: "; + } else { + $player->set("role", $role); + &recordEvent( + "ChangeRole", 0, + $player->{playerid}, + $role + ); + my $query = " + UPDATE + hlstats_Roles + SET + picked=picked+1 + WHERE + game='" . "eSQL($g_servers{$s_addr}->{game}) . "' + AND code='" . "eSQL($role) . "' + "; + &execNonQuery($query); + $player->updateDB(); + } + } + else + { + $desc = "(IGNORED) NOPLAYERINFO: "; + } + + return $desc . $playerstr . " changed role to \"$role\""; +} + + +# +# 007. Change Name +# + +sub doEvent_ChangeName +{ + my ($playerId, $playerUniqueId, $newname) = @_; + + my $desc; + my $player = lookupPlayer($s_addr, $playerId, $playerUniqueId); + my $playerstr = &getPlayerInfoString($player, $playerId); + + if ($player) + { + if (($g_servers{$s_addr}->{ignore_bots} == 1) && ($player->{is_bot} == 1)) + { + $player->updateDB(); + removePlayer($s_addr, $playerId, $playerUniqueId); + $desc = "(IGNORED) BOT: "; + } else { + my $name = $player->{name}; + &recordEvent( + "ChangeName", 0, + $player->{playerid}, + $name, + $newname + ); + + if (($g_mode eq "NameTrack") || ($player->{is_bot} == 1)) + { + $player->updateDB(); + removePlayer($s_addr, $playerId, $playerUniqueId); + } else { + $player->set("name", $newname); + $player->updateDB(); + } + } + } + else + { + $desc = "(IGNORED) NOPLAYERINFO: "; + } + + return $desc . $playerstr . " changed name to \"$newname\""; +} + + +# +# E008. Frag +# + +sub doEvent_Frag +{ + $rcmd = $g_servers{$s_addr}->{broadcasting_command}; + + my ($killerId, $killerUniqueId, $victimId, $victimUniqueId, $weapon, $headshot, $x, $y, $z, $vx, $vy, $vz, %properties) = @_; + + # if killer coords were on kill line, use them + if (defined($properties{attacker_position})) { + $coords = $properties{attacker_position}; + ($x,$y,$z) = split(/ /,$coords); + } elsif (defined($properties{killerpos})) { #pvkii + $coords = $properties{killerpos}; + ($x,$y,$z) = split(/ /,$coords); + } elsif ($g_servers{$s_addr}->{nextkillx} ne "") { + # else use coords stored from plugin if available + $x = $g_servers{$s_addr}->{nextkillx}; + $y = $g_servers{$s_addr}->{nextkilly}; + $z = $g_servers{$s_addr}->{nextkillz}; + } + # reset last kill coords + $g_servers{$s_addr}->{nextkillx} = ""; + $g_servers{$s_addr}->{nextkilly} = ""; + $g_servers{$s_addr}->{nextkillz} = ""; + + # if killer coords were on kill line, use them + if (defined($properties{victim_position})) { + $coords = $properties{victim_position}; + ($vx,$vy,$vz) = split(/ /,$coords); + } elsif (defined($properties{victimpos})) { #pvkii + $coords = $properties{victimpos}; + ($vx,$vy,$vz) = split(/ /,$coords); + } elsif ($g_servers{$s_addr}->{nextkillvicx} ne "") { + # else use coords stored from plugin if available + $vx = $g_servers{$s_addr}->{nextkillvicx}; + $vy = $g_servers{$s_addr}->{nextkillvicy}; + $vz = $g_servers{$s_addr}->{nextkillvicz}; + } + # reset last kill coords + $g_servers{$s_addr}->{nextkillvicx} = ""; + $g_servers{$s_addr}->{nextkillvicy} = ""; + $g_servers{$s_addr}->{nextkillzvic} = ""; + + # determine if kill was fake + # currently only applicable to TF2's "Dead Ringer" + my $isfake = (defined($properties{customkill}) && $properties{customkill} eq "feign_death")?1:0; + + my $desc; + my $killer = lookupPlayer($s_addr, $killerId, $killerUniqueId); + my $victim = lookupPlayer($s_addr, $victimId, $victimUniqueId); + my $killerstr = &getPlayerInfoString($killer, $killerId); + my $victimstr = &getPlayerInfoString($victim, $victimId); + + my $headshotFromAction = 0; + + if (($g_servers{$s_addr}->{num_trackable_players} < $g_servers{$s_addr}->{minplayers}) && $g_servers{$s_addr}->{play_game} != L4D()) { + $desc = "(IGNORED) NOTMINPLAYERS: "; + } elsif (checkBonusRound()) { + $desc = "(IGNORED) BonusRound: "; + } elsif ($killer && $victim) { + if (($g_servers{$s_addr}->{ignore_bots} == 1) && (($killer->{is_bot} == 1) || ($victim->{is_bot} == 1))) { + $desc = "(IGNORED) BOT: "; + } elsif ($g_servers{$s_addr}->{play_game} == L4D() && $killer->{team} eq "Infected" && $victim->{team} eq "Infected") { + $desc = "(IGNORED) L4D Infected TK: "; + } else { + + # weapon fixes + if ($weapon eq "") { + if ($killer->{role} ne "") { + $weapon = $killer->{role}; + } else { + $weapon = "world"; + } + } elsif ($g_servers{$s_addr}->{play_game} == ZPS()) { + # strip off the 2 at the end of some weapon names in FoF so weapon is right hand is counted the same as same weapon in the left hand + if ($weapon eq "broom") { + $weapon = "crowbar"; + } + } elsif ($g_servers{$s_addr}->{play_game} == FF()) { + if ($weapon eq "BOOM_HEADSHOT") { + $weapon = "weapon_sniperrifle"; + $headshot = 1; + } elsif ($weapon eq "grenade_napalmlet") { + $weapon = "grenade_napalm"; + } + } elsif ($g_servers{$s_addr}->{play_game} == TFC() && $weapon eq "headshot") { + $weapon = "sniperrifle"; + $headshot = 1; + } elsif ($g_servers{$s_addr}->{play_game} == FOF()) { + # strip off the 2 at the end of some weapon names in FoF so weapon is right hand is counted the same as same weapon in the left hand + $weapon =~ s/2$//; + if ($weapon eq "bow") { + $weapon = "arrow"; + } + } elsif ($g_servers{$s_addr}->{play_game} == PVKII()) { + if (defined($properties{killerclass})) { + my $krole = $properties{killerclass}; + if ($krole ne "" && $krole ne $killer->{role}) + { + &doEvent_RoleSelection( + $killer->{"userid"}, + $killer->{"uniqueid"}, + $krole + ); + } + } + if (defined($properties{victimclass})) { + my $vrole = $properties{victimclass}; + if ($vrole ne "" && $vrole ne $victim->{role}) + { + &doEvent_RoleSelection( + $victim->{"userid"}, + $victim->{"uniqueid"}, + $vrole + ); + } + } + } elsif ($g_servers{$s_addr}->{play_game} == TF()) { + # Change weapon code for player_penetration to machina and log an action + if ($weapon eq "player_penetration") { + $weapon = "machina"; + &doEvent_PlayerAction( + $killer->{userid}, + $killer->{uniqueid}, + "player_penetration", + $x, + $y, + $z + ); + } + } + + # if plugin marked next kill as headshot for this player, headshot + if ($g_servers{$s_addr}->{nextkillheadshot} == $killer->{playerid}) { + $headshot = 1; + $headshotFromAction = 1; + } + # reset nextkillheadshot flag at end of sub + + my $was_tk = &sameTeam($killer->{team}, $victim->{team}); + + # This is a fix for TF2 - they report player death after reporting team switch + # I don't know how soon after a team switch in other games that you can actually attempt to kill someone + if((($was_tk == 1) && ($victim->{last_team_change}+2 > $ev_remotetime)) && ($g_servers{$s_addr}->{play_game} == TF())) { + # print "NOT A TEAM KILL - RECENT TEAM SWITCH by " . $victim->{name} . "(Now:" . $ev_remotetime . ") Changed(" . $victim->{last_team_change} .")"; + $desc = "NOT A TEAM KILL - RECENT TEAM SWITCH"; + $was_tk=0; + } + if (($was_tk==0) || ($g_servers{$s_addr}->{game_type} == 1)) { + # Frag + + if($isfake == 0) { + #kill streak code by octo + + $killer->{"kills_per_life"}+=1; + $victim->{"deaths_in_a_row"}+=1; + if ($victim->{"deaths_in_a_row"} > $victim->{"death_streak"}) { + $victim->set("death_streak", $victim->{"deaths_in_a_row"}); + } + + # we don't need to update the killer's death_streak since it was done when he is a victim + $killer->{"deaths_in_a_row"} = 0; + + &endKillStreak($victim); + + &recordEvent( + "Frags", 0, + $killer->{playerid}, + $victim->{playerid}, + $weapon, + $headshot, + $killer->{role}, + $victim->{role}, + $x, + $y, + $z, + $vx, + $vy, + $vz + ); + + if ($killer->{role} ne "") { + my $query = " + UPDATE + hlstats_Roles + SET + kills=kills+1 + WHERE + game='" . "eSQL($g_servers{$s_addr}->{game}) . "' + AND code='" . "eSQL($killer->{role}) . "' + "; + &execNonQuery($query); + } + + if ($victim->{role} ne "") { + my $query = " + UPDATE + hlstats_Roles + SET + deaths=deaths+1 + WHERE + game='" . "eSQL($g_servers{$s_addr}->{game}) . "' + AND code='" . "eSQL($victim->{role}) . "' + "; + &execNonQuery($query); + } + + $killer->increment("total_kills"); + } else { + $desc = "FEIGN DEATH: "; + } + + my $killerskill = $killer->{skill}; + my $victimskill = $victim->{skill}; + + if ($g_servers{$s_addr}->{play_game} == L4D()) + { + $killerskill = &calcL4DSkill( + $killer->{skill}, + $weapon, + $g_servers{$s_addr}->{difficulty} + ); + } + else + { + ($killerskill, $victimskill) = &calcSkill( + $g_servers{$s_addr}->{"skill_mode"}, + $killer->{"skill"}, $killer->{"total_kills"}, + $victim->{"skill"}, $victim->{"total_kills"}, + $weapon, + $killer->{team} + ); + } + + $k_name = $killer->{name}; + $k_skill = $killerskill - $killer->{skill}; + $v_name = $victim->{name}; + $v_skill = $victimskill - $victim->{skill}; + + if ($g_servers{$s_addr}->{broadcasting_events} == 1) { + my $killer_add_text = ""; + if ($killer->{"total_kills"} < $g_player_minkills) { + $killer_add_text = " [".$killer->{"total_kills"}."/".$g_player_minkills."]"; + } + my $victim_add_text = ""; + if ($victim->{"total_kills"} < $g_player_minkills) { + $victim_add_text = " [".$victim->{"total_kills"}."/".$g_player_minkills."]"; + } + my $v_skill_text = ""; + if ($k_skill != ((-1) * $v_skill)) { + $v_skill_text = " [".$v_skill."]"; + } + my $k_userid = $g_servers{$s_addr}->format_userid($killer->{"userid"}); + my $v_userid = $g_servers{$s_addr}->format_userid($victim->{"userid"}); + my $colorparam = $g_servers{$s_addr}->{format_color}; + my $msg = ""; + if ($g_servers{$s_addr}->{play_game} == L4D()) { + $msg = sprintf("%s (%s)%s got %s points for killing %s",$k_name,&number_format($killerskill),$killer_add_text,$k_skill,$v_name); + } else { + $msg = sprintf("%s (%s)%s got %s points%s for killing %s (%s)%s",$k_name,&number_format($killerskill),$killer_add_text,$k_skill,$v_skill_text,$v_name,&number_format($victimskill),$victim_add_text); + } + my @rcmds; + if (($killer->{is_bot} == 0) && ($killer->{"display_events"} == 1) && ($killer->{userid} > 0)) { + my $cmd_str = sprintf("%s %s%s %s",$rcmd,$k_userid,$colorparam,$g_servers{$s_addr}->quoteparam($msg)); + push(@rcmds, $cmd_str); + if (($g_player_minkills > 20) && (($killer->{"total_kills"} < ($g_player_minkills / 4)) || (($killer->{"total_kills"} < $g_player_minkills) && (($killer->{"total_kills"} % 5) == 0)))) { + my $cmd_str = sprintf("%s %s %s",$rcmd,$k_userid,$g_servers{$s_addr}->quoteparam("You need ".&number_format($g_player_minkills-$killer->{"total_kills"})." kills to get regular points")); + push(@rcmds, $cmd_str); + + } + } + if (($victim->{is_bot} == 0) && ($victim->{"display_events"} == 1) && ($g_servers{$s_addr}->{play_game} != L4D()) && ($victim->{userid} > 0) && $isfake == 0) { + push(@rcmds, sprintf("%s %s%s %s",$rcmd,$v_userid,$colorparam,$g_servers{$s_addr}->quoteparam($msg))); + } + if (@rcmds) { + $g_servers{$s_addr}->dorcon_multi(@rcmds); + } + } + if($isfake == 0) { + $killer->set("skill", $killerskill); + $victim->set("skill", $victimskill); + + $killer->increment("session_skill", $k_skill); + $killer->increment("kills"); + $killer->increment("session_kills"); + $g_servers{$s_addr}->increment("kills"); + $g_servers{$s_addr}->increment("total_kills"); + + $update_data = "kills=kills+1"; + + if ($headshot == 1) { + $killer->increment("headshots"); + $killer->increment("session_headshots"); + $g_servers{$s_addr}->increment("headshots"); + $g_servers{$s_addr}->increment("total_headshots"); + $update_data .= ", headshots=headshots+1"; + + if ($headshotFromAction == 0) { + &doEvent_PlayerAction( + $killer->{"userid"}, + $killer->{"uniqueid"}, + "headshot" + ); + } + } + $victim->increment("deaths"); + $victim->increment("session_deaths"); + $victim->increment("session_skill", $v_skill); + + my $query = " + UPDATE + hlstats_Weapons + SET + $update_data + WHERE + game='" . "eSQL($g_servers{$s_addr}->{game}) . "' + AND code='" . "eSQL($weapon) . "' + "; + &execNonQuery($query); + + $query = " + INSERT INTO hlstats_Maps_Counts (game, map, kills, headshots) + VALUES ('"."eSQL($g_servers{$s_addr}->{game})."', '"."eSQL($g_servers{$s_addr}->get_map())."', 1, $headshot) + ON DUPLICATE KEY UPDATE kills=kills+1".(($headshot)?", headshots=headshots+1":""); + &execNonQuery($query); + + if ($g_servers{$s_addr}->{play_game} == L4D()) { + if ($victim->{team} eq "Infected" && $victim->{role} ne "infected" && $victim->{role} ne "") { + &doEvent_PlayerAction( + $killer->{userid}, + $killer->{uniqueid}, + "killed_".lc($victim->{role}), + $x, + $y, + $z + ); + } elsif ($victim->{team} eq "Survivor") { + &doEvent_PlayerPlayerAction( + $killer->{"userid"}, + $killer->{"uniqueid"}, + $victim->{"userid"}, + $victim->{"uniqueid"}, + "killed_survivor", + $x, + $y, + $z, + $vx, + $vy, + $vz + ); + } + } + } + } else { + # print "was TK"; + if ($weapon eq "dod_bomb_target") { + $desc = "IGNORED BOMBED TEAMKILL DODS "; + return $desc . $killerstr . " killed " . $victimstr . " with \"".$weapon."\""; + } + + # Teamkill + &recordEvent( + "Teamkills", 0, + $killer->{playerid}, + $victim->{playerid}, + $weapon, + $x, + $y, + $z, + $vx, + $vy, + $vz + ); + + my $tk_penalty = (-1) * $g_servers{$s_addr}->{"tk_penalty"}; + + $killer->increment("skill", $tk_penalty); + $killer->increment("session_skill", $tk_penalty); + $killer->increment('teamkills'); + + $k_name = $killer->{name}; + $k_skill = $killer->{skill}; + + if ($g_servers{$s_addr}->{broadcasting_events} == 1) { + my $k_userid = $g_servers{$s_addr}->format_userid($killer->{userid}); + my $v_userid = $g_servers{$s_addr}->format_userid($victim->{userid}); + my $colorparam = $g_servers{$s_addr}->{format_color}; + my $msg = sprintf("%s lost %s points (%s) for team-killing",$k_name,$tk_penalty,&number_format($k_skill)); + + my @rcmds; + if (($killer->{is_bot} == 0) && ($killer->{display_events} == 1) && ($killer->{userid} > 0)) { + my $cmd_str = sprintf("%s %s%s %s",$rcmd,$k_userid, $colorparam, $g_servers{$s_addr}->quoteparam($msg)); + push (@rcmds, $cmd_str); + } + if (($victim->{is_bot} == 0) && ($victim->{display_events} == 1) && ($victim->{userid} > 0)) { + my $cmd_str = sprintf("%s %s%s %s",$rcmd,$v_userid, $colorparam, $g_servers{$s_addr}->quoteparam($msg)); + push (@rcmds, $cmd_str); + } + if (@rcmds) { + $g_servers{$s_addr}->dorcon_multi(@rcmds); + } + } + + $update_data = "kills=kills+1"; + if ($headshot == 1) { + $update_data .= ", headshots=headshots+1"; + } + my $query = " + UPDATE + hlstats_Weapons + SET + $update_data + WHERE + game='" . "eSQL($g_servers{$s_addr}->{game}) . "' + AND code='" . "eSQL($weapon) . "' + "; + &execNonQuery($query); + $desc = "TEAMKILL: "; + } + } + if($isfake == 0) { + $killer->increment("map_kills"); + if ($headshot == 1) { + $killer->increment("map_headshots"); + } + $killer->updateDB(); + $victim->increment("map_deaths"); + $victim->set("is_dead", "1"); + if ($victim->{auto_type} eq "kill") { + &doEvent_Chat("say", + $victim->{"userid"}, + $victim->{"uniqueid"}, + $victim->{auto_command} + ); + $victim->updateDB(); + } + } + } else { + $desc = "(IGNORED) NOPLAYERINFO: "; + } + + # reset next kill headshot flag + $g_servers{$s_addr}->{nextkillheadshot} = 0; + + return $desc . $killerstr . " killed " . $victimstr . " with \"".$weapon."\""; +} + + + +# +# 010. Player-Player Actions +# + +sub doEvent_PlayerPlayerAction +{ + my ($playerId, $playerUniqueId, $victimId, $victimUniqueId, $action, $x, $y, $z, $vx, $vy, $vz, %properties) = @_; + $rcmd = $g_servers{$s_addr}->{broadcasting_command}; + + my $desc; + my $player = lookupPlayer($s_addr, $playerId, $playerUniqueId); + my $victim = lookupPlayer($s_addr, $victimId, $victimUniqueId); + my $playerstr = &getPlayerInfoString($player, $playerId); + my $victimstr = &getPlayerInfoString($victim, $victimId); + if ($playerId == $victimId) { + $desc = "(IGNORED) PLAYER SAME AS VICTIM: "; + } elsif ($g_servers{$s_addr}->{num_trackable_players} < $g_servers{$s_addr}->{minplayers}) { + $desc = "(IGNORED) NOTMINPLAYERS: "; + } elsif (checkBonusRound()) { + $desc = "(IGNORED) BonusRound: "; + } elsif ($player && $victim) { + if (($g_servers{$s_addr}->{ignore_bots} == 1) && (($killer->{is_bot} == 1) || ($victim->{is_bot} == 1))) { + $desc = "(IGNORED) BOT: "; + } else { + if ($g_servers{$s_addr}->{play_game} == TF() && $action eq "medic_death") { + #do heal points + + if (defined($properties{ubercharge}) && $properties{ubercharge} == 1) { + &doEvent_PlayerAction( + $playerId, + $playerUniqueId, + "killed_charged_medic", + $x, + $y, + $z + ); + &doEvent_PlayerPlayerAction( + $playerId, + $playerUniqueId, + $victimId, + $victimUniqueId, + "killed_charged_medic", + $x, + $y, + $z, + $vx, + $vy, + $vz + ); + } + } + + my $map = $g_servers{$s_addr}->get_map(); + if (defined($g_games{$g_servers{$s_addr}->{game}}{actions}{$map."_$action"}) && $g_games{$g_servers{$s_addr}->{game}}{actions}{$map."_$action"}{ppaction}) { + &doEvent_PlayerPlayerAction( + $playerId, + $playerUniqueId, + $victimId, + $victimUniqueId, + $map."_$action", + $x, + $y, + $z, + $vx, + $vy, + $vz + ); + } + if (defined($g_games{$g_servers{$s_addr}->{game}}{actions}{$action}) && $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{ppaction}) { + my $actionname = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{descr}; + my $actionid = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{id}; + my $reward_player = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{reward_player}; + my $reward_team = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{reward_team}; + my $team = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{team}; + + if (defined($properties{assister_position})) { + $coords = $properties{assister_position}; + ($x,$y,$z) = split(/ /,$coords); + } elsif (defined($properties{position})) { + $coords = $properties{position}; + ($x,$y,$z) = split(/ /,$coords); + } elsif (defined($properties{attacker_position})) { + $coords = $properties{attacker_position}; + ($x,$y,$z) = split(/ /,$coords); + } + + if (defined($properties{victim_position})) { + $coords = $properties{victim_position}; + ($vx,$vy,$vz) = split(/ /,$coords); + } + + &recordEvent( + "PlayerPlayerActions", 0, + $player->{playerid}, + $victim->{playerid}, + $actionid, + $reward_player, + $x, + $y, + $z, + $vx, + $vy, + $vz + ); + + my $query = " + UPDATE + hlstats_Actions + SET + count=count+1 + WHERE + game='" . "eSQL($g_servers{$s_addr}->{game}) . "' + AND code = '"."eSQL($action)."' + "; + &execNonQuery($query); + if ($reward_player != 0) { + my $victimreward = $reward_player * -1; + $player->increment("skill", $reward_player, 1); + $player->increment("session_skill", $reward_player, 1); + $victim->increment("skill", $victimreward, 1); + $victim->increment("session_skill", $victimreward, 1); + + if ($g_servers{$s_addr}->{broadcasting_events} == 1) { + if ($g_servers{$s_addr}->{broadcasting_player_actions} == 1) { + my $verb = ""; + if ($reward_player < 0) { + $verb = "lost"; + } else { + $verb = "got"; + } + my $colorparam = $g_servers{$s_addr}->{format_color}; + my $coloraction = $g_servers{$s_addr}->{format_action}; + my $colorend = $g_servers{$s_addr}->{format_actionend}; + my $p_name = $player->{name}; + my $p_skill = $player->{skill}; + my $v_name = $victim->{name}; + my $v_skill = $victim->{skill}; + my $msg = sprintf("%s %s %s points (%s) for %s%s%s against %s (%s)",$p_name,$verb,abs($reward_player), &number_format($p_skill),$coloraction,$actionname,$colorend, $v_name, &number_format($v_skill)); + my @rcmds; + if (($player->{is_bot} == 0) && ($player->{display_events} == 1) && ($player->{userid} > 0)) { + my $p_userid = $g_servers{$s_addr}->format_userid($player->{userid}); + my $cmd_str = sprintf("%s %s%s %s",$rcmd,$p_userid,$colorparam,$g_servers{$s_addr}->quoteparam($msg)); + push(@rcmds, $cmd_str); + } + if (($victim->{is_bot} == 0) && ($victim->{display_events} == 1) && ($victim->{userid} > 0)) { + my $v_userid = $g_servers{$s_addr}->format_userid($victim->{userid}); + my $cmd_str = sprintf("%s %s%s %s",$rcmd,$v_userid,$colorparam,$g_servers{$s_addr}->quoteparam($msg)); + push(@rcmds, $cmd_str); + } + if (@rcmds) { + $g_servers{$s_addr}->dorcon_multi(@rcmds); + } + } + } + } + if ($team && $reward_team != 0) { + &rewardTeam($team, $reward_team, $actionid, $actionname, $action); + } + + } else { + $desc = "(IGNORED) "; + } + } + } else { + $desc = "(IGNORED) NOPLAYERINFO: "; + } + + return $desc . $playerstr . " triggered \"".$action."\" against " . $victimstr; +} + + +# +# E011. Player Objectives/Actions +# + +sub doEvent_PlayerAction +{ + $rcmd = $g_servers{$s_addr}->{broadcasting_command}; + my ($playerId, $playerUniqueId, $action, $x, $y, $z, %properties) = @_; + my $desc; + my $player = lookupPlayer($s_addr, $playerId, $playerUniqueId); + my $playerstr = &getPlayerInfoString($player, $playerId); + + if ($g_servers{$s_addr}->{num_trackable_players} < $g_servers{$s_addr}->{minplayers}) { + $desc = "(IGNORED) NOTMINPLAYERS: "; + } elsif (checkBonusRound()) { + $desc = "(IGNORED) BonusRound: "; + } elsif ($player) { + if ($action eq "kill assist") { + if ($player->{"role"} eq "medic") { + $action = "kill_assist_medic"; + } + } + if (($g_servers{$s_addr}->{ignore_bots} == 1) && ($player->{is_bot} == 1)) { + $desc = "(IGNORED) BOT: "; + if ($action eq "Got_The_Bomb") { + $player->set("has_bomb", "1", 1); + } elsif ($action eq "Dropped_The_Bomb") { + $player->set("has_bomb", "0", 1); + } + $player->updateDB(); + } else { + if ($g_servers{$s_addr}->{play_game} == TF()) + { + my $objectstring = ""; + if (defined($properties{object})) { + $objectstring = "_".lc($properties{object}); + $objectstring =~ s/\s/_/g; + } + my $eventstring = ""; + if (defined($properties{event})) { + $eventstring = "_".lc($properties{event}); + $eventstring =~ s/\s/_/g; + } + my $ownerstring = ""; + if (defined($properties{objectowner})) { + my $owner = $properties{objectowner}; + $owner =~ /.+?.*/; + $owner = $1; + if ($owner eq $player->{uniqueid}) { + $ownerstring = "owner_"; + } + } + $action = $ownerstring.$action.$objectstring.$eventstring; + + if ($action eq "builtobject_obj_sentrygun") + { + $player->{last_sg_build}=$ev_remotetime; + } + elsif ($action eq "builtobject_obj_dispenser") + { + $player->{last_disp_build}=$ev_remotetime; + } + elsif ($action eq "builtobject_obj_teleporter_entrance") + { + $player->{last_entrance_build}=$ev_remotetime; + } + elsif ($action eq "builtobject_obj_teleporter_exit") + { + $player->{last_exit_build}=$ev_remotetime; + } + elsif ($action eq "flagevent_defended") + { + if ($player->{team} eq "Red") { + $g_servers{$s_addr}->{lastredflagdefend} = $ev_remotetime; + } else { + $g_servers{$s_addr}->{lastblueflagdefend} = $ev_remotetime; + } + } + elsif ($action eq "flagevent_dropped") + { + if ($player->{team} eq "Red" && ($ev_remotetime - $g_servers{$s_addr}->{lastblueflagdefend}) <= 1) { + $g_servers{$s_addr}->{lastblueflagdefend} = 0; + $action="flagevent_dropped_death"; + } elsif ($player->{team} eq "Blue" && ($ev_remotetime - $g_servers{$s_addr}->{lastredflagdefend}) <= 1) { + $g_servers{$s_addr}->{lastredflagdefend} = 0; + $action="flagevent_dropped_death"; + } + } + elsif ($action eq "player_extinguished") + { + $action = $player->{role}."_extinguish"; + } + else + { + if (($action eq "owner_killedobject_obj_sentrygun" && ($ev_remotetime - $player->{last_sg_build}) > 120) || + ($action eq "owner_killedobject_obj_dispenser" && ($ev_remotetime - $player->{last_disp_build}) > 120) || + ($action eq "owner_killedobject_obj_teleporter_entrance" && ($ev_remotetime - $player->{last_entrance_build}) > 120) || + ($action eq "owner_killedobject_obj_teleporter_exit" && ($ev_remotetime - $player->{last_exit_build}) > 120)) + { + return "(IGNORED) OBJECT MOVED: $playerstr triggered \"$action\""; + } + } + } + elsif ($g_servers{$s_addr}->{play_game} == TFC()) + { + if ($action eq "Sentry_Built_Level_1") + { + $player->{last_sg_build}=$ev_remotetime; + } + elsif ($action eq "Built_Dispenser") + { + $player->{last_disp_build}=$ev_remotetime; + } + elsif ($action eq "Teleporter_Entrance_Finished") + { + $player->{last_entrance_build}=$ev_remotetime; + } + elsif ($action eq "Teleporter_Exit_Finished") + { + $player->{last_exit_build}=$ev_remotetime; + } + else + { + if (($action eq "Sentry_Dismantle" && ($ev_remotetime - $player->{last_sg_build}) > 120) || + ($action eq "Dispenser_Dismantle" && ($ev_remotetime - $player->{last_disp_build}) > 120) || + ($action eq "Teleporter_Entrance_Dismantle" && ($ev_remotetime - $player->{last_entrance_build}) > 120) || + ($action eq "Teleporter_Exit_Dismantle" && ($ev_remotetime - $player->{last_exit_build}) > 120)) + { + return "(IGNORED) OBJECT MOVED: " . $playerstr . " triggered \"$action\""; + } + } + } + elsif ($g_servers{$s_addr}->{play_game} == FF()) + { + if ($action eq "build_sentrygun") + { + $player->{last_sg_build}=$ev_remotetime; + } + elsif ($action eq "build_dispenser") + { + $player->{last_disp_build}=$ev_remotetime; + } + else + { + if (($action eq "sentry_dismantled" && ($ev_remotetime - $player->{last_sg_build}) > 120) || + ($action eq "dispenser_dismantled" && ($ev_remotetime - $player->{last_disp_build}) > 120)) + { + return "(IGNORED) OBJECT MOVED: " . $playerstr . " triggered \"$action\""; + } + } + } + elsif ($g_servers{$s_addr}->{play_game} == NS()) + { + my $typestring = ""; + if (defined($properties{type})) { + $typestring = "_".lc($properties{type}); + $typestring =~ s/\s/_/g; + } + $action .= $typestring; + } + + if (defined($properties{position})) { + $coords = $properties{position}; + ($x,$y,$z) = split(/ /,$coords); + } elsif (defined($properties{assister_position})) { + $coords = $properties{assister_position}; + ($x,$y,$z) = split(/ /,$coords); + } elsif (defined($properties{attacker_position})) { + $coords = $properties{attacker_position}; + ($x,$y,$z) = split(/ /,$coords); + } + if ($action eq "headshot") { + $g_servers{$s_addr}->{nextkillheadshot} = $player->{playerid} + } + + my $map = $g_servers{$s_addr}->get_map(); + if (defined($g_games{$g_servers{$s_addr}->{game}}{actions}{$map."_$action"}) && $g_games{$g_servers{$s_addr}->{game}}{actions}{$map."_$action"}{paction}) { + &doEvent_PlayerAction( + $playerId, + $playerUniqueId, + $map."_$action", + $x, + $y, + $z + ); + } + if (defined($g_games{$g_servers{$s_addr}->{game}}{actions}{$action}) && $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{paction}) { + my $actionname = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{descr}; + my $actionid = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{id}; + my $reward_player = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{reward_player}; + my $reward_team = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{reward_team}; + my $team = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{team}; + + &recordEvent( + "PlayerActions", 0, + $player->{playerid}, + $actionid, + $reward_player, + $x, + $y, + $z + ); + my $query = " + UPDATE + hlstats_Actions + SET + count=count+1 + WHERE + game='" . "eSQL($g_servers{$s_addr}->{game}) . "' + AND code = '"."eSQL($action)."' + "; + &execNonQuery($query); + $player->increment("skill", $reward_player); + $player->increment("session_skill", $reward_player); + $p_name = $player->{name}; + $p_skill = $player->{skill}; + + + if (($g_servers{$s_addr}->{broadcasting_events} == 1) && ($reward_player != 0)) { + if ($g_servers{$s_addr}->{broadcasting_player_actions} == 1) { + my $p_userid = $g_servers{$s_addr}->format_userid($player->{userid}); + if (($player->{is_bot} == 0) && ($player->{display_events} == 1) && ($player->{userid} > 0)) { + my $colorparam = $g_servers{$s_addr}->{format_color}; + my $coloraction = $g_servers{$s_addr}->{format_action}; + if ($reward_player !=0) { + my $verb = "got"; + if ($reward_player < 0) { + $verb = "lost"; + } + my $msg = sprintf("%s %s %s points (%s) for %s%s", $p_name, $verb, abs($reward_player),&number_format($p_skill),$coloraction,$actionname); + my $cmd_str = sprintf("%s %s%s %s",$rcmd,$p_userid,$colorparam,$g_servers{$s_addr}->quoteparam($msg)); + $g_servers{$s_addr}->dorcon($cmd_str); + } + } + } + } + if ($action eq "Got_The_Bomb") { + $player->set("has_bomb", "1", 1); + } elsif ($action eq "Dropped_The_Bomb") { + $player->set("has_bomb", "0", 1); + } + $player->updateDB(); + + if ($reward_team != 0 && $action ne "pointcaptured") { + if (!$team) { + $team = $player->{team}; + } + # print "Reward Team ($team) giving ($reward_team) for $actionname\n"; + &rewardTeam($team, $reward_team, $actionid, $actionname, $action); + } + } else { + $desc = "(IGNORED) "; + } + } + } else { + $desc = "(IGNORED) NOPLAYERINFO: "; + } + + return $desc . $playerstr . " triggered \"$action\""; +} + + +# +# 012. Team Objectives/Actions +# + +sub doEvent_TeamAction +{ + $rcmd = $g_servers{$s_addr}->{broadcasting_command}; + my ($team, $action) = @_; + my $desc; + if ($g_servers{$s_addr}->{num_trackable_players} < $g_servers{$s_addr}->{minplayers}) { + $desc = "(IGNORED) NOTMINPLAYERS: "; + +# Team events, such as Round_Win can still occur during Bonus Round +# } elsif (checkBonusRound()) { +# $desc = "(IGNORED) BonusRound: "; + + } else { + my $map = $g_servers{$s_addr}->get_map(); + if ((defined($g_games{$g_servers{$s_addr}->{game}}{actions}{$action}) && $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{taction}) || (defined($g_games{$g_servers{$s_addr}->{game}}{actions}{$map."_$action"}) && $g_games{$g_servers{$s_addr}->{game}}{actions}{$map."_$action"}{taction})) { + my $actionname = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{descr}; + my $actionid = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{id}; + my $reward_team = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{reward_team}; + my $actionteam = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{team}; + if ($actionteam eq "") { + $actionteam = $team; + } + #print "T: ".$actionid." - ".$actionname." - ".$actionteam." - ".$reward_player." - ".$reward_team." - ".$team."\n"; + my $query = " + UPDATE + hlstats_Actions + SET + count=count+1 + WHERE + game='" . "eSQL($g_servers{$s_addr}->{game}) . "' + AND code IN ('" . "eSQL($g_servers{$s_addr}->get_map() . "_$action") . "', '" . "eSQL($action) . "') + "; + &execNonQuery($query); + + if ($g_servers{$s_addr}->{round_status} == 0) { + if ($reward_team != 0) { + &rewardTeam($actionteam, $reward_team, $actionid, $actionname, $action); + } + } + } else { + $desc = "(IGNORED) "; + } + } + if ($g_servers{$s_addr}->{round_status} == 0) { + if (($action eq "CTs_Win") || ($action eq "Bomb_Defused")) { + $g_servers{$s_addr}->increment("round_status"); + $g_servers{$s_addr}->increment("ct_wins"); + $g_servers{$s_addr}->increment("map_ct_wins"); + $g_servers{$s_addr}->add_round_winner("ct"); + if ($action eq "Bomb_Defused") { + $g_servers{$s_addr}->increment("bombs_defused"); + } + } elsif (($action eq "Terrorists_Win") || ($action eq "Target_Bombed")) { + $g_servers{$s_addr}->increment("round_status"); + $g_servers{$s_addr}->increment("ts_wins"); + $g_servers{$s_addr}->increment("map_ts_wins"); + $g_servers{$s_addr}->add_round_winner("ts"); + if ($action eq "Target_Bombed") { + $g_servers{$s_addr}->increment("bombs_planted"); + } + } + } + $g_servers{$s_addr}->updateDB(); + return $desc . "Team \"$team\" triggered \"$action\""; +} + +# +# 013. World Objectives/Actions +# + +sub doEvent_WorldAction +{ + my $rcmd = $g_servers{$s_addr}->{broadcasting_command}; + my ($action) = @_; + my $desc; + my $act_players = $g_servers{$s_addr}->{num_trackable_players}; + my $min_players = $g_servers{$s_addr}->{minplayers}; + + if ($act_players < $min_players) { + $desc = "(IGNORED) NOTMINPLAYERS: "; + if ($action eq "Round_Start") { + $g_servers{$s_addr}->messageAll(sprintf("HLstatsX:CE disabled! Need at least %s active players (%s/%s)", $min_players, $act_players, $min_players),0,1); + } + } else { + my $map = $g_servers{$s_addr}->get_map(); + if ((defined($g_games{$g_servers{$s_addr}->{game}}{actions}{$action}) && $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{waction}) || (defined($g_games{$g_servers{$s_addr}->{game}}{actions}{$map."_$action"}) && $g_games{$g_servers{$s_addr}->{game}}{actions}{$map."_$action"}{waction})) { + my $actionname = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{descr}; + my $actionid = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{id}; + my $reward_team = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{reward_team}; + my $team = $g_games{$g_servers{$s_addr}->{game}}{actions}{$action}{team}; + my $query = " + UPDATE + hlstats_Actions + SET + count=count+1 + WHERE + game='" . "eSQL($g_servers{$s_addr}->{game}) . "' + AND code IN ('"."eSQL($map."_$action")."', '"."eSQL($action)."') + "; + &execNonQuery($query); + if ($team && $reward_team != 0) { + &rewardTeam($team, $reward_team, $actionid, $actionname); + } + } else { + $desc = "(IGNORED) "; + } + } + if ($action eq "Round_End" || $action eq "Round_Win" || $action eq "Mini_Round_Win") { + if ($action eq "Round_End") { + $g_servers{$s_addr}->analyze_teams(); + } + + if (($g_servers{$s_addr}->{lastdisabledbonus}+5) < $ev_remotetime && $g_servers{$s_addr}->{bonusroundignore} > 0) { + $g_servers{$s_addr}->set("bonusroundtime_ts", $ev_remotetime); + $g_servers{$s_addr}->set("lastdisabledbonus", $ev_remotetime); + $g_servers{$s_addr}->set("bonusroundtime_state", 1); + if ($act_players >= $min_players) { + $g_servers{$s_addr}->messageAll("Round Over - All actions/frags are ignored by HLstatsX:CE until the next round starts",0,1); + } + } + + while (my($pl, $player) = each(%g_players) ) { + if ($player->{connect_time} == 0) { + $player->set("connect_time", time()); + } + + &endKillStreak($player); + + if ($player->{auto_type} eq "end") { + &doEvent_Chat("say", + $player->{"userid"}, + $player->{"uniqueid"}, + $player->{auto_command} + ); + } + $player->updateDB(); + } + } + + + if ($action eq "Round_Start" || $action eq "Mini_Round_Start") { + $g_servers{$s_addr}->set("bonusroundtime_state", 0); + if ($action eq "Round_Start") { + $g_servers{$s_addr}->set("round_status", 0); + $g_servers{$s_addr}->set("ba_player_switch", 0); + } + + while (my($pl, $player) = each(%g_players) ) + { + if ($player->{connect_time} == 0) { + $player->set("connect_time", time()); + } + if ($action eq "Round_Start") { + $player->set("is_dead", "0", 1); + if ($player->{auto_type} eq "start") { + &doEvent_Chat("say", + $player->{"userid"}, + $player->{"uniqueid"}, + $player->{auto_command} + ); + } + } + $player->updateDB(); + } + } + if ($action eq "Game_Commencing") { + $g_servers{$s_addr}->set("round_status", 0); + $g_servers{$s_addr}->set("map_started", time()); + $g_servers{$s_addr}->set("map_rounds", 0); + $g_servers{$s_addr}->set("map_ct_wins", 0); + $g_servers{$s_addr}->set("map_ts_wins", 0); + } + $g_servers{$s_addr}->updateDB(); + return $desc . "World triggered \"$action\" ($act_players/$min_players)"; +} + + +# +# E014. Chat +# + +sub doEvent_Chat +{ + my ($msg_type, $playerId, $playerUniqueId, $message) = @_; + $rcmd = $g_servers{$s_addr}->{player_command}; + + my $player = lookupPlayer($s_addr, $playerId, $playerUniqueId); + my $playerstr = &getPlayerInfoString($player, $playerId); + + if (($player) && ($player->{is_bot} == 0)) { + $player->updateDB(); + my $p_userid = $g_servers{$s_addr}->format_userid($player->{userid}); + my $messagelen = length($message); + + if ($g_global_chat > 0 || $g_log_chat > 0) { + $hlx_command = 0; + my $cmd = $message; + if (($cmd =~ /^\/?skill$/i) || ($cmd =~ /^\/?rank$/i) || ($cmd =~ /^\/?points$/i) || ($cmd =~ /^\/?place$/i) || + ($cmd =~ /^\/?kdratio$/i) || ($cmd =~ /^\/?kdeath$/i) || ($cmd =~ /^\/?kpd$/i) || + ($cmd =~ /^\/?session$/i) || ($cmd =~ /^\/?session_data$/i) || + ($cmd =~ /^\/?top\d{1,2}?$/i) || + ($cmd =~ /^\/?statsme$/i) || + ($cmd =~ /^\/?next$/i) || + ($cmd =~ /^\/?knife$/i) || ($cmd =~ /^\/?usp$/i) || ($cmd =~ /^\/?glock$/i) || ($cmd =~ /^\/?deagle$/i) || + ($cmd =~ /^\/?p228$/i) || ($cmd =~ /^\/?m3$/i) || ($cmd =~ /^\/?xm1014$/i) || ($cmd =~ /^\/?mp5navy$/i) || + ($cmd =~ /^\/?tmp$/i) || ($cmd =~ /^\/?p90$/i) || ($cmd =~ /^\/?m4a1$/i) || ($cmd =~ /^\/?ak47$/i) || + ($cmd =~ /^\/?sg552$/i) || ($cmd =~ /^\/?scout$/i) || ($cmd =~ /^\/?awp$/i) || ($cmd =~ /^\/?g3sg1$/i) || + ($cmd =~ /^\/?m249$/i) || ($cmd =~ /^\/?hegrenade$/i) || ($cmd =~ /^\/?flashbang$/i) || ($cmd =~ /^\/?elite$/i) || + ($cmd =~ /^\/?aug$/i) || ($cmd =~ /^\/?mac10$/i) || ($cmd =~ /^\/?fiveseven$/i) || ($cmd =~ /^\/?ump45$/i) || + ($cmd =~ /^\/?sg550$/i) || ($cmd =~ /^\/?famas$/i) || ($cmd =~ /^\/?galil$/i) || + ($cmd =~ /^\/?maps$/i) || ($cmd =~ /^\/?map_stats$/i) || ($cmd =~ /^\/?map$/i) || + ($cmd =~ /^\/?kill$/i) || ($cmd =~ /^\/?kills$/i) || ($cmd =~ /^\/?player_kills$/i) || + ($cmd =~ /^\/?weapon$/i) || ($cmd =~ /^\/?weapons$/i) || ($cmd =~ /^\/?weapon_usage$/i) || + ($cmd =~ /^\/?action$/i) || ($cmd =~ /^\/?actions$/i) || ($cmd =~ /^\/?hlx_menu$/i) || + ($cmd =~ /^\/?status$/i) || ($cmd =~ /^\/?load$/i) || ($cmd =~ /^\/?pro$/i) || ($cmd =~ /^\/?servers$/i) || + ($cmd =~ /^\/?clans$/i) || ($cmd =~ /^\/?cheaters$/i) || ($cmd =~ /^\/?bans$/i) || ($cmd =~ /^\/?statsme$/i) || + ($cmd =~ /^\/?help$/i) || ($cmd =~ /^\/?timeleft$/i) || ($cmd =~ /^\/?nextmap$/i) || ($cmd =~ /^\/?thetime$/i) || + ($cmd =~ /^\/?hlx_display/i) || ($cmd =~ /^\/?hlx_teams/i) || + ($cmd =~ /^\/?hlx_set/i) || ($cmd =~ /^\/?hlx_chat/i) || ($cmd =~ /^\/?hlx_auto/i)) { + + $hlx_command++; + } + + if (($messagelen > 3) && ($messagelen < 15)) { # filter buy scripts + + if (($cmd =~ /ak47/i) || ($cmd =~ /ak/i) || ($cmd =~ /m4/i) || ($cmd =~ /m4a1/i) || + ($cmd =~ /deagle/i) || ($cmd =~ /famas/i) || ($cmd =~ /galil/i) || + ($cmd =~ /scout/i) || ($cmd =~ /awp/i) || ($cmd =~ /awm/i) || + ($cmd =~ /aug/i) || ($cmd =~ /m249/i) || ($cmd =~ /para/i) || + ($cmd =~ /sig/i) || ($cmd =~ /tmp/i) || ($cmd =~ /ak47/i) || + ($cmd =~ /grenade/i) || ($cmd =~ /usp/i) || ($cmd =~ /glock/i)) { + + $hlx_command++; + } + } + + if ($g_log_chat > 0) { + my $log = 1; + if ($g_log_chat_admins == 0) { + my $steamid = $player->{uniqueid}; + if ($g_servers{$s_addr}->is_admin($steamid) == 1) { + $log = 0; + } + } + + if (($hlx_command == 0) && ($log == 1)) { + my $saytype = 1; + if ($msg_type eq "say_team") { + $saytype = 2; + } elsif ($msg_type eq "say_squad") { + $saytype = 3; + } + &recordEvent( + "Chat", + 0, + $player->{playerid}, + $saytype, + $message + ); + } + } + + if (($g_global_chat > 0) && ($hlx_command == 0)) { + my $p_name = $player->{name}; + my $dead = ""; + if ($player->{is_dead} == 1) { + $dead = "*DEAD* "; + } + my $b_message = $dead.$p_name." (".$g_servers{$s_addr}->{name}."): ".$message; + send_global_chat($b_message); + } + } + + if ($message =~ /^\/?hlx_set ([^ ]+) (.+)$/i) { + my $set_field = lc($1); + my $set_value = $2; + + if ($set_field eq "name" || $set_field eq "realname") { + &updatePlayerProfile($player, "fullName", $set_value); + } elsif ($set_field eq "email" || $set_field eq "e-mail") { + &updatePlayerProfile($player, "email", $set_value); + } elsif ($set_field eq "homepage" || $set_field eq "url") { + &updatePlayerProfile($player, "homepage", $set_value); + } elsif ($set_field eq "icq" || $set_field eq "uin") { + &updatePlayerProfile($player, "icq", $set_value); + } elsif ($set_field eq "geo") { + # string parsen aus $set_value + my $flag = substr($set_value,0,2); + my $cstr = substr($set_value,3,200); + $cstr =~ /\'([^\']+)\' /; + my $city = $1; + $cstr = substr($set_value,index($set_value,"'",4)+1,200); + $cstr =~ /([^ ]+) ([^ ]+)/; + my $lat = $1; + my $lng = $2; + my $region = ""; + } + } elsif ($message =~ /^\/?hlx_hideranking$/i) { + my $result = &doQuery(" + SELECT + hideranking + FROM + hlstats_Players + WHERE + playerId = " . $player->{playerid} + ); + my ($hideranking) = $result->fetchrow_array; + $result->finish; + + my $hidedesc = ""; + + if ($hideranking == 0) { + $hideranking = 1; + $hidedesc = "HIDDEN from"; + } else { + $hideranking = 0; + $hidedesc = "VISIBLE on"; + } + my $playerName = &abbreviate($player->{name}); + &execNonQuery(" + UPDATE + hlstats_Players + SET + hideranking='$hideranking' + WHERE + playerId=" . $player->{playerid} + ); + + if ($player->{display_events} == 1) { + $cmd_str = $rcmd." $p_userid ".$g_servers{$s_addr}->quoteparam("'$playerName' is now $hidedesc the rankings"); + } + $g_servers{$s_addr}->dorcon($cmd_str); + } + + if (($message =~ /^\/?hlx_auto ([^ ]+) ([^ ]+)$/i) || ($message =~ /^\/?hlx_auto_cmd ([^ ]+) ([^ ]+)$/i)) { + my $type = lc($1); + my $cmd = lc($2); + + if (($type =~ /^start$/i) || ($type =~ /^end$/i) || ($type =~ /^kill$/i)) { + $time = 0; + if (($cmd =~ /^\/?skill$/i) || ($cmd =~ /^\/?rank$/i) || ($cmd =~ /^\/?points$/i) || ($cmd =~ /^\/?place$/i) || + ($cmd =~ /^\/?kdratio$/i) || ($cmd =~ /^\/?kdeath$/i) || ($cmd =~ /^\/?kpd$/i) || + ($cmd =~ /^\/?session$/i) || ($cmd =~ /^\/?session_data$/i) || + ($cmd =~ /^\/?top\d{1,2}?$/i) || + ($cmd =~ /^\/?statsme$/i) || + ($cmd =~ /^\/?next$/i) || + ($cmd =~ /^\/?knife$/i) || ($cmd =~ /^\/?usp$/i) || ($cmd =~ /^\/?glock$/i) || ($cmd =~ /^\/?deagle$/i) || + ($cmd =~ /^\/?p228$/i) || ($cmd =~ /^\/?m3$/i) || ($cmd =~ /^\/?xm1014$/i) || ($cmd =~ /^\/?mp5navy$/i) || + ($cmd =~ /^\/?tmp$/i) || ($cmd =~ /^\/?p90$/i) || ($cmd =~ /^\/?m4a1$/i) || ($cmd =~ /^\/?ak47$/i) || + ($cmd =~ /^\/?sg552$/i) || ($cmd =~ /^\/?scout$/i) || ($cmd =~ /^\/?awp$/i) || ($cmd =~ /^\/?g3sg1$/i) || + ($cmd =~ /^\/?m249$/i) || ($cmd =~ /^\/?hegrenade$/i) || ($cmd =~ /^\/?flashbang$/i) || ($cmd =~ /^\/?elite$/i) || + ($cmd =~ /^\/?aug$/i) || ($cmd =~ /^\/?mac10$/i) || ($cmd =~ /^\/?fiveseven$/i) || ($cmd =~ /^\/?ump45$/i) || + ($cmd =~ /^\/?sg550$/i) || ($cmd =~ /^\/?famas$/i) || ($cmd =~ /^\/?galil$/i) || + ($cmd =~ /^\/?maps$/i) || ($cmd =~ /^\/?map_stats$/i) || ($cmd =~ /^\/?map$/i) || + ($cmd =~ /^\/?kill$/i) || ($cmd =~ /^\/?kills$/i) || ($cmd =~ /^\/?player_kills$/i) || + ($cmd =~ /^\/?weapon$/i) || ($cmd =~ /^\/?weapons$/i) || ($cmd =~ /^\/?weapon_usage$/i) || + ($cmd =~ /^\/?action$/i) || ($cmd =~ /^\/?actions$/i)) { + + $player->{"auto_type"} = $type; + $player->{"auto_command"} = $cmd; + $player->{"auto_time"} = $time; + $player->{"auto_time_count"} = 0; + + if ($player->{display_events} == 1) { + $cmd_str = $rcmd." $p_userid ".$g_servers{$s_addr}->quoteparam("Set auto command to ".$player->{"auto_command"}." on ".$player->{"auto_type"}."!"); + } + $g_servers{$s_addr}->dorcon($cmd_str); + } + } + } + if ($message =~ /^\/?hlx_auto clear$/i) { + $player->{"auto_type"} = ""; + $player->{"auto_command"} = ""; + + if ($player->{display_events} == 1) { + $cmd_str = $rcmd." $p_userid ".$g_servers{$s_addr}->quoteparam("Auto command is disabled!"); + } + $g_servers{$s_addr}->dorcon($cmd_str); + } + + ### Begin switching team balance + elsif ($message =~ /^\/?hlx_teams([ ][0-9])?$/i) { + my $steamid = $player->{uniqueid}; + my $mode = -1; + if (($1 eq " 0") || ($1 eq " 1")) { + $mode = substr($1, 1, length($1)-1); + } + if (($mode > -1) && ($g_servers{$s_addr}->is_admin($steamid) == 1)) { + if ($mode == 0) { + $g_servers{$s_addr}->set("ba_enabled", 0); + $admin_msg = "AUTO-TEAM BALANCER disabled"; + if ($g_servers{$s_addr}->{player_admin_command} ne "") { + $cmd_str = $g_servers{$s_addr}->{player_admin_command}." ".$g_servers{$s_addr}->quoteparam($admin_msg); + } else { + if ($player->{display_events} == 1) { + $cmd_str = $rcmd." $p_userid ".$g_servers{$s_addr}->quoteparam($admin_msg); + } + } + $g_servers{$s_addr}->dorcon($cmd_str); + } elsif ($mode == 1) { + $g_servers{$s_addr}->set("ba_enabled", 1); + $admin_msg = "AUTO-TEAM BALANCER enabled"; + if ($g_servers{$s_addr}->{player_admin_command} ne "") { + $cmd_str = $g_servers{$s_addr}->{player_admin_command}." ".$g_servers{$s_addr}->quoteparam($admin_msg); + } else { + if ($player->{display_events} == 1) { + $cmd_str = $rcmd." $p_userid ".$g_servers{$s_addr}->quoteparam($admin_msg); + } + } + $g_servers{$s_addr}->dorcon($cmd_str); + } + } + } + + ### Disabling hlx output + elsif ($message =~ /^\/?hlx_display\s?([01])$/i) { + my $mode = 0; + $mode = $1; + + if ($mode == 0) { + $msg = "All console events are disabled!"; + } else { + $msg = "All console events are enabled!"; + } + $player->set("display_events", $mode); + &updatePlayerProfile($player, "displayEvents", $mode); + $cmd_str = $rcmd." $p_userid ".$g_servers{$s_addr}->quoteparam($msg); + $g_servers{$s_addr}->dorcon($cmd_str); + } + + ### Disabling hlx global chat output + elsif ($message =~ /^\/?hlx_chat\s?([01])$/i) { + my $mode = 0; + $mode = $1; + + if ($mode == 0) { + $msg = "Global chat output is disabled!"; + } else { + $msg = "Global chat output is enabled!"; + } + $player->set("display_chat", $mode); + $cmd_str = $rcmd." $p_userid ".$g_servers{$s_addr}->quoteparam($msg); + $g_servers{$s_addr}->dorcon($cmd_str); + } + + ### Skill Addon + elsif (($message =~ /^\/?skill([ ][0-9]+)?$/i) || ($message =~ /^\/?rank([ ][0-9]+)?$/i) || ($message =~ /^\/?points([ ][0-9]+)?$/i) || ($message =~ /^\/?place([ ][0-9]+)?$/i)) { + my $error = 0; + if ($1 ne "") { + $userid = substr($1, 1, length($1)-1); + my $found = 0; + while (my($pl, $s_player) = each(%g_players) ) { + if ($userid == $s_player->{userid}) { + $player = $s_player; + $found++; + } + } + if ($found == 0) { + $error++; + } + } + my $playerName = $player->{name}; + my ($skill, $ranknumber, $totalplayers) = get_player_rank($player); + if ($ranknumber==0) { + $ranknumber = "(HIDDEN)"; + } + + if ($error == 0) { + if ($message !~ /^\/?place([ ][0-9]+)?$/i) { + $rcmd = $g_servers{$s_addr}->{player_command_osd}; + + if ($rcmd ne "") { + my ($skill, $kills, $deaths, $kd, $suicides, $headshots, $hpk, $acc, $connection_time, $fav_weapon, $fav_weapon_kills, $fav_weapon_acc, $s_fav_weapon, $s_fav_weapon_kills, $s_fav_weapon_acc) = get_player_data($player, 0); + my $osd_string = get_menu_text($player, $skill, $kills, $deaths, $kd, $suicides, $headshots, $hpk, $acc, $connection_time, $fav_weapon, $fav_weapon_kills, $fav_weapon_acc, $s_fav_weapon, $s_fav_weapon_kills, $s_fav_weapon_acc, $ranknumber, $totalplayers); + $cmd_str = sprintf("%s \"15\" %s %s", $rcmd, $p_userid, $g_servers{$s_addr}->quoteparam($osd_string)); + } else { + $rcmd = $g_servers{$s_addr}->{player_command}; + my $msg = ""; + if ($g_ranktype eq "kills") { + my $kills = $player->{total_kills}; + $msg = sprintf("%s is on rank %d of %d with %d kills!", $playerName, $ranknumber, $totalplayers, $kills); + } else { + $msg = sprintf("%s is on rank %d of %d with %d points!", $playerName, $ranknumber, $totalplayers, $skill); + } + $cmd_str = sprintf("%s %s %s", $rcmd, $p_userid, $g_servers{$s_addr}->quoteparam($msg)); + } + } elsif ($g_servers{$s_addr}->{"public_commands"} == 1) { + if ($g_ranktype eq "kills") { + my $kills = $player->{total_kills}; + $cmd_str = ""; + $g_servers{$s_addr}->messageAll(sprintf("%s is on rank %d of %d with %d kills!", $playerName, $ranknumber, $totalplayers, $kills)); + } else { + $cmd_str = ""; + $g_servers{$s_addr}->messageAll(sprintf("%s is on rank %d of %d with %d points!", $playerName, $ranknumber, $totalplayers, $skill)); + } + } + } else { + $rcmd = $g_servers{$s_addr}->{player_command}; + if ($player->{display_events} == 1) { + $cmd_str = "$rcmd $p_userid ".$g_servers{$s_addr}->quoteparam("No player found with this userid. Type status in console to get players userid!"); + } + } + if ($cmd_str ne "") + { + $g_servers{$s_addr}->dorcon($cmd_str); + } + } + ### End of Skill Addon + + ### Begin of Kill-Death Ratio + elsif (($message =~ /^\/?kdratio([ ][0-9]+)?$/i) || ($message =~ /^\/?kdeath([ ][0-9]+)?$/i) || ($message =~ /^\/?kpd([ ][0-9]+)?$/i)) { + my $userid = 0; + my $found = 0; + if ($1 eq "") { + $found++; + } else { + $userid = $1; + while (my($pl, $s_player) = each(%g_players) ) { + if ($userid == $s_player->{userid}) { + $player = $s_player; + $found++; + } + } + } + + if ($found > 0 ) { + my ($skill, $kills, $deaths, $kd, $suicides, $headshots, $hpk, $acc, $connection_time, $fav_weapon, $fav_weapon_kills, $fav_weapon_acc, $s_fav_weapon, $s_fav_weapon_kills, $s_fav_weapon_acc, $ranknumber, $totalplayers) = get_player_data($player, 1); + my $playerName = $player->{name}; + + my $accstring = ""; + if ($acc != "0.0") { + $accstring = $acc."% accuracy,"; + } + + if ($message !~ /^\/?kdeath([ ][0-9]+)?$/i) { + if ($g_servers{$s_addr}->{player_command_osd} ne "") { + my $osd_string = get_menu_text($player, $skill, $kills, $deaths, $kd, $suicides, $headshots, $hpk, $acc, $connection_time, $fav_weapon, $fav_weapon_kills, $fav_weapon_acc, $s_fav_weapon, $s_fav_weapon_kills, $s_fav_weapon_acc, $ranknumber, $totalplayers); + $osd_string = $g_servers{$s_addr}->quoteparam($osd_string); + $cmd_str = sprintf("%s \"10\" %s %s", $g_servers{$s_addr}->{"player_command_osd"}, $p_userid, $osd_string); + } else { + if ($player->{display_events} == 1) { + $cmd_str = $g_servers{$s_addr}->{player_command}." ".$p_userid." ".$g_servers{$s_addr}->quoteparam(sprintf("%s has %d:%d frags, %d headshots (%s%%),%s and a KD-Radio of %s", $playerName, $kills, $deaths, $headshots, $hpk, $accstring, $kd)); + } + } + } else { + if ($g_servers{$s_addr}->{"public_commands"} == 1) { + $cmd_str = ""; + $g_servers{$s_addr}->messageAll(sprintf("%s has %d:%d frags, %d headshots (%s%%),%s and a KD-Ratio of %s", $playerName, $kills, $deaths, $headshots, $hpk, $accstring, $kd)); + } else { + $cmd_str = $g_servers{$s_addr}->{player_command}." ".$p_userid." ".$g_servers{$s_addr}->quoteparam(sprintf("%s has %d:%d frags, %d headshots (%s%%),%s and a KD-Ratio of %s", $playerName, $kills, $deaths, $headshots, $hpk, $accstring, $kd)); + } + } + } else { + if ($player->{display_events} == 1) { + $cmd_str = $g_servers{$s_addr}->{player_command}." ".$p_userid." ".$g_servers{$s_addr}->quoteparam("No player found with this userid. Type status in console to get players userid!"); + } + } + if ($cmd_str ne "") + { + $g_servers{$s_addr}->dorcon($cmd_str); + } + + } + ### End of Kill-Death Ratio + + ### Begin of Session stats + elsif (($message =~ /^\/?session([ ][0-9]+)?$/i) || ($message =~ /^\/?session_data([ ][0-9]+)?$/i)) { + my $error = 0; + if ($1 ne "") { + $userid = substr($1, 1, length($1)-1); + my $found = 0; + while (my($pl, $s_player) = each(%g_players) ) { + if ($userid == $s_player->{userid}) { + $player = $s_player; + $found++; + } + } + if ($found == 0) { + $error++; + } + } + my $playerName = $player->{name}; + my $kills = $player->{session_kills}; + my $deaths = $player->{session_deaths}; + my $headshots = $player->{session_headshots}; + my $suicides = $player->{session_suicides}; + my $skill = $player->{session_skill}; + my $shots = $player->{session_shots}; + my $hits = $player->{session_hits}; + my $kdstring = ""; + + if ($deaths > 0) { + $kd = sprintf("%.3f", $kills/$deaths); + $kdstring = sprintf(" (%.2f%%)", $kills/$deaths) + } else { + $kd = sprintf("%.3f", $kills); + } + if ($kills > 0) { + $hpk = sprintf("%.0f", (100/$kills) * $headshots); + } else { + $hpk = sprintf("%.0f", $kills); + } + if ($shots > 0) { + $acc = sprintf("%.1f", (100/$shots) * $hits); + } else { + $acc = sprintf("%.1f", $shots); + } + my $accstring = ""; + if ($acc != "0.0") { + $accstring = $acc." accuracy,"; + } + + if ($error == 0) { + my $pointstr=""; + if ($g_ranktype ne "kills") { + $pointstr = sprintf(" and a skill change of %d points", $skill); + } + my $msg = sprintf("%s has %d:%d frags%s, %d headshots (%s%%),%s%s", $playerName, $kills, $deaths, $kdstring, $headshots, $hpk, $accstring, $pointstr); + if ($message !~ /^\/?session_data([ ][0-9]+)?$/i) { + if ($g_servers{$s_addr}->{player_command_osd} ne "") { + my ($se_skill, $se_kills, $se_deaths, $se_kd, $se_suicides, $se_headshots, $se_hpk, $se_acc, $se_connection_time, $se_fav_weapon, $se_fav_weapon_kills, $se_fav_weapon_acc, $se_s_fav_weapon, $se_s_fav_weapon_kills, $se_s_fav_weapon_acc, $se_ranknumber, $se_totalplayers) = get_player_data($player, 1); + my $osd_string = get_menu_text($player, $se_skill, $se_kills, $se_deaths, $se_kd, $se_suicides, $se_headshots, $se_hpk, $se_acc, $se_connection_time, $se_fav_weapon, $se_fav_weapon_kills, $se_fav_weapon_acc, $se_s_fav_weapon, $se_s_fav_weapon_kills, $se_s_fav_weapon_acc, $se_ranknumber, $se_totalplayers); + $osd_string = $g_servers{$s_addr}->quoteparam($osd_string); + $cmd_str = sprintf("%s \"10\" %s %s", $g_servers{$s_addr}->{player_command_osd}, $p_userid, $osd_string); + } else { + if ($player->{display_events} == 1) { + $cmd_str = sprintf("%s %s %s", $g_servers{$s_addr}->{player_command}, $p_userid, $g_servers{$s_addr}->quoteparam($msg)); + } + } + } else { + if ($g_servers{$s_addr}->{"public_commands"} == 1) { + $cmd_str = ""; + $g_servers{$s_addr}->messageAll($msg); + } else { + $cmd_str = sprintf("%s %s %s", $g_servers{$s_addr}->{player_command}, $p_userid, $g_servers{$s_addr}->quoteparam($msg)); + } + } + } else { + if ($player->{display_events} == 1) { + $cmd_str = $g_servers{$s_addr}->{player_command}." ".$p_userid." ".$g_servers{$s_addr}->quoteparam("No player found with this userid. Type status in console to get players userid!"); + } + } + if ($cmd_str ne "") + { + $g_servers{$s_addr}->dorcon($cmd_str); + } + } + ### End of Session stats + + ### status Addon + elsif ($message =~ /^\/?status([ ][0-9]+)?$/i) { + if ($g_servers{$s_addr}->{use_browser} > 0) { + my $url = $g_servers{$s_addr}->{ingame_url}; + my $server_id = $g_servers{$s_addr}->{id}; + my $game = $g_servers{$s_addr}->{game}; + if ($1 ne "") { + $server_id = substr($1, 1, length($1)-1); + } + my $fullurl = $g_servers{$s_addr}->quoteparam(sprintf("%s/ingame.php?game=%s&mode=status&server_id=%s", $url, $game, $server_id)); + my $browsecmd = $g_servers{$s_addr}->{browse_command}; + if (($g_servers{$s_addr}->{"mod"} eq "BEETLE")) { + $cmd_status = sprintf("%s %s say %s %s" , $g_servers{$s_addr}->{exec_command}, $p_userid, $browsecmd, $fullurl); + } else { + $cmd_status = sprintf("%s %s %s", $browsecmd, $p_userid, $fullurl); + } + $g_servers{$s_addr}->dorcon($cmd_status); + } + } + + ### load Addon + elsif ($message =~ /^\/?load$/i) { + if ($g_servers{$s_addr}->{use_browser} > 0) { + my $url = $g_servers{$s_addr}->{ingame_url}; + my $server_id = $g_servers{$s_addr}->{id}; + my $game = $g_servers{$s_addr}->{game}; + my $fullurl = $g_servers{$s_addr}->quoteparam(sprintf("%s/ingame.php?mode=status&server_id=%s&mode=load", $url, $server_id)); + my $browsecmd = $g_servers{$s_addr}->{browse_command}; + if ($g_servers{$s_addr}->{"mod"} eq "BEETLE") { + $cmd_load = sprintf("%s %s say %s %s", $g_servers{$s_addr}->{exec_command}, $p_userid, $browsecmd, $fullurl); + } else { + $cmd_load = sprintf("%s %s %s", $browsecmd, $p_userid, $fullurl); + } + $g_servers{$s_addr}->dorcon($cmd_load); + } + } + + ### servers Addon + elsif ($message =~ /^\/?servers$/i) { + if ($g_servers{$s_addr}->{use_browser} > 0) { + my $url = $g_servers{$s_addr}->{ingame_url}; + my $game = $g_servers{$s_addr}->{game}; + my $fullurl = $g_servers{$s_addr}->quoteparam(sprintf("%s/ingame.php?game=%s&mode=servers", $url, $game)); + my $browsecmd = $g_servers{$s_addr}->{browse_command}; + if ($g_servers{$s_addr}->{"mod"} eq "BEETLE") { + $cmd_servers = sprintf("%s %s say %s %s", $g_servers{$s_addr}->{exec_command}, $p_userid, $browsecmd, $fullurl); + } else { + $cmd_servers = sprintf("%s %s %s", $browsecmd, $p_userid, $fullurl); + } + $g_servers{$s_addr}->dorcon($cmd_servers); + } + } + + ### cheaters Addon + elsif ($message =~ /^\/?cheaters$/i || $message =~ /^\/?bans$/i) { + if ($g_servers{$s_addr}->{use_browser} > 0) { + my $url = $g_servers{$s_addr}->{ingame_url}; + my $game = $g_servers{$s_addr}->{game}; + my $fullurl = $g_servers{$s_addr}->quoteparam(sprintf("%s/ingame.php?game=%s&mode=bans", $url, $game)); + my $browsecmd = $g_servers{$s_addr}->{browse_command}; + if ($g_servers{$s_addr}->{"mod"} eq "BEETLE") { + $cmd_cheaters = sprintf("%s %s say %s %s", $g_servers{$s_addr}->{exec_command}, $p_userid, $browsecmd, $fullurl); + } else { + $cmd_cheaters = sprintf("%s %s %s", $browsecmd, $p_userid, $fullurl); + } + $g_servers{$s_addr}->dorcon($cmd_cheaters); + } + } + + ### Actions Addon + elsif (($message =~ /^\/?action$/i) || ($message =~ /^\/?actions$/i)) { + if ($g_servers{$s_addr}->{use_browser} > 0) { + my $p_playerid = $player->{playerid}; + my $url = $g_servers{$s_addr}->{ingame_url}; + my $game = $g_servers{$s_addr}->{game}; + my $fullurl = $g_servers{$s_addr}->quoteparam(sprintf("%s/ingame.php?game=%s&mode=actions&player=%s", $url, $game, $p_playerid)); + my $browsecmd = $g_servers{$s_addr}->{browse_command}; + + if ($g_servers{$s_addr}->{mod} eq "BEETLE") { + $cmd_actions = sprintf("%s %s say %s %s", $g_servers{$s_addr}->{exec_command}, $p_userid, $browsecmd, $fullurl); + } else { + $cmd_actions = sprintf("%s %s %s", $browsecmd, $p_userid, $fullurl); + } + $g_servers{$s_addr}->dorcon($cmd_actions); + } + } + + ### Accuracy Addon + elsif ($message =~ /^\/?accuracy$/i) { + if ($g_servers{$s_addr}->{use_browser} > 0) { + my $p_playerid = $player->{playerid}; + my $url = $g_servers{$s_addr}->{ingame_url}; + my $game = $g_servers{$s_addr}->{game}; + my $fullurl = $g_servers{$s_addr}->quoteparam(sprintf("%s/ingame.php?game=%s&mode=accuracy&player=%s", $url, $game, $p_playerid)); + my $browsecmd = $g_servers{$s_addr}->{browse_command}; + if ($g_servers{$s_addr}->{"mod"} eq "BEETLE") { + $cmd_accuracy = sprintf("%s %s say %s %s", $g_servers{$s_addr}->{exec_command}, $p_userid, $browsecmd, $fullurl); + } else { + $cmd_accuracy = sprintf("%s %s %s", $browsecmd, $p_userid, $fullurl); + } + $g_servers{$s_addr}->dorcon($cmd_accuracy); + } + } + + ### Targets Addon + elsif (($message =~ /^\/?targets$/i) || ($message =~ /^\/?target$/i)) { + if ($g_servers{$s_addr}->{use_browser} > 0) { + my $p_playerid = $player->{playerid}; + my $url = $g_servers{$s_addr}->{ingame_url}; + my $game = $g_servers{$s_addr}->{game}; + my $fullurl = $g_servers{$s_addr}->quoteparam(sprintf("%s/ingame.php?game=%s&mode=targets&player=%s", $url, $game, $p_playerid)); + my $browsecmd = $g_servers{$s_addr}->{browse_command}; + if ($g_servers{$s_addr}->{"mod"} eq "BEETLE") { + $cmd_targets = sprintf("%s %s say %s %s", $g_servers{$s_addr}->{exec_command}, $p_userid, $browsecmd, $fullurl); + } else { + $cmd_targets = sprintf("%s %s %s", $browsecmd, $p_userid, $fullurl); + } + $g_servers{$s_addr}->dorcon($cmd_targets); + } + } + + ### Clans Addon + elsif ($message =~ /^\/?clans$/i) { + if ($g_servers{$s_addr}->{use_browser} > 0) { + my $url = $g_servers{$s_addr}->{ingame_url}; + my $game = $g_servers{$s_addr}->{game}; + my $fullurl = $g_servers{$s_addr}->quoteparam(sprintf("%s/ingame.php?game=%s&mode=clans", $url, $game)); + my $browsecmd = $g_servers{$s_addr}->{browse_command}; + if ($g_servers{$s_addr}->{"mod"} eq "BEETLE") { + $cmd_clans = sprintf("%s %s say %s %s", $g_servers{$s_addr}->{exec_command}, $p_userid, $browsecmd, $fullurl); + } else { + $cmd_clans = sprintf("%s %s %s", $browsecmd, $p_userid, $fullurl); + } + $g_servers{$s_addr}->dorcon($cmd_clans); + } + } + + ### Begin of next Addon + elsif ($message =~ /^\/?next$/i) { + if ($g_servers{$s_addr}->{player_command_osd} ne "") { + my $playerName = $player->{name}; + my ($ranknumber, $totalplayers, $osd_message) = get_next_ranks($player); + $osd_string = $g_servers{$s_addr}->quoteparam($osd_message); + $cmd_str = sprintf("%s \"10\" %s %s", $g_servers{$s_addr}->{"player_command_osd"}, $p_userid, $osd_string); + $g_servers{$s_addr}->dorcon($cmd_str); + } + } + + ### Begin of Top-Players Addon + elsif ($message =~ /^\/?top\d{1,2}?$/i) { + my $limit = 10; + $message =~ /top(\d*)/i; + if ($1 > 12) { + $limit = 12; + } else { + $limit = $1; + } + if ($g_servers{$s_addr}->{player_command_osd} ne "") { + my $result = &doQuery(" + SELECT + $g_ranktype, lastName, + kills / IF(deaths=0,1,deaths) AS kpd + FROM + hlstats_Players + WHERE + game='"."eSQL($player->{game})."' + AND hideranking = 0 + AND kills >= 1 + ORDER BY + $g_ranktype DESC, kpd DESC + LIMIT 0, ".$limit); + + my $rankword = ""; + if ($g_ranktype eq "kills") { + $rankword = " kills"; + } + + my $osd_message = "->1 - Top Players\\n"; + my $i = 0; + my $last_base = ""; + while (my($base, $lastName) = $result->fetchrow_array) { + $i++; + if (length($lastName) > 20) { + $lastName = substr($lastName, 0, 17)."..."; + } + if ($last_base eq "") { + $osd_message .= sprintf(" %02d %s%s - %s", $i, &number_format($base), $rankword, $lastName)."\\n"; + $last_base = $base; + } else { + $osd_message .= sprintf(" %02d %s%s -%04d %s", $i, &number_format($base), $rankword, ($last_base-$base), $lastName)."\\n"; + } + } + $result->finish; + $osd_string = $g_servers{$s_addr}->quoteparam($osd_message); + $cmd_str = sprintf("%s \"15\" %s %s", $g_servers{$s_addr}->{player_command_osd}, $p_userid, $osd_string); + $g_servers{$s_addr}->dorcon($cmd_str); + + } elsif ($g_servers{$s_addr}->{use_browser} > 0) { + my $url = $g_servers{$s_addr}->{ingame_url}; + my $game = $g_servers{$s_addr}->{game}; + my $fullurl = $g_servers{$s_addr}->quoteparam(sprintf("%s/ingame.php?game=%s&mode=players", $url, $game)); + my $browsecmd = $g_servers{$s_addr}->{browse_command}; + if ($g_servers{$s_addr}->{"mod"} eq "BEETLE") { + $cmd_players = sprintf("%s %s say %s %s", $g_servers{$s_addr}->{exec_command}, $p_userid, $browsecmd, $fullurl); + } else { + $cmd_players = sprintf("%s %s %s", $browsecmd, $p_userid, $fullurl); + } + $g_servers{$s_addr}->dorcon($cmd_players); + } + } + ### End of Top10 Addon + + ### Begin of statsme Addon + elsif ($message =~ /^\/?statsme$/i) { + if ($g_servers{$s_addr}->{use_browser} > 0) { + my $p_playerid = $player->{playerid}; + my $url = $g_servers{$s_addr}->{ingame_url}; + my $game = $g_servers{$s_addr}->{game}; + my $fullurl = $g_servers{$s_addr}->quoteparam(sprintf("%s/ingame.php?game=%s&mode=statsme&player=%s", $url, $game, $p_playerid)); + my $browsecmd = $g_servers{$s_addr}->{browse_command}; + if ($g_servers{$s_addr}->{"mod"} eq "BEETLE") { + $cmd_statsme = sprintf("%s %s say %s %s", $g_servers{$s_addr}->{exec_command}, $p_userid, $browsecmd, $fullurl); + } else { + $cmd_statsme = sprintf("%s %s %s", $browsecmd, $p_userid, $fullurl); + } + $g_servers{$s_addr}->dorcon($cmd_statsme); + } + } + ### End of statsme Addon + + ### Begin Player-Weapons Addon + elsif (($message =~ /^\/?weapon$/i) || ($message =~ /^\/?weapons$/i)) { + if ($g_servers{$s_addr}->{use_browser} > 0) { + my $p_playerid = $player->{playerid}; + my $url = $g_servers{$s_addr}->{ingame_url}; + my $game = $g_servers{$s_addr}->{game}; + my $fullurl = $g_servers{$s_addr}->quoteparam(sprintf("%s/ingame.php?game=%s&mode=weapons&player=%s", $url, $game, $p_playerid)); + my $browsecmd = $g_servers{$s_addr}->{browse_command}; + if ($g_servers{$s_addr}->{"mod"} eq "BEETLE") { + $cmd_weapons = sprintf("%s %s say %s %s", $g_servers{$s_addr}->{exec_command}, $p_userid, $browsecmd, $fullurl); + } else { + $cmd_weapons = sprintf("%s %s %s", $browsecmd, $p_userid, $fullurl); + } + $g_servers{$s_addr}->dorcon($cmd_weapons); + } + } + ### End of Player-Weapons Addon + + ### Begin Player-Kills Addon + elsif (($message =~ /^\/?kill$/i) || ($message =~ /^\/?kills$/i) || ($message =~ /^\/?player_kills$/i)) { + if ($g_servers{$s_addr}->{use_browser} > 0) { + my $p_playerid = $player->{playerid}; + my $url = $g_servers{$s_addr}->{ingame_url}; + my $game = $g_servers{$s_addr}->{game}; + my $fullurl = $g_servers{$s_addr}->quoteparam(sprintf("%s/ingame.php?game=%s&mode=kills&player=%s", $url, $game, $p_playerid)); + my $browsecmd = $g_servers{$s_addr}->{browse_command}; + if ($g_servers{$s_addr}->{"mod"} eq "BEETLE") { + $cmd_kills = sprintf("%s %s say %s %s", $g_servers{$s_addr}->{exec_command}, $p_userid, $browsecmd, $fullurl); + } else { + $cmd_kills = sprintf("%s %s %s", $browsecmd, $p_userid, $fullurl); + } + $g_servers{$s_addr}->dorcon($cmd_kills); + } + } + ### End of Player-Kills Addon + + ### Begin Player-Map Performance Addon + elsif (($message =~ /^\/?maps$/i) || ($message =~ /^\/?map_stats$/i) || ($message =~ /^\/?map$/i)) { + if ($g_servers{$s_addr}->{use_browser} > 0) { + my $p_playerid = $player->{playerid}; + my $url = $g_servers{$s_addr}->{ingame_url}; + my $game = $g_servers{$s_addr}->{game}; + my $fullurl = $g_servers{$s_addr}->quoteparam(sprintf("%s/ingame.php?game=%s&mode=maps&player=%s", $url, $game, $p_playerid)); + my $browsecmd = $g_servers{$s_addr}->{browse_command}; + if ($g_servers{$s_addr}->{"mod"} eq "BEETLE") { + $cmd_maps = sprintf("%s %s say %s %s", $g_servers{$s_addr}->{exec_command}, $p_userid, $browsecmd, $fullurl); + } else { + $cmd_maps = sprintf("%s %s %s", $browsecmd, $p_userid, $fullurl); + } + $g_servers{$s_addr}->dorcon($cmd_maps); + } + } + ### End of Player-Map Performance Addon + + ### Begin Help Addon + elsif (($message =~ /^\/?help$/i) || ($message =~ /^\/?cmd$/i) || ($message =~ /^\/?cmds$/i) || ($message =~ /^\/?commands$/i)) { + if ($g_servers{$s_addr}->{use_browser} > 0) { + my $url = $g_servers{$s_addr}->{ingame_url}; + my $game = $g_servers{$s_addr}->{game}; + my $fullurl = $g_servers{$s_addr}->quoteparam(sprintf("%s/ingame.php?game=%s&mode=help", $url, $game)); + my $browsecmd = $g_servers{$s_addr}->{browse_command}; + + if ($g_servers{$s_addr}->{"mod"} eq "BEETLE") { + $cmd_help = sprintf("%s %s say %s %s", $g_servers{$s_addr}->{exec_command}, $p_userid, $browsecmd, $fullurl); + } else { + $cmd_help = sprintf("%s %s %s", $browsecmd, $p_userid, $fullurl); + } + $g_servers{$s_addr}->dorcon($cmd_help); + } + } + ### End of Help Addon + + } # end if player + + + return $playerstr . " $msg_type \"$message\""; +} + + +# +# 019. Map +# + +sub doEvent_ChangeMap +{ + my ($type, $newmap) = @_; + + $g_servers{$s_addr}->set("map", $newmap); + $g_servers{$s_addr}->clear_winner(); + + if ($type eq "loading") + { + while (my($pl, $player) = each(%g_players) ) + { + if ($player->{is_bot} == 1) + { + $player->updateDB(); + removePlayer($s_addr, $player->{userid}, $player->{uniqueid}, 1); + } + else + { + endKillStreak($player); + } + } + $g_servers{$s_addr}->setHlxCvars(); + return "Loading map \"$newmap\""; + } + elsif ($type eq "started") + { + $g_servers{$s_addr}->increment("map_changes"); + $g_servers{$s_addr}->set("map_started", time()); + $g_servers{$s_addr}->set("map_rounds", 0); + $g_servers{$s_addr}->set("map_ct_wins", 0); + $g_servers{$s_addr}->set("map_ts_wins", 0); + $g_servers{$s_addr}->set("map_ct_shots", 0); + $g_servers{$s_addr}->set("map_ct_hits", 0); + $g_servers{$s_addr}->set("map_ts_shots", 0); + $g_servers{$s_addr}->set("map_ts_hits", 0); + $g_servers{$s_addr}->updateDB(); + while (my($pl, $player) = each(%g_players) ) + { + $player->set("team", ""); + $player->set("map_kills", "0"); + $player->set("map_deaths", "0"); + $player->set("map_headshots", "0"); + $player->set("map_shots", "0"); + $player->set("map_hits", "0"); + $player->{trackable} = 0; + } + $g_servers{$s_addr}->updatePlayerCount(); + $g_servers{$s_addr}->{map} = $newmap; + &printNotice("Current map for server \"$s_addr\" is now \"" . $newmap . "\""); + + return "Started map \"$newmap\""; + } + else + { + return "Map \"$newmap\": $type"; + } +} + + +# +# 020. Rcon +# + +sub doEvent_Rcon +{ + my ($type, $command, $password, $ipAddr) = @_; + + if ($g_rcon_record) + { + &recordEvent( + "Rcon", 0, + $type, + $ipAddr, + $password, + $command + ); + } + else + { + $desc = "(IGNORED) "; + } + + return $desc . "$type Rcon from \"$ipAddr\": \"$command\""; +} + + +# +# 500. Admin +# + +sub doEvent_Admin +{ + my ($type, $message, $playerName) = @_; + + &recordEvent( + "Admin", 0, + $type, + $message, + $playerName + ); + + return "\"$type\" (\"$playerName\") \"$message\""; +} + +# +# 501. Statsme (weapon) +# + +sub doEvent_Statsme +{ + my ($playerId, $playerUniqueId, $weapon, $shots, $hits, $headshots, $damage, $kills, $deaths) = @_; + my $desc; + my $player = lookupPlayer($s_addr, $playerId, $playerUniqueId); + my $playerstr = &getPlayerInfoString($player, $playerId); + + if ($g_servers{$s_addr}->{num_trackable_players} < $g_servers{$s_addr}->{minplayers}) + { + $desc = "(IGNORED) NOTMINPLAYERS: "; + } + elsif ($player) + { + if (($g_servers{$s_addr}->{ignore_bots} == 1) && ($player->{is_bot} == 1)) + { + $desc = "(IGNORED) BOT: "; + } else { + + if ($weapon eq "bow") { + $weapon = "arrow"; + } + + if ($shots>0) + { + $player->increment("shots", $shots); + $player->increment("map_shots", $shots); + $player->increment("session_shots", $shots); + $g_servers{$s_addr}->increment("total_shots", $shots); + } + if ($hits>0) + { + $player->increment("hits", $hits); + $player->increment("map_hits", $hits); + $player->increment("session_hits", $hits); + $g_servers{$s_addr}->increment("total_hits", $hits); + } + $player->updateDB(); + + my $player_team = $player->{team}; + if ($player_team eq "CT") + { + if ($shots>0) + { + $g_servers{$s_addr}->increment("ct_shots", $shots); + $g_servers{$s_addr}->increment("map_ct_shots", $shots); + } + if ($hits>0) + { + $g_servers{$s_addr}->increment("ct_hits", $hits); + $g_servers{$s_addr}->increment("map_ct_hits", $hits); + } + } elsif ($player_team eq "TERRORIST") { + if ($shots>0) + { + $g_servers{$s_addr}->increment("ts_shots", $shots); + $g_servers{$s_addr}->increment("map_ts_shots", $shots); + } + if ($hits>0) + { + $g_servers{$s_addr}->increment("ts_hits", $hits); + $g_servers{$s_addr}->increment("map_ts_hits", $hits); + } + } + $g_servers{$s_addr}->updateDB(); + + &recordEvent("Statsme", 0, + $player->{playerid}, + $weapon, + $shots, + $hits, + $headshots, + $damage, + $kills, + $deaths + ); + } + } else + { + $desc = "(IGNORED) NOPLAYERINFO: "; + } + return $desc . $playerstr . " STATSME weaponstats (weapon \"$weapon\") (shots \"$shots\") (hits \"$hits\") (headshots \"$headshots\") (damage \"$damage\") (kills \"$kills\") (deaths \"$deaths\")"; +} + +# +# 502. Statsme (weapon2) +# + +sub doEvent_Statsme2 +{ + my ($playerId, $playerUniqueId, $weapon, $head, $chest, $stomach, $leftarm, $rightarm, $leftleg, $rightleg) = @_; + + my $desc; + my $player = lookupPlayer($s_addr, $playerId, $playerUniqueId); + my $playerstr = &getPlayerInfoString($player, $playerId); + + if ($g_servers{$s_addr}->{num_trackable_players} < $g_servers{$s_addr}->{minplayers}) + { + $desc = "(IGNORED) NOTMINPLAYERS: "; + } + elsif ($player) + { + if (($g_servers{$s_addr}->{ignore_bots} == 1) && ($player->{is_bot} == 1)) + { + $desc = "(IGNORED) BOT: "; + } else { + if ($g_servers{$s_addr}->{play_game} == FOF()) { + $weapon =~ s/2$//; + if ($weapon eq "bow") { + $weapon = "arrow"; + } + } + &recordEvent("Statsme2", 0, + $player->{playerid}, + $weapon, + $head, + $chest, + $stomach, + $leftarm, + $rightarm, + $leftleg, + $rightleg + ); + $player->updateDB(); + } + } + else + { + $desc = "(IGNORED) NOPLAYERINFO: "; + } + return $desc . $playerstr . " STATSME weaponstats2 (weapon \"$weapon\") (head \"$head\") (chest \"$chest\") (stomach \"$stomach\") (leftarm \"$leftarm\") (rightarm \"$rightarm\") (leftleg \"$leftleg\") (rightleg \"$rightleg\")"; +} + +# +# 503. Statsme (latency) +# + +sub doEvent_Statsme_Latency +{ + my ($playerId, $playerUniqueId, $ping) = @_; + + my $desc; + my $player = lookupPlayer($s_addr, $playerId, $playerUniqueId); + my $playerstr = &getPlayerInfoString($player, $playerId); + + if (($player) && ($player->{is_bot} == 0)) + { + &recordEvent( + "StatsmeLatency", 0, + $player->{playerid}, + $ping + ); + } + else + { + $desc = "(IGNORED) NOPLAYERINFO: "; + } + + return $desc . $playerstr . " STATSME average latency \"$ping\""; +} + +# +# 504. Statsme (time) +# + +sub doEvent_Statsme_Time +{ + my ($playerId, $playerUniqueId, $time) = @_; + + my $desc; + my $player = lookupPlayer($s_addr, $playerId, $playerUniqueId); + my $playerstr = &getPlayerInfoString($player, $playerId); + + if (($player) && ($player->{is_bot} == 0)) + { + &recordEvent( + "StatsmeTime", 0, + $player->{playerid}, + $time + ); + } + else + { + $desc = "(IGNORED) NOPLAYERINFO: "; + } + + return $desc . $playerstr . " STATSME connection time \"$time\""; +} + +# +# 505. Kill Location +# + +sub doEvent_Kill_Loc +{ + my (%properties) = @_; + + if (defined($properties{attacker_position})) { + my $coords = $properties{attacker_position}; + ($g_servers{$s_addr}->{nextkillx}, $g_servers{$s_addr}->{nextkilly}, $g_servers{$s_addr}->{nextkillz}) = split(/ /,$coords); + } + + if (defined($properties{victim_position})) { + my $coords = $properties{victim_position}; + ($g_servers{$s_addr}->{nextkillvicx}, $g_servers{$s_addr}->{nextkillvicy}, $g_servers{$s_addr}->{nextkillvicz}) = split(/ /,$coords); + } + + return sprintf("KILL LOCATION x=>%s, y=>%s, z=>%s stored for next kill", $g_servers{$s_addr}->{nextkillx}, $g_servers{$s_addr}->{nextkilly}, $g_servers{$s_addr}->{nextkillz}); +} + +1; diff --git a/scripts/HLstats_Game.pm b/scripts/HLstats_Game.pm new file mode 100644 index 0000000..32a2d96 --- /dev/null +++ b/scripts/HLstats_Game.pm @@ -0,0 +1,104 @@ +package HLstats_Game; +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + +# +# Constructor +# + +sub new +{ + my $class_name = shift; + my $game = shift; + + my $self = {}; + bless($self, $class_name); + + # Initialise Properties + $self->{game} = $game; + $self->{weapons} = (); + $self->{actions} = (); + + # Set Property Values + + die("HLstats_Game->new(): must specify game's game code\n") if ($game eq ""); + #&::printEvent("DEBUG","game is $game"); + my $weaponlist = &::doQuery("SELECT code, name, modifier FROM hlstats_Weapons WHERE game='".&::quoteSQL($game)."'"); + while ( my($code,$name,$modifier) = $weaponlist->fetchrow_array) { + $self->{weapons}{$code}{name} = $name; + $self->{weapons}{$code}{modifier} = $modifier; + #&::printEvent("DEBUG","Weapon: name is \"$name\"; modifier is $modifier"); + } + + my $actionlist = &::doQuery("SELECT id, code, reward_player, reward_team, team, description, for_PlayerActions, for_PlayerPlayerActions, for_TeamActions, for_WorldActions FROM hlstats_Actions WHERE game='".&::quoteSQL($game)."'"); + while ( my($id, $code, $reward_player,$reward_team,$team, $descr, $paction, $ppaction, $taction, $waction) = $actionlist->fetchrow_array) { + $self->{actions}{$code}{id} = $id; + $self->{actions}{$code}{descr} = $descr; + $self->{actions}{$code}{reward_player} = $reward_player; + $self->{actions}{$code}{reward_team} = $reward_team; + $self->{actions}{$code}{team} = $team; + $self->{actions}{$code}{paction} = $paction; + $self->{actions}{$code}{ppaction} = $ppaction; + $self->{actions}{$code}{taction} = $taction; + $self->{actions}{$code}{waction} = $waction; + } + $actionlist->finish; + + &::printNotice("Created new game object " . $game); + return $self; +} + +sub getTotalPlayers +{ + my ($self) = @_; + + my $query = " + SELECT + COUNT(*) + FROM + hlstats_Players + WHERE + game=? + AND hideranking = 0 + AND kills >= 1 + "; + my $resultTotalPlayers = &::execCached("get_game_total_players", $query, &::quoteSQL($self->{game})); + my ($totalplayers) = $resultTotalPlayers->fetchrow_array; + $resultTotalPlayers->finish; + + return $totalplayers; +} + +1; diff --git a/scripts/HLstats_GameConstants.plib b/scripts/HLstats_GameConstants.plib new file mode 100644 index 0000000..f681ee0 --- /dev/null +++ b/scripts/HLstats_GameConstants.plib @@ -0,0 +1,56 @@ +use constant { + UNKNOWN => -1, + CSS => 0, + HL2MP => 1, + TF => 2, + DODS => 3, + INSMOD => 4, + FF => 5, + HIDDEN => 6, + ZPS => 7, + AOC => 8, + CSTRIKE => 9, + TFC => 10, + DOD => 11, + NS => 12, + L4D => 13, + FOF => 14, + GES => 15, + BG2 => 16, + SGTLS => 17, + DYSTOPIA => 18, + NTS => 19, + PVKII => 20, + CSP => 21, + VALVE => 22, + NUCLEARDAWN => 23, + DDD => 24, +}; + +%gamecode_to_game = ( + 'css' => CSS(), + 'hl2mp' => HL2MP(), + 'tf' => TF(), + 'dods' => DODS(), + 'insmod' => INSMOD(), + 'ff' => FF(), + 'hidden' => HIDDEN(), + 'zps' => ZPS(), + 'aoc' => AOC(), + 'cstrike' => CSTRIKE(), + 'tfc' => TFC(), + 'dod' => DOD(), + 'ns' => NS(), + 'l4d' => L4D(), + 'fof' => FOF(), + 'ges' => GES(), + 'bg2' => BG2(), + 'sgtls' => SGTLS(), + 'dystopia' => DYSTOPIA(), + 'nts' => NTS(), + 'pvkii' => PVKII(), + 'csp' => CSP(), + 'valve' => VALVE(), + 'nucleardawn' => NUCLEARDAWN(), + 'dinodday' => DDD() +); \ No newline at end of file diff --git a/scripts/HLstats_Player.pm b/scripts/HLstats_Player.pm new file mode 100644 index 0000000..aca875e --- /dev/null +++ b/scripts/HLstats_Player.pm @@ -0,0 +1,1124 @@ +package HLstats_Player; +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + +# +# Constructor +# + +use Encode; + +do "$::opt_libdir/HLstats_GameConstants.plib"; + +sub new +{ + my $class_name = shift; + my %params = @_; + + my $self = {}; + bless($self, $class_name); + + # Initialise Properties + $self->{userid} = 0; + $self->{server} = ""; + $self->{server_id} = 1; + $self->{name} = ""; + $self->{uniqueid} = ""; + $self->{plain_uniqueid} = ""; + $self->{address} = ""; + $self->{cli_port} = ""; + $self->{ping} = 0; + $self->{connect_time} = time(); + $self->{last_update} = 0; + $self->{last_update_skill} = 0; + $self->{day_skill_change} = 0; + + $self->{city} = ""; + $self->{state} = ""; + $self->{country} = ""; + $self->{flag} = ""; + $self->{lat} = undef; + $self->{lng} = undef; + + $self->{playerid} = 0; + $self->{clan} = 0; + $self->{kills} = 0; + $self->{total_kills} = 0; + $self->{deaths} = 0; + $self->{suicides} = 0; + $self->{skill} = 1000; + $self->{game} = ""; + $self->{team} = ""; + $self->{role} = ""; + $self->{timestamp} = 0; + $self->{headshots} = 0; + $self->{shots} = 0; + $self->{hits} = 0; + $self->{teamkills} = 0; + $self->{kill_streak} = 0; + $self->{death_streak} = 0; + + $self->{auto_command} = ""; + $self->{auto_type} = ""; + $self->{auto_time} = 0; + $self->{auto_time_count} = 0; + + $self->{session_skill} = 0; + $self->{session_kills} = 0; + $self->{session_deaths} = 0; + $self->{session_suicides} = 0; + $self->{session_headshots} = 0; + $self->{session_shots} = 0; + $self->{session_hits} = 0; + $self->{session_start_pos} = -1; + + $self->{map_kills} = 0; + $self->{map_deaths} = 0; + $self->{map_suicides} = 0; + $self->{map_headshots} = 0; + $self->{map_shots} = 0; + $self->{map_hits} = 0; + $self->{is_dead} = 0; + $self->{has_bomb} = 0; + + $self->{is_banned} = 0; + $self->{is_bot} = 0; + + $self->{display_events} = 1; + $self->{display_chat} = 1; + $self->{kills_per_life} = 0; + $self->{last_history_day} = ""; + $self->{last_death_weapon} = 0; + $self->{last_sg_build} = 0; + $self->{last_disp_build} = 0; + $self->{last_entrance_build} = 0; + $self->{last_exit_build} = 0; + $self->{last_team_change} = ""; + $self->{deaths_in_a_row} = 0; + $self->{trackable} = 0; + $self->{needsupdate} = 0; + + + # Set Property Values + + die("HLstats_Player->new(): must specify player's uniqueid\n") + unless (defined($params{uniqueid})); + + + while (my($key, $value) = each(%params)) + { + if ($key ne "name" && $key ne "uniqueid") + { + $self->set($key, $value); + } + } + + $self->updateTrackable(); + $self->{plain_uniqueid} = $params{plain_uniqueid}; + $self->setUniqueId($params{uniqueid}); + if ($::g_stdin == 0 && $self->{userid} > 0) { + $self->insertPlayerLivestats(); + } + $self->setName($params{name}); + $self->getAddress(); + $self->flushDB(); + + + + &::printNotice("Created new player object " . $self->getInfoString()); + return $self; +} + +sub playerCleanup +{ + my ($self) = @_; + $self->flushDB(); + $self->deleteLivestats(); +} + + +# +# Set property 'key' to 'value' +# + +sub set +{ + my ($self, $key, $value, $no_updatetime) = @_; + + if (defined($self->{$key})) + { + if ($no_updatetime == 0) { + $self->{timestamp} = $::ev_daemontime; + } + + if ($self->{$key} eq $value) + { + if ($::g_debug > 2) + { + &::printNotice("Hlstats_Player->set ignored: Value of \"$key\" is already \"$value\""); + } + return 0; + } + + if ($key eq "uniqueid") + { + return $self->setUniqueId($value); + } + elsif ($key eq "name") + { + return $self->setName($value); + } + elsif ($key eq "skill" && $self->{userid} < 1) + { + return $self->{skill}; + } + else + { + $self->{$key} = $value; + return 1; + } + } + else + { + warn("HLstats_Player->set: \"$key\" is not a valid property name\n"); + return 0; + } +} + + +# +# Increment (or decrement) the value of 'key' by 'amount' (or 1 by default) +# + +sub increment +{ + my ($self, $key, $amount, $no_updatetime) = @_; + + if ($key eq "skill" && $self->{userid} < 1) { + return $self->{skill}; + } + + $amount = 1 if (!defined($amount)); + + if ($amount != 0) { + my $value = $self->{$key}; + $self->set($key, $value + $amount, $no_updatetime); + } +} + + +sub check_history +{ + my ($self) = @_; + + #my ($sec,$min,$hour,$mday,$mon,$year) = localtime(time()); + my ($sec,$min,$hour,$mday,$mon,$year) = localtime($::ev_unixtime); + my $date = sprintf("%04d-%02d-%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec); + my $srv_addr = $self->{server}; + my $is_bot = 0; + my $playerId = $self->{playerid}; + + if ($self->{is_bot} || $self->{userid} < 0) { + $is_bot = 1; + } + if (($playerId > 0) && ($::g_stdin == 0 || $::g_timestamp > 0)) + { + if (($is_bot == 0) || (($is_bot == 1) && ($::g_servers{$srv_addr}->{ignore_bots} == 0))) { + $self->{last_history_day} = sprintf("%02d", $mday); + my $query = " + SELECT + skill_change + FROM + hlstats_Players_History + WHERE + playerId=" . $playerId . " + AND eventTime='".$date."' + AND game='".&::quoteSQL($::g_servers{$srv_addr}->{game})."' + "; + my $result = &::doQuery($query); + + if ($result->rows < 1) + { + my $query = " + INSERT INTO + hlstats_Players_History + ( + playerId, + eventTime, + game + ) + VALUES + ( + $playerId, + '$date', + '" . &::quoteSQL($::g_servers{$srv_addr}->{game}) . "' + ) + "; + &::execNonQuery($query); + $self->{day_skill_change} = 0; + } else { + ($self->{day_skill_change}) = $result->fetchrow_array; + } + $result->finish; + } + } +} + + +# +# Set player's uniqueid +# + +sub setUniqueId +{ + my ($self, $uniqueid) = @_; + my $tempPlayerId = &::getPlayerId($uniqueid); + + if ($tempPlayerId > 0) + { + $self->{playerid} = $tempPlayerId; + # An existing player. Get their skill rating. + my $query = " + SELECT + skill, kills, displayEvents, flag + FROM + hlstats_Players + WHERE + playerId=$tempPlayerId + "; + my $result = &::doQuery($query); + if ($result->rows > 0) { + ($self->{skill}, $self->{total_kills}, $self->{display_events},$self->{flag}) = $result->fetchrow_array; + } else { + # Have record in hlstats_PlayerUniqueIds but not in hlstats_Players + $self->insertPlayer($tempPlayerId); + } + $self->{session_start_pos} = $self->getRank(); + $result->finish; + } + else + { + # This is a new player. Create a new record for them in the Players + # table. + $self->insertPlayer(); + + $query = " + INSERT INTO + hlstats_PlayerUniqueIds + ( + playerId, + uniqueId, + game + ) + VALUES + ( + ".$self->{playerid}.", + '" . &::quoteSQL($uniqueid) . "', + '" . &::quoteSQL($::g_servers{$self->{server}}->{game}) . "' + ) + "; + &::execNonQuery($query); + } + + $self->{uniqueid} = $uniqueid; + $self->check_history(); + + return 1; +} + + +# +# Inserts new player +# + +sub insertPlayer +{ + my ($self, $playerid) = @_; + + my $hideval = 0; + my $playeridins = ""; + my $playeridval = ""; + my $srv_addr = $self->{server}; + + if ($::g_servers{$srv_addr}->{play_game} == L4D() && $self->{userid} < 0) { + $hideval = 1; + } + if ($playerid) { + my $query = " + INSERT INTO + hlstats_Players + ( + lastName, + clan, + game, + displayEvents, + createdate, + hideranking, + playerId + ) + VALUES + ( + ?, + ?, + ?, + ?, + UNIX_TIMESTAMP(), + ?, + ? + ) + "; + my @vals = ($self->{name}, $self->{clan}, $::g_servers{$srv_addr}->{game}, $self->{display_events}, $hideval, $playerid); + &::execCached("player_insert_playerid", $query, @vals); + return $playerid; + } + + my $query = " + INSERT INTO + hlstats_Players + ( + lastName, + clan, + game, + displayEvents, + createdate, + hideranking + ) + VALUES + ( + ?, + ?, + ?, + ?, + UNIX_TIMESTAMP(), + ? + ) + "; + my @vals = ($self->{name}, $self->{clan}, $::g_servers{$srv_addr}->{game}, $self->{display_events}, $hideval); + &::execCached("player_insert", $query, @vals); + + $self->{playerid} = $::db_conn->{'mysql_insertid'}; +} + +# +# Insert initial live stats +# +sub insertPlayerLivestats +{ + my ($self) = @_; + my $query = " + REPLACE INTO + hlstats_Livestats + ( + player_id, + server_id, + cli_address, + steam_id, + name, + team, + ping, + connected, + skill, + cli_flag + ) + VALUES + ( + ?,?,?,?,?,?,?,?,?,? + ) + "; + my @vals = ($self->{playerid}, $self->{server_id}, $self->{address}, $self->{plain_uniqueid}, + $self->{name}, $self->{team}, $self->{ping}, $self->{connect_time}, $self->{skill}, $self->{flag}); + &::execCached("player_livestats_insert", $query, @vals); +} + + +# +# Set player's name +# + +sub setName +{ + my ($self, $name) = @_; + + my $oldname = $self->{name}; + + if ($oldname eq $name) + { + return 2; + } + + if ($oldname) + { + $self->updateDB(); + } + + $self->{name} = $name; + + my $is_bot = $self->{is_bot}; + my $server_address = $self->{server}; + if (($is_bot == 1) && ($::g_servers{$server_address}->{ignore_bots} == 1)) { + $self->{clan} = ""; + } else { + $self->{clan} = &::getClanId($name); + } + + my $playerid = $self->{playerid}; + + if ($playerid) + { + my $query = " + SELECT + playerId + FROM + hlstats_PlayerNames + WHERE + playerId = $playerid + AND name ='" . &::quoteSQL($self->{name}) . "' + "; + my $result = &::doQuery($query); + + if ($result->rows < 1) + { + my $query = " + REPLACE INTO + hlstats_PlayerNames + ( + playerId, + name, + lastuse, + numuses + ) + VALUES + ( + $playerid, + '" . &::quoteSQL($self->{name}) . "', + FROM_UNIXTIME(" . $::ev_unixtime . "), + 1 + ) + "; + &::execNonQuery($query); + } + else + { + my $query = " + UPDATE + hlstats_PlayerNames + SET + lastuse=FROM_UNIXTIME(" . $::ev_unixtime . "), + numuses=numuses+1 + WHERE + playerId = $playerid + AND name='" . &::quoteSQL($self->{name}) . "' + "; + &::execNonQuery($query); + } + + $result->finish; + } + else + { + &::error("HLstats_Player->setName(): No playerid"); + } +} + + + +# +# Update player information in database +# + +sub flushDB +{ + my ($self, $leaveLastUse, $callref) = @_; + + my $playerid = $self->{playerid}; + my $srv_addr = $self->{server}; + my $serverid = $self->{server_id}; + my $name = $self->{name}; + my $clan = $self->{clan}; + my $kills = $self->{kills}; + my $deaths = $self->{deaths}; + my $suicides = $self->{suicides}; + my $skill = $self->{skill}; + if ($skill < 0) {$skill = 0;} + my $headshots = $self->{headshots}; + my $shots = $self->{shots}; + my $hits = $self->{hits}; + my $teamkills = $self->{teamkills}; + + my $team = $self->{team}; + my $map_kills = $self->{map_kills}; + my $map_deaths = $self->{map_deaths}; + my $map_suicides = $self->{map_suicides}; + my $map_headshots = $self->{map_headshots}; + my $map_shots = $self->{map_shots}; + my $map_hits = $self->{map_hits}; + my $steamid = $self->{plain_uniqueid}; + + my $is_dead = $self->{is_dead}; + my $has_bomb = $self->{has_bomb}; + my $ping = $self->{ping}; + my $connected = $self->{connect_time}; + my $skill_change = $self->{session_skill}; + + my $death_streak = $self->{death_streak}; + my $kill_streak = $self->{kill_streak}; + + my $add_connect_time = 0; + if (($::g_stdin == 0) && ($self->{last_update} > 0)) { + $add_connect_time = time() - $self->{last_update}; + } elsif (($::g_stdin == 1) && ($self->{last_update} > 0)) { + $add_connect_time = $::ev_unixtime - $self->{last_update}; + } + if (($::g_stdin == 1) && ($add_connect_time > 600)) { + $self->{last_update} = $::ev_unixtime; + $add_connect_time = 0; + } + + my $address = $self->{address}; + + unless ($playerid) + { + warn ("Player->Update() with no playerid set!\n"); + return 0; + } + + if (($::g_stdin == 0) && ($self->{session_start_pos} == 0)) { + $self->{session_start_pos} = $self->getRank(); + } + + # TAG - review this, should probably be localtime($ev_unixtime); + # and why no Players_History if stdin? + #my ($sec,$min,$hour,$mday,$mon,$year) = localtime(time()); + my ($sec,$min,$hour,$mday,$mon,$year) = localtime($::ev_unixtime); + my $date = sprintf("%04d-%02d-%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec); + + if ($::g_stdin == 0 || $::g_timestamp > 0) { + my $last_history_day = $self->{last_history_day}; + if ($last_history_day ne sprintf("%02d", $mday)) { + my $query = " + INSERT IGNORE INTO + hlstats_Players_History + ( + playerId, + eventTime, + game + ) VALUES ( + ?, + ?, + ? + ) + "; + my @vals = ($playerid, $date, $::g_servers{$srv_addr}->{game}); + &::execCached("player_flushdb_history_1", $query, @vals); + $self->{day_skill_change} = 0; + $self->{last_history_day} = sprintf("%02d", $mday); + } + } + + my $add_history_skill = 0; + if ($self->{last_update_skill} > 0) { + $add_history_skill = $skill - $self->{last_update_skill}; + } + $self->{day_skill_change} += $add_history_skill; + my $last_skill_change = $self->{day_skill_change}; + + + my $is_bot = $self->{is_bot}; + my $server_address = $self->{server}; + if (($is_bot == 1) && ($::g_servers{$server_address}->{ignore_bots} == 1)) { + # Update player details + my $query = " + UPDATE + hlstats_Players + SET + connection_time = connection_time + ?, + lastName=?, + clan=0, + kills=kills + ?, + deaths=deaths + ?, + suicides=suicides + ?, + skill=0, + headshots=headshots + ?, + shots=shots + ?, + hits=hits + ?, + teamkills=teamkills + ?, + last_event=?, + hideranking=1 + WHERE + playerId=? + "; + my @vals = ($add_connect_time, $name, $kills, $deaths, $suicides, $headshots, + $shots, $hits, $teamkills, $::ev_unixtime, $playerid); + &::execCached("player_flushdb_player_1", $query, @vals); + } else { + # Update player details + my $query = " + UPDATE + hlstats_Players + SET + connection_time = connection_time + ?, + lastName=?, + clan=?, + kills=kills + ?, + deaths=deaths + ?, + suicides=suicides + ?, + skill=?, + headshots=headshots + ?, + shots=shots + ?, + hits=hits + ?, + teamkills=teamkills + ?, + last_event=?, + last_skill_change=?, + death_streak=IF(?>death_streak,?,death_streak), + kill_streak=IF(?>kill_streak,?,kill_streak), + hideranking=IF(hideranking=3,0,hideranking), + activity = 100 + WHERE + playerId=? + "; + my @vals = ($add_connect_time, $name, $clan, $kills, $deaths, $suicides, $skill, + $headshots, $shots, $hits, $teamkills, $::ev_unixtime, $last_skill_change, $death_streak, + $death_streak, $kill_streak, $kill_streak, $playerid); + &::execCached("player_flushdb_player_2", $query, @vals); + + if ($::g_stdin == 0 || $::g_timestamp > 0) { + # Update player details + my $query = " + UPDATE + hlstats_Players_History + SET + connection_time = connection_time + ?, + kills=kills + ?, + deaths=deaths + ?, + suicides=suicides + ?, + skill=?, + headshots=headshots + ?, + shots=shots + ?, + hits=hits + ?, + teamkills=teamkills + ?, + death_streak=IF(?>death_streak,?,death_streak), + kill_streak=IF(?>kill_streak,?,kill_streak), + skill_change=skill_change + ? + WHERE + playerId=? + AND eventTime=? + AND game=? + "; + my @vals = ($add_connect_time, $kills, $deaths, $suicides, $skill, $headshots, + $shots, $hits, $teamkills, $death_streak, $death_streak, $kill_streak, + $kill_streak, $add_history_skill, $playerid, $date, $::g_servers{$srv_addr}->{game}); + &::execCached("player_flushdb_history_2", $query, @vals); + } + } + + if ($name) + { + # Update alias details + $query = " + UPDATE + hlstats_PlayerNames + SET + connection_time = connection_time + ?, + kills=kills + ?, + deaths=deaths + ?, + suicides=suicides + ?, + headshots=headshots + ?, + shots=shots + ?, + hits=hits + ?" + ; + my @vals = ($add_connect_time, $kills, $deaths, $suicides, $headshots, $shots, $hits); + + unless ($leaveLastUse) + { + # except on ChangeName we update the last use on a player's old name + + $query .= ", + lastuse=FROM_UNIXTIME(?)" + ; + push(@vals, $::ev_unixtime); + } + + $query .= " + WHERE + playerId=? + AND name=? + "; + push(@vals, $playerid); + push(@vals, $self->{name}); + + &::execCached("player_flushdb_playernames", $query, @vals); + } + + # reset player stat properties + $self->set("kills", 0, 1); + $self->set("deaths", 0, 1); + $self->set("suicides", 0, 1); + $self->set("headshots", 0, 1); + $self->set("shots", 0, 1); + $self->set("hits", 0, 1); + $self->set("teamkills", 0, 1); + + if (($is_bot == 1) && ($::g_servers{$server_address}->{ignore_bots} == 1)) { + $skill = 0; + $skill_change = 0; + } + + if ($::g_stdin == 0 && $self->{userid} > 0) { + # Update live stats + my $query = " + UPDATE + hlstats_Livestats + SET + cli_address=?, + steam_id=?, + name=?, + team=?, + kills=?, + deaths=?, + suicides=?, + headshots=?, + shots=?, + hits=?, + is_dead=?, + has_bomb=?, + ping=?, + connected=?, + skill_change=?, + skill=? + WHERE + player_id=? + "; + my @vals = ($address, $steamid, $name, + $team, $map_kills, $map_deaths, $map_suicides, $map_headshots, $map_shots, + $map_hits, $is_dead, $has_bomb, $ping, $connected, $skill_change, $skill, $playerid); + &::execCached("player_flushdb_livestats", $query, @vals); + } + + if ($::g_stdin == 0) { + $self->{last_update} = time(); + } elsif ($::g_stdin == 1) { + $self->{last_update} = $::ev_unixtime; + } + + $self->{last_update_skill} = $skill; + + $self->{needsupdate} = 0; + + &::printNotice("Updated player object " . $self->getInfoString()); + + return 1; +} + + +# +# Update player timestamp (time of last event for player - used to detect idle +# players) +# + +sub updateTimestamp +{ + my ($self, $timestamp) = @_; + $timestamp = $::ev_unixtime + unless ($timestamp); + $self->{timestamp} = $::ev_daemontime; + return $timestamp; +} + +sub updateDB +{ + my ($self) = @_; + $self->{needsupdate} = 1; + +} + +sub deleteLivestats +{ + my ($self) = @_; + + # delete live stats + my $query = "DELETE FROM hlstats_Livestats WHERE player_id=".$self->{playerid}; + &::execNonQuery($query); +} + + +# +# Returns a string of information about the player. +# + +sub getInfoString +{ + my ($self) = @_; + return sprintf("\"%s\" \", $self->{name}, $self->{playerid}, $self->{userid}, $self->{uniqueid}, $self->{team}); +} + + +sub getAddress +{ + my ($self) = @_; + my $haveAddress = 0; + + if ($self->{address} ne "") + { + $haveAddress = 1; + } + elsif ($::g_stdin == 0 && $self->{is_bot} == 0 && $self->{userid} > 0) + { + $s_addr = $self->{server}; + + &::printNotice("rcon_getaddress"); + my $result = $::g_servers{$s_addr}->rcon_getaddress($self->{uniqueid}); + if ($result->{Address} ne "") { + $haveAddress = 1; + $self->{address} = $result->{Address}; + $self->{cli_port} = $result->{ClientPort}; + $self->{ping} = $result->{Ping}; + + &::printEvent("RCON", "Got Address $self->{address} for Player $self->{name}", 1); + &::printNotice("rcon_getaddress successfully"); + } + } + + if ($haveAddress > 0) + { + # Update player IP address in database + my $query = " + UPDATE + hlstats_Players + SET + lastAddress=? + WHERE + playerId=? + "; + my @vals = ($self->{address}, $self->{playerid}); + &::execCached("player_update_lastaddress", $query, @vals); + &::printEvent("DEBUG", "Updated IP for ".$self->{playerid}." to ".$self->{address}); + + $self->geoLookup(); + } + return 1; +} + +sub geoLookup +{ + my ($self) = @_; + my $ip_address = $self->{address}; + my $found = 0; + + if ($ip_address ne "") + { + my $country_code = undef; + my $country_code3 = undef; + my $country_name = undef; + my $region = undef; + my $city = undef; + my $postal_code = undef; + my $lat = undef; + my $lng = undef; + my $metro_code = undef; + my $area_code = undef; + + if ($::g_geoip_binary > 0) { + if(!defined($::g_gi)) { + return; + } + ($country_code, $country_code3, $country_name, $region, $city, $postal_code, $lat, $lng, $metro_code, $area_code) = $::g_gi->get_city_record($ip_address); + if ($lng) { + $found++; + $self->{city} = ((defined($city))?encode("utf8",$city):""); + $self->{state} = ((defined($region))?encode("utf8",$region):""); + $self->{country} = ((defined($country_name))?encode("utf8",$country_name):""); + $self->{flag} = ((defined($country_code))?encode("utf8",$country_code):""); + $self->{lat} = (($lat eq "")?undef:$lat); + $self->{lng} = (($lng eq "")?undef:$lng); + } + } else { + my @ipp = split (/\./,$ip_address); + my $ip_number = $ipp[0]*16777216+$ipp[1]*65536+$ipp[2]*256+$ipp[3]; + my $query = " + SELECT locId FROM geoLiteCity_Blocks WHERE startIpNum<=".$ip_number." AND endIpNum>=".$ip_number." LIMIT 1;"; + my $result = &::doQuery($query); + if ($result->rows > 0) { + my $locid = $result->fetchrow_array; + $result->finish; + my $query = "SELECT city, region AS state, name AS country, country AS flag, latitude AS lat, longitude AS lng FROM geoLiteCity_Location a inner join hlstats_Countries b ON a.country=b.flag WHERE locId=".$locid." LIMIT 1;"; + my $result = &::doQuery($query); + if ($result->rows > 0) { + $found++; + ($city, $state, $country, $flag, $lat, $lng) = $result->fetchrow_array; + $self->{city} = ((defined($city))?$city:""); + $self->{state} = ((defined($state))?$state:""); + $self->{country} = ((defined($country))?$country:""); + $self->{flag} = ((defined($flag))?$flag:""); + $self->{lat} = (($lat eq "")?undef:$lat); + $self->{lng} = (($lng eq "")?undef:$lng); + } + $result->finish; + } + } + if ($found > 0) { + &::execNonQuery(" + UPDATE + hlstats_Players + SET + city='".&::quoteSQL($self->{city})."', + `state`='".&::quoteSQL($self->{state})."', + country='".&::quoteSQL($self->{country})."', + flag='".&::quoteSQL($self->{flag})."', + lat=".((defined($self->{lat}))?$self->{lat}:"NULL").", + lng=".((defined($self->{lng}))?$self->{lng}:"NULL")." + WHERE + playerId = ".$self->{playerid} + ); + &::execNonQuery(" + UPDATE + hlstats_Livestats + SET + cli_city='".&::quoteSQL($self->{city})."', + cli_country='".&::quoteSQL($self->{country})."', + cli_flag='".&::quoteSQL($self->{flag})."', + cli_state='".&::quoteSQL($self->{state})."', + cli_lat=".((defined($self->{lat}))?$self->{lat}:"NULL").", + cli_lng=".((defined($self->{lng}))?$self->{lng}:"NULL")." + WHERE + player_id =".$self->{playerid} + ); + } + } +} + +sub getRank +{ + my ($self) = @_; + + my $srv_addr = $self->{server}; + $query = " + SELECT + kills, + deaths, + hideranking + FROM + hlstats_Players + WHERE + playerId=? + "; + my $result = &::execCached("get_player_rank_stats", $query, $self->{playerid}); + + my ($kills, $deaths, $hideranking) = $result->fetchrow_array; + $result->finish; + + return 0 if ($hideranking > 0); + + $deaths = 1 if ($deaths == 0); + my $kpd = $kills/$deaths; + + my $rank = 0; + + if ($::g_ranktype ne "kills") + { + if (!defined($self->{skill})) + { + &::printEvent("ERROR","Attempted to get rank for uninitialized player \"".$self->{name}."\""); + return 0; + } + + my $query = " + SELECT + COUNT(*) + FROM + hlstats_Players + WHERE + game=? + AND hideranking = 0 + AND kills >= 1 + AND ( + (skill > ?) OR ( + (skill = ?) AND ((kills/IF(deaths=0,1,deaths)) > ?) + ) + ) + "; + my @vals = ( + &::quoteSQL($self->{game}), + $self->{skill}, + $self->{skill}, + $kpd + ); + my $rankresult = &::execCached("get_player_skill_value", $query, @vals); + ($rank) = $rankresult->fetchrow_array; + $rankresult->finish; + $rank++; + } + else + { + my $query =" + SELECT + COUNT(*) + FROM + hlstats_Players + WHERE + game=? + AND hideranking = 0 + AND ( + (kills > ?) OR ( + (kills = ?) AND ((kills/IF(deaths=0,1,deaths)) > ?) + ) + ) + "; + my @vals = ( + &::quoteSQL($self->{game}), + $kills, + $kills, + $kpd + ); + my $rankresult = &::execCached("get_player_rank_value", $query, @vals); + ($rank) = $rankresult->fetchrow_array; + $rankresult->finish; + $rank++; + } + + return $rank; +} + +sub updateTrackable +{ + my ($self) = @_; + + if ((&::isTrackableTeam($self->{team}) == 0) || (($::g_servers{$self->{server}}->{ignore_bots} == 1) && (($self->{is_bot} == 1) || ($self->{userid} <= 0)))) { + $self->{trackable} = 0; + return; + } + $self->{trackable} = 1; +} + +1; diff --git a/scripts/HLstats_Server.pm b/scripts/HLstats_Server.pm new file mode 100644 index 0000000..cf9eda8 --- /dev/null +++ b/scripts/HLstats_Server.pm @@ -0,0 +1,1414 @@ +package HLstats_Server; +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + +use POSIX; +use IO::Socket; +use Socket; +use Encode; + +do "$::opt_libdir/HLstats_GameConstants.plib"; + +sub new +{ + my ($class_name, $serverId, $address, $port, $server_name, $rcon_pass, $game, $publicaddress, $gameengine, $realgame, $maxplayers) = @_; + + my ($self) = {}; + + bless($self, $class_name); + + $self->{id} = $serverId; + $self->{address} = $address; + $self->{port} = $port; + $self->{game} = $game; + $self->{rcon} = $rcon_pass; + $self->{srv_players} = (); + + # Game Engine + # HL1 - 1 + # HL2 (original) - 2 + # HL2ep2 ("OrangeBox") - 3 + $self->{game_engine} = $gameengine; + + $self->{rcon_obj} = undef; + $self->{name} = $server_name; + $self->{auto_ban} = 0; + $self->{contact} = ""; + $self->{hlstats_url} = ""; + $self->{publicaddress} = $publicaddress; + $self->{play_game} = -1; + + $self->{last_event} = 0; + $self->{last_check} = 0; + + $self->{lines} = 0; + $self->{map} = ""; + $self->{numplayers} = 0; + $self->{num_trackable_players} = 0; + $self->{minplayers} = 6; + $self->{maxplayers} = $maxplayers; + $self->{difficulty} = 0; + + $self->{players} = 0; + $self->{rounds} = 0; + $self->{kills} = 0; + $self->{suicides} = 0; + $self->{headshots} = 0; + $self->{ct_shots} = 0; + $self->{ct_hits} = 0; + $self->{ts_shots} = 0; + $self->{ts_hits} = 0; + $self->{bombs_planted} = 0; + $self->{bombs_defused} = 0; + $self->{ct_wins} = 0; + $self->{ts_wins} = 0; + $self->{map_started} = time(); + $self->{map_changes} = 0; + $self->{map_rounds} = 0; + $self->{map_ct_wins} = 0; + $self->{map_ts_wins} = 0; + $self->{map_ct_shots} = 0; + $self->{map_ct_hits} = 0; + $self->{map_ts_shots} = 0; + $self->{map_ts_hits} = 0; + + # team balancer + $self->{ba_enabled} = 0; + $self->{ba_ct_wins} = 0; + $self->{ba_ts_win} = 0; + $self->{ba_ct_frags} = 0; + $self->{ba_ts_frags} = 0; + $self->{ba_winner} = (); + $self->{ba_map_rounds} = 0; + $self->{ba_last_swap} = 0; + $self->{ba_player_switch} = 0; # player switched on his own + + # Messaging commands + $self->{show_stats} = 0; + $self->{broadcasting_events} = 0; + $self->{broadcasting_player_actions} = 0; + $self->{broadcasting_command} = ""; + $self->{broadcasting_command_announce} = "say"; + $self->{player_events} = 1; + $self->{player_command} = "say"; + $self->{player_command_osd} = ""; + $self->{player_command_hint} = ""; + $self->{player_admin_command} = 0; + $self->{default_display_events} = 1; + $self->{browse_command} = ""; + $self->{swap_command} = ""; + $self->{exec_command} = ""; + $self->{global_chat_command} = "say"; + + # Message format operators + $self->{format_color} = ""; + $self->{format_action} = ""; + $self->{format_actionend} = ""; + + $self->{total_kills} = 0; + $self->{total_headshots} = 0; + $self->{total_suicides} = 0; + $self->{total_rounds} = 0; + $self->{total_shots} = 0; + $self->{total_hits} = 0; + + $self->{track_server_load} = 0; + $self->{track_server_timestamp} = 0; + + $self->{ignore_nextban} = (); + $self->{use_browser} = 0; + $self->{round_status} = 0; + $self->{min_players_rank} = 1; + $self->{admins} = (); + $self->{ignore_bots} = 1; + $self->{tk_penalty} = 0; + $self->{suicide_penalty} = 0; + $self->{skill_mode} = 0; + $self->{game_type} = 0; + $self->{bonusroundignore} = 0; + $self->{bonusroundtime} = 0; + $self->{bonusroundtime_ts} = 0; + $self->{bonusroundtime_state} = 0; + $self->{lastdisabledbonus} = $::ev_unixtime; + $self->{mod} = ""; + $self->{switch_admins} = 0; + $self->{public_commands} = 1; + $self->{connect_announce} = 1; + $self->{update_hostname} = 0; + + $self->{lastblueflagdefend} = 0; + $self->{lastredflagdefend} = 0; + + # location hax + $self->{nextkillx} = ""; + $self->{nextkilly} = ""; + $self->{nextkillz} = ""; + $self->{nextkillvicx} = ""; + $self->{nextkillvicy} = ""; + $self->{nextkillvicz} = ""; + + $self->{nextkillheadshot} = 0; + + $self->{next_timeout} = 0; + $self->{next_flush} = 0; + $self->{next_plyr_flush} = 0; + $self->{needsupdate} = 0; + + $self->set_play_game($realgame); + + if ($self->{rcon}) + { + $self->init_rcon(); + } + + $self->updateDB(); + $self->update_server_loc(); + + return $self; +} + +sub set_play_game +{ + my ($self, $realgame) = @_; + + if (exists($gamecode_to_game{$realgame})) + { + $self->{play_game} = $gamecode_to_game{$realgame}; + } +} + +sub is_admin +{ + my($self, $steam_id) = @_; + for (@{$self->{admins}}) { + if ($_ eq $steam_id) { + return 1; + } + } + return 0; +} + +sub get_game_mod_opts +{ + # Runs immediately after server object is created and options are loaded. + my($self) = @_; + + if ($self->{mod} ne "") { + my $mod = $self->{mod}; + + if ($mod eq "SOURCEMOD") { + $self->{browse_command} = "hlx_sm_browse"; + $self->{swap_command} = "hlx_sm_swap"; + $self->{global_chat_command} = "hlx_sm_psay"; + $self->setHlxCvars(); + } elsif ($mod eq "MANI") { + $self->{browse_command} = "ma_hlx_browse"; + $self->{swap_command} = "ma_swapteam"; + $self->{exec_command} = "ma_cexec"; + $self->{global_chat_command} = "ma_psay"; + } elsif ($mod eq "AMXX") { + $self->{browse_command} = "hlx_amx_browse"; + $self->{swap_command} = "hlx_amx_swap"; + $self->{global_chat_command} = "hlx_amx_psay"; + $self->setHlxCvars(); + } elsif ($mod eq "BEETLE") { + $self->{browse_command} = "hlx_browse"; + $self->{swap_command} = "hlx_swap"; + $self->{exec_command} = "hlx_exec"; + $self->{global_chat_command} = "admin_psay"; + } elsif ($mod eq "MINISTATS") { + $self->{browse_command} = "ms_browse"; + $self->{swap_command} = "ms_swap"; + $self->{global_chat_command} = "ms_psay"; + } + + # Turn on color and add game-specific color modifiers for when using hlx:ce sourcemod plugin + if (($self->{mod} eq "SOURCEMOD" && + ( + $self->{play_game} == CSS() + || $self->{play_game} == TF() + || $self->{play_game} == L4D() + || $self->{play_game} == DODS() + || $self->{play_game} == HL2MP() + || $self->{play_game} == AOC() + || $self->{play_game} == ZPS() + || $self->{play_game} == FF() + || $self->{play_game} == GES() + || $self->{play_game} == FOF() + || $self->{play_game} == PVKII() + || $self->{play_game} == CSP() + || $self->{play_game} == NUCLEARDAWN() + || $self->{play_game} == DDD() + ) + ) + || ($self->{mod} eq "AMXX" + && $self->{play_game} == CSTRIKE()) + ) { + + $self->{format_color} = " 1"; + if ($self->{play_game} == ZPS() || $self->{play_game} == GES()) { + $self->{format_action} = "\x05"; + } elsif ($self->{play_game} == FF()) { + $self->{format_action} = "^4"; + } else { + $self->{format_action} = "\x04"; + } + + if ($self->{play_game} == FF()) { + $self->{format_actionend} = "^0"; + } else { + $self->{format_actionend} = "\x01"; + } + } + # Insurgency can only do one solid color afaik. The rest is handled in the plugin + if ($self->{mod} eq "SOURCEMOD" && $self->{play_game} == INSMOD()) { + $self->{format_color} = " 1"; + } + } +} + +sub format_userid { + my($self, $userid) = @_; + if ($self->{mod} eq "AMXX") { + return "#".$userid; + } + return "\"".$userid."\""; +} + +sub quoteparam { + my($self, $message) = @_; + $message =~ s/'/ ' /g; + $message =~ s/"/ '' /g; + + if (($self->{game_engine} != 2 || $self->{mod} eq "SOURCEMOD") && $self->{mod} ne "MANI") { + return "\"".$message."\""; + } + return $message; +} + +# +# Set property 'key' to 'value' +# + +sub set +{ + my ($self, $key, $value) = @_; + + if (defined($self->{$key})) + { + if ($self->{$key} eq $value) + { + if ($::g_debug > 2) + { + &::printNotice("Hlstats_Server->set ignored: Value of \"$key\" is already \"$value\""); + } + return 0; + } + + $self->{$key} = $value; + + if ($key eq "hlstats_url") { + # so ingame browsing will work correctly + $self->{ingame_url} = $value; + $self->{ingame_url} =~ s/\/hlstats.php//i; + $self->{ingame_url} =~ s/\/$//; + &::printEvent("SERVER", "Ingame-URL: ".$self->{ingame_url}, 1); + } + return 1; + } + else + { + warn("HLstats_Server->set: \"$key\" is not a valid property name\n"); + return 0; + } +} + + +# +# Increment (or decrement) the value of 'key' by 'amount' (or 1 by default) +# + +sub increment +{ + my ($self, $key, $amount) = @_; + if ($amount) { + $amount = int($amount); + } else { + $amount = 1 + } + + my $value = $self->{$key}; + $self->set($key, $value + $amount); +} + + +sub init_rcon +{ + my ($self) = @_; + my $server_ip = $self->{address}; + my $server_port = $self->{port}; + my $rcon_pass = $self->{rcon}; + my $game = $self->{game}; + + if ($::g_rcon && $rcon_pass) { + if ($self->{game_engine} == 1) { + $self->{rcon_obj} = new BASTARDrcon($self); + } else { + $self->{rcon_obj} = new TRcon($self); + } + } + if ($self->{rcon_obj}) { + &::printEvent ("SERVER", "Connecting to rcon on $server_ip:$server_port ... ok"); + #&::printEvent("SERVER", "Server running game: ".$self->{play_game}, 1); + &::printEvent("SERVER", "Server running map: ".$self->get_map(), 1); + if ($::g_mode eq "LAN") { + $self->get_lan_players(); + } + } +} + +sub dorcon +{ + my ($self, $command) = @_; + my $result; + my $rcon_obj = $self->{rcon_obj}; + if (($rcon_obj) && ($::g_rcon == 1) && ($self->{rcon} ne "")) { + + # replace ; to avoid executing multiple rcon commands. + $command =~ s/;//g; + + &::printNotice("RCON", $command, 1); + $result = $rcon_obj->execute($command); + } else { + &::printNotice("Rcon error: No Object available"); + } + return $result; +} + +sub dorcon_multi +{ + my ($self, @commands) = @_; + my $result; + my $rcon_obj = $self->{rcon_obj}; + if (($rcon_obj) && ($::g_rcon == 1) && ($self->{rcon} ne "")) { + if ($self->{game_engine} > 1) + { + my $fullcmd = ""; + foreach (@commands) + { + # replace ; to avoid executing multiple rcon commands. + my $cmd = $_; + $cmd =~ s/;//g; + $fullcmd .="$cmd;"; + } + &::printNotice("RCON", $fullcmd, 1); + $result = $rcon_obj->execute($fullcmd); + } + else + { + foreach (@commands) + { + &::printNotice("RCON", $_, 1); + $result = $rcon_obj->execute($_); + } + } + } else { + &::printNotice("Rcon error: No Object available"); + } + return $result; +} + +sub rcon_getaddress +{ + my ($self, $uniqueid) = @_; + my $result; + my $rcon_obj = $self->{rcon_obj}; + if (($rcon_obj) && ($::g_rcon == 1) && ($self->{rcon} ne "")) + { + $result = $rcon_obj->getPlayer($uniqueid); + } + else + { + &::printNotice("Rcon error: No Object available"); + } + return $result; +} + +sub rcon_getStatus +{ + my ($self) = @_; + my $rcon_obj = $self->{rcon_obj}; + my $map_result = ""; + my $max_player_result = -1; + my $servhostname = ""; + my $difficulty = 0; + + if (($rcon_obj) && ($::g_rcon == 1) && ($self->{rcon} ne "")) { + ($servhostname, $map_result, $max_player_result, $difficulty) = $rcon_obj->getServerData(); + ($visible_maxplayers) = $rcon_obj->getVisiblePlayers(); + if (($visible_maxplayers != -1) && ($visible_maxplayers < $max_player_result)) { + $max_player_result = $visible_maxplayers; + } + } else { + &::printNotice("Rcon error: No Object available"); + } + return ($map_result, $max_player_result, $servhostname, $difficulty); +} + +sub rcon_getplayers +{ + my ($self) = @_; + my %result; + my $rcon_obj = $self->{rcon_obj}; + if (($rcon_obj) && ($::g_rcon == 1) && ($self->{rcon} ne "")) + { + %result = $rcon_obj->getPlayers(); + } else { + &::printNotice("Rcon error: No Object available"); + } + return %result; +} + +sub track_server_load +{ + my ($self) = @_; + + if (($::g_stdin == 0) && ($self->{track_server_load} > 0)) + { + my $last_timestamp = $self->{track_server_timestamp}; + my $new_timestamp = time(); + if ($last_timestamp > 0) + { + if ($last_timestamp+299 < $new_timestamp) + { + # fetch fps and uptime via rcon + + # Old style stats output: + #$string = " 0.00 0.00 0.00 54 1 249.81 0 dhjdsk"; + + # New style stats output: + #CPU In (KB/s) Out (KB/s) Uptime Map changes FPS Players Connects + #0.00 0.00 0.00 0 0 00.00 0 0 + + + $string = $self->dorcon("stats"); + + # Remove first line of output + $string =~ /CPU.*\n(.*)\n*L{0,1}.*\Z/; + $string = $1; + + # Grab FPS and Uptime from the output + $string =~ /([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)\s*([^ ]*)/; + $uptime = $4; + $fps = $6; + + my $act_players = $self->{numplayers}; + my $max_players = $self->{maxplayers}; + if ($max_players > 0) { + if ($act_players > $max_players) { + $act_players = $max_players; + } + } + &::execCached("flush_server_load", + "INSERT IGNORE INTO hlstats_server_load + SET + server_id=?, + timestamp=?, + act_players=?, + min_players=?, + max_players=?, + map=?, + uptime=?, + fps=?", + $self->{id}, + $new_timestamp, + $act_players, + $self->{minplayers}, + $max_players, + $self->{map}, + (($uptime)?$uptime:0), + (($fps)?$fps:0) + ); + $self->set("track_server_timestamp", $new_timestamp); + &::printEvent("SERVER", "Insert new server load timestamp", 1); + } + } else { + $self->set("track_server_timestamp", $new_timestamp); + } + } +} + +sub dostats +{ + my ($self) = @_; + my $rcon_obj = $self->{rcon_obj}; + $rcmd = $self->{broadcasting_command_announce}; + + if (($::g_stdin == 0) && ($rcon_obj) && ($self->{rcon} ne "")) + { + if ($self->{broadcasting_events} == 1) + { + my $hpk = sprintf("%.0f", 0); + if ($self->{total_kills} > 0) { + $hpk = sprintf("%.2f", (100/$self->{total_kills})*$self->{total_headshots}); + } + if ($rcmd ne "") { + $self->dorcon("$rcmd ".$self->quoteparam("HLstatsX:CE - Tracking ".&::number_format($self->{players})." players with ".&::number_format($self->{total_kills})." kills and ".&::number_format($self->{total_headshots})." headshots ($hpk%)")); + } else { + $self->messageAll("HLstatsX:CE - Tracking ".&::number_format($self->{players})." players with ".&::number_format($self->{total_kills})." kills and ".&::number_format($self->{total_headshots})." headshots ($hpk%)"); + } + } + } +} + +sub get_map +{ + my ($self, $fromupdate) = @_; + + if ($::g_stdin == 0) { + if ((time() - $self->{last_check})>120) { + $self->{last_check} = time(); + &::printNotice("get_rcon_status"); + my $temp_map = ""; + my $temp_maxplayers = -1; + my $servhostname = ""; + my $difficulty = 0; + my $update = 0; + + if ($self->{rcon_obj}) + { + ($temp_map, $temp_maxplayers, $servhostname, $difficulty) = $self->rcon_getStatus(); + + if ($temp_map eq "") { + goto STATUSFAIL; + } + + if ($self->{map} ne $temp_map) { + $self->{map} = $temp_map; + $update++; + } + + if (($temp_maxplayers != -1) && ($temp_maxplayers > 0) && ($temp_maxplayers ne "")) { + if ($self->{maxplayers} != $temp_maxplayers) { + $self->{maxplayers} = $temp_maxplayers; + $update++; + } + } + if (($difficulty > 0) && ($self->{play_game} == L4D())) { + $self->{difficulty} = $difficulty; + } + if (($self->{update_hostname} > 0) && ($self->{name} ne $servhostname) && ($servhostname ne "")) { + $self->{name} = $servhostname; + $update++; + } + } + else + { # no rcon or status command failed + STATUSFAIL: + my ($querymap, $queryhost, $querymax) = &::queryServer($self->{address}, $self->{port}, 'mapname', 'hostname', 'maxplayers'); + if ($querymap ne "") { + $self->{map} = $querymap; + $update++; + + #if map is blank, query likely failed as a whole + + if (($querymax != -1) && ($querymax > 0)) { + if ($self->{maxplayers} != $querymax) { + $self->{maxplayers} = $querymax; + $update++; + } + } + if ($self->{update_hostname} > 0 && $queryhost ne "" && $self->{name} ne $queryhost) { + $self->{name} = $queryhost; + $update++; + } + } + } + + if ($update > 0 && $fromupdate == 0) { + $self->updateDB(); + } + + &::printNotice("get_rcon_status successfully"); + } + } + return $self->{map}; +} + + +sub update_players_pings +{ + my ($self) = @_; + + if ($self->{num_trackable_players} < $self->{minplayers}) + { + &::printNotice("(IGNORED) NOTMINPLAYERS: Update_player_pings"); + } + else + { + &::printNotice("update_player_pings"); + &::printEvent("RCON", "Update Player pings", 1); + my %players = $self->rcon_getplayers(); + while ( my($pl, $player) = each(%{$self->{srv_players}}) ) + { + my $uniqueid = $player->{uniqueid}; + if (defined($players{$uniqueid})) + { + if ($player->{is_bot} == 0 && $player->{userid} > 0) { + my $ping = $players{$uniqueid}->{"Ping"}; + $player->set("ping", $ping); + if ($ping > 0) { + &::recordEvent( + "Latency", 0, + $player->{playerid}, + $ping + ); + } + } + } + } + &::printNotice("update_player_pings successfully"); + } +} + +sub get_lan_players +{ + my ($self) = @_; + + if ($::g_mode eq "LAN") { + &::printNotice("get_lan_players"); + &::printEvent("RCON", "Get LAN players", 1); + my %players = $self->rcon_getplayers(); + while ( my($p_uid, $p_obj) = each(%players) ) + { + my $srv_addr = $self->{address}.":".$self->{port}; + my $userid = $p_obj->{"UserID"}; + my $name = $p_obj->{"Name"}; + my $address = $p_obj->{"Address"}; + ::g_lan_noplayerinfo->{"$srv_addr/$userid/$name"} = { + ipaddress => $address, + userid => $userid, + name => $name, + server => $srv_addr + }; + } + &::printNotice("get_lan_players successfully"); + } +} + +sub clear_winner +{ + my ($self) = @_; + &::printNotice("clear_winner"); + @{$self->{winner}} = (); +} + +sub add_round_winner +{ + my ($self, $team) = @_; + + &::printNotice("add_round_winner"); + $self->{winner}[($self->{map_rounds} % 7)] = $team; + $self->increment("ba_map_rounds"); + $self->increment("map_rounds"); + $self->increment("rounds"); + $self->increment("total_rounds"); + + $self->{ba_ct_wins} = 0; + $self->{ba_ts_wins} = 0; + + for (@{$self->{winner}}) + { + if ($_ eq "ct") { + $self->increment("ba_ct_wins"); + } elsif ($_ eq "ts") { + $self->increment("ba_ts_wins"); + } + } +} + +sub switch_player +{ + my ($self, $playerid, $name) = @_; + my $rcmd = $self->{player_command_hint}; + + $self->dorcon($self->{swap_command}." ".$self->format_userid($playerid)); + if ($self->{player_command_hint} eq "") { + $rcmd = $self->{player_command}; + } + $self->dorcon(sprintf("%s %s %s", $rcmd, $self->format_userid($playerid), $self->quoteparam("HLstatsX:CE - You were switched to balance teams"))); + if ($self->{player_admin_command} ne "") { + $self->dorcon(sprintf("%s %s",$self->{player_admin_command}, $self->quoteparam("HLstatsX:CE - $name was switched to balance teams"))); + } +} + + +sub analyze_teams +{ + my ($self) = @_; + + if (($::g_stdin == 0) && ($self->{num_trackable_players} < $self->{minplayers})) + { + &::printNotice("(IGNORED) NOTMINPLAYERS: analyze_teams"); + } + elsif (($::g_stdin == 0) && ($self->{ba_enabled} > 0)) + { + &::printNotice("analyze_teams"); + my $ts_skill = 0; + my $ts_avg_skill = 0; + my $ts_count = 0; + my $ts_wins = $self->{ba_ts_wins}; + my $ts_kills = 0; + my $ts_deaths = 0; + my $ts_diff = 0; + my @ts_players = (); + + my $ct_skill = 0; + my $ct_avg_skill = 0; + my $ct_count = 0; + my $ct_wins = $self->{ba_ct_wins}; + my $ct_kills = 0; + my $ct_deaths = 0; + my $ct_diff = 0; + my @ct_players = (); + + my $server_id = $self->{id}; + while ( my($pl, $player) = each(%{$self->{srv_players}}) ) + { + my @Player = ( $player->{name}, #0 + $player->{uniqueid}, #1 + $player->{skill}, #2 + $player->{team}, #3 + $player->{map_kills}, #4 + $player->{map_deaths}, #5 + ($player->{map_kills}-$player->{map_deaths}), #6 + $player->{is_dead}, #7 + $player->{userid}, #8 + ); + + if ($Player[3] eq "TERRORIST") + { + push(@{$ts_players[$ts_count]}, @Player); + $ts_skill += $Player[2]; + $ts_count += 1; + $ts_kills += $Player[4]; + $ts_deaths += $Player[5]; + } + elsif ($Player[3] eq "CT") + { + push(@{$ct_players[$ct_count]}, @Player); + $ct_skill += $Player[2]; + $ct_count += 1; + $ct_kills += $Player[4]; + $ct_deaths += $Player[5]; + } + } + @ct_players = sort { $b->[6] <=> $a->[6]} @ct_players; + @ts_players = sort { $b->[6] <=> $a->[6]} @ts_players; + + &::printEvent("TEAM", "Checking Teams", 1); + $admin_msg = "AUTO-TEAM BALANCER: CT ($ct_count) $ct_kills:$ct_deaths [$ct_wins - $ts_wins] $ts_kills:$ts_deaths ($ts_count) TS"; + if ($self->{player_events} == 1) + { + if ($self->{player_admin_command} ne "") { + $cmd_str = $self->{player_admin_command}." $admin_msg"; + $self->dorcon($cmd_str); + } + } + + $self->messageAll("HLstatsX:CE - ATB - Checking Teams", 0, 1); + + if ($self->{ba_map_rounds} >= 2) # need all players for numerical balacing, at least 2 for getting all players + { + my $action_done = 0; + if ($self->{ba_last_swap} > 0) + { + $self->{ba_last_swap}--; + } + + if ($ct_count + 1 < $ts_count) # ct need players + { + $needed_players = floor( ($ts_count - $ct_count) / 2); + if ($ct_wins < 2) + { + @ts_players = sort { $b->[7] <=> $a->[7]} @ts_players; + } + else + { + @ts_players = sort { $a->[7] <=> $b->[7]} @ts_players; + } + foreach my $entry (@ts_players) + { + if ($needed_players > 0) # how many we need to make teams even (only numerical) + { + if (@{$entry}[7] == 1) # only dead players!! + { + if (($self->{switch_admins} == 1) || (($self->{switch_admins} == 0) && ($self->is_admin(@{$entry}[1]) == 0))) { + $self->switch_player(@{$entry}[8], @{$entry}[0]); + $action_done++; + $needed_players--; + } + } + } + } + } + elsif ($ts_count + 1 < $ct_count) # ts need players + { + $needed_players = floor( ($ct_count - $ts_count) / 2); + if ($ts_wins < 2) + { + @ct_players = sort { $b->[6] <=> $a->[6]} @ct_players; # best player + } + else + { + @ct_players = sort { $a->[6] <=> $b->[6]} @ct_players; # worst player + } + foreach my $entry (@ct_players) + { + if ($needed_players > 0) # how many we need to make teams even (only numerical) + { + if (@{$entry}[7] == 1) # only dead players!! + { + if (($self->{switch_admins} == 1) || (($self->{switch_admins} == 0) && ($self->is_admin(@{$entry}[1]) == 0))) { + $self->switch_player(@{$entry}[8], @{$entry}[0]); + $action_done++; + $needed_players--; + } + } + } + } + } + + if (($action_done == 0) && ($self->{ba_last_swap} == 0) && ($self->{ba_map_rounds} >= 7) && ($self->{ba_player_switch} == 0)) # frags balancing (last swap 3 rounds before) + { + if ($ct_wins < 2) + { + if ($ct_count < $ts_count) # one player less we dont need swap just bring one over + { + my $ts_found = 0; + @ts_players = sort { $b->[6] <=> $a->[6]} @ts_players; # best player + foreach my $entry (@ts_players) + { + if ($ts_found == 0) + { + if (@{$entry}[7] == 1) # only dead players!! + { + if (($self->{switch_admins} == 1) || (($self->{switch_admins} == 0) && ($self->is_admin(@{$entry}[1]) == 0))) { + $self->{ba_last_swap} = 3; + $self->switch_player(@{$entry}[9], @{$entry}[0]); + $ts_found++; + } + } + } + } + } + else # need to swap to players + { + my $ts_playerid = 0; + my $ts_name = ""; + my $ts_kills = 0; + my $ts_deaths = 0; + my $ct_playerid = 0; + my $ct_name = ""; + my $ct_kills = 0; + my $ct_deaths = 0; + my $ts_found = 0; + @ts_players = sort { $b->[6] <=> $a->[6]} @ts_players; # best player + foreach my $entry (@ts_players) + { + if ($ts_found == 0) + { + if (@{$entry}[7] == 1) # only dead players!! + { + if (($self->{switch_admins} == 1) || (($self->{switch_admins} == 0) && ($self->is_admin(@{$entry}[1]) == 0))) { + $ts_playerid = @{$entry}[8]; + $ts_name = @{$entry}[0]; + $ts_found++; + } + } + } + } + + my $ct_found = 0; + @ct_players = sort { $a->[6] <=> $b->[6]} @ct_players; # worst player + foreach my $entry (@ct_players) + { + if ($ct_found == 0) + { + if (@{$entry}[7] == 1) # only dead players!! + { + if (($self->{switch_admins} == 1) || (($self->{switch_admins} == 0) && ($self->is_admin(@{$entry}[1]) == 0))) { + $ct_playerid = @{$entry}[8]; + $ct_name = @{$entry}[0]; + $ct_found++; + } + } + } + } + if (($ts_found>0) && ($ct_found>0)) + { + $self->{ba_last_swap} = 3; + $self->switch_player($ts_playerid, $ts_name); + $self->switch_player($ct_playerid, $ct_name); + } + } + } + elsif ($ts_wins < 2) + { + if ($ts_count < $ct_count) # one player less we dont need swap just bring one over + { + my $ct_found = 0; + @ct_players = sort { $b->[6] <=> $a->[6]} @ct_players; # best player + foreach my $entry (@ct_players) + { + if ($ct_found == 0) + { + if (@{$entry}[7] == 1) # only dead players!! + { + if (($self->{switch_admins} == 1) || (($self->{switch_admins} == 0) && ($self->is_admin(@{$entry}[1]) == 0))) { + $self->{ba_last_swap} = 3; + $self->switch_player(@{$entry}[8], @{$entry}[0]); + $ct_found++; + } + } + } + } + } + else # need to swap to players + { + my $ts_playerid = 0; + my $ts_name = ""; + my $ct_playerid = 0; + my $ct_name = ""; + my $ct_found = 0; + @ct_players = sort { $b->[6] <=> $a->[6]} @ct_players; # best player + foreach my $entry (@ct_players) + { + if ($ct_found == 0) + { + if (@{$entry}[7] == 1) # only dead players!! + { + if (($self->{switch_admins} == 1) || (($self->{switch_admins} == 0) && ($self->is_admin(@{$entry}[1]) == 0))) { + $ct_playerid = @{$entry}[8]; + $ct_name = @{$entry}[0]; + $ct_found++; + } + } + } + } + + my $ts_found = 0; + @ts_players = sort { $a->[6] <=> $b->[6]} @ts_players; # worst player + foreach my $entry (@ts_players) + { + if ($ts_found == 0) + { + if (@{$entry}[7] == 1) # only dead players!! + { + if (($self->{switch_admins} == 1) || (($self->{switch_admins} == 0) && ($self->is_admin(@{$entry}[1]) == 0))) { + $ts_playerid = @{$entry}[8]; + $ts_name = @{$entry}[0]; + $ts_found++; + } + } + } + } + if (($ts_found > 0) && ($ct_found > 0)) + { + $self->{ba_last_swap} = 3; + $self->switch_player($ts_playerid, $ts_name); + $self->switch_player($ct_playerid, $ct_name); + } + } + } + } + } # end if rounds > 1 + } +} + +# +# Marks server as needing flush +# + +sub updateDB +{ + my ($self) = @_; + $self->{needsupdate} = 1; +} + +# +# Flushes server information in database +# + +sub flushDB +{ + my ($self) = @_; + $self->get_map(1); + + my $serverid = $self->{id}; + + if ($self->{total_kills} == 0) + { + my $result = &::execCached( + "get_server_player_info", + "SELECT + kills, + headshots, + suicides, + rounds, + ct_shots+ts_shots as shots, + ct_hits+ts_hits as hits + FROM + hlstats_Servers + WHERE + serverId=?", + $self->{id} + ); + ($self->{total_kills}, $self->{total_headshots}, $self->{total_suicides},$self->{total_rounds},$self->{total_shots},$self->{total_hits}) = $result->fetchrow_array(); + $result->finish; + } + + my $result = &::execCached( + "get_player_count", + "SELECT count(*) as players FROM hlstats_Players WHERE game=? and hideranking<>2", + &::quoteSQL($self->{game})); + $self->{players} = $result->fetchrow_array(); + $result->finish; + + + # Update player details + my $query = " + UPDATE + hlstats_Servers + SET + name=?, + rounds=rounds + ?, + kills=kills + ?, + suicides=suicides + ?, + headshots=headshots + ?, + bombs_planted=bombs_planted + ?, + bombs_defused=bombs_defused + ?, + players=?, + ct_wins=ct_wins + ?, + ts_wins=ts_wins + ?, + act_players=?, + max_players=?, + act_map=?, + map_rounds=?, + map_ct_wins=?, + map_ts_wins=?, + map_started=?, + map_changes=map_changes + ?, + ct_shots=ct_shots + ?, + ct_hits=ct_hits + ?, + ts_shots=ts_shots + ?, + ts_hits=ts_hits + ?, + map_ct_shots=?, + map_ct_hits=?, + map_ts_shots=?, + map_ts_hits=?, + last_event=? + WHERE + serverId=? + "; + my @vals = ( + &::quoteSQL($self->{name}), + $self->{rounds}, + $self->{kills}, + $self->{suicides}, + $self->{headshots}, + $self->{bombs_planted}, + $self->{bombs_defused}, + $self->{players}, + $self->{ct_wins}, + $self->{ts_wins}, + $self->{numplayers}, + $self->{maxplayers}, + &::quoteSQL($self->{map}), + $self->{map_rounds}, + $self->{map_ct_wins}, + $self->{map_ts_wins}, + $self->{map_started}, + $self->{map_changes}, + $self->{ct_shots}, + $self->{ct_hits}, + $self->{ts_shots}, + $self->{ts_hits}, + $self->{map_ct_shots}, + $self->{map_ct_hits}, + $self->{map_ts_shots}, + $self->{map_ts_hits}, + $::ev_unixtime, + $serverid + ); + &::execCached("update_server_stats", $query, @vals); + + $self->set("rounds", 0); + $self->set("kills", 0); + $self->set("suicides", 0); + $self->set("headshots", 0); + $self->set("bombs_planted", 0); + $self->set("bombs_defused", 0); + $self->set("ct_wins", 0); + $self->set("ts_wins", 0); + $self->set("ct_shots", 0); + $self->set("ct_hits", 0); + $self->set("ts_shots", 0); + $self->set("ts_hits", 0); + $self->set("map_changes", 0); + $self->{needsupdate} = 0; +} + +sub flush_player_count +{ + my ($self) = @_; + + &::execCached("flush_plyr_cnt", + "UPDATE hlstats_Servers SET act_players=? WHERE serverId=?", + $self->{numplayers}, + $self->{id} + ); +} + +sub update_server_loc +{ + my ($self) = @_; + my $serverid = $self->{id}; + my $server_ip = $self->{address}; + my $publicaddress = $self->{publicaddress}; + + if ($publicaddress =~ /^(\d+\.\d+\.\d+\.\d+)/) { + $server_ip = $publicaddress; + } elsif ($publicaddress =~ /^([0-9a-zA-Z\-\.]+)\:*.*/) { + my $hostip = inet_aton($1); + if ($hostip) { + $server_ip = inet_ntoa($hostip); + } + } + my $found = 0; + my $servcity = ""; + my $servcountry = ""; + my $servlat=undef; + my $servlng=undef; + if ($::g_geoip_binary > 0) { + if(!defined($::g_gi)) { + return; + } + my ($country_code, $country_code3, $country_name, $region, $city, $postal_code, $latitude, $longitude, +$metro_code, $area_code) = $::g_gi->get_city_record($server_ip); + if ($longitude) { + $found++; + $servcity = ((defined($city))?encode("utf8",$city):""); + $servcountry = ((defined($country_name))?encode("utf8",$country_name):""); + $servlat = $latitude; + $servlng = $longitude; + } + } else { + my @ipp = split (/\./,$server_ip); + my $ip_number = $ipp[0]*16777216+$ipp[1]*65536+$ipp[2]*256+$ipp[3]; + my $query = " + SELECT locId FROM geoLiteCity_Blocks WHERE startIpNum<= $ip_number AND endIpNum>= $ip_number LIMIT 1"; + my $result = &::doQuery($query); + if ($result->rows > 0) { + my $locid = $result->fetchrow_array; + $result->finish; + my $query = " + SELECT + city, + name AS country, + latitude AS lat, + longitude AS lng + FROM + geoLiteCity_Location a + INNER JOIN + hlstats_Countries b ON a.country=b.flag + WHERE + locId= $locid + LIMIT 1"; + my $result = &::doQuery($query); + if ($result->rows > 0) { + $found++; + ($servcity,$servcountry,$servlat,$servlng) = $result->fetchrow_array; + $result->finish; + } + } + } + if ($found > 0) { + my $query = " + UPDATE + `hlstats_Servers` + SET + city = '".(defined($servcity)?&::quoteSQL($servcity):"")."', + country='".(defined($servcountry)?&::quoteSQL($servcountry):"")."', + lat=".((defined($servlat))?$servlat:"NULL").", + lng=".((defined($servlng))?$servlng:"NULL")." + WHERE + serverId =$serverid + "; + &::execNonQuery($query); + } +} + +sub messageAll +{ + my($self, $msg, $noshow, $force) = @_; + + if ($self->{broadcasting_events} == 1 || $force == 1) + { + if ($self->{mod} eq "SOURCEMOD" || $self->{mod} eq "AMXX") + { + my @userlist; + + foreach $player (values(%{$self->{srv_players}})) + { + if (($player->{is_bot} == 0) && ($player->{userid} > 0) && ($player->{playerid} != $noshow) && ($player->{display_events} == 1 || $force == 1)) + { + push(@userlist, $player->{userid}); + } + } + + if ($self->{play_game} != FF()) + { + $msg = $self->{format_action}.$msg; + } + $self->messageMany($msg, 1, @userlist); + } + else + { + $self->dorcon("say ".$msg); + } + } +} + +sub messageMany +{ + my($self, $msg, $toall, @userlist) = @_; + if (scalar(@userlist) > 0) + { + if ($self->{mod} eq "SOURCEMOD") + { + my $usersendlist = ""; + foreach (@userlist) + { + $usersendlist .= $_.","; + } + $usersendlist =~ s/,$//; + my $color = $self->{format_color}; + if ($toall > 0 && $color eq " 1") + { + $color = " 2"; + } + $self->dorcon($self->{player_command}." \"$usersendlist\"$color ".$self->quoteparam($msg)); + } + elsif ($self->{mod} eq "AMXX") + { + while (@userlist) + { + my $usersendlist = ""; + for ($i = 0; $i < 8; $i++) + { + $usersendlist .= shift(@userlist); + if ($i < 7) + { + $usersendlist .= ","; + } + } + $self->dorcon("hlx_amx_bulkpsay \"$usersendlist\"".$self->{format_color}." ".$self->quoteparam($msg)); + } + } + else + { + $rcmd = $self->{broadcasting_command}; + foreach (@userlist) + { + $self->dorcon(sprintf("%s %s%s %s",$rcmd, $self->format_userid($_), $self->{format_color}, $self->quoteparam($msg))); + } + } + } +} + + +sub setHlxCvars +{ + my ($self) = @_; + + if ($self->{hlstats_url} ne "") + { + $self->dorcon("hlxce_webpage \"".$self->{hlstats_url}."\""); + } + $self->dorcon("hlxce_version \"".$::g_version."\""); + + if ($self->{play_game} eq "MANI" && $self->dorcon("mani_hlx_prefix" =~ /gameme/i)) + { + $self->dorcon("mani_hlx_prefix \"HLstatsX\""); + } +} + +sub updatePlayerCount +{ + my ($self) = @_; + + if ($::g_debug > 1) { + &::printEvent("SERVER","Updating Player Count"); + } + + my $trackable = 0; + + if ($self->{play_game} == L4D()) { + my $num = 0; + while (my($pl, $player) = each(%{$self->{srv_players}})) { + if ($player->{trackable} == 1) { + $trackable++; + } + if ($player->{userid} > 0) { + $num++; + } + } + $self->{numplayers} = $num; + $self->{num_trackable_players} = $trackable; + } else { + $self->{numplayers} = scalar keys %{$self->{srv_players}}; + while (my($pl, $player) = each(%{$self->{srv_players}})) { + if ($player->{trackable} == 1) { + $trackable++; + } + } + $self->{num_trackable_players} = $trackable; + } + + $self->flush_player_count(); +} + +1; diff --git a/scripts/ImportBans/README b/scripts/ImportBans/README new file mode 100644 index 0000000..f845139 --- /dev/null +++ b/scripts/ImportBans/README @@ -0,0 +1,55 @@ +HLStatsX:CE Import Ban Options +---------------------- +Description: +HLStatsX:CE has two different scripts to use to import bans from your banning system: + +1) ImportBans +ImportBans.pl is a perl script and is the original importing script. +It only imports bans and does not unban a player if they're unbanned from your ban system. +This script supports SourceBans, AMXBans, BeetleMod, and GlobalBan. +You must have perl installed to use this script. + +2) hlstatsxbans +Hlstatsxbans is written by Peace-Maker and is written in PHP. +This script will ban AND UNBAN players as they are banned from your banning system. +Forum URL: http://forums.interwavestudios.com/topic/167-import-mysql-bans-to-hlxce-bancheater-page/ +This script suports SourceBans, AMXBans, BeetleMod, and GlobalBan. +You must have PHP installed to use this script. + +Configuration: +Select which ban system you want to use, either ImportBans or HLStatsXBans. You must then edit the corresponding file. + +*run_importbans +** Open run_importbans in a text editor +** Configure "BANSYSTEM" for which script you would like to use. + +*ImportBans +** Open ImportBans.pl in a text editor. +** Fill in the HLX DB INFO section (should be same as that found in hlstats.conf in the Perl directory) +** Fill in at least one of the other databases (Sourcebans, AMXBans, BeetlesMod) that you want to import from. + +NOTE: You can use any of these databases simultaneously -- fill in the ones you want +to pull from and those databases will be queried. + +*HLStatsXBans +** Open HLStatsXBans.cfg in a text editor. +** Fill in the database info for HLX. +** Fill in at least one of the other databases that you want to import from. + +Running: +You should run the script manually one time to make sure everything is working. +To run ImportBans, run: + +./importbans.pl + +To run HLStatsXBans, run: + +php hlstatsxbans.php + +If everything works, you can either schedule the exact same command you just executed to run at a configured interval or time, +or use the run_importbans wrapper to get logging (great for console use). +To use run_importbans, make sure you have edited the file for which script you want to use, and then simply execute: + +./run_importbans. + +Note: you will not get any output from this script. Everything is written into your logs/ directory. \ No newline at end of file diff --git a/scripts/ImportBans/hlstatsxban.cfg b/scripts/ImportBans/hlstatsxban.cfg new file mode 100644 index 0000000..1454ff1 --- /dev/null +++ b/scripts/ImportBans/hlstatsxban.cfg @@ -0,0 +1,105 @@ + \ No newline at end of file diff --git a/scripts/ImportBans/hlstatsxban.php b/scripts/ImportBans/hlstatsxban.php new file mode 100644 index 0000000..029a164 --- /dev/null +++ b/scripts/ImportBans/hlstatsxban.php @@ -0,0 +1,362 @@ +query("SELECT `authid` FROM `".SB_PREFIX."_bans` WHERE `RemoveType` IS NULL AND `length` = 0")) { + while ($banned = $bans->fetch_array(MYSQL_ASSOC)) { + if(!in_array($banned["authid"], $bannedplayers)) { + $bannedplayers[] = $banned["authid"]; + ++$bcnt; + } + } + } + else { + die('[-] Error retrieving banned players: ' . $con->error); + } + + + + // Read unbanned players + $ubcnt = 0; + if ($unbans = $con->query("SELECT `authid` FROM `".SB_PREFIX."_bans` WHERE `RemoveType` IS NOT NULL AND `RemovedOn` IS NOT NULL")) { + while ($unbanned = $unbans->fetch_array(MYSQL_ASSOC)) { + if(!in_array($unbanned["authid"], $bannedplayers) && !in_array($unbanned["authid"], $unbannedplayers)) { + $unbannedplayers[] = $unbanned["authid"]; + ++$ubcnt; + } + } + } + else { + die('[-] Error retrieving unbanned players: ' . $con->error); + } + + $con->close(); + print("[+] Retrieved ".$bcnt." banned and ".$ubcnt." unbanned players from SourceBans.\n"); +} + +//------------------------------ +// AMXBans Part +//------------------------------ +if ($useamx) +{ + // Connect to the AMXBans database. + $con = new mysqli(AMX_HOST, AMX_USER, AMX_PASS, AMX_NAME, AMX_PORT); + if (mysqli_connect_error()) die('[-] Can\'t connect to AMXBans Database (' . mysqli_connect_errno() . ') ' . mysqli_connect_error()); + + print("[+] Successfully connected to AMXBans database. Retrieving bans now.\n"); + + // Get permanent banned players + $bcnt = 0; + if ($bans = $con->query("SELECT `player_id` FROM `".AMX_PREFIX."_bans` WHERE `ban_length` = 0")) { + while ($banned = $bans->fetch_array(MYSQL_ASSOC)) { + if(!in_array($banned["player_id"], $bannedplayers)) + { + $bannedplayers[] = $banned["player_id"]; + ++$bcnt; + } + } + } else { + die('[-] Error retrieving banned players: ' . $con->error); + } + + + // Read unbanned players + $ubcnt = 0; + // Handles (apparently) pre-6.0 version DB or lower + if ($unbans = $con->query("SELECT `player_id` FROM `".AMX_PREFIX."_banhistory` WHERE `ban_length` = 0")) { + while ($unbanned = $unbans->fetch_array(MYSQL_ASSOC)) { + if(!in_array($unbanned["player_id"], $bannedplayers) && !in_array($unbanned["player_id"], $unbannedplayers)) + { + $unbannedplayers[] = $unbanned["player_id"]; + ++$ubcnt; + } + } + } + // Handles (apparently) 6.0 version DB or higher + else if ($unbans = $con->query("SELECT `player_id` FROM `".AMX_PREFIX."_bans` WHERE `expired` = 1")) { + while ($unbanned = $unbans->fetch_array(MYSQL_ASSOC)) { + if(!in_array($unbanned["player_id"], $bannedplayers) && !in_array($unbanned["player_id"], $unbannedplayers)) + { + $unbannedplayers[] = $unbanned["player_id"]; + ++$ubcnt; + } + } + } else { + die('[-] Error retrieving unbanned players: ' . $con->error); + } + + + $con->close(); + print("[+] Retrieved ".$bcnt." banned and ".$ubcnt." unbanned players from AMXBans.\n"); +} + +//------------------------------ +// Beetlesmod Part +//------------------------------ +if ($usebm) +{ + // Connect to the Beetlesmod database. + $con = new mysqli(BM_HOST, BM_USER, BM_PASS, BM_NAME, BM_PORT); + if (mysqli_connect_error()) die('[-] Can\'t connect to Beetlesmod Database (' . mysqli_connect_errno() . ') ' . mysqli_connect_error()); + + print("[+] Successfully connected to Beetlesmod database. Retrieving bans now.\n"); + + // Get permanent banned players + $bcnt = 0; + if ($bans = $con->query("SELECT `steamid` FROM `".BM_PREFIX."_bans` WHERE `Until` IS NULL")) { + while ($banned = $bans->fetch_array(MYSQL_ASSOC)) { + if(!in_array($banned["steamid"], $bannedplayers)) + { + $bannedplayers[] = $banned["steamid"]; + ++$bcnt; + } + } + } else { + die('[-] Error retrieving banned players: ' . $con->error); + } + + + // Read unbanned players + $ubcnt = 0; + if ($unbans = $con->query("SELECT `steamid` FROM `".BM_PREFIX."_bans` WHERE `Until` IS NULL AND `Remove` = 0")) { + while ($unbanned = $unbans->fetch_array(MYSQL_ASSOC)) { + if(!in_array($unbanned["steamid"], $bannedplayers) && !in_array($unbanned["steamid"], $unbannedplayers)) + { + $unbannedplayers[] = $unbanned["steamid"]; + ++$ubcnt; + } + } + } else { + die('[-] Error retrieving unbanned players: ' . $con->error); + } + + + $con->close(); + print("[+] Retrieved ".$bcnt." banned and ".$ubcnt." unbanned players from Beetlesmod.\n"); +} + +//------------------------------ +// Globalban Part +//------------------------------ +if ($usegb) +{ + // Connect to the Globalban database. + $con = new mysqli(GB_HOST, GB_USER, GB_PASS, GB_NAME, GB_PORT); + if (mysqli_connect_error()) die('[-] Can\'t connect to Globalban Database (' . mysqli_connect_errno() . ') ' . mysqli_connect_error()); + + print("[+] Successfully connected to Globalban database. Retrieving bans now.\n"); + + // Get permanent banned players + $bcnt = 0; + if ($bans = $con->query("SELECT `steam_id` FROM `".GB_PREFIX."_ban` WHERE `active` = 1 AND `pending` = 0 AND `length` = 0")) { + while ($banned = $bans->fetch_array(MYSQL_ASSOC)) { + if(!in_array($banned["steam_id"], $bannedplayers)) + { + $bannedplayers[] = $banned["steam_id"]; + ++$bcnt; + } + } + } else { + die('[-] Error retrieving banned players: ' . $con->error); + } + + + // Read unbanned players + $ubcnt = 0; + if ($unbans = $con->query("SELECT `steam_id` FROM `".GB_PREFIX."_ban` WHERE `active` = 0 AND `pending` = 0 AND `length` = 0")) { + while ($unbanned = $unbans->fetch_array(MYSQL_ASSOC)) { + if(!in_array($unbanned["steam_id"], $bannedplayers) && !in_array($unbanned["steam_id"], $unbannedplayers)) + { + $unbannedplayers[] = $unbanned["steam_id"]; + ++$ubcnt; + } + } + } else { + die('[-] Error retrieving unbanned players: ' . $con->error); + } + + + $con->close(); + print("[+] Retrieved ".$bcnt." banned and ".$ubcnt." unbanned players from Globalban.\n"); +} + +//------------------------------ +// MySQL Banning Part +//------------------------------ +if ($usemb) +{ + // Connect to the MySQL Banning database. + $con = new mysqli(MB_HOST, MB_USER, MB_PASS, MB_NAME, MB_PORT); + if (mysqli_connect_error()) die('[-] Can\'t connect to MySQL Banning Database (' . mysqli_connect_errno() . ') ' . mysqli_connect_error()); + + print("[+] Successfully connected to MySQL Banning database. Retrieving bans now.\n"); + + // Get permanent banned players + $bcnt = 0; + if ($bans = $con->query("SELECT `steam_id` FROM `".MB_PREFIX."_bans` WHERE `ban_length` = 0")) { + while ($banned = $bans->fetch_array(MYSQL_ASSOC)) { + if(!in_array($banned["steam_id"], $bannedplayers)) + { + $bannedplayers[] = $banned["steam_id"]; + ++$bcnt; + } + } + } else { + die('[-] Error retrieving banned players: ' . $con->error); + } + + + /****** SM MySQL Banning doesn't provide a ban history AFAIK ******/ + + // Read unbanned players + // $ubcnt = 0; + // if ($unbans = $con->query("SELECT `steam_id` FROM `".MB_PREFIX."_bans` WHERE `ban_length` = 0")) { + // while ($unbanned = $unbans->fetch_array(MYSQL_ASSOC)) { + // if(!in_array($unbanned["steam_id"], $bannedplayers) && !in_array($unbanned["steam_id"], $unbannedplayers)) + // { + // $unbannedplayers[] = $unbanned["steam_id"]; + // ++$ubcnt; + // } + // } + // } else { + // die('[-] Error retrieving unbanned players: ' . $con->error); + //} + + $con->close(); + //print("[+] Retrieved ".$bcnt." banned and ".$ubcnt." unbanned players from MySQL Banning.\n"); + print("[+] Retrieved ".$bcnt." banned players from MySQL Banning.\n"); +} + +//------------------------------ +// HLstatsX Part +//------------------------------ + +if(empty($bannedplayers) && empty($unbannedplayers)) + die('[-] Nothing to change. Exiting.'); + +$bannedsteamids="''"; +$unbannedsteamids="''"; + +if(!empty($bannedplayers)) +{ + $bannedsteamids = "'"; + foreach ($bannedplayers as $steamid) + { + $steamid = preg_replace('/^STEAM_[0-9]+?\:/i','',$steamid); + $bannedsteamids .= $steamid."','"; + } + $bannedsteamids .= preg_replace('/\,\'$/','',$steamid); + $bannedsteamids .= "'"; +} + +if(!empty($unbannedplayers)) +{ + $unbannedsteamids = "'"; + foreach ($unbannedplayers as $steamid) + { + $steamid = preg_replace('/^STEAM_[0-9]+?\:/i','',$steamid); + $unbannedsteamids .= $steamid."','"; + } + $unbannedsteamids .= preg_replace('/\,\'$/','',$steamid); + $unbannedsteamids .= "'"; +} + +// Connection to DB +$hlxcon = new mysqli(HLX_HOST, HLX_USER, HLX_PASS, '', HLX_PORT); +if (mysqli_connect_error()) die('[-] Can\'t connect to HLstatsX Database (' . mysqli_connect_errno() . ') ' . mysqli_connect_error()); + +print("[+] Successfully connected to HLstatsX database server. Updating players...\n"); +// Loop through all hlstatsx databases +foreach ($hlxdbs as $hlxdb) +{ + $unbancnt = $bancnt = 0; + $hlxcon->select_db($hlxdb); + // Hide all banned players + if ($hlxban = $hlxcon->query("UPDATE `".HLX_PREFIX."_Players` SET `hideranking` = 2 WHERE `hideranking` < 2 AND `playerId` IN (SELECT `playerId` FROM `".HLX_PREFIX."_PlayerUniqueIds` WHERE `uniqueId` IN (".$bannedsteamids."));")) { + $bancnt = ($hlxcon->affected_rows?$hlxcon->affected_rows:0); + } + else { + die('[-] Error hiding banned players: ' . $hlxcon->error); + } + + // Show all unbanned players + if ($hlxunban = $hlxcon->query("UPDATE `".HLX_PREFIX."_Players` SET `hideranking` = 0 WHERE `hideranking` = 2 AND `playerId` IN (SELECT `playerId` FROM `".HLX_PREFIX."_PlayerUniqueIds` WHERE `uniqueId` IN (".$unbannedsteamids."));")) { + $unbancnt = ($hlxcon->affected_rows?$hlxcon->affected_rows:0); + + if ($bancnt>0||$unbancnt>0) { + print("[+] ".$hlxdb.": ".$bancnt." players were marked as banned, ".$unbancnt." players were reenabled again.\n"); + } + else { + print("[-] ".$hlxdb.": No player changed.\n"); + } + } + else { + die('[-] Error showing unbanned players: ' . $hlxcon->error); + } +} +$hlxcon->close(); +?> \ No newline at end of file diff --git a/scripts/ImportBans/importbans.pl b/scripts/ImportBans/importbans.pl new file mode 100644 index 0000000..d32825a --- /dev/null +++ b/scripts/ImportBans/importbans.pl @@ -0,0 +1,194 @@ +#!/usr/bin/perl +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + +#### +# Script to mark banned users in HLstatsX Community Edition from a Sourcebans, AMXBans, Beetlesmod, or ES GlobalBan database (or multiple at once!) +# Last Revised: 2009-07-22 13:35 GMT +# BeetlesMod and GlobalBan support added by Bo Tribun (R3M) + +####################################################################################### +# You must fill info out for the HLX DB and at least one of the Bans databases +####################################################################################### +# Sourcebans DB Info +$sb_dbhost = "localhost"; +$sb_dbport = 3306; +$sb_dbuser = ""; +$sb_dbpass = ""; +$sb_dbname = ""; +$sb_prefix = "sb_"; # be sure to include the underscore (_) + +# AMXBans DB Info +$amxb_dbhost = "localhost"; +$amxb_dbport = 3306; +$amxb_dbuser = ""; +$amxb_dbpass = ""; +$amxb_dbname = ""; + +# BeetlesMod DB Info +$bm_dbhost = "localhost"; +$bm_dbport = 3306; +$bm_dbuser = ""; +$bm_dbpass = ""; +$bm_dbname = ""; + +# ES GlobalBan DB Info +$gb_dbhost = "localhost"; +$gb_dbport = 3306; +$gb_dbuser = ""; +$gb_dbpass = ""; +$gb_dbname = ""; + +# HLX DB Info +$hlx_dbhost = "localhost"; +$hlx_dbport = 3306; +$hlx_dbuser = ""; +$hlx_dbpass = ""; +$hlx_dbname = ""; + + +## +## +################################################################################ +## No need to edit below this line +## + +use DBI; + +$havesbinfo = ($sb_dbhost eq "" || $sb_dbuser eq "" || $sb_dbpass eq "" || $sb_dbname eq "")?0:1; +$haveamxbinfo = ($amxb_dbhost eq "" || $amxb_dbuser eq "" || $amxb_dbpass eq "" || $amxb_dbname eq "")?0:1; +$havebminfo = ($bm_dbhost eq "" || $bm_dbuser eq "" || $bm_dbpass eq "" || $bm_dbname eq "")?0:1; +$havegbinfo = ($gb_dbhost eq "" || $gb_dbuser eq "" || $gb_dbpass eq "" || $gb_dbname eq "")?0:1; +$havehlxinfo = ($hlx_dbhost eq "" || $hlx_dbuser eq "" || $hlx_dbpass eq "" || $hlx_dbname eq "")?0:1; + +die("DB login info incomplete. Exiting\n") if ($havehlxinfo == 0 || ($havesbinfo == 0 && $haveamxbinfo == 0 && $havebminfo == 0 && $havegbinfo == 0)); + +@steamids = (); + +if ($havesbinfo) { + print "Connecting to Sourcebans database...\n"; + my $sb_dbconn = DBI->connect( + "DBI:mysql:database=$sb_dbname;host=$sb_dbhost;port=$sb_dbport", + $sb_dbuser, $sb_dbpass) or die ("\nCan't connect to Sourcebans database '$sb_dbname' on '$sb_dbhost'\n" . + "Server error: $DBI::errstr\n"); + + print "Successfully connected to Sourcebans database. Retrieving banned Steam IDs now...\n"; + + my $result = &doQuery($sb_dbconn, "SELECT `authid` FROM ".$sb_prefix."bans WHERE `length` = 0 AND `RemovedBy` IS NULL"); + while ( my($steamid) = $result->fetchrow_array) { + push(@steamids, $steamid); + } + my $rows = $result->rows; + if ($rows) { + print $rows." banned users retrieved from Sourcebans.\n"; + } + $sb_dbconn->disconnect; +} + +if ($haveamxbinfo) { + print "Connecting to AMXBans database...\n"; + my $amxb_dbconn = DBI->connect( + "DBI:mysql:database=$amxb_dbname;host=$amxb_dbhost;port=$amxb_dbport", + $amxb_dbuser, $amxb_dbpass) or die ("\nCan't connect to AMXBans database '$amxb_dbname' on '$amxb_dbhost'\n" . + "Server error: $DBI::errstr\n"); + + print "Successfully connected to AMXBans database. Retrieving banned Steam IDs now...\n"; + + my $result = &doQuery($amxb_dbconn, "SELECT `player_id` FROM amx_bans WHERE `ban_length` = 0"); + while ( my($steamid) = $result->fetchrow_array) { + push(@steamids, $steamid); + } + my $rows = $result->rows; + if ($rows) { + print $rows." banned users retrieved from AMXBans.\n"; + } + $amxb_dbconn->disconnect; +} + +if ($havebminfo) { + print "Connecting to BeetlesMod database...\n"; + my $bm_dbconn = DBI->connect( + "DBI:mysql:database=$bm_dbname;host=$bm_dbhost;port=$bm_dbport", + $bm_dbuser, $bm_dbpass) or die ("\nCan't connect to BeetlesMod database '$bm_dbname' on '$bm_dbhost'\n" . + "Server error: $DBI::errstr\n"); + + print "Successfully connected to BeetlesMod database. Retrieving banned Steam IDs now...\n"; + + my $result = &doQuery($bm_dbconn, "SELECT `steamid` FROM `bm_bans` WHERE `Until` IS NULL"); + while ( my($steamid) = $result->fetchrow_array) { + push(@steamids, $steamid); + } + my $rows = $result->rows; + if ($rows) { + print $rows." banned users retrieved from BeetlesMod.\n"; + } + $bm_dbconn->disconnect; +} + +if ($havegbinfo) { + print "Connecting to ES GlobalBan database...\n"; + my $gb_dbconn = DBI->connect( + "DBI:mysql:database=$gb_dbname;host=$gb_dbhost;port=$gb_dbport", + $gb_dbuser, $gb_dbpass) or die ("\nCan't connect to ES GlobalBan database '$gb_dbname' on '$gb_dbhost'\n" . + "Server error: $DBI::errstr\n"); + + print "Successfully connected to ES GlobalBan database. Retrieving banned Steam IDs now...\n"; + + my $result = &doQuery($gb_dbconn, "SELECT `steam_id` FROM `gban_ban` WHERE `active` = 1 AND `pending` = 0 AND `length` = 0"); + while ( my($steamid) = $result->fetchrow_array) { + push(@steamids, $steamid); + } + my $rows = $result->rows; + if ($rows) { + print $rows." banned users retrieved from ES GlobalBan.\n"; + } + $gb_dbconn->disconnect; +} + +if (@steamids) { + $steamidstring = "'"; + foreach $steamid (@steamids) + { + $steamid =~ s/^STEAM_[0-9]+?\://i; + $steamidstring .= $steamid."','"; + } + $steamidstring =~ s/\,\'$//; + + print "Connecting to HLX:CE database...\n"; + $hlx_dbconn = DBI->connect( + "DBI:mysql:database=$hlx_dbname;host=$hlx_dbhost;port=$hlx_dbport", + $hlx_dbuser, $hlx_dbpass) or die ("\nCan't connect to HLX:CE database '$hlx_dbname' on '$hlx_dbhost'\n" . + "Server error: $DBI::errstr\n"); + print "Updating HLX:CE banned players...\n"; + $result = &doQuery($hlx_dbconn, "UPDATE `hlstats_Players` SET `hideranking` = 2 WHERE `playerId` IN (SELECT `playerId` FROM hlstats_PlayerUniqueIds WHERE `uniqueId` IN ($steamidstring)) AND `hideranking` < 2"); + print $result->rows." users newly marked as banned.\n"; + $hlx_dbconn->disconnect; +} else { + die("No banned users found in database(s). Exiting\n"); +} + +sub doQuery +{ + my ($dbconn, $query, $callref) = @_; + my $result = $dbconn->prepare($query) or die("Unable to prepare query:\n$query\n$DBI::errstr\n$callref"); + $result->execute or die("Unable to execute query:\n$query\n$DBI::errstr\n$callref"); + + return $result; +} diff --git a/scripts/ImportBans/run_importbans b/scripts/ImportBans/run_importbans new file mode 100644 index 0000000..918ab56 --- /dev/null +++ b/scripts/ImportBans/run_importbans @@ -0,0 +1,79 @@ +#!/bin/bash +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + +# HLStatsX:CE now has two import ban utilities: +# 1) ImportBans.pl is the original importing script written in perl. +# +# 2) hlstatsxbans.php is written by Peace-Maker and is written in PHP. +# +# More information on these scripts can be found in README. + + +# Please select your banning system below using the number that corresponds to the ban system above. +BANSYSTEM=1 + +### Nothing needs to be modified below here ### + +# verify that you have a logs directory +if [ ! -d logs ];then + mkdir logs +fi + +if [ ! -w logs ];then + echo "you need write access to the logs folder" + exit 1 +fi + +# print info to the log file and run importbans.pl +echo Importing Bans -- `date +%c` >> logs/log`date +_%Y%m%d` + +case $BANSYSTEM in + 1) + echo Using importbans.pl >> logs/log`date +_%Y%m%d` 2>&1 + ./importbans.pl >> logs/log`date +_%Y%m%d` 2>&1 + ;; + 2) + echo Using hlstatsxbans.php >> logs/log`date +_%Y%m%d` 2>&1 + /usr/bin/php `pwd`/hlstatsxban.php >> logs/log`date +_%Y%m%d` 2>&1 + ;; + *) + echo Warning: BANSYSTEM is not correctly configured. Please check your configuration. + ;; +esac +echo >> logs/log`date +_%Y%m%d` + + +exit 0 \ No newline at end of file diff --git a/scripts/TRcon.pm b/scripts/TRcon.pm new file mode 100644 index 0000000..735b554 --- /dev/null +++ b/scripts/TRcon.pm @@ -0,0 +1,500 @@ +package TRcon; +# +# TRcon Perl Module - execute commands on a remote Half-Life2 server using remote console. +# +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + +use strict; +no strict 'vars'; + +use Sys::Hostname; +use IO::Socket; +use IO::Select; +use bytes; +use Scalar::Util; + +do "$::opt_libdir/HLstats_GameConstants.plib"; + +my $VERSION = "1.00"; +my $TIMEOUT = 1.0; + +my $SERVERDATA_EXECCOMMAND = 2; +my $SERVERDATA_AUTH = 3; +my $SERVERDATA_RESPONSE_VALUE = 0; +my $SERVERDATA_AUTH_RESPONSE = 2; +my $REFRESH_SOCKET_COUNTER_LIMIT = 100; +my $AUTH_PACKET_ID = 1; +my $SPLIT_END_PACKET_ID = 2; + +# +# Constructor +# + +sub new +{ + my ($class_name, $server_object) = @_; + my ($self) = {}; + bless($self, $class_name); + + $self->{"rcon_socket"} = 0; + $self->{"server_object"} = $server_object; + Scalar::Util::weaken($self->{"server_object"}); + $self->{"auth"} = 0; + $self->{"refresh_socket_counter"} = 0; + $self->{"packet_id"} = 10; + return $self; +} + +sub execute +{ + my ($self, $command, $splitted_answer) = @_; + if ($::g_stdin == 0) { + my $answer = $self->sendrecv($command, $splitted_answer); + if ($answer =~ /bad rcon_password/i) { + &::printEvent("TRCON", "Bad Password"); + } + return $answer; + } +} + +sub get_auth_code +{ + my ($self, $id) = @_; + my $auth = 0; + + if ($id == $AUTH_PACKET_ID) { + &::printEvent("TRCON", "Rcon password accepted"); + $auth = 1; + $self->{"auth"} = 1; + } elsif( $id == -1) { + &::printEvent("TRCON", "Rcon password refused"); + $self->{"auth"} = 0; + $auth = 0; + } else { + &::printEvent("TRCON", "Bad password response id=$id"); + $self->{"auth"} = 0; + $auth = 0; + } + return $auth; + +} + + +sub sendrecv +{ + my ($self, $msg, $splitted_answer) = @_; + + my $rs_counter = $self->{"refresh_socket_counter"}; + if ($rs_counter % $REFRESH_SOCKET_COUNTER_LIMIT == 0) { + if ($self->{"rcon_socket"} > 0) { + shutdown($self->{"rcon_socket"}, 2); + $self->{"rcon_socket"} = 0; + } + my $server_object = $self->{"server_object"}; + $self->{"rcon_socket"} = IO::Socket::INET->new( + Proto=>"tcp", + PeerAddr=>$server_object->{address}, + PeerPort=>$server_object->{port}, + ); + if (!$self->{"rcon_socket"}) { + &::printEvent("TRCON", "Cannot setup TCP socket on ".$server_object->{address}.":".$server_object->{port}.": $!"); + } + $self->{"refresh_socket_counter"} = 0; + $self->{"auth"} = 0; + } + + + my $r_socket = $self->{"rcon_socket"}; + my $server = $self->{"server_object"}; + + my $auth = $self->{"auth"}; + my $response = ""; + my $packet_id = $self->{"packet_id"}; + + if (($r_socket) && ($r_socket->connected() )) { + if ($auth == 0) { + &::printEvent("TRCON", "Trying to get rcon access (auth)"); + if ($self->send_rcon($AUTH_PACKET_ID, $SERVERDATA_AUTH, $server->{rcon}, "")) { + &::printEvent("TRCON", "Couldn't send password"); + return; + } + my ($id, $command, $response) = $self->recieve_rcon($AUTH_PACKET_ID); + if($command == $SERVERDATA_AUTH_RESPONSE) { + $auth = $self->get_auth_code($id); + } elsif (($command == $SERVERDATA_RESPONSE_VALUE) && ($id == $AUTH_PACKET_ID)) { + #Source servers sends one junk packet during the authentication step, before it responds + # with the correct authentication response. + &::printEvent("TRCON", "Junk packet from Source Engine"); + my ($id, $command, $response) = $self->recieve_rcon($AUTH_PACKET_ID); + $auth = $self->get_auth_code($id); + } + } + + if ($auth == 1) { + $self->{"refresh_socket_counter"}++; + $self->send_rcon($packet_id, $SERVERDATA_EXECCOMMAND, $msg); + if ($splitted_answer > 0) { + $self->send_rcon($SPLIT_END_PACKET_ID, $SERVERDATA_EXECCOMMAND, ""); + } + my ($id, $command, $response) = $self->recieve_rcon($packet_id, $splitted_answer); + $self->{"packet_id"}++; + if ($self->{"packet_id"} > 32767) { + $self->{"packet_id"} = 10; + } + return $response; + } + } else { + $self->{"refresh_socket_counter"} = 0; + } + return; + +} + +# +# Send a package +# +sub send_rcon +{ + my ($self, $id, $command, $string1, $string2) = @_; + my $data = pack("VVZ*Z*", $id, $command, $string1, $string2); + my $size = length($data); + if($size > 4096) { + &::printEvent("TRCON", "Command to long to send!"); + return 1; + } + $data = pack("V", $size).$data; + + my $r_socket = $self->{"rcon_socket"}; + if ($r_socket && $r_socket->connected() && $r_socket->peeraddr()) { + $r_socket->send($data, 0); + return 0; + } else { + $self->{"refresh_socket_counter"} = 0; + } + return 1; +} + +# +# Recieve a package +# +sub recieve_rcon +{ + my ($self, $packet_id, $splitted_answer) = @_; + my ($size, $id, $command, $msg); + my $tmp = ""; + + my $r_socket = $self->{"rcon_socket"}; + my $server = $self->{"server_object"}; + my $auth = $self->{"auth"}; + my $packet_id = $self->{"packet_id"}; + + if (($r_socket) && ($r_socket->connected() )) { + if(IO::Select->new($r_socket)->can_read($TIMEOUT)) { # $TIMEOUT seconds timeout + $r_socket->recv($tmp, 1500); + $size = unpack("V", substr($tmp, 0, 4)); + if ($size == 0) { + $self->{"refresh_socket_counter"} = 0; + return (-1, -1, -1); + } + $id = unpack("V", substr($tmp, 4, 4)); + $command = unpack("V", substr($tmp, 8, 4)); + if ($id == $packet_id) { + $tmp = substr($tmp, 12, length($tmp)-12); + if ($splitted_answer > 0) { + my $last_packet_id = $id; + while ($last_packet_id != $SPLIT_END_PACKET_ID) { + if(IO::Select->new($r_socket)->can_read($TIMEOUT)) { + $r_socket->recv($split_data, 1500); + my $split_size = unpack("V", substr($split_data, 0, 4)); + my $split_id = unpack("V", substr($split_data, 4, 4)); + my $split_command = unpack("V", substr($split_data, 8, 4)); + if ($split_id == $last_packet_id) { + $split_data = substr($split_data, 12, length($split_data)-12); + } + if (!defined($split_id)){ + $last_packet_id = $SPLIT_END_PACKET_ID; + } else { + $last_packet_id = $split_id; + } + $tmp .= $split_data; + } else { + &::printNotice("TRCON", "Multiple packet error"); + $last_packet_id = $SPLIT_END_PACKET_ID; + } + } + } + if (length($tmp) > 0) { + $tmp .= "\x00"; + my ($string1, $string2) = unpack("Z*Z*", $tmp); + $msg = $string1.$string2; + } else { + $msg = ""; + } + } + return ($id, $command, $msg); + } else { + $self->{"refresh_socket_counter"} = 0; + return (-1, -1, -1); + } + } else { + $self->{"refresh_socket_counter"} = 0; + return (-1, -1, -1); + } +} + +# +# Get error message +# + +sub error +{ + my ($self) = @_; + return $self->{"rcon_error"}; +} + + + +# +# Parse "status" command output into player information +# + +sub getPlayers +{ + my ($self) = @_; + my $status = $self->execute("status", 1); + if (!$status) + { + return ("", -1, "", 0); + } + + my @lines = split(/[\r\n]+/, $status); + + my %players; + +# HL2 standard +# userid name uniqueid connected ping loss state adr +# 187 ".:[SoV]:.Evil Shadow" STEAM_0:1:6200412 13:48 97 0 active 213.10.196.229:24085 + +# L4D +# userid name uniqueid connected ping loss state rate adr +# 2 1 "psychonic" STEAM_1:1:4153990 00:45 68 1 active 20000 192.168.5.115:27006 + + foreach my $line (@lines) + { + if ($line =~ /^\#\s* + (\d+)\s+ # userid + (?:\d+\s+|) # extra number in L4D, not sure what this is?? + "(.+)"\s+ # name + (.+)\s+ # uniqueid + ([\d:]+)\s+ # time + (\d+)\s+ # ping + (\d+)\s+ # loss + ([A-Za-z]+)\s+ # state + (?:\d+\s+|) # rate (L4D only) + ([^:]+): # addr + (\S+) # port + $/x) + { + my $userid = $1; + my $name = $2; + my $uniqueid = $3; + my $time = $4; + my $ping = $5; + my $loss = $6; + my $state = $7; + my $address = $8; + my $port = $9; + + $uniqueid =~ s/^STEAM_[0-9]+?\://i; + + # &::printEvent("DEBUG", "USERID: '$userid', NAME: '$name', UNIQUEID: '$uniqueid', TIME: '$time', PING: '$ping', LOSS: '$loss', STATE: '$state', ADDRESS:'$address', CLI_PORT: '$port'", 1); + + if ($::g_mode eq "NameTrack") { + $players{$name} = { + "Name" => $name, + "UserID" => $userid, + "UniqueID" => $uniqueid, + "Time" => $time, + "Ping" => $ping, + "Loss" => $loss, + "State" => $state, + "Address" => $address, + "ClientPort" => $port + }; + } elsif ($::g_mode eq "LAN") { + $players{$address} = { + "Name" => $name, + "UserID" => $userid, + "UniqueID" => $uniqueid, + "Time" => $time, + "Ping" => $ping, + "Loss" => $loss, + "State" => $state, + "Address" => $address, + "ClientPort" => $port + }; + } else { + $players{$uniqueid} = { + "Name" => $name, + "UserID" => $userid, + "UniqueID" => $uniqueid, + "Time" => $time, + "Ping" => $ping, + "Loss" => $loss, + "State" => $state, + "Address" => $address, + "ClientPort" => $port + }; + } + + } + } + return %players; +} + +sub getServerData +{ + my ($self) = @_; + my $status = $self->execute("status", 1); + + my $server_object = $self->{server_object}; + my $game = $server_object->{play_game}; + + my @lines = split(/[\r\n]+/, $status); + + my $servhostname = ""; + my $map = ""; + my $max_players = 0; + my $difficulty = 0; + + foreach my $line (@lines) + { + if ($line =~ /^\s*hostname\s*:\s*([\S].*)$/) + { + $servhostname = $1; + } + elsif ($line =~ /^\s*map\s*:\s*([\S]+).*$/) + { + $map = $1; + } + elsif ($line =~ /^\s*players\s*:\s*\d+.+\((\d+)\smax.*$/) + { + $max_players = $1; + } + } + if ($game == L4D()) { + $difficulty = $self->getDifficulty(); + } + return ($servhostname, $map, $max_players, $difficulty); +} + + +sub getVisiblePlayers +{ + my ($self) = @_; + my $status = $self->execute("sv_visiblemaxplayers"); + + my @lines = split(/[\r\n]+/, $status); + + + my $max_players = -1; + foreach my $line (@lines) + { + # "sv_visiblemaxplayers" = "-1" + # - Overrides the max players reported to prospective clients + if ($line =~ /^\s*"sv_visiblemaxplayers"\s*=\s*"([-0-9]+)".*$/x) + { + $max_players = $1; + } + } + return ($max_players); +} + +my %l4d_difficulties = ( + 'Easy' => 1, + 'Normal' => 2, + 'Hard' => 3, + 'Impossible' => 4 +); + +sub getDifficulty +{ + #z_difficulty + #"z_difficulty" = "Normal" + # game replicated + # - Difficulty of the current game (Easy, Normal, Hard, Impossible) + + my ($self) = @_; + my $zdifficulty = $self->execute("z_difficulty"); + + my @lines = split(/[\r\n]+/, $zdifficulty); + + foreach my $line (@lines) + { + if ($line =~ /^\s*"z_difficulty"\s*=\s*"([A-Za-z]+)".*$/x) + { + if (exists($l4d_difficulties{$1})) + { + return $l4d_difficulties{$1}; + } + } + } + return 0; +} + + +# +# Get information about a player by userID +# + +sub getPlayer +{ + my ($self, $uniqueid) = @_; + my %players = $self->getPlayers(); + + if (defined($players{$uniqueid})) + { + return $players{$uniqueid}; + } + else + { + $self->{"error"} = "No such player # $uniqueid"; + return 0; + } +} + +1; +# end diff --git a/scripts/hlstats-awards.pl b/scripts/hlstats-awards.pl new file mode 100644 index 0000000..1f48d4b --- /dev/null +++ b/scripts/hlstats-awards.pl @@ -0,0 +1,1282 @@ +#!/usr/bin/perl +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + + +## +## Settings +## + +# $opt_configfile - Absolute path and filename of configuration file. +my $opt_configfile = "./hlstats.conf"; + +# $opt_libdir - Directory to look in for local required files +# (our *.plib, *.pm files). +my $opt_libdir = "./"; + + +## +## +################################################################################ +## No need to edit below this line +## + + +use Getopt::Long; +use DBI; +use Encode; + +require "$opt_libdir/ConfigReaderSimple.pm"; +do "$opt_libdir/HLstats.plib"; + +$|=1; +Getopt::Long::Configure ("bundling"); + + +binmode STDIN, ":utf8"; +binmode STDOUT, ":utf8"; + +## +## MAIN +## + +# Options + +my $opt_help = 0; +my $opt_version = 0; +my $opt_numdays = 1; +my $opt_player_activity = 0; +my $opt_awards = 0; +my $opt_ribbons = 0; +my $opt_geoip = 0; +my $opt_clans = 0; +my $opt_prune = 0; +my $opt_optimize = 0; +my $opt_verbose = 0; +our $opt_cpanelhack = 0; + +our $db_host = "localhost"; +our $db_user = ""; +our $db_pass = ""; +our $db_name = "hlstats"; + +my $date_ubase=""; +my $date_base="CURRENT_DATE()"; + + +my $usage = <new($opt_configfile); + $conf->parse(); + &doConf($conf, %conf_directives); +} +else +{ + print "-- Warning: unable to open configuration file $opt_configfile\n"; +} + +# Read Command Line Arguments + +GetOptions( + "help|h" => \$opt_help, + "version|v" => \$opt_version, + "numdays=i" => \$opt_numdays, + "date=s" => \$date_ubase, + "inactive|i" => \$opt_player_activity, + "awards|a" => \$opt_awards, + "ribbons|r" => \$opt_ribbons, + "geoip|g" => \$opt_geoip, + "clans|t" => \$opt_clans, + "prune|p" => \$opt_prune, + "optimize|o" => \$opt_optimize, + "db-host=s" => \$db_host, + "db-name=s" => \$db_name, + "db-password=s" => \$db_pass, + "db-username=s" => \$db_user, + "configfile|c=s" => \$configfile, + "verbose" => \$opt_verbose +) or die($usage); + +if ($opt_help) +{ + print $usage; + exit(0); +} + +if ($configfile && -r $configfile) { + $conf = ''; + $conf = ConfigReaderSimple->new($configfile); + $conf->parse(); + &doConf($conf, %conf_directives); +} + +print "-- Connecting to MySQL database '$db_name' on '$db_host' as user '$db_user' ... "; + +&doConnect; + +print "connected OK\n"; + +$result = &doQuery(" + SELECT + value + FROM + hlstats_Options + WHERE + keyname='version' +"); + +if ($result->rows > 0) { + $g_version = $result->fetchrow_array; +} + +if ($opt_version) +{ + print "\nhlstats-awards.pl (HLX:CE Awards Script) Version $g_version\n" + . "Real-time player and clan rankings and statistics for Half-Life\n\n" + . "Copyright (C) 2001 Simon Garner\n" + . "Modified & Enhanced in 2005 by Tobias Oetzel (Tobi@gameme.de)\n\n"; + + print "\nThis is free software; see the source for copying conditions. There is NO\n" + . "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\n"; + + exit(0); +} + +if ($date_ubase) +{ + $date_base = "'" . $date_ubase . "'"; +} + +if (0 == ($opt_player_activity + $opt_awards + $opt_ribbons + $opt_geoip + $opt_clans + $opt_prune + $opt_optimize)) +{ + $opt_player_activity = 1; + $opt_awards = 1; + $opt_ribbons = 1; + $opt_prune = 1; +} + +# Startup + +print "++ HLstatsX:CE Awards & Maintenance script version $g_version starting...\n\n"; + +DoPruning() if ($opt_prune); +DoOptimize() if ($opt_optimize); +DoInactive() if ($opt_player_activity); +DoAwards() if ($opt_awards); +DoRibbons() if ($opt_ribbons); +DoGeoIP() if ($opt_geoip); +DoClans() if ($opt_clans); + + +print "\n++ HLstatsX:CE Awards & Maintenance script finished.\n\n"; + +sub DoInactive +{ + print "++ Updating player activity... "; + $g_minactivity = 2419200; + # Inactive Players + my $result = &doQuery(" + SELECT + value + FROM + hlstats_Options + WHERE + keyname = 'MinActivity' + "); + + if ($result->rows > 0) { + my ($tempminact) = $result->fetchrow_array; + $g_minactivity = $tempminact * 86400; + } + + if ($g_minactivity > 0) + { + $g_timestamp = 0; + + $result = &doQuery(" + SELECT + value + FROM + hlstats_Options + WHERE + keyname = 'UseTimestamp' + "); + + + if ($result->rows > 0) { + ($g_timestamp) = $result->fetchrow_array; + } + + %last_events = (); + + if ($g_timestamp > 0) + { + $result = &doQuery(" + SELECT + game, + MAX(last_event) + FROM + hlstats_Servers + GROUP BY + game + "); + my %last_events = (); + + + while ( my($game, $last) = $result->fetchrow_array) { + $last_events{$game} = $last + } + while ( my($game, $last) = each(%last_events)) + { + &execNonQuery(" + UPDATE + hlstats_Players + SET + hlstats_Players.activity = IF(($g_minactivity > $last - hlstats_Players.last_event), ((100 / $g_minactivity) * ($g_minactivity - ($last - hlstats_Players.last_event))), -1) + WHERE + hlstats_Players.game = '"."eSQL($game)."' + "); + } + } + else + { + &execNonQuery(" + UPDATE + hlstats_Players + SET + hlstats_Players.activity = IF(($g_minactivity > UNIX_TIMESTAMP() - hlstats_Players.last_event), ((100 / $g_minactivity) * ($g_minactivity - (UNIX_TIMESTAMP() - hlstats_Players.last_event))), -1) + "); + } + + &execNonQuery(" + UPDATE + hlstats_Players + SET + hideranking = 3 + WHERE + hideranking = 0 + AND activity < 0 + "); + } + + print "done\n"; +} + +sub DoAwards +{ + print "++ Processing awards... "; + + my $resultAwards = &doQuery(" + SELECT + hlstats_Awards.awardId, + hlstats_Awards.game, + hlstats_Awards.awardType, + hlstats_Awards.code + FROM + hlstats_Awards + LEFT JOIN hlstats_Games ON + hlstats_Games.code = hlstats_Awards.game + WHERE + hlstats_Games.hidden='0' + ORDER BY + hlstats_Awards.game, + hlstats_Awards.awardType + "); + + my $result = &doQuery(" + SELECT + value, + DATE_SUB($date_base, INTERVAL 1 DAY) + FROM + hlstats_Options + WHERE + keyname = 'awards_d_date' + "); + + if ($result->rows > 0) + { + ($awards_d_date, $awards_d_date_new) = $result->fetchrow_array; + + &execNonQuery(" + UPDATE + hlstats_Options + SET + value='$awards_d_date_new' + WHERE + keyname='awards_d_date' + "); + + print "(generating awards for $awards_d_date_new (previous: $awards_d_date))... "; + } + else + { + &execNonQuery(" + INSERT INTO + hlstats_Options + ( + keyname, + value, + opttype + ) + VALUES + ( + 'awards_d_date', + DATE_SUB($date_base, INTERVAL 1 DAY), + 2 + ) + "); + } + + &execNonQuery(" + REPLACE INTO + hlstats_Options + ( + keyname, + value, + opttype + ) + VALUES + ( + 'awards_numdays', + $opt_numdays, + 2 + ) + "); + + while( ($awardId, $game, $awardType, $code) = $resultAwards->fetchrow_array ) + { + + if ($awardType eq "O") + { + $table = "hlstats_Events_PlayerActions"; + $join = "LEFT JOIN hlstats_Actions ON hlstats_Actions.id = $table.actionId"; + $matchfield = "hlstats_Actions.code"; + $playerfield = "$table.playerId"; + } + elsif ($awardType eq "W") + { + $table = "hlstats_Events_Frags"; + $playerfield = "$table.killerId"; + if ($code eq "headshot") { + $join = ""; + $matchfield = "$table.headshot"; + $code = 1; + } else { + $join = ""; + $matchfield = "$table.weapon"; + } + } + elsif ($awardType eq "P") + { + $table = "hlstats_Events_PlayerPlayerActions"; + $join = "LEFT JOIN hlstats_Actions ON hlstats_Actions.id = $table.actionId"; + $matchfield = "hlstats_Actions.code"; + $playerfield = "$table.playerId"; + } + elsif ($awardType eq "V") + { + $table = "hlstats_Events_PlayerPlayerActions"; + $join = "LEFT JOIN hlstats_Actions ON hlstats_Actions.id = $table.actionId"; + $matchfield = "hlstats_Actions.code"; + $playerfield = "$table.victimId"; + } + + if ($code eq "latency") { + $resultDaily = &doQuery(" + SELECT + hlstats_Events_Latency.playerId, + ROUND(ROUND(SUM(ping) / COUNT(ping), 0) / 2, 0) AS av_latency + FROM + hlstats_Events_Latency + INNER JOIN + hlstats_Servers ON + hlstats_Servers.serverId=hlstats_Events_Latency.serverId + AND hlstats_Servers.game='"."eSQL($game)."' + INNER JOIN + hlstats_Players ON + hlstats_Players.playerId = hlstats_Events_Latency.playerId + AND hlstats_Players.hideranking=0 + WHERE + hlstats_Events_Latency.eventTime < $date_base + AND hlstats_Events_Latency.eventTime > DATE_SUB($date_base, INTERVAL $opt_numdays DAY) + GROUP BY + hlstats_Events_Latency.playerId + ORDER BY + av_latency + LIMIT 1 + "); + $resultGlobal = &doQuery(" + SELECT + hlstats_Events_Latency.playerId, + ROUND(ROUND(SUM(ping) / COUNT(ping), 0) / 2, 0) AS av_latency + FROM + hlstats_Events_Latency + INNER JOIN + hlstats_Servers ON + hlstats_Servers.serverId=hlstats_Events_Latency.serverId + AND hlstats_Servers.game='"."eSQL($game)."' + INNER JOIN + hlstats_Players ON + hlstats_Players.playerId = hlstats_Events_Latency.playerId + AND hlstats_Players.hideranking=0 + GROUP BY + hlstats_Events_Latency.playerId + ORDER BY + av_latency + LIMIT 1 + "); + } elsif ($code eq "mostkills") { + $resultDaily = &doQuery(" + SELECT + hlstats_Players_History.playerId, + hlstats_Players_History.kills + FROM + hlstats_Players_History, + hlstats_Players + WHERE + hlstats_Players_History.game='"."eSQL($game)."' + AND hlstats_Players.playerId = hlstats_Players_History.playerId + AND hlstats_Players.hideranking=0 + AND eventTime = DATE_SUB($date_base, INTERVAL $opt_numdays DAY) + ORDER BY + kills DESC + LIMIT 1 + "); + $resultGlobal = &doQuery(" + SELECT + playerId, + kills + FROM + hlstats_Players + WHERE + hlstats_Players.game='"."eSQL($game)."' + AND hlstats_Players.hideranking=0 + ORDER BY + kills DESC + LIMIT 1 + "); + } + elsif ($code eq "suicide") { + $resultDaily = &doQuery(" + SELECT + hlstats_Players_History.playerId, + hlstats_Players_History.suicides + FROM + hlstats_Players_History, + hlstats_Players + WHERE + hlstats_Players_History.game='"."eSQL($game)."' + AND hlstats_Players.playerId = hlstats_Players_History.playerId + AND hlstats_Players.hideranking=0 + AND eventTime = DATE_SUB($date_base, INTERVAL $opt_numdays DAY) + ORDER BY + suicides DESC + LIMIT 1 + "); + $resultGlobal = &doQuery(" + SELECT + playerId, + suicides + FROM + hlstats_Players + WHERE + hlstats_Players.game='"."eSQL($game)."' + AND hlstats_Players.hideranking=0 + ORDER BY + suicides DESC + LIMIT 1 + "); + } elsif ($code eq "teamkills") { + $resultDaily = &doQuery(" + SELECT + hlstats_Players_History.playerId, + hlstats_Players_History.teamkills + FROM + hlstats_Players_History, + hlstats_Players + WHERE + hlstats_Players_History.game='"."eSQL($game)."' + AND hlstats_Players.playerId = hlstats_Players_History.playerId + AND hlstats_Players.hideranking=0 + AND eventTime = DATE_SUB($date_base, INTERVAL $opt_numdays DAY) + ORDER BY + teamkills DESC + LIMIT 1 + "); + $resultGlobal = &doQuery(" + SELECT + playerId, + teamkills + FROM + hlstats_Players + WHERE + hlstats_Players.game='"."eSQL($game)."' + AND hlstats_Players.hideranking=0 + ORDER BY + teamkills DESC + LIMIT 1 + "); + } elsif ($code eq "bonuspoints") { + $resultDaily = &doQuery(" + SELECT + actions.playerId, + SUM(actions.bonus) AS av_bonuspoints + FROM + (SELECT + playerId, bonus, serverId, eventTime + FROM + hlstats_Events_PlayerActions + WHERE + eventTime < $date_base AND eventTime > DATE_SUB($date_base, INTERVAL $opt_numdays DAY) + UNION ALL + SELECT + playerId, bonus, serverId, eventTime + FROM + hlstats_Events_PlayerPlayerActions + WHERE + eventTime < $date_base AND eventTime > DATE_SUB($date_base, INTERVAL $opt_numdays DAY) + ) actions + INNER JOIN + hlstats_Servers ON + hlstats_Servers.serverId=actions.serverId + AND hlstats_Servers.game='"."eSQL($game)."' + INNER JOIN + hlstats_Players ON + hlstats_Players.playerId = actions.playerId + AND hlstats_Players.hideranking=0 + GROUP BY + playerId + ORDER BY + av_bonuspoints DESC + LIMIT 1 + "); + $resultGlobal = &doQuery(" + SELECT + actions.playerId, + SUM(actions.bonus) AS av_bonuspoints + FROM + (SELECT + playerId, bonus, serverId, eventTime + FROM + hlstats_Events_PlayerActions + UNION ALL + SELECT + playerId, bonus, serverId, eventTime + FROM + hlstats_Events_PlayerPlayerActions + ) actions + INNER JOIN + hlstats_Servers ON + hlstats_Servers.serverId=actions.serverId + AND hlstats_Servers.game='"."eSQL($game)."' + INNER JOIN + hlstats_Players ON + hlstats_Players.playerId = actions.playerId + AND hlstats_Players.hideranking=0 + GROUP BY + playerId + ORDER BY + av_bonuspoints DESC + LIMIT 1 + "); + } elsif ($code eq "allsentrykills") { + $resultDaily = &doQuery(" + SELECT + hlstats_Events_Frags.killerId, + COUNT(hlstats_Events_Frags.weapon) AS awardcount + FROM + hlstats_Events_Frags + INNER JOIN hlstats_Players ON + hlstats_Players.playerId = hlstats_Events_Frags.killerId + AND hlstats_Players.hideranking=0 + WHERE + hlstats_Events_Frags.eventTime < $date_base + AND hlstats_Events_Frags.eventTime > DATE_SUB($date_base, INTERVAL $opt_numdays DAY) + AND hlstats_Players.game='"."eSQL($game)."' + AND hlstats_Events_Frags.weapon LIKE 'obj_sentrygun%' + GROUP BY + hlstats_Events_Frags.killerId + ORDER BY + awardcount DESC, + hlstats_Players.skill DESC + LIMIT 1 + "); + $resultGlobal = &doQuery(" + SELECT + hlstats_Events_Frags.killerId, + COUNT(hlstats_Events_Frags.weapon) AS awardcount + FROM + hlstats_Events_Frags + INNER JOIN hlstats_Players ON + hlstats_Players.playerId = hlstats_Events_Frags.killerId + AND hlstats_Players.hideranking=0 + WHERE + hlstats_Players.game='"."eSQL($game)."' + AND hlstats_Events_Frags.weapon LIKE 'obj_sentrygun%' + GROUP BY + hlstats_Events_Frags.killerId + ORDER BY + awardcount DESC, + hlstats_Players.skill DESC + LIMIT 1 + "); + } elsif ($code eq "connectiontime") { + $resultDaily = &doQuery(" + SELECT + hlstats_Players_History.playerId, + hlstats_Players_History.connection_time + FROM + hlstats_Players_History, + hlstats_Players + WHERE + hlstats_Players_History.game='"."eSQL($game)."' + AND hlstats_Players.playerId = hlstats_Players_History.playerId + AND hlstats_Players.hideranking=0 + AND eventTime = DATE_SUB($date_base, INTERVAL $opt_numdays DAY) + ORDER BY + connection_time DESC + LIMIT 1 + "); + $resultGlobal = &doQuery(" + SELECT + playerId, + connection_time + FROM + hlstats_Players + WHERE + hlstats_Players.game='"."eSQL($game)."' + AND hlstats_Players.hideranking=0 + ORDER BY + connection_time DESC + LIMIT 1 + "); + } elsif ($code eq "killstreak") { + $resultDaily = &doQuery(" + SELECT + hlstats_Players_History.playerId, + hlstats_Players_History.kill_streak + FROM + hlstats_Players_History, + hlstats_Players + WHERE + hlstats_Players_History.game='"."eSQL($game)."' + AND hlstats_Players.playerId = hlstats_Players_History.playerId + AND hlstats_Players.hideranking=0 + AND eventTime = DATE_SUB($date_base, INTERVAL $opt_numdays DAY) + ORDER BY + kill_streak DESC + LIMIT 1 + "); + $resultGlobal = &doQuery(" + SELECT + playerId, + kill_streak + FROM + hlstats_Players + WHERE + hlstats_Players.game='"."eSQL($game)."' + AND hlstats_Players.hideranking=0 + ORDER BY + kill_streak DESC + LIMIT 1 + "); + } elsif ($code eq "deathstreak") { + print "in deathstreak"; + $resultDaily = &doQuery(" + SELECT + hlstats_Players_History.playerId, + hlstats_Players_History.death_streak + FROM + hlstats_Players_History, + hlstats_Players + WHERE + hlstats_Players_History.game='"."eSQL($game)."' + AND hlstats_Players.playerId = hlstats_Players_History.playerId + AND hlstats_Players.hideranking=0 + AND eventTime = DATE_SUB($date_base, INTERVAL $opt_numdays DAY) + ORDER BY + death_streak DESC + LIMIT 1 + "); + $resultGlobal = &doQuery(" + SELECT + playerId, + death_streak + FROM + hlstats_Players + WHERE + hlstats_Players.game='"."eSQL($game)."' + AND hlstats_Players.hideranking=0 + ORDER BY + death_streak DESC + LIMIT 1 + "); + } else { + $resultDaily = &doQuery(" + SELECT + $playerfield, + COUNT($matchfield) AS awardcount + FROM + $table + INNER JOIN hlstats_Players ON + hlstats_Players.playerId = $playerfield + AND hlstats_Players.hideranking=0 + $join + WHERE + $table.eventTime < $date_base + AND $table.eventTime > DATE_SUB($date_base, INTERVAL $opt_numdays DAY) + AND hlstats_Players.game='"."eSQL($game)."' + AND $matchfield='$code' + GROUP BY + $playerfield + ORDER BY + awardcount DESC, + hlstats_Players.skill DESC + LIMIT 1 + "); + $resultGlobal = &doQuery(" + SELECT + $playerfield, + COUNT($matchfield) AS awardcount + FROM + $table + INNER JOIN hlstats_Players ON + hlstats_Players.playerId = $playerfield + AND hlstats_Players.hideranking=0 + $join + WHERE + hlstats_Players.game='"."eSQL($game)."' + AND $matchfield='$code' + GROUP BY + $playerfield + ORDER BY + awardcount DESC, + hlstats_Players.skill DESC + LIMIT 1 + "); + } + + ($d_winner_id, $d_winner_count) = $resultDaily->fetchrow_array; + ($g_winner_id, $g_winner_count) = $resultGlobal->fetchrow_array; + + if (!$d_winner_id || $d_winner_count < 1) + { + $d_winner_id = "NULL"; + $d_winner_count = "NULL"; + } + if (!$g_winner_id || $g_winner_count < 1) + { + $g_winner_id = "NULL"; + $g_winner_count = "NULL"; + } + + if ($opt_verbose) + { + print " - $d_winner_id ($d_winner_count)\n"; + print " - $g_winner_id ($g_winner_count)\n"; + } + + &execNonQuery(" + UPDATE + hlstats_Awards + SET + d_winner_id=$d_winner_id, + d_winner_count=$d_winner_count, + g_winner_id=$g_winner_id, + g_winner_count=$g_winner_count + WHERE + awardId=$awardId + "); + } + + + &execNonQuery(" + INSERT IGNORE INTO + hlstats_Players_Awards + SELECT + value, awardId, d_winner_id, d_winner_count, game + FROM + hlstats_Options INNER JOIN hlstats_Awards + WHERE + keyname='awards_d_date' AND NOT ISNULL(d_winner_id); + "); + + print "done\n"; +} + +sub DoRibbons +{ + print "++ Processing ribbons... "; + + my $result = &doQuery("SELECT `code` FROM `hlstats_Games`;"); + while( my($game) = $result->fetchrow_array ) { + + &execNonQuery("DELETE FROM hlstats_Players_Ribbons WHERE game='"."eSQL($game)."';"); + + $result2 = &doQuery(" + SELECT + `ribbonId`, + `awardCode`, + `awardCount`, + `special` + FROM + `hlstats_Ribbons` + WHERE + game='"."eSQL($game)."' AND + (special=0 OR special=2); + "); + while ( my($ribbonid, $code, $count, $special) = $result2->fetchrow_array ) { + # scan players for each ribbon ID + if ($special==2) { + # connection time + $result3 = &doQuery(" + SELECT + playerId, + (connection_time/3600) AS CNT + FROM + hlstats_Players + WHERE + game='"."eSQL($game)."' + AND hlstats_Players.hideranking=0 + AND (connection_time/3600)>=".$count." + "); + } else { + # awards ribbons + $having = "CNT>=".$count; + $result3 = &doQuery(" + SELECT + hlstats_Players_Awards.playerId, + COUNT(hlstats_Players_Awards.playerId) AS CNT + FROM + hlstats_Players_Awards + INNER JOIN + hlstats_Awards + ON + (hlstats_Awards.awardId=hlstats_Players_Awards.awardId AND + hlstats_Awards.game=hlstats_Players_Awards.game) + INNER JOIN + hlstats_Players + ON + hlstats_Players.playerId = hlstats_Players_Awards.playerId + AND hlstats_Players.hideranking=0 + WHERE + hlstats_Players_Awards.game='"."eSQL($game)."' AND + hlstats_Awards.code='".$code."' AND + hlstats_Awards.awardType<>'V' + GROUP BY + hlstats_Players_Awards.playerId + HAVING + ".$having." + "); + } + + while (my($playerid, $cnt) = $result3->fetchrow_array) { + &execNonQuery(" + INSERT INTO hlstats_Players_Ribbons + (playerId, ribbonId, game) + VALUES + (".$playerid.",".$ribbonid.",'"."eSQL($game)."') + "); + } + } + + } + print "done\n"; +} + +sub DoGeoIP +{ + print "++ Looking up missing player locations... "; + + my $useGeoIPBinary = 0; + my $gi = undef; + my $dogeo = 0; + my $cnt = 0; + + # Sanity checks to see if we can do geolocation updates + $result = &doQuery(" + SELECT + value + FROM + hlstats_Options + WHERE + keyname='UseGeoIPBinary' + AND value > '0' + LIMIT 1 + "); + + if ($result->rows > 0) + { + $useGeoIPBinary = 1; + $geoipfile = "$opt_libdir/GeoLiteCity/GeoLiteCity.dat"; + } + else + { + $useGeoIPBinary = 0; + } + + if ($useGeoIPBinary == 0) + { + my $result = &doQuery("SELECT locId FROM geoLiteCity_Blocks LIMIT 1;"); + if ($result->rows > 0) + { + $dogeo = 1; + } + else + { + &printEvent("ERROR", "GeoIP method set to database but geoLiteCity tables are empty.", 1); + } + } + elsif ($useGeoIPBinary == 1 && -r $geoipfile) + { + if ($opt_cpanelhack) { + my $home_dir = $ENV{ HOME }; + my $base_module_dir = (-d "$home_dir/perl" ? "$home_dir/perl" : ( getpwuid($>) )[7] . '/perl/'); + unshift @INC, map { $base_module_dir . $_ } @INC; + } + + eval { + require Geo::IP::PurePerl; + }; + import Geo::IP::PurePerl; + + $gi = Geo::IP::PurePerl->open($geoipfile, "GEOIP_STANDARD"); + if ($gi) + { + $dogeo = 1; + } + else + { + &printEvent("ERROR", "GeoIP method set to binary file lookup but $geoipfile errored while opening.", 1); + close($gi->{fh}); + } + } + else + { + &printEvent("ERROR", "GeoIP method set to binary file lookup but $geoipfile NOT FOUND", 1); + } + + + if ($dogeo) { + sub ip2number { + my ($ipstr) = @_; + my @ip = split(/\./, $ipstr); + my $number = ($ip[0]*16777216) + ($ip[1]*65536) + ($ip[2]*256) + $ip[3]; + + return $number; + } + + sub trim { + my $string = shift; + $string =~ s/^\s+|\s+$//g; + return $string; + } + $result = &doQuery("SELECT playerId, lastAddress, lastName FROM hlstats_Players WHERE flag='' AND lastAddress<>'';"); + + while (my($pid, $address, $name) = $result->fetchrow_array) { + $address = trim($address); + next if ($address !~ /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/); + if ($opt_verbose) + { + print "Attempting to find location for: ".$name." (".$address.")\n"; + } + my $number = ip2number($address); + my $update = 0; + my $foundflag = ""; + my $foundcountry = ""; + my $foundcity = ""; + my $foundstate = ""; + my $foundlat = 0; + my $foundlng = 0; + if ($useGeoIPBinary > 0) { + if ($opt_verbose) + { + print "2 ".$pid." ".$address."\n"; + } + my ($country_code, $country_code3, $country_name, $region, $city, $postal_code, $latitude, $longitude, $metro_code, $area_code) = $gi->get_city_record($address); + if ($longitude) { + $foundflag = encode("utf8",$country_code); + $foundcountry = encode("utf8",$country_name); + $foundcity = encode("utf8",$city); + $foundstate = encode("utf8",$region); + $foundlat = $latitude; + $foundlng = $longitude; + $update++; + } + } + else + { + $result2 = &doQuery("SELECT locId FROM geoLiteCity_Blocks WHERE startIpNum<=".$number." AND endIpNum>=".$number." LIMIT 1;"); + if ($result2->rows > 0) { + my ($locid) = $result2->fetchrow_array; + $data = &doQuery("SELECT city, region AS state, name AS country, country AS flag, latitude AS lat, longitude AS lng FROM geoLiteCity_Location a inner join hlstats_Countries b ON a.country=b.flag WHERE locId=".$locid." LIMIT 1;"); + if ($data->rows > 0) { + ($foundcity, $foundstate, $foundcountry, $foundflag, $foundlat, $foundlng) = $data->fetchrow_array; + $update++; + } + } + } + if ($update > 0) + { + &execNonQuery(" + UPDATE + hlstats_Players + SET + flag='"."eSQL($foundflag)."', + country='"."eSQL($foundcountry)."', + lat='".(($foundlat ne "")?$foundlat:undef)."', + lng='".(($foundlng ne "")?$foundlng:undef)."', + city='"."eSQL($foundcity)."', + state='"."eSQL($foundstate)."' + WHERE + playerId=".$pid + ); + $cnt++; + } + } + } + printf ("done%s\n", (($cnt>0)?" (updated $cnt players)":"")); +} + +sub DoClans +{ + print "++ Reparsing player names to recalculate clan affiliations... "; + + my @clanpatterns = (); + my $result = &doQuery(" + SELECT + pattern, + position, + LENGTH(pattern) AS pattern_length + FROM + hlstats_ClanTags + ORDER BY + pattern_length DESC, + id + "); + + while ( my($pattern, $position) = $result->fetchrow_array) { + my $regpattern = quotemeta($pattern); + $regpattern =~ s/([A-Za-z0-9]+[A-Za-z0-9_-]*)/\($1\)/; # to find clan name from tag + $regpattern =~ s/A/./g; + $regpattern =~ s/X/.?/g; + if ($position eq "START") { + push(@clanpatterns, "^($regpattern).+"); + } elsif ($position eq "END") { + push(@clanpatterns, ".+($regpattern)\$"); + } elsif ($position eq "EITHER") { + push(@clanpatterns, "^($regpattern).+"); + push(@clanpatterns, ".+($regpattern)\$"); + } + } + + $result = &doQuery(" + SELECT + playerId, lastName, game + FROM + hlstats_Players + "); + + while ( my($playerId, $name, $game) = $result->fetchrow_array) + { + my $clanTag = ""; + my $clanId = 0; + foreach (@clanpatterns) + { + $clanTag = ""; + if ($name =~ /$_/i) + { + $clanTag = $1; + $clanName = $2; + last; + } + } + if (!$clanTag) + { + &execCached("playerclan_clear", "UPDATE hlstats_Players SET clan=0 WHERE playerId=?", $playerId); + next; + } + + my $query = " + SELECT + clanId + FROM + hlstats_Clans + WHERE + tag=? AND + game=? + "; + my $clanresult = &execCached("clan_select", $query, $clanTag, $game); + + if ($clanresult->rows) { + my ($id) = $clanresult->fetchrow_array; + $clanresult->finish; + $clanId = $id; + } else { + # The clan doesn't exist yet, so we create it. + $query = " + REPLACE INTO + hlstats_Clans + ( + tag, + name, + game + ) + VALUES + ( + ?,?,? + ) + "; + &execCached("clan_insertupdate", $query, $clanTag, $clanName, $game); + + $clanId = $db_conn->{'mysql_insertid'}; + } + &execCached("playerclan_update", "UPDATE hlstats_Players SET clan=? WHERE playerId=?", $clanId, $playerId); + } + + print "done\n"; +} + +sub DoPruning +{ + $result = &doQuery("SELECT `value` FROM hlstats_Options WHERE keyname='DeleteDays'"); + my ($g_deletedays) = $result->fetchrow_array; + + print "++ Cleaning up database: deleting events older than $g_deletedays days... "; + + foreach $eventTable (keys(%g_eventTables)) + { + &execNonQuery(" + DELETE FROM + hlstats_Events_$eventTable + WHERE + eventTime < DATE_SUB(CURRENT_TIMESTAMP(), INTERVAL $g_deletedays DAY) + "); + } + + print "done\n++ Cleaning up database: deleting player history older than $g_deletedays days... "; + &execNonQuery(" + DELETE FROM + hlstats_Players_History + WHERE + eventTime < DATE_SUB(CURRENT_TIMESTAMP(), INTERVAL $g_deletedays DAY) + "); + + print "done\n++ Cleaning up database: deleting stale trend samples... "; + &execNonQuery(" + DELETE FROM + hlstats_Trend + WHERE + timestamp < (UNIX_TIMESTAMP() - 172800) + "); + + print "done\n++ Cleaning up database: deleting server load history older than one year... "; + &execNonQuery(" + DELETE FROM + hlstats_server_load + WHERE + timestamp < (UNIX_TIMESTAMP(CURRENT_TIMESTAMP() - INTERVAL 1 YEAR)) + "); + + print "done\n"; +} + +sub DoOptimize +{ + print "++ Optimizing all tables... "; + + $result = &doQuery("SHOW TABLES"); + while ( ($row) = $result->fetchrow_array ) { + push(@g_allTables, $row); + } + foreach $table (@g_allTables) { + &execNonQuery(" + OPTIMIZE TABLE $table + "); + } + print "done\n"; +} diff --git a/scripts/hlstats-resolve.pl b/scripts/hlstats-resolve.pl new file mode 100644 index 0000000..5dee39c --- /dev/null +++ b/scripts/hlstats-resolve.pl @@ -0,0 +1,374 @@ +#!/usr/bin/perl +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + + +## +## Settings +## + +# $opt_configfile - Absolute path and filename of configuration file. +$opt_configfile = "./hlstats.conf"; + +# $opt_libdir - Directory to look in for local required files +# (our *.plib, *.pm files). +$opt_libdir = "./"; + + +## +## +################################################################################ +## No need to edit below this line +## + + +use Getopt::Long; +use IO::Socket; +use DBI; + +require "$opt_libdir/ConfigReaderSimple.pm"; +do "$opt_libdir/HLstats.plib"; + +$|=1; +Getopt::Long::Configure ("bundling"); + + +## +## Functions +## + + +sub is_number ($) { ( $_[0] ^ $_[0] ) eq '0' } + + +# +# void printEvent (int code, string description) +# +# Logs event information to stdout. +# + +sub printEvent +{ + my ($code, $description, $update_timestamp) = @_; + + if ($g_debug > 0) + { + if ($update_timestamp > 0) + { + my ($sec,$min,$hour,$mday,$mon,$year) = localtime(time()); + my $timestamp = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec); + } else { + my $timestamp = $ev_timestamp; + } + print localtime(time) . "" unless ($timestamp); + if (is_number($code)) + { + printf("%s: %21s - E%03d: %s\n", $timestamp, $s_addr, $code, $description); + } else { + printf("%s: %21s - %s: %s\n", $timestamp, $s_addr, $code, $description); + } + } +} + + + +## +## MAIN +## + +# Options + +$opt_help = 0; +$opt_version = 0; +$opt_regroup = 0; + +$db_host = "localhost"; +$db_user = ""; +$db_pass = ""; +$db_name = "hlstats"; + +$g_dns_timeout = 5; +$g_debug = 0; + +# Usage message + +$usage = <new($opt_configfile); + $conf->parse(); + + %directives = ( + "DBHost", "db_host", + "DBUsername", "db_user", + "DBPassword", "db_pass", + "DBName", "db_name", + "DNSTimeout", "g_dns_timeout", + "DebugLevel", "g_debug" + ); + + &doConf($conf, %directives); +} +else +{ + print "-- Warning: unable to open configuration file '$opt_configfile'\n"; +} + +# Read Command Line Arguments + +GetOptions( + "help|h" => \$opt_help, + "version|v" => \$opt_version, + "debug|d+" => \$g_debug, + "nodebug|n+" => \$g_nodebug, + "db-host=s" => \$db_host, + "db-name=s" => \$db_name, + "db-password=s" => \$db_pass, + "db-username=s" => \$db_user, + "dns-timeout=i" => \$g_dns_timeout, + "regroup|r" => \$opt_regroup +) or die($usage); + +if ($opt_help) +{ + print $usage; + exit(0); +} + +if ($opt_version) +{ + print "hlstats-resolve.pl (HLstats) $g_version\n" + . "Real-time player and clan rankings and statistics for Half-Life\n\n" + . "Copyright (C) 2001 Simon Garner\n" + . "This is free software; see the source for copying conditions. There is NO\n" + . "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"; + exit(0); +} + +$g_debug -= $g_nodebug; +$g_debug = 0 if ($g_debug < 0); + +if ($g_debug >= 2) +{ + $opt_quiet = 0; +} +else +{ + $opt_quiet = 1; # quiet name resolution +} + +$g_dns_resolveip = 1; + + +# Startup + +print "++ HLstats Resolve $g_version starting...\n\n"; + +# Connect to the database + +print "-- Connecting to MySQL database '$db_name' on '$db_host' as user '$db_user' ... "; + +$db_conn = DBI->connect( + "DBI:mysql:$db_name:$db_host", + $db_user, $db_pass +) or die ("Can't connect to MySQL database '$db_name' on '$db_host'\n" . + "$DBI::errstr\n"); + +print "connected OK\n"; + +# Print configuration + +print "-- DNS timeout is $g_dns_timeout seconds. Debug level is $g_debug.\n"; + + +# Main data routine + +if ($opt_regroup) +{ + my $result = &doQuery(" + SELECT + id, + hostname + FROM + hlstats_Events_Connects + WHERE + hostname != '' + "); + + my $total = $result->rows; + + if ($total > 0) { + print "\n++ Re-grouping hosts (total $total hostnames) ... "; + + my $resultHG = &queryHostGroups(); + + if ($g_debug > 0) + { + print "\n\n"; + } + else + { + print " "; + } + + my $p = 1; + while( my($id, $hostname) = $result->fetchrow_array ) + { + my $percent = ($p / $total) * 100; + + my $hostgroup = &getHostGroup($hostname, $resultHG); + + &execNonQuery(" + UPDATE + hlstats_Events_Connects + SET + hostgroup='" . "eSQL($hostgroup) . "' + WHERE + id=$id + "); + + if ($g_debug > 0) + { + printf("-> (%3d%%) %50s = %s\n", $percent, $hostname, $hostgroup); + } + else + { + printf("\b\b\b\b%3d%%", $percent); + } + + $p++; + } + + print "\n" unless ($g_debug > 0); + } else { + print "\n++ No Connects found!\n"; + } +} +else +{ + my $result = &doQuery(" + SELECT + DISTINCT ipAddress, + hostname + FROM + hlstats_Events_Connects + "); + + my $total = $result->rows; + if ($total > 0) { + print "\n++ Resolving IPs and re-grouping hosts (total $total connects) ... "; + + my $resultHG = &queryHostGroups(); + + if ($g_debug > 0) + { + print "\n\n"; + } + else + { + print " "; + } + + my $p = 1; + while( my($ipAddress, $hostname) = $result->fetchrow_array ) + { + my $percent = ($p / $total) * 100; + + if ($hostname eq "") + { + $hostname = &resolveIp($ipAddress, $opt_quiet); + } + + my $hostgroup = &getHostGroup($hostname, $resultHG); + + &execNonQuery(" + UPDATE + hlstats_Events_Connects + SET + hostname='$hostname', + hostgroup='" . "eSQL($hostgroup) . "' + WHERE + ipAddress='$ipAddress' + "); + + if ($g_debug > 0) + { + printf("-> (%3d%%) %15s = %50s = %s\n", $percent, $ipAddress, $hostname, $hostgroup); + } + else + { + printf("\b\b\b\b%3d%%", $percent); + } + + $p++; + } + + print "\n" unless ($g_debug > 0); + } else { + print "\n++ No Connects found!\n"; + } +} + +print "\n++ Operation complete.\n"; +exit(0); diff --git a/scripts/hlstats.conf b/scripts/hlstats.conf new file mode 100644 index 0000000..6759f18 --- /dev/null +++ b/scripts/hlstats.conf @@ -0,0 +1,83 @@ +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + + +## +## Database Settings +## + +# DBHost - Database server "address" or "address:port". Address can be an IP or +# a hostname. The default MySQL port is 3306 (tcp). +DBHost "" + +# DBUsername - User to connect to the database as. +DBUsername "" + +# DBPassword - Password for the database user. +DBPassword "" + +# DBName - Name of the database to use. +DBName "" + + +## +## UDP Socket Settings (should match "logaddress ip port" on the game servers) +## + +# BindIP - IP address to bind to (leave empty to use all interfaces). +BindIP "" + +# Port - Port to listen on for log data from the game servers. this is also valid for proxy-daemon.pl +Port 27500 + +## +## Cpanel hack +## +## Set this to 1 if you use cpanel and need to use a user-installed Perl module +CpanelHack 0 + +## +## Event Queue +## +## Number of each type of events to queue before inserting events of that type +## (larger installs may try raising this for better performance +EventQueueSize 10 + +# DebugLevel - Set this to 1 to have debugging information printed on stdout. +# Set higher for even more debugging information. Set to 0 for +# quiet operation. It is recommended that you set this to 1 when +# first configuring HLstats, to help diagnose any problems. +DebugLevel 1 + diff --git a/scripts/hlstats.pl b/scripts/hlstats.pl new file mode 100644 index 0000000..0747740 --- /dev/null +++ b/scripts/hlstats.pl @@ -0,0 +1,3626 @@ +#!/usr/bin/perl +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + +use strict; +no strict 'vars'; + +$SIG{HUP} = 'HUP_handler'; +$SIG{INT} = 'INT_handler'; # unix +$SIG{INT2} = 'INT_handler'; # windows + +## +## Settings +## + +# $opt_configfile - Absolute path and filename of configuration file. +$opt_configfile = "./hlstats.conf"; + +# $opt_libdir - Directory to look in for local required files +# (our *.plib, *.pm files). +$opt_libdir = "./"; + + +## +## +################################################################################ +## No need to edit below this line +## + +use Getopt::Long; +use Time::Local; +use IO::Socket; +use IO::Select; +use DBI; +use Digest::MD5; +use Encode; +use bytes; + +require "$opt_libdir/ConfigReaderSimple.pm"; +require "$opt_libdir/TRcon.pm"; +require "$opt_libdir/BASTARDrcon.pm"; +require "$opt_libdir/HLstats_Server.pm"; +require "$opt_libdir/HLstats_Player.pm"; +require "$opt_libdir/HLstats_Game.pm"; +do "$opt_libdir/HLstats_GameConstants.plib"; +do "$opt_libdir/HLstats.plib"; +do "$opt_libdir/HLstats_EventHandlers.plib"; + +$|=1; +Getopt::Long::Configure ("bundling"); + +$last_trend_timestamp = 0; + +binmode STDIN, ":utf8"; +binmode STDOUT, ":utf8"; + +## +## Functions +## + +sub lookupPlayer +{ + my ($saddr, $id, $uniqueid) = @_; + if (defined($g_servers{$saddr}->{"srv_players"}->{"$id/$uniqueid"})) + { + return $g_servers{$saddr}->{"srv_players"}->{"$id/$uniqueid"}; + } + return undef; +} + +sub removePlayer +{ + my ($saddr, $id, $uniqueid, $dontUpdateCount) = @_; + my $deleteplayer = 0; + if(defined($g_servers{$saddr}->{"srv_players"}->{"$id/$uniqueid"})) + { + $deleteplayer = 1; + } + else + { + &::printEvent("400", "Bad attempted delete ($saddr) ($id/$uniqueid)"); + } + + if ($deleteplayer == 1) { + $g_servers{$saddr}->{"srv_players"}->{"$id/$uniqueid"}->playerCleanup(); + delete($g_servers{$saddr}->{"srv_players"}->{"$id/$uniqueid"}); + if (!$dontUpdateCount) # double negative, i know... + { + $g_servers{$saddr}->updatePlayerCount(); + } + } +} + +sub checkBonusRound +{ + if ($g_servers{$s_addr}->{bonusroundtime} > 0 && ($::ev_remotetime > ($g_servers{$s_addr}->{bonusroundtime_ts} + $g_servers{$s_addr}->{bonusroundtime}))) { + if ($g_servers{$s_addr}->{bonusroundtime_state} == 1) { + &printEvent("SERVER", "Bonus Round Expired",1); + } + $g_servers{$s_addr}->set("bonusroundtime_state",0); + } + + if($g_servers{$s_addr}->{bonusroundignore} == 1 && $g_servers{$s_addr}->{bonusroundtime_state} == 1) { + return 1; + } + return 0; +} + +sub is_number ($) { ( $_[0] ^ $_[0] ) eq '0' } + + +# +# void printNotice (string notice) +# +# Prins a debugging notice to stdout. +# + +sub printNotice +{ + my ($notice) = @_; + + if ($g_debug > 1) { + print ">> $notice\n"; + } +} + +sub track_hlstats_trend +{ + if ($last_trend_timestamp > 0) { + if ($last_trend_timestamp+299 < $ev_daemontime) { + my $query = " + SELECT + COUNT(*), + a.game + FROM + hlstats_Players a + INNER JOIN + ( + SELECT + game + FROM + hlstats_Servers + GROUP BY + game + ) AS b + ON + a.game = b.game + GROUP BY + a.game + "; + my $result = &execCached("get_total_player_counts", $query); + my $insvalues = ""; + while ( my($total_players, $game) = $result->fetchrow_array) { + my $query = " + SELECT + SUM(kills), + SUM(headshots), + COUNT(serverId), + SUM(act_players), + SUM(max_players) + FROM + hlstats_Servers + WHERE + game=? + "; + my $data = &execCached("get_game_stat_counts", $query, "eSQL($game)); + my ($total_kills, $total_headshots, $total_servers, $act_slots, $max_slots) = $data->fetchrow_array; + if ($max_slots > 0) { + if ($act_slots > $max_slots) { + $act_slots = $max_slots; + } + } + if ($insvalues ne "") { + $insvalues .= ","; + } + $insvalues .= " + ( + $ev_daemontime, + '"."eSQL($game)."', + $total_players, + $total_kills, + $total_headshots, + $total_servers, + $act_slots, + $max_slots + ) + "; + } + if ($insvalues ne "") { + &execNonQuery(" + INSERT INTO + hlstats_Trend + ( + timestamp, + game, + players, + kills, + headshots, + servers, + act_slots, + max_slots + ) + VALUES $insvalues + "); + } + $last_trend_timestamp = $ev_daemontime; + &::printEvent("HLSTATSX", "Insert new server trend timestamp", 1); + } + } else { + $last_trend_timestamp = $ev_daemontime; + } +} + +sub send_global_chat +{ + my ($message) = @_; + while( my($server) = each(%g_servers)) + { + if ($server ne $s_addr && $g_servers{$server}->{"srv_players"}) + { + my @userlist; + my %players_temp=%{$g_servers{$server}->{"srv_players"}}; + my $pcount = scalar keys %players_temp; + + if ($pcount > 0) { + while ( my($pl, $b_player) = each(%players_temp) ) { + my $b_userid = $b_player->{userid}; + if ($g_global_chat == 2) { + my $b_steamid = $b_player->{uniqueid}; + if ($g_servers{$server}->is_admin($b_steamid) == 1) { + if (($b_player->{display_events} == 1) && ($b_player->{display_chat} == 1)) { + push(@userlist, $b_player->{userid}); + } + } + } else { + if (($b_player->{display_events} == 1) && ($b_player->{display_chat} == 1)) { + push(@userlist, $b_player->{userid}); + } + } + } + $g_servers{$server}->messageMany($message, 0, @userlist); + } + } + } +} + +# +# void buildEventInsertData () +# +# Ran at startup to init event table queues, build initial queries, and set allowed-null columns +# + +my %g_eventtable_data = (); + +sub buildEventInsertData +{ + my $insertType = ""; + $insertType = " DELAYED" if ($db_lowpriority); + while ( my ($table, $colsref) = each(%g_eventTables) ) + { + $g_eventtable_data{$table}{queue} = []; + $g_eventtable_data{$table}{nullallowed} = 0; + $g_eventtable_data{$table}{lastflush} = $ev_daemontime; + $g_eventtable_data{$table}{query} = " + INSERT$insertType INTO + hlstats_Events_$table + ( + eventTime, + serverId, + map" + ; + my $j = 0; + foreach $i (@{$colsref}) + { + $g_eventtable_data{$table}{query} .= ",\n$i"; + if (substr($i, 0, 4) eq 'pos_') { + $g_eventtable_data{$table}{nullallowed} |= (1 << $j); + } + $j++; + } + $g_eventtable_data{$table}{query} .= ") VALUES\n"; + } +} + +# +# void recordEvent (string table, array cols, bool getid, [mixed eventData ...]) +# +# Queues an event for addition to an Events table, flushing when hitting table queue limit. +# + +sub recordEvent +{ + my $table = shift; + my $unused = shift; + my @coldata = @_; + + my $value = "(FROM_UNIXTIME($::ev_unixtime),".$g_servers{$s_addr}->{'id'}.",'".quoteSQL($g_servers{$s_addr}->get_map())."'"; + $j = 0; + for $i (@coldata) { + if ($g_eventtable_data{$table}{nullallowed} & (1 << $j) && (!defined($i) || $i eq "")) { + $value .= ",NULL"; + } elsif (!defined($i)) { + $value .= ",''"; + } else { + $value .= ",'".quoteSQL($i)."'"; + } + $j++; + } + $value .= ")"; + + push(@{$g_eventtable_data{$table}{queue}}, $value); + + if (scalar(@{$g_eventtable_data{$table}{queue}}) > $g_event_queue_size) + { + flushEventTable($table); + } +} + +sub flushEventTable +{ + my ($table) = @_; + + if (scalar(@{$g_eventtable_data{$table}{queue}}) == 0) + { + return; + } + + my $query = $g_eventtable_data{$table}{query}; + foreach (@{$g_eventtable_data{$table}{queue}}) + { + $query .= $_.","; + } + $query =~ s/,$//; + execNonQuery($query); + $g_eventtable_data{$table}{lastflush} = $ev_daemontime; + $g_eventtable_data{$table}{queue} = []; +} + + +# +# array calcSkill (int skill_mode, int killerSkill, int killerKills, int victimSkill, int victimKills, string weapon) +# +# Returns an array, where the first index contains the killer's new skill, and +# the second index contains the victim's new skill. +# + +sub calcSkill +{ + my ($skill_mode, $killerSkill, $killerKills, $victimSkill, $victimKills, $weapon, $killerTeam) = @_; + my @newSkill; + + # ignored bots never do a "comeback" + return ($g_skill_minchange, $victimSkill) if ($killerSkill < 1); + return ($killerSkill + $g_skill_minchange, $victimSkill) if ($victimSkill < 1); + + if ($g_debug > 2) { + &printNotice("Begin calcSkill: killerSkill=$killerSkill"); + &printNotice("Begin calcSkill: victimSkill=$victimSkill"); + } + + my $modifier = 1.00; + # Look up the weapon's skill modifier + if (defined($g_games{$g_servers{$s_addr}->{game}}{weapons}{$weapon})) { + $modifier = $g_games{$g_servers{$s_addr}->{game}}{weapons}{$weapon}{modifier}; + } + + # Calculate the new skills + + my $killerSkillChange = 0; + if ($g_skill_ratio_cap > 0) { + # SkillRatioCap, from *XYZ*SaYnt + # + # dgh...we want to cap the ratio between the victimkill and killerskill. For example, if the number 1 player + # kills a newbie, he gets 1000/5000 * 5 * 1 = 1 points. If gets killed by the newbie, he gets 5000/1000 * 5 *1 + # = -25 points. Not exactly fair. To fix this, I'm going to cap the ratio to 1/2 and 2/1. + # these numbers are designed such that an excellent player will have to get about a 2:1 ratio against noobs to + # hold steady in points. + my $lowratio = 0.7; + my $highratio = 1.0 / $lowratio; + my $ratio = ($victimSkill / $killerSkill); + if ($ratio < $lowratio) { $ratio = $lowratio; } + if ($ratio > $highratio) { $ratio = $highratio; } + $killerSkillChange = $ratio * 5 * $modifier; + } else { + $killerSkillChange = ($victimSkill / $killerSkill) * 5 * $modifier; + } + + if ($killerSkillChange > $g_skill_maxchange) { + &printNotice("Capping killer skill change of $killerSkillChange to $g_skill_maxchange") if ($g_debug > 2); + $killerSkillChange = $g_skill_maxchange; + } + + my $victimSkillChange = $killerSkillChange; + + if ($skill_mode == 1) + { + $victimSkillChange = $killerSkillChange * 0.75; + } + elsif ($skill_mode == 2) + { + $victimSkillChange = $killerSkillChange * 0.5; + } + elsif ($skill_mode == 3) + { + $victimSkillChange = $killerSkillChange * 0.25; + } + elsif ($skill_mode == 4) + { + $victimSkillChange = 0; + } + elsif ($skill_mode == 5) + { + #Zombie Panic: Source only + #Method suggested by heimer. Survivor's lose half of killer's gain when dying, but Zombie's only lose a quarter. + if ($killerTeam eq "Undead") + { + $victimSkillChange = $killerSkillChange * 0.5; + } + elsif ($killerTeam eq "Survivor") + { + $victimSkillChange = $killerSkillChange * 0.25; + } + } + + if ($victimSkillChange > $g_skill_maxchange) { + &printNotice("Capping victim skill change of $victimSkillChange to $g_skill_maxchange") if ($g_debug > 2); + $victimSkillChange = $g_skill_maxchange; + } + + if ($g_skill_maxchange >= $g_skill_minchange) { + if ($killerSkillChange < $g_skill_minchange) { + &printNotice("Capping killer skill change of $killerSkillChange to $g_skill_minchange") if ($g_debug > 2); + $killerSkillChange = $g_skill_minchange; + } + + if (($victimSkillChange < $g_skill_minchange) && ($skill_mode != 4)) { + &printNotice("Capping victim skill change of $victimSkillChange to $g_skill_minchange") if ($g_debug > 2); + $victimSkillChange = $g_skill_minchange; + } + } + if (($killerKills < $g_player_minkills ) || ($victimKills < $g_player_minkills )) { + $killerSkillChange = $g_skill_minchange; + if ($skill_mode != 4) { + $victimSkillChange = $g_skill_minchange; + } else { + $victimSkillChange = 0; + } + } + + $killerSkill += $killerSkillChange; + $victimSkill -= $victimSkillChange; + + # we want int not float + $killerSkill = sprintf("%d", $killerSkill + 0.5); + $victimSkill = sprintf("%d", $victimSkill + 0.5); + + if ($g_debug > 2) { + &printNotice("End calcSkill: killerSkill=$killerSkill"); + &printNotice("End calcSkill: victimSkill=$victimSkill"); + } + + return ($killerSkill, $victimSkill); +} + +sub calcL4DSkill +{ + my ($killerSkill, $weapon, $difficulty) = @_; + + # ignored bots never do a "comeback" + #return ($killerSkill, $victimSkill) if ($killerSkill < 1); + #return ($killerSkill, $victimSkill) if ($victimSkill < 1); + + if ($g_debug > 2) { + &printNotice("Begin calcSkill: killerSkill=$killerSkill"); + &printNotice("Begin calcSkill: victimSkill=$victimSkill"); + } + + my $modifier = 1.00; + # Look up the weapon's skill modifier + if (defined($g_games{$g_servers{$s_addr}->{game}}{weapons}{$weapon})) { + $modifier = $g_games{$g_servers{$s_addr}->{game}}{weapons}{$weapon}{modifier}; + } + + # Calculate the new skills + + $diffweight=0.5; + if ($difficulty > 0) { + $diffweight = $difficulty / 2; + } + + my $killerSkillChange = $pointvalue * $diffweight; + + if ($killerSkillChange > $g_skill_maxchange) { + &printNotice("Capping killer skill change of $killerSkillChange to $g_skill_maxchange") if ($g_debug > 2); + $killerSkillChange = $g_skill_maxchange; + } + + if ($g_skill_maxchange >= $g_skill_minchange) { + if ($killerSkillChange < $g_skill_minchange) { + &printNotice("Capping killer skill change of $killerSkillChange to $g_skill_minchange") if ($g_debug > 2); + $killerSkillChange = $g_skill_minchange; + } + } + + $killerSkill += $killerSkillChange; + # we want int not float + $killerSkill = sprintf("%d", $killerSkill + 0.5); + + if ($g_debug > 2) { + &printNotice("End calcSkill: killerSkill=$killerSkill"); + } + + return $killerSkill; +} + + +# Gives members of 'team' an extra 'reward' skill points. Members of the team +# who have been inactive (no events) for more than 2 minutes are not rewarded. +# + +sub rewardTeam +{ + my ($team, $reward, $actionid, $actionname, $actioncode) = @_; + $rcmd = $g_servers{$s_addr}->{broadcasting_command}; + + my $player; + + &printNotice("Rewarding team \"$team\" with \"$reward\" skill for action \"$actionid\" ..."); + my @userlist; + foreach $player (values(%g_players)) { + my $player_team = $player->{team}; + my $player_timestamp = $player->{timestamp}; + if (($g_servers{$s_addr}->{ignore_bots} == 1) && (($player->{is_bot} == 1) || ($player->{userid} <= 0))) { + $desc = "(IGNORED) BOT: "; + } else { + if ($player_team eq $team) { + if ($g_debug > 2) { + &printNotice("Rewarding " . $player->getInfoString() . " with \"$reward\" skill for action \"$actionid\""); + } + + &recordEvent( + "TeamBonuses", 0, + $player->{playerid}, + $actionid, + $reward + ); + $player->increment("skill", $reward, 1); + $player->increment("session_skill", $reward, 1); + $player->updateDB(); + } + if ($player->{is_bot} == 0 && $player->{userid} > 0 && $player->{display_events} == 1) { + push(@userlist, $player->{userid}); + } + } + } + if (($g_servers{$s_addr}->{broadcasting_events} == 1) && ($g_servers{$s_addr}->{broadcasting_player_actions} == 1)) { + my $coloraction = $g_servers{$s_addr}->{format_action}; + my $verb = "got"; + if ($reward < 0) { + $verb = "lost"; + } + my $msg = sprintf("%s %s %s points for %s%s", $team, $verb, abs($reward), $coloraction, $actionname); + $g_servers{$s_addr}->messageMany($msg, 0, @userlist); + } +} + + +# +# int getPlayerId (uniqueId) +# +# Looks up a player's ID number, from their unique (WON) ID. Returns their PID. +# + +sub getPlayerId +{ + my ($uniqueId) = @_; + + my $query = " + SELECT + playerId + FROM + hlstats_PlayerUniqueIds + WHERE + uniqueId='" . &::quoteSQL($uniqueId) . "' AND + game='" . $g_servers{$s_addr}->{game} . "' + "; + my $result = &doQuery($query); + + if ($result->rows > 0) { + my ($playerId) = $result->fetchrow_array; + $result->finish; + return $playerId; + } else { + $result->finish; + return 0; + } +} + + +# +# int updatePlayerProfile (object player, string field, string value) +# +# Updates a player's profile information in the database. +# + +sub updatePlayerProfile +{ + my ($player, $field, $value) = @_; + $rcmd = $g_servers{$s_addr}->{player_command}; + + unless ($player) { + &printNotice("updatePlayerInfo: Bad player"); + return 0; + } + + $value = "eSQL($value); + if ($value eq "none" || $value eq " ") { + $value = ""; + } + + my $playerName = &abbreviate($player->{name}); + my $playerId = $player->{playerid}; + + &execNonQuery(" + UPDATE + hlstats_Players + SET + $field='$value' + WHERE + playerId=$playerId + "); + + if ($g_servers{$s_addr}->{player_events} == 1) { + my $p_userid = $g_servers{$s_addr}->format_userid($player->{userid}); + my $p_is_bot = $player->{is_bot}; + $cmd_str = $rcmd." $p_userid ".$g_servers{$s_addr}->quoteparam("SET command successful for '$playerName'."); + $g_servers{$s_addr}->dorcon($cmd_str); + } + return 1; +} + + +# +# mixed getClanId (string name) +# +# Looks up a player's clan ID from their name. Compares the player's name to tag +# patterns in hlstats_ClanTags. Patterns look like: [AXXXXX] (matches 1 to 6 +# letters inside square braces, e.g. [ZOOM]Player) or =\*AAXX\*= (matches +# 2 to 4 letters between an equals sign and an asterisk, e.g. =*RAGE*=Player). +# +# Special characters in the pattern: +# A matches one character (i.e. a character is required) +# X matches zero or one characters (i.e. a character is optional) +# a matches literal A or a +# x matches literal X or x +# +# If no clan exists for the tag, it will be created. Returns the clan's ID, or +# 0 if the player is not in a clan. +# + +sub getClanId +{ + my ($name) = @_; + my $clanTag = ""; + my $clanName = ""; + my $clanId = 0; + my $result = &doQuery(" + SELECT + pattern, + position, + LENGTH(pattern) AS pattern_length + FROM + hlstats_ClanTags + ORDER BY + pattern_length DESC, + id + "); + + while ( my($pattern, $position) = $result->fetchrow_array) { + my $regpattern = quotemeta($pattern); + $regpattern =~ s/([A-Za-z0-9]+[A-Za-z0-9_-]*)/\($1\)/; # to find clan name from tag + $regpattern =~ s/A/./g; + $regpattern =~ s/X/.?/g; + + if ($g_debug > 2) { + &printNotice("regpattern=$regpattern"); + } + + if ((($position eq "START" || $position eq "EITHER") && $name =~ /^($regpattern).+/i) || + (($position eq "END" || $position eq "EITHER") && $name =~ /.+($regpattern)$/i)) { + + if ($g_debug > 2) { + &printNotice("pattern \"$regpattern\" matches \"$name\"! 1=\"$1\" 2=\"$2\""); + } + + $clanTag = $1; + $clanName = $2; + last; + } + } + + unless ($clanTag) { + return 0; + } + + my $query = " + SELECT + clanId + FROM + hlstats_Clans + WHERE + tag='" . "eSQL($clanTag) . "' AND + game='$g_servers{$s_addr}->{game}' + "; + $result = &doQuery($query); + + if ($result->rows) { + ($clanId) = $result->fetchrow_array; + $result->finish; + return $clanId; + } else { + # The clan doesn't exist yet, so we create it. + $query = " + REPLACE INTO + hlstats_Clans + ( + tag, + name, + game + ) + VALUES + ( + '" . "eSQL($clanTag) . "', + '" . "eSQL($clanName) . "', + '"."eSQL($g_servers{$s_addr}->{game})."' + ) + "; + &execNonQuery($query); + + $clanId = $db_conn->{'mysql_insertid'}; + + &printNotice("Created clan \"$clanName\" with tag " + . "\"$clanTag\" for player \"$name\""); + return $clanId; + } +} + + +# +# object getServer (string address, int port) +# +# Looks up a server's ID number in the Servers table, by searching for a +# matching IP address and port. NOTE you must specify IP addresses in the +# Servers table, NOT hostnames. +# +# Returns a new "Server object". +# + +sub getServer +{ + my ($address, $port) = @_; + + my $query = " + SELECT + a.serverId, + a.game, + a.name, + a.rcon_password, + a.publicaddress, + IFNULL(b.`value`,3) AS game_engine, + IFNULL(c.`realgame`, 'hl2mp') AS realgame, + IFNULL(a.max_players, 0) AS maxplayers + + FROM + hlstats_Servers a LEFT JOIN hlstats_Servers_Config b on a.serverId = b.serverId AND b.`parameter` = 'GameEngine' LEFT JOIN `hlstats_Games` c ON a.game = c.code + WHERE + address=? AND + port=? LIMIT 1 + "; + my @vals = ( + $address, + $port + ); + my $result = &execCached("get_server_information", $query, @vals); + + if ($result->rows) { + my ($serverId, $game, $name, $rcon_pass, $publicaddress, $gameengine, $realgame, $maxplayers) = $result->fetchrow_array; + $result->finish; + if (!defined($g_games{$game})) { + $g_games{$game} = new HLstats_Game($game); + } + # l4d code should be reused for l4d2 + # trying first using l4d as "realgame" code for l4d2 in db. if default server config settings won't work, will leave as own "realgame" code in db but uncomment line. + #$realgame = "l4d" if $realgame eq "l4d2"; + + return new HLstats_Server($serverId, $address, $port, $name, $rcon_pass, $game, $publicaddress, $gameengine, $realgame, $maxplayers); + } else { + $result->finish; + return 0; + } +} + +# +# +# +# +# + +sub queryServer +{ + my ($iaddr, $iport, @query) = @_; + my $game = ""; + my $timeout=2; + my $message = IO::Socket::INET->new(Proto=>"udp",Timeout=>$timeout,PeerPort=>$iport,PeerAddr=>$iaddr) or die "Can't make UDP socket: $@"; + $message->send("\xFF\xFF\xFF\xFFTSource Engine Query\x00"); + my ($datagram,$flags); + my $end = time + $timeout; + my $rin = ''; + vec($rin, fileno($message), 1) = 1; + + my %hash = (); + + while (1) { + my $timeleft = $end - time; + last if ($timeleft <= 0); + my ($nfound, $t) = select(my $rout = $rin, undef, undef, $timeleft); + last if ($nfound == 0); # either timeout or end of file + $message->recv($datagram,1024,$flags); + @hash{qw/key type netver hostname mapname gamedir gamename id numplayers maxplayers numbots dedicated os passreq secure gamever edf port/} = unpack("LCCZ*Z*Z*Z*vCCCCCCCZ*Cv",$datagram); + } + + return @hash{@query}; +} + + +sub getServerMod +{ + my ($address, $port) = @_; + my ($playgame); + + &printEvent ("DETECT", "Querying $address".":$port for gametype"); + + my @query = ( + 'gamename', + 'gamedir', + 'hostname', + 'numplayers', + 'maxplayers', + 'mapname' + ); + + my ($gamename, $gamedir, $hostname, $numplayers, $maxplayers, $mapname) = &queryServer($address, $port, @query); + + if ($gamename =~ /^Counter-Strike$/i) { + $playgame = "cstrike"; + } elsif ($gamename =~ /^Counter-Strike/i) { + $playgame = "css"; + } elsif ($gamename =~ /^Team Fortress C/i) { + $playgame = "tfc"; + } elsif ($gamename =~ /^Team Fortress/i) { + $playgame = "tf"; + } elsif ($gamename =~ /^Day of Defeat$/i) { + $playgame = "dod"; + } elsif ($gamename =~ /^Day of Defeat/i) { + $playgame = "dods"; + } elsif ($gamename =~ /^Insurgency/i) { + $playgame = "insmod"; + } elsif ($gamename =~ /^Neotokyo/i) { + $playgame = "nts"; + } elsif ($gamename =~ /^Fortress Forever/i) { + $playgame = "ff"; + } elsif ($gamename =~ /^Age of Chivalry/i) { + $playgame = "aoc"; + } elsif ($gamename =~ /^Dystopia/i) { + $playgame = "dystopia"; + } elsif ($gamename =~ /^Stargate/i) { + $playgame = "sgtls"; + } elsif ($gamename =~ /^Battle Grounds/i) { + $playgame = "bg2"; + } elsif ($gamename =~ /^Hidden/i) { + $playgame = "hidden"; + } elsif ($gamename =~ /^L4D /i) { + $playgame = "l4d"; + } elsif ($gamename =~ /^Left 4 Dead 2/i) { + $playgame = "l4d2"; + } elsif ($gamename =~ /^ZPS /i) { + $playgame = "zps"; + } elsif ($gamename =~ /^NS /i) { + $playgame = "ns"; + } elsif ($gamename =~ /^pvkii/i) { + $playgame = "pvkii"; + } elsif ($gamename =~ /^CSPromod/i) { + $playgame = "csp"; + } elsif ($gamename eq "Half-Life") { + $playgame = "valve"; + } elsif ($gamename eq "Nuclear Dawn") { + $playgame = "nucleardawn"; + + # We didn't found our mod, trying secondary way. This is required for some games such as FOF and GES and is a fallback for others + } elsif ($gamedir =~ /^ges/i) { + $playgame = "ges"; + } elsif ($gamedir =~ /^fistful_of_frags/i || $gamedir =~ /^fof/i) { + $playgame = "fof"; + } elsif ($gamedir =~ /^hl2mp/i) { + $playgame = "hl2mp"; + } elsif ($gamedir =~ /^tfc/i) { + $playgame = "tfc"; + } elsif ($gamedir =~ /^tf/i) { + $playgame = "tf"; + } elsif ($gamedir =~ /^ins/i) { + $playgame = "insmod"; + } elsif ($gamedir =~ /^neotokyo/i) { + $playgame = "nts"; + } elsif ($gamedir =~ /^fortressforever/i) { + $playgame = "ff"; + } elsif ($gamedir =~ /^ageofchivalry/i) { + $playgame = "aoc"; + } elsif ($gamedir =~ /^dystopia/i) { + $playgame = "dystopia"; + } elsif ($gamedir =~ /^sgtls/i) { + $playgame = "sgtls"; + } elsif ($gamedir =~ /^hidden/i) { + $playgame = "hidden"; + } elsif ($gamedir =~ /^left4dead/i) { + $playgame = "l4d"; + } elsif ($gamedir =~ /^left4dead2/i) { + $playgame = "l4d2"; + } elsif ($gamedir =~ /^zps/i) { + $playgame = "zps"; + } elsif ($gamedir =~ /^ns/i) { + $playgame = "ns"; + } elsif ($gamedir =~ /^bg/i) { + $playgame = "bg2"; + } elsif ($gamedir =~ /^pvkii/i) { + $playgame = "pvkii"; + } elsif ($gamedir =~ /^cspromod/i) { + $playgame = "csp"; + } elsif ($gamedir =~ /^valve$/i) { + $playgame = "valve"; + } elsif ($gamedir =~ /^nucleardawn$/i) { + $playgame = "nucleardawn"; + } elsif ($gamedir =~ /^dinodday$/i) { + $playgame = "dinodday"; + } else { + # We didn't found our mod, giving up. + &printEvent("DETECT", "Failed to get Server Mod"); + return 0; + } + &printEvent("DETECT", "Saving server " . $address . ":" . $port . " with gametype " . $playgame); + &addServerToDB($address, $port, $hostname, $playgame, $numplayers, $maxplayers, $mapname); + return $playgame; +} + +sub addServerToDB +{ + my ($address, $port, $name, $game, $act_players, $max_players, $act_map) = @_; + my $sql = "INSERT INTO hlstats_Servers (address, port, name, game, act_players, max_players, act_map) VALUES ('$address', $port, '"."eSQL($name)."', '"."eSQL($game)."', $act_players, $max_players, '"."eSQL($act_map)."')"; + &execNonQuery($sql); + + my $last_id = $db_conn->{'mysql_insertid'}; + &execNonQuery("DELETE FROM `hlstats_Servers_Config` WHERE serverId = $last_id"); + &execNonQuery("INSERT INTO `hlstats_Servers_Config` (`serverId`, `parameter`, `value`) + SELECT $last_id, `parameter`, `value` + FROM `hlstats_Mods_Defaults` WHERE `code` = '';"); + &execNonQuery("INSERT INTO `hlstats_Servers_Config` (`serverId`, `parameter`, `value`) VALUES + ($last_id, 'Mod', '');"); + &execNonQuery("INSERT INTO `hlstats_Servers_Config` (`serverId`, `parameter`, `value`) + SELECT $last_id, `parameter`, `value` + FROM `hlstats_Games_Defaults` WHERE `code` = '"."eSQL($game)."' + ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);"); + &readDatabaseConfig(); + + return 1; +} + +# +# boolean sameTeam (string team1, string team2) +# +# This should be expanded later to allow for team alliances (e.g. TFC-hunted). +# + +sub sameTeam +{ + my ($team1, $team2) = @_; + + if (($team1 eq $team2) && (($team1 ne "Unassigned") || ($team2 ne "Unassigned"))) { + return 1; + } else { + return 0; + } +} + + +# +# string getPlayerInfoString (object player, string ident) +# + +sub getPlayerInfoString +{ + my ($player) = shift; + my @ident = @_; + + if ($player) { + return $player->getInfoString(); + } else { + return "(" . join(",", @ident) . ")"; + } +} + + + +# +# array getPlayerInfo (string player, string $ipAddr) +# +# Get a player's name, uid, wonid and team from "Name". +# + +sub getPlayerInfo +{ + my ($player, $create_player, $ipAddr) = @_; + + if ($player =~ /^(.*?)<(\d+)><([^<>]*)><([^<>]*)>(?:<([^<>]*)>)?.*$/) { + my $name = $1; + my $userid = $2; + my $uniqueid = $3; + my $team = $4; + my $role = $5; + my $bot = 0; + my $haveplayer = 0; + + $plainuniqueid = $uniqueid; + $uniqueid =~ s/^STEAM_[0-9]+?\://; + + if (($uniqueid eq "Console") && ($team eq "Console")) { + return 0; + } + if ($g_servers{$s_addr}->{play_game} == L4D()) { + #for l4d, create meta player object for each role + if ($uniqueid eq "") { + #infected & witch have blank steamid + if ($name eq "infected") { + $uniqueid = "BOT-Horde"; + $team = "Infected"; + $userid = -9; + } elsif ($name eq "witch") { + $uniqueid = "BOT-Witch"; + $team = "Infected"; + $userid = -10; + } else { + return 0; + } + } elsif ($uniqueid eq "BOT") { + #all other bots have BOT for steamid + if ($team eq "Survivor") { + if ($name eq "Nick") { + $userid = -11; + } elsif ($name eq "Ellis") { + $userid = -13; + } elsif ($name eq "Rochelle") { + $userid = -14; + } elsif ($name eq "Coach") { + $userid = -12; + } elsif ($name eq "Louis") { + $userid = -4; + } elsif ($name eq "Zoey") { + $userid = -1; + } elsif ($name eq "Francis") { + $userid = -2; + } elsif ($name eq "Bill") { + $userid = -3; + } else { + &printEvent("ERROR", "No survivor match for $name",0,1); + $userid = -4; + } + } else { + if ($name eq "Smoker") { + $userid = -5; + } elsif ($name eq "Boomer") { + $userid = -6; + } elsif ($name eq "Hunter") { + $userid = -7; + } elsif ($name eq "Spitter") { + $userid = -15; + } elsif ($name eq "Jockey") { + $userid = -16; + } elsif ($name eq "Charger") { + $userid = -17; + } elsif ($name eq "Tank") { + $userid = -8; + } else { + &printEvent("DEBUG", "No infected match for $name",0,1); + $userid = -8; + } + } + $uniqueid = "BOT-".$name; + $name = "BOT-".$name; + } + } + + if ($ipAddr eq "none") { + $ipAddr = ""; + } + + $bot = botidcheck($uniqueid); + + if ($g_mode eq "NameTrack") { + $uniqueid = $name; + } else { + if ($g_mode eq "LAN" && !$bot && $userid > 0) { + if ($ipAddr ne "") { + $g_lan_noplayerinfo->{"$s_addr/$userid/$name"} = { + ipaddress => $ipAddr, + userid => $userid, + name => $name, + server => $s_addr + }; + $uniqueid = $ipAddr; + } else { + while ( my($index, $player) = each(%g_players) ) { + if (($player->{userid} eq $userid) && + ($player->{name} eq $name)) { + + $uniqueid = $player->{uniqueid}; + $haveplayer = 1; + last; + } + } + if (!$haveplayer) { + while ( my($index, $player) = each(%g_lan_noplayerinfo) ) { + if (($player->{server} eq $s_addr) && + ($player->{userid} eq $userid) && + ($player->{name} eq $name)) { + + $uniqueid = $player->{ipaddress}; + $haveplayer = 1; + } + } + } + if (!$haveplayer) { + $uniqueid = "UNKNOWN"; + } + } + } else { + # Normal (steamid) mode player and bot, as well as lan mode bots + if ($bot) { + $md5 = Digest::MD5->new; + $md5->add($name); + $md5->add($s_addr); + $uniqueid = "BOT:" . $md5->hexdigest; + $unique_id = $uniqueid if ($g_mode eq "LAN"); + } + + if ($uniqueid eq "UNKNOWN" + || $uniqueid eq "STEAM_ID_PENDING" || $uniqueid eq "STEAM_ID_LAN" + || $uniqueid eq "VALVE_ID_PENDING" || $uniqueid eq "VALVE_ID_LAN" + ) { + return { + name => $name, + userid => $userid, + uniqueid => $uniqueid, + team => $team + }; + } + } + } + + if (!$haveplayer) + { + while ( my ($index, $player) = each(%g_players) ) { + # Cannot exit loop early as more than one player can exist with same uniqueid + # (bug? or just bad logging) + # Either way, we disconnect any that don't match the current line + if ($player->{uniqueid} eq $uniqueid) { + $haveplayer = 1; + # Catch players reconnecting without first disconnecting + if ($player->{userid} != $userid) { + + &doEvent_Disconnect( + $player->{"userid"}, + $uniqueid, + "" + ); + $haveplayer = 0; + } + } + } + } + + if ($haveplayer) { + my $player = lookupPlayer($s_addr, $userid, $uniqueid); + if ($player) { + # The only time team should go /back/ to unassigned ("") is on mapchange + # (which is already handled in the ChangeMap handler) + # So ignore when team is blank (<>) from lazy log lines + if ($team ne "" && $player->{team} ne $team) { + &doEvent_TeamSelection( + $userid, + $uniqueid, + $team + ); + } + if ($role ne "" && $role ne $player->{role}) { + &doEvent_RoleSelection( + $player->{"userid"}, + $player->{"uniqueid"}, + $role + ); + } + + $player->updateTimestamp(); + } + } else { + if ($userid != 0) { + if ($create_player > 0) { + my $preIpAddr = ""; + if ($g_preconnect->{"$s_addr/$userid/$name"}) { + $preIpAddr = $g_preconnect->{"$s_addr/$userid/$name"}->{"ipaddress"}; + } + # Add the player to our hash of player objects + $g_servers{$s_addr}->{"srv_players"}->{"$userid/$uniqueid"} = new HLstats_Player( + server => $s_addr, + server_id => $g_servers{$s_addr}->{id}, + userid => $userid, + uniqueid => $uniqueid, + plain_uniqueid => $plainuniqueid, + game => $g_servers{$s_addr}->{game}, + name => $name, + team => $team, + role => $role, + is_bot => $bot, + display_events => $g_servers{$s_addr}->{default_display_events}, + address => (($preIpAddr ne "") ? $preIpAddr : $ipAddr) + ); + + if ($preIpAddr ne "") { + &printEvent("SERVER", "LATE CONNECT [$name/$userid] - steam userid validated"); + &doEvent_Connect($userid, $uniqueid, $preIpAddr); + delete($g_preconnect->{"$s_addr/$userid/$name"}); + } + # Increment number of players on server + $g_servers{$s_addr}->updatePlayerCount(); + } + } elsif (($g_mode eq "LAN") && (defined($g_lan_noplayerinfo{"$s_addr/$userid/$name"}))) { + if ((!$haveplayer) && ($uniqueid ne "UNKNOWN") && ($create_player > 0)) { + $g_servers{$s_addr}->{srv_players}->{"$userid/$uniqueid"} = new HLstats_Player( + server => $s_addr, + server_id => $g_servers{$s_addr}->{id}, + userid => $userid, + uniqueid => $uniqueid, + plain_uniqueid => $plainuniqueid, + game => $g_servers{$s_addr}->{game}, + name => $name, + team => $team, + role => $role, + is_bot => $bot + ); + delete($g_lan_noplayerinfo{"$s_addr/$userid/$name"}); + # Increment number of players on server + + $g_servers{$s_addr}->updatePlayerCount(); + } + } else { + &printNotice("No player object available for player \"$name\" "); + } + } + + return { + name => $name, + userid => $userid, + uniqueid => $uniqueid, + team => $team, + is_bot => $bot + }; + } elsif ($player =~ /^(.+)<([^<>]+)>$/) { + my $name = $1; + my $uniqueid = $2; + my $bot = 0; + + if (&botidcheck($uniqueid)) { + $md5 = Digest::MD5->new; + $md5->add($ev_daemontime); + $md5->add($s_addr); + $uniqueid = "BOT:" . $md5->hexdigest; + $bot = 1; + } + return { + name => $name, + uniqueid => $uniqueid, + is_bot => $bot + }; + } elsif ($player =~ /^<><([^<>]+)><>$/) { + my $uniqueid = $1; + my $bot = 0; + if (&botidcheck($uniqueid)) { + $md5 = Digest::MD5->new; + $md5->add($ev_daemontime); + $md5->add($s_addr); + $uniqueid = "BOT:" . $md5->hexdigest; + $bot = 1; + } + return { + uniqueid => $uniqueid, + is_bot => $bot + }; + } else { + return 0; + } +} + + +# +# hash getProperties (string propstring) +# +# Parse (key "value") properties into a hash. +# + +sub getProperties +{ + my ($propstring) = @_; + my %properties; + my $dods_flag = 0; + + while ($propstring =~ s/^\s*\((\S+)(?:(?: "(.+?)")|(?: ([^\)]+)))?\)//) { + my $key = $1; + if (defined($2)) { + if ($key eq "player") { + if ($dods_flag == 1) { + $key = "player_a"; + $dods_flag++; + } elsif ($dods_flag == 2) { + $key = "player_b"; + } + } + $properties{$key} = $2; + } elsif (defined($3)) { + $properties{$key} = $3; + } else { + $properties{$key} = 1; # boolean property + } + if ($key eq "flagindex") { + $dods_flag++; + } + } + + return %properties; +} + + +# +# boolean like (string subject, string compare) +# +# Returns true if 'subject' equals 'compare' with optional whitespace. +# + +sub like +{ + my ($subject, $compare) = @_; + + if ($subject =~ /^\s*\Q$compare\E\s*$/) { + return 1; + } else { + return 0; + } +} + + +# +# boolean botidcheck (string uniqueid) +# +# Returns true if 'uniqueid' is that of a bot. +# + +sub botidcheck +{ + # needs cleaned up + # added /^00000000\:\d+\:0$/ check for "whichbot" + my ($uniqueid) = @_; + if ($uniqueid eq "BOT" || $uniqueid eq "0" || $uniqueid =~ /^00000000\:\d+\:0$/) { + return 1 + } + return 0; +} + +sub isTrackableTeam +{ + my ($team) = @_; + #if ($team =~ /spectator/i || $team =~ /unassigned/i || $team eq "") { + if ($team =~ /spectator/i || $team eq "") { + return 0; + } + return 1; +} + +sub reloadConfiguration +{ + &flushAll; + &readDatabaseConfig; +} + + +sub flushAll +{ + # we only need to flush events if we're about to shut down. they are unaffected by server/player deletion + my ($flushevents) = @_; + if ($flushevents) + { + while ( my ($table, $colsref) = each(%g_eventTables) ) + { + flushEventTable($table); + } + } + + while( my($se, $server) = each(%g_servers)) + { + while ( my($pl, $player) = each(%{$server->{"srv_players"}}) ) + { + if ($player) + { + $player->playerCleanup(); + } + } + $server->flushDB(); + } +} + + +## +## MAIN +## + +# Options + +$opt_help = 0; +$opt_version = 0; + +$db_host = "localhost"; +$db_user = ""; +$db_pass = ""; +$db_name = "hlstats"; +$db_lowpriority = 1; + +$s_ip = ""; +$s_port = "27500"; + +$g_mailto = ""; +$g_mailpath = "/bin/mail"; +$g_mode = "Normal"; +$g_deletedays = 5; +$g_requiremap = 0; +$g_debug = 1; +$g_nodebug = 0; +$g_rcon = 1; +$g_rcon_ignoreself = 0; +$g_rcon_record = 1; +$g_stdin = 0; +$g_server_ip = ""; +$g_server_port = 27015; +$g_timestamp = 0; +$g_cpanelhack = 0; +$g_event_queue_size = 10; +$g_dns_resolveip = 1; +$g_dns_timeout = 5; +$g_skill_maxchange = 100; +$g_skill_minchange = 2; +$g_skill_ratio_cap = 0; +$g_geoip_binary = 0; +$g_player_minkills = 50; +$g_onlyconfig_servers = 1; +$g_track_stats_trend = 0; +%g_lan_noplayerinfo = (); +%g_preconnect = (); +$g_global_banning = 0; +$g_log_chat = 0; +$g_log_chat_admins = 0; +$g_global_chat = 0; +$g_ranktype = "skill"; +$g_gi = undef; + +my %dysweaponcodes = ( + "1" => "Light Katana", + "2" => "Medium Katana", + "3" => "Fatman Fist", + "4" => "Machine Pistol", + "5" => "Shotgun", + "6" => "Laser Rifle", + "7" => "BoltGun", + "8" => "SmartLock Pistols", + "9" => "Assault Rifle", + "10" => "Grenade Launcher", + "11" => "MK-808 Rifle", + "12" => "Tesla Rifle", + "13" => "Rocket Launcher", + "14" => "Minigun", + "15" => "Ion Cannon", + "16" => "Basilisk", + "17" => "Frag Grenade", + "18" => "EMP Grenade", + "19" => "Spider Grenade", + "22" => "Cortex Bomb" +); + +# Usage message + +$usage = <fetchrow_array) { + $g_config_servers{$addr} = (); + my $serverConfig = &doQuery("SELECT parameter,value FROM hlstats_Servers_Config WHERE serverId=$serverId"); + while ( my($p,$v) = $serverConfig->fetchrow_array) { + $g_config_servers{$addr}{$p} = $v; + } + } + $srv_id->finish; + # hlxce: read the global settings from the database! + my $gsettings = &doQuery("SELECT keyname,value FROM hlstats_Options WHERE opttype <= 1"); + while ( my($p,$v) = $gsettings->fetchrow_array) { + if ($g_debug > 1) { + print "Config parameter '$p' = '$v'\n"; + } + $tmp = "\$".$directives_mysql{$p}." = '$v'"; + #print " -> setting ".$tmp."\n"; + eval $tmp; + } + $gsettings->finish; + # setting defaults + + &printEvent("DAEMON", "Proxy_Key DISABLED", 1) if ($proxy_key eq ""); + while (my($addr, $server) = each(%g_config_servers)) { + + if (!defined($g_config_servers{$addr}{"MinPlayers"})) { + $g_config_servers{$addr}{"MinPlayers"} = 6; + } + if (!defined($g_config_servers{$addr}{"DisplayResultsInBrowser"})) { + $g_config_servers{$addr}{"DisplayResultsInBrowser"} = 0; + } + if (!defined($g_config_servers{$addr}{"BroadCastEvents"})) { + $g_config_servers{$addr}{"BroadCastEvents"} = 0; + } + if (!defined($g_config_servers{$addr}{"BroadCastPlayerActions"})) { + $g_config_servers{$addr}{"BroadCastPlayerActions"} = 0; + } + if (!defined($g_config_servers{$addr}{"BroadCastEventsCommand"})) { + $g_config_servers{$addr}{"BroadCastEventsCommand"} = "say"; + } + if (!defined($g_config_servers{$addr}{"BroadCastEventsCommandAnnounce"})) { + $g_config_servers{$addr}{"BroadCastEventsCommandAnnounce"} = "say"; + } + if (!defined($g_config_servers{$addr}{"PlayerEvents"})) { + $g_config_servers{$addr}{"PlayerEvents"} = 1; + } + if (!defined($g_config_servers{$addr}{"PlayerEventsCommand"})) { + $g_config_servers{$addr}{"PlayerEventsCommand"} = "say"; + } + if (!defined($g_config_servers{$addr}{"PlayerEventsCommandOSD"})) { + $g_config_servers{$addr}{"PlayerEventsCommandOSD"} = ""; + } + if (!defined($g_config_servers{$addr}{"PlayerEventsCommandHint"})) { + $g_config_servers{$addr}{"PlayerEventsCommandHint"} = ""; + } + if (!defined($g_config_servers{$addr}{"PlayerEventsAdminCommand"})) { + $g_config_servers{$addr}{"PlayerEventsAdminCommand"} = ""; + } + if (!defined($g_config_servers{$addr}{"ShowStats"})) { + $g_config_servers{$addr}{"ShowStats"} = 1; + } + if (!defined($g_config_servers{$addr}{"AutoTeamBalance"})) { + $g_config_servers{$addr}{"AutoTeamBalance"} = 0; + } + if (!defined($g_config_servers{$addr}{"AutoBanRetry"})) { + $g_config_servers{$addr}{"AutoBanRetry"} = 0; + } + if (!defined($g_config_servers{$addr}{"TrackServerLoad"})) { + $g_config_servers{$addr}{"TrackServerLoad"} = 0; + } + if (!defined($g_config_servers{$addr}{"MinimumPlayersRank"})) { + $g_config_servers{$addr}{"MinimumPlayersRank"} = 0; + } + if (!defined($g_config_servers{$addr}{"Admins"})) { + $g_config_servers{$addr}{"Admins"} = ""; + } + if (!defined($g_config_servers{$addr}{"SwitchAdmins"})) { + $g_config_servers{$addr}{"SwitchAdmins"} = 0; + } + if (!defined($g_config_servers{$addr}{"IgnoreBots"})) { + $g_config_servers{$addr}{"IgnoreBots"} = 1; + } + if (!defined($g_config_servers{$addr}{"SkillMode"})) { + $g_config_servers{$addr}{"SkillMode"} = 0; + } + if (!defined($g_config_servers{$addr}{"GameType"})) { + $g_config_servers{$addr}{"GameType"} = 0; + } + if (!defined($g_config_servers{$addr}{"BonusRoundTime"})) { + $g_config_servers{$addr}{"BonusRoundTime"} = 0; + } + if (!defined($g_config_servers{$addr}{"BonusRoundIgnore"})) { + $g_config_servers{$addr}{"BonusRoundIgnore"} = 0; + } + if (!defined($g_config_servers{$addr}{"Mod"})) { + $g_config_servers{$addr}{"Mod"} = ""; + } + if (!defined($g_config_servers{$addr}{"EnablePublicCommands"})) { + $g_config_servers{$addr}{"EnablePublicCommands"} = 1; + } + if (!defined($g_config_servers{$addr}{"ConnectAnnounce"})) { + $g_config_servers{$addr}{"ConnectAnnounce"} = 1; + } + if (!defined($g_config_servers{$addr}{"UpdateHostname"})) { + $g_config_servers{$addr}{"UpdateHostname"} = 0; + } + if (!defined($g_config_servers{$addr}{"DefaultDisplayEvents"})) { + $g_config_servers{$addr}{"DefaultDisplayEvents"} = 1; + } + } + + &printEvent("CONFIG", "I have found the following server configs in database:", 1); + while (my($addr, $server) = each(%g_config_servers)) { + &printEvent("S_CONFIG", $addr, 1); + } + + my $geotell = ((!defined($g_gi)) ? -1 : tell $g_gi{fh}); + + if ($g_geoip_binary > 0 && $geotell == -1) { + my $geoipfile = "$opt_libdir/GeoLiteCity/GeoLiteCity.dat"; + if (-r $geoipfile) { + eval "use Geo::IP::PurePerl"; my $hasGeoIP = $@ ? 0 : 1; + if ($hasGeoIP) { + $g_gi = Geo::IP::PurePerl->open($geoipfile, "GEOIP_STANDARD"); + } else { + &printEvent("ERROR", "GeoIP method set to binary file lookup but Geo::IP::PurePerl module NOT FOUND", 1); + $g_gi = undef; + } + } else { + &printEvent("ERROR", "GeoIP method set to binary file lookup but $geoipfile NOT FOUND", 1); + $g_gi = undef; + } + } elsif ($g_geoip_binary == 0 && $geotell > -1) { + close($g_gi{fh}); + $g_gi = undef; + } +} + +# Read Config File + +if ($opt_configfile && -r $opt_configfile) { + $conf = ConfigReaderSimple->new($opt_configfile); + $conf->parse(); + %directives = ( + "DBHost", "db_host", + "DBUsername", "db_user", + "DBPassword", "db_pass", + "DBName", "db_name", + "DBLowPriority", "db_lowpriority", + "BindIP", "s_ip", + "Port", "s_port", + "DebugLevel", "g_debug", + "CpanelHack", "g_cpanelhack", + "EventQueueSize", "g_event_queue_size" + ); + + %directives_mysql = ( + "version", "g_version", + "MailTo", "g_mailto", + "MailPath", "g_mailpath", + "Mode", "g_mode", + "DeleteDays", "g_deletedays", + "UseTimestamp", "g_timestamp", + "DNSResolveIP", "g_dns_resolveip", + "DNSTimeout", "g_dns_timeout", + "RconIgnoreSelf", "g_rcon_ignoreself", + "Rcon", "g_rcon", + "RconRecord", "g_rcon_record", + "MinPlayers", "g_minplayers", + "SkillMaxChange", "g_skill_maxchange", + "SkillMinChange", "g_skill_minchange", + "PlayerMinKills", "g_player_minkills", + "AllowOnlyConfigServers", "g_onlyconfig_servers", + "TrackStatsTrend", "g_track_stats_trend", + "GlobalBanning", "g_global_banning", + "LogChat", "g_log_chat", + "LogChatAdmins", "g_log_chat_admins", + "GlobalChat", "g_global_chat", + "SkillRatioCap", "g_skill_ratio_cap", + "rankingtype", "g_ranktype", + "UseGeoIPBinary", "g_geoip_binary", + "Proxy_Key", "proxy_key" + ); + +# "Servers", "g_config_servers" + &doConf($conf, %directives); + +} else { + print "-- Warning: unable to open configuration file '$opt_configfile'\n"; +} + +# Read Command Line Arguments + +%copts = (); + +GetOptions( + "help|h" => \$copts{opt_help}, + "version|v" => \$copts{opt_version}, + "debug|d+" => \$copts{g_debug}, + "nodebug|n+" => \$copts{g_nodebug}, + "mode|m=s" => \$copts{g_mode}, + "configfile|c=s" => \$copts{configfile}, + "db-host=s" => \$copts{db_host}, + "db-name=s" => \$copts{db_name}, + "db-password=s" => \$copts{db_pass}, + "db-username=s" => \$copts{db_user}, + "dns-resolveip!" => \$copts{g_dns_resolveip}, + "dns-timeout=i" => \$copts{g_dns_timeout}, + "ip|i=s" => \$copts{s_ip}, + "port|p=i" => \$copts{s_port}, + "rcon!" => \$copts{g_rcon}, + "r" => \$copts{g_rcon}, + "stdin!" => \$copts{g_stdin}, + "s" => \$copts{g_stdin}, + "server-ip=s" => \$copts{g_server_ip}, + "server-port=i" => \$copts{g_server_port}, + "timestamp!" => \$copts{g_timestamp}, + "t" => \$copts{g_timestamp}, + "event-queue-size" => \$copts{g_event_queue_size} +) or die($usage); + + +if ($configfile && -r $configfile) { + $conf = ''; + $conf = ConfigReaderSimple->new($configfile); + $conf->parse(); + &doConf($conf, %directives); +} + +# these are set above, we then reload them to override values in the actual config +setOptionsConf(%copts); + +if ($g_cpanelhack) { + my $home_dir = $ENV{ HOME }; + my $base_module_dir = (-d "$home_dir/perl" ? "$home_dir/perl" : ( getpwuid($>) )[7] . '/perl/'); + unshift @INC, map { $base_module_dir . $_ } @INC; +} + +eval { + require Geo::IP::PurePerl; +}; +import Geo::IP::PurePerl; + +if ($opt_help) { + print $usage; + exit(0); +} + +if ($opt_version) { + &doConnect; + my $result = &doQuery(" + SELECT + value + FROM + hlstats_Options + WHERE + keyname='version' + "); + + if ($result->rows > 0) { + $g_version = $result->fetchrow_array; + } + $result->finish; + print "\nhlstats.pl (HLstatsX Community Edition) Version $g_version\n" + . "Real-time player and clan rankings and statistics for Half-Life 2\n" + . "Modified (C) 2008-20XX Nicholas Hastings (nshastings@gmail.com)\n" + . "Copyleft (L) 2007-2008 Malte Bayer\n" + . "Modified (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com)\n" + . "Original (C) 2001 by Simon Garner \n\n"; + + print "Using ConfigReaderSimple module version $ConfigReaderSimple::VERSION\n"; + if ($g_rcon) { + print "Using rcon module\n"; + } + + print "\nThis is free software; see the source for copying conditions. There is NO\n" + . "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\n"; + exit(0); +} + +# Connect to the database + +&doConnect; +&readDatabaseConfig; +&buildEventInsertData; + +if ($g_mode ne "Normal" && $g_mode ne "LAN" && $g_mode ne "NameTrack") { + $g_mode = "Normal"; +} + +$g_debug -= $g_nodebug; +$g_debug = 0 if ($g_debug < 0); + + +# Init Timestamp +my ($sec,$min,$hour,$mday,$mon,$year) = localtime(time()); +$ev_timestamp = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec); +$ev_unixtime = time(); +$ev_daemontime = $ev_unixtime; + +# Startup + +&printEvent("HLSTATSX", "HLstatsX:CE $g_version starting...", 1); + +# Create the UDP & TCP socket + +if ($g_stdin) { + $g_rcon = 0; + &printEvent("UDP", "UDP listen socket disabled, reading log data from STDIN.", 1); + if (!$g_server_ip || !$g_server_port) { + &printEvent("UDP", "ERROR: You must specify source of STDIN data using --server-ip and --server-port", 1); + &printEvent("UDP", "Example: ./hlstats.pl --stdin --server-ip 12.34.56.78 --server-port 27015", 1); + exit(255); + } else { + &printEvent("UDP", "All data from STDIN will be allocated to server '$g_server_ip:$g_server_port'.", 1); + $s_peerhost = $g_server_ip; + $s_peerport = $g_server_port; + $s_addr = "$s_peerhost:$s_peerport"; + } +} else { + if ($s_ip) { $ip = $s_ip . ":"; } else { $ip = "port "; } + $s_socket = IO::Socket::INET->new( + Proto=>"udp", + LocalAddr=>"$s_ip", + LocalPort=>"$s_port" + ) or die ("\nCan't setup UDP socket on $ip$s_port: $!\n"); + + &printEvent("UDP", "Opening UDP listen socket on $ip$s_port ... ok", 1); +} + +if ($g_track_stats_trend > 0) { + &printEvent("HLSTATSX", "Tracking Trend of the stats are enabled", 1); +} + +if ($g_global_banning > 0) { + &printEvent("HLSTATSX", "Global Banning on all servers is enabled", 1); +} + +&printEvent("HLSTATSX", "Maximum Skill Change on all servers are ".$g_skill_maxchange." points", 1); +&printEvent("HLSTATSX", "Minimum Skill Change on all servers are ".$g_skill_minchange." points", 1); +&printEvent("HLSTATSX", "Minimum Players Kills on all servers are ".$g_player_minkills." kills", 1); + +if ($g_log_chat > 0) { + &printEvent("HLSTATSX", "Players chat logging is enabled", 1); + if ($g_log_chat_admins > 0) { + &printEvent("HLSTATSX", "Admins chat logging is enabled", 1); + } +} + +if ($g_global_chat == 1) { + &printEvent("HLSTATSX", "Broadcasting public chat to all players is enabled", 1); +} elsif ($g_gloabl_chat == 2) { + &printEvent("HLSTATSX", "Broadcasting public chat to admins is enabled", 1); +} else { + &printEvent("HLSTATSX", "Broadcasting public chat is disabled", 1); +} + +&printEvent("HLSTATSX", "Event queue size is set to ".$g_event_queue_size, 1); + + +%g_servers = (); + +&printEvent("HLSTATSX", "HLstatsX:CE is now running ($g_mode mode, debug level $g_debug)", 1); + +$start_time = time(); +if ($g_stdin) { + $g_timestamp = 1; + $start_parse_time = time(); + $import_logs_count = 0; + &printEvent("IMPORT", "Start importing logs. Every dot signs 500 parsed lines", 1, 1); +} + +# Main data loop +$c = 0; + +sub getLine +{ + if ($g_stdin) { + return ; + } else { + return 1; + } +} + + +&execNonQuery("TRUNCATE TABLE hlstats_Livestats"); +$timeout = 0; +$s_output = ""; +my ($proxy_s_peerhost, $proxy_s_peerport); +while ($loop = &getLine()) { + + my ($sec,$min,$hour,$mday,$mon,$year) = localtime(time()); + $ev_timestamp = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec); + $ev_unixtime = time(); + $ev_daemontime = $ev_unixtime; #time() + + if ($g_stdin) { + $s_output = $loop; + if (($import_logs_count > 0) && ($import_logs_count % 500 == 0)) { + $parse_time = $ev_unixtime - $start_parse_time; + if ($parse_time == 0) { + $parse_time++; + } + print ". [".($parse_time)." sec (".sprintf("%.3f", (500 / $parse_time)).")]\n"; + $start_parse_time = $ev_unixtime; + } + } else { + if(IO::Select->new($s_socket)->can_read(2)) { # 2 second timeout + $s_socket->recv($s_output, 1024); + $s_output = decode( 'utf8', $s_output ); + $timeout = 0; + } else { + $timeout++; + if ($timeout % 60 == 0) { + &printEvent("HLSTATSX", "No data since 120 seconds"); + } + } + + if (($s_output =~ /^.*PROXY Key=(.+) (.*)PROXY.+/) && $proxy_key ne "") { + $rproxy_key = $1; + $s_addr = $2; + + if ($s_addr ne "") { + ($s_peerhost, $s_peerport) = split(/:/, $s_addr); + } + + $proxy_s_peerhost = $s_socket->peerhost; + $proxy_s_peerport = $s_socket->peerport; + &printEvent("PROXY", "Detected proxy call from $proxy_s_peerhost:$proxy_s_peerport") if ($d_debug > 2); + + + if ($proxy_key eq $rproxy_key) { + $s_output =~ s/PROXY.*PROXY //; + if ($s_output =~ /^C;HEARTBEAT;/) { + &printEvent("PROXY, Heartbeat request from $proxy_s_peerhost:$proxy_s_peerport"); + } elsif ($s_output =~ /^C;RELOAD;/) { + &printEvent("PROXY, Reload request from $proxy_s_peerhost:$proxy_s_peerport"); + } elsif ($s_output =~ /^C;KILL;/) { + &printEvent("PROXY, Kill request from $proxy_s_peerhost:$proxy_s_peerport"); + } else { + &printEvent("PROXY", $s_output); + } + } else { + &printEvent("PROXY", "proxy_key mismatch, dropping package"); + &printEvent("PROXY", $s_output) if ($g_debug > 2); + $s_output = ""; + next; + } + } else { + # Reset the proxy stuff and use it as "normal" + $rproxy_key = ""; + $proxy_s_peerhost = ""; + $proxy_s_peerport = ""; + + $s_peerhost = $s_socket->peerhost; + $s_peerport = $s_socket->peerport; + + $s_addr = "$s_peerhost:$s_peerport"; + } + } + + if ($timeout == 0) { + my ($address, $port); + my @data = split ";", $s_output; + $cmd = $data[0]; + if ($cmd eq "C" && ($s_peerhost eq "127.0.0.1" || (($proxy_key eq $rproxy_key) && $proxy_key ne ""))) { + &printEvent("CONTROL", "Command received: ".$data[1], 1); + if ($proxy_s_peerhost ne "" && $proxy_s_peerport ne "") { + $address = $proxy_s_peerhost; + $port = $proxy_s_peerport; + } else { + $address = $s_peerhost; + $port = $s_peerport; + } + + $s_addr = "$address:$port"; + + my $dest = sockaddr_in($port, inet_aton($address)); + if ($data[1] eq "HEARTBEAT") { + my $msg = "Heartbeat OK"; + $bytes = send($::s_socket, $msg, 0, $dest); + &printEvent("CONTROL", "Send heartbeat status to frontend at '$address:$port'", 1); + } else { + my $msg = "OK, EXECUTING COMMAND: ".$data[1]; + $bytes = send($::s_socket, $msg, 0, $dest); + &printEvent("CONTROL", "Sent $bytes bytes to frontend at '$address:$port'", 1); + } + + if ($data[1] eq "RELOAD") { + &printEvent("CONTROL", "Re-Reading Configuration by request from Frontend...", 1); + &reloadConfiguration; + } + + if ($data[1] eq "KILL") { + &printEvent("CONTROL", "SHUTTING DOWN SCRIPT", 1); + &flushAll; + die "Exit script by request"; + } + + next; + } + $s_output =~ s/[\r\n\0]//g; # remove naughty characters + $s_output =~ s/\[No.C-D\]//g; # remove [No C-D] tag + $s_output =~ s/\[OLD.C-D\]//g; # remove [OLD C-D] tag + $s_output =~ s/\[NOCL\]//g; # remove [NOCL] tag + + # Get the server info, if we know the server, otherwise ignore the data + if (!defined($g_servers{$s_addr})) { + if (($g_onlyconfig_servers == 1) && (!defined($g_config_servers{$s_addr}))) { + # HELLRAISER disabled this for testing + &printEvent(997, "NOT ALLOWED SERVER: " . $s_output); + next; + } elsif (!defined($g_config_servers{$s_addr})) { # create std cfg. + my %std_cfg; + $std_cfg{"MinPlayers"} = 6; + $std_cfg{"HLStatsURL"} = ""; + $std_cfg{"DisplayResultsInBrowser"} = 0; + $std_cfg{"BroadCastEvents"} = 0; + $std_cfg{"BroadCastPlayerActions"} = 0; + $std_cfg{"BroadCastEventsCommand"} = "say"; + $std_cfg{"BroadCastEventsCommandAnnounce"} = "say", + $std_cfg{"PlayerEvents"} = 1; + $std_cfg{"PlayerEventsCommand"} = "say"; + $std_cfg{"PlayerEventsCommandOSD"} = "", + $std_cfg{"PlayerEventsCommandHint"} = "", + $std_cfg{"PlayerEventsAdminCommand"} = ""; + $std_cfg{"ShowStats"} = 1; + $std_cfg{"TKPenalty"} = 50; + $std_cfg{"SuicidePenalty"} = 5; + $std_cfg{"AutoTeamBalance"} = 0; + $std_cfg{"AutobanRetry"} = 0; + $std_cfg{"TrackServerLoad"} = 0; + $std_cfg{"MinimumPlayersRank"} = 0; + $std_cfg{"EnablePublicCommands"} = 1; + $std_cfg{"Admins"} = ""; + $std_cfg{"SwitchAdmins"} = 0; + $std_cfg{"IgnoreBots"} = 1; + $std_cfg{"SkillMode"} = 0; + $std_cfg{"GameType"} = 0; + $std_cfg{"Mod"} = ""; + $std_cfg{"BonusRoundIgnore"} = 0; + $std_cfg{"BonusRoundTime"} = 20; + $std_cfg{"UpdateHostname"} = 0; + $std_cfg{"ConnectAnnounce"} = 1; + $std_cfg{"DefaultDisplayEvents"} = 1; + %{$g_config_servers{$s_addr}} = %std_cfg; + &printEvent("CFG", "Created default config for unknown server [$s_addr]"); + &printEvent("DETECT", "New server with game: " . &getServerMod($s_peerhost, $s_peerport)); + } + + if ($g_config_servers{$s_addr}) { + my $tempsrv = &getServer($s_peerhost, $s_peerport); + next if ($tempsrv == 0); + $g_servers{$s_addr} = $tempsrv; + my %s_cfg = %{$g_config_servers{$s_addr}}; + $g_servers{$s_addr}->set("minplayers", $s_cfg{"MinPlayers"}); + $g_servers{$s_addr}->set("hlstats_url", $s_cfg{"HLStatsURL"}); + if ($s_cfg{"DisplayResultsInBrowser"} > 0) { + $g_servers{$s_addr}->set("use_browser", 1); + &printEvent("SERVER", "Query results will displayed in valve browser", 1); + } else { + $g_servers{$s_addr}->set("use_browser", 0); + &printEvent("SERVER", "Query results will not displayed in valve browser", 1); + } + if ($s_cfg{"ShowStats"} == 1) { + $g_servers{$s_addr}->set("show_stats", 1); + &printEvent("SERVER", "Showing stats is enabled", 1); + } else { + $g_servers{$s_addr}->set("show_stats", 0); + &printEvent("SERVER", "Showing stats is disabled", 1); + } + if ($s_cfg{"BroadCastEvents"} == 1) { + $g_servers{$s_addr}->set("broadcasting_events", 1); + $g_servers{$s_addr}->set("broadcasting_player_actions", $s_cfg{"BroadCastPlayerActions"}); + $g_servers{$s_addr}->set("broadcasting_command", $s_cfg{"BroadCastEventsCommand"}); + if ($s_cfg{"BroadCastEventsCommandAnnounce"} eq "ma_hlx_csay") { + $s_cfg{"BroadCastEventsCommandAnnounce"} = $s_cfg{"BroadCastEventsCommandAnnounce"}." #all"; + } + $g_servers{$s_addr}->set("broadcasting_command_announce", $s_cfg{"BroadCastEventsCommandAnnounce"}); + + &printEvent("SERVER", "Broadcasting Live-Events with \"".$s_cfg{"BroadCastEventsCommand"}."\" is enabled", 1); + if ($s_cfg{"BroadCastEventsCommandAnnounce"} ne "") { + &printEvent("SERVER", "Broadcasting Announcements with \"".$s_cfg{"BroadCastEventsCommandAnnounce"}."\" is enabled", 1); + } + } else { + $g_servers{$s_addr}->set("broadcasting_events", 0); + &printEvent("SERVER", "Broadcasting Live-Events is disabled", 1); + } + if ($s_cfg{"PlayerEvents"} == 1) { + $g_servers{$s_addr}->set("player_events", 1); + $g_servers{$s_addr}->set("player_command", $s_cfg{"PlayerEventsCommand"}); + $g_servers{$s_addr}->set("player_command_osd", $s_cfg{"PlayerEventsCommandOSD"}); + $g_servers{$s_addr}->set("player_command_hint", $s_cfg{"PlayerEventsCommandHint"}); + $g_servers{$s_addr}->set("player_admin_command", $s_cfg{"PlayerEventsAdminCommand"}); + &printEvent("SERVER", "Player Event-Handler with \"".$s_cfg{"PlayerEventsCommand"}."\" is enabled", 1); + if ($s_cfg{"PlayerEventsCommandOSD"} ne "") { + &printEvent("SERVER", "Displaying amx style menu with \"".$s_cfg{"PlayerEventsCommandOSD"}."\" is enabled", 1); + } + } else { + $g_servers{$s_addr}->set("player_events", 0); + &printEvent("SERVER", "Player Event-Handler is disabled", 1); + } + if ($s_cfg{"DefaultDisplayEvents"} > 0) { + $g_servers{$s_addr}->set("default_display_events", "1"); + &printEvent("SERVER", "New Players defaulting to show event messages", 1); + } else { + $g_servers{$s_addr}->set("default_display_events", "0"); + &printEvent("SERVER", "New Players defaulting to NOT show event messages", 1); + } + if ($s_cfg{"TrackServerLoad"} > 0) { + $g_servers{$s_addr}->set("track_server_load", "1"); + &printEvent("SERVER", "Tracking server load is enabled", 1); + } else { + $g_servers{$s_addr}->set("track_server_load", "0"); + &printEvent("SERVER", "Tracking server load is disabled", 1); + } + + if ($s_cfg{"TKPenalty"} > 0) { + $g_servers{$s_addr}->set("tk_penalty", $s_cfg{"TKPenalty"}); + &printEvent("SERVER", "Penalty team kills with ".$s_cfg{"TKPenalty"}." points", 1); + } + if ($s_cfg{"SuicidePenalty"} > 0) { + $g_servers{$s_addr}->set("suicide_penalty", $s_cfg{"SuicidePenalty"}); + &printEvent("SERVER", "Penalty suicides with ".$s_cfg{"SuicidePenalty"}." points", 1); + } + if ($s_cfg{"BonusRoundTime"} > 0) { + $g_servers{$s_addr}->set("bonusroundtime", $s_cfg{"BonusRoundTime"}); + &printEvent("SERVER", "Bonus Round time set to: ".$s_cfg{"BonusRoundTime"}, 1); + } + if ($s_cfg{"BonusRoundIgnore"} > 0) { + $g_servers{$s_addr}->set("bonusroundignore", $s_cfg{"BonusRoundIgnore"}); + &printEvent("SERVER", "Bonus Round is being ignored. Length: (".$s_cfg{"BonusRoundTime"}.")", 1); + } + if ($s_cfg{"AutoTeamBalance"} > 0) { + $g_servers{$s_addr}->set("ba_enabled", "1"); + &printEvent("TEAMS", "Auto-Team-Balancing is enabled", 1); + } else { + $g_servers{$s_addr}->set("ba_enabled", "0"); + &printEvent("TEAMS", "Auto-Team-Balancing is disabled", 1); + } + if ($s_cfg{"AutoBanRetry"} > 0) { + $g_servers{$s_addr}->set("auto_ban", "1"); + &printEvent("TEAMS", "Auto-Retry-Banning is enabled", 1); + } else { + $g_servers{$s_addr}->set("auto_ban", "0"); + &printEvent("TEAMS", "Auto-Retry-Banning is disabled", 1); + } + + if ($s_cfg{"MinimumPlayersRank"} > 0) { + $g_servers{$s_addr}->set("min_players_rank", $s_cfg{"MinimumPlayersRank"}); + &printEvent("SERVER", "Requires minimum players rank is enabled [MinPos:".$s_cfg{"MinimumPlayersRank"}."]", 1); + } else { + $g_servers{$s_addr}->set("min_players_rank", "0"); + &printEvent("SERVER", "Requires minimum players rank is disabled", 1); + } + + if ($s_cfg{"EnablePublicCommands"} > 0) { + $g_servers{$s_addr}->set("public_commands", $s_cfg{"EnablePublicCommands"}); + &printEvent("SERVER", "Public chat commands are enabled", 1); + } else { + $g_servers{$s_addr}->set("public_commands", "0"); + &printEvent("SERVER", "Public chat commands are disabled", 1); + } + + if ($s_cfg{"Admins"} ne "") { + @{$g_servers{$s_addr}->{admins}} = split(/,/, $s_cfg{"Admins"}); + foreach(@{$g_servers{$s_addr}->{admins}}) + { + $_ =~ s/^STEAM_[0-9]+?\://i; + } + &printEvent("SERVER", "Admins: ".$s_cfg{"Admins"}, 1); + } + + if ($s_cfg{"SwitchAdmins"} > 0) { + $g_servers{$s_addr}->set("switch_admins", "1"); + &printEvent("TEAMS", "Switching Admins on Auto-Team-Balance is enabled", 1); + } else { + $g_servers{$s_addr}->set("switch_admins", "0"); + &printEvent("TEAMS", "Switching Admins on Auto-Team-Balance is disabled", 1); + } + + if ($s_cfg{"IgnoreBots"} > 0) { + $g_servers{$s_addr}->set("ignore_bots", "1"); + &printEvent("SERVER", "Ignoring bots is enabled", 1); + } else { + $g_servers{$s_addr}->set("ignore_bots", "0"); + &printEvent("SERVER", "Ignoring bots is disabled", 1); + } + $g_servers{$s_addr}->set("skill_mode", $s_cfg{"SkillMode"}); + &printEvent("SERVER", "Using skill mode ".$s_cfg{"SkillMode"}, 1); + + if ($s_cfg{"GameType"} == 1) { + $g_servers{$s_addr}->set("game_type", $s_cfg{"GameType"}); + &printEvent("SERVER", "Game type: Counter-Strike: Source - Deathmatch", 1); + } else { + $g_servers{$s_addr}->set("game_type", "0"); + &printEvent("SERVER", "Game type: Normal", 1); + } + + $g_servers{$s_addr}->set("mod", $s_cfg{"Mod"}); + + if ($s_cfg{"Mod"} ne "") { + &printEvent("SERVER", "Using plugin ".$s_cfg{"Mod"}." for internal functions!", 1); + } + if ($s_cfg{"ConnectAnnounce"} == 1) { + $g_servers{$s_addr}->set("connect_announce", $s_cfg{"ConnectAnnounce"}); + &printEvent("SERVER", "Connect Announce is enabled", 1); + } else { + $g_servers{$s_addr}->set("connect_announce", "0"); + &printEvent("SERVER", "Connect Announce is disabled", 1); + } + if ($s_cfg{"UpdateHostname"} == 1) { + $g_servers{$s_addr}->set("update_hostname", $s_cfg{"UpdateHostname"}); + &printEvent("SERVER", "Auto-updating hostname is enabled", 1); + } else { + $g_servers{$s_addr}->set("update_hostname", "0"); + &printEvent("SERVER", "Auto-updating hostname is disabled", 1); + } + $g_servers{$s_addr}->get_game_mod_opts(); + } + } + + if (!$g_servers{$s_addr}->{"srv_players"}) + { + $g_servers{$s_addr}->{"srv_players"} = (); + %g_players=(); + } + else + { + %g_players=%{$g_servers{$s_addr}->{"srv_players"}}; + } + + # Get the datestamp (or complain) + #if ($s_output =~ s/^.*L (\d\d)\/(\d\d)\/(\d{4}) - (\d\d):(\d\d):(\d\d):\s*//) + + #$is_streamed = 0; + #$test_for_date = 0; + #$is_streamed = ($s_output !~ m/^L\s*/); + + #if ( !$is_streamed ) { + # $test_for_date = ($s_output =~ s/^L (\d\d)\/(\d\d)\/(\d{4}) - (\d\d):(\d\d):(\d\d):\s*//); + #} else { + # $test_for_date = ($s_output =~ s/^\S*L (\d\d)\/(\d\d)\/(\d{4}) - (\d\d):(\d\d):(\d\d):\s*//); + #} + + #if ($test_for_date) + + # EXPLOIT FIX + + if ($s_output =~ s/^(?:.*?)?L (\d\d)\/(\d\d)\/(\d{4}) - (\d\d):(\d\d):(\d\d):\s*//) { + $ev_month = $1; + $ev_day = $2; + $ev_year = $3; + $ev_hour = $4; + $ev_min = $5; + $ev_sec = $6; + $ev_time = "$ev_hour:$ev_min:$ev_sec"; + $ev_remotetime = timelocal($ev_sec,$ev_min,$ev_hour,$ev_day,$ev_month-1,$ev_year); + + if ($g_timestamp) { + $ev_timestamp = "$ev_year-$ev_month-$ev_day $ev_time"; + $ev_unixtime = $ev_remotetime; + if ($g_stdin) + { + $ev_daemontime = $ev_unixtime; + } + } + } else { + &printEvent(998, "MALFORMED DATA: " . $s_output); + next; + } + + if ($g_debug >= 4) { + print $s_addr.": \"".$s_output."\"\n"; + } + + if (($g_stdin == 0) && ($g_servers{$s_addr}->{last_event} > 0) && ( ($ev_unixtime - $g_servers{$s_addr}->{last_event}) > 299) ) { + $g_servers{$s_addr}->set("map", ""); + $g_servers{$s_addr}->get_map(); + } + + $g_servers{$s_addr}->set("last_event", $ev_unixtime); + + # Now we parse the events. + + my $ev_type = 0; + my $ev_status = ""; + my $ev_team = ""; + my $ev_player = 0; + my $ev_verb = ""; + my $ev_obj_a = ""; + my $ev_obj_b = ""; + my $ev_obj_c = ""; + my $ev_properties = ""; + my %ev_properties = (); + my %ev_player = (); + + # pvkii parrot log lines also fit the death line parsing + if ($g_servers{$s_addr}->{play_game} == PVKII() + && $s_output =~ /^ + "(.+?(?:<[^>]*>){3})" # player string + \s[a-z]{6}\s # 'killed' + "npc_parrot<.+?>" # parrot string + \s[a-z]{5}\s[a-z]{2}\s # 'owned by' + "(.+?(?:<[^>]*>){3})" # owner string + \s[a-z]{4}\s # 'with' + "([^"]*)" #weapon + (.*) #properties + /x) + { + $ev_player = $1; # player + $ev_obj_b = $2; # victim + $ev_obj_c = $3; # weapon + $ev_properties = $4; + %ev_properties_hash = &getProperties($ev_properties); + + my $playerinfo = &getPlayerInfo($ev_player, 1); + my $victiminfo = &getPlayerInfo($ev_obj_b, 1); + $ev_type = 10; + + if ($playerinfo) { + if ($victiminfo) { + $ev_status = &doEvent_PlayerPlayerAction( + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + $victiminfo->{"userid"}, + $victiminfo->{"uniqueid"}, + "killed_parrot", + undef, + undef, + undef, + undef, + undef, + undef, + &getProperties($ev_properties) + ); + } + + $ev_type = 11; + + $ev_status = &doEvent_PlayerAction( + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + "killed_parrot", + undef, + undef, + undef, + &getProperties($ev_properties) + ); + } + } elsif ($s_output =~ /^ + (?:\(DEATH\))? # l4d prefix, such as (DEATH) or (INCAP) + "(.+?(?:<.+?>)*? + (?:)*? + (?:{"userid"}; + my $killerUniqueId = $killerinfo->{"uniqueid"}; + my $killer = lookupPlayer($s_addr, $killerId, $killerUniqueId); + + # octo + if($killer->{role} eq "scout") { + $ev_status = &doEvent_PlayerAction( + $killerinfo->{"userid"}, + $killerinfo->{"uniqueid"}, + "kill_as_scout", + "kill_as_scout" + ); + } + if($killer->{role} eq "spy") { + $ev_status = &doEvent_PlayerAction( + $killerinfo->{"userid"}, + $killerinfo->{"uniqueid"}, + "kill_as_spy", + "kill_as_spy" + ); + } + + my $victimId = $victiminfo->{"userid"}; + my $victimUniqueId = $victiminfo->{"uniqueid"}; + my $victim = lookupPlayer($s_addr, $victimId, $victimUniqueId); + + $ev_status = &doEvent_Frag( + $killerinfo->{"userid"}, + $killerinfo->{"uniqueid"}, + $victiminfo->{"userid"}, + $victiminfo->{"uniqueid"}, + $ev_obj_b, + $headshot, + $ev_Xcoord, + $ev_Ycoord, + $ev_Zcoord, + $ev_XcoordKV, + $ev_YcoordKV, + $ev_ZcoordKV, + %ev_properties_hash + ); + } + } elsif ($g_servers{$s_addr}->{play_game} == L4D() && $s_output =~ /^ + \(INCAP\) # l4d prefix, such as (DEATH) or (INCAP) + "(.+?(?:<.+?>)*? + )*? + {team} eq "Infected") { + $victiminfo = undef; + } + $ev_type = 800; + + $headshot = 0; + if ($ev_properties =~ m/headshot/) { + $headshot = 1; + } + if ($killerinfo && $victiminfo) { + my $killerId = $killerinfo->{"userid"}; + my $killerUniqueId = $killerinfo->{"uniqueid"}; + my $killer = lookupPlayer($s_addr, $killerId, $killerUniqueId); + + my $victimId = $victiminfo->{"userid"}; + my $victimUniqueId = $victiminfo->{"uniqueid"}; + my $victim = lookupPlayer($s_addr, $victimId, $victimUniqueId); + + $ev_status = &doEvent_Frag( + $killerinfo->{"userid"}, + $killerinfo->{"uniqueid"}, + $victiminfo->{"userid"}, + $victiminfo->{"uniqueid"}, + $ev_obj_b, + $headshot, + $ev_l4dXcoord, + $ev_l4dYcoord, + $ev_l4dZcoord, + $ev_l4dXcoordKV, + $ev_l4dYcoordKV, + $ev_l4dZcoordKV, + &getProperties($ev_properties) + ); + } + } elsif ($g_servers{$s_addr}->{play_game} == L4D() && $s_output =~ /^\(TONGUE\)\sTongue\sgrab\sstarting\.\s+Smoker:"(.+?(?:<.+?>)*?(?:|)*?(?:|{"userid"}, + $playerinfo->{"uniqueid"}, + "tongue_grab" + ); + } + if ($playerinfo && $victiminfo) { + $ev_status = &doEvent_PlayerPlayerAction( + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + $victiminfo->{"userid"}, + $victiminfo->{"uniqueid"}, + "tongue_grab", + $ev_l4dXcoord, + $ev_l4dYcoord, + $ev_l4dZcoord, + $ev_l4dXcoordV, + $ev_l4dYcoordV, + $ev_l4dZcoordV + ); + } + } elsif ($s_output =~ /^ + "(.+?(?:<.+?>)*? + )" # player string + \s(triggered(?:\sa)?)\s # verb (ex. attacked, killed, triggered) + "(.+?(?:<.+?>)*? + )" # player string as above or action name + \s[a-zA-Z]+\s # (ex. with, against) + "(.+?(?:<.+?>)*? + )" # player string as above or weapon name + (?:\s[a-zA-Z]+\s"(.+?)")? # weapon name on plyrplyr actions + (.*) #properties + /x) + { + + # 10. Player-Player Actions + + # no l4d/2 actions are logged with the locations (in fact, very few are logged period) so the l4d/2 location parsing can be skipped + + $ev_player = $1; + $ev_verb = $2; # triggered or triggered a + $ev_obj_a = $3; # action + $ev_obj_b = $4; # victim + $ev_obj_c = $5; # weapon (optional) + $ev_properties = $6; + %ev_properties_hash = &getProperties($ev_properties); + + + if ($ev_verb eq "triggered") { # it's either 'triggered' or 'triggered a' + + my $playerinfo = &getPlayerInfo($ev_player, 1); + my $victiminfo = &getPlayerInfo($ev_obj_b, 1); + $ev_type = 10; + + if ($playerinfo) { + if ($victiminfo) { + $ev_status = &doEvent_PlayerPlayerAction( + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + $victiminfo->{"userid"}, + $victiminfo->{"uniqueid"}, + $ev_obj_a, + undef, + undef, + undef, + undef, + undef, + undef, + &getProperties($ev_properties) + ); + } + + $ev_type = 11; + + $ev_status = &doEvent_PlayerAction( + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + $ev_obj_a, + undef, + undef, + undef, + &getProperties($ev_properties) + ); + } + } else { + my $playerinfo = &getPlayerInfo($ev_player, 1); + $ev_type = 11; + + if ($playerinfo) { + $ev_status = &doEvent_PlayerAction( + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + $ev_obj_a, + undef, + undef, + undef, + &getProperties($ev_properties) + ); + } + } + } elsif ( $s_output =~ /^(?:\[STATSME\] )?"(.+?(?:<.+?>)*)" triggered "(weaponstats\d{0,1})"(.*)$/ ) { + # Prototype: [STATSME] "player" triggered "weaponstats?"[properties] + # Matches: + # 501. Statsme weaponstats + # 502. Statsme weaponstats2 + + $ev_player = $1; + $ev_verb = $2; # weaponstats; weaponstats2 + $ev_properties = $3; + %ev_properties = &getProperties($ev_properties); + + if (like($ev_verb, "weaponstats")) { + $ev_type = 501; + my $playerinfo = &getPlayerInfo($ev_player, 0); + + if ($playerinfo) { + my $playerId = $playerinfo->{"userid"}; + my $playerUniqueId = $playerinfo->{"uniqueid"}; + my $ingame = 0; + + $ingame = 1 if (lookupPlayer($s_addr, $playerId, $playerUniqueId)); + + if (!$ingame) { + &getPlayerInfo($ev_player, 1); + } + + $ev_status = &doEvent_Statsme( + $playerId, + $playerUniqueId, + $ev_properties{"weapon"}, + $ev_properties{"shots"}, + $ev_properties{"hits"}, + $ev_properties{"headshots"}, + $ev_properties{"damage"}, + $ev_properties{"kills"}, + $ev_properties{"deaths"} + ); + + if (!$ingame) { + &doEvent_Disconnect( + $playerId, + $playerUniqueId, + "" + ); + } + } + } elsif (like($ev_verb, "weaponstats2")) { + $ev_type = 502; + my $playerinfo = &getPlayerInfo($ev_player, 0); + + if ($playerinfo) { + my $playerId = $playerinfo->{"userid"}; + my $playerUniqueId = $playerinfo->{"uniqueid"}; + my $ingame = 0; + + $ingame = 1 if (lookupPlayer($s_addr, $playerId, $playerUniqueId)); + + if (!$ingame) { + &getPlayerInfo($ev_player, 1); + } + + $ev_status = &doEvent_Statsme2( + $playerId, + $playerUniqueId, + $ev_properties{"weapon"}, + $ev_properties{"head"}, + $ev_properties{"chest"}, + $ev_properties{"stomach"}, + $ev_properties{"leftarm"}, + $ev_properties{"rightarm"}, + $ev_properties{"leftleg"}, + $ev_properties{"rightleg"} + ); + + if (!$ingame) { + &doEvent_Disconnect( + $playerId, + $playerUniqueId, + "" + ); + } + } + } + } elsif ( $s_output =~ /^(?:\[STATSME\] )?"(.+?(?:<.+?>)*)" triggered "(latency|time)"(.*)$/ ) { + # Prototype: [STATSME] "player" triggered "latency|time"[properties] + # Matches: + # 503. Statsme latency + # 504. Statsme time + + $ev_player = $1; + $ev_verb = $2; # latency; time + $ev_properties = $3; + %ev_properties = &getProperties($ev_properties); + + if ($ev_verb eq "time") { + $ev_type = 504; + my $playerinfo = &getPlayerInfo($ev_player, 0); + + if ($playerinfo) { + my ($min, $sec) = split(/:/, $ev_properties{"time"}); + my $hour = sprintf("%d", $min / 60); + + if ($hour) { + $min = $min % 60; + } + + $ev_status = &doEvent_Statsme_Time( + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + "$hour:$min:$sec" + ); + } + } else { # latency + $ev_type = 503; + my $playerinfo = &getPlayerInfo($ev_player, 0); + + if ($playerinfo) { + $ev_status = &doEvent_Statsme_Latency( + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + $ev_properties{"ping"} + ); + } + } + } elsif ($s_output =~ /^"(.+?(?:<.+?>)*?)" ([a-zA-Z,_\s]+) "(.+?)"(.*)$/) { + # Prototype: "player" verb "obj_a"[properties] + # Matches: + # 1. Connection + # 4. Suicides + # 5. Team Selection + # 6. Role Selection + # 7. Change Name + # 11. Player Objectives/Actions + # 14. a) Chat; b) Team Chat + + $ev_player = $1; + $ev_verb = $2; + $ev_obj_a = $3; + $ev_properties = $4; + %ev_properties = &getProperties($ev_properties); + + if ($ev_verb eq "connected, address") { + my $ipAddr = $ev_obj_a; + my $playerinfo; + + if ($ipAddr =~ /([\d\.]+):(\d+)/) { + $ipAddr = $1; + } + + $playerinfo = &getPlayerInfo($ev_player, 1, $ipAddr); + + $ev_type = 1; + + if ($playerinfo) { + if (($playerinfo->{"uniqueid"} =~ /UNKNOWN/) || ($playerinfo->{"uniqueid"} =~ /PENDING/) || ($playerinfo->{"uniqueid"} =~ /VALVE_ID_LAN/)) { + $ev_status = "(DELAYING CONNECTION): $s_output"; + + if ($g_mode ne "LAN") { + my $p_name = $playerinfo->{"name"}; + my $p_userid = $playerinfo->{"userid"}; + &printEvent("SERVER", "LATE CONNECT [$p_name/$p_userid] - STEAM_ID_PENDING"); + $g_preconnect->{"$s_addr/$p_userid/$p_name"} = { + ipaddress => $ipAddr, + name => $p_name, + server => $s_addr, + timestamp => $ev_daemontime + }; + } + } else { + $ev_status = &doEvent_Connect( + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + $ipAddr + ); + } + } + } elsif ($ev_verb eq "committed suicide with") { + my $playerinfo = &getPlayerInfo($ev_player, 1); + + $ev_type = 4; + + if ($playerinfo) { + $ev_status = &doEvent_Suicide( + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + $ev_obj_a, + %ev_properties + ); + } + } elsif ($ev_verb eq "joined team") { + my $playerinfo = &getPlayerInfo($ev_player, 1); + + $ev_type = 5; + + if ($playerinfo) { + $ev_status = &doEvent_TeamSelection( + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + $ev_obj_a + ); + } + } elsif ($ev_verb eq "changed role to") { + my $playerinfo = &getPlayerInfo($ev_player, 1); + + $ev_type = 6; + + if ($playerinfo) { + $ev_status = &doEvent_RoleSelection( + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + $ev_obj_a + ); + } + } elsif ($ev_verb eq "changed name to") { + my $playerinfo = &getPlayerInfo($ev_player, 1); + + $ev_type = 7; + + if ($playerinfo) { + $ev_status = &doEvent_ChangeName( + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + $ev_obj_a + ); + } + } elsif ($ev_verb eq "triggered") { + + # in cs:s players dropp the bomb if they are the only ts + # and disconnect...the dropp the bomb after they disconnected :/ + if ($ev_obj_a eq "Dropped_The_Bomb") { + $playerinfo = &getPlayerInfo($ev_player, 0); + } else { + $playerinfo = &getPlayerInfo($ev_player, 1); + } + if ($playerinfo) { + if ($ev_obj_a eq "player_changeclass" && defined($ev_properties{newclass})) { + + $ev_type = 6; + + $ev_status = &doEvent_RoleSelection( + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + $ev_properties{newclass} + ); + } else { + if ($g_servers{$s_addr}->{play_game} == TFC()) + { + if ($ev_obj_a eq "Sentry_Destroyed") + { + $ev_obj_a = "Sentry_Dismantle"; + } + elsif ($ev_obj_a eq "Dispenser_Destroyed") + { + $ev_obj_a = "Dispenser_Dismantle"; + } + elsif ($ev_obj_a eq "Teleporter_Entrance_Destroyed") + { + $ev_obj_a = "Teleporter_Entrance_Dismantle" + } + elsif ($ev_obj_a eq "Teleporter_Exit_Destroyed") + { + $ev_obj_a = "Teleporter_Exit_Dismantle" + } + } + + $ev_type = 11; + + $ev_status = &doEvent_PlayerAction( + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + $ev_obj_a, + undef, + undef, + undef, + %ev_properties + ); + } + } + } elsif ($ev_verb eq "triggered a") { + my $playerinfo = &getPlayerInfo($ev_player, 1); + + $ev_type = 11; + + if ($playerinfo) + { + $ev_status = &doEvent_PlayerAction( + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + $ev_obj_a, + undef, + undef, + undef, + %ev_properties + ); + } + } elsif ($ev_verb eq "say" || $ev_verb eq "say_team" || $ev_verb eq "say_squad") { + my $playerinfo = &getPlayerInfo($ev_player, 1); + + $ev_type = 14; + + if ($playerinfo) { + $ev_status = &doEvent_Chat( + $ev_verb, + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + $ev_obj_a + ); + } + } + } elsif ($s_output =~ /^(?:Kick: )?"(.+?(?:<.+?>)*)" ([^\(]+)(.*)$/) { + # Prototype: "player" verb[properties] + # Matches: + # 2. Enter Game + # 3. Disconnection + + $ev_player = $1; + $ev_verb = $2; + $ev_properties = $3; + %ev_properties = &getProperties($ev_properties); + + if (like($ev_verb, "entered the game")) { + my $playerinfo = &getPlayerInfo($ev_player, 1); + + if ($playerinfo) { + $ev_type = 2; + $ev_status = &doEvent_EnterGame($playerinfo->{"userid"}, $playerinfo->{"uniqueid"}, $ev_obj_a); + } + } elsif (like($ev_verb, "disconnected") || like($ev_verb, "was kicked")) { + my $playerinfo = &getPlayerInfo($ev_player, 0); + + if ($playerinfo) { + $ev_type = 3; + + $userid = $playerinfo->{userid}; + $uniqueid = $playerinfo->{uniqueid}; + + $ev_status = &doEvent_Disconnect( + $playerinfo->{userid}, + $playerinfo->{uniqueid}, + $ev_properties + ); + } + } + elsif (like($ev_verb, "STEAM USERID validated") || like($ev_verb, "VALVE USERID validated")) { + my $playerinfo = &getPlayerInfo($ev_player, 0); + + if ($playerinfo) { + $ev_type = 1; + + } + } + } elsif ($s_output =~ /^Team "(.+?)" ([^"\(]+) "([^"]+)"(.*)$/) { + # Prototype: Team "team" verb "obj_a"[properties] + # Matches: + # 12. Team Objectives/Actions + # 1200. Team Objective With Players involved + # 15. Team Alliances + + $ev_team = $1; + $ev_verb = $2; + $ev_obj_a = $3; + $ev_properties = $4; + %ev_properties_hash = &getProperties($ev_properties); + + if ($ev_obj_a eq "pointcaptured") { + $numcappers = $ev_properties_hash{numcappers}; + if ($g_debug > 1) { + print "NumCappers = ".$numcappers."\n"; + } + foreach ($i = 1; $i <= $numcappers; $i++) { + # reward each player involved in capturing + $player = $ev_properties_hash{"player".$i}; + if ($g_debug > 1) { + print $i." -> ".$player."\n"; + } + #$position = $ev_properties_hash{"position".$i}; + my $playerinfo = &getPlayerInfo($player, 1); + if ($playerinfo) { + $ev_status = &doEvent_PlayerAction( + $playerinfo->{"userid"}, + $playerinfo->{"uniqueid"}, + $ev_obj_a, + "", + "", + "", + &getProperties($ev_properties) + ); + } + } + } + if ($ev_obj_a eq "captured_loc") { + # $flag_name = $ev_properties_hash{flagname}; + $player_a = $ev_properties_hash{player_a}; + $player_b = $ev_properties_hash{player_b}; + + my $playerinfo_a = &getPlayerInfo($player_a, 1); + if ($playerinfo_a) { + $ev_status = &doEvent_PlayerAction( + $playerinfo_a->{"userid"}, + $playerinfo_a->{"uniqueid"}, + $ev_obj_a, + "", + "", + "", + &getProperties($ev_properties) + ); + } + + my $playerinfo_b = &getPlayerInfo($player_b, 1); + if ($playerinfo_b) { + $ev_status = &doEvent_PlayerAction( + $playerinfo_b->{"userid"}, + $playerinfo_b->{"uniqueid"}, + $ev_obj_a, + "", + "", + "", + &getProperties($ev_properties) + ); + } + } + + if (like($ev_verb, "triggered")) { + if ($ev_obj_a ne "captured_loc") { + $ev_type = 12; + $ev_status = &doEvent_TeamAction( + $ev_team, + $ev_obj_a, + &getProperties($ev_properties) + ); + } + } elsif (like($ev_verb, "triggered a")) { + $ev_type = 12; + $ev_status = &doEvent_TeamAction( + $ev_team, + $ev_obj_a + ); + } + } elsif ($s_output =~ /^(Rcon|Bad Rcon): "rcon [^"]+"([^"]+)"\s+(.+)" from "([0-9\.]+?):(\d+?)"(.*)$/) { + # Prototype: verb: "rcon ?..."obj_a" obj_b" from "obj_c"[properties] + # Matches: + # 20. HL1 a) Rcon; b) Bad Rcon + + $ev_verb = $1; + $ev_obj_a = $2; # password + $ev_obj_b = $3; # command + $ev_obj_c = $4; # ip + $ev_obj_d = $5; # port + $ev_properties = $6; + %ev_properties = &getProperties($ev_properties); + if ($g_rcon_ignoreself == 0 || $ev_obj_c ne $s_ip) { + $ev_obj_b = substr($ev_obj_b, 0, 255); + if (like($ev_verb, "Rcon")) { + $ev_type = 20; + $ev_status = &doEvent_Rcon( + "OK", + $ev_obj_b, + "", + $ev_obj_c + ); + } elsif (like($ev_verb, "Bad Rcon")) { + $ev_type = 20; + $ev_status = &doEvent_Rcon( + "BAD", + $ev_obj_b, + $ev_obj_a, + $ev_obj_c + ); + } + } else { + $ev_status = "(IGNORED) Rcon from \"$ev_obj_a:$ev_obj_b\": \"$ev_obj_c\""; + } + } elsif ($s_output =~ /^rcon from "(.+?):(.+?)": (?:command "(.*)".*|(Bad) Password)$/) { + # Prototype: verb: "rcon ?..."obj_a" obj_b" from "obj_c"[properties] + # Matches: + # 20. a) Rcon; + + $ev_obj_a = $1; # ip + $ev_obj_b = $2; # port + $ev_obj_c = $3; # command + $ev_isbad = $4; # if bad, "Bad" + if ($g_rcon_ignoreself == 0 || $ev_obj_a ne $s_ip) { + if ($ev_isbad ne "Bad") { + $ev_type = 20; + @cmds = split(/;/,$ev_obj_c); + foreach(@cmds) + { + $ev_status = &doEvent_Rcon( + "OK", + substr($_, 0, 255), + "", + $ev_obj_a + ); + } + } else { + $ev_type = 20; + $ev_status = &doEvent_Rcon( + "BAD", + "", + "", + $ev_obj_a + ); + } + } else { + $ev_status = "(IGNORED) Rcon from \"$ev_obj_a:$ev_obj_b\": \"$ev_obj_c\""; + } + } elsif ($s_output =~ /^\[(.+)\.(smx|amxx)\]\s*(.+)$/i) { + # Prototype: Cmd:[SM] obj_a + # Matches: + # Admin Mod messages + + my $ev_plugin = $1; + my $ev_adminmod = $2; + $ev_obj_a = $3; + $ev_type = 500; + $ev_status = &doEvent_Admin( + (($ev_adminmod eq "smx")?"Sourcemod":"AMXX")." ($ev_plugin)", + substr($ev_obj_a, 0, 255) + ); + } elsif ($s_output =~ /^([^"\(]+) "([^"]+)"(.*)$/) { + # Prototype: verb "obj_a"[properties] + # Matches: + # 13. World Objectives/Actions + # 19. a) Loading map; b) Started map + # 21. Server Name + + $ev_verb = $1; + $ev_obj_a = $2; + $ev_properties = $3; + %ev_properties = &getProperties($ev_properties); + + if (like($ev_verb, "World triggered")) { + $ev_type = 13; + if ($ev_obj_a eq "killlocation") { + $ev_status = &doEvent_Kill_Loc( + %ev_properties + ); + } else { + $ev_status = &doEvent_WorldAction( + $ev_obj_a + ); + if ($ev_obj_a eq "Round_Win" || $ev_obj_a eq "Mini_Round_Win") { + $ev_team = $ev_properties{"winner"}; + $ev_status = &doEvent_TeamAction( + $ev_team, + $ev_obj_a + ); + } + } + } elsif (like($ev_verb, "Loading map")) { + $ev_type = 19; + $ev_status = &doEvent_ChangeMap( + "loading", + $ev_obj_a + ); + } elsif (like($ev_verb, "Started map")) { + $ev_type = 19; + $ev_status = &doEvent_ChangeMap( + "started", + $ev_obj_a + ); + } + } elsif ($s_output =~ /^\[MANI_ADMIN_PLUGIN\]\s*(.+)$/) { + # Prototype: [MANI_ADMIN_PLUGIN] obj_a + # Matches: + # Mani-Admin-Plugin messages + + $ev_obj_a = $1; + $ev_type = 500; + $ev_status = &doEvent_Admin( + "Mani Admin Plugin", + substr($ev_obj_a, 0, 255) + ); + } elsif ($s_output =~ /^\[BeetlesMod\]\s*(.+)$/) { + # Prototype: Cmd:[BeetlesMod] obj_a + # Matches: + # Beetles Mod messages + + $ev_obj_a = $1; + $ev_type = 500; + $ev_status = &doEvent_Admin( + "Beetles Mod", + substr($ev_obj_a, 0, 255) + ); + } elsif ($s_output =~ /^\[ADMIN:(.+)\] ADMIN Command: \1 used command (.+)$/) { + # Prototype: [ADMIN] obj_a + # Matches: + # Admin Mod messages + + $ev_obj_a = $1; + $ev_obj_b = $2; + $ev_type = 500; + $ev_status = &doEvent_Admin( + "Admin Mod", + substr($ev_obj_b, 0, 255), + $ev_obj_a + ); + } elsif ($g_servers{$s_addr}->{play_game} == DYSTOPIA()) { + if ($s_output =~ /^weapon { steam_id: 'STEAM_\d+:(.+?)', weapon_id: (\d+), class: \d+, team: \d+, shots: \((\d+),(\d+)\), hits: \((\d+),(\d+)\), damage: \((\d+),(\d+)\), headshots: \((\d+),(\d+)\), kills: \(\d+,\d+\) }$/ && $g_mode eq "Normal") { + + # Prototype: weapon { steam_id: 'STEAMID', weapon_id: X, class: X, team: X, shots: (X,X), hits: (X,X), damage: (X,X), headshots: (X,X), kills: (X,X) } + # Matches: + # 501. Statsme weaponstats (Dystopia) + + my $steamid = $1; + my $weapon = $2; + my $shots = $3 + $4; + my $hits = $5 + $6; + my $damage = $7 + $8; + my $headshots = $9 + $10; + my $kills = $11 + $12; + + $ev_type = 501; + + my $weapcode = $dysweaponcodes{$weapon}; + + foreach $player (values(%g_players)) { + if ($player->{uniqueid} eq $steamid) { + $ev_status = &doEvent_Statsme( + $player->{"userid"}, + $steamid, + $weapcode, + $shots, + $hits, + $headshots, + $damage, + $kills, + 0 + ); + last; + } + } + } elsif ($s_output =~ /^(?:join|change)_class { steam_id: 'STEAM_\d+:(.+?)', .* (?:new_|)class: (\d+), .* }$/ && $g_mode eq "Normal") { + # Prototype: join_class { steam_id: 'STEAMID', team: X, class: Y, time: ZZZZZZZZZ } + # Matches: + # 6. Role Selection (Dystopia) + + my $steamid = $1; + my $role = $2; + $ev_type = 6; + + foreach $player (values(%g_players)) { + if ($player->{uniqueid} eq $steamid) { + $ev_status = &doEvent_RoleSelection( + $player->{"userid"}, + $steamid, + $role + ); + last; + } + } + } elsif ($s_output =~ /^objective { steam_id: 'STEAM_\d+:(.+?)', class: \d+, team: \d+, objective: '(.+?)', time: \d+ }$/ && $g_mode eq "Normal") { + # Prototype: objective { steam_id: 'STEAMID', class: X, team: X, objective: 'TEXT', time: X } + # Matches: + # 11. Player Action (Dystopia Objectives) + + my $steamid = $1; + my $action = $2; + foreach $player (values(%g_players)) { + if ($player->{uniqueid} eq $steamid) { + $ev_status = &doEvent_PlayerAction( + $player->{"userid"}, + $steamid, + $action + ); + last; + } + } + } + } + + if ($ev_type) { + if ($g_debug > 2) { + print <)*)" was (?:kicked and )?banned "for ([0-9]+).00 minutes" by "Console"$/) || + ($s_output =~ /^Banid: "(.+?(?:<.+?>)*)" was (?:kicked and )?banned "(permanently)" by "Console"$/)) { + + # Prototype: "player" verb[properties] + # Banid: huaaa<1804><>" was kicked and banned "permanently" by "Console" + + $ev_player = $1; + $ev_bantime = $2; + my $playerinfo = &getPlayerInfo($ev_player, 1); + + if ($ev_bantime eq "5") { + &printEvent("BAN", "Auto Ban - ignored"); + } elsif ($playerinfo) { + if (($g_global_banning > 0) && ($g_servers{$s_addr}->{ignore_nextban}->{$playerinfo->{"uniqueid"}} == 1)) { + delete($g_servers{$s_addr}->{ignore_nextban}->{$playerinfo->{"uniqueid"}}); + &printEvent("BAN", "Global Ban - ignored"); + } elsif (!$g_servers{$s_addr}->{ignore_nextban}->{$playerinfo->{"uniqueid"}}) { + my $p_steamid = $playerinfo->{"uniqueid"}; + my $player_obj = lookupPlayer($s_addr, $playerId, $p_steamid); + &printEvent("BAN", "Steamid: ".$p_steamid); + + if ($player_obj) { + $player_obj->{"is_banned"} = 1; + } + if (($p_steamid ne "") && ($playerinfo->{"is_bot"} == 0) && ($playerinfo->{"userid"} > 0)) { + if ($g_global_banning > 0) { + if ($ev_bantime eq "permanently") { + &printEvent("BAN", "Hide player!"); + &execNonQuery("UPDATE hlstats_Players SET hideranking=2 WHERE playerId IN (SELECT playerId FROM hlstats_PlayerUniqueIds WHERE uniqueId='"."eSQL($p_steamid)."')"); + $ev_bantime = 0; + } + my $pl_steamid = $playerinfo->{"plain_uniqueid"}; + while (my($addr, $server) = each(%g_servers)) { + if ($addr ne $s_addr) { + &printEvent("BAN", "Global banning on ".$addr); + $server->{ignore_nextban}->{$p_steamid} = 1; + $server->dorcon("banid ".$ev_bantime." $pl_steamid"); + $server->dorcon("writeid"); + } + } + } + } + } + } else { + &printEvent("BAN", "No playerinfo"); + } + &printEvent("BAN", $s_output); + } else { + # Unrecognized event + # HELLRAISER + if ($g_debug > 1) { + &printEvent(999, "UNRECOGNIZED: " . $s_output); + } + } + + if (!$g_stdin && defined($g_servers{$s_addr}) && $ev_daemontime > $g_servers{$s_addr}->{next_plyr_flush}) { + &printEvent("MYSQL", "Flushing player updates to database...",1); + if ($g_servers{$s_addr}->{"srv_players"}) { + while ( my($pl, $player) = each(%{$g_servers{$s_addr}->{"srv_players"}}) ) { + if ($player->{needsupdate}) { + $player->flushDB(); + } + } + } + &printEvent("MYSQL", "Flushing player updates to database is complete.",1); + + $g_servers{$s_addr}->{next_plyr_flush} = $ev_daemontime + 15+int(rand(15)); + } + + if (($g_stdin == 0) && defined($g_servers{$s_addr})) { + $s_lines = $g_servers{$s_addr}->{lines}; + # get ping from players + if ($s_lines % 1000 == 0) { + $g_servers{$s_addr}->update_players_pings(); + } + + if ($g_servers{$s_addr}->{show_stats} == 1) { + # show stats + if ($s_lines % 2500 == 40) { + $g_servers{$s_addr}->dostats(); + } + } + + if ($s_lines > 500000) { + $g_servers{$s_addr}->set("lines", 0); + } else { + $g_servers{$s_addr}->increment("lines"); + } + } + } else { + $s_addr = ""; + } + + while( my($server) = each(%g_servers)) + { + if($g_servers{$server}->{next_timeout}<$ev_daemontime) + { + #print "checking $ev_unixtime\n"; + # Clean up + # look + if($g_servers{$server}->{"srv_players"}) + { + my %players_temp=%{$g_servers{$server}->{"srv_players"}}; + while ( my($pl, $player) = each(%players_temp) ) { + my $timeout = 250; # 250; + if ($g_mode eq "LAN") { + $timeout = $timeout * 2; + } + #if (($player->{is_bot} == 0) && (($ev_unixtime - $player->get("timestamp")) > ($timeout - 20))) { + # &printEvent(400, $player->getInfoString()." is ".($ev_unixtime - $player->get("timestamp"))."s idle"); + #} + my $userid = $player->{userid}; + my $uniqueid = $player->{uniqueid}; + if ( ($ev_daemontime - $player->{timestamp}) > $timeout ) { + #printf("%s - %s %s\n",$server, $player->{userid}, $player->{uniqueid}); + # we delete any player who is inactive for over $timeout sec + # - they probably disconnected silently somehow. + if (($player->{is_bot} == 0) || ($g_stdin)) { + &printEvent(400, "Auto-disconnecting " . $player->getInfoString() ." for idling (" . ($ev_daemontime - $player->{timestamp}) . " sec) on server (".$server.")"); + removePlayer($server, $userid, $uniqueid); + } + } + } + } + $g_servers{$server}->{next_timeout}=$ev_daemontime+30+rand(30); + } + + if ($ev_daemontime > $g_servers{$server}->{next_flush} + && $g_servers{$server}->{needsupdate} + ) + { + $g_servers{$server}->flushDB(); + $g_servers{$server}->{next_flush} = $ev_daemontime + 20; + } + } + + while ( my($pl, $player) = each(%g_preconnect) ) { + my $timeout = 600; + if ( ($ev_unixtime - $player->{"timestamp"}) > $timeout ) { + &printEvent(401, "Clearing pre-connect entry with key ".$pl); + delete($g_preconnect{$pl}); + } + } + + if ($g_stdin == 0) { + # Track the Trend + if ($g_track_stats_trend > 0) { + track_hlstats_trend(); + } + while (my($addr, $server) = each(%g_servers)) { + if (defined($server)) { + $server->track_server_load(); + } + } + + while( my($table) = each(%g_eventtable_data)) + { + if ($g_eventtable_data{$table}{lastflush} + 30 < $ev_daemontime) + { + flushEventTable($table); + } + } + + if (defined($g_servers{$s_addr})) { + if (($g_servers{$s_addr}->{map} eq "") && (($timeout > 0) && ($timeout % 60 == 0))) { + $g_servers{$s_addr}->get_map(); + } + } + } + + $c++; + $c = 1 if ($c > 500000); + $import_logs_count++ if ($g_stdin); +} + +$end_time = time(); +if ($g_stdin) { + if ($import_logs_count > 0) { + print "\n"; + } + + &flushAll(1); + &execNonQuery("UPDATE hlstats_Players SET last_event=UNIX_TIMESTAMP();"); + &printEvent("IMPORT", "Import of log file complete. Scanned ".$import_logs_count." lines in ".($end_time-$start_time)." seconds", 1, 1); +} + +sub INT_handler +{ + print "SIGINT received. Flushing data and shutting down...\n"; + flushAll(1); + exit(0); +} + +sub HUP_handler +{ + print "SIGHUP received. Flushing data and reloading configuration...\n"; + &reloadConfiguration; +} diff --git a/scripts/proxy-daemon.pl b/scripts/proxy-daemon.pl new file mode 100644 index 0000000..708959d --- /dev/null +++ b/scripts/proxy-daemon.pl @@ -0,0 +1,456 @@ +#!/usr/bin/perl +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + +use strict; +use DBI; +use IO::Socket; +use IO::Select; +use Getopt::Long; +use Time::Local; + +no strict 'vars'; + +## +## Settings +## + +# $opt_configfile - Absolute path and filename of configuration file. +$opt_configfile = "./hlstats.conf"; + +# $opt_libdir - Directory to look in for local required files +# (our *.plib, *.pm files). +$opt_libdir = "./"; +$heartbeat = 30; + +## +## +################################################################################ +## No need to edit below this line +## +require "$opt_libdir/ConfigReaderSimple.pm"; +do "$opt_libdir/HLstats.plib"; + +$|=1; +Getopt::Long::Configure ("bundling"); + +binmode STDIN, ":utf8"; +binmode STDOUT, ":utf8"; + +# Variables +my %srv_list = (); +my ($datagram,$flags); +my $oldtime = (time + $heartbeat); + +$usage = <new($opt_configfile); + $conf->parse(); + + %directives = ( + "DBHost", "db_host", + "DBUsername", "db_user", + "DBPassword", "db_pass", + "DBName", "db_name", + "BindIP", "s_ip", + "Port", "proxy_port", + "DebugLevel", "g_debug", + ); + + &doConf($conf, %directives); +} else { + &printEvent("CONFIG", "-- Warning: unable to open configuration file '$opt_configfile'", 1); +} + +# Read Command Line Arguments +GetOptions( + "help|h" => \$opt_help, + "configfile|c=s" => \$configfile, + "debug|d+" => \$g_debug +) or die($usage); + +if ($opt_help) { + print $usage; + exit(0); +} + +if ($configfile && -r $configfile) { + $conf = ''; + $conf = ConfigReaderSimple->new($configfile); + $conf->parse(); + &doConf($conf, %directives); +} + +# +# assignDaemon(string ipaddr, string ipport, hash daemon, hash srv_list) +# +# Round-Robin kind of way of spreading the load to different daemons. +# +sub assignDaemon +{ + my ($ipaddr, $ipport, $daemon, $srv_list) = @_; + my $next = ""; + + if (defined($$srv_list{'rr-next'})) { + $next = $$srv_list{'rr-next'}; + } else { + $next = 0; + } + + my $max = keys %$daemon; + + if (!defined($$srv_list{$ipaddr}{$ipport})) { + if ($next eq $max) { + $next = 1; + } else { + $next++; + } + $$srv_list{'rr-next'} = $next; + + $$srv_list{$ipaddr}{$ipport}{'dest_ip'} = $$daemon{$next}{'ip'}; + $$srv_list{$ipaddr}{$ipport}{'dest_port'} = $$daemon{$next}{'port'}; + } + return; +} + +# +# checkHeartbeat (hash daemon, string proxy_key) +# +# Prints and update the state of the perl daemons, if they are up or not. +# +sub checkHeartbeat +{ + my ($daemon, $proxy_key) = @_; + + my $state = ''; + foreach my $key (keys(%$daemon)) { + my $value = $$daemon{$key}; + my $socket = IO::Socket::INET->new( Proto=>"udp", + PeerHost=>$$daemon{$key}{'ip'}, + PeerPort=>$$daemon{$key}{'port'} + ); + $packet = "C;HEARTBEAT;"; + $socket->send("PROXY Key=$proxy_key PROXY $packet"); + + if(IO::Select->new($socket)->can_read(4)) { # 4 second timeout + $socket->recv($msg,1024); + if ($msg =~ /Heartbeat OK/) { + $state = "up"; + } else { + $state = "down"; + } + } + if ($$daemon{$key}{'curstate'} eq "") { + $$daemon{$key}{'curstate'} = "n/a"; + } + + $$daemon{$key}{'oldstate'} = $$daemon{$key}{'curstate'}; + $$daemon{$key}{'curstate'} = $state; + + &printEvent("HEARTBEAT", "Sending HB to $$daemon{$key}{'ip'}:$$daemon{$key}{'port'}... state: $$daemon{$key}{'curstate'} (old: $$daemon{$key}{'oldstate'})", 1); + $state = ''; + } + return; +} + +# +# string retunrServerList(hash srv_list) +# +# Return a list of servers to requestor (udp package C;SERVERLIST). +# +sub returnServerList +{ + my ($srv_list) = @_; + #$srv_list{$ipaddr}{$ipport}{'dest_ip'} + + for my $ip (keys(%srv_list)) { + for my $port (keys(%{$srv_list{$ip}})) { + $msg = $msg . "$ip:$port -> $srv_list{$ip}{$port}{'dest_ip'}:$srv_list{$ip}{$port}{'dest_port'}\n"; + } + } + + return $msg; +} + +# +# string theTime(int sec, int min, int hour, int mday, int year, int wday, int yday, int isdst) +# +# Makes a pretty timestampformat to output +# +sub theTime +{ + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); + $year = $year + 1900; + $mon = $mon + 1; + + if ($mon <= 9) { $mon = "0$mon"; } + if ($mday <= 9) { $mday = "0$mday"; } + if ($hour <= 9) { $hour = "0$hour"; } + if ($min <= 9) { $min = "0$min"; } + if ($sec <= 9) { $sec = "0$sec"; } + + my $time = "[$year-$mon-$mday $hour:$min:$sec] "; + + return $time; +} + +# +# string reloadDaemon(hash daemon, string proxy_key) +# +# Sends reload package to all daemons specified in hlstats.Options.Proxy_Daemons +# +sub reloadDaemon +{ + my ($daemon, $proxy_key) = @_; + my $fake_ip = "127.0.0.1"; + my $fake_port = "30000"; + my $msg = ''; + $packet = "C;RELOAD;"; + + foreach my $key (keys(%$daemon)) { + if ($$daemon{$key}{'curstate'} eq "up") { + &printEvent("CONTROL", "Sending RELOAD packet to $$daemon{$key}{'ip'}:$$daemon{$key}{'port'}", 1); + $msg = $msg . &theTime() . "Sending RELOAD packet to $daemon{$key}{'ip'}:$daemon{$key}{'port'}\n"; + + # Sedning actual message to the daemon. + my $cmd = IO::Socket::INET->new( Proto=>"udp", + PeerHost=>$$daemon{$key}{'ip'}, + PeerPort=>$$daemon{$key}{'port'} + ); + $cmd->send("PROXY Key=$proxy_key PROXY $packet"); + } + } + + return $msg; +} + +# +# string getProxyKey () +# +# Get the value for Proxy_Key +# +sub getProxyKey +{ + my $query = "SELECT `value` FROM hlstats_Options WHERE `keyname` = 'Proxy_Key'"; + my $result = &doQuery($query); + my ($proxy_key) = $result->fetchrow_array; + $result->finish; + + return $proxy_key; +} + +sub is_number ($) { ( $_[0] ^ $_[0] ) eq '0' } + + +############## Main program ############## +$g_stdin = 0; + +# Connect yo mysql DB to get required settings +&doConnect(); + +my $proxy_key = &getProxyKey(); + +# Get the daemons you will use +$query = "SELECT `value` FROM hlstats_Options WHERE `keyname` = 'Proxy_Daemons'"; +$result = &doQuery($query); + +my ($daemonlist) = $result->fetchrow_array; +$result->finish; +my @proxy_daemons = split(/,/, $daemonlist); +my $total_daemons = scalar(@proxy_daemons); + +my %daemon = (); +my $i = 1; + +while ($i <= $total_daemons) { + ($daemon{$i}{'ip'}, $daemon{$i}{'port'}) = split(/:/, $proxy_daemons[$i-1]); + $daemon{$i}{'oldstate'} = ""; + $daemon{$i}{'curstate'} = ""; + $i++; +} + +# Setting up the proxy port to listen on. +my $server = IO::Socket::INET->new( LocalPort=>$proxy_port, + Proto=>"udp" + ) or die "Can't create UDP server: $@"; + +# It went ok, lets start recive messages... +&printEvent("DAEMON", "HlstatsX Proxy Daemon up and running on port: $proxy_port, key: $proxy_key", 1); + +# Do initial heartbeat check. +&checkHeartbeat(\%daemon, $proxy_key); + +# Reload all child daemons config +&reloadDaemon(\%daemon, $proxy_key); + +while ($server->recv($datagram,1024,$flags)) { + my $control = 0; + # Checks the subdaemons every 30 sec if they are alive. + # the interval can be changed by modify $heartbeat value in beginning of script. + if (time > $oldtime) { + &checkHeartbeat(\%daemon, $proxy_key); + $oldtime = (time + $heartbeat); + } + + my $ipaddr = $server->peerhost; + my $ipport = $server->peerport; + + if ($ipaddr eq "127.0.0.1" && $datagram =~/C;HEARTBEAT;/) { + $control = 1; + $msg = ''; + $msg = "Heartbeat OK"; + &printEvent("CONTROL", "Sending Heartbeat to $ipaddr:$ipport", 1); + } elsif ($ipaddr eq "127.0.0.1" && $datagram =~/C;SERVERLIST;/) { + $control = 1; + $msg = ''; + $msg = returnServerList($srv_list); + $msg = "ServerList\n$msg"; + &printEvent("CONTROL", "Sending Serverlist to $ipaddr:$ipport", 1); + } elsif ($ipaddr eq "127.0.0.1" && $datagram =~/C;RELOAD;/) { + $control = 1; + $msg = ''; + $msg = &reloadDaemon($daemon); + } + + if ($ipaddr eq "127.0.0.1" && $control == 1) { + # Sending actual message to the daemon. + my $dest = sockaddr_in($ipport, inet_aton($ipaddr)); + my $bytes = send($server, $msg, 0, $dest); + + next; + } + + + + if ($datagram =~ /PROXY Key=(.+) (.*)PROXY (.+)/) { + if ($proxy_key eq $1) { + if ($3 =~ /C;HEARTBEAT;/) { + $msg = ''; + $msg = "Heartbeat OK"; + &printEvent("CONTROL", "Sending Heartbeat to $ipaddr:$ipport", 1); + } elsif ($3 =~ /C;SERVERLIST;/) { + $msg = ''; + $msg = returnServerList($srv_list); + $msg = "ServerList\n$msg"; + &printEvent("CONTROL", "Sending Serverlist to $ipaddr:$ipport", 1); + &printEvent("CONTROL", $msg, 1); + } elsif ($3 =~ /C;RELOAD;/) { + $msg = ''; + $msg = &reloadDaemon($daemon); + } + } else { + $msg = "FAILED PROXY REQUEST ($ipaddr:$ipport)\n"; + &printEvent("E403", "Sending FAILED PROXY REQUEST to $ipaddr:$ipport", 1); + } + + + # Sedning actual message to the daemon. + my $dest = sockaddr_in($ipport, inet_aton($ipaddr)); + my $bytes = send($server, $msg, 0, $dest); + + next; + } + + if (defined($srv_list{$ipaddr}{$ipport})) { + # Check the oldstate, curstate of your logging daemon + foreach my $key (keys %daemon) { + if ($srv_list{$ipaddr}{$ipport}{'dest_ip'} eq $daemon{$key}{'ip'} && $srv_list{$ipaddr}{$ipport}{'dest_port'} eq $daemon{$key}{'port'}) {; + if ($daemon{$key}{'curstate'} eq "up" && $daemon{$key}{'oldstate'} eq "down") { + # Recovering, should do a reload of some kind here. + %srv_list = (); + + } elsif ($daemon{$key}{'curstate'} eq "down" && $daemon{$key}{'oldstate'} eq "up") { + # Daemon died, assing a new daemon to server + + delete $srv_list{$ipaddr}{$ipport}; + ($daemon, $srv_list) = &assignDaemon($ipaddr, $ipport, $daemon, $srv_list); + &printEvent("BALANCE", "down - up: Re-Assing daemon $srv_list{$ipaddr}{$ipport}{'dest_ip'}:$srv_list{$ipaddr}{$ipport}{'dest_port'} to $ipaddr:$ipport", 1); + } elsif ($daemon{$key}{'curstate'} eq "down" && $daemon{$key}{'oldstate'} eq "down") { + # DOWN, should already reassinged the daemon. + + delete $srv_list{$ipaddr}{$ipport}; + ($daemon, $srv_list) = &assignDaemon($ipaddr, $ipport, $daemon, $srv_list); + &printEvent("BALANCE", "down-down: Re-Assing daemon $srv_list{$ipaddr}{$ipport}{'dest_ip'}:$srv_list{$ipaddr}{$ipport}{'dest_port'} to $ipaddr:$ipport", 1); + } elsif ($daemon{$key}{'curstate'} eq "down" && $daemon{$key}{'oldstate'} eq "n/a") { + # Daemon down when we started proxy, assing another daemon. + + delete $srv_list{$ipaddr}{$ipport}; + ($daemon, $srv_list) = &assignDaemon($ipaddr, $ipport, $daemon, $srv_list); + &printEvent("BALANCE", "down - na: Assing daemon $srv_list{$ipaddr}{$ipport}{'dest_ip'}:$srv_list{$ipaddr}{$ipport}{'dest_port'} to $ipaddr:$ipport from down/na", 1); + } + } + } + } else { + # Assign a logging daemon for your server:port + delete $srv_list{$ipaddr}{$ipport}; + &assignDaemon($ipaddr, $ipport, \%daemon, \%srv_list); + &printEvent("BALANCE", "Assing daemon $srv_list{$ipaddr}{$ipport}{'dest_ip'}:$srv_list{$ipaddr}{$ipport}{'dest_port'} to $ipaddr:$ipport", 1); + } + + + + if ($datagram =~ /.*rcon from.*: command "status".*/ || $datagram =~ /.*rcon from.*: command "stats".*/ || $datagram =~ /.*rcon from.*: command "".*/) { + # skip messages that looks like this, to ease the load on the sub daemons alittle + &printEvent("NOTICE", "Skipping message...", 1) if ($g_debug > 1); + } else { + if (defined($srv_list{$ipaddr}{$ipport}{'dest_ip'}) && defined($srv_list{$ipaddr}{$ipport}{'dest_port'})) { + $datagram =~ s/^.*RL /RL /g; + + &printEvent("NOTICE", "Sending $datagram to daemon $srv_list{$ipaddr}{$ipport}{'dest_ip'}:$srv_list{$ipaddr}{$ipport}{'dest_port'}", 1) if ($g_debug > 1); + # Sedning actual message to the daemon. + my $forward = IO::Socket::INET->new( Proto=>"udp", + PeerHost=>$srv_list{$ipaddr}{$ipport}{'dest_ip'}, + PeerPort=>$srv_list{$ipaddr}{$ipport}{'dest_port'} + ); + $forward->send("PROXY Key=$proxy_key $ipaddr:".$ipport."PROXY $datagram"); + } + } +} + diff --git a/scripts/run_hlstats b/scripts/run_hlstats new file mode 100644 index 0000000..41cd7d8 --- /dev/null +++ b/scripts/run_hlstats @@ -0,0 +1,791 @@ +#!/bin/bash +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxce.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + +#------------------------------------------------------------------------------ +# Usage +# Information on how to use this script can be found on our wiki: +# http://wiki.hlxce.com +#------------------------------------------------------------------------------ + +#------------------------------------------------------------------------------ +# Script Configuration +# These parameters allow you to adjust various functions of the daemon. +# In general, they should not need to be modified. +# Please visit our wiki for more information: http://wiki.hlxce.com + +#------------------------------------------------------------------------------ +# SCRIPTPATH: +# File system path to daemon and supporting files +# NOTE: This is only needed if the other scripts files will be in another directory. +# In general, NO TOUCHY! :) +SCRIPTPATH=. +#------------------------------------------------------------------------------ + +#------------------------------------------------------------------------------ +# CONFFILE: +# Specifies the configuration file (relative to SCRIPTPATH) to use for the daemon +CONFFILE=hlstats.conf +#------------------------------------------------------------------------------ + +#------------------------------------------------------------------------------ +# DAEMON: +# Specifies the daemon Perl script to be used +DAEMON=hlstats.pl +#------------------------------------------------------------------------------ + +#------------------------------------------------------------------------------ +# LOGDIR: +# Specifies the location to store logs +LOGDIR=${SCRIPTPATH}/logs +#------------------------------------------------------------------------------ + +#------------------------------------------------------------------------------ +# LOGDATE: +# Specifies the date format to use in log file names +LOGDATE_FORMAT=%Y-%m-%d_%H-%M-%S +#------------------------------------------------------------------------------ + +#------------------------------------------------------------------------------ +# PIDDIR: +# Specifies location to store daemon PID files +PIDDIR=${SCRIPTPATH} +#------------------------------------------------------------------------------ + + +#------------------------------------------------------------------------------ +# Nothing to modify below here +WEBSITE=http://www.hlxce.com +WIKI=http://wiki.hlxce.com + +# Start output +echo +echo "HLstatsX:CE daemon control" +echo "${WEBSITE}" +echo "---------------------------" + +# Change to directory of script +cd `dirname ${0}` + +# Perform some initial checks before we encounter later errors +# Check if we can write to the SCRIPTPATH +if [ ! -w ${SCRIPTPATH} ]; then + echo "CRITICAL ERROR: Could not write to SCRIPTPATH: ${SCRIPTPATH}" + echo "Verify you have write access to this directory." + echo "Visit our wiki for more information: ${WIKI}." + exit 1 +fi + +# Check if the daemon perl script exists +if [ ! -f ${SCRIPTPATH}/${DAEMON} ]; then + echo "CRITICAL ERROR: Cannot access the daemon: ${DAEMON}" + echo "Verify that the daemon, and corresponding files, exist in ${SCRIPTPATH}" + echo "Visit our wiki for more information: ${WIKI}." + exit 1 +fi + +# Verify shebang line in daemon +SHEBANG=`head -n1 ${SCRIPTPATH}/${DAEMON}` +if [[ ${SHEBANG} =~ ^#! ]]; then + SHEBANG_BINARY=`echo "${SHEBANG}" | sed 's/^#!//'` + if [ ! -f ${SHEBANG_BINARY} ]; then + echo "CRITICAL ERROR: The path to Perl is incorrect in ${DAEMON}." + echo "Current Perl path in shebang: ${SHEBANG_BINARY}" + echo "Visit our wiki for more information: ${WIKI}." + echo + echo "Potential paths for Perl: " + echo `which perl` + exit 1 + fi +else + echo "CRITICAL ERROR: The shebang line is incorrectly configured. Please verify that your shebang line is correct in ${DAEMON}." + echo "Current shebang line: ${SHEBANG}" + echo "Visit our wiki for more information: ${WIKI}." + exit 1 +fi + +# Create logdir if needed +if [ ! -d ${LOGDIR} ]; then + mkdir ${LOGDIR} +fi + +# Make sure we can write to logdir +if [ ! -w ${LOGDIR} ]; then + echo "CRITICAL ERROR: Could not write to the log folder: ${LOGDIR}" + echo "Verify that you have write access to the log folder." + echo "Visit our wiki for more information: ${WIKI}." + exit 1 +fi + +# Daemon control functions +function start_daemon { + # This function handles the creation of a new daemon process. + # This function requires one parameter: PORT + # Returns: + # 0 - Daemon started + # 1 - Daemon failed to start + # 2 - Daemon already running + + if [ ! $1 ]; then + echo "CRITICAL ERROR: No port was received on function start_daemon" + exit 1 + else + local PORT=$1 + fi + + local LOG=${LOGDIR}/hlstats_${PORT}_`date +${LOGDATE_FORMAT}` + + local PID=`get_pid ${PORT}` + # Check if a PID exists for this port number + if [ "${PID}" != "" ]; then + # PID exists -- check if the daemon is running. + kill -0 ${PID} &> /dev/null + if [ $? -eq 0 ]; then + # Daemon running -- nothing to do. + return 2 + else + # Daemon not running -- remove pid. + remove_pidfile ${PORT} + fi + fi + + # Start the daemon on requested port + echo -ne "Attempting to start HLstatsX:CE daemon on port ${PORT}..." + ${SCRIPTPATH}/${DAEMON} --configfile=${CONFFILE} --port=${PORT} &> ${LOG} & + # Store PID in memory until we verify Daemon has launched + PID=$! + + # Perform one quick check to see if PID is running + kill -0 ${PID} &> /dev/null + if [ $? -eq 0 ]; then + create_pidfile ${PORT} ${PID} + echo "" + return 0 + else + # PID not detected in time, keep checking for 10 more seconds. + local i=1 + while [ $i -le 10 ] + do + echo -ne " ${i}" + sleep 1 + # Perform a kill check against saved PID + kill -0 ${PID} &> /dev/null + # Check results of pid test + if [ $? -eq 1 ]; then + # Process does not exist + let i++ + if [ $i -eq 10 ]; then + # Daemon did not respond to start request within 10 seconds. + return 1 + fi + else + # Daemon started successfully -- commit PID to file + create_pidfile ${PORT} ${PID} + echo "" + return 0 + fi + done + fi +} + +function stop_daemon { + # This function handles shutting a daemon down. + # This function requires one parameter: PORT. + + # Returns: + # 0 - Daemon gracefully stopped + # 1 - Daemon forcefully stopped + # 2 - Daemon could not be stopped + # 3 - No daemon to stop or PID missing + + if [ ! $1 ]; then + echo "CRITICAL ERROR: No port was received on function stop_daemon" + exit 1 + else + local PORT=$1 + fi + + local PID=`get_pid ${PORT}` + + if [ ${PID} -eq 0 ]; then + return 3 + fi + + # Attempt to stop the daemon + echo -n "Attempting graceful shutdown of HLstatsX:CE daemon on port ${PORT} " + kill -INT ${PID} &> /dev/null + + if [ $? -ne 0 ]; then + # Daemon is not running, purge the PID. + remove_pidfile ${PORT} + echo "" + return 3 + else + # Found running PID -- perform a quick check before entering loop + kill -0 ${PID} &> /dev/null + if [ $? -eq 1 ]; then + # Daemon stopped, remove PID + remove_pidfile ${PORT} + echo "" + return 0 + else + local i=1 + while [ $i -le 10 ] + do + echo -n " ${i}" + sleep 1 + # Perform a kill check against saved PID + kill -0 ${PID} &> /dev/null + if [ $? -eq 0 ]; then + # Daemon still operating + let i++ + else + # Daemon stopped, remove PID + remove_pidfile ${PORT} + echo "" + return 0 + fi + done + fi + + # Daemon did not respond to shutdown, attempt a forced kill + echo "" + echo "WARNING: Daemon did not respond to a graceful shut down. Forcing a shut down on port ${PORT} " + local i=1 + while [ $i -le 5 ] + do + kill -KILL ${PID} &> /dev/null + echo -n " ${i}" + sleep 1 + + # Check if PID is still present + kill -0 ${PID} &> /dev/null + + if [ $? -eq 0 ]; then + # Daemon still operating + let i++ + else + # Daemon stopped successfully. + remove_pidfile ${PORT} + echo "" + return 1 + fi + done + return 2 + fi +} + +function reload_daemon { + # This function handles reloading a daemon down. + # This function requires one parameter: PORT. + + # Returns: + # 0 - Reload sent successfully + # 1 - Daemon not running or pid file missing + + # Sanity check on incoming required parameter + if [ ! $1 ]; then + echo "CRITICAL ERROR: No port was received on function reload_daemon" + exit 1 + else + local PORT=$1 + fi + + + local PID=`get_pid ${PORT}` + # Check to verify the daemon is operational + if [ ${PID} -ne 0 ]; then + kill -0 ${PID} &> /dev/null + if [ $? -eq 0 ]; then + kill -HUP ${PID} &> /dev/null + return 0 + else + return 1 + fi + else + return 1 + fi +} + +function check_port { + # This function verifies user input on the port number + # One argument is required + + # Returns: + # 0 - Valid input + # 1 - Invalid Input (non-digit or not in UDP port range) + + if [ $1 ]; then + # Perform regex test on input + echo ${1} | grep -q '^[0-9]\{1,5\}$' + # Check if within range and if grep test was successful. + if [ $? -eq 0 ] && [ $1 -le 65535 ] && [ $1 -ge 1 ]; then + return 0 + else + return 1 + fi + fi +} + +function get_status { + # This function performs a lookup for the PID on specified port and checks status + # Parameters: + # 1 - port + + # Returns: + # 0 - PID is running + # 1 - PID is not running + # 2 - Invalid PID + + if [ $1 ]; then + local PID=`get_pid ${1}` + if [ "${PID}" != "" ]; then + kill -0 ${PID} &> /dev/null + if [ $? -eq 0 ]; then + return 0 + else + return 1 + fi + else + return 2 + fi + fi +} + +function create_pidfile { + # This function will handle the creation of a PID file for a corresponding port + # Parameters required: + # 1 - port number + # 2 - PID + + # Returns: + # 0 - PID saved + # 1 - Unable to save PID + + if [[ $1 && $2 ]]; then + PIDFILE=${PIDDIR}/hlstats_${1}.pid + echo ${2} > ${PIDFILE} + + if [ "`cat ${PIDFILE}`" -eq "${2}" ]; then + return 0 + else + return 1 + fi + fi +} + +function remove_pidfile { + # This function will handle the deletion of a PID file for a corresponding port + # Parameters required: + # 1 - port number + + # Returns: + # 0 - PID removed + # 1 - PID does not exist + + if [ $1 ]; then + PIDFILE=${PIDDIR}/hlstats_${1}.pid + rm -f ${PIDFILE} &> /dev/null + if [ $? -eq 0 ]; then + return 0 + else + return 1 + fi + fi +} + + +function get_pid { + # This function will echo out the found pid and return 0, or return 1 if it finds nothing + # Parameters required: + # 1 - port number + + # Output + # Requested PID on return 0 + + # Returns: + # 0 - PID number for corresponding process + # 1 - No PID file for specified port + + if [ $1 ]; then + PIDFILE=${PIDDIR}/hlstats_${1}.pid + PID=`cat ${PIDFILE} 2> /dev/null` + if [ $? -eq 0 ]; then + echo ${PID} + return 0 + else + return 1 + fi + fi +} + +# Cleanup old legacy run_hlstats stuff +# Check if hlstats.pid exists (original pid from legacy run_hlstats) +if [ -f ${PIDDIR}/hlstats.pid ]; then + echo "WARNING: A old PID file has been detected. To prevent further troubles this daemon will be shut down." + kill -KILL `cat ${PIDDIR}/hlstats.pid` &> /dev/null + sleep 1 + # Check if PID is dead + i=1 + while [ $i -le 5 ] + do + kill -0 `cat ${PIDDIR}/hlstats.pid` &> /dev/null + if [ $? -eq 0 ]; then + # Daemon still operating + let i++ + sleep 1 + else + # Daemon stopped successfully. + rm -f ${PIDDIR}/hlstats.pid + echo "" + echo "HLstatsX:CE daemon has been forcefully stopped." + echo "Please re-run this script to control your daemon." + exit + fi + done +fi + +# Daemon control case switcher +case "$1" in + start) + # Usage: run_hlstats start <# of daemons> + # All arguments are optional + # Defaults: # of Daemons = 1; First port number = 27500; Port increment number = 1 + NUMDAEMONS=1 + STARTPORT=27500 + INCREMENT=1 + + # Get user-specified number of daemons + if [ $2 ]; then + NUMDAEMONS=$2 + fi + + if [ $3 ]; then + check_port $3 + if [ $? -eq 0 ]; then + STARTPORT=$3 + else + echo "CRITICAL ERROR: An invalid port number was specified." + exit 1 + fi + fi + + if [ $4 ]; then + INCREMENT=$4 + fi + + # Saving this for a future release -- right now this would prevent people from running run_hlstats every few minutes to make sure their daemon is operational. + #else + # # Lookup the highest currently used port number + # LASTPORT=`ls ${PIDDIR} | egrep 'hlstats_[0-9]{1,5}.pid' | egrep -o '[0-9]{1,5}' | tail -1` + # if [ "${LASTPORT}" != "" ]; then + # # We have currently running daemons, to take the current highest port number and increment it + # let STARTPORT=LASTPORT+INCREMENT + # fi + # + #fi + + i=0 + CURRENTPORT=${STARTPORT} + while [ ${i} -lt ${NUMDAEMONS} ] + do + start_daemon ${CURRENTPORT} + case $? in + 0) + echo "Daemon successfully started on port ${CURRENTPORT}" + let CURRENTPORT=CURRENTPORT+INCREMENT + let i++ + ;; + 1) + echo "CRITICAL ERROR: Unable to start daemon on port ${CURRENTPORT}" + exit 1 + ;; + 2) + echo "Daemon is already running on port ${CURRENTPORT}" + let CURRENTPORT=CURRENTPORT+INCREMENT + let i++ + ;; + esac + done + ;; + + stop) + # Usage: run_hlstats stop + # All arguments are optional + # Defaults: port = ALL + + if [ $2 ]; then + check_port $2 + if [ $? -eq 0 ]; then + PORT=$2 + else + echo "CRITICAL ERROR: An invalid port number was specified." + exit 1 + fi + else + PORT=0 + fi + + # Stop a single daemon + if [ ${PORT} -ne 0 ]; then + stop_daemon ${PORT} + case $? in + 0) + echo "Daemon gracefully stopped on port ${PORT}" + exit 0 + ;; + 1) + echo "Daemon forcefully stopped on port ${PORT}" + exit 0 + ;; + 2) + echo "WARNING: Daemon could not be stopped on port ${PORT}" + exit 1 + ;; + 3) + echo "No daemon running on port ${PORT} or PID file is missing." + exit 1 + ;; + esac + fi + + # Stop all daemons + PORTS=`ls ${PIDDIR} | egrep 'hlstats_[0-9]{1,5}.pid' | egrep -o '[0-9]{1,5}'` + if [ $? -eq 0 ]; then + for port in ${PORTS} ; do + stop_daemon ${port} + case $? in + 0) + echo "Daemon gracefully stopped on port ${port}" + ;; + 1) + echo "Daemon forcefully stopped on port ${port}" + ;; + 2) + echo "WARNING: Daemon could not be stopped on port ${port}" + ;; + 3) + echo "No daemon running on port ${port} or PID file is missing." + ;; + esac + done + else + echo "No daemons found running, or PID files are missing." + exit 1 + fi + ;; + + restart) + # Usage: run_hlstats restart + # All arguments are optional + # Defaults: port = ALL + + if [ $2 ]; then + check_port $2 + if [ $? -eq 0 ]; then + PORT=$2 + else + echo "CRITICAL ERROR: An invalid port number was specified." + exit 1 + fi + else + PORT=0 + fi + + # Handle individual restart request + if [ ${PORT} -ne 0 ]; then + stop_daemon ${PORT} + case $? in + 0 | 1 | 3) + start_daemon ${PORT} + if [ $? -eq 0 ]; then + echo "Daemon successfully restarted on port ${PORT}" + exit 0 + else + echo "CRITICAL ERROR: Failed to restart daemon on port ${PORT}" + exit 1 + fi + ;; + 2) + echo "WARNING: Daemon could not be stopped on port ${port}" + exit 1 + ;; + esac + fi + + # Restart all PIDs + PORTS=`ls ${PIDDIR} | egrep 'hlstats_[0-9]{1,5}.pid' | egrep -o '[0-9]{1,5}'` + if [ $? -eq 0 ]; then + for port in ${PORTS} ; do + stop_daemon ${port} + case $? in + 0 | 1 | 3) + start_daemon ${port} + if [ $? -eq 0 ]; then + echo "Daemon successfully restarted on port ${port}" + else + echo "WARNING: Failed to restart daemon on port ${port}" + fi + ;; + 2) + echo "WARNING: Daemon could not be stopped on port ${port}" + exit 1 + ;; + esac + done + else + echo "WARNING: No HLstatsX:CE daemons currently running." + exit 1 + fi + ;; + + reload) + # Usage: run_hlstats reload + # All arguments are optional + # Defaults: port = ALL + + if [ $2 ]; then + check_port $2 + if [ $? -eq 0 ]; then + PORT=$2 + else + echo "CRITICAL ERROR: An invalid port number was specified." + exit 1 + fi + else + PORT=0 + fi + + # Handle individual reload request + if [ ${PORT} -ne 0 ]; then + reload_daemon ${PORT} + if [ $? -eq 0 ]; then + echo "Successfully reloaded daemon running on port ${PORT}" + exit 0 + else + echo "WARNING: Unable to reload daemon on port ${PORT} (daemon might not be running)" + exit 1 + fi + fi + + # Reload all PIDs + PORTS=`ls ${PIDDIR} | egrep 'hlstats_[0-9]{1,5}.pid' | egrep -o '[0-9]{1,5}'` + if [ "${PORTS}" != "" ]; then + for port in ${PORTS} ; do + reload_daemon ${port} + if [ $? -eq 0 ]; then + echo "Successfully reloaded daemon running on port ${port}" + else + echo "WARNING: Unable to reload daemon on port ${port} (daemon might not be running)" + fi + done + else + echo "WARNING: No HLstatsX:CE daemons currently running." + exit 1 + fi + ;; + + status) + # Usage: run_hlstats status + # All arguments are optional + # Defaults: port = ALL + + if [ $2 ]; then + check_port $2 + if [ $? -eq 0 ]; then + PORT=$2 + else + echo "CRITICAL ERROR: An invalid port number was specified." + exit 1 + fi + else + PORT=0 + fi + + # Handle individual status request + if [ ${PORT} -ne 0 ]; then + get_status ${PORT} + case $? in + 0) + echo "Daemon on port ${PORT} is currently running." + exit 0 + ;; + 1) + echo "A stale process was found for daemon on port ${PORT}." + exit 0 + ;; + 2) + echo "There is no daemon running on port ${PORT}." + exit 0 + ;; + esac + fi + + # Reload all PIDs + PORTS=`ls ${PIDDIR} | egrep 'hlstats_[0-9]{1,5}.pid' | egrep -o '[0-9]{1,5}'` + if [ "${PORTS}" != "" ]; then + for port in ${PORTS} ; do + get_status ${port} + case $? in + 0) + echo "Daemon on port ${port} is currently running." + ;; + 1) + echo "A stale process was found for daemon on port ${port}. It has been removed." + ;; + 2) + echo "There is no daemon running on port ${port}." + ;; + esac + done + else + echo "WARNING: No HLstatsX:CE daemons currently running." + exit 1 + fi + ;; + + *) + echo "Usage" + echo "All optional arguments are in <>. The default is in ()." + echo "" + echo -e "\trun_hlstats start " + echo -e "\trun_hlstats stop " + echo -e "\trun_hlstats status " + echo -e "\trun_hlstats restart " + echo -e "\trun_hlstats reload " + ;; +esac +exit + diff --git a/scripts/run_hlstats_multi b/scripts/run_hlstats_multi new file mode 100644 index 0000000..de8b042 --- /dev/null +++ b/scripts/run_hlstats_multi @@ -0,0 +1,50 @@ +#!/bin/bash +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + +# If you wish to run this script as a daemon script (in /etc/init.d, etc.) +# you must configure the "confdir" variable below. +# The other variables are optional and really should not be modified. + +echo " +WARNING: run_hlstats_multi is deprecated as of 1.6.11. +If you need to start multiple daemons you should now use run_hlstats with the correct parameters. + +For information run_hlstats, run: + +./run_hlstats help + +or visit http://wiki.hlxce.com/wiki/Controlling_the_HLXCE_daemon" +exit 0 diff --git a/scripts/run_proxy b/scripts/run_proxy new file mode 100644 index 0000000..0abf4ba --- /dev/null +++ b/scripts/run_proxy @@ -0,0 +1,151 @@ +#!/bin/bash +# HLstatsX Community Edition - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Nicholas Hastings (nshastings@gmail.com) +# http://www.hlxcommunity.com +# +# HLstatsX Community Edition is a continuation of +# ELstatsNEO - Real-time player and clan rankings and statistics +# Copyleft (L) 2008-20XX Malte Bayer (steam@neo-soft.org) +# http://ovrsized.neo-soft.org/ +# +# ELstatsNEO is an very improved & enhanced - so called Ultra-Humongus Edition of HLstatsX +# HLstatsX - Real-time player and clan rankings and statistics for Half-Life 2 +# http://www.hlstatsx.com/ +# Copyright (C) 2005-2007 Tobias Oetzel (Tobi@hlstatsx.com) +# +# HLstatsX is an enhanced version of HLstats made by Simon Garner +# HLstats - Real-time player and clan rankings and statistics for Half-Life +# http://sourceforge.net/projects/hlstats/ +# Copyright (C) 2001 Simon Garner +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# For support and installation notes visit http://www.hlxcommunity.com + +# If you wish to run this script as a daemon script (in /etc/init.d, etc.) +# you must configure the "confdir" variable below. +# The other variables are optional and really should not be modified. + +# Set the path to the scripts folder (you should not need to modify this.) +confdir=. + +# Set the configuration file for the daemon (you should not need to modify this.) +conffile=hlstats.conf +# Set the filename of the daemon to use (you should not need to modify this.) +daemonfile=proxy-daemon.pl +# Set where you want to log to. This is a directory inside of the confdir setting. +# (You should not need to modify this.) +logdir=logs +# Set the format of the log file +logfile=proxy`date +_%Y%m%d_%H-%M-%S` +# Set where to store PID files +piddir=${confdir} + +# verify that we can see the daemon file +if [ ! -f ${confdir}/${daemonfile} ]; then + echo "Can't see the daemon. (${daemonfile})" + exit 1 +fi + +# verify that you have write access to the directories +if [ ! -w ${confdir} ]; then + echo "you need write access to ${confdir}" + exit 1 +fi + +# verify that you have a logs directory +if [ ! -d ${confdir}/${logdir} ];then + mkdir ${confdir}/${logdir} +fi + +if [ ! -w ${confdir}/${logdir} ];then + echo "you need write access to ${confdir}/${logdir}" + exit 1 +fi + +# Grab out original directory so we can return after +origdir=`pwd` + +# Move into configured directory +cd ${confdir} + +case "$1" in + start) + # verify that we have a .conf file + if [ ! -f ${conffile} ]; then + echo "You're missing your configuration file. (${conffile})" + exit 1 + fi + echo "Starting HLstatsX:CE..."; + if [ -f ${piddir}/proxy-daemon.pid ]; then + kill -0 `cat ${piddir}/proxy-daemon.pid` >/dev/null 2>&1 + if [ "$?" == "0" ]; then + echo "HLstatsX:CE already running!" + else + rm -rf ${piddir}/proxy-daemon.pid + ./${daemonfile} --configfile=${conffile} > ${logdir}/${logfile} 2>&1 & + echo $! >${piddir}/proxy-daemon.pid + echo "PID file created" + echo "Started successfully" + fi + else + ./${daemonfile} --configfile=${conffile} > ${logdir}/${logfile} 2>&1 & + echo $! >${piddir}/proxy-daemon.pid + echo "PID file created" + echo "Started successfully" + fi + ;; + + stop) + echo "Stopping HLstatsX:CE..." + kill -9 `cat ${piddir}/proxy-daemon.pid` >/dev/null 2>&1 + if [ "$?" == "0" ]; then + rm -rf ${piddir}/proxy-daemon.pid + echo "Stopped successfully" + else + echo "No HLstatsX:CE running!" + fi + ;; + + restart) + echo "Restarting HLstatsX:CE..." + kill -9 `cat ${piddir}/proxy-daemon.pid` >/dev/null 2>&1 + if [ "$?" == "0" ]; then + rm -rf ${piddir}/proxy-daemon.pid + ./${daemonfile} --configfile=${conffile} > ${logdir}/${logfile} 2>&1 & + echo $! >${piddir}/proxy-daemon.pid + echo "PID file created" + echo "Restarted successfully" + else + echo "HLstatsX:CE" + if [ -f ${piddir}/proxy-daemon.pid ]; then + rm -rf ${piddir}/proxy-daemon.pid + fi + ./${daemonfile} --configfile=${conffile} > ${logdir}/${logfile} 2>&1 & + echo $! >${piddir}/proxy-daemon.pid + echo "PID file created" + echo "Started successfully" + fi + ;; + + *) + echo "Usage: ./run_hlstats [ start | stop | restart ]" + ;; +esac + +# Return to original directory +cd ${origdir} + +exit 0 diff --git a/sourcemod/plugins/hlstatsx.smx b/sourcemod/plugins/hlstatsx.smx new file mode 100644 index 0000000..68c693e Binary files /dev/null and b/sourcemod/plugins/hlstatsx.smx differ diff --git a/sourcemod/scripting/README.txt b/sourcemod/scripting/README.txt new file mode 100644 index 0000000..eae801b --- /dev/null +++ b/sourcemod/scripting/README.txt @@ -0,0 +1 @@ +As of HLX:CE 1.6.0, the hlstatsx.sp does not add any extra log information. To get auxilary logging for more actions supported by HLX:CE, please run the SuperLogs plugin for your game (in extras/SuperLogs or on the AlliedModders site). If a SuperLogs plugin is not available for your game, it is likely that the "SuperLogs: Generic" plugin will still be of use to add some extra information. \ No newline at end of file diff --git a/sourcemod/scripting/hlstatsx.sp b/sourcemod/scripting/hlstatsx.sp new file mode 100644 index 0000000..8289506 --- /dev/null +++ b/sourcemod/scripting/hlstatsx.sp @@ -0,0 +1,2147 @@ +/** + * HLstatsX Community Edition - SourceMod plugin to display ingame messages + * http://www.hlxcommunity.com/ + * Copyright (C) 2008-2009 Nicholas Hastings + * Copyright (C) 2007-2009 TTS Oetzel & Goerz GmbH + * Modified by Nicholas Hastings (psychonic) for use with HLstatsX Community Edition + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma semicolon 1 + +#define REQUIRE_EXTENSIONS +#include +#include +#include +#undef REQUIRE_EXTENSIONS +#include +#include + +#define VERSION "1.6.19" +#define HLXTAG "HLstatsX:CE" + +enum GameType { + Game_Unknown = -1, + Game_CSS, + Game_DODS, + Game_L4D, + Game_TF, + Game_HL2MP, + Game_INSMOD, + Game_FF, + Game_ZPS, + Game_AOC, + Game_FOF, + Game_GES, + Game_PVKII, + Game_CSP, + Game_ND, + Game_DDD, + Game_CSGO, +}; + +new GameType:gamemod = Game_Unknown; + +new Handle: hlx_block_chat_commands; +new Handle: hlx_message_prefix; +new Handle: hlx_protect_address; +new Handle: hlx_server_tag; +new Handle: sv_tags; +new Handle: message_recipients; +new const String: blocked_commands[][] = { "rank", "skill", "points", "place", "session", "session_data", + "kpd", "kdratio", "kdeath", "next", "load", "status", "servers", + "top20", "top10", "top5", "clans", "bans", "cheaters", "statsme", "weapons", + "weapon", "action", "actions", "accuracy", "targets", "target", "kills", + "kill", "player_kills", "cmd", "cmds", "command", "hlx_display 0", + "hlx_display 1", "hlx_teams 0", "hlx_teams 1", "hlx_hideranking", + "hlx_chat 0", "hlx_chat 1", "hlx_menu", "servers 1", "servers 2", + "servers 3", "hlx", "hlstatsx", "help" }; + +new Handle:HLstatsXMenuMain; +new Handle:HLstatsXMenuAuto; +new Handle:HLstatsXMenuEvents; + +new Handle: PlayerColorArray; +new ColorSlotArray[] = { -1, -1, -1, -1, -1, -1 }; + +new const String:ct_models[][] = { + "models/player/ct_urban.mdl", + "models/player/ct_gsg9.mdl", + "models/player/ct_sas.mdl", + "models/player/ct_gign.mdl" +}; + +new const String:ts_models[][] = { + "models/player/t_phoenix.mdl", + "models/player/t_leet.mdl", + "models/player/t_arctic.mdl", + "models/player/t_guerilla.mdl" +}; + +new const String: modnamelist[][] = { + "Counter-Strike: Source", + "Day of Defeat: Source", + "Left 4 Dead (1 or 2)", + "Team Fortress 2", + "Half-Life 2 Deathmatch", + "Insurgency", + "Fortress Forever", + "Zombie Panic: Source", + "Age of Chivalry", + "Fistful of Frags", + "GoldenEye: Source", + "Pirates, Vikings, and Knights", + "CSPromod", + "Nuclear Dawn", + "Dino D-Day", + "Counter-Strike: Global Offensive" +}; + +new String: message_prefix[32]; +new bool:g_bPlyrCanDoMotd[MAXPLAYERS+1]; +new bool:g_bGameCanDoMotd = true; +new bool:g_bTrackColors4Chat; +new g_iSDKVersion = SOURCE_SDK_UNKNOWN; +new Handle:g_cvarTeamPlay = INVALID_HANDLE; +new bool:g_bTeamPlay; +new bool:g_bLateLoad = false; +new bool:g_bIgnoreNextTagChange = false; +new Handle:g_hCustomTags; + +#define SVTAGSIZE 128 + +public Plugin:myinfo = { + name = "HLstatsX CE Ingame Plugin", + author = "psychonic", + description = "Provides ingame functionality for interaction from an HLstatsX CE installation", + version = VERSION, + url = "http://www.hlxcommunity.com" +}; + + +public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) +{ + g_bLateLoad = late; + MarkNativeAsOptional("CS_SwitchTeam"); + MarkNativeAsOptional("CS_RespawnPlayer"); + MarkNativeAsOptional("SetCookieMenuItem"); + + return APLRes_Success; +} + + +public OnPluginStart() +{ + get_server_mod(); + + CreateHLstatsXMenuMain(HLstatsXMenuMain); + CreateHLstatsXMenuAuto(HLstatsXMenuAuto); + CreateHLstatsXMenuEvents(HLstatsXMenuEvents); + + RegServerCmd("hlx_sm_psay", hlx_sm_psay); + RegServerCmd("hlx_sm_psay2", hlx_sm_psay2); + RegServerCmd("hlx_sm_bulkpsay", hlx_sm_psay); + RegServerCmd("hlx_sm_csay", hlx_sm_csay); + RegServerCmd("hlx_sm_msay", hlx_sm_msay); + RegServerCmd("hlx_sm_tsay", hlx_sm_tsay); + RegServerCmd("hlx_sm_hint", hlx_sm_hint); + RegServerCmd("hlx_sm_browse", hlx_sm_browse); + RegServerCmd("hlx_sm_swap", hlx_sm_swap); + RegServerCmd("hlx_sm_redirect", hlx_sm_redirect); + RegServerCmd("hlx_sm_player_action", hlx_sm_player_action); + RegServerCmd("hlx_sm_team_action", hlx_sm_team_action); + RegServerCmd("hlx_sm_world_action", hlx_sm_world_action); + + if (gamemod == Game_INSMOD) + { + AddCommandListener(hlx_block_commands, "say2"); + } + else if (gamemod == Game_ND) + { + AddCommandListener(hlx_block_commands, "say_squad"); + } + + AddCommandListener(hlx_block_commands, "say"); + AddCommandListener(hlx_block_commands, "say_team"); + + switch (gamemod) + { + case Game_CSS, Game_L4D, Game_TF, Game_HL2MP, Game_AOC, Game_FOF, Game_PVKII, Game_ND, Game_DDD, Game_CSGO: + { + g_bTrackColors4Chat = true; + HookEvent("player_team", HLstatsX_Event_PlyTeamChange, EventHookMode_Pre); + } + } + + switch (gamemod) + { + case Game_L4D, Game_INSMOD, Game_GES: + { + g_bGameCanDoMotd = false; + } + } + + if (gamemod == Game_HL2MP) + { + g_cvarTeamPlay = FindConVar("mp_teamplay"); + if (g_cvarTeamPlay != INVALID_HANDLE) + { + g_bTeamPlay = GetConVarBool(g_cvarTeamPlay); + HookConVarChange(g_cvarTeamPlay, OnTeamPlayChange); + } + } + + CreateConVar("hlxce_plugin_version", VERSION, "HLstatsX:CE Ingame Plugin", FCVAR_PLUGIN|FCVAR_NOTIFY); + CreateConVar("hlxce_version", "", "HLstatsX:CE", FCVAR_PLUGIN|FCVAR_NOTIFY); + CreateConVar("hlxce_webpage", "http://www.hlxcommunity.com", "http://www.hlxcommunity.com", FCVAR_PLUGIN|FCVAR_NOTIFY); + + hlx_block_chat_commands = CreateConVar("hlx_block_commands", "1", "If activated HLstatsX commands are blocked from the chat area", FCVAR_PLUGIN); + hlx_message_prefix = CreateConVar("hlx_message_prefix", "", "Define the prefix displayed on every HLstatsX ingame message", FCVAR_PLUGIN); + hlx_protect_address = CreateConVar("hlx_protect_address", "", "Address to be protected for logging/forwarding", FCVAR_PLUGIN); + hlx_server_tag = CreateConVar("hlx_server_tag", "1", "If enabled, adds \"HLstatsX:CE\" to server tags on supported games. 1 = Enabled (default), 0 = Disabled", + FCVAR_PLUGIN, true, 0.0, true, 1.0); + + g_hCustomTags = CreateArray(SVTAGSIZE); + sv_tags = FindConVar("sv_tags"); + g_iSDKVersion = GuessSDKVersion(); + + if (g_bLateLoad) + { + GetConVarString(hlx_message_prefix, message_prefix, sizeof(message_prefix)); + decl String:protaddr[24]; + GetConVarString(hlx_protect_address, protaddr, sizeof(protaddr)); + OnProtectAddressChange(hlx_protect_address, "", protaddr); + } + + MyAddServerTag(HLXTAG); + + HookConVarChange(hlx_message_prefix, OnMessagePrefixChange); + HookConVarChange(hlx_protect_address, OnProtectAddressChange); + HookConVarChange(hlx_server_tag, OnServerTagChange); + if (sv_tags != INVALID_HANDLE) + { + HookConVarChange(sv_tags, OnSVTagsChange); + } + + RegServerCmd("log", ProtectLoggingChange); + RegServerCmd("logaddress_del", ProtectForwardingChange); + RegServerCmd("logaddress_delall", ProtectForwardingDelallChange); + RegServerCmd("hlx_message_prefix_clear", MessagePrefixClear); + + PlayerColorArray = CreateArray(); + message_recipients = CreateStack(); + + GetTeams(gamemod == Game_INSMOD); +} + + +public OnAllPluginsLoaded() +{ + if (LibraryExists("clientprefs")) + { + SetCookieMenuItem(HLXSettingsMenu, 0, "HLstatsX:CE Settings"); + } +} + +public HLXSettingsMenu(client, CookieMenuAction:action, any:info, String:buffer[], maxlen) +{ + if (action == CookieMenuAction_SelectOption) + { + DisplayMenu(HLstatsXMenuMain, client, MENU_TIME_FOREVER); + } +} + + +public OnMapStart() +{ + GetTeams(gamemod == Game_INSMOD); + + if (g_bTrackColors4Chat) + { + find_player_team_slot(2); + find_player_team_slot(3); + if (gamemod == Game_PVKII) + { + find_player_team_slot(4); + } + } +} + +bool:BTagsSupported() +{ + return (sv_tags != INVALID_HANDLE && (g_iSDKVersion == SOURCE_SDK_EPISODE2 || g_iSDKVersion == SOURCE_SDK_EPISODE2VALVE || gamemod == Game_ND)); +} + +stock MyAddServerTag(const String:tag[]) +{ + if (!BTagsSupported()) + { + // game doesn't support sv_tags + return; + } + + if (!GetConVarBool(hlx_server_tag)) + { + return; + } + + if (FindStringInArray(g_hCustomTags, tag) == -1) + { + PushArrayString(g_hCustomTags, tag); + } + + decl String:current_tags[SVTAGSIZE]; + GetConVarString(sv_tags, current_tags, sizeof(current_tags)); + if (StrContains(current_tags, tag) > -1) + { + // already have tag + return; + } + + decl String:new_tags[SVTAGSIZE]; + Format(new_tags, sizeof(new_tags), "%s%s%s", current_tags, (current_tags[0]!=0)?",":"", tag); + + new flags = GetConVarFlags(sv_tags); + SetConVarFlags(sv_tags, flags & ~FCVAR_NOTIFY); + g_bIgnoreNextTagChange = true; + SetConVarString(sv_tags, new_tags); + g_bIgnoreNextTagChange = false; + SetConVarFlags(sv_tags, flags); +} + +stock MyRemoveServerTag(const String:tag[]) +{ + if (!BTagsSupported()) + { + // game doesn't support sv_tags + return; + } + + new idx = FindStringInArray(g_hCustomTags, tag); + if (idx > -1) + { + RemoveFromArray(g_hCustomTags, idx); + } + + decl String:current_tags[SVTAGSIZE]; + GetConVarString(sv_tags, current_tags, sizeof(current_tags)); + if (StrContains(current_tags, tag) == -1) + { + // tag isn't on here, just bug out + return; + } + + ReplaceString(current_tags, sizeof(current_tags), tag, ""); + ReplaceString(current_tags, sizeof(current_tags), ",,", ""); + + new flags = GetConVarFlags(sv_tags); + SetConVarFlags(sv_tags, flags & ~FCVAR_NOTIFY); + g_bIgnoreNextTagChange = true; + SetConVarString(sv_tags, current_tags); + g_bIgnoreNextTagChange = false; + SetConVarFlags(sv_tags, flags); +} + +get_server_mod() +{ + new String: game_description[64]; + GetGameDescription(game_description, sizeof(game_description), true); + + if (StrContains(game_description, "Counter-Strike", false) != -1) + { + gamemod = Game_CSS; + } + else if (StrContains(game_description, "Day of Defeat", false) != -1) + { + gamemod = Game_DODS; + } + else if (StrContains(game_description, "Half-Life 2 Deathmatch", false) != -1) + { + gamemod = Game_HL2MP; + } + else if (StrContains(game_description, "Team Fortress", false) != -1) + { + gamemod = Game_TF; + } + else if (StrContains(game_description, "L4D", false) != -1 || StrContains(game_description, "Left 4 D", false) != -1) + { + gamemod = Game_L4D; + } + else if (StrContains(game_description, "Insurgency", false) != -1) + { + gamemod = Game_INSMOD; + //psychonic - added detection for more supported games + } + else if (StrContains(game_description, "Fortress Forever", false) != -1) + { + gamemod = Game_FF; + } + else if (StrContains(game_description, "ZPS", false) != -1) + { + gamemod = Game_ZPS; + } + else if (StrContains(game_description, "Age of Chivalry", false) != -1) + { + gamemod = Game_AOC; + } + else if (StrContains(game_description, "PVKII", false) != -1) + { + gamemod = Game_PVKII; + } + else if (StrContains(game_description, "CSPromod", false) != -1) + { + gamemod = Game_CSP; + } + else if (StrContains(game_description, "Nuclear Dawn", false) != -1) + { + gamemod = Game_ND; + } + + // game mod could not detected, try further + if (gamemod == Game_Unknown) + { + new String: game_folder[64]; + GetGameFolderName(game_folder, sizeof(game_folder)); + if (StrContains(game_folder, "cstrike", false) != -1) + { + gamemod = Game_CSS; + } + else if (strncmp(game_folder, "dod", 3, false) == 0) + { + gamemod = Game_DODS; + } + else if (StrContains(game_folder, "hl2mp", false) != -1 || StrContains(game_folder, "hl2ctf", false) != -1) + { + gamemod = Game_HL2MP; + } + else if (StrContains(game_folder, "fistful_of_frags", false) != -1) + { + gamemod = Game_FOF; + } + else if (strncmp(game_folder, "tf", 2, false) == 0) + { + gamemod = Game_TF; + } + else if (StrContains(game_folder, "left4dead", false) != -1) + { + gamemod = Game_L4D; + } + else if (StrContains(game_folder, "insurgency", false) != -1) + { + gamemod = Game_INSMOD; + //psychonic - added detection for more supported games + } + else if (StrContains(game_folder, "FortressForever", false) != -1) + { + gamemod = Game_FF; + } + else if (StrContains(game_folder, "zps", false) != -1) + { + gamemod = Game_ZPS; + } + else if (StrContains(game_folder, "ageofchivalry", false) != -1) + { + gamemod = Game_AOC; + } + else if (StrContains(game_folder, "gesource", false) != -1) + { + gamemod = Game_GES; + } + else if (StrContains(game_folder, "pvkii", false) != -1) + { + gamemod = Game_PVKII; + } + else if (StrContains(game_folder, "cspromod", false) != -1) + { + gamemod = Game_CSP; + } + else if (StrContains(game_folder, "nucleardawn", false) != -1) + { + gamemod = Game_ND; + } + else if (StrContains(game_folder, "dinodday", false) != -1) + { + gamemod = Game_DDD; + } + else if (StrContains(game_folder, "csgo", false) != -1) + { + gamemod = Game_CSGO; + } + else + { + LogToGame("HLX:CE Mod Not In Detected List, Using Defaults (%s, %s)", game_description, game_folder); + LogToGame("HLX:CE If this is incorrect, please file a bug at hlxcommunity.com"); + } + } + if (gamemod > Game_Unknown) + { + LogToGame("HLX:CE Mod Detection: %s", modnamelist[_:gamemod]); + LogToGame("HLX:CE If this is incorrect, please file a bug at hlxcommunity.com"); + } +} + +public OnClientPostAdminCheck(client) +{ + if (g_bGameCanDoMotd && !IsFakeClient(client)) + { + QueryClientConVar(client, "cl_disablehtmlmotd", motdQuery); + } +} + + +public motdQuery(QueryCookie:cookie, client, ConVarQueryResult:result, const String:cvarName[], const String:cvarValue[]) +{ + if (result == ConVarQuery_Okay && StringToInt(cvarValue) == 0 || result != ConVarQuery_Okay) + { + g_bPlyrCanDoMotd[client] = true; + } +} + + +public OnServerTagChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + if (GetConVarBool(hlx_server_tag)) + { + MyAddServerTag(HLXTAG); + } + else + { + MyRemoveServerTag(HLXTAG); + } +} + +public OnSVTagsChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + if (g_bIgnoreNextTagChange) + { + // we fired this callback, no need to reapply tags + return; + } + + // reapply each custom tag + new cnt = GetArraySize(g_hCustomTags); + for (new i = 0; i < cnt; i++) + { + decl String:tag[SVTAGSIZE]; + GetArrayString(g_hCustomTags, i, tag, sizeof(tag)); + MyAddServerTag(tag); + } +} + + +public OnProtectAddressChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + if (newVal[0] > 0) + { + decl String: log_command[192]; + Format(log_command, sizeof(log_command), "logaddress_add %s", newVal); + LogToGame("Command: %s", log_command); + ServerCommand(log_command); + } +} + +public OnTeamPlayChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + g_bTeamPlay = GetConVarBool(g_cvarTeamPlay); +} + +public Action:ProtectLoggingChange(args) +{ + if (hlx_protect_address != INVALID_HANDLE) + { + decl String: protect_address[192]; + GetConVarString(hlx_protect_address, protect_address, sizeof(protect_address)); + if (strcmp(protect_address, "") != 0) + { + if (args >= 1) + { + decl String: log_action[192]; + GetCmdArg(1, log_action, sizeof(log_action)); + if ((strcmp(log_action, "off") == 0) || (strcmp(log_action, "0") == 0)) + { + LogToGame("HLstatsX address protection active, logging reenabled!"); + ServerCommand("log 1"); + } + } + } + } + return Plugin_Continue; +} + + +public Action:ProtectForwardingChange(args) +{ + if (hlx_protect_address != INVALID_HANDLE) + { + decl String: protect_address[192]; + GetConVarString(hlx_protect_address, protect_address, sizeof(protect_address)); + if (strcmp(protect_address, "") != 0) + { + if (args == 1) + { + decl String: log_action[192]; + GetCmdArg(1, log_action, sizeof(log_action)); + if (strcmp(log_action, protect_address) == 0) + { + decl String: log_command[192]; + Format(log_command, sizeof(log_command), "logaddress_add %s", protect_address); + LogToGame("HLstatsX address protection active, logaddress readded!"); + ServerCommand(log_command); + } + } + else if (args > 1) + { + new String: log_action[192]; + for(new i = 1; i <= args; i++) + { + decl String: temp_argument[192]; + GetCmdArg(i, temp_argument, sizeof(temp_argument)); + strcopy(log_action[strlen(log_action)], sizeof(log_action), temp_argument); + } + if (strcmp(log_action, protect_address) == 0) + { + decl String: log_command[192]; + Format(log_command, sizeof(log_command), "logaddress_add %s", protect_address); + LogToGame("HLstatsX address protection active, logaddress readded!"); + ServerCommand(log_command); + } + + } + } + } + return Plugin_Continue; +} + + +public Action:ProtectForwardingDelallChange(args) +{ + if (hlx_protect_address != INVALID_HANDLE) + { + decl String: protect_address[192]; + GetConVarString(hlx_protect_address, protect_address, sizeof(protect_address)); + if (strcmp(protect_address, "") != 0) + { + decl String: log_command[192]; + Format(log_command, sizeof(log_command), "logaddress_add %s", protect_address); + LogToGame("HLstatsX address protection active, logaddress readded!"); + ServerCommand(log_command); + } + } + return Plugin_Continue; +} + + +public OnMessagePrefixChange(Handle:cvar, const String:oldVal[], const String:newVal[]) +{ + strcopy(message_prefix, sizeof(message_prefix), newVal); +} + + +public Action:MessagePrefixClear(args) +{ + message_prefix = ""; +} + + +find_player_team_slot(team_index) +{ + if (team_index > -1) + { + ColorSlotArray[team_index] = -1; + for(new i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && GetClientTeam(i) == team_index) + { + ColorSlotArray[team_index] = i; + break; + } + } + } +} + + +stock validate_team_colors() +{ + for (new i = 0; i < sizeof(ColorSlotArray); i++) + { + new color_client = ColorSlotArray[i]; + if (color_client > 0) + { + if (IsClientInGame(color_client) && GetClientTeam(color_client) != color_client) + { + find_player_team_slot(i); + } + } + else + { + if (i == 2 || i == 3 || (i == 4 && gamemod == Game_PVKII)) + { + find_player_team_slot(i); + } + } + } +} + +public OnClientDisconnect(client) +{ + if (g_bTrackColors4Chat && client > 0 && IsClientInGame(client)) + { + new team_index = GetClientTeam(client); + if (client == ColorSlotArray[team_index]) + { + ColorSlotArray[team_index] = -1; + } + } + + g_bPlyrCanDoMotd[client] = false; +} + +color_player(color_type, player_index, String: client_message[192]) +{ + new color_player_index = -1; + if (g_bTrackColors4Chat || (gamemod == Game_DODS) || (gamemod == Game_ZPS) || (gamemod == Game_GES) || (gamemod == Game_CSP)) + { + decl String: client_name[192]; + GetClientName(player_index, client_name, sizeof(client_name)); + if ((strcmp(client_message, "") != 0) && (strcmp(client_name, "") != 0)) + { + if (color_type == 1) + { + decl String: search_client_name[192]; + Format(search_client_name, sizeof(search_client_name), "%s ", client_name); + decl String: colored_player_name[192]; + switch (gamemod) + { + case Game_DODS, Game_GES, Game_CSP: + Format(colored_player_name, sizeof(colored_player_name), "\x04%s\x01 ", client_name); + case Game_HL2MP: + Format(colored_player_name, sizeof(colored_player_name), "%c%s\x01 ", g_bTeamPlay?3:4, client_name); + case Game_ZPS: + Format(colored_player_name, sizeof(colored_player_name), "\x05%s\x01 ", client_name); + default: + Format(colored_player_name, sizeof(colored_player_name), "\x03%s\x01 ", client_name); + } + if (ReplaceString(client_message, sizeof(client_message), search_client_name, colored_player_name) > 0) + { + return player_index; + } + } + else + { + decl String: search_client_name[192]; + Format(search_client_name, sizeof(search_client_name), " %s ", client_name); + decl String: colored_player_name[192]; + switch (gamemod) + { + case Game_ZPS: + Format(colored_player_name, sizeof(colored_player_name), " \x05%s\x01 ", client_name); + case Game_GES: + Format(colored_player_name, sizeof(colored_player_name), " \x05%s\x01 ", client_name); + default: + Format(colored_player_name, sizeof(colored_player_name), " \x04%s\x01 ", client_name); + } + ReplaceString(client_message, sizeof(client_message), search_client_name, colored_player_name); + } + } + } + else if (gamemod == Game_FF) + { + decl String: client_name[192]; + GetClientName(player_index, client_name, sizeof(client_name)); + + new team = GetClientTeam(player_index); + if (team > 1 && team < 6) + { + decl String: colored_player_name[192]; + Format(colored_player_name, sizeof(colored_player_name), "^%d%s^0", (team-1), client_name); + if (ReplaceString(client_message, sizeof(client_message), client_name, colored_player_name) > 0) + { + return player_index; + } + } + } + return color_player_index; +} + + +color_all_players(String: message[192]) +{ + new color_index = -1; + if ((g_bTrackColors4Chat || (gamemod == Game_DODS) || (gamemod == Game_ZPS) || (gamemod == Game_FF) || (gamemod == Game_GES) || (gamemod == Game_CSP)) && (PlayerColorArray != INVALID_HANDLE)) + { + if (strcmp(message, "") != 0) + { + ClearArray(PlayerColorArray); + + new lowest_matching_pos = 192; + new lowest_matching_pos_client = -1; + + for(new i = 1; i <= MaxClients; i++) + { + new client = i; + if (IsClientInGame(client)) + { + decl String: client_name[32]; + GetClientName(client, client_name, sizeof(client_name)); + + if (strcmp(client_name, "") != 0) + { + new message_pos = StrContains(message, client_name); + if (message_pos > -1) + { + if (lowest_matching_pos > message_pos) + { + lowest_matching_pos = message_pos; + lowest_matching_pos_client = client; + } + new TempPlayerColorArray[1]; + TempPlayerColorArray[0] = client; + PushArrayArray(PlayerColorArray, TempPlayerColorArray); + } + } + } + } + new size = GetArraySize(PlayerColorArray); + for (new i = 0; i < size; i++) + { + new temp_player_array[1]; + GetArrayArray(PlayerColorArray, i, temp_player_array); + new temp_client = temp_player_array[0]; + if (temp_client == lowest_matching_pos_client) + { + new temp_color_index = color_player(1, temp_client, message); + color_index = temp_color_index; + } + else + { + color_player(0, temp_client, message); + } + } + ClearArray(PlayerColorArray); + } + } + + return color_index; +} + + + +color_team_entities(String:message[192]) +{ + switch(gamemod) + { + case Game_CSS, Game_CSGO: + { + if (strcmp(message, "") != 0) + { + if (ColorSlotArray[2] > -1) + { + if (ReplaceString(message, sizeof(message), "TERRORIST ", "\x03TERRORIST\x01 ") > 0) + { + return ColorSlotArray[2]; + } + } + if (ColorSlotArray[3] > -1) + { + if (ReplaceString(message, sizeof(message), "CT ", "\x03CT\x01 ") > 0) + { + return ColorSlotArray[3]; + } + } + } + } + case Game_L4D: + { + if (strcmp(message, "") != 0) + { + if (ColorSlotArray[2] > -1) + { + if (ReplaceString(message, sizeof(message), "Survivors ", "\x03Survivors\x01 ") > 0) + { + return ColorSlotArray[2]; + } + } + if (ColorSlotArray[3] > -1) + { + if (ReplaceString(message, sizeof(message), "Infected ", "\x03Infected\x01 ") > 0) + { + return ColorSlotArray[3]; + } + } + } + } + case Game_TF: + { + if (strcmp(message, "") != 0) + { + if (ColorSlotArray[2] > -1) + { + if (ReplaceString(message, sizeof(message), "Red ", "\x03Red\x01 ") > 0) + { + return ColorSlotArray[2]; + } + } + if (ColorSlotArray[3] > -1) + { + if (ReplaceString(message, sizeof(message), "Blue ", "\x03Blue\x01 ") > 0) + { + return ColorSlotArray[3]; + } + } + } + } + case Game_FF: + { + if (strcmp(message, "") != 0) + { + if (ReplaceString(message, sizeof(message), "Red Team", "^2Red Team^0") > 0) + { + return 0; + } + if (ReplaceString(message, sizeof(message), "Blue Team", "^1Blue Team^0") > 0) + { + return 0; + } + if (ReplaceString(message, sizeof(message), "Yellow Team", "^3Yellow Team^0") > 0) + { + return 0; + } + if (ReplaceString(message, sizeof(message), "Green Team", "^4Green Team^0") > 0) + { + return 0; + } + } + } + case Game_AOC: + { + if (strcmp(message, "") != 0) + { + if (ColorSlotArray[2] > -1) + { + if (ReplaceString(message, sizeof(message), "Agathia Knights ", "\x03Agathia Knights\x01 ") > 0) + { + return ColorSlotArray[2]; + } + } + if (ColorSlotArray[3] > -1) + { + if (ReplaceString(message, sizeof(message), "The Mason Order ", "\x03The Mason Order\x01 ") > 0) + { + return ColorSlotArray[3]; + } + } + } + } + case Game_FOF: + { + if (strcmp(message, "") != 0) + { + if (ColorSlotArray[2] > -1) + { + if (ReplaceString(message, sizeof(message), "Desperados ", "\x03Desperados\x01 ") > 0 + || ReplaceString(message, sizeof(message), "Desparados ", "\x03Desperados\x01 ") > 0) + { + return ColorSlotArray[2]; + } + } + if (ColorSlotArray[3] > -1) + { + if (ReplaceString(message, sizeof(message), "Vigilantes ", "\x03Vigilantes\x01 ") > 0) + { + return ColorSlotArray[3]; + } + } + } + } + case Game_HL2MP: + { + if (g_bTeamPlay && strcmp(message, "") != 0) + { + if (ColorSlotArray[2] > -1) + { + if (ReplaceString(message, sizeof(message), "The Combine ", "\x03The Combine\x01 ") > 0) + { + return ColorSlotArray[2]; + } + } + if (ColorSlotArray[3] > -1) + { + if (ReplaceString(message, sizeof(message), "Rebel Forces ", "\x03Rebel Forces\x01 ") > 0) + { + return ColorSlotArray[3]; + } + } + } + } + case Game_PVKII: + { + if (strcmp(message, "") != 0) + { + if (ColorSlotArray[2] > -1) + { + if (ReplaceString(message, sizeof(message), "Pirates ", "\x03Pirates\x01 ") > 0) + { + return ColorSlotArray[2]; + } + } + if (ColorSlotArray[3] > -1) + { + if (ReplaceString(message, sizeof(message), "Vikings ", "\x03Vikings\x01 ") > 0) + { + return ColorSlotArray[3]; + } + } + if (ColorSlotArray[4] > -1) + { + if (ReplaceString(message, sizeof(message), "Knights ", "\x03Knights\x01 ") > 0) + { + return ColorSlotArray[4]; + } + } + } + } + case Game_ND: + { + if (strcmp(message, "") != 0) + { + if (ColorSlotArray[2] > -1) + { + if (ReplaceString(message, sizeof(message), "EMPIRE ", "\x03Empire\x01 ") > 0) + { + return ColorSlotArray[2]; + } + } + if (ColorSlotArray[3] > -1) + { + if (ReplaceString(message, sizeof(message), "CONSORTIUM ", "\x03Consortium\x01 ") > 0) + { + return ColorSlotArray[3]; + } + } + } + } + case Game_DDD: + { + if (g_bTeamPlay && strcmp(message, "") != 0) + { + if (ColorSlotArray[2] > -1) + { + if (ReplaceString(message, sizeof(message), "Allies ", "\x03Allies\x01 ") > 0) + { + return ColorSlotArray[2]; + } + } + if (ColorSlotArray[3] > -1) + { + if (ReplaceString(message, sizeof(message), "Axis ", "\x03Axis\x01 ") > 0) + { + return ColorSlotArray[3]; + } + } + } + } + } + + return -1; +} + + +display_menu(player_index, time, String: full_message[1024], need_handler = 0) +{ + ReplaceString(full_message, sizeof(full_message), "\\n", "\10"); + if (need_handler == 0) + { + InternalShowMenu(player_index, full_message, time); + } + else + { + InternalShowMenu(player_index, full_message, time, (1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<4)|(1<<5)|(1<<6)|(1<<7)|(1<<8)|(1<<9), InternalMenuHandler); + } +} + + +public InternalMenuHandler(Handle:menu, MenuAction:action, param1, param2) +{ + new client = param1; + if (IsClientInGame(client)) + { + if (action == MenuAction_Select) + { + decl String: player_event[192]; + IntToString(param2, player_event, sizeof(player_event)); + LogPlayerEvent(client, "selected", player_event); + } + else if (action == MenuAction_Cancel) + { + LogPlayerEvent(client, "selected", "cancel"); + } + } +} + + +public Action:hlx_sm_psay(args) +{ + if (args < 2) + { + PrintToServer("Usage: hlx_sm_psay - sends private message"); + return Plugin_Handled; + } + + decl String: client_list[192]; + GetCmdArg(1, client_list, sizeof(client_list)); + BuildClientList(client_list); + + decl String: colored_param[32]; + GetCmdArg(2, colored_param, sizeof(colored_param)); + new is_colored = 0; + new ignore_param = 0; + + if (strcmp(colored_param, "1") == 0) + { + is_colored = 1; + ignore_param = 1; + } + else if (strcmp(colored_param, "2") == 0) + { + is_colored = 2; + ignore_param = 1; + } + else if (strcmp(colored_param, "0") == 0) + { + ignore_param = 1; + } + + new String: client_message[192]; + GetCmdArg((ignore_param + 2), client_message, sizeof(client_message)); + + if (IsStackEmpty(message_recipients)) + { + return Plugin_Handled; + } + + new color_index = -1; + decl String: display_message[192]; + + switch (gamemod) + { + case Game_CSS, Game_DODS, Game_L4D, Game_TF, Game_HL2MP, Game_ZPS, Game_AOC, Game_FOF, Game_GES, Game_PVKII, Game_CSP, Game_ND, Game_DDD, Game_CSGO: + { + if (is_colored > 0) + { + if (is_colored == 1) + { + new player_color_index = color_all_players(client_message); + if (player_color_index > -1) + { + color_index = player_color_index; + } + else + { + if (g_bTrackColors4Chat) + { + validate_team_colors(); + } + color_index = color_team_entities(client_message); + } + } + } + if (strcmp(message_prefix, "") == 0) + { + Format(display_message, sizeof(display_message), "\x01%s", client_message); + } + else + { + Format(display_message, sizeof(display_message), "%c%s\x01 %s", ((gamemod == Game_ZPS || gamemod == Game_GES)?5:4), message_prefix, client_message); + } + + new bool: setupColorForRecipients = false; + if (color_index == -1) + { + setupColorForRecipients = true; + } + + if (g_bTrackColors4Chat && is_colored != 2) + { + while (IsStackEmpty(message_recipients) == false) + { + new recipient_client = -1; + PopStackCell(message_recipients, recipient_client); + + new player_index = GetClientOfUserId(recipient_client); + if (player_index > 0 && !IsFakeClient(player_index) && IsClientInGame(player_index)) + { + if (setupColorForRecipients == true) + { + color_index = player_index; + } + new Handle:hBf; + hBf = StartMessageOne("SayText2", player_index); + if (hBf != INVALID_HANDLE) + { + BfWriteByte(hBf, color_index); + BfWriteByte(hBf, 0); + + if (gamemod == Game_CSGO) + { + // hackhackhack... + // CS:GO won't print any colors unless you not only start with standard color (1) + // like in other games, but also have a 'printable' character following it. We will just + // use an unused control code + BfWriteByte(hBf, 1); + BfWriteByte(hBf, 11); + } + + BfWriteString(hBf, display_message); + EndMessage(); + } + } + } + } + else + { + PrintToChatRecipients(display_message); + } + } + case Game_FF: + { + // thanks to hlstriker for help with this + + decl String: client_message_backup[192]; + strcopy(client_message_backup, sizeof(client_message_backup), client_message); + + if (is_colored == 1) + { + color_index = color_all_players(client_message); + if (color_index == -1) + { + color_team_entities(client_message); + } + } + + if (strcmp(message_prefix, "") == 0) + { + Format(display_message, sizeof(display_message), "Console: %s%s\n", ((is_colored == 2)?"^4":""), client_message); + } + else + { + Format(display_message, sizeof(display_message), "Console: ^4%s:%s %s\n", message_prefix, ((is_colored == 2)?"":"^"), client_message); + } + + PrintToChatRecipientsFF(display_message); + } + default: + { + if (strcmp(message_prefix, "") != 0) + { + Format(display_message, sizeof(display_message), "%s %s", message_prefix, client_message); + PrintToChatRecipients(display_message); + return Plugin_Handled; + } + PrintToChatRecipients(client_message); + } + } + return Plugin_Handled; +} + + +public Action:hlx_sm_psay2(args) +{ + if (args < 2) + { + PrintToServer("Usage: hlx_sm_psay2 - sends green colored private message"); + return Plugin_Handled; + } + + decl String: client_list[192]; + GetCmdArg(1, client_list, sizeof(client_list)); + BuildClientList(client_list); + + decl String: colored_param[32]; + GetCmdArg(2, colored_param, sizeof(colored_param)); + + new ignore_param = 0; + if (strcmp(colored_param, "2") == 0 || strcmp(colored_param, "1") == 0 || strcmp(colored_param, "0") == 0) + { + ignore_param = 1; + } + + new String: client_message[192]; + GetCmdArg((ignore_param + 2), client_message, sizeof(client_message)); + + if (IsStackEmpty(message_recipients)) { + return Plugin_Handled; + } + + // Strip color control codes + decl String:buffer_message[192]; + new j = 0; + for (new i = 0; i < sizeof(client_message); i++) + { + new char = client_message[i]; + if (char < 5 && char > 0) + { + continue; + } + buffer_message[j] = client_message[i]; + if (char == 0) + { + break; + } + j++; + } + + switch(gamemod) + { + case Game_INSMOD: + { + new prefix = 0; + if (strcmp(message_prefix, "") != 0) + { + prefix = 1; + Format(client_message, sizeof(client_message), "%s: %s", message_prefix, buffer_message); + } + + while (IsStackEmpty(message_recipients) == false) + { + new recipient_client = -1; + PopStackCell(message_recipients, recipient_client); + + new player_index = GetClientOfUserId(recipient_client); + if (player_index > 0 && !IsFakeClient(player_index) && IsClientInGame(player_index)) + { + // thanks to Fyren and IceMatrix for help with this + new Handle:hBf; + hBf = StartMessageOne("SayText", player_index); + if (hBf != INVALID_HANDLE) + { + BfWriteByte(hBf, 1); + BfWriteBool(hBf, true); + BfWriteByte(hBf, player_index); + + if (prefix == 0) + { + BfWriteString(hBf, buffer_message); + } + else + { + BfWriteString(hBf, client_message); + } + EndMessage(); + } + } + } + } + case Game_FF: + { + if (strcmp(message_prefix, "") == 0) + { + Format(client_message, sizeof(client_message), "Console: \x02^4%s\n", buffer_message); + } + else + { + Format(client_message, sizeof(client_message), "Console: \x02^4%s: %s\n", message_prefix, buffer_message); + } + + PrintToChatRecipientsFF(client_message); + } + case Game_ZPS, Game_GES: + { + if (strcmp(message_prefix, "") == 0) + { + Format(client_message, sizeof(client_message), "\x05%s", buffer_message); + } + else + { + Format(client_message, sizeof(client_message), "\x05%s %s", message_prefix, buffer_message); + } + PrintToChatRecipients(client_message); + } + default: + { + if (strcmp(message_prefix, "") == 0) + { + Format(client_message, sizeof(client_message), "\x04%s", buffer_message); + } + else + { + Format(client_message, sizeof(client_message), "\x04%s %s", message_prefix, buffer_message); + } + PrintToChatRecipients(client_message); + } + } + return Plugin_Handled; +} + + +public Action:hlx_sm_csay(args) +{ + if (args < 1) + { + PrintToServer("Usage: hlx_sm_csay - display center message"); + return Plugin_Handled; + } + + new String: display_message[192]; + GetCmdArg(1, display_message, sizeof(display_message)); + + if (strcmp(display_message, "") != 0) + { + if (gamemod == Game_L4D) + { + PrintToChatAll("\x03%s", display_message); + } + else + { + PrintCenterTextAll("%s", display_message); + } + } + + return Plugin_Handled; +} + + +public Action:hlx_sm_msay(args) +{ + if (args < 3) + { + PrintToServer("Usage: hlx_sm_msay