diff --git a/configs/config.json.cluster.example b/configs/config.json.cluster.example index 83e267ffa1..516e51e0cd 100644 --- a/configs/config.json.cluster.example +++ b/configs/config.json.cluster.example @@ -120,7 +120,10 @@ "Razz Berry": { "keep" : 100 } }, "recycle_wait_min": 3, - "recycle_wait_max": 5 + "recycle_wait_max": 5, + "recycle_force": true, + "recycle_force_min": "00:00:00", + "recycle_force_max": "00:01:00" } }, { @@ -159,6 +162,11 @@ "spin_wait_max": 5 } }, + { "type": "UpdateWebInventory", + "config": { + "enabled": true + } + }, { "type": "FollowCluster", "config": { diff --git a/configs/config.json.example b/configs/config.json.example index 64eca099c6..cae70b2e5a 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -133,7 +133,10 @@ "Razz Berry": { "keep" : 100 } }, "recycle_wait_min": 3, - "recycle_wait_max": 5 + "recycle_wait_max": 5, + "recycle_force": true, + "recycle_force_min": "00:00:00", + "recycle_force_max": "00:01:00" } }, { @@ -174,6 +177,11 @@ "spin_wait_max": 5 } }, + { "type": "UpdateWebInventory", + "config": { + "enabled": true + } + }, { "type": "MoveToFort", "config": { diff --git a/configs/config.json.map.example b/configs/config.json.map.example index 0dbed8a709..04cd61fbb9 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -120,7 +120,10 @@ "Razz Berry": { "keep" : 100 } }, "recycle_wait_min": 1, - "recycle_wait_max": 4 + "recycle_wait_max": 4, + "recycle_force": true, + "recycle_force_min": "00:00:00", + "recycle_force_max": "00:01:00" } }, { @@ -159,6 +162,11 @@ "spin_wait_max": 5 } }, + { "type": "UpdateWebInventory", + "config": { + "enabled": true + } + }, { "type": "MoveToMapPokemon", "config": { diff --git a/configs/config.json.optimizer.example b/configs/config.json.optimizer.example index d0491517e5..a10548a94a 100644 --- a/configs/config.json.optimizer.example +++ b/configs/config.json.optimizer.example @@ -181,7 +181,10 @@ "Razz Berry": { "keep" : 100 } }, "recycle_wait_min": 3, - "recycle_wait_max": 5 + "recycle_wait_max": 5, + "recycle_force": true, + "recycle_force_min": "00:00:00", + "recycle_force_max": "00:01:00" } }, { @@ -221,6 +224,11 @@ "spin_wait_max": 5 } }, + { "type": "UpdateWebInventory", + "config": { + "enabled": true + } + }, { "type": "MoveToFort", "config": { diff --git a/configs/config.json.path.example b/configs/config.json.path.example index 9e3e27e71b..b7bd6a8bbe 100644 --- a/configs/config.json.path.example +++ b/configs/config.json.path.example @@ -120,7 +120,10 @@ "Razz Berry": { "keep" : 100 } }, "recycle_wait_min": 3, - "recycle_wait_max": 5 + "recycle_wait_max": 5, + "recycle_force": true, + "recycle_force_min": "00:00:00", + "recycle_force_max": "00:01:00" } }, { @@ -160,6 +163,11 @@ "spin_wait_max": 5 } }, + { "type": "UpdateWebInventory", + "config": { + "enabled": true + } + }, { "type": "FollowPath", "config": { diff --git a/configs/config.json.pokemon.example b/configs/config.json.pokemon.example index 2942f04fab..4ad7d83483 100644 --- a/configs/config.json.pokemon.example +++ b/configs/config.json.pokemon.example @@ -119,7 +119,10 @@ "Razz Berry": { "keep" : 100 } }, "recycle_wait_min": 3, - "recycle_wait_max": 5 + "recycle_wait_max": 5, + "recycle_force": true, + "recycle_force_min": "00:00:00", + "recycle_force_max": "00:01:00" } }, { @@ -159,6 +162,11 @@ "spin_wait_max": 5 } }, + { "type": "UpdateWebInventory", + "config": { + "enabled": true + } + }, { "type": "MoveToFort", "config":{ diff --git a/docs/configuration_files.md b/docs/configuration_files.md index 9fc17488b7..b296b7c89e 100644 --- a/docs/configuration_files.md +++ b/docs/configuration_files.md @@ -120,6 +120,10 @@ The behaviors of the bot are configured via the `tasks` key in the `config.json` * `max_potions_keep`: Default `None` | Maximum amount of potions to keep in inventory * `max_berries_keep`: Default `None` | Maximum amount of berries to keep in inventory * `max_revives_keep`: Default `None` | Maximum amount of revives to keep in inventory + * `recycle_force`: Default `False` | Force scheduled recycle, even if min_empty_space not exceeded + * `recycle_force_min`: Default `00:01:00` | Minimum time to wait before scheduling next forced recycle + * `recycle_force_max`: Default `00:10:00` | Maximum time to wait before scheduling next forced recycle + * SpinFort * TransferPokemon * `min_free_slot`: Default `5` | Once the pokebag has less empty slots than this amount, the transfer process is triggered. | Big values (i.e 9999) will trigger the transfer process after each catch. diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index 345ac03057..ca8f2b4595 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -221,6 +221,15 @@ def _register_events(self): ) ) + # recycle stuff + self.event_manager.register_event( + 'next_force_recycle', + parameters=( + 'time' + ) + ) + self.event_manager.register_event('force_recycle') + # random alive pause self.event_manager.register_event( 'next_random_alive_pause', diff --git a/pokemongo_bot/cell_workers/__init__.py b/pokemongo_bot/cell_workers/__init__.py index 5e748b9ae6..4388881967 100644 --- a/pokemongo_bot/cell_workers/__init__.py +++ b/pokemongo_bot/cell_workers/__init__.py @@ -21,4 +21,5 @@ from catch_pokemon import CatchPokemon from complete_tutorial import CompleteTutorial from random_pause import RandomPause +from update_web_inventory import UpdateWebInventory from random_alive_pause import RandomAlivePause diff --git a/pokemongo_bot/cell_workers/recycle_items.py b/pokemongo_bot/cell_workers/recycle_items.py index 1ca8bde91b..922d8744e1 100644 --- a/pokemongo_bot/cell_workers/recycle_items.py +++ b/pokemongo_bot/cell_workers/recycle_items.py @@ -8,6 +8,9 @@ from pokemongo_bot.services.item_recycle_worker import ItemRecycler from pokemongo_bot.tree_config_builder import ConfigException from pokemongo_bot.worker_result import WorkerResult +from random import uniform +from datetime import datetime as dt, timedelta + DEFAULT_MIN_EMPTY_SPACE = 6 @@ -15,6 +18,9 @@ class RecycleItems(BaseTask): """ Recycle undesired items if there is less than five space in inventory. You can use either item's name or id. For the full list of items see ../../data/items.json + + Can also force a recycle to occur at a pseudo-random time between recycle_force_min and + recycle_force_max minutes. It's highly recommended to put this task before move_to_fort and spin_fort task in the config file so you'll most likely be able to loot. @@ -38,8 +44,14 @@ class RecycleItems(BaseTask): "Revive": {"keep": 0}, "Max Revive": {"keep": 20}, "Razz Berry": {"keep": 20} - } + }, + "recycle_wait_min": 1, + "recycle_wait_max": 4, + "recycle_force": true, + "recycle_force_min": "00:00:00", + "recycle_force_max": "00:01:00" } + } """ SUPPORTED_TASK_API_VERSION = 1 @@ -54,7 +66,56 @@ def initialize(self): self.max_revives_keep = self.config.get('max_revives_keep', None) self.recycle_wait_min = self.config.get('recycle_wait_min', 1) self.recycle_wait_max = self.config.get('recycle_wait_max', 4) + self.recycle_force = self.config.get('recycle_force', False) + self.recycle_force_min = self.config.get('recycle_force_min', '00:01:00') + self.recycle_force_max = self.config.get('recycle_force_max', '00:10:00') + self.minInterval = self.getSeconds(self.recycle_force_min) + self.maxInterval = self.getSeconds(self.recycle_force_max) self._validate_item_filter() + + if self.recycle_force: + self._schedule_next_force() + + def getSeconds(self, strTime): + ''' + Return the duration in seconds of a time string + :param strTime: string time of format %H:%M:%S + ''' + try: + x = dt.strptime(strTime, '%H:%M:%S') + seconds = int(timedelta(hours=x.hour,minutes=x.minute,seconds=x.second).total_seconds()) + except ValueError: + seconds = 0; + + if seconds < 0: + seconds = 0; + + return seconds + + def _schedule_next_force(self): + ''' + Schedule the time aof the next forced recycle. + ''' + self._next_force = self._get_next_force_schedule() + self.emit_event( + 'next_force_recycle', + formatted="Next forced item recycle at {time}", + data={ + 'time': str(self._next_force.strftime("%H:%M:%S")) + } + ) + + def _should_force_now(self): + if dt.now() >= self._next_force: + return True + + return False + + def _get_next_force_schedule(self): + now = dt.now() + next_time = now + timedelta(seconds=int(uniform(self.minInterval, self.maxInterval))) + + return next_time def _validate_item_filter(self): """ @@ -77,6 +138,15 @@ def should_run(self): :return: True if the recycling process should be run; otherwise, False. :rtype: bool """ + + if self.recycle_force and self._should_force_now(): + self.emit_event( + 'force_recycle', + formatted="Forcing item recycle based on schedule" + ) + self._schedule_next_force() + return True + if inventory.Items.get_space_left() <= (DEFAULT_MIN_EMPTY_SPACE if self.min_empty_space is None else self.min_empty_space): return True return False @@ -92,24 +162,33 @@ def work(self): if self.should_run(): if not (self.max_balls_keep is None): - worker_result = self.recycle_excess_category_max(self.max_balls_keep, [1,2,3,4]) + this_worker_result = self.recycle_excess_category_max(self.max_balls_keep, [1,2,3,4]) + if this_worker_result <> WorkerResult.SUCCESS: + worker_result = this_worker_result + if not (self.max_potions_keep is None): - worker_result = self.recycle_excess_category_max(self.max_potions_keep, [101,102,103,104]) + this_worker_result = self.recycle_excess_category_max(self.max_potions_keep, [101,102,103,104]) + if this_worker_result <> WorkerResult.SUCCESS: + worker_result = this_worker_result + if not (self.max_berries_keep is None): - worker_result = self.recycle_excess_category_max(self.max_berries_keep, [701,702,703,704,705]) + this_worker_result = self.recycle_excess_category_max(self.max_berries_keep, [701,702,703,704,705]) + if this_worker_result <> WorkerResult.SUCCESS: + worker_result = this_worker_result + if not (self.max_revives_keep is None): - worker_result = self.recycle_excess_category_max(self.max_revives_keep, [201,202]) - - inventory.refresh_inventory() - + this_worker_result = self.recycle_excess_category_max(self.max_revives_keep, [201,202]) + if this_worker_result <> WorkerResult.SUCCESS: + worker_result = this_worker_result + for item_in_inventory in inventory.items().all(): - if self.item_should_be_recycled(item_in_inventory): # Make the bot appears more human action_delay(self.recycle_wait_min, self.recycle_wait_max) # If at any recycling process call we got an error, we consider that the result of this task is error too. if ItemRecycler(self.bot, item_in_inventory, self.get_amount_to_recycle(item_in_inventory)).work() == WorkerResult.ERROR: - worker_result = WorkerResult.ERROR + worker_result = WorkerResult.ERROR + return worker_result def recycle_excess_category_max(self, category_max, category_items_list): diff --git a/pokemongo_bot/cell_workers/update_live_stats.py b/pokemongo_bot/cell_workers/update_live_stats.py index f7ae1877e6..13dd8576cf 100644 --- a/pokemongo_bot/cell_workers/update_live_stats.py +++ b/pokemongo_bot/cell_workers/update_live_stats.py @@ -1,10 +1,13 @@ import ctypes +import json +import os from sys import stdout, platform as _platform from datetime import datetime, timedelta from pokemongo_bot.base_task import BaseTask from pokemongo_bot.worker_result import WorkerResult from pokemongo_bot.tree_config_builder import ConfigException +from pokemongo_bot.base_dir import _base_dir class UpdateLiveStats(BaseTask): @@ -92,11 +95,15 @@ def work(self): """ if not self._should_display(): return WorkerResult.SUCCESS - line = self._get_stats_line(self._get_player_stats()) + + player_stats = self._get_player_stats() + line = self._get_stats_line(player_stats) # If line is empty, it couldn't be generated. if not line: return WorkerResult.SUCCESS - + + self.update_web_stats(player_stats) + if self.terminal_title: self._update_title(line, _platform) @@ -356,3 +363,16 @@ def _get_player_stats(self): for x in inventory_items if x.get("inventory_item_data", {}).get("player_stats", {})), None) + + def update_web_stats(self,player_data): + web_inventory = os.path.join(_base_dir, "web", "inventory-%s.json" % self.bot.config.username) + + with open(web_inventory, "r") as infile: + json_stats = json.load(infile) + + json_stats = [x for x in json_stats if not x.get("inventory_item_data", {}).get("player_stats", None)] + + json_stats.append({"inventory_item_data": {"player_stats": player_data}}) + + with open(web_inventory, "w") as outfile: + json.dump(json_stats, outfile) diff --git a/pokemongo_bot/cell_workers/update_web_inventory.py b/pokemongo_bot/cell_workers/update_web_inventory.py new file mode 100644 index 0000000000..02b82128f3 --- /dev/null +++ b/pokemongo_bot/cell_workers/update_web_inventory.py @@ -0,0 +1,12 @@ +from pokemongo_bot.base_task import BaseTask +from pokemongo_bot import inventory + + +class UpdateWebInventory(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + + def initialize(self): + pass + + def work(self): + inventory.update_web_inventory() diff --git a/pokemongo_bot/event_handlers/colored_logging_handler.py b/pokemongo_bot/event_handlers/colored_logging_handler.py index 8fe8559dc7..80088f2f6b 100644 --- a/pokemongo_bot/event_handlers/colored_logging_handler.py +++ b/pokemongo_bot/event_handlers/colored_logging_handler.py @@ -23,6 +23,8 @@ class ColoredLoggingHandler(EventHandler): 'inventory_full': 'yellow', 'item_discard_fail': 'red', 'item_discarded': 'green', + 'next_force_recycle': 'green', + 'force_recycle': 'green', 'keep_best_release': 'green', 'level_up': 'green', 'level_up_reward': 'green', diff --git a/pokemongo_bot/inventory.py b/pokemongo_bot/inventory.py index 26e4e239e7..0577b65e20 100644 --- a/pokemongo_bot/inventory.py +++ b/pokemongo_bot/inventory.py @@ -1092,14 +1092,43 @@ def refresh(self): inventory = inventory['responses']['GET_INVENTORY']['inventory_delta']['inventory_items'] for i in (self.pokedex, self.candy, self.items, self.pokemons): i.refresh(inventory) - - user_web_inventory = os.path.join(_base_dir, 'web', 'inventory-%s.json' % (self.bot.config.username)) - try: - with open(user_web_inventory, 'w') as outfile: - json.dump(inventory, outfile) - except IOError as e: - errmsg = '[x] Error while opening location file: user_web_inventory' - + + self.update_web_inventory() + + + def update_web_inventory(self): + web_inventory = os.path.join(_base_dir, "web", "inventory-%s.json" % self.bot.config.username) + + with open(web_inventory, "r") as infile: + json_inventory = json.load(infile) + + json_inventory = [x for x in json_inventory if not x.get("inventory_item_data", {}).get("pokedex_entry", None)] + json_inventory = [x for x in json_inventory if not x.get("inventory_item_data", {}).get("candy", None)] + json_inventory = [x for x in json_inventory if not x.get("inventory_item_data", {}).get("item", None)] + json_inventory = [x for x in json_inventory if not x.get("inventory_item_data", {}).get("pokemon_data", None)] + + json_inventory = json_inventory + self.jsonify_inventory() + + with open(web_inventory, "w") as outfile: + json.dump(json_inventory, outfile) + + def jsonify_inventory(self): + json_inventory = [] + + for pokedex in self.pokedex.all(): + json_inventory.append({"inventory_item_data": {"pokedex_entry": pokedex}}) + + for family_id, candy in self.candy._data.items(): + json_inventory.append({"inventory_item_data": {"candy": {"family_id": family_id, "candy": candy.quantity}}}) + + for item_id, item in self.items._data.items(): + json_inventory.append({"inventory_item_data": {"item": {"item_id": item_id, "count": item.count}}}) + + for pokemon in self.pokemons.all(): + json_inventory.append({"inventory_item_data": {"pokemon_data": pokemon._data}}) + + return json_inventory + def retrieve_inventories_size(self): """ Retrieves the item inventory size @@ -1197,6 +1226,9 @@ def refresh_inventory(): :rtype: None """ _inventory.refresh() + +def update_web_inventory(): + _inventory.update_web_inventory() def get_item_inventory_size(): """