11from __future__ import annotations
22
3- from steam . ext . dota2 import Hero
3+ from typing import TYPE_CHECKING , Any , override
44
5- from utils import errors , fuzzy
5+ import steam
6+ from steam .ext .dota2 import Hero , User as Dota2User
7+ from twitchio .ext import commands
8+
9+ try :
10+ from utils import const , errors , fuzzy
11+ except ModuleNotFoundError :
12+ import sys
13+
14+ # Just for lazy testing (in the end of this file);
15+ sys .path .append ("D:/CODE/IreBot/src" )
16+
17+ from utils import const , errors , fuzzy
18+
19+ if TYPE_CHECKING :
20+ from steam .ext .dota2 import ProfileCard
21+
22+ from core import IreContext
23+
24+
25+ __all__ = (
26+ "SteamUserConverter" ,
27+ "SteamUserNotFound" ,
28+ "extract_hero_index" ,
29+ "rank_medal_display_name" ,
30+ )
631
7- __all__ = ("extract_hero_index" ,)
832
933# /* cSpell:disable */
1034HERO_ALIASES = {
35+ # HERO ALIASES.
36+ #
37+ # The list mainly for !profile/!items command so people can just write "!items CM"
38+ # and the bot will send Crystal Maiden's items.
39+ #
40+ # The list includes mostly
41+ # * abbreviations, i.e. "cm";
42+ # * persona names i.e. "wei";
43+ # * dota 1 names , i.e. "traxex"
44+ # * official names, i.e. "beastmaster";
45+ # * short forms of any from above, i.e. "cent";
46+ # * just nicknames, aliases or common names that people sometimes *actually* use for Dota 2 heroes ;
47+ #
48+ # It doesn't include aliases that people don't use
49+ # i.e. nobody calls techies as "Squee, Spleen & Spoon"
50+ #
1151 Hero .Abaddon : ["abaddon" , "aba" ],
1252 Hero .Alchemist : ["alch" , "alchemist" ],
1353 Hero .AncientApparition : ["aa" , "apparition" , "ancient apparition" ],
153193}
154194
155195
196+ class SteamUserNotFound (commands .BadArgument ):
197+ """For when a matching user cannot be found."""
198+
199+ def __init__ (self , argument : str ) -> None :
200+ self .argument = argument
201+ super ().__init__ (f"User { argument !r} not found." , value = argument )
202+
203+
204+ class SteamUserConverter (commands .Converter [Dota2User ]):
205+ """Simple Steam User converter."""
206+
207+ @override
208+ async def convert (self , ctx : IreContext , argument : str ) -> Dota2User :
209+ try :
210+ return await ctx .bot .dota .fetch_user (steam .utils .parse_id64 (argument ))
211+ except steam .InvalidID :
212+ id64 = await steam .utils .id64_from_url (argument )
213+ if id64 is None :
214+ raise SteamUserNotFound (argument ) from None
215+ return await ctx .bot .dota .fetch_user (id64 )
216+ except TimeoutError :
217+ raise SteamUserNotFound (argument ) from None
218+
219+
220+ def rank_medal_display_name (profile_card : ProfileCard ) -> str :
221+ """Get human-readable rank medal string out of player's Dota 2 Profile Card."""
222+ display_name = profile_card .rank_tier .division
223+ if stars := profile_card .rank_tier .stars :
224+ display_name += f" \N{BLACK STAR} { stars } "
225+ if number_rank := profile_card .leaderboard_rank :
226+ display_name += f" #{ number_rank } "
227+ return display_name
228+
229+
230+ def is_allowed_to_add_notable () -> Any :
231+ """Allow !npm add/remove/rename to only be invoked by certain people."""
232+
233+ def predicate (ctx : IreContext ) -> bool :
234+ # Maybe we will edit this to be some proper dynamic database thing;
235+ allowed_ids = (const .UserID .Irene , const .UserID .Aluerie , const .UserID .Xas )
236+ return ctx .chatter .id in allowed_ids
237+
238+ return commands .guard (predicate )
239+
240+
156241def extract_hero_index (argument : str , heroes : list [Hero ]) -> tuple [Hero , int ]:
157242 """Convert command argument provided by user (twitch chatter) into a player_slot in the match.
158243
@@ -169,49 +254,53 @@ def extract_hero_index(argument: str, heroes: list[Hero]) -> tuple[Hero, int]:
169254 Matched hero as well as its index in the provided `heroes` list.
170255 This is because usually when this function is called, the `player slot` is also of a big interest.
171256 """
172- if argument .isdigit ():
257+ if argument .isnumeric ():
173258 # then the user typed only a number and our life is easy because it is a player slot
174259 # let's consider users normal: they start enumerating slots from 1 instead of 0.
175- player_slot = int (argument ) - 1
176- if not 0 <= player_slot <= 9 :
177- msg = "Sorry, player_slot can only be of 1-10 values."
260+ index = int (argument ) - 1
261+ if index < 0 :
262+ msg = f'Detected numeric input " { argument } " but player slot cannot be a negative number.'
178263 raise errors .RespondWithError (msg )
179- return heroes [player_slot ], player_slot
264+
265+ try :
266+ return heroes [index ], index
267+ except IndexError :
268+ msg = f"Detected numeric input for player slot #{ argument } but there are { len (heroes )} players in this match."
269+ raise errors .RespondWithError (msg ) from None
180270
181271 # Otherwise - we have to use the fuzzy search
272+ result : tuple [Hero | None , int ] = (None , 0 )
182273
183- # Step 1. Colors;
184- player_slot_choice = (None , 0 )
274+ # Step 1. Color aliases;
185275 for player_slot , color_aliases in COLOR_ALIASES .items ():
186276 find = fuzzy .extract_one (argument , color_aliases , scorer = fuzzy .quick_token_sort_ratio , score_cutoff = 49 )
187- if find and find [1 ] > player_slot_choice [1 ]:
188- player_slot_choice = (player_slot , find [1 ])
277+ if find and find [1 ] > result [1 ]:
278+ try :
279+ result = (heroes [player_slot ], find [1 ])
280+ except ValueError :
281+ continue
189282
190- # Step 2. let's see if hero aliases can beat official
191- hero_slot_choice = (None , 0 )
192- # Sort the hero list so heroes in the match come first (i.e. so "es" alias triggers on a hero in the match)
283+ # Step 2. Hero aliases
284+ # Sort the hero list so heroes in the match come first (i.e. so "es" alias triggers on a hero in the match first)
193285 for hero , hero_aliases in sorted (HERO_ALIASES .items (), key = lambda x : x [0 ] in heroes , reverse = True ):
194286 find = fuzzy .extract_one (argument , hero_aliases , scorer = fuzzy .quick_token_sort_ratio , score_cutoff = 49 )
195- if find and find [1 ] > hero_slot_choice [1 ]:
196- hero_slot_choice = (hero , find [1 ])
197-
198- error_message = 'Sorry, didn\' t understand your query. Try something like "PA / 7 / Phantom Assassin / Blue".'
199- if player_slot_choice [1 ] > hero_slot_choice [1 ]:
200- # then color matched better
201- player_slot = player_slot_choice [0 ]
202- if player_slot is None :
203- raise errors .RespondWithError (error_message )
204- return heroes [player_slot ], player_slot
205-
206- # Else: hero aliases matched better;
207- hero = hero_slot_choice [0 ]
208- if hero is None :
209- raise errors .RespondWithError (error_message )
210-
211- try :
212- player_slot = heroes .index (hero )
213- except ValueError :
214- msg = f"Hero { hero } is not present in the match."
215- raise errors .RespondWithError (msg ) from None
216-
217- return hero , player_slot
287+ if find and find [1 ] > result [1 ]:
288+ result = (hero , find [1 ])
289+
290+ if result [0 ] is None :
291+ msg = 'Sorry, didn\' t understand your query. Try something like "PA / 7 / Phantom Assassin / Blue".'
292+ raise errors .RespondWithError (msg )
293+ if result [0 ] not in heroes :
294+ msg = f"Hero { result [0 ]} is not present in the match."
295+ raise errors .RespondWithError (msg )
296+
297+ return result
298+
299+
300+ if __name__ == "__main__" :
301+ # A little test.
302+ argument = "PA"
303+ self_heroes = [Hero .PhantomAssassin , Hero .Kez , Hero .KeeperOfTheLight , Hero .Io ]
304+
305+ res = extract_hero_index (argument , self_heroes )
306+ print (res ) # noqa: T201
0 commit comments