def run(self):
        """ The main routine which controls everything """

        update_notifier = self.check_for_update()
        framecount = 0

        # Create drawing tool to use to draw everything - it'll create its own screen
        drawing_tool = DrawingTool(self.file_prefix)
        drawing_tool.set_window_title(update_notifier)
        parser = LogParser(self.file_prefix, self.tracker_version)
        opt = Options()
        log = logging.getLogger("tracker")

        event_result = None
        state = None
        read_from_server = opt.read_from_server
        write_to_server = opt.write_to_server
        state_version = -1
        twitch_username = None
        new_states_queue = []
        screen_error_message = None

        while event_result != Event.DONE:

            # Check for events and handle them
            event_result = drawing_tool.handle_events()
            # A change means the user has (de)activated an option
            if opt.read_from_server != read_from_server\
            or opt.twitch_name != twitch_username:
                twitch_username = opt.twitch_name
                read_from_server = opt.read_from_server
                new_states_queue = []
                # Also restart version count if we go back and forth from log.txt to server
                if read_from_server:
                    state_version = -1
                    state = None
                    # show who we are watching in the title bar
                    drawing_tool.set_window_title(update_notifier, watching_player=twitch_username, updates_queued=len(new_states_queue))
                else:
                    drawing_tool.set_window_title(update_notifier)

            if opt.write_to_server and opt.write_to_server != write_to_server:
                write_to_server = True
                drawing_tool.set_window_title(update_notifier, uploading=True)

            if not opt.write_to_server:
                write_to_server = False

            if opt.read_from_server:
                # Change the delay for polling, as we probably don't want to fetch it every second
                update_timer = 2
            else:
                update_timer = self.read_timer

            if event_result == Event.OPTIONS_UPDATE:
                # By setting the framecount to 0 we ensure we'll refresh the state right away
                framecount = 0
                screen_error_message = None
                # force updates after changing options
                if state is not None:
                    state.modified = True


            # Now we re-process the log file to get anything that might have loaded;
            # do it every update_timer seconds (making sure to truncate to an integer
            # or else it might never mod to 0)
            if (framecount % int(Options().framerate_limit * update_timer) == 0):
                # Let the parser do his thing and give us a state
                if opt.read_from_server:
                    base_url = opt.trackerserver_url + "/tracker/api/user/" + opt.twitch_name
                    json_dict = None
                    try:
                        json_version = urllib2.urlopen(base_url + "/version").read()
                        if int(json_version) > state_version:
                            # FIXME better handling of 404 error ?
                            json_state = urllib2.urlopen(base_url).read()
                            json_dict = json.loads(json_state)
                            new_state = TrackerState.from_json(json_dict)
                            if new_state is None:
                                raise Exception
                            state_version = int(json_version)
                            new_states_queue.append((state_version, new_state))
                            drawing_tool.set_window_title(update_notifier, watching_player=twitch_username, updates_queued=len(new_states_queue), read_delay=opt.read_delay)
                    except Exception:
                        state = None
                        log.error("Couldn't load state from server")
                        import traceback
                        log.error(traceback.format_exc())
                        if json_dict is not None:
                            their_version = ""
                            if "tracker_version" in json_dict:
                                their_version = json_dict["tracker_version"]
                            else:
                                # this is the only version that can upload to the server but doesn't include a version string
                                their_version = "0.10-beta1"

                            if their_version != self.tracker_version:
                                screen_error_message = "They are using tracker version " + their_version + " but you have " + self.tracker_version
                else:
                    force_draw = state and state.modified
                    state = parser.parse()
                    if force_draw:
                        state.modified = True
                    if write_to_server and not opt.trackerserver_authkey:
                        screen_error_message = "Your authkey is blank. Get a new authkey in the options menu and paste it into the authkey text field."
                    if state is not None and write_to_server and state.modified and screen_error_message is None:
                        opener = urllib2.build_opener(urllib2.HTTPHandler)
                        put_url = opt.trackerserver_url + "/tracker/api/update/" + opt.trackerserver_authkey
                        json_string = json.dumps(state, cls=TrackerStateEncoder, sort_keys=True)
                        request = urllib2.Request(put_url,
                                                  data=json_string)
                        request.add_header('Content-Type', 'application/json')
                        request.get_method = lambda: 'PUT'
                        try:
                            result = opener.open(request)
                            result_json = json.loads(result.read())
                            updated_user = result_json["updated_user"]
                            if updated_user is None:
                                screen_error_message = "The server didn't recognize you. Try getting a new authkey in the options menu."
                            else:
                                screen_error_message = None
                        except Exception as e:
                            import traceback
                            errmsg = traceback.format_exc()
                            log.error("ERROR: Couldn't send item info to server")
                            log.error(errmsg)
                            screen_error_message = "ERROR: Couldn't send item info to server, check tracker_log.txt"


            # check the new state at the front of the queue to see if it's time to use it
            if len(new_states_queue) > 0:
                (state_timestamp, new_state) = new_states_queue[0]
                current_timestamp = int(time.time())
                if current_timestamp - state_timestamp >= opt.read_delay or state is None:
                    state = new_state
                    new_states_queue.pop(0)
                    drawing_tool.set_window_title(update_notifier, watching_player=twitch_username, updates_queued=len(new_states_queue), read_delay=opt.read_delay)



            if state is None and screen_error_message is None:
                if read_from_server:
                    screen_error_message = "Unable to read state from server. Please verify your options setup and tracker_log.txt"
                else:
                    screen_error_message = "log.txt not found. Put the RebirthItemTracker folder inside the isaac folder, next to log.txt"

            if screen_error_message is not None:
                drawing_tool.write_error_message(screen_error_message)
            else:
                # We got a state, now we draw it
                drawing_tool.draw_state(state)

            drawing_tool.tick()
            framecount += 1

        # main loop finished. program is exiting
        drawing_tool.save_window_position()
Exemple #2
0
    def run(self):
        """ The main routine which controls everything """
        framecount = 0

        # Create drawing tool to use to draw everything - it'll create its own screen
        drawing_tool = DrawingTool(wdir_prefix)
        drawing_tool.set_window_title_info(
            update_notifier=(" v" + self.tracker_version))
        opt = Options()

        parser = LogParser(wdir_prefix, self.tracker_version, LogFinder())

        event_result = None
        state = None
        custom_title_enabled = opt.custom_title_enabled
        read_from_server = opt.read_from_server
        write_to_server = opt.write_to_server
        game_version = opt.game_version
        state_version = -1
        twitch_username = None
        new_states_queue = []
        screen_error_message = None
        retry_in = 0
        update_timer = opt.log_file_check_seconds
        last_game_version = None

        while event_result != Event.DONE:
            # Check for events and handle them
            event_result = drawing_tool.handle_events()

            # The user checked or unchecked the "Custom Title Enabled" checkbox
            if opt.custom_title_enabled != custom_title_enabled:
                custom_title_enabled = opt.custom_title_enabled
                drawing_tool.update_window_title()

            # The user started or stopped watching someone from the server (or they started watching a new person from the server)
            if opt.read_from_server != read_from_server or opt.twitch_name != twitch_username:
                twitch_username = opt.twitch_name
                read_from_server = opt.read_from_server
                new_states_queue = []
                # Also restart version count if we go back and forth from log.txt to server
                if read_from_server:
                    state_version = -1
                    state = None
                    # Change the delay for polling, as we probably don't want to fetch it every second
                    update_timer_override = 2
                    # Show who we are watching in the title bar
                    drawing_tool.set_window_title_info(
                        watching=True,
                        watching_player=twitch_username,
                        updates_queued=len(new_states_queue))
                else:
                    drawing_tool.set_window_title_info(watching=False)
                    update_timer_override = 0

            # The user started or stopped broadcasting to the server
            if opt.write_to_server != write_to_server:
                write_to_server = opt.write_to_server
                drawing_tool.set_window_title_info(
                    uploading=opt.write_to_server)

            if opt.game_version != game_version:
                parser.reset()
                game_version = opt.game_version

            # Force refresh state if we updated options or if we need to retry
            # to contact the server.
            if (event_result == Event.OPTIONS_UPDATE
                    or (screen_error_message is not None and retry_in == 0)):
                # By setting the framecount to 0 we ensure we'll refresh the state right away
                framecount = 0
                screen_error_message = None
                retry_in = 0
                # Force updates after changing options
                if state is not None:
                    state.modified = True

            # normally we check for updates based on how the option is set
            # when doing network stuff, this can be overridden
            update_delay = opt.log_file_check_seconds
            if update_timer_override != 0:
                update_delay = update_timer_override

            # Now we re-process the log file to get anything that might have loaded;
            # do it every update_timer seconds (making sure to truncate to an integer
            # or else it might never mod to 0)
            frames_between_checks = int(Options().framerate_limit *
                                        update_delay)
            if frames_between_checks <= 0:
                frames_between_checks = 1

            if framecount % frames_between_checks == 0:
                if retry_in != 0:
                    retry_in -= 1
                # Let the parser do his thing and give us a state
                if opt.read_from_server:
                    base_url = opt.trackerserver_url + "/tracker/api/user/" + opt.twitch_name
                    json_dict = None
                    try:
                        json_version = urllib.request.urlopen(
                            base_url + "/version").read()
                        if int(json_version) > state_version:
                            # FIXME better handling of 404 error ?
                            json_state = urllib.request.urlopen(
                                base_url).read()
                            json_dict = json.loads(json_state)
                            new_state = TrackerState.from_json(json_dict)
                            if new_state is None:
                                raise Exception("server gave us empty state")
                            state_version = int(json_version)
                            new_states_queue.append((state_version, new_state))
                            drawing_tool.set_window_title_info(
                                updates_queued=len(new_states_queue))
                    except Exception:
                        state = None
                        log_error("Couldn't load state from server\n" +
                                  traceback.format_exc())
                        if json_dict is not None:
                            if "tracker_version" in json_dict:
                                their_version = json_dict["tracker_version"]
                            else:
                                # This is the only version that can upload to the server but doesn't include a version string
                                their_version = "0.10-beta1"

                            if their_version != self.tracker_version:
                                screen_error_message = "They are using tracker version " + their_version + " but you have " + self.tracker_version
                else:
                    force_draw = state and state.modified
                    state = parser.parse()
                    if force_draw and state is not None:
                        state.modified = True
                    if write_to_server and not opt.trackerserver_authkey:
                        screen_error_message = "Your authkey is blank. Get a new authkey in the options menu and paste it into the authkey text field."
                    if state is not None and write_to_server and state.modified and screen_error_message is None:
                        opener = urllib.request.build_opener(
                            urllib.request.HTTPHandler)
                        put_url = opt.trackerserver_url + "/tracker/api/update/" + opt.trackerserver_authkey
                        json_string = json.dumps(
                            state, cls=TrackerStateEncoder,
                            sort_keys=True).encode("utf-8")
                        request = urllib.request.Request(put_url,
                                                         data=json_string)
                        request.add_header('Content-Type', 'application/json')
                        request.get_method = lambda: 'PUT'
                        try:
                            result = opener.open(request)
                            result_json = json.loads(result.read())
                            updated_user = result_json["updated_user"]
                            if updated_user is None:
                                screen_error_message = "The server didn't recognize you. Try getting a new authkey in the options menu."
                            else:
                                screen_error_message = None
                        except Exception as e:
                            log_error(
                                "ERROR: Couldn't send item info to server\n" +
                                traceback.format_exc())
                            screen_error_message = "ERROR: Couldn't send item info to server, check tracker_log.txt"
                            # Retry to write the state in 10*update_timer (aka 10 sec in write mode)
                            retry_in = 10

            # Check the new state at the front of the queue to see if it's time to use it
            if len(new_states_queue) > 0:
                (state_timestamp, new_state) = new_states_queue[0]
                current_timestamp = int(time.time())
                if current_timestamp - state_timestamp >= opt.read_delay or opt.read_delay == 0 or state is None:
                    state = new_state
                    new_states_queue.pop(0)
                    drawing_tool.set_window_title_info(
                        updates_queued=len(new_states_queue))

            if state is None and screen_error_message is None:
                if read_from_server:
                    screen_error_message = "Unable to read state from server. Please verify your options setup and tracker_log.txt"
                    # Retry to read the state in 5*update_timer (aka 10 sec in read mode)
                    retry_in = 5
                else:
                    screen_error_message = "log.txt for " + opt.game_version + " not found. Make sure you have the right game selected in the options."

            if screen_error_message is not None:
                drawing_tool.write_error_message(screen_error_message)
            else:
                # We got a state, now we draw it
                drawing_tool.draw_state(state, framecount)

            # if we're watching someone and they change their game version, it can require us to reset
            if state and last_game_version != state.game_version:
                drawing_tool.reset_options()
                last_game_version = state.game_version

            drawing_tool.tick()
            framecount += 1

        # Main loop finished; program is exiting
        drawing_tool.save_window_position()
        Options().save_options(wdir_prefix + "options.json")
class IsaacTracker:
    def __init__(self, verbose=False, debug=False, read_delay=1):
        # Class variables
        self.verbose          = verbose
        self.debug            = debug
        self.text_height      = 0
        self.text_margin_size = None  # Will be changed in load_options
        self.font             = None  # Will be changed in load_options
        self.seek             = 0
        self.framecount       = 0
        self.read_delay       = read_delay
        self.run_ended        = True
        self.log_not_found    = False
        self.content          = ""  # Cached contents of log
        self.splitfile        = []  # Log split into lines
        self.drawing_tool     = None
        self.file_prefix      = "../"

        # Initialize isaac stuff
        self.collected_items         = [] # List of items collected this run
        self.collected_item_info     = [] # List of "immutable" ItemInfo objects used for determining the layout to draw
        self.guppy_set               = set() # Used to keep track of whether we're guppy or not
        self.num_displayed_items     = 0
        self.selected_item_idx       = None
        self.seed                    = ""
        self.current_room            = ""
        self.blind_floor             = False
        self.getting_start_items     = False
        self.run_start_line          = 0
        self.run_start_frame         = 0
        self.bosses                  = []
        self.last_run                = {}
        self._image_library          = {}
        self.in_summary_list         = []
        self.summary_condition_list  = []
        self.items_info              = {}
        self.floors                  = []
        self.player_stats            = {}
        self.player_stats_display    = {}
        self.reset_player_stats()
        self.item_message_start_time = 0
        self.item_pickup_time        = 0
        self.item_position_index     = []
        self.current_floor           = None
        self.floor_tuple             = () # Tuple with first value being floor number, second value being alt stage value (0 or 1, r.n.)
        self.spawned_coop_baby       = 0  # The last spawn of a co-op baby
        self.roll_icon               = None
        self.blind_icon              = None

        self.GAME_VERSION = "" # I KNOW THIS IS WRONG BUT I DON'T KNOW WHAT ELSE TO DO

        # Load items info
        with open(self.file_prefix + "items.json", "r") as items_file:
            self.items_info = json.load(items_file)

    def save_options(self):
        with open(self.file_prefix + "options.json", "w") as json_file:
            json.dump(self.options, json_file, indent=3, sort_keys=True)

    # This is just for debugging
    def log_msg(self, msg, level):
        if level == "V" and self.verbose: print msg
        if level == "D" and self.debug:   print msg

    # This is just for the suffix of the boss kill number
    def suffix(self, d):
        return 'th' if 11 <= d <= 13 else {1: 'st', 2: 'nd', 3: 'rd'}.get(d % 10, 'th')

    def check_end_run(self, line, cur_line_num):
        if not self.run_ended:
            died_to  = ""
            end_type = ""
            if self.bosses and self.bosses[-1][0] in ['???', 'The Lamb', 'Mega Satan']:
                end_type = "Won"
            elif (self.seed != '') and line.startswith('RNG Start Seed:'):
                end_type = "Reset"
            elif line.startswith('Game Over.'):
                end_type = "Death"
                died_to = re.search('(?i)Killed by \((.*)\) spawned', line).group(1)
            if end_type:
                self.last_run = {
                    "bosses":   self.bosses,
                    "items":    self.collected_items,
                    "seed":     self.seed,
                    "died_to":  died_to,
                    "end_type": end_type
                }
                self.run_ended = True
                self.log_msg("End of Run! %s" % self.last_run, "D")
                if end_type != "Reset":
                    self.save_file(self.run_start_line, cur_line_num, self.seed)

    def save_file(self, start, end, seed):
        self.mkdir("run_logs")
        timestamp = int(time.time())
        seed = seed.replace(" ", "")
        data = "\n".join(self.splitfile[start:end + 1])
        data = "%s\nRUN_OVER_LINE\n%s" % (data, self.last_run)
        run_name = "%s%s.log" % (seed, timestamp)
        in_memory_file = StringIO.StringIO()
        with zipfile.ZipFile(in_memory_file, mode='w', compression=zipfile.ZIP_DEFLATED) as zf:
            zf.writestr(run_name, data)
        with open(self.file_prefix + "run_logs/" + run_name + ".zip", "wb") as f:
            f.write(in_memory_file.getvalue())

    def mkdir(self, dn):
        if not os.path.isdir(dn):
            os.mkdir(dn)

    def add_stats_for_item(self, item, item_id):
        item_info = item.info
        for stat in Stat.LIST:
            if stat not in item_info:
                continue
            change = float(item_info.get(stat))
            self.player_stats[stat] += change
            value = self.player_stats[stat]

            # Round to 2 decimal places then ignore trailing zeros and trailing periods
            display = format(value, ".2f").rstrip("0").rstrip(".") # Doing just 'rstrip("0.")' breaks on "0.00"

            # For example, set "0.6" to ".6"
            if abs(value) < 1:
                display = display.lstrip("0")

            if value > -0.00001:
                display = "+" + display
            self.player_stats_display[stat] = display
            with open(self.file_prefix + "overlay text/" + stat + ".txt", "w+") as f:
                f.write(display)

        # If this can make us guppy, check if we're guppy
        if Stat.IS_GUPPY in item_info and item_info.get(Stat.IS_GUPPY):
            self.guppy_set.add(item)
            display = ""
            if len(self.guppy_set) >= 3:
                display = "yes"
            else:
                display = str(len(self.guppy_set))
            with open(self.file_prefix + "overlay text/" + stat + ".txt", "w+") as f:
                f.write(display)
            self.player_stats_display[Stat.IS_GUPPY] = display

    def reset_player_stats(self):
        for stat in Stat.LIST:
            self.player_stats[stat] = 0.0
            self.player_stats_display[stat] = "+0"

    # TODO: take SRL .comment length limit of 140 chars into account? would require some form of weighting
    # TODO: space bar items (Undefined, Teleport...) - a bit tricky because a simple "touch" shouldn't count
    def generate_run_summary(self):
        components = []
        floors = self.get_items_per_floor()
        for floor_id, items in floors.iteritems():
            floor_summary = self.generate_floor_summary(floor_id, items)
            floor = self.get_floor(floor_id)
            if floor_summary:
                components.append(floor_summary)

        components.insert(0, self.seed)
        components.append(self.generate_run_summary_stats())
        summary = string.join(components, ", ")

        if len(self.collected_guppy_items) is 2:
            two_thirds_text = ", 2/3 Guppy"
            if len(summary) <= (140 - len(two_thirds_text)):
                summary += two_thirds_text

        pygame.scrap.init()
        pygame.scrap.put(SCRAP_TEXT, summary)

    # TODO: this should be configurable with a string like the overlay
    def generate_run_summary_stats(self):
        return string.join(
            [("D:" + self.get_stat(Stat.DMG)),
             ("T:" + self.get_stat(Stat.TEARS)),
             ("S:" + self.get_stat(Stat.SPEED))], "/")

    def get_stat(self, stat):
        return self.player_stats_display[stat]

    def get_floor_label(self, floor_id):
        # TODO: Broken - fix
        floor = self.get_floor(floor_id) # A floor can't be lost _and_ blind (with Amnesia it could be, but we can't tell from log.txt)
        return self.get_floor_name(floor_id)

    def generate_floor_summary(self, floor_id, items):
        # TODO: Broken - fix
        floor_label = self.get_floor_label(floor_id)
        floor = self.get_floor(floor_id)

        if floor is None:
            # This should not happen
            return ""
        if not items:
            # Lost floors are still relevant even without items
            return floor_label if floor.lost else None
        return floor_label + " " + string.join(items, "/")

    def get_floor_name(self, floor_id):
        # TODO: Broken - fix
        return self.floor_id_to_label[floor_id]

    def get_floor(self, floor_id):
        # TODO: Broken - fix
        for floor in self.floors:
            if floor.id is floor_id:
                return floor
        return None

    def get_items_per_floor(self):
        # TODO: Make this work again using state model
        # TODO: Redo this using new state model
        floors = {}
        current_floor_id = None
        # A counter is necessary to find out *when* we became Guppy
        guppy_count = 0

        for item in self.collected_item_info:
            # TODO: why are the ids in the collected_item_info list not lstripped?
            short_id = item.id.lstrip("0")
            if item.floor:  # This is actually a floor, not an item
                floors[item.id] = []
                current_floor_id = item.id
            elif short_id in self.in_summary_list:
                item_info = self.get_item_info(item.id)
                floors[current_floor_id].append(self.get_summary_name(item_info))

            if short_id in self.guppy_list:
                guppy_count += 1
                if guppy_count >= 3:
                    floors[current_floor_id].append(u"Guppy")

        summary_condition_list_copy = list(self.summary_condition_list)
        return self.process_summary_conditions(floors, summary_condition_list_copy, [])

    def process_summary_conditions(self, floors, summary_conditions_left, keep_list):
        if len(summary_conditions_left) <= 0:
            return self.remove_items_not_in_list(floors, keep_list)
        item_id = summary_conditions_left.pop()
        condition = self.items_info[item_id][ItemProperty.SUMMARY_CONDITION]
        for floor in floors.itervalues():
            for item in floor:
                if item == condition:
                    keep_list.append(self.get_summary_name(self.items_info[item_id]))
        return self.process_summary_conditions(floors, summary_conditions_left, keep_list)

    def remove_items_not_in_list(self, floors, keep_list):
        new_floors = {}
        for floor_id in floors:
            new_floors[floor_id] = []
            for item_name in floors[floor_id]:
                if not self.in_summary_condition_list(item_name):
                    new_floors[floor_id].append(item_name)
                elif item_name in keep_list:
                    new_floors[floor_id].append(item_name)
        return new_floors

    def in_summary_condition_list(self, item_summary_name):
        for item_id in self.summary_condition_list:
            name = self.get_summary_name(self.items_info[item_id])
            if name is item_summary_name:
                return True
        return False

    def get_summary_name(self, item_info):
        if ItemProperty.SUMMARY_NAME in item_info:
            return item_info.get(ItemProperty.SUMMARY_NAME)
        return item_info.get(ItemProperty.NAME)

    def load_log_file(self):
        self.log_not_found = False
        path = None
        logfile_location = ""
        game_names = ("Afterbirth", "Rebirth")
        if platform.system() == "Windows":
            logfile_location = os.environ['USERPROFILE'] + '/Documents/My Games/Binding of Isaac {}/'
        elif platform.system() == "Linux":
            logfile_location = os.getenv('XDG_DATA_HOME',
                os.path.expanduser('~') + '/.local/share') + '/binding of isaac {}/'
            game_names = ("afterbirth", "rebirth")
        elif platform.system() == "Darwin":
            logfile_location = os.path.expanduser('~') + '/Library/Application Support/Binding of Isaac {}/'
        if os.path.exists(logfile_location.format(game_names[0])):
            logfile_location = logfile_location.format(game_names[0])
            self.GAME_VERSION = "Afterbirth"
        else:
            logfile_location = logfile_location.format(game_names[1])
            self.GAME_VERSION = "Rebirth"

        for check in (self.file_prefix + '../log.txt', logfile_location + 'log.txt'):
            if os.path.isfile(check):
                path = check
                break
        if path is None:
            self.log_not_found = True
            return

        cached_length = len(self.content)
        file_size = os.path.getsize(path)
        if cached_length > file_size or cached_length == 0: # New log file or first time loading the log
            self.content = open(path, 'rb').read()
        elif cached_length < file_size:  # Append existing content
            f = open(path, 'rb')
            f.seek(cached_length + 1)
            self.content += f.read()

    # Returns text to put in the title bar
    def check_for_update(self):
        try:
            github_info_json = urllib2.urlopen("https://api.github.com/repos/Hyphen-ated/RebirthItemTracker/releases/latest").read()
            info = json.loads(github_info_json)
            latest_version = info["name"]

            with open(self.file_prefix + 'version.txt', 'r') as f:
                current_version = f.read()
                title_text = " v" + current_version
                if latest_version != current_version:
                    title_text += " (new version available)"
                return title_text
        except Exception as e:
            self.log_msg("Failed to find update info: " + e.message, "D")
        return ""

    def get_item_info(self, item_id):
        id_padded = item_id.zfill(3)
        return self.items_info[id_padded]

    def start_new_run(self, current_line_number):
        self.run_start_line = current_line_number + self.seek
        self.log_msg("Starting new run, seed: %s" % self.seed, "D")
        self.run_start_frame = self.framecount
        self.collected_items = []
        self.log_msg("Emptied item array", "D")
        self.bosses = []
        self.log_msg("Emptied boss array", "D")
        self.run_ended = False
        self.reset_player_stats()
        self.current_floor = None
        self.drawing_tool.reset()
        self.guppy_set=set()
        self.log_msg("Reset drawing tool", "D")
        with open(self.file_prefix + "overlay text/seed.txt", "w+") as f:
            f.write(self.seed)

    def run(self):
        self.current_floor = None

        # Initialize pygame system stuff
        pygame.init()
        update_notifier = self.check_for_update()
        pygame.display.set_caption("Rebirth Item Tracker" + update_notifier)

        # Create drawing tool to use to draw everything - it'll create its own screen
        self.drawing_tool = DrawingTool()
        os.environ['SDL_VIDEO_WINDOW_POS'] = "%d, %d" % (
            self.drawing_tool.options[Option.X_POSITION],
            self.drawing_tool.options[Option.Y_POSITION])
        self.drawing_tool.start_pygame()
        pygame.display.set_icon(self.drawing_tool.get_image("collectibles_333.png"))
        done = False
        clock = pygame.time.Clock()
        winInfo = None
        if platform.system() == "Windows":
            winInfo = pygameWindowInfo.PygameWindowInfo()

        del os.environ['SDL_VIDEO_WINDOW_POS']
        while not done:
            # pygame logic
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    if platform.system() == "Windows":
                        winPos = winInfo.getScreenPosition()
                        self.drawing_tool.options[Option.X_POSITION] = winPos["left"]
                        self.drawing_tool.options[Option.Y_POSITION] = winPos["top"]
                        self.drawing_tool.save_options()
                    done = True
                elif event.type == VIDEORESIZE:
                    screen = pygame.display.set_mode(event.dict['size'], RESIZABLE)
                    self.drawing_tool.options[Option.WIDTH] = event.dict["w"]
                    self.drawing_tool.options[Option.HEIGHT] = event.dict["h"]
                    self.drawing_tool.save_options()
                    self.drawing_tool.reflow(self.collected_items)
                    pygame.display.flip()
                elif event.type == MOUSEMOTION:
                    if pygame.mouse.get_focused():
                        pos = pygame.mouse.get_pos()
                        self.drawing_tool.select_item_on_hover(*pos)
                elif event.type == KEYDOWN:
                    if len(self.collected_items) > 0:
                        if event.key == pygame.K_RIGHT:
                            self.drawing_tool.adjust_select_item_on_keypress(1)
                        elif event.key == pygame.K_LEFT:
                            self.drawing_tool.adjust_select_item_on_keypress(-1)
                        elif event.key == pygame.K_RETURN:
                            self.drawing_tool.load_selected_detail_page()
                        elif event.key == pygame.K_c and pygame.key.get_mods() & pygame.KMOD_CTRL:
                            pass
                            #self.generate_run_summary() # This is commented out because run summaries are broken with the new "state" model rewrite of the item tracker
                elif event.type == MOUSEBUTTONDOWN:
                    if event.button == 1:
                        self.drawing_tool.load_selected_detail_page()
                    if event.button == 3:
                        import option_picker
                        pygame.event.set_blocked([QUIT, MOUSEBUTTONDOWN, KEYDOWN, MOUSEMOTION])
                        option_picker.options_menu(self.file_prefix + "options.json").run()
                        pygame.event.set_allowed([QUIT, MOUSEBUTTONDOWN, KEYDOWN, MOUSEMOTION])
                        self.drawing_tool.reset()
                        self.drawing_tool.load_options()
                        self.drawing_tool.reflow(self.collected_items)

            # Drawing logic
            clock.tick(int(self.drawing_tool.options[Option.FRAMERATE_LIMIT]))

            if self.log_not_found:
                self.drawing_tool.write_message("log.txt not found. Put the RebirthItemTracker folder inside the isaac folder, next to log.txt")

            self.drawing_tool.draw_items(self)
            self.framecount += 1

            # Now we re-process the log file to get anything that might have loaded; do it every read_delay seconds (making sure to truncate to an integer or else it might never mod to 0)
            if self.framecount % int(self.drawing_tool.options[Option.FRAMERATE_LIMIT] * self.read_delay) == 0:
                self.load_log_file()
                self.splitfile = self.content.splitlines()

                # Return to start if seek passes the end of the file (usually because the log file restarted)
                if self.seek > len(self.splitfile):
                    self.log_msg("Current line number longer than lines in file, returning to start of file", "D")
                    self.seek = 0

                should_reflow = False
                getting_start_items = False # This will become true if we are getting starting items

                # Process log's new output
                for current_line_number, line in enumerate(self.splitfile[self.seek:]):
                    self.log_msg(line, "V")

                    # The end floor boss should be defeated now
                    if line.startswith('Mom clear time:'):
                        kill_time = int(line.split(" ")[-1])

                        # If you re-enter a room you get a "mom clear time" again, check for that (can you fight the same boss twice?)
                        if self.current_room not in [x[0] for x in self.bosses]:
                            self.bosses.append((self.current_room, kill_time))
                            self.log_msg(
                                "Defeated %s%s boss %s at time %s" % (len(self.bosses),
                                self.suffix(len(self.bosses)), self.current_room, kill_time), "D")

                    # Check and handle the end of the run; the order is important - we want it after boss kill but before "RNG Start Seed"
                    self.check_end_run(line, current_line_number + self.seek)

                    if line.startswith('RNG Start Seed:'): # The start of a run
                        self.seed = line[16:25] # This assumes a fixed width, but from what I see it seems safe

                    if line.startswith('Room'):
                        self.current_room = re.search('\((.*)\)', line).group(1)
                        if 'Start Room' not in line:
                            getting_start_items = False
                        self.log_msg("Entered room: %s" % self.current_room,"D")
                    if line.startswith('Level::Init'):
                        if self.GAME_VERSION == "Afterbirth":
                            floor_tuple = tuple([re.search("Level::Init m_Stage (\d+), m_StageType (\d+)",line).group(x) for x in [1, 2]])
                        else:
                            floor_tuple = tuple([re.search("Level::Init m_Stage (\d+), m_AltStage (\d+)",line).group(x) for x in [1, 2]])

                        # Assume floors aren't cursed until we see they are
                        self.blind_floor = False
                        getting_start_items = True
                        floor = int(floor_tuple[0])
                        alt = floor_tuple[1]


                        # Special handling for cath and chest
                        if alt == '1' and (floor == 9 or floor == 11):
                            floor += 1
                        floor_id = 'f' + str(floor)

                        # when we see a new floor 1, that means a new run has started
                        if floor == 1:
                            self.start_new_run(current_line_number)

                        self.current_floor=Floor(floor_id,self,(alt=='1'))
                        should_reflow = True



                    if line.startswith("Curse of the Labyrinth!"):
                        # It SHOULD always begin with f (that is, it's a floor) because this line only comes right after the floor line
                        self.current_floor.add_curse(Curse.Labyrinth)
                    if line.startswith("Curse of Blind"):
                        self.current_floor.add_curse(Curse.Blind)
                    if line.startswith("Curse of the Lost!"):
                        self.current_floor.add_curse(Curse.Lost)
                    if line.startswith("Spawn co-player!"):
                        self.spawned_coop_baby = current_line_number + self.seek
                    if re.search("Added \d+ Collectibles", line):
                        self.log_msg("Reroll detected!", "D")
                        map(lambda item: item.rerolled(),self.collected_items)
                    if line.startswith('Adding collectible'):
                        if len(self.splitfile) > 1 and self.splitfile[current_line_number + self.seek - 1] == line:
                            self.log_msg("Skipped duplicate item line from baby presence", "D")
                            continue
                        space_split = line.split(" ") # Hacky string manipulation
                        item_id = space_split[2] # A string has the form of "Adding collectible 105 (The D6)"

                        # Check if the item ID exists
                        if item_id.zfill(3) not in self.items_info:
                            item_id = "NEW"
                        item_info = self.get_item_info(item_id)

                        # Default current floor to basement 1 if none
                        if self.current_floor is None:
                            self.current_floor = Floor("f1", self, False)
                        # If the item IDs are equal, it should say this item already exists
                        temp_item = Item(item_id,self.current_floor,item_info,getting_start_items)
                        if ((current_line_number + self.seek) - self.spawned_coop_baby) < (len(self.collected_items) + 10) \
                                and temp_item in self.collected_items:
                            self.log_msg("Skipped duplicate item line from baby entry","D")
                            continue
                        item_name = " ".join(space_split[3:])[1:-1]
                        self.log_msg("Picked up item. id: %s, name: %s" % (item_id, item_name), "D")
                        with open(self.file_prefix + "overlay text/itemInfo.txt", "w+") as f:
                            desc = temp_item.generate_item_description()
                            f.write(item_info[ItemProperty.NAME] + ":" + desc)

                        # Ignore repeated pickups of space bar items
                        if not (item_info.get(ItemProperty.SPACE,False) and temp_item in self.collected_items):
                            self.collected_items.append(temp_item)
                            self.item_message_start_time = self.framecount
                            self.item_pickup_time = self.framecount
                            self.drawing_tool.item_picked_up()
                        else:
                            self.log_msg("Skipped adding item %s to avoid space-bar duplicate" % item_id, "D")
                        self.add_stats_for_item(temp_item, item_id)
                        should_reflow = True

                self.seek = len(self.splitfile)
                if should_reflow:
                    self.drawing_tool.reflow(self.collected_items)
    def run(self):
        self.current_floor = None

        # Initialize pygame system stuff
        pygame.init()
        update_notifier = self.check_for_update()
        pygame.display.set_caption("Rebirth Item Tracker" + update_notifier)

        # Create drawing tool to use to draw everything - it'll create its own screen
        self.drawing_tool = DrawingTool()
        os.environ['SDL_VIDEO_WINDOW_POS'] = "%d, %d" % (
            self.drawing_tool.options[Option.X_POSITION],
            self.drawing_tool.options[Option.Y_POSITION])
        self.drawing_tool.start_pygame()
        pygame.display.set_icon(self.drawing_tool.get_image("collectibles_333.png"))
        done = False
        clock = pygame.time.Clock()
        winInfo = None
        if platform.system() == "Windows":
            winInfo = pygameWindowInfo.PygameWindowInfo()

        del os.environ['SDL_VIDEO_WINDOW_POS']
        while not done:
            # pygame logic
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    if platform.system() == "Windows":
                        winPos = winInfo.getScreenPosition()
                        self.drawing_tool.options[Option.X_POSITION] = winPos["left"]
                        self.drawing_tool.options[Option.Y_POSITION] = winPos["top"]
                        self.drawing_tool.save_options()
                    done = True
                elif event.type == VIDEORESIZE:
                    screen = pygame.display.set_mode(event.dict['size'], RESIZABLE)
                    self.drawing_tool.options[Option.WIDTH] = event.dict["w"]
                    self.drawing_tool.options[Option.HEIGHT] = event.dict["h"]
                    self.drawing_tool.save_options()
                    self.drawing_tool.reflow(self.collected_items)
                    pygame.display.flip()
                elif event.type == MOUSEMOTION:
                    if pygame.mouse.get_focused():
                        pos = pygame.mouse.get_pos()
                        self.drawing_tool.select_item_on_hover(*pos)
                elif event.type == KEYDOWN:
                    if len(self.collected_items) > 0:
                        if event.key == pygame.K_RIGHT:
                            self.drawing_tool.adjust_select_item_on_keypress(1)
                        elif event.key == pygame.K_LEFT:
                            self.drawing_tool.adjust_select_item_on_keypress(-1)
                        elif event.key == pygame.K_RETURN:
                            self.drawing_tool.load_selected_detail_page()
                        elif event.key == pygame.K_c and pygame.key.get_mods() & pygame.KMOD_CTRL:
                            pass
                            #self.generate_run_summary() # This is commented out because run summaries are broken with the new "state" model rewrite of the item tracker
                elif event.type == MOUSEBUTTONDOWN:
                    if event.button == 1:
                        self.drawing_tool.load_selected_detail_page()
                    if event.button == 3:
                        import option_picker
                        pygame.event.set_blocked([QUIT, MOUSEBUTTONDOWN, KEYDOWN, MOUSEMOTION])
                        option_picker.options_menu(self.file_prefix + "options.json").run()
                        pygame.event.set_allowed([QUIT, MOUSEBUTTONDOWN, KEYDOWN, MOUSEMOTION])
                        self.drawing_tool.reset()
                        self.drawing_tool.load_options()
                        self.drawing_tool.reflow(self.collected_items)

            # Drawing logic
            clock.tick(int(self.drawing_tool.options[Option.FRAMERATE_LIMIT]))

            if self.log_not_found:
                self.drawing_tool.write_message("log.txt not found. Put the RebirthItemTracker folder inside the isaac folder, next to log.txt")

            self.drawing_tool.draw_items(self)
            self.framecount += 1

            # Now we re-process the log file to get anything that might have loaded; do it every read_delay seconds (making sure to truncate to an integer or else it might never mod to 0)
            if self.framecount % int(self.drawing_tool.options[Option.FRAMERATE_LIMIT] * self.read_delay) == 0:
                self.load_log_file()
                self.splitfile = self.content.splitlines()

                # Return to start if seek passes the end of the file (usually because the log file restarted)
                if self.seek > len(self.splitfile):
                    self.log_msg("Current line number longer than lines in file, returning to start of file", "D")
                    self.seek = 0

                should_reflow = False
                getting_start_items = False # This will become true if we are getting starting items

                # Process log's new output
                for current_line_number, line in enumerate(self.splitfile[self.seek:]):
                    self.log_msg(line, "V")

                    # The end floor boss should be defeated now
                    if line.startswith('Mom clear time:'):
                        kill_time = int(line.split(" ")[-1])

                        # If you re-enter a room you get a "mom clear time" again, check for that (can you fight the same boss twice?)
                        if self.current_room not in [x[0] for x in self.bosses]:
                            self.bosses.append((self.current_room, kill_time))
                            self.log_msg(
                                "Defeated %s%s boss %s at time %s" % (len(self.bosses),
                                self.suffix(len(self.bosses)), self.current_room, kill_time), "D")

                    # Check and handle the end of the run; the order is important - we want it after boss kill but before "RNG Start Seed"
                    self.check_end_run(line, current_line_number + self.seek)

                    if line.startswith('RNG Start Seed:'): # The start of a run
                        self.seed = line[16:25] # This assumes a fixed width, but from what I see it seems safe

                    if line.startswith('Room'):
                        self.current_room = re.search('\((.*)\)', line).group(1)
                        if 'Start Room' not in line:
                            getting_start_items = False
                        self.log_msg("Entered room: %s" % self.current_room,"D")
                    if line.startswith('Level::Init'):
                        if self.GAME_VERSION == "Afterbirth":
                            floor_tuple = tuple([re.search("Level::Init m_Stage (\d+), m_StageType (\d+)",line).group(x) for x in [1, 2]])
                        else:
                            floor_tuple = tuple([re.search("Level::Init m_Stage (\d+), m_AltStage (\d+)",line).group(x) for x in [1, 2]])

                        # Assume floors aren't cursed until we see they are
                        self.blind_floor = False
                        getting_start_items = True
                        floor = int(floor_tuple[0])
                        alt = floor_tuple[1]


                        # Special handling for cath and chest
                        if alt == '1' and (floor == 9 or floor == 11):
                            floor += 1
                        floor_id = 'f' + str(floor)

                        # when we see a new floor 1, that means a new run has started
                        if floor == 1:
                            self.start_new_run(current_line_number)

                        self.current_floor=Floor(floor_id,self,(alt=='1'))
                        should_reflow = True



                    if line.startswith("Curse of the Labyrinth!"):
                        # It SHOULD always begin with f (that is, it's a floor) because this line only comes right after the floor line
                        self.current_floor.add_curse(Curse.Labyrinth)
                    if line.startswith("Curse of Blind"):
                        self.current_floor.add_curse(Curse.Blind)
                    if line.startswith("Curse of the Lost!"):
                        self.current_floor.add_curse(Curse.Lost)
                    if line.startswith("Spawn co-player!"):
                        self.spawned_coop_baby = current_line_number + self.seek
                    if re.search("Added \d+ Collectibles", line):
                        self.log_msg("Reroll detected!", "D")
                        map(lambda item: item.rerolled(),self.collected_items)
                    if line.startswith('Adding collectible'):
                        if len(self.splitfile) > 1 and self.splitfile[current_line_number + self.seek - 1] == line:
                            self.log_msg("Skipped duplicate item line from baby presence", "D")
                            continue
                        space_split = line.split(" ") # Hacky string manipulation
                        item_id = space_split[2] # A string has the form of "Adding collectible 105 (The D6)"

                        # Check if the item ID exists
                        if item_id.zfill(3) not in self.items_info:
                            item_id = "NEW"
                        item_info = self.get_item_info(item_id)

                        # Default current floor to basement 1 if none
                        if self.current_floor is None:
                            self.current_floor = Floor("f1", self, False)
                        # If the item IDs are equal, it should say this item already exists
                        temp_item = Item(item_id,self.current_floor,item_info,getting_start_items)
                        if ((current_line_number + self.seek) - self.spawned_coop_baby) < (len(self.collected_items) + 10) \
                                and temp_item in self.collected_items:
                            self.log_msg("Skipped duplicate item line from baby entry","D")
                            continue
                        item_name = " ".join(space_split[3:])[1:-1]
                        self.log_msg("Picked up item. id: %s, name: %s" % (item_id, item_name), "D")
                        with open(self.file_prefix + "overlay text/itemInfo.txt", "w+") as f:
                            desc = temp_item.generate_item_description()
                            f.write(item_info[ItemProperty.NAME] + ":" + desc)

                        # Ignore repeated pickups of space bar items
                        if not (item_info.get(ItemProperty.SPACE,False) and temp_item in self.collected_items):
                            self.collected_items.append(temp_item)
                            self.item_message_start_time = self.framecount
                            self.item_pickup_time = self.framecount
                            self.drawing_tool.item_picked_up()
                        else:
                            self.log_msg("Skipped adding item %s to avoid space-bar duplicate" % item_id, "D")
                        self.add_stats_for_item(temp_item, item_id)
                        should_reflow = True

                self.seek = len(self.splitfile)
                if should_reflow:
                    self.drawing_tool.reflow(self.collected_items)
    def run(self):
        """ The main routine which controls everything """
        framecount = 0

        # Create drawing tool to use to draw everything - it'll create its own screen
        drawing_tool = DrawingTool(wdir_prefix)
        drawing_tool.set_window_title_info(update_notifier=(" v" + self.tracker_version))
        opt = Options()

        parser = LogParser(wdir_prefix, self.tracker_version, LogFinder())

        event_result = None
        state = None
        custom_title_enabled = opt.custom_title_enabled
        read_from_server = opt.read_from_server
        write_to_server = opt.write_to_server
        game_version = opt.game_version
        state_version = -1
        twitch_username = None
        new_states_queue = []
        screen_error_message = None
        retry_in = 0
        update_timer = opt.log_file_check_seconds
        last_game_version = None

        while event_result != Event.DONE:
            # Check for events and handle them
            event_result = drawing_tool.handle_events()

            # The user checked or unchecked the "Custom Title Enabled" checkbox
            if opt.custom_title_enabled != custom_title_enabled:
                custom_title_enabled = opt.custom_title_enabled
                drawing_tool.update_window_title()

            # The user started or stopped watching someone from the server (or they started watching a new person from the server)
            if opt.read_from_server != read_from_server or opt.twitch_name != twitch_username:
                twitch_username = opt.twitch_name
                read_from_server = opt.read_from_server
                new_states_queue = []
                # Also restart version count if we go back and forth from log.txt to server
                if read_from_server:
                    state_version = -1
                    state = None
                    # Change the delay for polling, as we probably don't want to fetch it every second
                    update_timer_override = 2
                    # Show who we are watching in the title bar
                    drawing_tool.set_window_title_info(watching=True, watching_player=twitch_username, updates_queued=len(new_states_queue))
                else:
                    drawing_tool.set_window_title_info(watching=False)
                    update_timer_override = 0

            # The user started or stopped broadcasting to the server
            if opt.write_to_server != write_to_server:
                write_to_server = opt.write_to_server
                drawing_tool.set_window_title_info(uploading=opt.write_to_server)

            if opt.game_version != game_version:
                parser.reset()
                game_version = opt.game_version

            # Force refresh state if we updated options or if we need to retry
            # to contact the server.
            if (event_result == Event.OPTIONS_UPDATE or
                (screen_error_message is not None and retry_in == 0)):
                # By setting the framecount to 0 we ensure we'll refresh the state right away
                framecount = 0
                screen_error_message = None
                retry_in = 0
                # Force updates after changing options
                if state is not None:
                    state.modified = True

            # normally we check for updates based on how the option is set
            # when doing network stuff, this can be overridden
            update_delay = opt.log_file_check_seconds
            if update_timer_override != 0:
                update_delay = update_timer_override
                
            # Now we re-process the log file to get anything that might have loaded;
            # do it every update_timer seconds (making sure to truncate to an integer
            # or else it might never mod to 0)
            frames_between_checks = int(Options().framerate_limit * update_delay)
            if frames_between_checks <= 0:
                frames_between_checks = 1
            
            if framecount % frames_between_checks == 0:
                if retry_in != 0:
                    retry_in -= 1
                # Let the parser do his thing and give us a state
                if opt.read_from_server:
                    base_url = opt.trackerserver_url + "/tracker/api/user/" + opt.twitch_name
                    json_dict = None
                    try:
                        json_version = urllib.request.urlopen(base_url + "/version").read()
                        if int(json_version) > state_version:
                            # FIXME better handling of 404 error ?
                            json_state = urllib.request.urlopen(base_url).read()
                            json_dict = json.loads(json_state, "utf-8")
                            new_state = TrackerState.from_json(json_dict)
                            if new_state is None:
                                raise Exception("server gave us empty state")
                            state_version = int(json_version)
                            new_states_queue.append((state_version, new_state))
                            drawing_tool.set_window_title_info(updates_queued=len(new_states_queue))
                    except Exception:
                        state = None
                        log_error("Couldn't load state from server\n" + traceback.format_exc())
                        if json_dict is not None:
                            if "tracker_version" in json_dict:
                                their_version = json_dict["tracker_version"]
                            else:
                                # This is the only version that can upload to the server but doesn't include a version string
                                their_version = "0.10-beta1"

                            if their_version != self.tracker_version:
                                screen_error_message = "They are using tracker version " + their_version + " but you have " + self.tracker_version
                else:
                    force_draw = state and state.modified
                    state = parser.parse()
                    if force_draw:
                        state.modified = True
                    if write_to_server and not opt.trackerserver_authkey:
                        screen_error_message = "Your authkey is blank. Get a new authkey in the options menu and paste it into the authkey text field."
                    if state is not None and write_to_server and state.modified and screen_error_message is None:
                        opener = urllib.request.build_opener(urllib.request.HTTPHandler)
                        put_url = opt.trackerserver_url + "/tracker/api/update/" + opt.trackerserver_authkey
                        json_string = json.dumps(state, cls=TrackerStateEncoder, sort_keys=True)
                        request = urllib.request.Request(put_url,
                                                  data=json_string)
                        request.add_header('Content-Type', 'application/json')
                        request.get_method = lambda: 'PUT'
                        try:
                            result = opener.open(request)
                            result_json = json.loads(result.read())
                            updated_user = result_json["updated_user"]
                            if updated_user is None:
                                screen_error_message = "The server didn't recognize you. Try getting a new authkey in the options menu."
                            else:
                                screen_error_message = None
                        except Exception as e:
                            log_error("ERROR: Couldn't send item info to server\n" + traceback.format_exc())
                            screen_error_message = "ERROR: Couldn't send item info to server, check tracker_log.txt"
                            # Retry to write the state in 10*update_timer (aka 10 sec in write mode)
                            retry_in = 10

            # Check the new state at the front of the queue to see if it's time to use it
            if len(new_states_queue) > 0:
                (state_timestamp, new_state) = new_states_queue[0]
                current_timestamp = int(time.time())
                if current_timestamp - state_timestamp >= opt.read_delay or opt.read_delay == 0 or state is None:
                    state = new_state
                    new_states_queue.pop(0)
                    drawing_tool.set_window_title_info(updates_queued=len(new_states_queue))

            if state is None and screen_error_message is None:
                if read_from_server:
                    screen_error_message = "Unable to read state from server. Please verify your options setup and tracker_log.txt"
                    # Retry to read the state in 5*update_timer (aka 10 sec in read mode)
                    retry_in = 5
                else:
                    screen_error_message = "log.txt for " + opt.game_version + " not found. Make sure you have the right game selected in the options."

            if screen_error_message is not None:
                drawing_tool.write_error_message(screen_error_message)
            else:
                # We got a state, now we draw it
                drawing_tool.draw_state(state)

            # if we're watching someone and they change their game version, it can require us to reset
            if state and last_game_version != state.game_version:
                drawing_tool.reset_options()
                last_game_version = state.game_version

            drawing_tool.tick()
            framecount += 1

        # Main loop finished; program is exiting
        drawing_tool.save_window_position()
        Options().save_options(wdir_prefix + "options.json")