├── .gitattributes ├── .gitignore ├── README.md ├── afk.py ├── anti_rape.py ├── autospec.py ├── centerprint.py ├── disable_votes.py ├── funlimit.py ├── gauntonly.py ├── intermission.py ├── iouonegirl.py ├── mybalance.py ├── myban.py ├── myessentials.py ├── myirc.py ├── player_info.py ├── railable.py ├── sets.py ├── specprotect.py └── translate.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *backup* 2 | *kopie* 3 | balance.py 4 | ban.py 5 | branding.py 6 | clan.py 7 | damagetwo.py 8 | damage.py 9 | counter.py 10 | docs.py 11 | essentials.py 12 | maps.py 13 | motd.py 14 | names.py 15 | permission.py 16 | raw.py 17 | plugin_manager.py 18 | stationfun.py 19 | *.7z 20 | *.txt 21 | *.jar 22 | *.db 23 | *.zip 24 | *.cfg 25 | /* 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # minqlx-plugins by iouonegirl 2 | 3 | This repo will contain several plugins which I have developed for [Mino's minqlx](https://github.com/MinoMino/minqlx "MinoMino/minqlx"). 4 | 5 | Most ideas have been created, worked out and evaluated on , the official forum of the 'Bus Station' servers. 6 | 7 | Feel free to change variables and output messages in the files themselves, but this could introduce bugs if you don't really know what you're doing. As with most software, my plugins are not 100% foolproof and occasional unexpected behavior can occur. 8 | If you would notice any unusual behavior resulting from an 'iouplugin' on your server, please contact me about it. 9 | This also goes for any advice or crazy ideas for new plugins anyone might have. 10 | My email address can be found on github, and I can usually be found on the [Quakenet IRC](http://webchat.quakenet.org/?channels=minqlbot) channels #busstation, #iouonegirl, #minqlbot, #qldedsrv. 11 | 12 | If you wish to donate, please find a little paragraph and paypal link [below](https://github.com/dsverdlo/minqlx-plugins#donate). 13 | # Plugin list: 14 | | Name | Short Description | Raw | 15 | | ---- | :---------------: | :-- | 16 | [`afk`](https://github.com/dsverdlo/minqlx-plugins#afk)|Detect AFK people and place them in spectator (after a warning).|[`raw`](https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/afk.py) 17 | [`anti-rape`](https://github.com/dsverdlo/minqlx-plugins#anti-rape)|In round-based game modes; apply calculated handicaps to people playing above the server average|[`raw`](https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/anti_rape.py) 18 | [`autospec`](https://github.com/dsverdlo/minqlx-plugins#autospec)|If CA or FT teams are uneven, make the last person spec.|[`raw`](https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/autospec.py) 19 | [`centerprint`](https://github.com/dsverdlo/minqlx-plugins#centerprint)|Provides easy way to broadcast messages on peoples screens, and provides a "last enemy standing" toggle.|[`raw`](https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/centerprint.py) 20 | [`disable_votes`](https://github.com/dsverdlo/minqlx-plugins#disable_votes)|Disable the ability to make certain callvotes during a game.|[`raw`](https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/disable_votes.py) 21 | [`gauntonly`](https://github.com/dsverdlo/minqlx-plugins#gauntonly)|When 1 last standing person faces a lot of enemies, start gauntonly mode.|[`raw`](https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/gauntonly.py) 22 | [`funlimit`](https://github.com/dsverdlo/minqlx-plugins#funlimit)|Automatically disables fun(.py) sounds during a match/rounds.|[`raw`](https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/funlimit.py) 23 | [`intermission`](https://github.com/dsverdlo/minqlx-plugins#intermission)|Play 1 song out of a list after every match end.|[`raw`](https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/intermission.py) 24 | [`mybalance`](https://github.com/dsverdlo/minqlx-plugins#mybalance)|Elo-limits, warmup reminders, team balancing for CA,TDM,CTF,FT.|[`raw`](https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/mybalance.py) 25 | [`myban`](https://github.com/dsverdlo/minqlx-plugins#myban)|Use the !ban command with a player's name instead of ID.|[`raw`](https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/myban.py) 26 | [`myessentials`](https://github.com/dsverdlo/minqlx-plugins#myessentials)|Use names with the essential commands, like !red iou, !mute iou, !kick iou, ...|[`raw`](https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/myessentials.py) 27 | [`myirc`](https://github.com/dsverdlo/minqlx-plugins#myirc)|Supports broadcasting to keyed(passworded) channels, shows more colors, and broadcasts live updates to the topic.|[`raw`](https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/myirc.py) 28 | [`player_info`](https://github.com/dsverdlo/minqlx-plugins#player_info)|Display some player information. Maybe upon player connect if you want.(Also gives a warning or a ban for deactivated qlstats accounts)|[`raw`](https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/player_info.py) 29 | [`railable`](https://github.com/dsverdlo/minqlx-plugins#railable)|Toggle to get a 'railable' message when your health drops too low.|[`raw`](https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/railable.py) 30 | [`translate`](https://github.com/dsverdlo/minqlx-plugins#translate)|Look up normal and urban definitions, translate works and sentences, translate last 3 things someone said.|[`raw`](https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/translate.py) 31 | 32 | 33 | 34 | # **afk** 35 | - Detects players who are not moving and will slap them until they die and then puts them to spectator. 36 | - CVARS 37 | - qlx_afk_warning_seconds "10" 38 | - qlx_afk_detection_seconds "20" 39 | - qlx_afk_put_to_spec "1" 40 | 41 | # **anti-rape** 42 | - This plugin tries to detect overpowered players (for CA servers that like to maintain a certain range of skill), based on their score/second values. This plugin only works for round-based game modes. If players' score/second values are above a certain threshold (in regards to the server score/second average), an appropriate handicap will be assigned to them. This has proven to be an effective method of preventing one-sided games, ending in 0-10. The complete process of thoughts can be found in the 'handicap-thread' on the Bus Station forum. 43 | - CVARS 44 | - At the moment the values are all hard coded in the plugin itself 45 | - COMMANDS 46 | - !hc [\] - Get your own or somebody elses handicap % (this can sometimes be unreadable by profile pictures) 47 | - !handicaps - View all the currently given handicaps and their %'s 48 | - !gaps [silent] - View all the players who are playing above the server average and how % they are above it. (add 'silent' if you don't want anyone to see it) 49 | - NOTES 50 | - **Disclaimer:** The term 'rape' in this context is only used to describe an overpowered online player making the game unfair for others below his skill level. It is not meant in any way to offend or refer to the horrible crime that is also known under this name. 51 | 52 | # **auto_voice_switch** 53 | - This plugin is evaluated to be useless, since "g_allTalk 0" will automatically limit voice communication to team-only during a match. Plugin has been removed. 54 | 55 | # **autospec** 56 | - Displays a message during round countdown if teams are uneven, and forces the person (of the largest team) with the lowest score to spectate. If there is a big difference between teams, players will be autom. moved over. 57 | - CVARS 58 | - qlx_autospec_minplayers "2" (The minimum amount of players needed on the server to work) 59 | - qlx_autospec_maxplayers "99" (If there are more players than this cvar, autospec won't operate) 60 | 61 | # **centerprint** 62 | - Provides a way to broadcast a message on everyone's screen, or just to individual people. Handy for important server announcements. Also shows a 'One enemy left' message on the screen if people want it. 63 | - CVARS 64 | - qlx_cp_message "One enemy left. Start the hunt" 65 | - COMMANDS: 66 | - !showlast - toggle on/off if you want to see '1 enemy left' message 67 | - !print \ - print a message to a person's screen 68 | - !broadcast \ - print a message on everybody's screen 69 | - NOTES 70 | - The showlast message only works for round-based game modes, ofc 71 | 72 | # **disable_votes** 73 | - This plugin will disable the ability to callvote certain things during a match 74 | - CVARS 75 | - set qlx_disabled_votes_midgame "map" 76 | - set qlx_disabled_specvotes_midgame "teamsize, kick, clientkick" 77 | - set qlx_disabled_votes_permission "1" 78 | - COMMANDS 79 | - !disabled - shows which votes will be disabled 80 | - NOTES 81 | - For spectators, the combination of the two lists will be their restriction (unless they have permission, of course) 82 | 83 | # **gauntonly** 84 | - This plugin will activate a special mode when one player in a team-based gametype is left last standing against a large number of opponents. The minimum amount of opponnents needed can be specified via MAX and the special mode will turn itself off when a minimum is reached. With the default variables, a 1v5 will activate it, and if it becomes 1v2 the mode will turn off 85 | - CVARS 86 | - set qlx_gaunt_min "2" 87 | - set qlx_gaunt_max "4" 88 | 89 | # **funlimit** 90 | - Annoyed with the 'fun' sounds constantly being spammed? This plugin will disable all the sounds of fun.py when a match is in progress. In between rounds it is also shortly turned on, for round-based game-types. 91 | - CVARS 92 | - qlx_funlimit_messages "1" (this will display a message in chat everytime the sounds are enabled/disabled) 93 | - COMMANDS 94 | - !funsounds - This command will tell you if fun sounds are currently enabled or disabled 95 | - NOTES 96 | 97 | # **intermission** 98 | - A music plugin similar to roasticle's intermission. This plugin will loop over a specified collection of sounds/music, by playing one sound at the end of a match. Upload sounds/music in a PK3 file to the workshop for it to work. 99 | - CVARS 100 | - COMMANDS 101 | - NOTES 102 | - Read the installation and usage in the plugin code itself 103 | 104 | # **mybalance** 105 | - This plugin is designed to be used TOGETHER with Mino's balance plugin, but adds some more features, like skill rating-limits for connecting players, using the elo commands by names, and applying an action to the last person on uneven teams (slay, spec or ignore). 106 | Furthermore this plugin uses a text file in which exceptions can be placed for the elo restrictions, and adds a little bump to the elo restriction for regular players. Players falling outside the provided skill rating interval can be blocked on their connection screen, be kicked after a while on the server, or can be allowed to just spectate. Furthermore warmup reminders can be scheduled to repeat at certain intervals to remind players to ready up (if warmup takes too long). In CTF and TDM matches (no rounds), a player will be frozen in place until the teams are even again. Otherwise he is sent back to spectator. 107 | - CVARS 108 | - qlx_elo_limit_min "0" 109 | - qlx_elo_limit_max "1600" (set to 9999 or something to have no real upper limit) 110 | - qlx_elo_games_needed "10" (games needed before skill restriction is aplied) 111 | - qlx_mybalance_perm_allowed "2" (players with this perm-level will always be allowed) 112 | - qlx_mybalance_autoshuffle "0" (set "1" if you want an automatic shuffle before every match) 113 | - qlx_mybalance_exclude "0" (set "1" if you want to kick players who don't have enough info/games) 114 | - qlx_elo_kick "1" (set "1" to kick spectators after they joined) 115 | - qlx_elo_block_connecters "0" (set "1" to block players from connecting) 116 | - qlx_elo_close_enough "20" (if block_connecters is on, you can allow some people to join the server, who are 'close enough' to the limit, and they will get a normal kick. (which can be canceled via !nokick). Example, limit is 1800, then someone with 1805 will not be blocked, but get a normal kick) 117 | - qlx_mybalance_warmup_seconds "300" (how many seconds of warmup before readyup messages come. Set to -1 to disable) 118 | - qlx_mybalance_warmup_interval "60" (interval in seconds for readyup messages) 119 | - qlx_mybalance_uneven_time "10" (for CTF and TDM, specify how many seconds to wait before balancing uneven teams) 120 | - qlx_mybalance_elo_bump_regs "[[50,100],[100,300]]" (with this cvar you can setup an elo bump for regular players (example 50 games played on server = 100 more elo allowed)) 121 | - COMMANDS 122 | - !limit, !limits, !elolimit - view the skill rating limits, and the action which will be performed on outliers 123 | - !elomin [n] - without number; shows minimum allowed glicko. with number; temporary changes the minimum glicko 124 | - !elomax [n] - without number; shows maximum allowed glicko. with number; temporary changes the maximum glicko 125 | - !rankings [A|B] - without A or B; shows which rankings are being fetched (A (normal) or B (fun settings)) 126 | - !reminders [ON|OFF] - can turn warmup reminder messages on or off 127 | - !elo, !getelo - can get your own skill rating or that of someone else 128 | - !belo - this is like elo, but will show both your A-ranking and your B-ranking 129 | - !elokicked - view list of kicked people 130 | - !remkicked \ - after !elokicked, you can use the ID from that list to remove them from the kicklist) 131 | - !add_exception \ - adds an exception to the exception list 132 | - !nokick [\], !dontkick [\] - prevent a person from being kicked. (if only one player is being kicked, you dont have to pass their name) 133 | - !reload_exceptions - This will reload all the exceptions from the exceptions file. You will be able to see the ID's and names in the console. 134 | - NOTES 135 | - If you enable strict mode (qlx_mybalance_exclude "1") and qlstats goes down, players will not be able to join the server, since they won't have enough information to prove that they fall in the right skill rating limitation. 136 | 137 | # **myban** 138 | - This plugin enhances all the commands of the ban plugin. With this plugin you can also pass (part of) names of players to commands, instead of ID's only. 139 | - CVARS 140 | - COMMANDS 141 | - ... all the same as the ban plugin ... 142 | - NOTES 143 | - You don't have to remove the ban plugin, the loading of myban will unload it automatically 144 | 145 | # **myessentials** 146 | - This plugin enhances all the commands of the essentials plugin. With this plugin you can also give names, or part of names of players to call the commands, instead of ID's only. 147 | - CVARS 148 | - COMMANDS 149 | - ... all the same as the essentials plugin ... 150 | - NOTES 151 | - You don't have to remove the essentials plugin, the loading of myessentials will unload it automatically 152 | 153 | # **myirc** 154 | - This plugin is an extension of Mino's original plugin, aimed to enhance it a little. It uses more colors/formatting to make messages clearer. MyIrc plugin can also connect and broadcast to/from passworded irc channels via a new cvar, and a live broadcast is shown in the topic. 155 | - (EXTRA) CVARS 156 | - qlx_ircRelayChannelPw = "" 157 | - (EXTRA) IRC COMMANDS 158 | - .topic - Grabs the latest server update and sets it as the topic 159 | - (EXTRA) OIDENTD 160 | - Support for ident-server oidentd. Each qlds running myirc.py can have its unique ident. Quakenet requires to apply for a trust if people want more than 5 clients from the same ip, a working ident server is among their conditions. See [`here`](https://www.quakenet.org/help/trusts/connection-limit). Example for oidentd base config [`/etc/oidentd.conf`](https://gist.github.com/mattiZed/78d4d0d21c035c73efdff4f1af2040f6) 161 | - NOTES 162 | - If the plugin is not changing the topic in the IRC channel, make sure it has the required privileges (+t) 163 | - Preview: http://i.imgur.com/JyFsgjD.png 164 | 165 | # **player_info** 166 | - Displays some more info about a player if the info command is used, and also provides a method to check a player's scoreboard information (in big CA matches people sometimes fall off / just below the scoreboard) 167 | - CVARS 168 | - qlx_pinfo_display_auto "0" (set this to 1 if you want to see automatic info upon player connect) 169 | - qlx_pinfo_show_deactivated "1" (while this is "1" it will display a warning when a player connects with deactivated qlstats acccount (due to cheating or other bad things)) 170 | - qlx_pinfo_ban_deactivated "0" (set this to "1" to automatically ban players with the deactivated qlstats status) 171 | - COMMANDS 172 | - !info [\] - display some information, like games played, quit frequency, glicko 173 | - !scoreboard - display scoreboard information when players fall 'below' it 174 | - !allelo [\] - for one person, display the known skill ratings of each game-mode 175 | - NOTES 176 | 177 | # **railable** 178 | - This plugin can give you a message (centerprinted) when your health drops to a level where you can be killed with 1 rail. Developed for Clan Arena. 179 | - CVARS 180 | - COMMANDS 181 | - !railable (this command toggles the service on and off) 182 | - !railmsg \ (with this command you can choose the msg to be printed) 183 | - NOTES 184 | - This plugin will do several checks each second, so if you notice too much CPU usage, it is advised to unload the plugin 185 | - This plugin is not considered cheating, since you could also get your HUD to display this information 186 | 187 | # **translate** 188 | - Provides methods to translate any words or sentences into another language, using the Yandex Translate API. Also able to look up normal english definitions and Urban Dictionaries definitions. 189 | - CVARS 190 | - qlx_translate_api_key "" 191 | - COMMANDS 192 | - !translate-last \ \ - Translates the last 3 things the given player said into the language specified 193 | - !translate \ \ 194 | - !translate en Deze zin is vertaald geweest. -> Translation: This sentence has been translated. 195 | - !translate Russian Understood -> Translation (en-ru): Понимал 196 | - !define match -> Definition: a contest in which people or teams compete against each other in a particular sport. 197 | - !urban bye -> UrbanDef: a nicer way to say "your f-ing ugly. get out of my face" 198 | - !languages - Displays all the supported languages and their tags 199 | - !translations - Displays all the supported translation directions 200 | - NOTES 201 | - Get your FREE yandex API key here: https://tech.yandex.com/keys/get/?service=trnsl 202 | - Example usage: http://i.imgur.com/WL5zNOR.png 203 | 204 | 205 | # **Donate** 206 | When minqlbot became popular and I found out we could write our own plugins, I saw an opportunity and a vision to put my coding skills to good use and try to improve the Quake Live gameplay experience. This hobby, as I would call it, somewhat pays for its own in the satisfaction I feel of having contributed something useful to my favorite game. As I see my plugins used and liked by so many players and servers, I find all the hours spent coding well worthwhile. Since some people asked, I will provide a donation link below for any of you generous people to use. Any donations given will be incredibly appreciated and will be going straight towards the necessary stuff that keeps my motivation and creativity at their peak. (Like coffee ^^) 207 | 208 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.me/dsverdlo) 209 | -------------------------------------------------------------------------------- /afk.py: -------------------------------------------------------------------------------- 1 | # This is a plugin created by iouonegirl(@gmail.com) 2 | # Copyright (c) 2016 iouonegirl 3 | # https://github.com/dsverdlo/minqlx-plugins 4 | # 5 | # You are free to modify this plugin to your custom, 6 | # except for the version command related code. 7 | # 8 | # Its purpose is to detect afk players and provide actions 9 | # to be taken on them. For now only working in team-based 10 | # gametypes. 11 | # 12 | # Uses: 13 | # - qlx_afk_warning_seconds "10" 14 | # - qlx_afk_detection_seconds "20" 15 | # - qlx_afk_put_to_spec "1" 16 | 17 | 18 | import minqlx 19 | import threading 20 | import time 21 | import os 22 | import requests 23 | 24 | VERSION = "v0.15" 25 | 26 | VAR_WARNING = "qlx_afk_warning_seconds" 27 | VAR_DETECTION = "qlx_afk_detection_seconds" 28 | VAR_PUT_SPEC = "qlx_afk_put_to_spec" 29 | 30 | # Interval for the thread to update positions. Default = 0.33 31 | interval = 0.33 32 | 33 | # This code makes sure the required superclass is loaded automatically 34 | try: 35 | from .iouonegirl import iouonegirlPlugin 36 | except: 37 | try: 38 | abs_file_path = os.path.join(os.path.dirname(__file__), "iouonegirl.py") 39 | res = requests.get("https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/iouonegirl.py") 40 | if res.status_code != requests.codes.ok: raise 41 | with open(abs_file_path,"a+") as f: f.write(res.text) 42 | from .iouonegirl import iouonegirlPlugin 43 | except Exception as e : 44 | minqlx.CHAT_CHANNEL.reply("^1iouonegirl abstract plugin download failed^7: {}".format(e)) 45 | raise 46 | 47 | # Start plugin 48 | class afk(iouonegirlPlugin): 49 | 50 | def __init__(self): 51 | super().__init__(self.__class__.__name__, VERSION) 52 | 53 | # Set required cvars once. DONT EDIT THEM HERE BUT IN SERVER.CFG 54 | self.set_cvar_once(VAR_WARNING, "10") 55 | self.set_cvar_once(VAR_DETECTION, "20") 56 | self.set_cvar_once(VAR_PUT_SPEC, "1") 57 | 58 | # Get required cvars 59 | self.warning = int(self.get_cvar(VAR_WARNING)) 60 | self.detection = int(self.get_cvar(VAR_DETECTION)) 61 | self.put_to_spec = int(self.get_cvar(VAR_PUT_SPEC)) 62 | 63 | # steamid : [position, seconds] 64 | self.positions = {} 65 | 66 | # keep looking for AFK players 67 | self.running = False 68 | 69 | # punished players 70 | self.punished = [] 71 | 72 | self.add_hook("round_start", self.handle_round_start) 73 | self.add_hook("round_end", self.handle_round_end) 74 | self.add_hook("team_switch", self.handle_player_switch) 75 | self.add_hook("unload", self.handle_unload) 76 | self.add_hook("death", self.handle_death) 77 | 78 | 79 | 80 | 81 | 82 | def handle_unload(self, plugin): 83 | if plugin == self.__class__.__name__: 84 | self.running = False 85 | self.punished = [] 86 | 87 | def handle_round_start(self, round_number): 88 | teams = self.teams() 89 | for p in teams['red'] + teams['blue']: 90 | self.positions[p.steam_id] = [self.help_get_pos(p), 0] 91 | 92 | self.punished = [] 93 | 94 | # start checking thread 95 | self.running = True 96 | self.help_create_thread() 97 | 98 | def handle_round_end(self, round_number): 99 | self.running = False 100 | self.punished = [] 101 | 102 | def handle_player_switch(self, player, old, new): 103 | if new == 'spectator': 104 | if player.steam_id in self.positions: 105 | del self.positions[player.steam_id] 106 | if player in self.punished: 107 | self.punished.remove(player) 108 | 109 | if new in ['red', 'blue']: 110 | self.positions[player.steam_id] = [self.help_get_pos(player), 0] 111 | 112 | @minqlx.thread 113 | def help_create_thread(self): 114 | while self.running and self.game and self.game.state == 'in_progress': 115 | teams = self.teams() 116 | for p in teams['red'] + teams['blue']: 117 | pid = p.steam_id 118 | 119 | if not p.is_alive: continue 120 | 121 | if pid not in self.positions: 122 | self.positions[pid] = [self.help_get_pos(p), 0] 123 | 124 | prev_pos, secs = self.positions[pid] 125 | curr_pos = self.help_get_pos(p) 126 | 127 | # If position stayed the same, add the time difference and check for thresholds 128 | if prev_pos == curr_pos: 129 | self.positions[pid] = [curr_pos, secs+interval] 130 | if secs+interval >= self.warning and secs < self.warning: 131 | self.help_warn(p) 132 | elif secs+interval >= self.detection and secs < self.detection: 133 | self.help_detected_print(p) 134 | else: 135 | self.positions[pid] = [curr_pos, 0] 136 | if p in self.punished: 137 | # if the player started moving, remove him from punished players 138 | self.punished.remove(p) 139 | 140 | time.sleep(interval) 141 | 142 | def handle_death(self, victim, killer, data): 143 | if victim in self.punished: 144 | self.punished.remove(victim) 145 | if victim.steam_id in self.positions: 146 | del self.positions[victim.steam_id] 147 | 148 | @minqlx.next_frame 149 | def help_warn(self, player): 150 | message = "You have been inactive for {} seconds...".format(self.warning) 151 | minqlx.send_server_command(player.id, "cp \"\n\n\n{}\"".format(message)) 152 | 153 | @minqlx.next_frame 154 | def help_detected_print(self, player): 155 | self.msg("^1{} ^1has been inactive for {} seconds! Commencing punishment!".format(player.name, int(self.positions[player.steam_id][1]))) 156 | self.punished.append(player) 157 | self.punish(player) 158 | 159 | 160 | @minqlx.thread 161 | def punish(self, player, pain=10, wait=0.5): 162 | @minqlx.next_frame 163 | def spec(_p): _p.put('spectator') 164 | @minqlx.next_frame 165 | def subtract_health(_p, _h): _p.health -= _h 166 | 167 | while self.game and self.game.state == 'in_progress' and player in self.punished: 168 | if not player.is_alive or player.health < pain: 169 | self.punished.remove(player) 170 | if self.put_to_spec: spec(player) 171 | break 172 | 173 | subtract_health(player, pain) 174 | if player.steam_id in self.positions: 175 | s = int((self.positions[player.steam_id])[1]) 176 | else: 177 | s = self.detection 178 | message = "^1Inactive for {} seconds! \n\n^7Move or keep getting damage!".format(s) 179 | minqlx.send_server_command(player.id, "cp \"\n\n\n{}\"".format(message)) 180 | time.sleep(wait) 181 | 182 | 183 | return 184 | 185 | 186 | def help_get_pos(self, player): 187 | return player.position() 188 | 189 | -------------------------------------------------------------------------------- /anti_rape.py: -------------------------------------------------------------------------------- 1 | # This is a plugin created by iouonegirl(@gmail.com) 2 | # Copyright (c) 2016 iouonegirl 3 | # https://github.com/dsverdlo/minqlx-plugins 4 | # 5 | # You are free to modify this plugin to your custom, 6 | # except for the version command related code. 7 | # 8 | # The creative ideas for this plugin came from correspondance 9 | # with Gelenkbusfahrer and have been mainly discussed 10 | # station.boards.net, forum of the Bus Station server(s). 11 | # 12 | # Its purpose is to detect rapers and give them fair handicaps. 13 | # The detection rules have been intensely discussed and finetuned, 14 | # and can be found on station.boards.net (home forum of the bus station). 15 | 16 | # Disclaimer: any use of the word 'rape' is not meant to be offensive or 17 | # offend anyone in any way. This was simply the most fitting term for an 18 | # overpowered player, winning the game and defeating a lot of people with 19 | # ease. Actual rape in the real world is a terrible crime and nothing to 20 | # be taken lightly or laughed with. 21 | 22 | import minqlx 23 | import time 24 | import os 25 | import datetime 26 | import threading 27 | import requests 28 | from math import floor 29 | 30 | VERSION = "v0.51" 31 | 32 | 33 | # From which percentage we classify a rape. 34 | # Anything over the upper rape gap will mark a player as a raper 35 | # Rapers will receive to get a handicap if they are not in a losing team, 36 | # or is their gap is below the lower rape gap. 37 | # For more information about these gaps please visit station.boards.net 38 | RAPE_MIDER_GAP = 20 # default 20 39 | RAPE_UPPER_GAP = 20 # default 20 40 | 41 | # We want the first rounds to have a higher treshhold to mark a player 42 | # Adjust this dictionary to multiply the UPPER_GAP by an amount for a given round 43 | # E.g. in (early) round 3 we can multiply UPPER_GAP * 4 44 | # Note: this dict starts from round 2 because that's what we define later as the ROUNDS_NEEDED 45 | RAPE_UPPER_GAP_ADJUSTMENTS = {2:8, 3:4, 4:3, 5:2} # remove items in {brackets} to disable service 46 | 47 | # The lowest possible handicap that will be forced 48 | HC_LOWEST = 50 49 | 50 | # This allows us to multiply the original handicap according to round differences 51 | # E.g. if the round difference is 3 --> multiply the handicap (%) with 0.8 to make it stronger 52 | USE_HANDICAP_ADJUSTMENTS = True 53 | HANDICAP_ADJUSTMENTS = {0:1, 1:1, 2:0.9, 3:0.8} 54 | DEFAULT_HANDICAP_ADJUSTMENT = 0.7 55 | 56 | # The amount of rounds and people needed before we start calculating. 57 | ROUNDS_NEEDED = 2 # default 2 58 | PEOPLE_NEEDED = 4 # default 4 59 | 60 | 61 | # Do not modify any of these variables 62 | TIME_FORMAT = "%Y-%m-%d %H:%M:%S" 63 | COMPLETED_KEY = "minqlx:players:{}:games_completed" 64 | PLAYER_KEY = "minqlx:players:{}" 65 | _name_key = "minqlx:players:{}:colored_name" 66 | HC_TAG = "new" 67 | SHC_TAG = "new:" 68 | 69 | # This code makes sure the required superclass is loaded automatically 70 | try: 71 | from .iouonegirl import iouonegirlPlugin 72 | except: 73 | try: 74 | abs_file_path = os.path.join(os.path.dirname(__file__), "iouonegirl.py") 75 | res = requests.get("https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/iouonegirl.py") 76 | if res.status_code != requests.codes.ok: raise 77 | with open(abs_file_path,"a+") as f: f.write(res.text) 78 | from .iouonegirl import iouonegirlPlugin 79 | except Exception as e : 80 | minqlx.CHAT_CHANNEL.reply("^1iouonegirl abstract plugin download failed^7: {}".format(e)) 81 | raise 82 | 83 | 84 | class anti_rape(iouonegirlPlugin): 85 | def __init__(self): 86 | super().__init__(self.__class__.__name__, VERSION) 87 | 88 | # Some dictionaries (self explanatory) keys: steam_id, values: value 89 | self.handicaps = {} 90 | self.help_remove_handicaps() 91 | 92 | # Store a counter of the amount of rounds played per player 93 | self.rounds_played = {} 94 | 95 | # A frozen snapshot of the damages until the round ends 96 | # Format example: { '77935123':['red', 200], '78139445':['blue',50] } 97 | self.scores_snapshot = {} 98 | 99 | # Real scores for handicapped people steam_id:realscore 100 | self.realscores = {} 101 | self.realdamage = {} 102 | 103 | # List of players that have just joined. Start of round will empty this. 104 | # Ignore the players at the end of the round that are in here 105 | self.just_joined = [] 106 | 107 | # Track when we are in round countdown (to compensate self-kills) 108 | self.round_countdown = True 109 | 110 | self.add_command("hc", self.cmd_get_hc, usage="[|] [silent]") 111 | self.add_command("sethc", self.cmd_set_hc, 2, usage="[|] [<1-100>]") 112 | self.add_command(("remhcs", "clearhcs", "delhcs"), self.cmd_rem_handicaps, 2) 113 | self.add_command(("viewhcs", "listhcs", "handicaps"), self.cmd_list_handicaps, usage="[silent]") 114 | self.add_command(("hc_info", "hcinfo"), self.cmd_info) 115 | self.add_command(("hc_gaps", "gaps"), self.cmd_get_gaps, usage="[silent]") 116 | self.add_command(("rapers", "getrapers"), self.cmd_get_rapers, 2, usage="[silent]") 117 | self.add_command(("raper", "setraper", "mark"), self.cmd_set_raper, 2, usage="|") 118 | self.add_command("unmark", self.cmd_unsert_raper, 2, usage="|") 119 | self.add_command(("hc_cmds", "hccmds"), self.cmd_hc_commands) 120 | self.add_hook("game_countdown",self.handle_game_countdown) 121 | self.add_hook("player_loaded", self.handle_player_loaded) 122 | self.add_hook("team_switch", self.handle_team_switch) 123 | self.add_hook("round_start", self.handle_round_start) 124 | self.add_hook("round_end", self.handle_round_end) 125 | self.add_hook("userinfo", self.handle_user_info) 126 | self.add_hook("death", self.handle_death) 127 | 128 | 129 | def handle_game_countdown(self): 130 | # Remove all the handicaps 131 | self.help_remove_handicaps() 132 | 133 | # Clear all marks 134 | self.handicaps = {} 135 | 136 | # Clear the snapshots 137 | self.scores_snapshot = {} 138 | 139 | # Self explanatory 140 | self.realscores = {} 141 | self.realdamage = {} 142 | 143 | # Self explanatory 144 | self.round_countdown = True 145 | 146 | # Set the round count at 0 for each playing player 147 | teams = self.teams() 148 | for _p in teams['red'] + teams['blue']: 149 | self.rounds_played[_p.steam_id] = 0 150 | 151 | def handle_team_switch(self, player, old, new): 152 | # Not in-game, nothing to be done 153 | if not self.game or (self.game.state != "in_progress"): return 154 | 155 | # If a player joins/switches during the match, set his counter to 0 156 | if new in ['red', 'blue']: 157 | self.rounds_played[player.steam_id] = 0 158 | self.scores_snapshot[player.steam_id] = [new, 0] 159 | self.realscores[player.steam_id] = 0 160 | self.realdamage[player.steam_id] = 0 161 | if not player.steam_id in self.just_joined: 162 | self.just_joined.append(player.steam_id) 163 | 164 | # If a player specs during the match, remove his counters 165 | if new in ['spec', 'free']: 166 | if player.steam_id in self.rounds_played: 167 | del self.rounds_played[player.steam_id] 168 | if player.steam_id in self.scores_snapshot: 169 | del self.scores_snapshot[player.steam_id] 170 | if player.steam_id in self.realscores: 171 | del self.realscores[player.steam_id] 172 | if player.steam_id in self.realdamage: 173 | del self.realdamage[player.steam_id] 174 | 175 | 176 | # On disconnect, remove from our dictionaries 177 | def handle_player_disconnect(self, player, reason): 178 | if player.steam_id in self.rounds_played: 179 | del self.rounds_played[player.steam_id] 180 | if player.steam_id in self.handicaps: 181 | del self.handicaps[player.steam_id] 182 | if player.steam_id in self.realscores: 183 | del self.realscores[player.steam_id] 184 | if player.steam_id in self.realdamage: 185 | del self.realdamage[player.steam_id] 186 | 187 | 188 | # just a little longer delay than the myban plugin 189 | @minqlx.delay(6) 190 | def handle_player_loaded(self, player): 191 | 192 | try: 193 | player.update() 194 | except minqlx.NonexistentPlayerError: 195 | return 196 | 197 | if self.game and self.game.state == "in_progress": 198 | # Remove a potential previous handicap 199 | if ('handicap' in player.cvars) and (int(player.cvars['handicap']) < 100): 200 | self.set_silent_handicap(player, 100) 201 | del self.handicaps[player.steam_id] 202 | 203 | # We intercept every user info change, in order to block client handicap change requests 204 | def handle_user_info(self, player, d): 205 | if not 'handicap' in d: return d 206 | 207 | # If the player's handicap has been set by the server: 208 | if player.steam_id in self.handicaps: 209 | set_hc = self.handicaps[player.steam_id] 210 | 211 | # Is this a result of server forcing a handicap? 212 | if set_hc.startswith(HC_TAG): 213 | self.handicaps[player.steam_id] = set_hc.strip(SHC_TAG) 214 | if not (SHC_TAG in set_hc): 215 | minqlx.CHAT_CHANNEL.reply("^6{}^7's handicap has been set to: ^3{}^7%".format(player.name, self.handicaps[player.steam_id])) 216 | return d 217 | 218 | # At this stage, the request was not started by the server, but by a player. 219 | # Restore his previous handicap and tell him this is not allowed on the server 220 | prev_hc = int(player.cvars['handicap']) 221 | new_hc = int( d['handicap'] ) 222 | player.tell("^3Handicap request denied. This server will automatically set appropriate handicap levels.") 223 | d['handicap'] = prev_hc 224 | return d 225 | 226 | 227 | 228 | def handle_death(self, victim, killer, data): 229 | if not (self.game.state != 'in-progress' or self.round_countdown or killer): 230 | self.realscores[victim.steam_id] = self.realscores.get(victim.steam_id, 0) - 1 231 | 232 | def handle_round_start(self, round_number): 233 | self.just_joined = [] 234 | self.round_countdown = False 235 | 236 | # On round end check if we need to check for rapist warnings 237 | @minqlx.delay(6.5) 238 | def handle_round_end(self, data): 239 | def calc_time_delta(time1, time2): 240 | return abs(time1 - time2) 241 | 242 | def say(message): 243 | minqlx.CHAT_CHANNEL.reply("^7"+message) 244 | 245 | def is_rapist(gap, steam_id): 246 | # If he was already marked 247 | if steam_id in self.handicaps: return True 248 | # Or if he satisfies the conditions 249 | if steam_id in self.rounds_played: 250 | # get rounds played of player 251 | n = self.rounds_played[steam_id] 252 | # get an adjustment maybe 253 | adjusted_gap = RAPE_UPPER_GAP * RAPE_UPPER_GAP_ADJUSTMENTS.get(n, 1) 254 | # if the player played the amount of rounds needed and is higher than threshold, mark him 255 | if n >= ROUNDS_NEEDED and gap >= adjusted_gap: 256 | return True 257 | 258 | # Else not 259 | return False 260 | 261 | def mark_rapist(player): 262 | # add him into handicap table with default HC to mark him 263 | if not (player.steam_id in self.handicaps): 264 | self.handicaps[player.steam_id] = 100 265 | 266 | def unmark_rapist(player): 267 | if player.steam_id in self.handicaps: 268 | self.set_silent_handicap(player, 100) 269 | del self.handicaps[player.steam_id] 270 | 271 | def reset(player): 272 | if player.steam_id in self.handicaps: 273 | self.set_silent_handicap(player, 100) 274 | 275 | def update_realscores(teams): 276 | minqlx.console_command("echo Updating realscores (red: {} - blue: {})".format(self.game.red_score, self.game.blue_score)) 277 | 278 | # Calculate if we are in a special case (in case of plugin reload) 279 | first_round = self.game.red_score + self.game.blue_score == 1 280 | special_case = not (self.scores_snapshot or first_round) 281 | 282 | for _p in teams['red'] + teams['blue']: 283 | # Gather the data 284 | sid = _p.steam_id 285 | score = _p.stats.score 286 | frags = _p.stats.kills 287 | curr_dmg = _p.stats.damage_dealt 288 | hc = int(_p.cvars.get('handicap', 100)) 289 | prev_dmg = self.scores_snapshot.get(sid, [None, curr_dmg if special_case else 0])[1] 290 | diff = curr_dmg - prev_dmg 291 | actual_diff = diff / hc 292 | 293 | # Calculate / update the 'real' scores 294 | self.realdamage[sid] = self.realdamage.get(sid, 0) + actual_diff 295 | self.realscores[sid] = int(self.realdamage[sid] + frags/2) 296 | 297 | # while we're here, update snapshots for next round 298 | self.scores_snapshot[sid] = [_p.team, curr_dmg] 299 | 300 | #dbg = "echo DBG: {}({}%) pdmg: {} cdmg: {} diff: {} tot.kills: {} scr: {} rscr: {}" 301 | #minqlx.console_command(dbg.format(_p.name, hc, prev_dmg, curr_dmg, diff, frags, score, self.realscores[sid])) 302 | 303 | # If this was the last round, nothing to do 304 | if self.game.roundlimit in [self.game.blue_score, self.game.red_score]: 305 | return self.clear_all_handicaps() 306 | 307 | # We are now in round countdown until round starts 308 | self.round_countdown = True 309 | 310 | # Add players that werent in it before (maybe plugin reload during game) 311 | teams = self.teams() 312 | for _p in teams['red'] + teams['blue']: 313 | if _p.steam_id not in self.rounds_played: 314 | self.rounds_played[_p.steam_id] = 0 315 | 316 | # Increase all the round counters (except people that just joined during a round) 317 | for _id in self.rounds_played: 318 | if not _id in self.just_joined: 319 | self.rounds_played[_id] += 1 320 | 321 | # Update the realscores 322 | update_realscores(teams) 323 | 324 | # Check will contain a list of players in the winning team(s) 325 | check = teams['blue'].copy() 326 | if self.game.red_score == self.game.blue_score: 327 | check += teams['red'] 328 | elif self.game.red_score > self.game.blue_score: 329 | check = teams['red'] 330 | 331 | # For all playing players 332 | all_players = teams['red'] + teams['blue'] 333 | for p in all_players: 334 | gap = self.help_calc_rape_gap(p) 335 | if gap == "invalid": return # no server average available, return function 336 | 337 | # If we have a rapist, mark him for the game 338 | rapist = is_rapist(gap, p.steam_id) 339 | #if rapist: mark_rapist(p) 340 | 341 | # If player is not in losing team 342 | if p.steam_id in map(lambda _p: _p.steam_id, check): 343 | # If he is a rapist and has higher than MID gap, set a normal handicap 344 | if rapist and (gap >= RAPE_MIDER_GAP): 345 | hc = self.help_get_hc_suggestion(gap) 346 | if hc: self.set_silent_handicap(p, hc) 347 | if hc: p.center_print("HC {}%".format(hc)) 348 | if hc: self.delay(["","^6{}^7 score index: ^1{}^7% above average - Handicap set to: ^3{}^7%".format(p.name, gap, hc)], 0.3) 349 | continue 350 | 351 | # If player is in losing team or was not a rapist with a high gap 352 | reset(p) 353 | 354 | 355 | # Little debug command, to check the rape scores during a game. Rape scores <= 0% are not shown. 356 | # don't forget to add 'silent' behind it to get silent calculations 357 | def cmd_get_gaps(self, player, msg, channel): 358 | if self.game.state != "in_progress": 359 | player.tell("^1Error^7: No game in progress!") 360 | return minqlx.RET_STOP_ALL 361 | 362 | teams = self.teams() 363 | 364 | reds = {} 365 | for p in teams['red']: 366 | reds[p.name] = self.help_calc_rape_gap(p) 367 | blues = {} 368 | for p in teams['blue']: 369 | blues[p.name] = self.help_calc_rape_gap(p) 370 | 371 | if ("invalid" in reds.values()) or ("invalid" in blues.values()): 372 | if len(msg) < 2: 373 | channel.reply("^7Not enough data to calculate server average score/min...") 374 | return 375 | player.tell("^6Psst: ^7not enough data to calculate server average score/min...") 376 | return minqlx.RET_STOP_ALL 377 | 378 | rreds = [] 379 | for name in sorted(reds, key=lambda i: reds[i], reverse=True): # sort small -> big 380 | gap = reds[name] 381 | if gap <= 0: continue 382 | rreds.append("{}:^3{}%^7".format(name, gap)) # append at the end 383 | 384 | bblues = [] 385 | for name in sorted(blues, key=lambda i: blues[i], reverse=True): 386 | gap = blues[name] 387 | if gap <= 0: continue 388 | bblues.append("{}:^3{}%^7".format(name, gap)) 389 | 390 | messages = ["^7Bus Station current score/min values compared to server average:", 391 | "^1Red^7: {}".format("^1,^7".join(rreds)), 392 | "^4Blue^7: {}".format("^4,^7".join(bblues))] 393 | 394 | if len(msg) < 2: 395 | self.delay(messages, 0.3) 396 | else: 397 | self.delaytell(messages, player, 0.3) 398 | return minqlx.RET_STOP_ALL 399 | 400 | 401 | 402 | def cmd_get_hc(self, player, msg, channel): 403 | """Check a person's handicap percentage. If no one was specified, 404 | display the handicap of the command calling player 405 | 406 | Ex: !hc - Returns callers' own HC 407 | Ex: !hc 2 - Returns the HC of player with ingame id 2 408 | Ex: !hc 2 silent - Returns a pm of the HC of player with ingame id 2 409 | Ex: !hc iou - Returns the HC of person with iou in their name. 410 | Ex: !hc iou silent - Returns a PM of the HC of person with iou in their name. 411 | """ 412 | 413 | if len(msg) == 1: 414 | target_player = player 415 | silent = False 416 | elif len(msg) == 2: 417 | target_player = self.find_by_name_or_id(player, msg[1]) 418 | silent = False 419 | elif len(msg) == 3 and msg[2] == "silent": 420 | target_player = self.find_by_name_or_id(player, msg[1]) 421 | silent = True 422 | else: 423 | return minqlx.RET_USAGE 424 | 425 | if target_player: 426 | name = target_player.name 427 | try: 428 | target_player.update() 429 | hc = target_player.cvars["handicap"] 430 | if int(hc) < 100: 431 | m = "^7Player ^6{} ^7is currently playing with handicap ^3{}^7%".format(target_player.name, hc) 432 | if silent: 433 | player.tell("^6Psst: "+m) 434 | return minqlx.RET_STOP_ALL 435 | else: 436 | channel.reply(m) 437 | else: 438 | m = "^7Player ^6{} ^7has no active handicap.".format(target_player.name) 439 | if silent: 440 | player.tell("^6Psst: "+m) 441 | return minqlx.RET_STOP_ALL 442 | else: 443 | channel.reply(m) 444 | except Exception as e: 445 | minqlx.console_command("echo Error: {}".format(e)) 446 | m = "^7Something unexpected happened while getting ^6{}^7's handicap.".format(name) 447 | if silent: 448 | player.tell("^6Psst: "+m) 449 | else: 450 | channel.reply(m) 451 | 452 | 453 | def cmd_get_rapers(self, player, msg, channel): 454 | def id_to_name(steam_id): 455 | # Try in the names database 456 | if self.db[_name_key.format(steam_id)]: 457 | return self.db[_name_key.format(steam_id)] 458 | # Try every player 459 | for p in self.players(): 460 | if p.steam_id == steam_id: 461 | return p.name 462 | # Give up 463 | return steam_id 464 | 465 | if (not self.game) or (self.game.state != "in_progress"): 466 | message = "^7No game in progress..." 467 | else: 468 | message = "^7Rapers: {}".format(",".join(['%s(^3%s%^7)' % (id_to_name(key), value) for (key, value) in self.handicaps.items()])) 469 | 470 | if len(msg) == 2 and msg[1] == "silent": 471 | player.tell("^6Psst: " + message) 472 | return minqlx.RET_STOP_ALL 473 | 474 | channel.reply(message) 475 | 476 | def cmd_hc_commands(self, player, msg, channel): 477 | cmds = ["!hc", "^1!sethc", "!hc_info", "!hc_info_mid", "!gaps", "^1!rapers", "^1!raper", "!unmark" ] 478 | channel.reply("^7Available anti_rape commands: ^2{}^7.".format("^7, ^2".join(cmds))) 479 | 480 | def cmd_info(self, player, msg, channel): 481 | channel.reply("^7Players with more than ^3{}^7% score/min than server average will be handicapped.".format(RAPE_UPPER_GAP)) 482 | return minqlx.RET_STOP_ALL 483 | 484 | 485 | def cmd_list_handicaps(self, player, msg, channel): 486 | handicapable_players = [] 487 | message = "^7There are no active handicaps on the server." 488 | 489 | for p in self.players(): 490 | if int(p.cvars.get('handicap', '100')) < 100: 491 | handicapable_players.append(p) 492 | 493 | if handicapable_players: 494 | message = "^7" + ",".join(list(map(lambda _p: "{}-^3{}%^7".format(_p.name, _p.cvars['handicap']), handicapable_players))) 495 | 496 | if len(msg) < 2: 497 | return channel.reply(message) 498 | elif len(msg) < 3 and msg[1] == 'silent': 499 | player.tell("^6Psst: ^7" + message) 500 | return minqlx.RET_STOP_ALL 501 | 502 | return minqlx.RET_USAGE 503 | 504 | def cmd_rem_handicaps(self, player, msg, channel): 505 | self.help_remove_handicaps() 506 | channel.reply("^7Done! There are no current handicaps on the server.") 507 | 508 | 509 | def cmd_set_hc(self, player, msg, channel): 510 | if len(msg) == 3: 511 | target = msg[1] 512 | target_player = self.find_by_name_or_id(player, target) 513 | if not target_player: 514 | return minqlx.RET_STOP_ALL 515 | try: 516 | hc = int(msg[2]) 517 | except: 518 | return minqlx.RET_USAGE 519 | elif len(msg) == 2: 520 | target_player = player 521 | try: 522 | hc = int(msg[1]) 523 | if not ( 1 <= hc <= 100 ): 524 | raise ValueError 525 | except: 526 | return minqlx.RET_USAGE 527 | else: 528 | return minqlx.RET_USAGE 529 | 530 | self.set_handicap(target_player, hc) 531 | # Delete so the plugin doesnt think it's a raper 532 | del self.handicaps[target_player.steam_id] 533 | 534 | 535 | 536 | def cmd_set_raper(self, player, msg, channel): 537 | if len(msg) < 2: 538 | return minqlx.RET_USAGE 539 | 540 | target_player = self.find_by_name_or_id(player, msg[1]) 541 | if not target_player: 542 | return minqlx.RET_STOP_ALL 543 | 544 | if target_player.steam_id in self.handicaps: 545 | player.tell("^6Player {} is already marked as a rapist :-)".format(target_player.name)) 546 | return minqlx.RET_STOP_ALL 547 | 548 | self.set_silent_handicap(target_player, 100) 549 | player.tell("^6Player {} has succesfully been marked as a raper!".format(target_player.name)) 550 | return minqlx.RET_STOP_ALL 551 | 552 | def cmd_unsert_raper(self, player, msg, channel): 553 | if len(msg) < 2: 554 | return minqlx.RET_USAGE 555 | 556 | target_player = self.find_by_name_or_id(player, msg[1]) 557 | if not target_player: 558 | return minqlx.RET_STOP_ALL 559 | 560 | if target_player.steam_id in self.handicaps: 561 | self.set_silent_handicap(target_player, 100) 562 | del self.handicaps[target_player.steam_id] 563 | player.tell("^6Player {} has been unmarked as a rapist :-)".format(target_player.name)) 564 | return minqlx.RET_STOP_ALL 565 | 566 | player.tell("^6Player {} was not even a raper!".format(target_player.name)) 567 | return minqlx.RET_STOP_ALL 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | # ==================================================================== 577 | # HELPERS 578 | # ==================================================================== 579 | def delay(self, messages, interval = 1): 580 | channel = lambda m: minqlx.CHAT_CHANNEL.reply("^7{}".format(m)) 581 | threading.Thread(target=self.thread_list, args=(messages, channel, interval)).start() 582 | 583 | def delaytell(self, messages, player, interval = 1): 584 | channel = lambda m: player.tell("^6{}".format(m)) 585 | threading.Thread(target=self.thread_list, args=(messages, channel, interval)).start() 586 | 587 | def thread_list(self, items, channel, interval): 588 | for m in items: 589 | if m: channel(m) # allow "" to be used as a skip 590 | time.sleep(interval) 591 | 592 | def find_players(self, query): 593 | players = [] 594 | for p in self.find_player(query): 595 | if p not in players: 596 | players.append(p) 597 | return players 598 | 599 | def clear_all_handicaps(self): 600 | for _p in self.players(): 601 | self.set_silent_handicap(_p, 100) 602 | del self.handicaps[_p.steam_id] 603 | 604 | 605 | def set_handicap(self, player, hc): 606 | self.handicaps[player.steam_id] = HC_TAG+str(hc) 607 | player.handicap = hc 608 | 609 | def set_silent_handicap(self, player, hc): 610 | self.handicaps[player.steam_id] = SHC_TAG+str(hc) 611 | player.handicap = hc 612 | 613 | def help_get_avg_score(self): 614 | teams = self.teams() 615 | # Get scores/second of all players 616 | avg_scores = [] 617 | for p in teams['red'] + teams['blue']: 618 | if self.rounds_played.get(p.steam_id, 0): 619 | score = self.realscores.get(p.steam_id, p.stats.score) 620 | avg_scores.append(score / p.stats.time) 621 | 622 | # Now calculate the averages 623 | if len(avg_scores): return sum(avg_scores) / len(avg_scores) 624 | return -1 625 | 626 | # Calculate handicap suggestion (as discussed on station.boards.net): 627 | def help_get_hc_suggestion(self, rape_score): 628 | hc = int( 108 - rape_score / 2.1 ) 629 | 630 | diff = abs ( self.game.red_score - self.game.blue_score ) 631 | 632 | if USE_HANDICAP_ADJUSTMENTS: 633 | hc *= HANDICAP_ADJUSTMENTS.get(diff, DEFAULT_HANDICAP_ADJUSTMENT) 634 | 635 | hc = min(int(hc), 100) 636 | hc = max(hc, HC_LOWEST) 637 | return hc 638 | 639 | 640 | # Calculate rape percentage 641 | def help_calc_rape_gap(self, player): 642 | score = self.realscores.get(player.steam_id, player.stats.score) 643 | time = player.stats.time 644 | sps = score / time 645 | 646 | avg_score = self.help_get_avg_score() 647 | if avg_score > 0: return int ( sps * 100 / avg_score - 100 ) 648 | return "invalid" 649 | 650 | def help_remove_handicaps(self): 651 | # Reset al handicaps and suggestions 652 | for _p in self.players(): 653 | self.set_silent_handicap(_p, 100) 654 | del self.handicaps[_p.steam_id] 655 | 656 | -------------------------------------------------------------------------------- /autospec.py: -------------------------------------------------------------------------------- 1 | # This is a plugin created by iouonegirl(@gmail.com) 2 | # Copyright (c) 2016 iouonegirl 3 | # https://github.com/dsverdlo/minqlx-plugins 4 | # 5 | # You are free to modify this plugin to your custom, 6 | # except for the version command related code. 7 | # 8 | # Thanks to Minkyn for his assistance in this plugin. 9 | # 10 | # Its purpose if to force the last player to spectate 11 | # Algorithm: http://i.imgur.com/8P60gRq.png 12 | # 13 | # Uses: 14 | # set qlx_autospec_minplayers "2" 15 | # set qlx_autospec_maxplayers "999" 16 | # ^ The autospec algorithm will only work for #players within this interval 17 | 18 | import minqlx 19 | import requests 20 | import itertools 21 | import threading 22 | import random 23 | import time 24 | import os 25 | import re 26 | 27 | VERSION = "v0.19" 28 | 29 | # This code makes sure the required superclass is loaded automatically 30 | try: 31 | from .iouonegirl import iouonegirlPlugin 32 | except: 33 | try: 34 | abs_file_path = os.path.join(os.path.dirname(__file__), "iouonegirl.py") 35 | res = requests.get("https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/iouonegirl.py") 36 | if res.status_code != requests.codes.ok: raise 37 | with open(abs_file_path,"a+") as f: f.write(res.text) 38 | from .iouonegirl import iouonegirlPlugin 39 | except Exception as e : 40 | minqlx.CHAT_CHANNEL.reply("^1iouonegirl abstract plugin download failed^7: {}".format(e)) 41 | raise 42 | 43 | 44 | class autospec(iouonegirlPlugin): 45 | def __init__(self): 46 | super().__init__(self.__class__.__name__, VERSION) 47 | 48 | self.jointimes = {} 49 | 50 | self.set_cvar_once("qlx_autospec_minplayers", "2") 51 | self.set_cvar_once("qlx_autospec_maxplayers", "999") 52 | 53 | self.add_hook("round_countdown", self.handle_round_count) 54 | self.add_hook("round_start", self.handle_round_start) 55 | self.add_hook("player_connect", self.handle_player_connect) 56 | self.add_hook("player_disconnect", self.handle_player_disconnect) 57 | 58 | 59 | def handle_player_connect(self, player): 60 | self.jointimes[player.steam_id] = time.time() 61 | 62 | 63 | def handle_player_disconnect(self, player, reason): 64 | if player.steam_id in self.jointimes: 65 | del self.jointimes[player.steam_id] 66 | 67 | 68 | def find_time(self, player): 69 | if not (player.steam_id in self.jointimes): 70 | self.jointimes[player.steam_id] = time.time() 71 | return self.jointimes[player.steam_id] 72 | 73 | 74 | # When a round starts counting down, we check some conditions 75 | # and show a comforting message about how we will balance the 76 | # teams before the round starts 77 | def handle_round_count(self, round_number): 78 | 79 | # Grab the teams and amount of players in each team 80 | teams = self.teams() 81 | player_count = len(teams["red"] + teams["blue"]) 82 | 83 | # If not enough players to balance... 84 | if player_count < self.get_cvar("qlx_autospec_minplayers", int): 85 | return 86 | 87 | # if so many players that we don't care 88 | if player_count > self.get_cvar("qlx_autospec_maxplayers", int): 89 | return 90 | 91 | diff = len(teams['red']) - len(teams['blue']) 92 | to, fr = ['blue', 'red'] if diff > 0 else ['red','blue'] 93 | last = self.help_get_last() 94 | n = int(abs(diff) / 2) # amount of players that will be switched 95 | 96 | # If there is a difference in teams of more or equal than 1, 97 | # Display what is going to happen 98 | if abs(diff) >= 1: 99 | if self.is_even(diff): 100 | n = last.name if n == 1 else "{} players".format(n) 101 | self.msg("^6Uneven teams detected!^7 Server will move {} to {}".format(n, to)) 102 | else: 103 | m = 'lowest player' if n == 1 else '{} lowest players'.format(n) 104 | m = " and move the {} to {}".format(m, to) if n else '' 105 | self.msg("^6Uneven teams detected!^7 Server will auto spec {}{}.".format(last.name, m)) 106 | 107 | # Start counting (in a thread) to just before a round and then balance 108 | # So that switched players can still participate in the coming round 109 | self.balance_before_start(round_number) 110 | 111 | 112 | # To be sure no one joined in the last millisecond, or in the case that 113 | # there was no round delay, check again on round start 114 | def handle_round_start(self, round_number): 115 | self.balance_before_start(round_number, True) 116 | 117 | 118 | # Wait until just before the round starts and then balance 119 | @minqlx.thread 120 | def balance_before_start(self, roundnumber, direct = False): 121 | 122 | # Wait until round almost starts 123 | countdown = int(self.get_cvar('g_roundWarmupDelay')) 124 | if self.game.type_short == "ft": 125 | countdown = int(self.get_cvar('g_freezeRoundDelay')) 126 | if not direct: time.sleep(max(countdown / 1000 - 0.3, 0)) 127 | 128 | # Do the thing (game logic) in next frame 129 | self.balance_before_start_next_frame() 130 | 131 | 132 | # Move players around, make the teams even 133 | @minqlx.next_frame 134 | def balance_before_start_next_frame(self): 135 | 136 | def red_min_blue(): 137 | t = self.teams() 138 | return len(t['red']) - len(t['blue']) 139 | 140 | # Grab the teams 141 | teams = self.teams() 142 | player_count = len(teams["red"] + teams["blue"]) 143 | 144 | # If it is the last player, don't do this and let the game finish normally 145 | if player_count == 1: 146 | return 147 | 148 | # If there are less people than wanted, ignore 149 | if player_count < self.get_cvar("qlx_autospec_minplayers", int): 150 | return 151 | 152 | # If there are so many players that we don't care: 153 | if player_count > self.get_cvar("qlx_autospec_maxplayers", int): 154 | return 155 | 156 | # While there is a difference in teams of more or equal than 1 157 | while abs(red_min_blue()) >= 1: 158 | last = self.help_get_last() 159 | diff = red_min_blue() 160 | 161 | if self.is_even(diff): # one team has an even amount of people more than the other 162 | 163 | to, fr = ['blue','red'] if diff > 0 else ['red', 'blue'] 164 | last.put(to) 165 | self.msg("^6Uneven teams action^7: Moved {} from {} to {}".format(last.name, fr, to)) 166 | 167 | else: 168 | 169 | last.put("spectator") 170 | self.msg("^6Uneven teams action^7: {} was moved to spec to even teams!".format(last.name)) 171 | 172 | 173 | 174 | # Returns the last player of the team with the largest amount of players 175 | # Sorted by score. If lowest score is equal, look at jointimes. 176 | def help_get_last(self): 177 | 178 | teams = self.teams() 179 | 180 | # See which team is bigger than the other 181 | if len(teams["red"]) < len(teams["blue"]): 182 | bigger_team = teams["blue"].copy() 183 | else: 184 | bigger_team = teams["red"].copy() 185 | 186 | if (self.game.red_score + self.game.blue_score) >= 1: 187 | 188 | minqlx.console_command("echo Autospec: Picking someone to spec based on score") 189 | # Get the last person in that team 190 | lowest_players = [bigger_team[0]] 191 | 192 | for p in bigger_team: 193 | if p.stats.score < lowest_players[0].stats.score: 194 | lowest_players = [p] 195 | elif p.stats.score == lowest_players[0].stats.score: 196 | lowest_players.append(p) 197 | 198 | # Sort on joining times highest(newest) to lowest(oldest) 199 | lowest_players.sort(key= lambda el: self.find_time(el), reverse=True ) 200 | lowest_player = lowest_players[0] 201 | 202 | else: 203 | 204 | minqlx.console_command("echo Autospec: Picking someone to spec based on join times.") 205 | bigger_team.sort(key = lambda el: self.find_time(el), reverse=True) 206 | lowest_player = bigger_team[0] 207 | 208 | minqlx.console_command("echo Autospec: Picked {} from the {} team.".format(lowest_player.name, lowest_player.team)) 209 | return lowest_player 210 | 211 | 212 | -------------------------------------------------------------------------------- /centerprint.py: -------------------------------------------------------------------------------- 1 | # This is a plugin created by iouonegirl(@gmail.com) 2 | # Copyright (c) 2016 iouonegirl 3 | # https://github.com/dsverdlo/minqlx-plugins 4 | # 5 | # You are free to modify this plugin to your custom, 6 | # except for the version command related code. 7 | # 8 | # It provides a method to !print something in the center of a player's screen, 9 | # or !broadcast it over the whole server (print on everybody's screen) 10 | # and a toggle command (!showlast) to view when there is only one enemy left 11 | # 12 | # Uses 13 | # - qlx_cp_message "One enemy left. Start the hunt" 14 | 15 | 16 | import minqlx 17 | import datetime 18 | import time 19 | import re 20 | import requests 21 | 22 | VERSION = "v0.7" 23 | 24 | PLAYER_KEY = "minqlx:players:{}" 25 | NOTIFY_LAST_KEY = PLAYER_KEY + ":notifylast" 26 | 27 | class centerprint(minqlx.Plugin): 28 | def __init__(self): 29 | super().__init__() 30 | 31 | # Set required cvars once. EDIT THIS IN THE SERVER.CFG 32 | self.set_cvar_once("qlx_cp_message", "One enemy left. Start the hunt") 33 | 34 | self.add_command(("print", "pprint", "cprint", "centerprint"), self.cmd_center_print, 3, usage="| ") 35 | self.add_command("broadcast", self.cmd_broadcast, 3) 36 | self.add_command("showlast", self.cmd_toggle_pref) 37 | self.add_command("v_centerprint", self.cmd_version) 38 | self.add_hook("death", self.handle_death) 39 | self.add_hook("player_connect", self.handle_player_connect) 40 | 41 | def handle_player_connect(self, player): 42 | if self.db.has_permission(player, 5): 43 | self.check_version(player=player) 44 | 45 | def cmd_version(self, player, msg, channel): 46 | self.check_version(channel=channel) 47 | 48 | @minqlx.thread 49 | def check_version(self, player=None, channel=None): 50 | url = "https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/{}.py".format(self.__class__.__name__) 51 | res = requests.get(url) 52 | last_status = res.status_code 53 | if res.status_code != requests.codes.ok: return 54 | for line in res.iter_lines(): 55 | if line.startswith(b'VERSION'): 56 | line = line.replace(b'VERSION = ', b'') 57 | line = line.replace(b'"', b'') 58 | # If called manually and outdated 59 | if channel and VERSION.encode() != line: 60 | channel.reply("^7Currently using ^3iou^7one^4girl^7's ^6{}^7 plugin ^1outdated^7 version ^6{}^7.".format(self.__class__.__name__, VERSION)) 61 | # If called manually and alright 62 | elif channel and VERSION.encode() == line: 63 | channel.reply("^7Currently using ^3iou^7one^4girl^7's latest ^6{}^7 plugin version ^6{}^7.".format(self.__class__.__name__, VERSION)) 64 | # If routine check and it's not alright. 65 | elif player and VERSION.encode() != line: 66 | time.sleep(15) 67 | try: 68 | player.tell("^3Plugin update alert^7:^6 {}^7's latest version is ^6{}^7 and you're using ^6{}^7!".format(self.__class__.__name__, line.decode(), VERSION)) 69 | except Exception as e: minqlx.console_command("echo {}".format(e)) 70 | return 71 | def cmd_broadcast(self, player, msg, channel): 72 | for p in self.players(): 73 | message = " ".join(msg[1:]) 74 | minqlx.send_server_command(p.id, "cp \"\n\n\n{}\"".format(message)) 75 | player.tell("^6Psst^7: Broadcast successful: '{}'".format(message)) 76 | 77 | def cmd_center_print(self, player, msg, channel): 78 | if len(msg) < 3: 79 | return minqlx.RET_USAGE 80 | 81 | target = self.find_by_name_or_id(player, msg[1]) 82 | if not target: 83 | return minqlx.RET_STOP_ALL 84 | 85 | message = " ".join(msg[2:]) 86 | minqlx.send_server_command(target.id, "cp \"\n\n\n{}\"".format(message)) 87 | player.tell("^6Psst^7: succesfully printed '{}' on {}'s screen.".format(message, target.name)) 88 | return minqlx.RET_STOP_ALL 89 | 90 | def handle_death(self, victim, killer, data): 91 | _vic = self.find_player(victim.name)[0] 92 | _vic_team = _vic.team 93 | 94 | if data['MOD'] == 'SWITCHTEAM': 95 | _vic_team == "red" if data['VICTIM']['TEAM'] == 2 else "blue" 96 | 97 | if self.game and self.game.state == 'in_progress': 98 | teams = self.teams() 99 | 100 | if int(data['TEAM_ALIVE']) == 1: # viewpoint of victim 101 | for _p in teams["red" if _vic_team == "blue" else "blue"]: 102 | if self.get_notif_pref(_p.steam_id): 103 | minqlx.send_server_command(_p.id, "cp \"\n\n\n{}!\"".format(self.get_cvar("qlx_cp_message"))) 104 | 105 | 106 | 107 | def cmd_toggle_pref(self, player, msg, channel): 108 | if len(msg) > 2: 109 | return minqlx.RET_USAGE 110 | 111 | self.set_notif_pref(player.steam_id) 112 | 113 | if self.get_notif_pref(player.steam_id): 114 | channel.reply("^7{} will now see a message if there is only 1 enemy left.".format(player.name)) 115 | else: 116 | channel.reply("^7{} will stop seeing '1 enemy left' messages.".format(player.name)) 117 | 118 | 119 | # ==================================================================== 120 | # HELPERS 121 | # ==================================================================== 122 | 123 | def get_notif_pref(self, sid): 124 | try: 125 | return int(self.db[NOTIFY_LAST_KEY.format(sid)]) 126 | except: 127 | return False 128 | 129 | def set_notif_pref(self, sid): 130 | self.db[NOTIFY_LAST_KEY.format(sid)] = 0 if self.get_notif_pref(sid) else 1 131 | 132 | 133 | 134 | def find_by_name_or_id(self, player, target): 135 | # Find players returns a list of name-matching players 136 | def find_players(query): 137 | players = [] 138 | for p in self.find_player(query): 139 | if p not in players: 140 | players.append(p) 141 | return players 142 | 143 | # Tell a player which players matched 144 | def list_alternatives(players, indent=2): 145 | player.tell("A total of ^6{}^7 players matched for {}:".format(len(players),target)) 146 | out = "" 147 | for p in players: 148 | out += " " * indent 149 | out += "{}^6:^7 {}\n".format(p.id, p.name) 150 | player.tell(out[:-1]) 151 | 152 | # Get the list of matching players on name 153 | target_players = find_players(target) 154 | 155 | # even if we get only 1 person, we need to check if the input was meant as an ID 156 | # if we also get an ID we should return with ambiguity 157 | 158 | try: 159 | i = int(target) 160 | target_player = self.player(i) 161 | if not (0 <= i < 64) or not target_player: 162 | raise ValueError 163 | # Add the found ID if the player was not already found 164 | if not target_player in target_players: 165 | target_players.append(target_player) 166 | except ValueError: 167 | pass 168 | 169 | # If there were absolutely no matches 170 | if not target_players: 171 | player.tell("Sorry, but no players matched your tokens: {}.".format(target)) 172 | return None 173 | 174 | # If there were more than 1 matches 175 | if len(target_players) > 1: 176 | list_alternatives(target_players) 177 | return None 178 | 179 | # By now there can only be one person left 180 | return target_players.pop() 181 | -------------------------------------------------------------------------------- /disable_votes.py: -------------------------------------------------------------------------------- 1 | # This is a plugin created by iouonegirl(@gmail.com) 2 | # Copyright (c) 2016 iouonegirl 3 | # https://github.com/dsverdlo/minqlx-plugins 4 | # 5 | # You are free to modify this plugin to your custom, 6 | # except for the version command related code. 7 | # 8 | # This plugin disables certain votes during a game 9 | # 10 | # Uses 11 | # set qlx_disabled_votes_midgame "map" 12 | # set qlx_disabled_specvotes_midgame "teamsize, kick, clientkick" 13 | # set qlx_disabled_votes_permission "1" 14 | 15 | import minqlx 16 | import time 17 | import os 18 | import requests 19 | 20 | VERSION = "v0.12" 21 | 22 | # This code makes sure the required superclass is loaded automatically 23 | try: 24 | from .iouonegirl import iouonegirlPlugin 25 | except: 26 | try: 27 | abs_file_path = os.path.join(os.path.dirname(__file__), "iouonegirl.py") 28 | res = requests.get("http://wilma.vub.ac.be/~dsverdlo/minqlx-plugins/iouonegirl.py") 29 | if res.status_code != requests.codes.ok: raise 30 | with open(abs_file_path,"a+") as f: f.write(res.text) 31 | from .iouonegirl import iouonegirlPlugin 32 | except Exception as e : 33 | minqlx.CHAT_CHANNEL.reply("^1iouonegirl abstract plugin download failed^7: {}".format(e)) 34 | raise 35 | 36 | class disable_votes(iouonegirlPlugin): 37 | 38 | def __init__(self): 39 | super().__init__(self.__class__.__name__, VERSION) 40 | 41 | self.set_cvar_once("qlx_disabled_votes_midgame", "map") 42 | self.set_cvar_once("qlx_disabled_specvotes_midgame", "teamsize, kick, clientkick") 43 | self.set_cvar_once("qlx_disabled_votes_permission", "1") 44 | 45 | self.add_hook("vote_called", self.handle_vote) 46 | self.add_command(("disabled","disabled_votes"), self.cmd_disabled) 47 | 48 | def cmd_disabled(self, player, msg, channel): 49 | disabled_for_all = [] 50 | for v in self.get_cvar("qlx_disabled_votes_midgame", set): 51 | disabled_for_all.append(v) 52 | 53 | disabled_for_spec = [] 54 | for v in self.get_cvar("qlx_disabled_specvotes_midgame", set): 55 | disabled_for_spec.append(v) 56 | 57 | if disabled_for_all: 58 | m = "^7Unless perm ^6{}^7+, in-game votes for ^2{}^7 are disabled." 59 | m = m.format(self.get_cvar("qlx_disabled_votes_permission"), "^7, ^2".join(disabled_for_all)) 60 | if disabled_for_spec: 61 | m += " Specs can also not vote for: ^2{}^7." 62 | m = m.format("^7, ^2".join(disabled_for_spec)) 63 | 64 | elif disabled_for_spec: 65 | m = "Unless they have perm ^6{}^7+, specs can't vote for: ^2{}^7." 66 | m = m.format(self.get_cvar("qlx_disabled_votes_permission"), "^7, ^2".join(disabled_for_spec)) 67 | else: 68 | m = "^7There are no callvotes that will be disabled during a match." 69 | 70 | channel.reply(m) 71 | 72 | 73 | def handle_vote(self, player, vote, args): 74 | 75 | if self.game.state != "in_progress": return 76 | 77 | if self.db.has_permission(player, self.get_cvar("qlx_disabled_votes_permission", int)): 78 | return # go ahead 79 | 80 | disabled_votes = [] 81 | 82 | # These will always apply 83 | for v in self.get_cvar("qlx_disabled_votes_midgame", set): 84 | disabled_votes.append(v) 85 | 86 | # If spectator: extra restrictions 87 | if player.team == "spectator": 88 | for v in self.get_cvar("qlx_disabled_specvotes_midgame", set): 89 | disabled_votes.append(v) 90 | 91 | if vote in disabled_votes: 92 | player.tell('^1You are not allowed to callvote {} during a match!'.format(vote)) 93 | return minqlx.RET_STOP_ALL 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /funlimit.py: -------------------------------------------------------------------------------- 1 | # This is a plugin created by iouonegirl(@gmail.com) 2 | # Copyright (c) 2016 iouonegirl 3 | # https://github.com/dsverdlo/minqlx-plugins 4 | # 5 | # You are free to modify this plugin to your custom, 6 | # except for the version command related code. 7 | # 8 | # This plugin disables (fun) sounds during a match. 9 | # 10 | # Thanks to Cabbe and ph0en|X of TNT for their input 11 | # in the making of this plugin 12 | # 13 | # Uses: 14 | # set qlx_funlimit_messages "1" 15 | # ^ (Set to "1" to see messages, "0" to disable) 16 | # 17 | # set qlx_funlimit_fun_pluginname "fun" 18 | # ^ (Name of the fun plugin that you use, like: fun/myFun) 19 | 20 | import minqlx 21 | import threading 22 | import time 23 | import os 24 | import requests 25 | 26 | VERSION = "v0.1.3" 27 | 28 | # This code makes sure the required superclass is loaded automatically 29 | try: 30 | from .iouonegirl import iouonegirlPlugin 31 | except: 32 | try: 33 | abs_file_path = os.path.join(os.path.dirname(__file__), "iouonegirl.py") 34 | res = requests.get("https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/iouonegirl.py") 35 | if res.status_code != requests.codes.ok: raise 36 | with open(abs_file_path,"a+") as f: f.write(res.text) 37 | from .iouonegirl import iouonegirlPlugin 38 | except Exception as e : 39 | minqlx.CHAT_CHANNEL.reply("^1iouonegirl abstract plugin download failed^7: {}".format(e)) 40 | raise 41 | 42 | 43 | class funlimit(iouonegirlPlugin): 44 | def __init__(self): 45 | super().__init__(self.__class__.__name__, VERSION) 46 | 47 | # CVARS 48 | self.set_cvar_once("qlx_funlimit_messages", "1") 49 | self.set_cvar_once("qlx_funlimit_fun_pluginname", "fun") # some servers use myFun 50 | 51 | # HOOKS 52 | self.add_hook("game_countdown", self.handle_game_start) 53 | self.add_hook("game_end", self.handle_game_end) 54 | self.add_hook("round_end", self.handle_round_end) 55 | self.add_hook("round_start", self.handle_round_start) 56 | self.add_hook("map", self.handle_map) 57 | self.add_hook("new_game", self.handle_new_game) 58 | self.add_hook("unload", self.handle_unload) 59 | 60 | # COMMANDS 61 | self.add_command("funsounds", self.cmd_funsounds) 62 | 63 | # Instance variables 64 | self.store_hook = None 65 | self.funPluginName = self.get_cvar("qlx_funlimit_fun_pluginname") 66 | 67 | if self.game and self.game.state == "in_progress": 68 | self.disable_sounds() 69 | else: 70 | self.allow_sounds() 71 | 72 | def handle_round_end(self, data): 73 | self.allow_sounds() 74 | 75 | def handle_round_start(self, roundnumber): 76 | self.disable_sounds() 77 | 78 | def handle_game_start(self, data=None): # works on countdown now 79 | # Don't disable yet for round-based gametypes (rounds will do it) 80 | if self.game and self.game.type_short in ['ca', 'ft', 'ad']: return 81 | self.disable_sounds() 82 | 83 | def handle_game_end(self, data): 84 | # Don't enable after round-based gametypes 85 | if self.game and self.game.type_short in ['ca', 'ft', 'ad']: return 86 | self.allow_sounds() 87 | 88 | def handle_map(self, mapname, factory): 89 | self.allow_sounds() 90 | 91 | def handle_new_game(self): 92 | if self.game.state != "countdown": 93 | self.allow_sounds() 94 | 95 | def handle_unload(self, plugin): 96 | if plugin == self.__class__.__name__: 97 | self.allow_sounds() 98 | 99 | def cmd_funsounds(self, player, msg, channel): 100 | enabled = "^1not loaded" 101 | if self.funPluginName in self.plugins: 102 | enabled = "^1disabled" 103 | fun = self.plugins[self.funPluginName] 104 | for hook in fun.hooks: 105 | if hook[0] == "chat": 106 | enabled = "^2enabled" 107 | 108 | channel.reply("{} sounds are currently {}.".format(self.funPluginName, enabled)) 109 | 110 | 111 | def disable_sounds(self): 112 | if not self.funPluginName in self.plugins: return 113 | fun = self.plugins[self.funPluginName] 114 | for hook in fun.hooks: 115 | if hook[0] == "chat": 116 | if not self.store_hook: 117 | self.store_hook = hook 118 | fun.remove_hook(hook[0], hook[1], hook[2]) 119 | self.delay_msg("^7{} sounds temporarily ^1disabled^7.".format(self.funPluginName)) 120 | return 121 | 122 | def allow_sounds(self): 123 | if not self.store_hook: return 124 | if not self.funPluginName in self.plugins: return 125 | fun = self.plugins[self.funPluginName] 126 | for hook in fun.hooks: 127 | if hook[0] == "chat": return 128 | fun.add_hook(self.store_hook[0], self.store_hook[1], self.store_hook[2]) 129 | self.delay_msg("^7{} sounds ^2enabled^7.".format(self.funPluginName)) 130 | 131 | @minqlx.delay(0.5) 132 | def delay_msg(self, m): 133 | if self.get_cvar("qlx_funlimit_messages", int): 134 | self.msg(m) 135 | 136 | -------------------------------------------------------------------------------- /gauntonly.py: -------------------------------------------------------------------------------- 1 | # This is a plugin written by iouonegirl(@gmail.com) and Gelenkbusfahrer 2 | # Copyright (c) 2016 iouonegirl, Gelenkbusfahrer 3 | # https://github.com/dsverdlo/minqlx-plugins 4 | # 5 | # You are free to modify this plugin to your custom, 6 | # except for the version command related code. 7 | # 8 | # The idea for this plugin comes from Gelenkbusfahrer 9 | # and has been discussed on station.boards.net. 10 | # Home forum of the Bus Station server. 11 | # 12 | # This plugin detects when a last standing player is 13 | # facing more than [MAX] opponents and will disable all 14 | # weapons until there are only [MIN] players left standing. 15 | # 16 | # Round based modes only 17 | 18 | # Uses 19 | # - set qlx_gaunt_max "4" 20 | # - set qlx_gaunt_min "2" Invulnerability 21 | 22 | import minqlx 23 | import datetime 24 | import time 25 | import os 26 | import re 27 | import requests 28 | import random 29 | 30 | VERSION = "v0.3" 31 | 32 | # This code makes sure the required superclass is loaded automatically 33 | try: 34 | from .iouonegirl import iouonegirlPlugin 35 | except: 36 | try: 37 | abs_file_path = os.path.join(os.path.dirname(__file__), "iouonegirl.py") 38 | res = requests.get("https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/iouonegirl.py") 39 | if res.status_code != requests.codes.ok: raise 40 | with open(abs_file_path,"a+") as f: f.write(res.text) 41 | from .iouonegirl import iouonegirlPlugin 42 | except Exception as e : 43 | minqlx.CHAT_CHANNEL.reply("^1iouonegirl abstract plugin download failed^7: {}".format(e)) 44 | raise 45 | 46 | class gauntonly(iouonegirlPlugin): 47 | def __init__(self): 48 | super().__init__(self.__class__.__name__, VERSION) 49 | 50 | self.set_cvar_once("qlx_gaunt_min", "2") 51 | self.set_cvar_once("qlx_gaunt_max", "4") 52 | 53 | self.add_hook("death", self.handle_death) 54 | self.add_hook("team_switch", self.handle_switch) 55 | self.add_hook("round_end", self.handle_round_end) 56 | 57 | self.min_opp = self.get_cvar("qlx_gaunt_min", int) 58 | self.max_opp = self.get_cvar("qlx_gaunt_max", int) 59 | 60 | self.gauntmode = False 61 | self.weapons_taken = None 62 | self.checkonce = True 63 | self.blinking = False 64 | 65 | def handle_switch(self, player, _old, _new): 66 | # Somebody noob quits? Check if we need to switch modes 67 | if self.checkonce: self.detect() 68 | 69 | def handle_death(self, victim, killer, data): 70 | # Some noob dies? Check if we need to switch modes 71 | if self.checkonce: self.detect() 72 | 73 | def handle_round_end(self, data): 74 | # Round finally ended? Turn the mode off 75 | self.gauntmode = False 76 | self.checkonce = True 77 | 78 | 79 | def detect(self): 80 | # Not in a match? Do nothing 81 | if not self.game: return 82 | if self.game.roundlimit in [self.game.blue_score, self.game.red_score]: return 83 | 84 | # Grab some info and put that stuff into variables for readability yo 85 | teams = self.teams() 86 | alive_r = list(filter(lambda p: p.is_alive, teams['red'])) 87 | alive_b = list(filter(lambda p: p.is_alive, teams['blue'])) 88 | 89 | #if self.gauntmode: 90 | # if not (alive_r and alive_b): 91 | # self.stop_sound() 92 | 93 | # If we do not have a last standing, do nothing 94 | if not 1 in [len(alive_r), len(alive_b)]: return 95 | 96 | # If one team is completely dead, do nothing 97 | if not (alive_r and alive_b): 98 | return 99 | 100 | # Ok things are getting more complicated, listen closely! 101 | 102 | # If the gauntmode was active, and both teams have less than [MIN] players: TURN IT OFF 103 | if len(alive_r) <= self.min_opp and len(alive_b) <= self.min_opp and self.gauntmode: 104 | 105 | # Deactivate gaunt mode: take everyone's gauntlets, display warning and then restore weapons 106 | self.gauntmode = False 107 | for p in self.players(): # Take their gauntlet away and switch to empty 108 | p.weapons(g=False, mg=False, sg=False, gl=False, rl=False, lg=False, rg=False, pg=False, bfg=False, gh=False, ng=False, pl=False, cg=False, hmg=False, hands=False) 109 | p.weapon(15) 110 | # Countdown will display messages and then restore the weapons 111 | r = "^3Restoring weapons in " 112 | self.countdown([r+"5", r+"4", r+"3", r+"2", r+"1", "^2FIGHT!"]) 113 | return 114 | 115 | # If we were not in gaunt mode yet, check if we have enough opponents. 116 | # Remember at least one team has 1 player; add the teams and subtract one 117 | if (not self.gauntmode) and self.game.state == "in_progress" and len(alive_b + alive_r[1:]) > self.max_opp: 118 | if self.checkonce: 119 | 120 | # Compare healths+armors and decide if last standing had a chance winning. If there's a good chance, this doesn't gonna be a pummel round. 121 | if len(alive_r) > len(alive_b): 122 | calc_opp = alive_r 123 | ha_last = alive_b[0].health + alive_b[0].armor 124 | else: 125 | calc_opp = alive_b 126 | ha_last = alive_r[0].health + alive_r[0].armor 127 | ha_opp_total = 0 128 | for p in calc_opp: 129 | ha_opp_total += p.health + p.armor 130 | ha_required = 1000*(ha_last/300)/1.43+300 131 | 132 | p_amount = len(alive_b + alive_r[1:]) 133 | if p_amount == 5: chance = 10 134 | elif p_amount == 6: chance = 20 135 | elif p_amount == 7: chance = 40 136 | else: chance = 100 137 | if random.randint(1,100) > chance or ha_opp_total < ha_required: 138 | self.checkonce = False 139 | return 140 | 141 | self.gauntmode = True 142 | self.weapons_taken = self.weapons_taken or alive_r[0].weapons() 143 | for p in self.players(): 144 | p.weapons(g=False, mg=False, sg=False, gl=False, rl=False, lg=False, rg=False, pg=False, bfg=False, gh=False, ng=False, pl=False, cg=False, hmg=False, hands=False) 145 | p.weapon(15) 146 | self.blink(["Prepare your pummel!", ""] * 9 + ["^2{}vs{} - Go pummeling!".format(len(alive_r), len(alive_b))]) 147 | self.blinking = True 148 | self.msg("^7Pummel showdown! Weapons will be restored when ^3{}^7 players are left standing.".format(self.min_opp+1)) 149 | return 150 | 151 | # If we didn't have to turn the gauntmode ON, and not OFF, 152 | # it will be the case someone died or changed teams 153 | if self.gauntmode and not self.blinking: 154 | self.msg("^7Pummel showdown! Gaunt ^3{}^7 more enemies to restore weapons".format(len(alive_b+alive_r)-1-self.min_opp)) 155 | 156 | # Let's make some noise 157 | d = random.randint(1, 3) 158 | self.play_sound("sound/vo/humiliation{}".format(d)) 159 | 160 | if len(alive_r) > len(alive_b): 161 | self.blink2((["{} Reds left!".format(len(alive_r))] + [""]) * 6) 162 | else: 163 | self.blink2((["{} Blues left!".format(len(alive_b))] + [""]) * 6) 164 | 165 | 166 | @minqlx.thread 167 | def blink(self, messages, interval = .12): 168 | @minqlx.next_frame 169 | def logic(_m): self.center_print("^3{}".format(_m)) 170 | @minqlx.next_frame 171 | def setgaunt(_p): 172 | _p.weapons(g=True, mg=False, sg=False, gl=False, rl=False, lg=False, rg=False, pg=False, bfg=False, gh=False, ng=False, pl=False, cg=False, hmg=False, hands=True) 173 | _p.weapon(1) 174 | _p.powerups(haste=30) 175 | # First centerprint all the messages 176 | for m in messages: 177 | logic(m) 178 | time.sleep(interval) 179 | # Then change the weapons to gauntlet 180 | for p in self.players(): 181 | setgaunt(p) 182 | self.play_sound("sound/vo_evil/go") 183 | self.blinking = False 184 | 185 | @minqlx.thread 186 | def blink2(self, messages, interval = .12): 187 | @minqlx.next_frame 188 | def logic(_m): self.center_print("^3{}".format(_m)) 189 | for m in messages: 190 | logic(m) 191 | time.sleep(interval) 192 | 193 | @minqlx.thread 194 | def countdown(self, messages, interval = 1): 195 | @minqlx.next_frame 196 | def logic(_m): self.center_print("^3{}".format(_m)) 197 | @minqlx.next_frame 198 | def setweap(_p): minqlx.set_weapons(_p.id, self.weapons_taken or _p.weapons()) 199 | # First centerprint to warn players 200 | for m in messages: 201 | time.sleep(interval) 202 | logic(m) 203 | # Then restore their weapons 204 | for p in self.players(): 205 | setweap(p) 206 | self.play_sound("sound/vo_evil/fight") 207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /intermission.py: -------------------------------------------------------------------------------- 1 | # This is a plugin created by iouonegirl(@gmail.com) 2 | # Copyright (c) 2016,2017 iouonegirl 3 | # https://github.com/dsverdlo/minqlx-plugins 4 | # 5 | # You are free to modify this plugin to your custom, 6 | # except for the version command related code. 7 | # 8 | # Original idea from , but edited to support 9 | # a list of sounds which will be played one by one after each match. 10 | # 11 | # Place the sounds files in a PK3 file and upload it to a workshop. 12 | # 13 | # If you have trouble hearing the music, I heard Mino's 14 | # workshop.py plugin helps. 15 | 16 | 17 | import minqlx 18 | import time 19 | import requests 20 | 21 | VERSION = "v0.30" 22 | 23 | # These songs will be looped one by one. Don't forget to remove the #'s if you want to use songs 24 | # or set the cvar qlx_intermission_songs to a comma-delimited list of songs in server.cfg, like: 25 | # set qlx_intermission_songs "sound/songname/songtitle1.ogg,sound/songname/songtitle2.ogg,sound/songname/songtitle3.ogg" 26 | 27 | SONGS = [ 28 | #"sound/songname/songtitle1.ogg", 29 | #"sound/songname/songtitle2.ogg", 30 | #"sound/songname/songtitle3.ogg", 31 | ] 32 | 33 | # When qlx_intermission_songs has been set, then the value of SONGS is ignored. 34 | 35 | 36 | class intermission(minqlx.Plugin): 37 | def __init__(self): 38 | self.index = 0 39 | 40 | self.set_cvar_once( "qlx_intermission_songs", ",".join(SONGS) ) 41 | self.songs = self.get_cvar( "qlx_intermission_songs", list ) 42 | 43 | if not self.songs: 44 | self.msg("^1Error: No song list for intermission.^7") 45 | return 46 | 47 | self.add_hook("game_end", self.handle_game_end) 48 | self.add_hook("player_connect", self.handle_player_connect) 49 | self.add_command("v_intermission", self.cmd_version) 50 | self.add_command("intermission", self.cmd_intermission, 3 ) 51 | 52 | def cmd_intermission( self, player, msg, channel): 53 | for p in self.players(): 54 | if self.db.get_flag(p, "essentials:sounds_enabled", default=True): 55 | self.stop_sound(p) 56 | self.handle_game_end( { "ABORTED":0 } ) 57 | 58 | x = 0 59 | for x, song in enumerate(self.songs): 60 | channel.reply( "{}{}".format( song, " (now playing)" if x == self.index else "" ) ) 61 | x += 1 62 | 63 | 64 | @minqlx.delay(0.3) 65 | def handle_game_end(self, data ): 66 | # If there are no songs defined, return 67 | if not self.songs or data["ABORTED"]: 68 | return 69 | 70 | # If last time the index was incremented too high, loop around 71 | if self.index >= len(self.songs): 72 | self.index = 0 73 | 74 | # Try to play sound file 75 | try: 76 | for p in self.players(): 77 | if self.db.get_flag(p, "essentials:sounds_enabled", default=True): 78 | self.stop_music(p) 79 | self.play_sound(self.songs[self.index],p) 80 | except Exception as e: 81 | self.msg("^1Error: ^7{}".format(e)) 82 | 83 | # Increase the counter so next round the next sound will be played 84 | self.index += 1 85 | 86 | def handle_player_connect(self, player): 87 | if self.db.has_permission(player, 5): 88 | self.check_version(player=player) 89 | 90 | def cmd_version(self, player, msg, channel): 91 | self.check_version(channel=channel) 92 | 93 | @minqlx.thread 94 | def check_version(self, player=None, channel=None): 95 | url = "https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/{}.py".format(self.__class__.__name__) 96 | res = requests.get(url) 97 | last_status = res.status_code 98 | if res.status_code != requests.codes.ok: return 99 | for line in res.iter_lines(): 100 | if line.startswith(b'VERSION'): 101 | line = line.replace(b'VERSION = ', b'') 102 | line = line.replace(b'"', b'') 103 | # If called manually and outdated 104 | if channel and VERSION.encode() != line: 105 | channel.reply("^7Currently using ^3iou^7one^4girl^7's ^6{}^7 plugin ^1outdated^7 version ^6{}^7.".format(self.__class__.__name__, VERSION)) 106 | # If called manually and alright 107 | elif channel and VERSION.encode() == line: 108 | channel.reply("^7Currently using ^3iou^7one^4girl^7's latest ^6{}^7 plugin version ^6{}^7.".format(self.__class__.__name__, VERSION)) 109 | # If routine check and it's not alright. 110 | elif player and VERSION.encode() != line: 111 | time.sleep(15) 112 | try: 113 | player.tell("^3Plugin update alert^7:^6 {}^7's latest version is ^6{}^7 and you're using ^6{}^7!".format(self.__class__.__name__, line.decode(), VERSION)) 114 | except Exception as e: minqlx.console_command("echo {}".format(e)) 115 | return 116 | -------------------------------------------------------------------------------- /iouonegirl.py: -------------------------------------------------------------------------------- 1 | # This is a plugin created by iouonegirl(@gmail.com) 2 | # Copyright (c) 2016 iouonegirl 3 | # https://github.com/dsverdlo/minqlx-plugins 4 | # 5 | # Thanks to Minkyn for his idea for an abstract plugin. 6 | # 7 | # DO NOT MANUALLY LOAD THIS ABSTRACT "PLUGIN" 8 | # OR CHANGE ANY CODE IN IT. TRUST ME. 9 | # 10 | # Uses: 11 | # set qlx_autoupdate_iouplugins "0" 12 | # ^ Set to "1" to enable automatic updates for all iou-plugins! 13 | 14 | 15 | import minqlx 16 | import threading 17 | import time 18 | import random 19 | import os 20 | import urllib 21 | import requests 22 | import re 23 | 24 | VERSION = "v0.34 IMPORTANT" 25 | 26 | class iouonegirlPlugin(minqlx.Plugin): 27 | def __init__(self, name, vers): 28 | super().__init__() 29 | 30 | self._name = name 31 | self._vers = vers 32 | self._loc = "https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/" 33 | self._flag = self.iouonegirlplugin_getflag() # flagged for restart 34 | 35 | self.cr_latest = "latest" 36 | self.cr_outdated = "outdated" 37 | self.cr_custom = "custom" 38 | self.cr_advanced = "futuristic" 39 | 40 | # One time set cvar for automatic updates 41 | self.set_cvar_once("qlx_autoupdate_iouplugins", "0") 42 | 43 | # Add custom v_PLUGINNAME command 44 | self.add_command("v_"+name, self.iouonegirlplugin_cmd_version) 45 | 46 | # these will be added with every subclass, so use RET_STOP in them 47 | self.add_command(("v_iouonegirlplugin", "v_iouonegirlPlugin", "v_iouonegirl"), self.iouonegirlplugin_cmd_myversion) 48 | self.add_command("update", self.iouonegirlplugin_cmd_autoupdate, 5, usage="|all") 49 | self.add_command("install", self.iouonegirlplugin_cmd_install, 5, usage="") 50 | self.add_command("iouplugins", self.iouonegirlplugin_cmd_list) 51 | self.add_command("versions", self.iouonegirlplugin_cmd_versions) 52 | self.add_hook("player_connect", self.iouonegirlplugin_handle_player_connect) 53 | 54 | # Check version of implementing plugin 55 | def iouonegirlplugin_cmd_version(self, player, msg, channel): 56 | self.iouonegirlplugin_check_version(player, channel) 57 | 58 | # command for checking superclass plugin. One handler is enough 59 | def iouonegirlplugin_cmd_myversion(self, player, msg, channel): 60 | self.iouonegirlplugin_check_myversion(player=player, channel=channel) 61 | return minqlx.RET_STOP 62 | 63 | @minqlx.thread 64 | def iouonegirlplugin_check_version(self, player=None, channel=None): 65 | @minqlx.next_frame 66 | def reply(m): channel.reply(m) 67 | @minqlx.next_frame 68 | def tell(m): player.tell(m) 69 | 70 | url = "{}{}.py".format(self._loc, self._name) 71 | res = requests.get(url) 72 | last_status = res.status_code 73 | if res.status_code != requests.codes.ok: 74 | m = "^7Currently using ^3iou^7one^4girl^7's ^6{}^7 plugin version ^6{}^7." 75 | if channel: reply(m.format(self._name, self._vers)) 76 | elif player: tell(m.format(self._name, self._vers)) 77 | return 78 | for line in res.iter_lines(): 79 | if line.startswith(b'VERSION'): 80 | line = line.replace(b'VERSION = ', b'') 81 | line = line.replace(b'"', b'') 82 | serv_version = line.decode() 83 | comp = self.v_compare(self._vers, serv_version) 84 | # If called manually and outdated 85 | if channel and comp in [self.cr_outdated, self.cr_custom]: 86 | if self.get_cvar("qlx_autoupdate_iouplugins", int): 87 | reply("^1{} ^3iou^7one^4girl^7's ^6{}^7 plugin detected. Autoupdating...".format(comp,self._name)) 88 | self.iouonegirlplugin_update(player, None, channel) 89 | else: 90 | reply("^7Currently using ^3iou^7one^4girl^7's ^6{}^7 plugin ^1{}^7 version ^6{}^7.".format(self._name, comp, self._vers)) 91 | # If called manually and alright 92 | elif channel and comp in [self.cr_advanced, self.cr_latest]: 93 | reply("^7Currently using ^3iou^7one^4girl^7's {} ^6{}^7 plugin version ^6{}^7.".format(comp, self._name, self._vers)) 94 | # If routine check and it's not alright. 95 | elif player and comp in [self.cr_outdated, self.cr_custom]: 96 | if self.get_cvar("qlx_autoupdate_iouplugins", int): 97 | minqlx.console_command("echo Autoupdating iouonegirl's {} plugin.".format(self._name)) 98 | self.iouonegirlplugin_update(player, None, player.channel) 99 | else: 100 | time.sleep(15) 101 | try: 102 | tell("^3Plugin update alert^7:^6 {}^7's latest version is ^6{}^7 and you're using ^6{}^7!".format(self._name, line.decode(), self._vers)) 103 | except Exception as e: minqlx.console_command("echo Error: {}".format(e)) 104 | return 105 | 106 | # Check abstract plugin version 107 | @minqlx.thread 108 | def iouonegirlplugin_check_myversion(self, player=None, channel=None): 109 | @minqlx.next_frame 110 | def reply(m): channel.reply(m) 111 | @minqlx.next_frame 112 | def tell(m): player.tell(m) 113 | 114 | url = "https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/iouonegirl.py" 115 | res = requests.get(url) 116 | last_status = res.status_code 117 | if res.status_code != requests.codes.ok: 118 | m = "^7Currently using ^3iou^7one^4girl^7's ^6iouonegirl^7 superplugin version ^6{}^7." 119 | if channel: reply(m.format(VERSION)) 120 | elif player: tell(m.format(VERSION)) 121 | return 122 | for line in res.iter_lines(): 123 | if line.startswith(b'VERSION'): 124 | line = line.replace(b'VERSION = ', b'') 125 | line = line.replace(b'"', b'') 126 | comp = self.v_compare(VERSION, line.decode()) 127 | if channel and self._flag: 128 | reply("^7Latest ^3iou^7one^4girl^7's superplugin update has been downloaded and is waiting for a restart.") 129 | # If called manually and outdated 130 | elif channel and comp in [self.cr_outdated, self.cr_custom]: 131 | reply("^7Currently using ^3iou^7one^4girl^7's superplugin ^1{}^7 version ^6{}^7!".format(comp.upper(), VERSION)) 132 | # If called manually and alright 133 | elif channel and comp in [self.cr_latest, self.cr_advanced]: 134 | reply("^7Currently using ^3iou^7one^4girl^7's {} ^6iouonegirl^7 superplugin version ^6{}^7.".format(comp, VERSION)) 135 | # If routine check and it's not alright. 136 | elif player and comp in [self.cr_outdated, self.cr_custom]: 137 | if self.get_cvar('qlx_autoupdate_iouplugins', int): 138 | self.iouonegirlplugin_updateAbstractDelayed(player,None,player.channel) 139 | else: 140 | time.sleep(15) 141 | try: 142 | tell("^3Plugin update alert^7:^6 iouonegirl^7's latest version is ^6{}^7 and you're using ^6{}^7! ---> ^2!update iouonegirl".format(line.decode(), VERSION)) 143 | except Exception as e: minqlx.console_command("echo IouoneError: {}".format(e)) 144 | return 145 | 146 | 147 | def iouonegirlplugin_cmd_install(self, player, msg, channel): 148 | @minqlx.thread 149 | def fetch(url): 150 | try: 151 | abs_file_path = os.path.join(os.path.dirname(__file__), "{}.py".format(msg)) 152 | res = requests.get(url) 153 | if res.status_code != requests.codes.ok: raise 154 | with open(abs_file_path,"a+") as f: f.write(res.text) 155 | done() 156 | except Exception as e: 157 | fail(e) 158 | @minqlx.next_frame 159 | def done(): 160 | minqlx.reload_plugin(msg) 161 | channel.reply("{} ^2succesfully ^7installed!".format(msg)) 162 | 163 | @minqlx.next_frame 164 | def fail(e): 165 | self.msg("{} plugin installation ^1failed^7: {}".format(msg, e)) 166 | 167 | if len(msg) < 2: 168 | return minqlx.RET_USAGE 169 | 170 | msg = msg[1].lower() 171 | 172 | url = "https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/{}.py" 173 | fetch(url.format(msg)) 174 | return minqlx.RET_STOP 175 | 176 | def iouonegirlplugin_cmd_autoupdate(self, player, msg, channel): 177 | if len(msg) < 2: 178 | return minqlx.RET_USAGE 179 | 180 | if msg[1].startswith("iouonegirl"): 181 | self.iouonegirlplugin_setflag(True) # update all flags, update once 182 | self.iouonegirlplugin_updateAbstract(player, msg, channel) 183 | return minqlx.RET_STOP 184 | 185 | if msg[1] == 'all': # do this all just once 186 | for plugin_name in self.plugins: 187 | plugin = self.plugins[plugin_name] 188 | if iouonegirlPlugin in plugin.__class__.__bases__: 189 | plugin.iouonegirlplugin_update(player, msg, channel) 190 | 191 | self.iouonegirlplugin_updateAbstractDelayed(player, msg, channel) 192 | return minqlx.RET_STOP 193 | 194 | if msg[1] == self._name: # let every iouplugin execute this 195 | self.iouonegirlplugin_update(player, msg, channel) 196 | 197 | 198 | 199 | def iouonegirlplugin_cmd_list(self, player, msg, channel): 200 | m = "^7Currently using following iouonegirl plugins: ^6{}^7." 201 | iou_plugins = [] 202 | for plugin_name in self.plugins: 203 | plugin = self.plugins[plugin_name] 204 | if iouonegirlPlugin in plugin.__class__.__bases__: 205 | iou_plugins.append(plugin_name) 206 | iou_plugins.sort() 207 | channel.reply("{}^7: ^2{}".format(player.name, " ".join(msg))) 208 | if iou_plugins: 209 | channel.reply(m.format("^7, ^6".join(iou_plugins))) 210 | else: 211 | channel.reply("^7No iouonegirl plugins installed... Get some from ^6https://github.com/dsverdlo/minqlx-plugins") 212 | return minqlx.RET_STOP # once is enough, thanks 213 | 214 | def iouonegirlplugin_cmd_versions(self, player, msg, channel): 215 | for plugin_name in self.plugins: 216 | plugin = self.plugins[plugin_name] 217 | if iouonegirlPlugin in plugin.__class__.__bases__: 218 | plugin.iouonegirlplugin_check_version(player, channel) 219 | 220 | return minqlx.RET_STOP # once is enough, thanks 221 | 222 | def iouonegirlplugin_handle_player_connect(self, player): 223 | if self.db.has_permission(player, 5): 224 | self.iouonegirlplugin_check_version(player=player) 225 | 226 | # If there is no flag and this is the first plugin, check 227 | if (not self._flag) and self.is_first_plugin(): 228 | self.iouonegirlplugin_check_myversion(player=player) 229 | 230 | def iouonegirlplugin_setflag(self, boolean): 231 | for plugin_name in self.plugins: 232 | plugin = self.plugins[plugin_name] 233 | if iouonegirlPlugin in plugin.__class__.__bases__: 234 | plugin._flag = boolean 235 | 236 | def iouonegirlplugin_getflag(self): 237 | for plugin_name in self.plugins: 238 | if plugin_name == self._name: continue 239 | plugin = self.plugins[plugin_name] 240 | if iouonegirlPlugin in plugin.__class__.__bases__: 241 | if plugin._flag: return True 242 | return False 243 | 244 | # Lists all the loaded iou-plugin names alphabetically and returns True if this is the first one 245 | def is_first_plugin(self): 246 | iou_plugins = [] # collect names 247 | for plugin_name in self.plugins: 248 | plugin = self.plugins[plugin_name] 249 | if iouonegirlPlugin in plugin.__class__.__bases__: 250 | iou_plugins.append(plugin_name) 251 | # It shouldn't happen that there are 0 iou plugins, but just to prevent an error, we return False 252 | if len(iou_plugins) == 0: 253 | return False 254 | # There are iou plugins, check if we are the first one 255 | iou_plugins.sort() 256 | return self._name == iou_plugins[0] 257 | 258 | 259 | @minqlx.thread 260 | def iouonegirlplugin_update(self, player, msg, channel): 261 | @minqlx.next_frame 262 | def ready(): 263 | minqlx.reload_plugin(self._name) 264 | channel.reply("^2Updated ^3iou^7one^4girl^7's ^6{} ^7plugin to the latest version!".format(self._name)) 265 | @minqlx.next_frame 266 | def fail(e): 267 | channel.reply("^1Update failed for {}^7: {}".format(self._name, e)) 268 | try: 269 | url = "{}{}.py".format(self._loc, self._name) 270 | res = requests.get(url) 271 | if res.status_code != requests.codes.ok: return 272 | script_dir = os.path.dirname(__file__) #<-- absolute dir the script is in 273 | abs_file_path = os.path.join(script_dir, "{}.py".format(self._name)) 274 | with open(abs_file_path,"w") as f: f.write(res.text) 275 | ready() 276 | return True 277 | except Exception as e : 278 | fail(e) 279 | return False 280 | 281 | @minqlx.delay(4) 282 | def iouonegirlplugin_updateAbstractDelayed(self, player, msg, channel): 283 | self.iouonegirlplugin_setflag(True) 284 | self.iouonegirlplugin_updateAbstract(player, msg, channel) 285 | 286 | @minqlx.thread 287 | def iouonegirlplugin_updateAbstract(self, player, msg, channel): 288 | @minqlx.next_frame 289 | def ready(): 290 | if channel: 291 | channel.reply("^2Updated ^7abstract plugin, but requires a pyrestart for the changes to take effect.") 292 | 293 | url = "https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/iouonegirl.py" 294 | res = requests.get(url) 295 | if res.status_code != requests.codes.ok: return 296 | abs_file_path = os.path.join(os.path.dirname(__file__), "iouonegirl.py") 297 | with open(abs_file_path,"w") as f: f.write(res.text) 298 | ready() 299 | 300 | 301 | def find_by_name_or_id(self, player, target): 302 | # Find players returns a list of name-matching players 303 | def find_players(query): 304 | players = [] 305 | for p in self.find_player(query): 306 | if p not in players: 307 | players.append(p) 308 | return players 309 | 310 | # Tell a player which players matched 311 | def list_alternatives(players, indent=2): 312 | player.tell("A total of ^6{}^7 players matched for {}:".format(len(players),target)) 313 | out = "" 314 | for p in players: 315 | out += " " * indent 316 | out += "{}^6:^7 {}\n".format(p.id, p.name) 317 | player.tell(out[:-1]) 318 | 319 | # Get the list of matching players on name 320 | target_players = find_players(target) 321 | 322 | # If id:X is given and it amounts to a player, give it precedence. 323 | # This is to avoid deadlocks 324 | match = re.search("(id[=:][0-9]{1,2})", target) 325 | if match and match.group() == target: 326 | try: 327 | match_id = re.search("([0-9]{1,2})", target) 328 | player = self.player(int(match_id.group())) 329 | if player.steam_id: 330 | return player 331 | except: 332 | pass 333 | 334 | # even if we get only 1 person, we need to check if the input was meant as an ID 335 | # if we also get an ID we should return with ambiguity 336 | 337 | try: 338 | i = int(target) 339 | target_player = self.player(i) 340 | if not (0 <= i < 64) or not target_player: 341 | raise ValueError 342 | # Add the found ID if the player was not already found 343 | if not target_player in target_players: 344 | target_players.append(target_player) 345 | except ValueError: 346 | pass 347 | 348 | # If there were absolutely no matches 349 | if not target_players: 350 | player.tell("Sorry, but no players matched your tokens: {}.".format(target)) 351 | return None 352 | 353 | # If there were more than 1 matches 354 | if len(target_players) > 1: 355 | list_alternatives(target_players) 356 | return None 357 | 358 | # By now there can only be one person left 359 | return target_players.pop() 360 | 361 | def delaytell(self, messages, player, interval = 1): 362 | def tell(mess): 363 | return lambda: player.tell("^6{}".format(mess)) if mess else None 364 | self.interval_functions(map(tell, messages), interval) 365 | 366 | def delaymsg(self, messages, interval = 1): 367 | def msg(m): 368 | return lambda: minqlx.CHAT_CHANNEL.reply("^7{}".format(m)) if m else None 369 | self.interval_functions(map(msg, messages), interval) 370 | 371 | # Executes functions in a seperate thread with a certain interval 372 | @minqlx.thread 373 | def interval_functions(self, items, interval): 374 | @minqlx.next_frame 375 | def do(func): func() 376 | 377 | for m in items: 378 | if m: do(m) # allow "" to be used as a skip 379 | time.sleep(interval) 380 | 381 | def is_even(self, number): 382 | return number % 2 == 0 383 | 384 | def is_odd(self, number): 385 | return not self.is_even(number) 386 | 387 | def v_compare(self, old, new): 388 | # If exact same version 389 | if old == new: return self.cr_latest 390 | 391 | old = re.findall("[0-9]+", old) 392 | new = re.findall("[0-9]+", new) 393 | 394 | # If numbers are the same 395 | if old == new: return self.cr_custom 396 | 397 | for i,_ in enumerate(old): 398 | try: 399 | v_old = int(old[i]) 400 | v_new = int(new[i]) 401 | if v_old > v_new: return self.cr_advanced 402 | if v_old < v_new: return self.cr_outdated 403 | except: 404 | # If new cannot follow 405 | return self.cr_advanced 406 | # New has more numbers 407 | return self.cr_outdated 408 | -------------------------------------------------------------------------------- /myban.py: -------------------------------------------------------------------------------- 1 | # minqlx - A Quake Live server administrator bot. 2 | # Copyright (C) 2015 Mino 3 | 4 | # This file is part of minqlx. 5 | 6 | # minqlx is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # minqlx is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with minqlx. If not, see . 18 | 19 | # Edited by iouonegirl(@gmail.com) so that commands also take names 20 | # as arguments, instead of ID's only. 21 | # 22 | # You are free to modify this plugin to your custom, 23 | # except for the version command related code. 24 | 25 | import minqlx 26 | import datetime 27 | import time 28 | import re 29 | import requests 30 | 31 | LENGTH_REGEX = re.compile(r"(?P[0-9]+) (?Pseconds?|minutes?|hours?|days?|weeks?|months?|years?)") 32 | TIME_FORMAT = "%Y-%m-%d %H:%M:%S" 33 | PLAYER_KEY = "minqlx:players:{}" 34 | 35 | # This code makes sure the required superclass is loaded automatically 36 | try: 37 | from .iouonegirl import iouonegirlPlugin 38 | except: 39 | try: 40 | abs_file_path = os.path.join(os.path.dirname(__file__), "iouonegirl.py") 41 | res = requests.get("https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/iouonegirl.py") 42 | if res.status_code != requests.codes.ok: raise 43 | with open(abs_file_path,"a+") as f: f.write(res.text) 44 | from .iouonegirl import iouonegirlPlugin 45 | except Exception as e : 46 | minqlx.CHAT_CHANNEL.reply("^1iouonegirl abstract plugin download failed^7: {}".format(e)) 47 | raise 48 | 49 | VERSION = "v0.16" 50 | 51 | class myban(iouonegirlPlugin): 52 | def __init__(self): 53 | super().__init__(self.__class__.__name__, VERSION) 54 | 55 | self.unload_ban() 56 | 57 | self.add_hook("player_connect", self.handle_player_connect, priority=minqlx.PRI_HIGH) 58 | self.add_hook("player_loaded", self.handle_player_loaded) 59 | self.add_hook("player_disconnect", self.handle_player_disconnect) 60 | self.add_hook("game_countdown", self.handle_game_countdown) 61 | self.add_hook("game_start", self.handle_game_start) 62 | self.add_hook("game_end", self.handle_game_end) 63 | self.add_hook("team_switch", self.handle_team_switch) 64 | self.add_command("ban", self.cmd_ban, 2, usage=" seconds|minutes|hours|days|... [reason]") 65 | self.add_command("unban", self.cmd_unban, 2, usage="") 66 | self.add_command("checkban", self.cmd_checkban, usage="") 67 | self.add_command("forgive", self.cmd_forgive, 2, usage=" [leaves_to_forgive]") 68 | 69 | # Cvars. 70 | self.set_cvar_once("qlx_leaverBan", "0") 71 | self.set_cvar_limit_once("qlx_leaverBanThreshold", "0.63", "0", "1") 72 | self.set_cvar_limit_once("qlx_leaverBanWarnThreshold", "0.78", "0", "1") 73 | self.set_cvar_once("qlx_leaverBanMinimumGames", "15") 74 | 75 | # List of players playing that could potentially be considered leavers. 76 | self.players_start = [] 77 | self.pending_warnings = {} 78 | 79 | @minqlx.delay(3) 80 | def unload_ban(self): 81 | try: 82 | minqlx.unload_plugin('ban') 83 | except Exception as e: 84 | pass 85 | 86 | 87 | def handle_player_connect(self, player): 88 | status = self.leave_status(player.steam_id) 89 | # Check if a player has been banned for leaving, if we're doing that. 90 | if status and status[0] == "ban": 91 | return "You have been banned from this server for leaving too many games." 92 | # Check if player needs to be warned. 93 | elif status and status[0] == "warn": 94 | self.pending_warnings[player.steam_id] = status[1] 95 | 96 | # Check if a player has been banned manually. 97 | banned = self.is_banned(player.steam_id) 98 | if banned: 99 | expires, reason = banned 100 | if reason: 101 | return "You are banned until {}: {}".format(expires, reason) 102 | else: 103 | return "You are banned until {}.".format(expires) 104 | 105 | @minqlx.delay(4) 106 | def handle_player_loaded(self, player): 107 | # Update first, since player might be gone in those 4 seconds. 108 | if player.steam_id in self.pending_warnings: 109 | try: 110 | player.update() 111 | except minqlx.NonexistentPlayerError: 112 | return 113 | 114 | self.warn_player(player, self.pending_warnings[player.steam_id]) 115 | 116 | def handle_player_disconnect(self, player, reason): 117 | # Allow people to disconnect without getting a leave if teams are uneven. 118 | teams = self.teams() 119 | if len(teams["red"] + teams["blue"]) % 2 != 0 and player in self.players_start: 120 | self.players_start.remove(player) 121 | 122 | def handle_game_countdown(self): 123 | if self.get_cvar("qlx_leaverBan", bool): 124 | self.msg("Leavers are being kept track of. Repeat offenders ^6will^7 be banned.") 125 | 126 | # Needs a delay here because players will sometimes have their teams reset during the event. 127 | # TODO: Proper fix to self.teams() in game_start. 128 | @minqlx.delay(1) 129 | def handle_game_start(self, game): 130 | teams = self.teams() 131 | self.players_start = teams["red"] + teams["blue"] 132 | 133 | def handle_game_end(self, data): 134 | if data["ABORTED"]: 135 | self.players_start = [] 136 | return 137 | 138 | teams = self.teams() 139 | players_end = teams["red"] + teams["blue"] 140 | leavers = [] 141 | 142 | for player in self.players_start.copy(): 143 | if player not in players_end: 144 | # Populate player list. 145 | leavers.append(player) 146 | # Remove leavers from initial list so we can use it to award games completed. 147 | self.players_start.remove(player) 148 | 149 | db = self.db.pipeline() 150 | for player in self.players_start: 151 | db.incr(PLAYER_KEY.format(player.steam_id) + ":games_completed") 152 | for player in leavers: 153 | db.incr(PLAYER_KEY.format(player.steam_id) + ":games_left") 154 | db.execute() 155 | 156 | if leavers: 157 | self.msg("^7Leavers: ^6{}".format(" ".join([p.clean_name for p in leavers]))) 158 | self.players_start = [] 159 | 160 | def handle_team_switch(self, player, old_team, new_team): 161 | # Allow people to spectate without getting a leave if teams are uneven. 162 | if (old_team == "red" or old_team == "blue") and new_team == "spectator": 163 | teams = self.teams() 164 | if len(teams["red"] + teams["blue"]) % 2 == 0 and player in self.players_start: 165 | self.players_start.remove(player) 166 | # Add people to the list of participating players if they join mid-game. 167 | if (old_team == "spectator" and (new_team == "red" or new_team == "blue") and 168 | self.game.state == "in_progress" and player not in self.players_start): 169 | self.players_start.append(player) 170 | 171 | def cmd_ban(self, player, msg, channel): 172 | """Bans a player temporarily. A very long period works for all intents and 173 | purposes as a permanent ban, so there's no separate command for that. 174 | 175 | Example #1: !ban Mino 1 day Very rude! 176 | 177 | Example #2: !ban sponge 50 years""" 178 | if len(msg) < 4: 179 | return minqlx.RET_USAGE 180 | 181 | try: 182 | ident = int(msg[1]) 183 | assert len(msg[1]) == 17 184 | name = ident 185 | except: 186 | target_player = self.find_by_name_or_id(player, msg[1]) 187 | if not target_player: 188 | return minqlx.RET_STOP_ALL 189 | 190 | ident = target_player.steam_id 191 | name = target_player.name 192 | 193 | # Permission level 5 players not bannable. 194 | if self.db.has_permission(ident, 5): 195 | channel.reply("^6{}^7 has permission level 5 and cannot be banned.".format(name)) 196 | return 197 | 198 | if len(msg) > 4: 199 | reason = " ".join(msg[4:]) 200 | else: 201 | reason = "" 202 | 203 | r = LENGTH_REGEX.match(" ".join(msg[2:4]).lower()) 204 | if r: 205 | number = float(r.group("number")) 206 | if number <= 0: return 207 | scale = r.group("scale").rstrip("s") 208 | td = None 209 | 210 | if scale == "second": 211 | td = datetime.timedelta(seconds=number) 212 | elif scale == "minute": 213 | td = datetime.timedelta(minutes=number) 214 | elif scale == "hour": 215 | td = datetime.timedelta(hours=number) 216 | elif scale == "day": 217 | td = datetime.timedelta(days=number) 218 | elif scale == "week": 219 | td = datetime.timedelta(weeks=number) 220 | elif scale == "month": 221 | td = datetime.timedelta(days=number * 30) 222 | elif scale == "year": 223 | td = datetime.timedelta(weeks=number * 52) 224 | 225 | now = datetime.datetime.now().strftime(TIME_FORMAT) 226 | expires = (datetime.datetime.now() + td).strftime(TIME_FORMAT) 227 | base_key = PLAYER_KEY.format(ident) + ":bans" 228 | ban_id = self.db.zcard(base_key) 229 | db = self.db.pipeline() 230 | db.zadd(base_key, time.time() + td.total_seconds(), ban_id) 231 | ban = {"expires": expires, "reason": reason, "issued": now, "issued_by": player.steam_id} 232 | db.hmset(base_key + ":{}".format(ban_id), ban) 233 | db.execute() 234 | 235 | try: 236 | self.kick(ident, "has been banned until ^6{}^7: {}".format(expires, reason)) 237 | except ValueError: 238 | channel.reply("^6{} ^7has been banned. Ban expires on ^6{}^7.".format(name, expires)) 239 | 240 | def cmd_unban(self, player, msg, channel): 241 | """Unbans a player if banned.""" 242 | if len(msg) < 2: 243 | return minqlx.RET_USAGE 244 | 245 | try: 246 | ident = int(msg[1]) 247 | assert len(msg[1]) == 17 248 | name = ident 249 | except: 250 | target_player = self.find_by_name_or_id(player, msg[1]) 251 | if not target_player: 252 | return minqlx.RET_STOP_ALL 253 | ident = target_player.steam_id 254 | name = target_player.name 255 | 256 | base_key = PLAYER_KEY.format(ident) + ":bans" 257 | bans = self.db.zrangebyscore(base_key, time.time(), "+inf", withscores=True) 258 | if not bans: 259 | channel.reply("^7 No active bans on ^6{}^7 found.".format(name)) 260 | else: 261 | db = self.db.pipeline() 262 | for ban_id, score in bans: 263 | db.zincrby(base_key, ban_id, -score) 264 | db.execute() 265 | channel.reply("^6{}^7 has been unbanned.".format(name)) 266 | 267 | def cmd_checkban(self, player, msg, channel): 268 | """Checks whether a player has been banned, and if so, why.""" 269 | if len(msg) < 2: 270 | return minqlx.RET_USAGE 271 | 272 | try: 273 | ident = int(msg[1]) 274 | assert len(msg[1]) == 17 275 | name = ident 276 | except: 277 | target_player = self.find_by_name_or_id(player, msg[1]) 278 | if not target_player: 279 | return minqlx.RET_STOP_ALL 280 | ident = target_player.steam_id 281 | name = target_player.name 282 | 283 | # Check manual bans first. 284 | res = self.is_banned(ident) 285 | if res: 286 | expires, reason = res 287 | if reason: 288 | channel.reply("^6{}^7 is banned until ^6{}^7 for the following reason:^6 {}".format(name, *res)) 289 | else: 290 | channel.reply("^6{}^7 is banned until ^6{}^7.".format(name, expires)) 291 | return 292 | elif self.get_cvar("qlx_leaverBan", bool): 293 | status = self.leave_status(ident) 294 | if status and status[0] == "ban": 295 | channel.reply("^6{} ^7is banned for having left too many games.".format(name)) 296 | return 297 | 298 | channel.reply("^6{} ^7is not banned.".format(name)) 299 | 300 | def cmd_forgive(self, player, msg, channel): 301 | """Removes a leave from a player. Optional integer can be provided to remove multiple leaves.""" 302 | if len(msg) < 2: 303 | return minqlx.RET_USAGE 304 | 305 | try: 306 | ident = int(msg[1]) 307 | assert len(msg[1]) == 17 308 | name = ident 309 | except: 310 | target_player = self.find_by_name_or_id(player, msg[1]) 311 | if not target_player: 312 | return minqlx.RET_STOP_ALL 313 | 314 | ident = target_player.steam_id 315 | name = target_player.name 316 | 317 | base_key = PLAYER_KEY.format(ident) 318 | if base_key not in self.db: 319 | channel.reply("I do not know ^6{}^7.".format(name)) 320 | return 321 | 322 | try: 323 | leaves = int(self.db[base_key + ":games_left"]) 324 | except: 325 | leaves = 0 326 | 327 | if leaves <= 0: 328 | channel.reply("^6{}^7's leaves are already at ^6{}^7.".format(name, leaves)) 329 | return 330 | 331 | if len(msg) == 2: 332 | leaves_to_forgive = 1 333 | else: 334 | try: 335 | leaves_to_forgive = int(msg[2]) 336 | except ValueError: 337 | channel.reply("Unintelligible number of leaves to forgive. Please use numbers.") 338 | return 339 | 340 | new_leaves = leaves - leaves_to_forgive 341 | if new_leaves <= 0: 342 | self.db[base_key + ":games_left"] = 0 343 | channel.reply("^6{}^7's leaves have been reduced to ^60^7.".format(name)) 344 | else: 345 | self.db[base_key + ":games_left"] = new_leaves 346 | channel.reply("^6{}^7 games have been forgiven, putting ^6{}^7 at ^6{}^7 leaves." 347 | .format(leaves_to_forgive, name, new_leaves)) 348 | 349 | 350 | # ==================================================================== 351 | # HELPERS 352 | # ==================================================================== 353 | 354 | def is_banned(self, steam_id): 355 | base_key = PLAYER_KEY.format(steam_id) + ":bans" 356 | bans = self.db.zrangebyscore(base_key, time.time(), "+inf", withscores=True) 357 | if not bans: 358 | return None 359 | 360 | longest_ban = self.db.hgetall(base_key + ":{}".format(bans[-1][0])) 361 | expires = datetime.datetime.strptime(longest_ban["expires"], TIME_FORMAT) 362 | if (expires - datetime.datetime.now()).total_seconds() > 0: 363 | return expires, longest_ban["reason"] 364 | 365 | return None 366 | 367 | def leave_status(self, steam_id): 368 | """Get a player's status when it comes to leaving, given automatic leaver ban is on. 369 | 370 | """ 371 | if not self.get_cvar("qlx_leaverBan", bool): 372 | return None 373 | 374 | try: 375 | completed = self.db[PLAYER_KEY.format(steam_id) + ":games_completed"] 376 | left = self.db[PLAYER_KEY.format(steam_id) + ":games_left"] 377 | except KeyError: 378 | return None 379 | 380 | completed = int(completed) 381 | left = int(left) 382 | 383 | min_games_completed = self.get_cvar("qlx_leaverBanMinimumGames", int) 384 | warn_threshold = self.get_cvar("qlx_leaverBanWarnThreshold", float) 385 | ban_threshold = self.get_cvar("qlx_leaverBanThreshold", float) 386 | 387 | # Check their games completed to total games ratio. 388 | total = completed + left 389 | if not total: 390 | return None 391 | elif total < min_games_completed: 392 | # If they have played less than the minimum, check if they can possibly recover by the time 393 | # they have played the minimum amount of games. 394 | ratio = (completed + (min_games_completed - total)) / min_games_completed 395 | else: 396 | ratio = completed / total 397 | 398 | if ratio <= warn_threshold and (ratio > ban_threshold or total < min_games_completed): 399 | action = "warn" 400 | elif ratio <= ban_threshold and total >= min_games_completed: 401 | action = "ban" 402 | else: 403 | action = None 404 | 405 | return action, ratio 406 | 407 | def warn_player(self, player, ratio): 408 | player.tell("^7You have only completed ^6{}^7 percent of your games.".format(round(ratio * 100, 1))) 409 | player.tell("^7If you keep leaving you ^6will^7 be banned.") 410 | -------------------------------------------------------------------------------- /myirc.py: -------------------------------------------------------------------------------- 1 | # minqlx - A Quake Live server administrator bot. 2 | # Copyright (C) 2015 Mino 3 | 4 | # This file is part of minqlx. 5 | 6 | # minqlx is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | 11 | # minqlx is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with minqlx. If not, see . 18 | 19 | # Edited by iouonegirl to show different colors, broadcast more channels 20 | # like red_team, blue_team, free_team, spec_team chat (thanks b1ngo) 21 | # also updates the topic of an irc channel with LIVE updates 22 | 23 | import minqlx 24 | import threading 25 | import asyncio 26 | import threading 27 | import urllib 28 | import requests 29 | import os 30 | import random 31 | import time 32 | import re 33 | import fcntl 34 | 35 | VERSION = "v0.2.6" 36 | 37 | # This code makes sure the required superclass is loaded automatically 38 | try: 39 | from .iouonegirl import iouonegirlPlugin 40 | except: 41 | try: 42 | abs_file_path = os.path.join(os.path.dirname(__file__), "iouonegirl.py") 43 | res = requests.get("https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/iouonegirl.py") 44 | if res.status_code != requests.codes.ok: raise 45 | with open(abs_file_path,"a+") as f: f.write(res.text) 46 | from .iouonegirl import iouonegirlPlugin 47 | except Exception as e : 48 | minqlx.CHAT_CHANNEL.reply("^1iouonegirl abstract plugin download failed^7: {}".format(e)) 49 | raise 50 | 51 | 52 | # Colors using the mIRC color standard palette (which several other clients also comply with). 53 | # ^0 Black ^1 RED ^2 GREEN ^3 YLW ^4 BLUE ^5 CYAN ^6 PURPLE ^7WHITE 54 | COLORS = ("\x0301", "\x0304", "\x0303", "\x0308", "\x0302", "\x0311", "\x0306", "\x0300") 55 | EXTRA = {'connect': "\u001d\x0314", 'disconnect': "\u001d\x0314", 'map': "\x0310", 'vote':"\x0306"} 56 | BOLDCHAT = "\x02" # set to "" to disable 57 | FONTRESET = "\x0f" 58 | 59 | class myirc(iouonegirlPlugin): 60 | def __init__(self): 61 | super().__init__(self.__class__.__name__, VERSION) 62 | self.add_hook("chat", self.handle_chat, priority=minqlx.PRI_LOWEST) 63 | self.add_hook("unload", self.handle_unload) 64 | self.add_hook("player_connect", self.handle_player_connect, priority=minqlx.PRI_LOWEST) 65 | self.add_hook("player_disconnect", self.handle_player_disconnect, priority=minqlx.PRI_LOWEST) 66 | self.add_hook("vote_started", self.handle_vote_started) 67 | self.add_hook("vote_ended", self.handle_vote_ended) 68 | self.add_hook("map", self.handle_map) 69 | self.add_command(("admin", "report"), self.cmd_admin, client_cmd_perm=0) 70 | 71 | # Update topic on these hooks 72 | for hook in ["round_end", "game_start", "game_end", "map", "game_countdown"]: 73 | self.add_hook(hook, self.update_topic, priority=minqlx.PRI_LOW) 74 | 75 | self.set_cvar_once("qlx_ircServer", "irc.quakenet.org") 76 | self.set_cvar_once("qlx_ircRelayChannel", "") 77 | self.set_cvar_once("qlx_ircRelayChannelPw", "") 78 | self.set_cvar_once("qlx_ircRelayIrcChat", "1") 79 | self.set_cvar_once("qlx_ircIdleChannels", "") 80 | self.set_cvar_once("qlx_ircReportChannel", "") 81 | self.set_cvar_once("qlx_ircReportChannelPw", "") 82 | self.set_cvar_once("qlx_ircNickname", "minqlx-{}".format(random.randint(1000, 9999))) 83 | self.set_cvar_once("qlx_ircPassword", "") 84 | self.set_cvar_once("qlx_ircColors", "0") 85 | self.set_cvar_once("qlx_ircQuakenetUser", "") 86 | self.set_cvar_once("qlx_ircQuakenetPass", "") 87 | self.set_cvar_once("qlx_ircQuakenetHidden", "0") 88 | 89 | self.server = self.get_cvar("qlx_ircServer") 90 | self.relay = self.get_cvar("qlx_ircRelayChannel") 91 | self.relay_pw = self.get_cvar("qlx_ircRelayChannelPw") 92 | self.idle = self.get_cvar("qlx_ircIdleChannels", list) 93 | self.report = self.get_cvar("qlx_ircReportChannel") 94 | self.report_pw = self.get_cvar("qlx_ircReportChannelPw") 95 | self.nickname = self.get_cvar("qlx_ircNickname") 96 | self.password = self.get_cvar("qlx_ircPassword") 97 | self.qnet = (self.get_cvar("qlx_ircQuakenetUser"), 98 | self.get_cvar("qlx_ircQuakenetPass"), 99 | self.get_cvar("qlx_ircQuakenetHidden", bool)) 100 | self.is_relaying = self.get_cvar("qlx_ircRelayIrcChat", bool) 101 | 102 | self.authed = set() 103 | self.auth_attempts = {} 104 | 105 | if not self.server: 106 | self.logger.warning("IRC plugin loaded, but no IRC server specified.") 107 | elif not self.relay and not self.idle and not self.password: 108 | self.logger.warning("IRC plugin loaded, but no channels or password set. Not connecting.") 109 | else: 110 | self.irc = SimpleAsyncIrc(self.server, self.nickname, self.handle_msg, self.handle_perform, self.handle_raw) 111 | self.irc.start() 112 | self.logger.info("Connecting to {}...".format(self.server)) 113 | 114 | self.topic = "" + u"\x0304[\u2022 Live] \x0301" + " {}" 115 | self.update_topic() 116 | 117 | def handle_chat(self, player, msg, channel): 118 | handled_channels = {"chat": [COLORS[2], ""], 119 | "red_team_chat": [COLORS[1], ""], 120 | "blue_team_chat": [COLORS[4], ""], 121 | "spectator_chat": ["\x0310", "(spec)"]} # teal 122 | team_color = {"red": COLORS[1], "blue": COLORS[4], "spec": "\x0301", "free":COLORS[2]} 123 | 124 | if self.irc and self.relay and channel.name in handled_channels: 125 | color, label = handled_channels[channel.name] 126 | text = "<{tc}{p}{tc}{l}> {c}{b}{m}{b}{c}".format(tc=team_color.get(player.team, "\x0301"), c=color,p=player.name, l=label, m=msg, b=BOLDCHAT) 127 | self.irc.msg(self.relay, self.translate_colors(text)) 128 | 129 | 130 | def handle_unload(self, plugin): 131 | if plugin == self.__class__.__name__ and self.irc and self.irc.is_alive(): 132 | self.irc.quit("Plugin unloaded!") 133 | self.irc.stop() 134 | 135 | def handle_player_connect(self, player): 136 | if self.irc and self.relay: 137 | self.irc.msg(self.relay, EXTRA.get('connect', '') + self.translate_colors("{} connected.".format(player.name))) 138 | if self.game.state == "warmup": 139 | self.update_topic() 140 | 141 | def handle_player_disconnect(self, player, reason): 142 | if reason and reason[-1] not in ("?", "!", "."): 143 | reason = reason + "." 144 | 145 | if self.irc and self.relay: 146 | self.irc.msg(self.relay, EXTRA.get('disconnect', '') + self.translate_colors("{} {}".format(player.name, reason))) 147 | 148 | if self.game.state == "warmup": 149 | self.update_topic() 150 | 151 | 152 | def handle_vote_started(self, caller, vote, args): 153 | if self.irc and self.relay: 154 | caller = caller.name if caller else "The server" 155 | self.irc.msg(self.relay, EXTRA.get('vote', '') + self.translate_colors("{} called a vote: {} {}".format(caller, vote, args))) 156 | 157 | def handle_vote_ended(self, votes, vote, args, passed): 158 | if self.irc and self.relay: 159 | if passed: 160 | self.irc.msg(self.relay, EXTRA.get('vote', '') + self.translate_colors("Vote passed ({} - {}).".format(*votes))) 161 | else: 162 | self.irc.msg(self.relay, EXTRA.get('vote', '') + self.translate_colors("Vote failed.")) 163 | 164 | def handle_map(self, map, factory): 165 | if self.irc and self.relay: 166 | self.irc.msg(self.relay, EXTRA.get('map', '') + self.translate_colors("Changing map to {}...".format(map))) 167 | 168 | def handle_msg(self, irc, user, channel, msg): 169 | if not msg: 170 | return 171 | 172 | cmd = msg[0].lower() 173 | if channel.lower() == self.relay.lower(): 174 | if cmd in (".players", ".status", ".info", ".map", ".server"): 175 | self.server_report(self.relay) 176 | elif cmd in (".topic"): 177 | self.update_topic() 178 | elif self.is_relaying: 179 | minqlx.CHAT_CHANNEL.reply("[IRC] ^6{}^7:^2 {}".format(user[0], " ".join(msg))) 180 | elif channel == user[0]: # Is PM? 181 | if len(msg) > 1 and msg[0].lower() == ".auth" and self.password: 182 | if user in self.authed: 183 | irc.msg(channel, "You are already authenticated.") 184 | elif msg[1] == self.password: 185 | self.authed.add(user) 186 | irc.msg(channel, "You have been successfully authenticated. You can now use .qlx to execute commands.") 187 | else: 188 | # Allow up to 3 attempts for the user's IP to authenticate. 189 | if user[2] not in self.auth_attempts: 190 | self.auth_attempts[user[2]] = 3 191 | self.auth_attempts[user[2]] -= 1 192 | if self.auth_attempts[user[2]] > 0: 193 | irc.msg(channel, "Wrong password. You have {} attempts left.".format(self.auth_attempts[user[2]])) 194 | elif len(msg) > 1 and user in self.authed and msg[0].lower() == ".qlx": 195 | @minqlx.next_frame 196 | def f(): 197 | try: 198 | minqlx.COMMANDS.handle_input(IrcDummyPlayer(self.irc, user[0]), " ".join(msg[1:]), IrcChannel(self.irc, user[0])) 199 | except Exception as e: 200 | irc.msg(channel, "{}: {}".format(e.__class__.__name__, e)) 201 | minqlx.log_exception() 202 | f() 203 | 204 | def handle_perform(self, irc): 205 | self.logger.info("Connected to IRC!".format(self.server)) 206 | 207 | quser, qpass, qhidden = self.qnet 208 | if quser and qpass and "NETWORK" in self.irc.server_options and self.irc.server_options["NETWORK"] == "QuakeNet": 209 | self.logger.info("Authenticating on Quakenet as \"{}\"...".format(quser)) 210 | self.irc.msg("Q@CServe.quakenet.org", "AUTH {} {}".format(quser, qpass)) 211 | if qhidden: 212 | self.irc.mode(self.irc.nickname, "+x") 213 | 214 | for channel in self.idle: 215 | irc.join(channel) 216 | if self.relay: 217 | irc.join(self.relay, self.relay_pw) 218 | if self.report: 219 | irc.join(self.report, self.report_pw) 220 | 221 | def handle_raw(self, irc, msg): 222 | split_msg = msg.split() 223 | if len(split_msg) > 2 and split_msg[1] == "NICK": 224 | user = re_user.match(split_msg[0][1:]) 225 | if user and user.groups() in self.authed: 226 | # Update nick if an authed user changed it. 227 | self.authed.remove(user.groups()) 228 | self.authed.add((split_msg[2][1:], user.groups()[1], user.groups()[2])) 229 | elif len(split_msg) > 1 and split_msg[1] == "433": 230 | irc.nick(irc.nickname + "_") 231 | 232 | @classmethod 233 | def translate_colors(cls, text): 234 | if not cls.get_cvar("qlx_ircColors", bool): 235 | return cls.clean_text(text) 236 | 237 | for i, color in enumerate(COLORS): 238 | text = text.replace("^{}".format(i), color) 239 | 240 | return text 241 | 242 | @minqlx.next_frame 243 | def server_report(self, channel, topic=None): 244 | teams = self.teams() 245 | players = teams["free"] + teams["red"] + teams["blue"] + teams["spectator"] 246 | game = self.game 247 | 248 | # If game is None, there is nothing to report 249 | if game is None: 250 | return 251 | 252 | # Make a list of players. 253 | plist = [] 254 | for t in teams: 255 | if not teams[t]: 256 | continue 257 | elif t == "free": 258 | plist.append("Free: " + ", ".join([p.clean_name for p in teams["free"]])) 259 | elif t == "red": 260 | plist.append(BOLDCHAT + "\x0304Red"+FONTRESET+"\x03: " + ", ".join([p.clean_name for p in teams["red"]])) 261 | elif t == "blue": 262 | plist.append(BOLDCHAT + "\x0302Blue"+FONTRESET+"\x03: " + ", ".join([p.clean_name for p in teams["blue"]])) 263 | elif t == "spectator": 264 | plist.append("\x02Spec\x02: " + ", ".join([p.clean_name for p in teams["spectator"]])) 265 | 266 | 267 | # Info about the game state. 268 | if game.state == "in_progress": 269 | if game.type_short == "race" or game.type_short == "ffa": 270 | ginfo = "The game is in progress" 271 | else: 272 | ginfo = "The score is \x02\x0304{}\x03 - \x0302{}\x03\x02".format(game.red_score, game.blue_score) 273 | elif game.state == "countdown": 274 | ginfo = "The game is about to start" 275 | else: 276 | ginfo = "The game is in warmup" 277 | 278 | ginfo_format = "{} on \x02{}\x02 ({}) with \x02{}/{}\x02 players:" .format(ginfo, self.clean_text(game.map_title), 279 | game.type_short.upper(), len(players), self.get_cvar("sv_maxClients")) 280 | 281 | if topic: 282 | self.irc.topic(self.relay, self.topic.format(ginfo_format)) 283 | return 284 | 285 | self.irc.msg(channel, ginfo_format) 286 | self.irc.msg(channel, "{}".format(" ".join(plist))) 287 | 288 | def update_topic(self, one="", two="", three="", four="", five=""): 289 | self.server_report(self.relay, True) 290 | 291 | 292 | def cmd_admin(self, player, msg, channel): 293 | if self.irc and self.report: 294 | text = " ".join(msg[1:]) 295 | self.irc.msg(self.report, self.translate_colors('{} ({}); {}"{}"'.format(player.name, player.steam_id, BOLDCHAT, text))) 296 | player.tell("Thank you for your report.") 297 | return minqlx.RET_STOP_ALL 298 | 299 | # ==================================================================== 300 | # DUMMY PLAYER & IRC CHANNEL 301 | # ==================================================================== 302 | 303 | class IrcChannel(minqlx.AbstractChannel): 304 | name = "irc" 305 | def __init__(self, irc, recipient): 306 | self.irc = irc 307 | self.recipient = recipient 308 | 309 | def __repr__(self): 310 | return "{} {}".format(str(self), self.recipient) 311 | 312 | def reply(self, msg): 313 | for line in msg.split("\n"): 314 | self.irc.msg(self.recipient, line) 315 | 316 | class IrcDummyPlayer(minqlx.AbstractDummyPlayer): 317 | def __init__(self, irc, user): 318 | self.irc = irc 319 | self.user = user 320 | super().__init__(name="IRC-{}".format(irc.nickname)) 321 | 322 | @property 323 | def steam_id(self): 324 | return minqlx.owner() 325 | 326 | @property 327 | def channel(self): 328 | return IrcChannel(self.irc, self.user) 329 | 330 | def tell(self, msg): 331 | for line in msg.split("\n"): 332 | self.irc.msg(self.user, line) 333 | 334 | # ==================================================================== 335 | # SIMPLE INTERPROCESS LOCK 336 | # ==================================================================== 337 | 338 | class FLock: 339 | """Simple(st) filelock to provide interprocess syncronisation featuring fcntl.""" 340 | def __init__(self, filename): 341 | self.filename = filename 342 | self.handle = open(filename, 'w') 343 | 344 | def acquire(self): 345 | """Acquire the lock.""" 346 | fcntl.flock(self.handle, fcntl.LOCK_EX) 347 | 348 | def release(self): 349 | """Release the lock.""" 350 | fcntl.flock(self.handle, fcntl.LOCK_UN) 351 | 352 | def __del__(self): 353 | self.handle.close() 354 | 355 | # ==================================================================== 356 | # SIMPLE ASYNC IRC 357 | # ==================================================================== 358 | 359 | re_msg = re.compile(r"^:([^ ]+) PRIVMSG ([^ ]+) :(.*)$") 360 | re_user = re.compile(r"^(.+)!(.+)@(.+)$") 361 | 362 | try: 363 | IDENTFILE = os.path.join(os.path.expanduser("~"), ".oidentd.conf") 364 | except: 365 | IDENTFILE = None 366 | 367 | IDENTFMT = 'global {{ reply "{}" }}' 368 | LOCKFILE = "/tmp/ident.lock" 369 | 370 | class SimpleAsyncIrc(threading.Thread): 371 | def __init__(self, address, nickname, msg_handler, perform_handler, raw_handler=None, stop_event=threading.Event(), ident=None): 372 | split_addr = address.split(":") 373 | self.host = split_addr[0] 374 | self.port = int(split_addr[1]) if len(split_addr) > 1 else 6667 375 | self.nickname = nickname 376 | self.msg_handler = msg_handler 377 | self.perform_handler = perform_handler 378 | self.raw_handler = raw_handler 379 | self.stop_event = stop_event 380 | self.reader = None 381 | self.writer = None 382 | self.server_options = {} 383 | super().__init__() 384 | 385 | self._lock = threading.Lock() 386 | self._old_nickname = self.nickname 387 | 388 | # support for ident server oidentd 389 | self.idnt = ident if ident else nickname 390 | self.ifile_buf = None 391 | self.flock = FLock(LOCKFILE) 392 | 393 | def run(self): 394 | loop = asyncio.new_event_loop() 395 | logger = minqlx.get_logger("irc") 396 | asyncio.set_event_loop(loop) 397 | while not self.stop_event.is_set(): 398 | try: 399 | loop.run_until_complete(self.connect()) 400 | except Exception: 401 | minqlx.log_exception() 402 | 403 | # Disconnected. Try reconnecting in 30 seconds. 404 | logger.info("Disconnected from IRC. Reconnecting in 30 seconds...") 405 | time.sleep(30) 406 | loop.close() 407 | 408 | def stop(self): 409 | self.stop_event.set() 410 | 411 | def write(self, msg): 412 | if self.writer: 413 | with self._lock: 414 | self.writer.write(msg.encode(errors="ignore")) 415 | 416 | @asyncio.coroutine 417 | def connect(self): 418 | # Tell oidentd our 'self.ident' before connecting 419 | self.writeIdentFile() 420 | 421 | self.reader, self.writer = yield from asyncio.open_connection(self.host, self.port) 422 | self.write("NICK {0}\r\nUSER {0} 0 * :{0}\r\n".format(self.nickname)) 423 | 424 | while not self.stop_event.is_set(): 425 | line = yield from self.reader.readline() 426 | if not line: 427 | break 428 | line = line.decode("utf-8", errors="ignore").rstrip() 429 | if line: 430 | yield from self.parse_data(line) 431 | 432 | self.write("QUIT Quit by user.\r\n") 433 | self.writer.close() 434 | 435 | @asyncio.coroutine 436 | def parse_data(self, msg): 437 | split_msg = msg.split() 438 | if len(split_msg) > 1 and split_msg[0] == "PING": 439 | self.pong(split_msg[1].lstrip(":")) 440 | elif len(split_msg) > 3 and split_msg[1] == "PRIVMSG": 441 | r = re_msg.match(msg) 442 | user = re_user.match(r.group(1)).groups() 443 | channel = user[0] if self.nickname == r.group(2) else r.group(2) 444 | self.msg_handler(self, user, channel, r.group(3).split()) 445 | elif len(split_msg) > 2 and split_msg[1] == "NICK": 446 | user = re_user.match(split_msg[0][1:]) 447 | if user and user.group(1) == self.nickname: 448 | self.nickname = split_msg[2][1:] 449 | elif split_msg[1] == "005": 450 | for option in split_msg[3:-1]: 451 | opt_pair = option.split("=", 1) 452 | if len(opt_pair) == 1: 453 | self.server_options[opt_pair[0]] = "" 454 | else: 455 | self.server_options[opt_pair[0]] = opt_pair[1] 456 | # We're connected, restore the ident file 457 | self.restoreIdentFile() 458 | elif len(split_msg) > 1 and split_msg[1] == "433": 459 | self.nickname = self._old_nickname 460 | # Stuff to do after we get the MOTD. 461 | elif re.match(r":[^ ]+ (376|422) .+", msg): 462 | self.perform_handler(self) 463 | 464 | # If we have a raw handler, let it do its stuff now. 465 | if self.raw_handler: 466 | self.raw_handler(self, msg) 467 | 468 | def msg(self, recipient, msg): 469 | self.write("PRIVMSG {} :{}\r\n".format(recipient, msg)) 470 | 471 | def nick(self, nick): 472 | with self._lock: 473 | self._old_nickname = self.nickname 474 | self.nickname = nick 475 | self.write("NICK {}\r\n".format(nick)) 476 | 477 | def join(self, channels, pw=""): 478 | self.write("JOIN {} {}\r\n".format(channels, pw)) 479 | 480 | def part(self, channels): 481 | self.write("PART {}\r\n".format(channels)) 482 | 483 | def mode(self, what, mode): 484 | self.write("MODE {} {}\r\n".format(what, mode)) 485 | 486 | def kick(self, channel, nick, reason): 487 | self.write("KICK {} {}:{}\r\n".format(channel, nick, reason)) 488 | 489 | def quit(self, reason): 490 | self.write("QUIT :{}\r\n".format(reason)) 491 | 492 | def pong(self, n): 493 | self.write("PONG :{}\r\n".format(n)) 494 | 495 | def topic(self, channel, newtopic): 496 | self.write("TOPIC {} : {}\r\n".format(channel, newtopic)) 497 | 498 | def writeIdentFile(self): 499 | """Write self.ident to oidentd's user cfg file but 500 | keep any entries for restoring them later.""" 501 | 502 | if not os.path.isfile(IDENTFILE): 503 | return 504 | 505 | # In the process of connecting, acquire the lock 506 | self.flock.acquire() 507 | try: 508 | with open(IDENTFILE, 'r') as ifile: 509 | self.ifile_buf = ifile.readlines() 510 | with open(IDENTFILE, 'w') as ifile: 511 | ifile.write(IDENTFMT.format(self.idnt)) 512 | except Exception: 513 | minqlx.log_exception() 514 | 515 | def restoreIdentFile(self): 516 | """Restore the identfile.""" 517 | if not os.path.isfile(IDENTFILE): 518 | return 519 | 520 | try: 521 | with open(IDENTFILE, 'w') as ifile: 522 | for l in self.ifile_buf: 523 | ifile.write(l) 524 | except Exception: 525 | minqlx.log_exception() 526 | # We're done, release the lock so other 527 | # minqlx-plugins can do the same 528 | self.flock.release() 529 | -------------------------------------------------------------------------------- /player_info.py: -------------------------------------------------------------------------------- 1 | # This is a plugin created by iouonegirl(@gmail.com) 2 | # Copyright (c) 2016 iouonegirl + Minkyn 3 | # https://github.com/dsverdlo/minqlx-plugins 4 | # 5 | # You are free to modify this plugin to your custom, 6 | # except for the version command related code. 7 | # 8 | # Its purpose is to display some information about the players. 9 | # When players fall off the scoreboard, they are now also able 10 | # to view their information 11 | # 12 | # Players deactivated on qlstats can be banned or just 13 | # trigger a server warning. 14 | # Can also use a different elo rating site by changing 15 | # cvar qlx_balanceUrl 16 | # 17 | # Uses: 18 | # - set qlx_pinfo_display_auto "0" 19 | # - set qlx_pinfo_show_deactivated "1" 20 | # ^ (If this is 1 then a warning will be shown of players who are deactivated on qlstats/other) 21 | # - set qlx_pinfo_ban_deactivated "0" 22 | # - set qlx_pinfo_ban_duration_weeks "1" 23 | # ^ If ban_deactivated is "1", then this var will specify for how many weeks the ban will last 24 | # (please only use integers (no decimals) here) 25 | 26 | import minqlx 27 | import requests 28 | import itertools 29 | import threading 30 | import datetime 31 | import random 32 | import time 33 | import os 34 | import re 35 | 36 | # This code makes sure the required superclass is loaded automatically 37 | try: 38 | from .iouonegirl import iouonegirlPlugin 39 | except: 40 | try: 41 | abs_file_path = os.path.join(os.path.dirname(__file__), "iouonegirl.py") 42 | res = requests.get("https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/iouonegirl.py") 43 | if res.status_code != requests.codes.ok: raise 44 | with open(abs_file_path,"a+") as f: f.write(res.text) 45 | from .iouonegirl import iouonegirlPlugin 46 | except Exception as e : 47 | minqlx.CHAT_CHANNEL.reply("^1iouonegirl abstract plugin download failed^7: {}".format(e)) 48 | raise 49 | 50 | VERSION = "v0.36" 51 | 52 | PLAYER_KEY = "minqlx:players:{}" 53 | COMPLETED_KEY = PLAYER_KEY + ":games_completed" 54 | LEFT_KEY = PLAYER_KEY + ":games_left" 55 | LENGTH_REGEX = re.compile(r"(?P[0-9]+) (?Pseconds?|minutes?|hours?|days?|weeks?|months?|years?)") 56 | TIME_FORMAT = "%Y-%m-%d %H:%M:%S" 57 | 58 | # Elo retrieval vars 59 | EXT_SUPPORTED_GAMETYPES = ("ca", "ctf", "dom", "ft", "tdm", "duel", "ffa") 60 | RATING_KEY = "minqlx:players:{0}:ratings:{1}" # 0 == steam_id, 1 == short gametype. 61 | MAX_ATTEMPTS = 3 62 | CACHE_EXPIRE = 60*30 # 30 minutes TTL. 63 | DEFAULT_RATING = 1500 64 | SUPPORTED_GAMETYPES = ("ca", "ctf", "dom", "ft", "tdm") 65 | 66 | 67 | class player_info(iouonegirlPlugin): 68 | def __init__(self): 69 | super().__init__(self.__class__.__name__, VERSION) 70 | 71 | # set cvars once. EDIT THESE IN SERVER.CFG 72 | self.set_cvar_once("qlx_balanceApi", "elo") 73 | self.set_cvar_once("qlx_balanceUrl", "qlstats.net") 74 | self.set_cvar_once("qlx_pinfo_display_auto", "0") 75 | self.set_cvar_once("qlx_pinfo_show_deactivated", "1") 76 | self.set_cvar_once("qlx_pinfo_ban_deactivated", "0") 77 | self.set_cvar_once("qlx_pinfo_ban_duration_weeks", "1") 78 | 79 | self.add_command("info", self.cmd_player_info, usage="[|]") 80 | self.add_command("scoreboard", self.cmd_scoreboard, usage="[|]") 81 | self.add_command(("allelo", "allelos", "aelo", "eloall"), self.cmd_all_elos, usage="[|]") 82 | 83 | self.add_hook("player_connect", self.handle_player_connect, priority=minqlx.PRI_LOWEST) 84 | 85 | self.cache_cvars() 86 | 87 | 88 | def cache_cvars(self): 89 | self.balanceApiUrl = self.get_cvar("qlx_balanceUrl") 90 | self.api_url = "http://{}/{}/".format(self.balanceApiUrl, self.get_cvar("qlx_balanceApi")) 91 | 92 | 93 | def handle_player_connect(self, player): 94 | cond = self.get_cvar("qlx_pinfo_display_auto", int) 95 | cond += self.get_cvar("qlx_pinfo_show_deactivated", int) 96 | cond += self.get_cvar("qlx_pinfo_ban_deactivated", int) 97 | 98 | human = str(player.steam_id)[0] != "9" 99 | 100 | if human and cond: 101 | self.fetch(player, self.game.type_short, None) 102 | 103 | 104 | 105 | 106 | def cmd_player_info(self, player, msg, channel): 107 | if len(msg) > 2: 108 | return minqlx.RET_USAGE 109 | 110 | if len(msg) < 2: 111 | target_player = player 112 | else: 113 | try: 114 | sid = int(msg[1]) 115 | assert len(msg[1]) == 17 116 | target_player = sid 117 | except: 118 | target_player = self.find_by_name_or_id(player, msg[1]) 119 | if not target_player: return minqlx.RET_STOP_EVENT 120 | 121 | # If there is a duel going on and a spec called the command, 122 | # ensure that the playing players don't see it 123 | if self.game.type_short == "duel" and self.game.state == "in_progress": 124 | if player.team != "free": 125 | channel = minqlx.SPECTATOR_CHAT_CHANNEL 126 | 127 | # go fetch his elo 128 | self.fetch(target_player, self.game.type_short, channel) 129 | 130 | 131 | def cmd_all_elos(self, player, msg, channel): 132 | if len(msg) > 2: 133 | return minqlx.RET_USAGE 134 | 135 | if len(msg) < 2: 136 | target_player = player 137 | else: 138 | try: 139 | sid = int(msg[1]) 140 | assert len(msg[1]) == 17 141 | target_player = sid 142 | except: 143 | target_player = self.find_by_name_or_id(player, msg[1]) 144 | if not target_player: return minqlx.RET_STOP_EVENT 145 | 146 | # go fetch his elo 147 | self.fetch(target_player, None, channel) 148 | 149 | # Show info of people fallen off the scoreboard 150 | def cmd_scoreboard(self, player, msg, channel): 151 | def show(target): 152 | _n = target.name 153 | _s = target.stats.score 154 | _k = target.stats.kills 155 | _d = target.stats.deaths 156 | try: 157 | _p = target.stats.ping 158 | except: 159 | _p = "--" # in case of older minqlx version 160 | 161 | _tm = int(target.stats.time / 60000 ) 162 | _ts = int((target.stats.time % 60000) / 1000) 163 | _dd = target.stats.damage_dealt 164 | _t = target.team 165 | _ad = "{}^2ALIVE" if target.is_alive else "{}^1DEAD" 166 | _hc = int(target.cvars.get('handicap', 100)) 167 | _c = '^7,' 168 | if _t == 'blue': _c = '^4,' 169 | if _t == 'red': _c = '^1,' 170 | _hc = "^3{}%^7-".format(_hc) if (_hc < 100) else '' 171 | _ad = _ad.format(_hc) 172 | 173 | 174 | message = "{}^7({}^7) {k}score ^7{}{c} {k}k/d ^7{}/{}{c} {k}dmg ^7{}{c} {k}time ^7{}m{}s{c} {k}ping ^7{}" 175 | message = message.format(_n, _ad, _s, _k, _d, _dd, _tm, _ts, _p, c=_c, k=_c[0:-1]) 176 | channel.reply("^7" + message) 177 | 178 | teams = self.teams() 179 | scoreboard_length = 8 180 | 181 | players = [] 182 | if len(teams['red']) > scoreboard_length: 183 | sorted_red = sorted(teams["red"], key=lambda p: p.score, reverse=True) 184 | for p in sorted_red[scoreboard_length:]: 185 | players.append(p) 186 | if len(teams['blue']) > scoreboard_length: 187 | sorted_blue = sorted(teams['blue'], key=lambda p: p.score, reverse=True) 188 | for p in sorted_blue[scoreboard_length:]: 189 | players.append(p) 190 | 191 | if not players: 192 | channel.reply("^7No players falling off the scoreboard...") 193 | return 194 | 195 | for p in players: 196 | show(p) 197 | 198 | 199 | 200 | 201 | 202 | @minqlx.thread 203 | def fetch(self, player, gt, channel): 204 | try: 205 | sid = player.steam_id 206 | except: 207 | sid = player 208 | 209 | attempts = 0 210 | last_status = 0 211 | while attempts < MAX_ATTEMPTS: 212 | attempts += 1 213 | url = f"{self.api_url}{sid}" 214 | res = requests.get(url) 215 | last_status = res.status_code 216 | if res.status_code != requests.codes.ok: 217 | continue 218 | 219 | js = res.json() 220 | if "players" not in js: 221 | last_status = -1 222 | continue 223 | 224 | if "deactivated" in js and js["deactivated"]: 225 | 226 | # If we notice deactivated, ban player (auto or cmd initiated) 227 | if self.get_cvar("qlx_pinfo_ban_deactivated", int): 228 | self.ban_deactivated(player) 229 | return 230 | 231 | elif self.get_cvar("qlx_pinfo_show_deactivated", int): 232 | @minqlx.next_frame 233 | def warn(): 234 | self.msg("^3SERVER WARNING^7! {}^7's account has been ^1DEACTIVATED^7 on {}.".format(player.name, self.balanceApiUrl)) 235 | warn() 236 | 237 | # if we came here from a connect trigger, for a server that doesnt want auto info, return 238 | if not channel and not self.get_cvar("qlx_pinfo_display_auto", int): 239 | return 240 | 241 | if not channel: 242 | channel = minqlx.CHAT_CHANNEL 243 | if self.game.state == "in_progress": 244 | channel = minqlx.SPECTATOR_CHAT_CHANNEL 245 | 246 | 247 | for p in js["players"]: 248 | _sid = int(p["steamid"]) 249 | if _sid == sid: # got our player 250 | # If they want all the elos 251 | if not gt: return self.callback_all(player, p, channel) 252 | # If the request gametype is found 253 | if gt in p: return self.callback(player, p[gt]["elo"], p[gt]["games"], channel) 254 | # If the gametype was not found 255 | else: return self.callback(player, 0,0, channel) 256 | 257 | 258 | 259 | return self.callback(player, 0, 0, channel) 260 | 261 | 262 | def callback_all(self, player, modes, channel): 263 | info = [] 264 | for mode in modes: 265 | if mode not in EXT_SUPPORTED_GAMETYPES: continue 266 | elo = modes[mode]['elo'] 267 | games = modes[mode]["games"] 268 | info.append(" ^3{}^7: {} ({} games)".format(mode.upper(), elo, games)) 269 | 270 | if not info: 271 | channel.reply("^6{}^7 has no tracked elos.".format(player.name)) 272 | else: 273 | b = 'b' if self.get_cvar('qlx_balanceApi') == 'elo_b' else '' 274 | channel.reply("^6{}^7's {}ELO's: {}".format(player.name, b, ", ".join(info))) 275 | 276 | 277 | def callback(self, target_player, elo, games, channel): 278 | 279 | try: 280 | ident = target_player.steam_id 281 | name = target_player.name 282 | except: 283 | ident = target_player 284 | name = target_player 285 | 286 | try: 287 | completed = int(self.db[COMPLETED_KEY.format(ident)]) 288 | except: 289 | completed = 0 290 | try: 291 | left = int(self.db[LEFT_KEY.format(ident)]) 292 | except: 293 | left = 0 294 | 295 | 296 | if left + completed == 0: 297 | games_here_p = 1 298 | else: 299 | games_here_p = left + completed 300 | 301 | 302 | info = ["^6{} ^7games here".format(completed + left)] 303 | info[0] = info[0] + " ^7(^6{}^7 tracked {})".format(games, self.game.type_short) 304 | 305 | info.append("^7quit ^6{}^7%".format(round(left/(games_here_p)*100))) 306 | 307 | info.append("^3{} ^7{}ELO: ^6{}^7".format(self.game.type_short.upper(),'b' if self.get_cvar('qlx_balanceApi') == 'elo_b' else '', elo, games)) 308 | 309 | return channel.reply("^6{}^7: ".format(name) + "^7, ".join(info) + "^7.") 310 | 311 | @minqlx.delay(2) 312 | def ban_deactivated(self, player): 313 | try: 314 | duration = self.get_cvar("qlx_pinfo_ban_duration_weeks", int) 315 | td = datetime.timedelta(weeks=duration) 316 | now = datetime.datetime.now().strftime(TIME_FORMAT) 317 | expires = (datetime.datetime.now() + td).strftime(TIME_FORMAT) 318 | base_key = PLAYER_KEY.format(player.steam_id) + ":bans" 319 | ban_id = self.db.zcard(base_key) 320 | db = self.db.pipeline() 321 | db.zadd(base_key, time.time() + td.total_seconds(), ban_id) 322 | ban = {"expires": expires, "reason": "deactivated account", "issued": now, "issued_by": "player_info"} 323 | db.hmset(base_key + ":{}".format(ban_id), ban) 324 | db.execute() 325 | self.kick(player.id, "banned from this server because of deactivated account.") 326 | except: 327 | n = player.name 328 | self.kick(player.id, "kicked because of deactivated account.") 329 | self.msg("{} has been kicked, but could not be banned. Contact iouonegirl".format(n)) 330 | -------------------------------------------------------------------------------- /railable.py: -------------------------------------------------------------------------------- 1 | # This is a plugin created by iouonegirl(@gmail.com) 2 | # Copyright (c) 2016 iouonegirl 3 | # https://github.com/dsverdlo/minqlx-plugins 4 | # 5 | # You are free to modify this plugin to your custom, 6 | # except for the version command related code. 7 | # 8 | # It provides an option to receive a message when you become railable 9 | # 10 | 11 | import minqlx 12 | import datetime 13 | import time 14 | import threading 15 | import requests 16 | 17 | VERSION = "v0.2" 18 | 19 | PLAYER_KEY = "minqlx:players:{}" 20 | RAIL_KEY = PLAYER_KEY + ":railable" 21 | RAIL_MSG_KEY = PLAYER_KEY + ":railmsg" 22 | 23 | DEFAULT_MESSAGE = "^7Railable! (edit via ^2!railmsg ^7or disable via ^2!railable^7)" 24 | 25 | class railable(minqlx.Plugin): 26 | def __init__(self): 27 | super().__init__() 28 | 29 | self.shown = [] 30 | self.running = False 31 | 32 | self.add_hook("round_start", self.handle_round_start) 33 | self.add_hook("round_end", self.handle_round_end) 34 | self.add_hook("death", self.handle_death) 35 | self.add_command("railable", self.cmd_toggle_pref) 36 | self.add_command("railmsg", self.cmd_set_rail_msg, usage="") 37 | self.add_command("railinfo", self.cmd_info) 38 | self.add_command("v_railable", self.cmd_version) 39 | self.add_hook("player_connect", self.handle_player_connect) 40 | 41 | def cmd_version(self, player, msg, channel): 42 | self.check_version(channel=channel) 43 | 44 | @minqlx.thread 45 | def check_version(self, player=None, channel=None): 46 | url = "https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/{}.py".format(self.__class__.__name__) 47 | res = requests.get(url) 48 | last_status = res.status_code 49 | if res.status_code != requests.codes.ok: return 50 | for line in res.iter_lines(): 51 | if line.startswith(b'VERSION'): 52 | line = line.replace(b'VERSION = ', b'') 53 | line = line.replace(b'"', b'') 54 | # If called manually and outdated 55 | if channel and VERSION.encode() != line: 56 | channel.reply("^7Currently using ^3iou^7one^4girl^7's ^6{}^7 plugin ^1outdated^7 version ^6{}^7.".format(self.__class__.__name__, VERSION)) 57 | # If called manually and alright 58 | elif channel and VERSION.encode() == line: 59 | channel.reply("^7Currently using ^3iou^7one^4girl^7's latest ^6{}^7 plugin version ^6{}^7.".format(self.__class__.__name__, VERSION)) 60 | # If routine check and it's not alright. 61 | elif player and VERSION.encode() != line: 62 | time.sleep(15) 63 | try: 64 | player.tell("^3Plugin update alert^7:^6 {}^7's latest version is ^6{}^7 and you're using ^6{}^7!".format(self.__class__.__name__, line.decode(), VERSION)) 65 | except Exception as e: minqlx.console_command("echo {}".format(e)) 66 | return 67 | 68 | def cmd_info(self, player, msg, channel): 69 | channel.reply("^7Get a customizable ^2!railmsg ^7when you become ^2!railable^7.") 70 | 71 | def cmd_toggle_pref(self, player, msg, channel): 72 | self.set_notif_pref(player.steam_id) 73 | 74 | if self.get_notif_pref(player.steam_id): 75 | channel.reply("^7{} will now get a hint when ^2!railable^7.".format(player.name)) 76 | else: 77 | channel.reply("^7{} will stop seeing ^2!railable ^7notifications.".format(player.name)) 78 | 79 | 80 | def cmd_set_rail_msg(self, player, msg, channel): 81 | if len(msg) < 2: 82 | if not self.get_notif_pref(player.steam_id): 83 | channel.reply("^7{} does not have the ^2!railable ^7notifications activated.".format(player.name)) 84 | return 85 | m = self.get_rail_msg(player.steam_id) 86 | channel.reply("^7{}^7's ^2!railmsg ^7is: {}.".format(player.name, m or DEFAULT_MESSAGE)) 87 | else: 88 | self.set_rail_msg(player.steam_id, " ".join(msg[1:])) 89 | self.db[RAIL_KEY.format(player.steam_id)] = 1 90 | channel.reply("^7{}^7's ^2!railmsg ^7has been changed and ^2!railable ^7activated!".format(player.name)) 91 | 92 | 93 | def handle_death(self, victim, killer, data): 94 | if victim.id in self.shown: 95 | self.shown.remove() 96 | 97 | def handle_round_start(self, n): 98 | self.running = True 99 | self.shown = [] 100 | threading.Thread(target=self.looking).start() 101 | 102 | def handle_round_end(self, n): 103 | self.running = False 104 | 105 | def handle_player_connect(self, player): 106 | if self.db.has_permission(player, 5): 107 | self.check_version(player=player) 108 | 109 | def looking(self): 110 | while self.game.state == 'in_progress' and self.running: 111 | teams = self.teams() 112 | for p in teams['red'] + teams['blue']: 113 | pid = p.steam_id 114 | if self.get_notif_pref(pid) and self.railable(p) and not pid in self.shown: 115 | railmsg = self.get_rail_msg(pid) 116 | p.center_print(railmsg or DEFAULT_MESSAGE) 117 | self.shown.append(pid) 118 | time.sleep(0.33) 119 | 120 | def railable(self, p): 121 | if p.is_alive: 122 | h = p.health 123 | a = p.armor 124 | return (h <= 26) or (h+a <= 80) 125 | return False 126 | 127 | def get_notif_pref(self, sid): 128 | try: 129 | return int(self.db[RAIL_KEY.format(sid)]) 130 | except: 131 | return False 132 | 133 | def set_notif_pref(self, sid): 134 | self.db[RAIL_KEY.format(sid)] = 0 if self.get_notif_pref(sid) else 1 135 | 136 | def get_rail_msg(self, sid): 137 | try: 138 | return self.db[RAIL_MSG_KEY.format(sid)] 139 | except: 140 | return False 141 | 142 | def set_rail_msg(self, sid, msg): 143 | self.db[RAIL_MSG_KEY.format(sid)] = msg -------------------------------------------------------------------------------- /sets.py: -------------------------------------------------------------------------------- 1 | # This is a plugin created by iouonegirl(@gmail.com) 2 | # Copyright (c) 2016 iouonegirl 3 | # https://github.com/dsverdlo/minqlx-plugins 4 | # 5 | # This plugin allows two players to duel a certain amount of games 6 | # in succession without being interrupted by other players 7 | # 8 | # Thanks to Arctus Grande and Turkey for helping me test the beta 9 | # 10 | # Uses: 11 | # set qlx_sets_maximum "7" 12 | # ^ Maximum amount of games players can reserve for their set 13 | # 14 | 15 | 16 | import minqlx 17 | import threading 18 | import urllib 19 | import time 20 | import re 21 | import os 22 | import requests 23 | 24 | VERSION = "v0.1" 25 | 26 | CVAR_MAX = "qlx_sets_maximum" 27 | 28 | # This code makes sure the required superclass is loaded automatically 29 | try: 30 | from .iouonegirl import iouonegirlPlugin 31 | except: 32 | try: 33 | abs_file_path = os.path.join(os.path.dirname(__file__), "iouonegirl.py") 34 | res = requests.get("https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/iouonegirl.py") 35 | if res.status_code != requests.codes.ok: raise 36 | with open(abs_file_path,"a+") as f: f.write(res.text) 37 | from .iouonegirl import iouonegirlPlugin 38 | except Exception as e : 39 | minqlx.CHAT_CHANNEL.reply("^1iouonegirl abstract plugin download failed^7: {}".format(e)) 40 | raise 41 | 42 | 43 | # Set class 44 | class myset(object): 45 | def __init__(self, parent, p1, p2, matches=0): 46 | self._parent = parent 47 | self._p1 = p1.steam_id 48 | self._p1n = p1.name 49 | self._p2 = p2.steam_id 50 | self._p2n = p2.name 51 | self._matches = matches 52 | self._maps = [] 53 | self._scores = [] 54 | 55 | def add_score(self, mapname, score_p1, score_p2): 56 | self._scores.append({'map':mapname, 'sp1':score_p1, 'sp2':score_p2}) 57 | 58 | def games_played(self): 59 | return len(self._scores) 60 | 61 | def played_games(self): 62 | return self.games_played() 63 | 64 | def is_ended(self): 65 | return len(self._scores) >= self._matches 66 | 67 | def is_started(self): 68 | return len(self._scores) 69 | 70 | def reserved(self): 71 | return self._matches 72 | 73 | def players(self): 74 | return [self._p1, self._p2] 75 | 76 | def winner(self): 77 | p1wins = 0 78 | for s in self._scores: 79 | if not s['map']: continue # If terminated, stop 80 | if s['sp1'] > s['sp2']: p1wins += 1 81 | if s['sp1'] < s['sp2']: p1wins -= 1 82 | 83 | if p1wins > 0: 84 | return {'winner':self._p1, 'victories':p1wins} 85 | if p1wins < 0: 86 | return {'winner':self._p2, 'victories':-p1wins} 87 | 88 | return None 89 | 90 | def terminate(self, forfeit_id): 91 | while len(self._scores) < self._matches: 92 | if self._p1 == forfeit_id: 93 | self.add_score(None, -1, 0) 94 | else: 95 | self.add_score(None, 0, -1) 96 | 97 | def __str__(self): 98 | def colr(s1, s2): 99 | return ("^1{}" if s2 > s1 else "^2{}").format(s1) 100 | def func(record): 101 | s1 = colr(record['sp1'], record['sp2']) 102 | s2 = colr(record['sp2'], record['sp1']) 103 | return "^7{} ({}^7:{}^7)".format(record['map'], s1, s2) 104 | m1 = "^4Set of {}^7: {} ^7vs {} ^7map results:\n" 105 | m1 = m1.format(self._matches, self._parent.player(self._p1), self._parent.player(self._p2)) 106 | if self._scores: 107 | return m1 + ", ".join(list(map(lambda s: func(s), self._scores))) 108 | else: 109 | return m1.replace('map results:\n', 'ready to start!') 110 | 111 | class GetMaps(minqlx.AbstractChannel): 112 | """A channel that gets the maps and triggers a callback.""" 113 | def __init__(self, callback): 114 | super().__init__("getmaps") 115 | self.callback = callback 116 | 117 | def reply(self, msg): 118 | if not str(msg).startswith("Directory"): return 119 | matches = re.findall("\\n.*\.bsp", str(msg)) 120 | if not matches: return # not expected 121 | maps = map(lambda el: el[1:-4], matches) 122 | self.callback(list(maps)) 123 | 124 | # Start plugin 125 | class sets(iouonegirlPlugin): 126 | 127 | def __init__(self): 128 | super().__init__(self.__class__.__name__, VERSION) 129 | 130 | # Set required cvars once. DONT EDIT THEM HERE BUT IN SERVER.CFG 131 | self.set_cvar_once(CVAR_MAX, "7") 132 | 133 | self._sets = [] 134 | self._id_to_pass_vote = [] 135 | self.supported_maps = [] 136 | 137 | self.add_command(("suggest", "complain"), self.cmd_suggest) 138 | self._suggestions = {} 139 | 140 | self.add_command(("set", "reserve", "reserved"), self.cmd_reserve, usage="".format(self.get_cvar(CVAR_MAX))) 141 | self.add_command("forfeit", self.cmd_forfeit) 142 | self.add_command("setstatus", self.cmd_set_status) 143 | self.add_command(("setnext", "nextmap"), self.cmd_set_next) 144 | self.add_command("startset", self.cmd_start_set, 2) 145 | self.add_command("lastset", self.cmd_last_set) 146 | self.add_command("setmaps", self.cmd_maps, usage="[ ... ]") 147 | self.add_command(("setcmds", "sethelp", "setcommands"), self.cmd_set_cmds) 148 | self.add_hook("map", self.handle_map) 149 | self.add_hook("game_end", self.handle_game_end) 150 | self.add_hook("game_countdown", self.handle_game_countdown) 151 | self.add_hook("new_game", self.handle_new_game, priority=minqlx.PRI_HIGH) 152 | self.add_hook("vote", self.handle_vote) 153 | self.add_hook("vote_ended", self.handle_vote_ended) 154 | self.add_hook("player_connect", self.handle_player_connect) 155 | self.add_hook("player_disconnect", self.handle_player_disconnect) 156 | self.add_hook("team_switch_attempt", self.handle_ts_attempt) 157 | 158 | self.q3mapTranslationMapNamesQ3 = ["q3dm0", "q3dm1", "q3dm2", "q3dm3", "q3dm4", "q3dm5", "q3dm6", "q3dm7", "q3dm8", "q3dm9", "q3dm10", "q3dm11", "q3dm12", "q3dm13", "q3dm14", "q3dm15", "q3dm16", "q3dm17", "q3dm18", "q3dm19", "q3tourney1", "q3tourney2", "q3tourney3", "q3tourney4", "q3tourney5", "q3tourney6", "q3ctf1", "q3ctf2", "q3ctf3", "q3ctf4"] 159 | self.q3mapTranslationMapNamesQL = ["introduction", "arenagate", "spillway", "hearth", "eviscerated", "forgotten", "campgrounds", "retribution", "brimstoneabbey", "heroskeep", "namelessplace", "chemicalreaction", "dredwerkz", "lostworld", "grimdungeons", "demonkeep", "cobaltstation", "longestyard", "spacechamber", "terminalheights", "powerstation", "provinggrounds", "hellsgate", "verticalvengeance", "fatalinstinct", "beyondreality", "duelingkeeps", "troubledwaters", None, "spacectf"] 160 | 161 | self.maptranslations = {} 162 | 163 | self.grabmaps() 164 | # HOOKS 165 | 166 | def cmd_suggest(self, p, m, c): 167 | @minqlx.next_frame 168 | def done(): 169 | c.reply("Suggestion/complain sent, thank you!") 170 | @minqlx.thread 171 | def send(): 172 | par = {'port':self.get_cvar('net_port'), 'suggestion': " ".join(m[1:]) } 173 | par['suggestion'] = urllib.parse.quote(par['suggestion'], safe=' ') 174 | requests.get("http://iouonegirl.dsverdlo.be/tr/suggestions.php", params=par) 175 | done() 176 | 177 | 178 | if not p.steam_id in self._suggestions: 179 | self._suggestions[p.steam_id] = 0 180 | 181 | if self._suggestions[p.steam_id] >= 10: 182 | p.tell("You have already made 10 suggestions and cannot post more") 183 | return minqlx.RET_STOP_ALL 184 | else: 185 | send() 186 | self._suggestions[p.steam_id] += 1 187 | 188 | 189 | @minqlx.delay(3) 190 | def grabmaps(self): 191 | if self.supported_maps: return 192 | 193 | def callback(maps=[]): 194 | self.supported_maps = maps 195 | 196 | with minqlx.redirect_print(GetMaps(callback)): 197 | minqlx.console_command("dir maps bsp") 198 | 199 | 200 | 201 | def handle_ts_attempt(self, p, old, new): 202 | if self.stop(): return 203 | if self.is_set_going_on(): 204 | aset = self._sets[-1] 205 | if p.steam_id in aset.players(): 206 | if new != "free": 207 | p.tell("^4Set of {}^7: You are playing a set and cannot spec. To forfeit, type ^2!forfeit".format(aset._matches)) 208 | return minqlx.RET_STOP_ALL 209 | 210 | 211 | def handle_vote(self, player, yes): 212 | if self.stop(): return 213 | if not self.is_vote_active(): return 214 | if not self._id_to_pass_vote: return 215 | 216 | if not player.steam_id in self._id_to_pass_vote: return 217 | 218 | minqlx.force_vote(yes) 219 | m = "^2accepted" if yes else "^1denied" 220 | self.msg("^7{} {} ^7the ^4set ^7vote.".format(player.name, m)) 221 | 222 | def handle_vote_ended(self, votes, vote, args, passed): 223 | self._id_to_pass_vote = [] 224 | 225 | @minqlx.delay(1) 226 | def handle_new_game(self): 227 | # If game starts from warmup, ignore 228 | #if self.game.state in ["warmup"]: return 229 | if self.stop(): return 230 | 231 | if self.is_set_going_on(): 232 | self.handle_map(self.game.map, self.game.factory) 233 | 234 | 235 | def handle_map(self, mapname, factory): 236 | @minqlx.delay(2) 237 | def cvmap(planned_map, aset): 238 | if self.game.state == "in_progress": return 239 | self._id_to_pass_vote = aset.players() 240 | self.callvote("map {}".format(planned_map),"^4Set of {}^7: Go to map {}?".format(aset._matches, planned_map)) 241 | def grab_unwanted(teams, _p1, _p2): 242 | for _p in teams['free']: 243 | if _p.steam_id not in [_p1, _p2]: 244 | return _p 245 | 246 | if self.stop(): return 247 | if self.is_set_going_on(): 248 | aset = self._sets[-1] 249 | try: 250 | p1 = self.player(aset._p1) 251 | except: 252 | self.msg("One of the set players was not found on the server. Terminating set...") 253 | aset.terminate(aset._p1) 254 | return 255 | try: 256 | p2 = self.player(aset._p2) 257 | except: 258 | self.msg("One of the set players was not found on the server. Terminating set...") 259 | aset.terminate(aset._p2) 260 | return 261 | 262 | 263 | # for each player of the set, see if they are in 'free' 264 | # otherwise replace them by an unwanted player 265 | for p in [p1, p2]: 266 | if p.team != "free": 267 | if self.game.state == "in_progress": self.abort() 268 | unwanted = grab_unwanted(self.teams(), p1.steam_id, p2.steam_id) 269 | if unwanted: self.switch(unwanted, p) 270 | else: p.put('free') 271 | 272 | # If there are planned maps # todo mappoolfile and test maps 273 | if aset._maps and len(aset._maps) >= aset.games_played(): 274 | planned_map = self.ra3_ql(aset._maps[aset.games_played()]) 275 | if planned_map == self.game.map: return 276 | self.msg("^4Set of {}^7: Next planned map is: {}".format(aset._matches, planned_map)) 277 | if (planned_map != mapname) and (planned_map in self.supported_maps): 278 | cvmap(planned_map, aset) 279 | 280 | @minqlx.delay(5) 281 | def handle_player_connect(self, player): 282 | 283 | @minqlx.delay(1) 284 | def delay(): 285 | player.tell("This server supports ^4sets^7. Type ^2!setcmds^7 for the commands, and ^2!suggest^7 or ^2!complain^7 anonymously.") 286 | 287 | if self.stop(): return 288 | 289 | if self.is_set_going_on(): 290 | aset = self._sets[-1] 291 | if self.game.state == "in_progress": 292 | played = aset.games_played() 293 | reserved = aset.reserved() 294 | player.tell("^5Hello {}^5! Current players are playing game {} of a set of {}.".format(player.name, played+1, reserved)) 295 | player.center_print("Players are currently in a set ({}/{})".format(played+1, reserved)) 296 | else: 297 | nmore = aset.reserved() - aset.games_played() 298 | player.tell("^5Hello {}^5! Current players will be playing {} more games in their set.".format(player.name, nmore)) 299 | player.center_print("Players will play {} more games in their set.".format(nmore)) 300 | 301 | player.tell("^5You can stay and watch, but you won't be able to play until they are done.") 302 | else: 303 | delay() 304 | 305 | def handle_player_disconnect(self, player, reason): 306 | if self.stop(): return 307 | 308 | if self.is_set_going_on(): 309 | aset = self._sets[-1] 310 | if player.steam_id in aset.players(): 311 | winner = aset._p1 if player.steam_id == aset._p2 else aset._p2 312 | aset.terminate(player.steam_id) 313 | self.msg("^4Set of {}^7: Player {}^7 disconnected, terminating the set. Winner: {}".format(aset._matches, player.name, self.player(winner).name)) 314 | 315 | @minqlx.delay(1) 316 | def handle_game_countdown(self): 317 | if self.stop(): return 318 | 319 | if self.is_set_going_on(): 320 | aset = self._sets[-1] 321 | 322 | teams = self.teams() 323 | 324 | if list(map(lambda p: p.steam_id, teams['free'])) != aset.players(): 325 | self.abort() 326 | self.msg("Aborted game because there is a set going on between other players.") 327 | #self.handle_map(self.game.map, self.game.factory) 328 | return 329 | 330 | played = aset.games_played() 331 | reserved = aset.reserved() 332 | planned_map = None 333 | if aset._maps and len(aset._maps) > played: 334 | planned_map = self.ra3_ql(aset._maps[played]) 335 | 336 | if planned_map and (planned_map != self.game.map): 337 | m = "^4Set of {}^7: Starting game {} on ^3{}^7 while planned map was ^3{}^7." 338 | self.msg(m.format(reserved, played+1, self.game.map, planned_map)) 339 | return 340 | 341 | self.msg("^4Set of {}^7: Starting game {}. Good luck!".format(reserved, played+1)) 342 | 343 | 344 | def handle_game_end(self, data): 345 | if self.stop(): return 346 | 347 | if self.is_set_going_on(): 348 | aset = self._sets[-1] 349 | p1 = self.player(aset._p1) 350 | p2 = self.player(aset._p2) 351 | aset.add_score(self.game.map, p1.stats.score,p2.stats.score) 352 | if aset.is_ended(): 353 | winner = aset.winner() 354 | self.msg("{}".format(aset)) 355 | if winner: 356 | self.msg("^4Set of {}^7: ended in victory for: {}, winning {} more games.".format(aset._matches, self.player(winner['winner']), winner['victories'])) 357 | else: 358 | self.msg("^4Set of {}^7: ended in a draw ({0} - {0}).".format(aset._matches, aset.is_started()/2)) 359 | else: 360 | p = p1 if p1.stats.score > p2.stats.score else p2 361 | played = aset.games_played() 362 | total = aset.reserved() 363 | tied = "" if aset.winner() else " (Set is tied!)" 364 | self.msg("^4Set of {}^7: Map {} goes to {}!{}".format(aset._matches, played, p.name, tied)) 365 | 366 | 367 | # COMMANDS 368 | def cmd_maps(self, player, msg, channel): 369 | if self.stop(): 370 | channel.reply("Duel gametype is needed for this command.") 371 | return 372 | 373 | if not self.is_set_going_on(): 374 | channel.reply("^4There is no set going on...") 375 | return 376 | 377 | aset = self._sets[-1] 378 | 379 | if len(msg) < 2: # get maps 380 | 381 | played_maps = "^7none" 382 | if aset.is_started(): 383 | played_maps = "^7, ^2".join(list(map(lambda s: s['map'], aset._scores))) 384 | 385 | planned_maps = "^7none" 386 | if aset._maps: 387 | planned_maps = "^7, ^3".join(aset._maps) 388 | 389 | channel.reply("^4Set of {}^7: Played: ^2{}^7. Planned: ^3{}".format(aset._matches, played_maps, planned_maps)) 390 | return 391 | 392 | else: # set maps 393 | 394 | if not player.steam_id in aset.players(): 395 | player.tell("Only players of the set can plan the map picks.") 396 | return minqlx.RET_STOP_ALL 397 | 398 | for _map in msg[1:]:#aset._matches+1-aset.is_started()]: 399 | if self.ra3_ql(_map) in self.supported_maps: continue 400 | player.tell("^1Error^7: map {} not found.".format(_map)) 401 | return minqlx.RET_STOP_ALL 402 | 403 | aset._maps = msg[1:]#aset._matches+1-aset.is_started()] 404 | reserved = aset.reserved() 405 | played = aset.games_played() 406 | channel.reply("^4Set of {}^7: Maps succesfully planned!".format(reserved)) 407 | 408 | if self.game.state != "in_progress" and aset._maps and len(aset._maps) > played: 409 | planned_map = self.ra3_ql(aset._maps[played]) 410 | if planned_map != self.game.map: 411 | self._id_to_pass_vote = aset.players() 412 | self.callvote("map {}".format(planned_map),"^4Set of {}^7: Go to next set map {}?".format(aset._matches, planned_map)) 413 | 414 | def cmd_forfeit(self, player, msg, channel): 415 | if self.stop(): 416 | channel.reply("Duel gametype is needed for this command.") 417 | return 418 | 419 | if self.is_set_going_on(): 420 | aset = self._sets[-1] 421 | if player.steam_id in aset.players(): 422 | aset.terminate(player.steam_id) 423 | self.msg("Set forfeitted by {}".format(player.name)) 424 | player.put('spectator') 425 | else: 426 | player.tell("Only players from the set can forfeit.") 427 | return minqlx.RET_STOP_ALL 428 | else: 429 | player.tell("There is no set going on!") 430 | return minqlx.RET_STOP_ALL 431 | 432 | def cmd_set_cmds(self, player, msg, channel): 433 | player.tell("^6Sets commands for the set plugin:\n") 434 | player.tell("^2!set/reserve [n]^7: start a set of n games. (Shows status if no n given)") 435 | player.tell("^2!setstatus^7: show some information about a set") 436 | player.tell("^2!setmaps [m1 m2 ...]^7: plan the maps for the set") 437 | player.tell("^2!forfeit^7: forfeit a set") 438 | player.tell("^2!lastset^7: show information about the last set") 439 | player.tell("^2!setnext/!nextmap^7: callvotes the next planned map") 440 | channel.reply("[See console] Set commands: !set/!reserve, !setstatus, !setmaps, !setnext/!nextmap, !lastset, !forfeit") 441 | 442 | 443 | def cmd_set_next(self, player, msg, channel): 444 | if self.stop(): 445 | channel.reply("Duel gametype is required for this command.") 446 | return 447 | 448 | if not self.is_set_going_on(): 449 | channel.reply("There is no set going on. Start one with !set ") 450 | return minqlx.RET_STOP_ALL 451 | 452 | else: 453 | aset = self._sets[-1] 454 | 455 | if not player.steam_id in aset.players(): 456 | channel.reply("Only players in the set can call this command.") 457 | return minqlx.RET_STOP_ALL 458 | 459 | played = aset.played_games() 460 | maps = aset._maps 461 | 462 | if aset._maps: 463 | if len(aset._maps) > played: 464 | planned_map = self.ra3_ql(aset._maps[played]) 465 | if planned_map == self.game.map: 466 | channel.reply("You are already on the next map of your set.") 467 | return 468 | self._id_to_pass_vote = aset.players() 469 | self.callvote("map {}".format(planned_map),"^4Set of {}^7: Go to next map {}?".format(aset._matches, planned_map)) 470 | return 471 | channel.reply("^4Set of {}^7: No map chosen for match # {}.".format(aset._matches, played+1)) 472 | return 473 | 474 | 475 | def cmd_set_status(self, player, msg, channel): 476 | if self.stop(): 477 | channel.reply("Duel gametype is required for this command.") 478 | return 479 | 480 | if self.is_set_going_on(): 481 | aset = self._sets[-1] 482 | if not aset.is_started(): 483 | plan = (" Planned: "+",".join(aset._maps)) if aset._maps else "" 484 | channel.reply("^4Set of {}^7: No maps played yet.{}".format(aset.reserved(), plan)) 485 | return 486 | if aset.is_ended(): 487 | winner = aset.winner() 488 | channel.reply("{}".format(aset)) 489 | if winner: 490 | channel.reply("^4Set of {}^7: ended in victory for: {}, winning {} more game{}.".format(aset._matches, self.player(winner['winner']), winner['victories'], "s" if winner['victories'] > 1 else "")) 491 | else: 492 | channel.reply("^4Set of {}^7: ended in a draw.".format(aset._matches)) 493 | else: 494 | winner = aset.winner() 495 | channel.reply("{}".format(aset)) 496 | if winner: 497 | channel.reply("^4Set of {}^7: {} is in the lead, up by {}.".format(aset._matches, self.player(winner['winner']), winner['victories'])) 498 | else: 499 | channel.reply("^4Set of {}^7: is currently tied {0} to {0}.".format(aset._matches, aset.is_started()/2)) 500 | 501 | else: 502 | m = " Tip: !lastset shows last set info." if self._sets else "" 503 | channel.reply("^4There is no set going on at the moment." + m) 504 | 505 | def cmd_last_set(self, player, msg, channel): 506 | if self.stop(): 507 | return 508 | 509 | sets = self._sets.copy() 510 | sets.reverse() 511 | lastset = None 512 | 513 | for aset in sets: 514 | if aset.is_ended(): 515 | lastset = aset 516 | break 517 | 518 | if not lastset: 519 | channel.reply("^4There is no last set in memory...") 520 | return 521 | 522 | aset = lastset 523 | winner = aset.winner() 524 | channel.reply("{}".format(aset)) 525 | if winner: 526 | self.msg("^4Set of {}^7: ended in victory for: {}, winning {} more game{}.".format(aset._matches, self.player(winner['winner']), winner['victories'], "s" if winner['victories'] > 1 else "")) 527 | else: 528 | self.msg("^4Set of {}^7: ended in a draw ({0} to {0}).".format(aset._matches, aset.is_started()/2)) 529 | 530 | 531 | 532 | def cmd_reserve(self, player, msg, channel): 533 | if self.stop(): 534 | channel.reply("Duel gametype is required for this command.") 535 | return 536 | 537 | if len(msg) < 2: 538 | self.cmd_set_status(player, None, channel) 539 | return 540 | 541 | if player.team != "free": 542 | player.tell("You cannot call this command from a spectator position.") 543 | return minqlx.RET_STOP_ALL 544 | 545 | 546 | if len(self.teams()['free']) <= 1: 547 | player.tell("Two players are needed to start a set.") 548 | return minqlx.RET_STOP_ALL 549 | 550 | if self.is_set_going_on(): 551 | aset = self._sets[-1] 552 | if player.steam_id in aset.players(): 553 | channel.reply("You are already playing a set.") 554 | return 555 | else: 556 | channel.reply("Other players are already playing a set...") 557 | return 558 | 559 | else: 560 | try: 561 | n = int(msg[1]) 562 | assert 0 < n <= self.get_cvar(CVAR_MAX, int) 563 | assert self.is_odd(n) 564 | except: 565 | return minqlx.RET_USAGE 566 | 567 | t = self.teams()['free'] 568 | if player.steam_id == t[0].steam_id: 569 | self._id_to_pass_vote.append(t[1].steam_id) 570 | else: 571 | self._id_to_pass_vote.append(t[0].steam_id) 572 | self.callvote("qlx !startset {} {} {}".format(t[0].steam_id, t[1].steam_id, n), "Set of {} games for {}^3 and {}^3".format(n, t[0].name, t[1].name)) 573 | 574 | def cmd_start_set(self, player, msg, channel): 575 | if self.stop(): return 576 | 577 | if len(msg) < 4: return 578 | 579 | try: 580 | set_p1 = self.player(int(msg[1])) 581 | set_p2 = self.player(int(msg[2])) 582 | set_matches = int(msg[3]) 583 | except: 584 | channel.reply("^1Could not find required players to initialize set.") 585 | return 586 | 587 | new_set = myset(self, set_p1, set_p2, set_matches) 588 | self._sets.append(new_set) 589 | setmsg = "^4Set of {}^7: Reserved! ".format(set_matches) 590 | self.msg(setmsg + "You may ^2F3^7 or plan maps with ^2!setmaps map1 map2 ...") 591 | self.center_print(setmsg + "Ready up, or ^2!setmaps") 592 | 593 | @minqlx.delay(3) 594 | def gogo(): 595 | for _p in self.players(): 596 | _p.tell("\n\n^3A ^4set ^3has started. If you have any suggestions or complaints on this new plugin, they can be sent via ^2!suggest^3 or ^2!complain^3.\n\n") 597 | 598 | gogo() 599 | 600 | 601 | # CLASS HELPER FUNCTIONS 602 | def is_set_going_on(self): 603 | if not self._sets: return False 604 | return not self._sets[-1].is_ended() 605 | 606 | def stop(self): 607 | return not(self.game and self.game.type_short == "duel") 608 | 609 | def ra3_ql(self, mapname): 610 | mapdict = {'dm6': 'campgrounds', 611 | 'dm17': 'longestyard', 612 | 'dm14': 'grimdungeons', 613 | 'dm12': 'dredwerkz', 614 | 'dm16': 'cobaltstation', 615 | 't5': 'fatalinstinct', 616 | 'dm2': 'spillway', 617 | 'dm13': 'lostworld', 618 | 'dm15': 'demonkeep', 619 | 't2': 'provinggrounds', 620 | 't4': 'verticalvengeance', 621 | 'dm1': 'arenagate', 622 | 'dm18': 'spacechamber', 623 | 'dm10': 'namelessplace', 624 | 'dm4': 'eviscerated', 625 | 'dm11': 'chemicalreaction', 626 | 'dm5': 'forgotten', 627 | 'dm3': 'hearth', 628 | 'dm8': 'brimstoneabbey', 629 | 'dm7': 'retribution', 630 | 'dm9': 'heroskeep', 631 | 'dm0': 'introduction', 632 | 'dm19': 'terminalheights', 633 | 't3': 'hellsgate', 634 | 't6': 'beyondreality', 635 | 't1': 'powerstation', 636 | 't7': 'furiousheights', 637 | 'ztn': 'bloodrun', 638 | 'bf': 'battleforged', 639 | } 640 | if mapname in mapdict: 641 | return mapdict[mapname] 642 | return mapname 643 | -------------------------------------------------------------------------------- /specprotect.py: -------------------------------------------------------------------------------- 1 | # This is a plugin created by iouonegirl(@gmail.com) 2 | # Copyright (c) 2016 iouonegirl 3 | # https://github.com/dsverdlo/minqlx-plugins 4 | # 5 | # You are free to modify this plugin to your custom, 6 | # except for the version command related code. 7 | # 8 | # This plugin protects spectators from being targeted 9 | # by kick callvotes. 10 | # 11 | # Uses: 12 | 13 | 14 | import minqlx 15 | import threading 16 | import time 17 | import os 18 | import requests 19 | 20 | VERSION = "v0.1" 21 | 22 | # This code makes sure the required superclass is loaded automatically 23 | try: 24 | from .iouonegirl import iouonegirlPlugin 25 | except: 26 | try: 27 | abs_file_path = os.path.join(os.path.dirname(__file__), "iouonegirl.py") 28 | res = requests.get("https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/iouonegirl.py") 29 | if res.status_code != requests.codes.ok: raise 30 | with open(abs_file_path,"a+") as f: f.write(res.text) 31 | from .iouonegirl import iouonegirlPlugin 32 | except Exception as e : 33 | minqlx.CHAT_CHANNEL.reply("^1iouonegirl abstract plugin download failed^7: {}".format(e)) 34 | raise 35 | 36 | 37 | class specprotect(iouonegirlPlugin): 38 | def __init__(self): 39 | super().__init__(self.__class__.__name__, VERSION) 40 | 41 | # CVARS 42 | 43 | # HOOKS 44 | self.add_hook("vote_called", self.handle_vote_called) 45 | 46 | # COMMANDS 47 | 48 | # Instance variables 49 | 50 | def handle_vote_called(self, caller, vote, args): 51 | # If it is not vote, whatever 52 | if not vote.lower() in ["kick", "clientkick"]: return 53 | if not args: return 54 | 55 | try: 56 | target = self.player(int(args)) 57 | except ValueError: 58 | target = self.player(args) 59 | 60 | if target and target.team == "spectator": 61 | caller.tell("^3Server^7: spectators cannot be kicked on this server.") 62 | return minqlx.RET_STOP_ALL 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /translate.py: -------------------------------------------------------------------------------- 1 | # This is a plugin created by iouonegirl(@gmail.com) 2 | # Copyright (c) 2016 iouonegirl 3 | # https://github.com/dsverdlo/minqlx-plugins 4 | # 5 | # You are free to modify this plugin to your custom, 6 | # except for the version command related code. 7 | # 8 | # It tracksprovides commands to translate words and sentences, 9 | # look up definitions, and much more 10 | # 11 | # To use the translation functions, please make a free account 12 | # on yandex.net and get an API key, which you'll set as a cvar. 13 | # 14 | # Uses 15 | # set qlx_translate_api_key "apikey1337thisisnotanactualapikey69" 16 | # ^ get your key at: https://tech.yandex.com/keys/get/?service=trnsl 17 | 18 | import minqlx 19 | import os 20 | import time 21 | import re 22 | import random 23 | import threading 24 | import requests 25 | 26 | VERSION = "v0.14" 27 | 28 | 29 | 30 | # This code makes sure the required superclass is loaded automatically 31 | try: 32 | from .iouonegirl import iouonegirlPlugin 33 | except: 34 | try: 35 | abs_file_path = os.path.join(os.path.dirname(__file__), "iouonegirl.py") 36 | res = requests.get("https://raw.githubusercontent.com/dsverdlo/minqlx-plugins/master/iouonegirl.py") 37 | if res.status_code != requests.codes.ok: raise 38 | with open(abs_file_path,"a+") as f: f.write(res.text) 39 | from .iouonegirl import iouonegirlPlugin 40 | except Exception as e : 41 | minqlx.CHAT_CHANNEL.reply("^1iouonegirl abstract plugin download failed^7: {}".format(e)) 42 | raise 43 | 44 | # customizable vars 45 | C = "^4" 46 | DEFAULT_LANG = "en" 47 | SERVER_DEFAULT = "en" 48 | AUTO_TRANS_COMMANDS = False 49 | 50 | PLAYER_KEY = "minqlx:players:{}" 51 | LANG_KEY = PLAYER_KEY + ":language" 52 | AUTO_KEY = PLAYER_KEY + ":autotranslate" 53 | 54 | LANGS = { 55 | 'Albanian': 'sq', 56 | 'Armenian': 'hy', 57 | 'Azerbaijani': 'az', 58 | 'Belarusian': 'be', 59 | 'Bulgarian': 'bg', 60 | 'Catalan': 'ca', 61 | 'Croatian': 'hr', 62 | 'Czech': 'cs', 63 | 'Danish': 'da', 64 | 'Dutch': 'nl', 65 | 'English': 'en', 66 | 'Estonian': 'et', 67 | 'Finnish': 'fi', 68 | 'French': 'fr', 69 | 'German': 'de', 70 | 'Greek': 'el', 71 | 'Hungarian': 'hu', 72 | 'Italian': 'it', 73 | 'Latvian': 'lv', 74 | 'Lithuanian': 'lt', 75 | 'Macedonian': 'mk', 76 | 'Norwegian': 'no', 77 | 'Polish': 'pl', 78 | 'Portuguese': 'pt', 79 | 'Romanian': 'ro', 80 | 'Russian': 'ru', 81 | 'Serbian': 'sr', 82 | 'Slovak': 'sk', 83 | 'Slovenian': 'sl', 84 | 'Spanish': 'es', 85 | 'Swedish': 'sv', 86 | 'Turkish': 'tr', 87 | 'Ukrainian': 'uk'} 88 | 89 | yandex = [ 90 | "az-ru","be-bg","be-cs","be-de","be-en","be-es","be-fr","be-it","be-pl", 91 | "be-ro","be-ru","be-sr","be-tr","bg-be","bg-ru","bg-uk","ca-en","ca-ru", 92 | "cs-be","cs-en","cs-ru","cs-uk","da-en","da-ru","de-be","de-en","de-es", 93 | "de-fr","de-it","de-ru","de-tr","de-uk","el-en","el-ru","en-be","en-ca", 94 | "en-cs","en-da","en-de","en-el","en-es","en-et","en-fi","en-fr","en-hu", 95 | "en-it","en-lt","en-lv","en-mk","en-nl","en-no","en-pt","en-ru","en-sk", 96 | "en-sl","en-sq","en-sv","en-tr","en-uk","es-be","es-de","es-en","es-ru", 97 | "es-uk","et-en","et-ru","fi-en","fi-ru","fr-be","fr-de","fr-en","fr-ru", 98 | "fr-uk","hr-ru","hu-en","hu-ru","hy-ru","it-be","it-de","it-en","it-ru", 99 | "it-uk","lt-en","lt-ru","lv-en","lv-ru","mk-en","mk-ru","nl-en","nl-ru", 100 | "no-en","no-ru","pl-be","pl-ru","pl-uk","pt-en","pt-ru","ro-be","ro-ru", 101 | "ro-uk","ru-az","ru-be","ru-bg","ru-ca","ru-cs","ru-da","ru-de","ru-el", 102 | "ru-en","ru-es","ru-et","ru-fi","ru-fr","ru-hr","ru-hu","ru-hy","ru-it", 103 | "ru-lt","ru-lv","ru-mk","ru-nl","ru-no","ru-pl","ru-pt","ru-ro","ru-sk", 104 | "ru-sl","ru-sq","ru-sr","ru-sv","ru-tr","ru-uk","sk-en","sk-ru","sl-en", 105 | "sl-ru","sq-en","sq-ru","sr-be","sr-ru","sr-uk","sv-en","sv-ru","tr-be", 106 | "tr-de","tr-en","tr-ru","tr-uk","uk-bg","uk-cs","uk-de","uk-en","uk-es", 107 | "uk-fr","uk-it","uk-pl","uk-ro","uk-ru","uk-sr","uk-tr"] 108 | 109 | TAGS = {v: k for k, v in LANGS.items()} 110 | 111 | 112 | class translate(iouonegirlPlugin): 113 | def __init__(self): 114 | super().__init__(self.__class__.__name__, VERSION) 115 | 116 | self.set_cvar_once("qlx_translate_api_key", "") 117 | 118 | self.add_command("urban", self.cmd_urban, usage="|") 119 | #self.add_command(("leet", "1337", "l33t"), self.cmd_leet, usage="|") 120 | self.add_command("languages", self.cmd_languages) 121 | self.add_command("translations", self.cmd_translations) 122 | self.add_command(("translate-last", "trans-last", "translast", "translatelast"), self.cmd_translate_last, usage=" ") 123 | self.add_hook("player_disconnect", self.handle_player_disconnect) 124 | self.buffer = {} 125 | 126 | self.add_hook("chat", self.handle_chat) 127 | self.add_command(("translate", "trans"), self.cmd_translate, usage=" ") 128 | self.add_command(("stranslate", "strans"), self.cmd_silent_translate, usage=" ") 129 | #self.add_command(("define", "def", "definition"), self.cmd_define, usage="") 130 | ## self.add_command(("lang", "language"), self.cmd_language, usage="[|]") 131 | ## self.add_command(("autotrans", "autotranslate"), self.cmd_auto_translate) 132 | self.add_command(("translatecmds", "transcmds", "transcommands", "translatecommands"), self.cmd_commands) 133 | self.add_command(("transexamples", "translateexamples"), self.cmd_examples) 134 | if not self.get_cvar("qlx_translate_api_key"): 135 | self.msg("No Yandex API key set. Get one for free: https://tech.yandex.com/keys/get/?service=trnsl") 136 | 137 | def handle_chat(self, player, msg, channel): 138 | if channel != "chat": return 139 | 140 | #line = " ".join(msg) 141 | if msg[0] == "!": return 142 | 143 | if not player.steam_id in self.buffer: 144 | self.buffer[player.steam_id] = [] 145 | 146 | self.buffer[player.steam_id].append(msg) 147 | 148 | if len(self.buffer[player.steam_id]) > 3: 149 | self.buffer[player.steam_id].pop(0) 150 | 151 | 152 | ## def callback(player, query, results): 153 | ## def callback_autotrans(_p, _q, _r): 154 | ## _j = _r.json() 155 | ## _lang = _j['lang'] 156 | ## _text = _j['text'][0] 157 | ## _p.tell("^6AutoTrans^7({}){}: {}".format(_lang, C, _text)) 158 | ## 159 | ## # Okay we received a lang tag for the chat message. check if anyone needs a translation 160 | ## json = results.json() 161 | ## l = json['lang'] 162 | ## if l != SERVER_DEFAULT: # If the tag is not the server default language... 163 | ## for p in self.players(): 164 | ## # If the message comes from this player, go to next 165 | ## if p.id == player.id: continue 166 | ## # If this player doesnt want auto trans, go to next 167 | ## if not self.help_get_auto_pref(p): continue 168 | ## # If this is the say_team from another team, go to next 169 | ## if channel == minqlx.RED_TEAM_CHAT_CHANNEL and p.team != 'red': continue 170 | ## if channel == minqlx.BLUE_TEAM_CHAT_CHANNEL and p.team != 'blue': continue 171 | ## # if this is the default language of the player, go to next 172 | ## pref_tag = self.help_get_lang_tag(p) 173 | ## if pref_tag == l: continue 174 | ## # Go fetch the translation 175 | ## url = 'https://translate.yandex.net/api/v1.5/tr.json/translate' 176 | ## params = {'key':'trnsl.1.1.20160215T152939Z.eb62d98148b07bcd.911d314592dc12c39c3be65184da90198998225c', 177 | ## 'text':query, 'lang':pref_tag} 178 | ## hdr = None 179 | ## self.help_fetch(p, query, url, hdr, params, callback_autotrans) 180 | ## 181 | ## if not player: return 182 | ## if not msg: return 183 | ## 184 | ## # If you don't want the server to translate commands to people 185 | ## if not AUTO_TRANS_COMMANDS: 186 | ## # check if a command was spoken 187 | ## if msg[0].startswith("!"): return 188 | ## 189 | ## if not self.get_cvar("qlx_translate_api_key"): 190 | ## self.msg("^7No yandex.net API key found. Cannot translate without one...") 191 | ## return 192 | ## 193 | ## url = 'https://translate.yandex.net/api/v1.5/tr.json/detect' 194 | ## params = {'key':self.get_cvar("qlx_translate_api_key"), 195 | ## 'text':msg} 196 | ## hdr = None 197 | ## 198 | ## # Fetch the language of the msg 199 | ## self.help_fetch(player, msg, url, hdr, params, callback) 200 | 201 | 202 | 203 | def cmd_examples(self, player, msg, channel): 204 | player.tell("^6Translation example commands:\n") 205 | player.tell("^2!translate ru What is love? ^3(tr to russian)") 206 | player.tell("^2!stranslate fr No one will see this ^3(silent translation)") 207 | player.tell("^2!translate nl-en Boom ^3(force origin language)") 208 | player.tell("^2!translate-last en cooller ^3(tr last 3 cooller msgs to english)") 209 | 210 | 211 | def cmd_commands(self, player, msg, channel): 212 | channel.reply("^7TranslationCommands: ^2!languages^7, ^2!translations^7, ^2!urban^7, ^2!translate^7, ^2!stranslate^7, ^2!translate-last") 213 | 214 | def cmd_silent_translate(self, player, msg, channel): 215 | #player.tell("^6TranslateRequest: ^2{}".format(msg[1:])) 216 | self.cmd_translate(player, msg, channel, True) 217 | return minqlx.RET_STOP_ALL 218 | 219 | def cmd_translate(self, player, msg, channel, silent=False): 220 | def callback(_player, _query, _results): 221 | _res = _results.json() 222 | translated = _res['text'][0] 223 | output = "^7({}): {}{}".format(_res['lang'], C, translated) 224 | if silent: 225 | player.tell("^6Psst: " + output) 226 | return minqlx.RET_STOP_ALL 227 | if self.help_be_quiet(player): 228 | minqlx.SPECTATOR_CHAT_CHANNEL.reply(output) 229 | else: 230 | channel.reply(output) 231 | 232 | 233 | if len(msg) < 3: 234 | return minqlx.RET_USAGE 235 | 236 | if "-" in msg[1].lower(): 237 | if msg[1].lower() in yandex: 238 | to = msg[1] 239 | else: 240 | player.tell("^6Translation ({}) not supported... Try ^2!translations ^6for a list.".format(msg[1])) 241 | return minqlx.RET_STOP_ALL 242 | elif msg[1].lower() in TAGS: 243 | to = msg[1] 244 | elif msg[1].title() in LANGS: 245 | to = LANGS[msg[1].title()] 246 | else: 247 | matches = [] 248 | for lang in LANGS: 249 | if msg[1] in lang.lower(): 250 | matches.append([lang, LANGS[lang]]) 251 | if not matches: 252 | player.tell("^6No languages matched {}... Try ^2!languages ^6for a list.".format(msg[1])) 253 | return minqlx.RET_STOP_ALL 254 | elif len(matches) == 1: 255 | lang, tag = matches[0] 256 | to = tag 257 | else: 258 | _map = map(lambda pair: "{}-{}".format(pair[0], pair[1]), matches) 259 | player.tell("^6Multiple matches found: ^7" + ", ".join(list(_map))) 260 | return minqlx.RET_STOP_ALL 261 | 262 | 263 | message = " ".join(msg[2:]) 264 | if not self.get_cvar("qlx_translate_api_key"): 265 | player.tell("^7No yandex.net API key installed. Please contact server admin.") 266 | return minqlx.RET_STOP_ALL 267 | 268 | url = 'https://translate.yandex.net/api/v1.5/tr.json/translate' 269 | params = {'key':self.get_cvar("qlx_translate_api_key"), 270 | 'text':message, 271 | 'lang':to} 272 | hdr = None 273 | 274 | self.help_fetch(player, message, url, hdr, params, callback) 275 | 276 | def cmd_translate_last(self, player, msg, channel, silent=False): 277 | @minqlx.next_frame 278 | def callback(_player, _query, _results): 279 | _res = _results.json() 280 | translated = _res['text'][0] 281 | output = "^7({}): {}{}".format(_res['lang'], C, translated) 282 | player.tell(output) 283 | return minqlx.RET_STOP_ALL 284 | 285 | @minqlx.thread 286 | def fetch_intervals(lst): 287 | for message in lst: 288 | url = 'https://translate.yandex.net/api/v1.5/tr.json/translate' 289 | params = {'key':self.get_cvar("qlx_translate_api_key"), 290 | 'text':message, 291 | 'lang':to} 292 | hdr = None 293 | self.help_fetch(player, message, url, hdr, params, callback) 294 | time.sleep(0.8) 295 | 296 | if len(msg) < 3: 297 | return minqlx.RET_USAGE 298 | 299 | if "-" in msg[1].lower(): 300 | if msg[1].lower() in yandex: 301 | to = msg[1] 302 | else: 303 | player.tell("^6Translation ({}) not supported... Try ^2!translations ^6for a list.".format(msg[1])) 304 | return minqlx.RET_STOP_ALL 305 | elif msg[1].lower() in TAGS: 306 | to = msg[1] 307 | elif msg[1].title() in LANGS: 308 | to = LANGS[msg[1].title()] 309 | else: 310 | matches = [] 311 | for lang in LANGS: 312 | if msg[1] in lang.lower(): 313 | matches.append([lang, LANGS[lang]]) 314 | if not matches: 315 | player.tell("^6No languages matched {}... Try ^2!languages ^6for a list.".format(msg[1])) 316 | return minqlx.RET_STOP_ALL 317 | elif len(matches) == 1: 318 | lang, tag = matches[0] 319 | to = tag 320 | else: 321 | _map = map(lambda pair: "{}-{}".format(pair[0], pair[1]), matches) 322 | player.tell("^6Multiple matches found: ^7" + ", ".join(list(_map))) 323 | return minqlx.RET_STOP_ALL 324 | 325 | 326 | target_player = self.find_by_name_or_id(player, msg[2]) 327 | if not target_player: 328 | return minqlx.RET_STOP_ALL 329 | # move up 330 | if not target_player.steam_id in self.buffer or not self.buffer[target_player.steam_id]: 331 | player.tell("Translate error: No chat buffered from {}.".format(target_player.name)) 332 | 333 | if not self.get_cvar("qlx_translate_api_key"): 334 | player.tell("^7No yandex.net API key installed. Cannot translate without one...") 335 | return minqlx.RET_STOP_ALL 336 | 337 | fetch_intervals(self.buffer[target_player.steam_id]) 338 | return minqlx.RET_STOP_ALL 339 | 340 | 341 | 342 | 343 | 344 | ## def cmd_language(self, player, msg, channel): 345 | ## # if no arguments given, just check the language 346 | ## if len(msg) < 2: 347 | ## if self.help_get_auto_pref(player): 348 | ## tag = self.help_get_lang_tag(player) 349 | ## lang = TAGS.get(tag, DEFAULT_LANG) 350 | ## channel.reply("^7Your default language is: ^6{}^7({}). Use ^2!lang^7 to change it.".format(lang, tag)) 351 | ## return 352 | ## else: 353 | ## channel.reply("^7AutoTranslation is turned off. Activate with !autotrans or !language X") 354 | ## return 355 | ## # otherwise try to set a new language 356 | ## else: 357 | ## lang = TAGS.get(msg[1].lower()) # try correct tag 358 | ## if lang: 359 | ## self.help_set_lang_tag(player, msg[1].lower()) 360 | ## channel.reply("^7AutoTranslate language changed to: ^6{}^7({}).".format(lang, msg[1])) 361 | ## self.help_change_auto_pref(player, 1) 362 | ## return 363 | ## else: # try every language for a match 364 | ## maybe = [] 365 | ## for lang in LANGS: 366 | ## if msg[1].title() in lang: 367 | ## maybe.append([lang, LANGS[lang]]) 368 | ## if not maybe: 369 | ## player.tell("^6No languages matched {}... Try ^2!languages ^6for a list.".format(msg[1])) 370 | ## return minqlx.RET_STOP_ALL 371 | ## elif len(maybe) == 1: 372 | ## lang, tag = maybe[0] 373 | ## self.help_set_lang_tag(player, tag) 374 | ## channel.reply("^7AutoTranslate language changed to: ^6{}^7({}).".format(lang, tag)) 375 | ## self.help_change_auto_pref(player, 1) 376 | ## return 377 | ## else: 378 | ## _map = map(lambda pair: "{}->{}".format(pair[0], pair[1]), maybe) 379 | ## player.tell("^6Multiple matches found: ^7" + ", ".join(list(_map))) 380 | ## return minqlx.RET_STOP_ALL 381 | 382 | 383 | 384 | def cmd_languages(self, player, msg, channel): 385 | _printable = [] 386 | keys = list(LANGS.keys()) 387 | keys.sort() 388 | for i,lang in enumerate(keys): 389 | newline = "" if i % 4 else "\n" 390 | _printable.append("{}^5{}^7: ^4{}".format(newline, lang, LANGS[lang])) 391 | #_printable.sort() 392 | player.tell("^6Supported languages: ^7" + "^7, ".join(_printable)) 393 | 394 | msg = "^7{} can open their console to see all the supported languages.".format(player.name) 395 | if self.help_be_quiet(player): 396 | minqlx.SPECTATOR_CHAT_CHANNEL.reply(msg) 397 | else: 398 | channel.reply(msg) 399 | 400 | 401 | ## def cmd_auto_translate(self, player, msg, channel): 402 | ## # Get the preference 403 | ## old_pref = self.help_get_auto_pref(player) 404 | ## # Change it 405 | ## self.help_change_auto_pref(player) 406 | ## 407 | ## if old_pref: 408 | ## channel.reply("^7{} will stop receiving automatic translations.".format(player.name)) 409 | ## else: 410 | ## tag = self.help_get_lang_tag(player) 411 | ## channel.reply("^7{} activated auto translations in their default language ({}).".format(player.name, tag)) 412 | 413 | def cmd_translations(self, player, msg, channel): 414 | def add_colors(tr): 415 | return "{}{}^7-{}{}".format("^5", tr[0:2], "^4", tr[3:5]) 416 | 417 | _printable = map(add_colors, yandex) 418 | player.tell("^6Supported translations: \n" + "^7, ".join(list(_printable))) 419 | m = "^7{} can open their console to see all the supported translations.".format(player.name) 420 | if self.help_be_quiet(player): 421 | minqlx.SPECTATOR_CHAT_CHANNEL.reply(m) 422 | else: 423 | channel.reply(m) 424 | 425 | 426 | def cmd_urban(self, player, msg, channel): 427 | if len(msg) < 2: 428 | return minqlx.RET_USAGE 429 | 430 | query = " ".join(msg[1:]) 431 | url = 'https://mashape-community-urban-dictionary.p.rapidapi.com/define?term={}'.format(query) 432 | headers = { "X-RapidAPI-Key": "Uj9TzyJ4uumshsTjD5yV3ZxE2JfWp1yib2SjsnJbjRHQJjSjs5" } 433 | 434 | self.help_fetch(player, query, url, headers, None, self.help_callback_urban) 435 | 436 | def cmd_leet(self, player, msg, channel): 437 | if len(msg)< 2: 438 | return minqlx.RET_USAGE 439 | query = "+".join(msg[1:]) 440 | url = 'https://montanaflynn-l33t-sp34k.p.rapidapi.com/encode?text={}'.format(query) 441 | headers = { "X-RapidAPI-Key": "Uj9TzyJ4uumshsTjD5yV3ZxE2JfWp1yib2SjsnJbjRHQJjSjs5" } 442 | self.help_fetch(player, query, url, headers, None, self.help_callback_leet) 443 | return minqlx.RET_STOP_ALL 444 | 445 | @minqlx.thread 446 | def help_fetch(self, player, query, url, headers, params, callback): 447 | @minqlx.next_frame 448 | def error(m): self.msg(m) 449 | res = requests.get(url, headers=headers, params=params) 450 | if res.status_code != requests.codes.ok: 451 | error("^1RequestError^7: code {}.".format(res.status_code)) 452 | else: 453 | callback(player, query, res) 454 | 455 | def help_callback_urban(self, player, query, results): 456 | @minqlx.next_frame 457 | def msg(m): 458 | if self.help_be_quiet(player): 459 | minqlx.SPECTATOR_CHAT_CHANNEL.reply(m) 460 | else: 461 | self.msg(m) 462 | 463 | results = results.json() 464 | if results['result_type'] != "no_results": 465 | first_result = results['list'][0] 466 | definition = first_result['definition'] 467 | example = first_result['example'] 468 | 469 | msg("^5UrbanDef: ^7{}".format(definition)) 470 | if example: 471 | msg("^5UrbanExample: ^7{}".format(example)) 472 | else: 473 | msg("{}No urban dict results found for: ".format(C, query)) 474 | 475 | def help_be_quiet(self, player): 476 | return player.team == "spectator" and self.game.type_short == "duel" and self.game.state == "in_progress" 477 | 478 | def help_callback_leet(self, player, query, results): 479 | self.msg("{}^7: ^2(!leet) {}".format(player.name, results)) 480 | 481 | ## def help_get_auto_pref(self, player): 482 | ## key = AUTO_KEY.format(player.steam_id) 483 | ## if not (key in self.db): self.db[key] = 0 484 | ## return int(self.db[key]) 485 | ## 486 | ## def help_change_auto_pref(self, player, force=0): 487 | ## key = AUTO_KEY.format(player.steam_id) 488 | ## self.db[key] = force or 0 if self.help_get_auto_pref(player) else 1 489 | ## 490 | ## def help_get_lang_tag(self, player): 491 | ## # formulate key 492 | ## key = LANG_KEY.format(player.steam_id) 493 | ## # if no language defined yet, set the default 494 | ## if not (key in self.db): self.help_set_lang_tag(player, DEFAULT_LANG) 495 | ## # return tag 496 | ## return self.db[key] 497 | ## 498 | ## def help_set_lang_tag(self, player, tag): 499 | ## # formulate key 500 | ## key = LANG_KEY.format(player.steam_id) 501 | ## # set it (after a quick test that it exists) 502 | ## if TAGS.get(tag): self.db[key] = tag 503 | 504 | @minqlx.delay(0.3) 505 | def help_delay_msg(self, message): 506 | self.msg(message) 507 | 508 | def handle_player_disconnect(self, player, reason): 509 | if player.steam_id in self.buffer: 510 | del self.buffer[player.steam_id] 511 | --------------------------------------------------------------------------------