diff --git a/configs/config.json.example b/configs/config.json.example index 600201d6e4..b963879223 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -1,7 +1,7 @@ { "websocket_server": false, "heartbeat_threshold": 10, - "enable_social": true, + "enable_social": false, "live_config_update": { "enabled": false, "tasks_only": false @@ -211,6 +211,74 @@ "recycle_force_max": "00:05:00" } }, + { + "type": "Sniper", + "config": { + "enabled": false, + "mode": "social", + "bullets": 1, + "homing_shots": true, + "special_iv": 100, + "order": ["missing", "vip", "priority"], + "sources": [ + { + "enabled": false, + "url": "http://localhost:5000/raw_data", + "key": "pokemons", + "mappings": { + "id": { "param": "pokemon_id" }, + "name": { "param": "pokemon_name" }, + "latitude": { "param": "latitude" }, + "longitude": { "param": "longitude" }, + "expiration": { "param": "disappear_time", "format": "milliseconds" } + } + }, + { + "enabled": false, + "url": "https://pokewatchers.com/grab/", + "mappings": { + "iv": { "param": "iv" }, + "id": { "param": "pid" }, + "name": { "param": "pokemon" }, + "latitude": { "param": "cords" }, + "longitude": { "param": "cords" }, + "expiration": { "param": "timeend", "format": "milliseconds" } + } + }, + { + "enabled": false, + "url": "http://pokesnipers.com/api/v1/pokemon.json", + "key": "results", + "mappings": { + "iv": { "param": "iv" }, + "name": { "param": "name" }, + "latitude": { "param": "coords" }, + "longitude": { "param": "coords" }, + "expiration": { "param": "until", "format": "utc" } + } + } + ], + "catch": { + "Snorlax": 1000, + "Dragonite": 1000, + "Growlithe": 600, + "Clefairy": 500, + "Kabuto": 500, + "Dratini": 500, + "Dragonair": 500, + "Mr. Mime": 500, + "Magmar": 500, + "Electabuzz": 500, + "Tangela": 500, + "Tauros": 500, + "Primeape": 500, + "Chansey": 500, + "Pidgey": 100, + "Caterpie": 100, + "Weedle": 100 + } + } + }, { "type": "CatchPokemon", "config": { @@ -382,16 +450,10 @@ "Any pokemon put here directly force to use Berry & Best Ball to capture, to secure the capture rate": {}, "any": {"catch_above_cp": 1200, "catch_above_iv": 0.9, "logic": "or" }, "Lapras": {}, - "Moltres": {}, - "Zapdos": {}, - "Articuno": {}, "// S-Tier pokemons (if pokemon can be evolved into tier, list the representative)": {}, - "Mewtwo": {}, "Dragonite": {}, "Snorlax": {}, - "// Mew evolves to Mewtwo": {}, - "Mew": {}, "Arcanine": {}, "Vaporeon": {}, "Gyarados": {}, diff --git a/configs/config.json.map.example b/configs/config.json.map.example index dfb85bc391..6b3c16a941 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -468,24 +468,48 @@ { "type": "Sniper", "config": { - "enabled": true, + "enabled": false, "mode": "social", - "bullets": 3, + "bullets": 1, "homing_shots": true, "special_iv": 100, - "order": ["missing", "iv", "priority", "vip", "expiration_timestamp_ms"], + "order": ["missing", "vip", "priority"], "sources": [ { + "enabled": false, "url": "http://localhost:5000/raw_data", "key": "pokemons", "mappings": { - "iv": { "param": "iv" }, "id": { "param": "pokemon_id" }, "name": { "param": "pokemon_name" }, "latitude": { "param": "latitude" }, "longitude": { "param": "longitude" }, "expiration": { "param": "disappear_time", "format": "milliseconds" } } + }, + { + "enabled": false, + "url": "https://pokewatchers.com/grab/", + "mappings": { + "iv": { "param": "iv" }, + "id": { "param": "pid" }, + "name": { "param": "pokemon" }, + "latitude": { "param": "cords" }, + "longitude": { "param": "cords" }, + "expiration": { "param": "timeend", "format": "milliseconds" } + } + }, + { + "enabled": false, + "url": "http://pokesnipers.com/api/v1/pokemon.json", + "key": "results", + "mappings": { + "iv": { "param": "iv" }, + "name": { "param": "name" }, + "latitude": { "param": "coords" }, + "longitude": { "param": "coords" }, + "expiration": { "param": "until", "format": "utc" } + } } ], "catch": { diff --git a/docs/configuration_files.md b/docs/configuration_files.md index fe4834f971..a01b4b3555 100644 --- a/docs/configuration_files.md +++ b/docs/configuration_files.md @@ -736,21 +736,25 @@ This task is an upgrade version of the MoveToMapPokemon task. It will fetch poke ### Options [[back to top](#table-of-contents)] +* `enabled` - Defines whether the **WHOLE** task is enabled or not. Please bear in mind that even if the task is enabled, all or any of its sources can be disabled. (default: false) * `mode` - The mode on which the sniper will fetch the informations. (default: social) - `social` - Information will come from the social network. - `url` - Information will come from one or multiple urls. -* `bullets` - Each bullet corresponds to an attempt of catching a pokemon. (default: 1) -* `homing_shots` - This will ensure that each bullet will catch a target. (default: true) -* `special_iv` - This will skip the catch list if the value is greater than the target's IV. This currently does not work with `social` mode and only works if the given `url` has this information. (default: 100) +* `bullets` - Each bullet corresponds to an **ATTEMPT** of catching a pokemon. (default: 1) +* `homing_shots` - This will ensure that each bullet **will catch** a target. If disabled, a target might not exist and thus it wont be caught. When enabled, this will jump to the next target (if any) and try again to catch it. This will be repeated untill you've spent all the bullets. (default: true) +* `special_iv` - This will skip the catch list if the value is greater than or equal to the target's IV. This currently does not work with `social` mode and only works if the given `url` has this information. (default: 100) * `time_mask` - The time mask used (if `expiration.format` is a full date). The default mask is '%Y-%m-%d %H:%M:%S'. -* `order` - The order on which you want to snipe. This can be one or multiple of the following values (default: [`missing`, `vip`, `threshold`]): - - `iv` - Order by IV, if any. See `min_iv_to_ignore_catch_list`. +* `order` - The order on which you want to snipe. This can be one or multiple of the following values (default: [`missing`, `vip`, `priority`]): + - `iv` - Order by IV, if any. See `special_iv`. - `vip` - Order by VIP. - `missing` - Order by the target's pokedex missing status. - - `threshold` - Order by the threshold you have specified in the `catch` list. + - `priority` - Order by the priority you have specified in the `catch` list. - `expiration_timestamp_ms` - Order by the expiration time. -* `sources` - This should map a JSON param values from a given url. For example: different urls will provide different JSON response formats. If a param does not exist, you DO NOT have to specify it! Map bellow their corresponding values: - - `iv` - The JSON param that corresponds to the pokemon IV. Only certain sources provide this info. NOTE: social does not provide this info! +* `sources` - This should map a JSON param values from a given url. For example: different urls will provide different JSON response formats. **PLEASE ADVISED THAT, IF A PARAM DOES NOT EXIST (OR CONTAINS WRONG DATA LIKE PokeSnipers's ID PARAM), DO NOT SPECIFY IT!** Pokesnipers is a special case where it does provide IDs, however theyre wrong. Map bellow their corresponding values: +* `sources.key` - The JSON key that contains the results, eg.: For a JSON response such as `{ "SomeWeirdoName": [{"id": 123, ...}, {"id": 143, ...}]}`, `SomeWeirdoName` would be the key name. +* `sources.url` - The URL that will provide the JSON. +* `sources.enabled` - Defines whether this source is enabled or not. This has nothing to do with the task's `enabled`. + - `iv` - The JSON param that corresponds to the pokemon IV. Only certain sources provide this info. **NOTE:** `social` mode does not provide this info! - `id` - The JSON param that corresponds to the pokemon ID. (required) - `name` - The JSON param that corresponds to the pokemon name. (required) - `latitude` - The JSON param that corresponds to the latitude. It will work if a single param is used for both `latitude` and `longitude`, eg.: "coords": "1.2345, 6.7890" (required) @@ -790,13 +794,23 @@ This task is an upgrade version of the MoveToMapPokemon task. It will fetch poke "url": "http://localhost:5000/raw_data", "key": "pokemons", "mappings": { - "iv": { "param": "iv" }, "id": { "param": "pokemon_id" }, "name": { "param": "pokemon_name" }, "latitude": { "param": "latitude" }, "longitude": { "param": "longitude" }, "expiration": { "param": "disappear_time", "format": "milliseconds" } } + }, + { + "url": "https://pokewatchers.com/grab/", + "mappings": { + "iv": { "param": "iv" }, + "id": { "param": "pid" }, + "name": { "param": "pokemon" }, + "latitude": { "param": "cords" }, + "longitude": { "param": "cords" }, + "expiration": { "param": "timeend", "format": "milliseconds" } + } } ], "catch": { diff --git a/pokemongo_bot/cell_workers/sniper.py b/pokemongo_bot/cell_workers/sniper.py index 2e92d539a8..83a5e6c366 100644 --- a/pokemongo_bot/cell_workers/sniper.py +++ b/pokemongo_bot/cell_workers/sniper.py @@ -6,7 +6,7 @@ import calendar from random import uniform -from datetime import datetime, timedelta +from datetime import datetime from pokemongo_bot import inventory from pokemongo_bot.item_list import Item from pokemongo_bot.base_task import BaseTask @@ -19,6 +19,7 @@ class SniperSource(object): def __init__(self, data): self.url = data.get('url', '') self.key = data.get('key', '') + self.enabled = data.get('enabled', False) self.time_mask = data.get('time_mask', '%Y-%m-%d %H:%M:%S') self.mappings = SniperSourceMapping(data.get('mappings', {})) @@ -62,15 +63,19 @@ def fetch(self, timeout): expiration = expiration * 1000 elif self.mappings.expiration.format == SniperSourceMappingTimeFormat.UTC: utc_date = datetime.strptime(expiration.replace("T", " ")[:19], self.time_mask) - timestamp = calendar.timegm(utc_date.timetuple()) - local_date = datetime.fromtimestamp(timestamp) + unix_timestamp = calendar.timegm(utc_date.timetuple()) + local_date = datetime.fromtimestamp(unix_timestamp) local_date = local_date.replace(microsecond=utc_date.microsecond) expiration = time.mktime(local_date.timetuple()) * 1000 + else: + minutes_to_expire = 3 + seconds_per_minute = 60 + expiration = (time.time() + minutes_to_expire * seconds_per_minute) * 1000 # If either name or ID are invalid, fix it using each other if not name or not id: if not name and id: - name = Pokemons.name_for(id - 1) + name = Pokemons.name_for(id) if not id and name: id = Pokemons.id_for(name) @@ -97,33 +102,36 @@ def fetch(self, timeout): def validate(self): try: - errors = [] - data = self.fetch_raw(7) - - # Check whether the params really exist if they have been specified like so - if data: - if self.mappings.iv.exists and self.mappings.iv.param not in data[0]: - errors.append(self.mappings.iv.param) - if self.mappings.id.exists and self.mappings.id.param not in data[0]: - errors.append(self.mappings.id.param) - if self.mappings.name.exists and self.mappings.name.param not in data[0]: - errors.append(self.mappings.name.param) - if self.mappings.latitude.exists and self.mappings.latitude.param not in data[0]: - errors.append(self.mappings.latitude.param) - if self.mappings.longitude.exists and self.mappings.longitude.param not in data[0]: - errors.append(self.mappings.longitude.param) - if self.mappings.expiration.exists and self.mappings.expiration.param not in data[0]: - errors.append(self.mappings.expiration.param) - if self.mappings.encounter.exists and self.mappings.encounter.param not in data[0]: - errors.append(self.mappings.encounter.param) - if self.mappings.spawnpoint.exists and self.mappings.spawnpoint.param not in data[0]: - errors.append(self.mappings.spawnpoint.param) - - # All wrong mappings were gathered at once for a better usability (instead of raising multiple exceptions) - if errors: - raise LookupError("The following params dont exist: {}".format(", ".join(errors))) + if self.enabled: + errors = [] + data = self.fetch_raw(10) + + # Check whether the params really exist if they have been specified like so + if data: + if self.mappings.iv.exists and self.mappings.iv.param not in data[0]: + errors.append(self.mappings.iv.param) + if self.mappings.id.exists and self.mappings.id.param not in data[0]: + errors.append(self.mappings.id.param) + if self.mappings.name.exists and self.mappings.name.param not in data[0]: + errors.append(self.mappings.name.param) + if self.mappings.latitude.exists and self.mappings.latitude.param not in data[0]: + errors.append(self.mappings.latitude.param) + if self.mappings.longitude.exists and self.mappings.longitude.param not in data[0]: + errors.append(self.mappings.longitude.param) + if self.mappings.expiration.exists and self.mappings.expiration.param not in data[0]: + errors.append(self.mappings.expiration.param) + if self.mappings.encounter.exists and self.mappings.encounter.param not in data[0]: + errors.append(self.mappings.encounter.param) + if self.mappings.spawnpoint.exists and self.mappings.spawnpoint.param not in data[0]: + errors.append(self.mappings.spawnpoint.param) + + # All wrong mappings were gathered at once for a better usability (instead of raising multiple exceptions) + if errors: + raise LookupError("The following params dont exist: {}".format(", ".join(errors))) + else: + raise ValueError("Empty reply") else: - raise ValueError("Empty reply") + raise ValueError("Source is not enabled") except requests.exceptions.Timeout: raise ValueError("Fetching has timed out") except requests.exceptions.ConnectionError: @@ -184,7 +192,7 @@ class Sniper(BaseTask): MIN_SECONDS_ALLOWED_FOR_CELL_CHECK = 10 MIN_SECONDS_ALLOWED_FOR_REQUESTING_DATA = 5 MIN_BALLS_FOR_CATCHING = 10 - CACHE_LIST_MAX_SIZE = 200 + MAX_CACHE_LIST_SIZE = 200 def __init__(self, bot, config): super(Sniper, self).__init__(bot, config) @@ -206,33 +214,35 @@ def initialize(self): self.altitude = uniform(self.bot.config.alt_min, self.bot.config.alt_max) self.sources = [SniperSource(data) for data in self.config.get('sources', [])] - # Validate ordering - for ordering in self.order: - if ordering not in vars(SniperOrderMode).values(): - raise ValueError("Unrecognized ordering: '{}'".format(ordering)) + # Dont bother validating config if task is not even enabled + if self.enabled: + # Validate ordering + for ordering in self.order: + if ordering not in vars(SniperOrderMode).values(): + raise ValueError("Unrecognized ordering: '{}'".format(ordering)) - # Validate mode and sources - if self.mode not in vars(SniperMode).values(): - raise ValueError("Unrecognized mode: '{}'".format(self.mode)) - else: - # Selected mode is valid. Validate sources if mode is URL - if self.mode == SniperMode.URL: - self._log("Validating sources: {}...".format(", ".join([source.url for source in self.sources]))) - - # Create a copy of the list so we can iterate and remove elements at the same time - for source in list(self.sources): - try: - source.validate() - self._log("Source '{}' is good!".format(source.url)) - # TODO: On ValueError, remember source and validate later (pending validation) - except (LookupError, ValueError) as exception: - self._error("Source '{}' contains errors. Details: {}. Removing from sources list...".format(source.url, exception)) - self.sources.remove(source) - - # Notify user if all sources are invalid and cant proceed - if not self.sources: - self._error("There is no source available. Disabling Sniper...") - self.disabled = True + # Validate mode and sources + if self.mode not in vars(SniperMode).values(): + raise ValueError("Unrecognized mode: '{}'".format(self.mode)) + else: + # Selected mode is valid. Validate sources if mode is URL + if self.mode == SniperMode.URL: + self._log("Validating sources: {}...".format(", ".join([source.url for source in self.sources]))) + + # Create a copy of the list so we can iterate and remove elements at the same time + for source in list(self.sources): + try: + source.validate() + self._log("Source '{}' is good!".format(source.url)) + # TODO: On ValueError, remember source and validate later (pending validation) + except (LookupError, ValueError) as exception: + self._error("Source '{}' contains errors. Details: {}. Removing from sources list...".format(source.url, exception)) + self.sources.remove(source) + + # Notify user if all sources are invalid and cant proceed + if not self.sources: + self._error("There is no source available. Disabling Sniper...") + self.disabled = True def is_snipeable(self, pokemon): pokeballs_count = self.inventory.get(Item.ITEM_POKE_BALL.value).count @@ -256,19 +266,19 @@ def is_snipeable(self, pokemon): return False # Skip if not in catch list, not a VIP and/or IV sucks (if any) - if pokemon.get('pokemon_name', '') not in self.catch_list: - # This is not in the catch list. Lets see if its a VIP one - if not pokemon.get('pokemon_name') in self.bot.config.vips: - # It is not a VIP either. Lets see if its IV is good (if any) - if pokemon.get('iv', 0) < self.special_iv: - self._trace('{} is not listed to catch, nor a VIP and its IV sucks. Skipping...'.format(pokemon.get('pokemon_name'))) + if pokemon.get('pokemon_name', '') in self.catch_list: + self._trace('{} is catchable!'.format(pokemon.get('pokemon_name'))) + else: + # Not catchable. Having a good IV should suppress the not in catch/vip list (most important) + if pokemon.get('iv', 0) and pokemon.get('iv', 0) < self.special_iv: + self._trace('{} is not catchable, but has a decent IV!'.format(pokemon.get('pokemon_name'))) + else: + # Not catchable and IV is not good enough (if any). Check VIP list + if pokemon.get('vip', False): + self._trace('{} is not catchable and bad IV (if any), however its a VIP!'.format(pokemon.get('pokemon_name'))) + else: + self._trace('{} is not catachable, nor a VIP and bad IV (if any). Skipping...'.format(pokemon.get('pokemon_name'))) return False - # else: - # self._trace('{} has a decent IV ({}), therefore a valid target'.format(pokemon.get('pokemon_name'), pokemon.get('iv'))) - # else: - # self._trace('{} is a VIP, therefore a valid target'.format(pokemon.get('pokemon_name'))) - # else: - # self._trace('{} is in the catch list, therefore a valid target'.format(pokemon.get('pokemon_name'))) return True @@ -278,7 +288,7 @@ def snipe(self, pokemon): # Apply snipping business rules and snipe if its good if not self.is_snipeable(pokemon): - self._error('{} is not snipeable! Skipping...'.format(pokemon['pokemon_name'])) + self._trace('{} is not snipeable! Skipping...'.format(pokemon['pokemon_name'])) else: # Backup position before anything last_position = self.bot.position[0:2] @@ -290,6 +300,7 @@ def snipe(self, pokemon): # If social is enabled and if no verification is needed, trust it. Otherwise, update IDs! verify = not pokemon.get('encounter_id') or not pokemon.get('spawn_point_id') exists = not verify and self.mode == SniperMode.SOCIAL + success = exists # If information verification have to be done, do so if verify: @@ -378,18 +389,11 @@ def _parse_pokemons(self, pokemon_dictionary_list): # Build up the pokemon. Pops are used to destroy random attribute names and keep the known ones! for pokemon in pokemon_dictionary_list: - # Even thought the dict might have the name in it, use ID instead for safety (social vs url) - pokemon_name = Pokemons.name_for(pokemon.get('pokemon_id') - 1) - - # TODO: See below - # The plan is to only keep valid data in the broker, so if it hasnt ever been verified, we'll verify it and - # send the information back to the broker. Untill then, dont trust it. - # Sniper should send back to the broker whether it really exists or not. Use this in the snipe() function. - pokemon['verified'] = pokemon.get('verified', False) pokemon['iv'] = pokemon.get('iv', 0) - pokemon['vip'] = pokemon_name in self.bot.config.vips + pokemon['pokemon_name'] = pokemon.get('pokemon_name', Pokemons.name_for(pokemon.get('pokemon_id'))) + pokemon['vip'] = pokemon.get('pokemon_name') in self.bot.config.vips pokemon['missing'] = not self.pokedex.captured(pokemon.get('pokemon_id')) - pokemon['priority'] = self.catch_list.get(pokemon_name, 0) + pokemon['priority'] = self.catch_list.get(pokemon.get('pokemon_name'), 0) # Check whether this is a valid target if self.is_snipeable(pokemon): @@ -409,27 +413,28 @@ def _get_pokemons_from_url(self): results_hash_map = {} seconds_since_last_valid_request = time.time() - self.last_data_request_time - # If something is requesting this info too fast, skip it, otherwise lets merge the results! + # If something is requesting this info too fast, skip it (we might crash their servers) if (seconds_since_last_valid_request > self.MIN_SECONDS_ALLOWED_FOR_REQUESTING_DATA): self.last_data_request_time = time.time() self._trace("Fetching pokemons from the sources...") for source in self.sources: try: - source_pokemons = source.fetch(3) - self._trace("Source '{}' returned {} results".format(source.url, len(source_pokemons))) - - # Merge lists, making sure to exclude repeated data. Use location as the hash key - for source_pokemon in source_pokemons: - hash_key = self._hash(source_pokemon) - - # Add if new - if not results_hash_map.has_key(hash_key): - results_hash_map[hash_key] = source_pokemon + if source.enabled: + source_pokemons = source.fetch(3) + self._trace("Source '{}' returned {} results".format(source.url, len(source_pokemons))) + + # Merge lists, making sure to exclude repeated data. Use location as the hash key + for source_pokemon in source_pokemons: + hash_key = self._hash(source_pokemon) + + # Add if new + if not results_hash_map.has_key(hash_key): + results_hash_map[hash_key] = source_pokemon + else: + self._trace("Source '{}' is disabled".format(source.url)) except Exception as exception: self._error("Could not fetch data from '{}'. Details: {}. Skipping...".format(source.url, exception)) - continue - self._trace("After merging, we've got {} results".format(len(results_hash_map.values()))) else: self._trace("Not ready yet to retrieve data...") @@ -454,7 +459,7 @@ def _cache(self, pokemon): # Skip repeated items if not self._is_cached(pokemon): # Free space if full and store it - if len(self.cache) >= self.CACHE_LIST_MAX_SIZE: + if len(self.cache) >= self.MAX_CACHE_LIST_SIZE: self.cache.pop(0) self.cache.append(pokemon)