示例#1
0
    def __init__(self, key, db_path=None, slider_dir=None, loader=None, \
        cache=True):
        self.log = logging.getLogger(__name__)

        # allow for people to pass their own loader implementation/subclass.
        # Mostly exposed for circleguard (the gui).
        if loader:
            self.loader = loader
        else:
            self.loader = Loader(key, db_path, write_to_cache=cache)

        if slider_dir:
            self.library = Library(slider_dir)
        else:
            # If slider dir wasn't passed, use a temporary library which will
            # effectively cache beatmaps for just this cg instance.
            # Have to keep a reference to this dir or the folder gets deleted.
            self.slider_dir = TemporaryDirectory()
            self.library = Library(self.slider_dir.name)
            # clean up our library (which resides in a temporary dir) or else
            # garbage collection of this cg object (and subsequently the
            # temp dir and library) will cause an error to be thrown. This
            # happens because the temp dir's finalizer is called first, which
            # tries to remove the directory, but it can't because the library's
            # sql connection to the db file in that dir is still alive, and a
            # PermissionError is thrown. We need to close the library before
            # the temp dir is finalized.
            # Errors that happen during garbage collection are ignored I
            # believe, so this only fixes the error message appearing (which is
            # still a good thing to do) rather than actually fixing any programs
            # that broke because of this.
            self._finalizer = weakref.finalize(self, self._cleanup,
                self.library)
    def __init__(self, key, db_path=None, slider_dir=None, loader=None, \
        cache=True):
        self.cache = cache
        self.cacher = None
        if db_path is not None:
            # resolve relative paths
            db_path = Path(db_path).absolute()
            # they can set cache to False later with
            # :func:`~.circleguard.set_options` if they want; assume caching is
            # desired if db path is passed
            self.cacher = Cacher(self.cache, db_path)

        self.log = logging.getLogger(__name__)

        # allow for people to pass their own loader implementation/subclass
        LoaderClass = Loader if loader is None else loader
        self.loader = LoaderClass(key, self.cacher)

        if slider_dir is None:
            # have to keep a reference to it or the folder gets deleted and
            # can't be walked by Library
            self.slider_dir = TemporaryDirectory()
            self.library = None
        else:
            self.library = Library(slider_dir)
示例#3
0
    def __init__(self):
        QFrame.__init__(self)
        SingleLinkableSetting.__init__(self, "api_key")

        self.library = Library(get_setting("cache_dir"))

        self.loadables_combobox = QComboBox(self)
        self.loadables_combobox.setInsertPolicy(QComboBox.NoInsert)
        for loadable in MainTab.LOADABLES_COMBOBOX_REGISTRY:
            self.loadables_combobox.addItem(loadable, loadable)
        self.loadables_combobox.activated.connect(self.add_loadable)

        self.checks_combobox = QComboBox(self)
        self.checks_combobox.setInsertPolicy(QComboBox.NoInsert)
        for check in MainTab.CHECKS_COMBOBOX_REGISTRY:
            self.checks_combobox.addItem(check, check)
        self.checks_combobox.activated.connect(self.add_check)

        self.loadables_scrollarea = QScrollArea(self)
        self.loadables_scrollarea.setWidget(ScrollableLoadablesWidget())
        self.loadables_scrollarea.setWidgetResizable(True)

        self.checks_scrollarea = QScrollArea(self)
        self.checks_scrollarea.setWidget(ScrollableChecksWidget())
        self.checks_scrollarea.setWidgetResizable(True)

        self.loadables = [] # for deleting later
        self.checks = [] # for deleting later

        self.print_results_signal.connect(self.print_results)
        self.write_to_terminal_signal.connect(self.write)

        self.q = Queue()
        self.cg_q = Queue()
        self.helper_thread_running = False
        self.runs = [] # Run objects for canceling runs
        self.run_id = 0
        self.visualizer = None

        terminal = QTextEdit(self)
        terminal.setFocusPolicy(Qt.ClickFocus)
        terminal.setReadOnly(True)
        terminal.ensureCursorVisible()
        self.terminal = terminal

        self.run_button = QPushButton()
        self.run_button.setText("Run")
        self.run_button.clicked.connect(self.add_circleguard_run)
        # disable button if no api_key is stored
        self.on_setting_changed("api_key", get_setting("api_key"))

        layout = QGridLayout()
        layout.addWidget(self.loadables_combobox, 0, 0, 1, 4)
        layout.addWidget(self.checks_combobox, 0, 8, 1, 4)
        layout.addWidget(self.loadables_scrollarea, 1, 0, 4, 8)
        layout.addWidget(self.checks_scrollarea, 1, 8, 4, 8)
        layout.addWidget(self.terminal, 5, 0, 2, 16)
        layout.addWidget(self.run_button, 7, 0, 1, 16)

        self.setLayout(layout)
示例#4
0
def run_job(user, replay_cache_dir, model_cache_dir, age, library, api_key):
    import pathlib

    from lain import ErrorModel
    from lain.train import load_replay_directory
    import pandas as pd
    from slider import Client, Library

    from combine.utils import model_path

    osu_client = Client(Library(library), api_key)
    replays = load_replay_directory(
        pathlib.Path(replay_cache_dir) / user,
        client=osu_client,
        age=pd.Timedelta(age) if age is not None else None,
        save=True,
        verbose=True,
    )

    model = ErrorModel()
    model.fit(replays)

    user_models = model_path(model_cache_dir, user)
    user_models.mkdir(parents=True, exist_ok=True)
    model.save_path(user_models)
示例#5
0
文件: main.py 项目: aticie/osu-badge
import scipy
from badgeWidget import VisualizerWindow
from PyQt5.QtWidgets import QApplication

USER = "******"
MAP = "2097898"
OSU_API_KEY = ""
USE_REPLAY = False  # if the file should be used instead of downloading the replay
REPLAY_PATH = "./replay/nonexistingfile.osr"
CACHE_DIR = "./cache/"
_api = ossapi(OSU_API_KEY)
_cg = Circleguard(OSU_API_KEY)
_loader = _cg.loader
if not os.path.exists(CACHE_DIR):
    os.mkdir(CACHE_DIR)
_library = Library.create_db(CACHE_DIR)


def _get_score_info():
    print("@retrieving score info")
    return _api.get_scores({"b":MAP, "u":USER})[0]

def _get_replay(score_info):
    if USE_REPLAY:
        print("@USE_REPLAY set, using REPLAY_PATH")
        replay = ReplayPath(REPLAY_PATH)
    else:
        print("@USE_REPLAY not set, trying to download replay")
        if score_info["replay_available"] != "0":
            print("@Downloading Replay")
            replay = ReplayMap(user_id=USER,map_id=MAP)
    def run(self, loadables, detect, loadables2=None, max_angle=DEFAULT_ANGLE, \
        min_distance=DEFAULT_DISTANCE, num_chunks=DEFAULT_CHUNKS) \
        -> Iterable[Result]:
        """
        Investigates loadables for cheats.

        Parameters
        ----------
        loadables: list[:class:`~.Loadable`]
            The loadables to investigate.
        detect: :class:`~.Detect`
            What cheats to investigate for.
        loadables2: list[:class:`~.Loadable`]
            For :data:`~Detect.STEAL`, compare each loadable in ``loadables``
            against each loadable in ``loadables2`` for replay stealing,
            instead of to other loadables in ``loadables``.
        max_angle: float
            For :data:`Detect.CORRECTION`, consider only points (a,b,c) where
            ``∠abc < max_angle``.
        min_distance: float
            For :data:`Detect.CORRECTION`, consider only points (a,b,c) where
            ``|ab| > min_distance`` and ``|bc| > min_distance``.
        num_chunks: int
            For :data:`detect.STEAL_CORR`, how many chunks to split the replay
            into when comparing. Note that runtime increases linearly with the
            number of chunks.

        Yields
        ------
        :class:`~.Result`
            A result representing an investigation of one or more of the replays
            in ``loadables``, depending on the ``detect`` passed.

        Notes
        -----
        :class:`~.Result`\s are yielded one at a time, as circleguard finishes
        investigating them. This means that you can process results from
        :meth:`~.run` without waiting for all of the investigations to finish.
        """

        c = Check(loadables, self.cache, loadables2=loadables2)
        self.log.info("Running circleguard with check %r", c)

        c.load(self.loader)
        # comparer investigations
        if detect & (Detect.STEAL_SIM | Detect.STEAL_CORR):
            replays1 = c.all_replays1()
            replays2 = c.all_replays2()
            comparer = Comparer(replays1, replays2, detect, num_chunks)
            yield from comparer.compare()

        # investigator investigations
        if detect & (Detect.RELAX | Detect.CORRECTION | Detect.TIMEWARP):
            if detect & Detect.RELAX:
                if not self.library:
                    # connect to library since it's a temporary one
                    library = Library(self.slider_dir.name)
                else:
                    library = self.library

            for replay in c.all_replays():
                bm = None
                # don't download beatmap unless we need it for relax
                if detect & Detect.RELAX:
                    bm = library.lookup_by_id(replay.map_id, download=True, \
                                              save=True)
                investigator = Investigator(replay, detect, max_angle, \
                                            min_distance, beatmap=bm)
                yield from investigator.investigate()

            if detect & Detect.RELAX:
                if not self.library:
                    # disconnect from temporary library
                    library.close()
示例#7
0
 def library(self):
     if not self._library:
         from slider import Library
         self._library = Library(get_setting("cache_dir"))
     return self._library
示例#8
0
    def __init__(self, beatmap_info, replays, events, library, speeds, \
        start_speed, paint_info, statistic_functions, snaps_args):
        super().__init__()
        self.speeds = speeds
        self.replays = replays
        self.library = library
        self.snaps_args = snaps_args
        self.current_replay_info = None
        # maps `circleguard.Replay` to `circlevis.ReplayInfo`, as its creation
        # is relatively expensive and users might open and close the same info
        # panel multiple times
        self.replay_info_cache = {}

        # we calculate some statistics in the background so users aren't hit
        # with multi-second wait times when accessing replay info. Initialize
        # with `None` so if the replay info *is* accessed before we calculate
        # everything, no harm - `ReplayInfo` will calculate it instead.
        self.replay_statistics_precalculated = {}
        for replay in replays:
            self.replay_statistics_precalculated[replay] = (None, None, None,
                                                            None)

        # only precalculate statistics if we're visualizing 5 or fewer replays.
        # Otherwise, the thread is too overworked and lags the main draw thread
        # significantly until all statistics are precalculated.
        # TODO This may be resolved properly by using a QThread with a low
        # priority instead, so as not to starve the draw thread. We should be
        # using QThreads instead of python threads regardless.

        if len(replays) <= 5:
            # and here's the thread which will actually start those calculations
            cg_statistics_worked = Thread(target=self.calculate_cg_statistics)
            # allow users to quit before we're done calculating
            cg_statistics_worked.daemon = True
            cg_statistics_worked.start()

        # create our own library in a temp dir if one wasn't passed
        if not self.library:
            # keep a reference so it doesn't get deleted
            self.temp_dir = TemporaryDirectory()
            self.library = Library(self.temp_dir.name)

        self.beatmap = None
        if beatmap_info.path:
            self.beatmap = Beatmap.from_path(beatmap_info.path)
        elif beatmap_info.map_id:
            # TODO move temporary directory creation to slider probably, since
            # this logic is now duplicated here and in circlecore
            self.beatmap = self.library.lookup_by_id(beatmap_info.map_id,
                                                     download=True,
                                                     save=True)

        dt_enabled = any(Mod.DT in replay.mods for replay in replays)
        ht_enabled = any(Mod.HT in replay.mods for replay in replays)
        if dt_enabled:
            start_speed = 1.5
        if ht_enabled:
            start_speed = 0.75

        self.renderer = Renderer(self.beatmap, replays, events, start_speed,
                                 paint_info, statistic_functions)
        self.renderer.update_time_signal.connect(self.update_slider)
        # if the renderer wants to pause itself (eg when the playback hits the
        # end of the replay), we kick it back to us (the `Interface`) so we can
        # also update the pause button's state.
        self.renderer.pause_signal.connect(self.toggle_pause)

        # we want to give `VisualizerControls` the union of all the replay's
        # mods
        mods = Mod.NM
        for replay in replays:
            mods += replay.mods

        self.controls = VisualizerControls(start_speed, mods, replays)
        self.controls.pause_button.clicked.connect(self.toggle_pause)
        self.controls.play_reverse_button.clicked.connect(self.play_reverse)
        self.controls.play_normal_button.clicked.connect(self.play_normal)
        self.controls.next_frame_button.clicked.connect(
            lambda: self.change_frame(reverse=False))
        self.controls.previous_frame_button.clicked.connect(
            lambda: self.change_frame(reverse=True))
        self.controls.speed_up_button.clicked.connect(self.increase_speed)
        self.controls.speed_down_button.clicked.connect(self.lower_speed)
        self.controls.copy_to_clipboard_button.clicked.connect(
            self.copy_to_clipboard)
        self.controls.time_slider.sliderMoved.connect(self.renderer.seek_to)
        self.controls.time_slider.setRange(self.renderer.playback_start,
                                           self.renderer.playback_end)

        self.controls.raw_view_changed.connect(self.renderer.raw_view_changed)
        self.controls.only_color_keydowns_changed.connect(
            self.renderer.only_color_keydowns_changed)
        self.controls.hitobjects_changed.connect(
            self.renderer.hitobjects_changed)
        self.controls.approach_circles_changed.connect(
            self.renderer.approach_circles_changed)
        self.controls.num_frames_changed.connect(
            self.renderer.num_frames_changed)
        self.controls.draw_hit_error_bar_changed.connect(
            self.renderer.draw_hit_error_bar_changed)
        self.controls.circle_size_mod_changed.connect(
            self.renderer.circle_size_mod_changed)
        self.controls.show_info_for_replay.connect(self.show_info_panel)

        self.splitter = QSplitter()
        # splitter lays widgets horizontally by default, so combine renderer and
        # controls into one single widget vertically
        self.splitter.addWidget(
            Combined([self.renderer, self.controls], Qt.Vertical))

        layout = QGridLayout()
        layout.addWidget(self.splitter, 1, 0, 1, 1)
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)
示例#9
0
class Interface(QWidget):
    def __init__(self, beatmap_info, replays, events, library, speeds, \
        start_speed, paint_info, statistic_functions, snaps_args):
        super().__init__()
        self.speeds = speeds
        self.replays = replays
        self.library = library
        self.snaps_args = snaps_args
        self.current_replay_info = None
        # maps `circleguard.Replay` to `circlevis.ReplayInfo`, as its creation
        # is relatively expensive and users might open and close the same info
        # panel multiple times
        self.replay_info_cache = {}

        # we calculate some statistics in the background so users aren't hit
        # with multi-second wait times when accessing replay info. Initialize
        # with `None` so if the replay info *is* accessed before we calculate
        # everything, no harm - `ReplayInfo` will calculate it instead.
        self.replay_statistics_precalculated = {}
        for replay in replays:
            self.replay_statistics_precalculated[replay] = (None, None, None,
                                                            None)

        # only precalculate statistics if we're visualizing 5 or fewer replays.
        # Otherwise, the thread is too overworked and lags the main draw thread
        # significantly until all statistics are precalculated.
        # TODO This may be resolved properly by using a QThread with a low
        # priority instead, so as not to starve the draw thread. We should be
        # using QThreads instead of python threads regardless.

        if len(replays) <= 5:
            # and here's the thread which will actually start those calculations
            cg_statistics_worked = Thread(target=self.calculate_cg_statistics)
            # allow users to quit before we're done calculating
            cg_statistics_worked.daemon = True
            cg_statistics_worked.start()

        # create our own library in a temp dir if one wasn't passed
        if not self.library:
            # keep a reference so it doesn't get deleted
            self.temp_dir = TemporaryDirectory()
            self.library = Library(self.temp_dir.name)

        self.beatmap = None
        if beatmap_info.path:
            self.beatmap = Beatmap.from_path(beatmap_info.path)
        elif beatmap_info.map_id:
            # TODO move temporary directory creation to slider probably, since
            # this logic is now duplicated here and in circlecore
            self.beatmap = self.library.lookup_by_id(beatmap_info.map_id,
                                                     download=True,
                                                     save=True)

        dt_enabled = any(Mod.DT in replay.mods for replay in replays)
        ht_enabled = any(Mod.HT in replay.mods for replay in replays)
        if dt_enabled:
            start_speed = 1.5
        if ht_enabled:
            start_speed = 0.75

        self.renderer = Renderer(self.beatmap, replays, events, start_speed,
                                 paint_info, statistic_functions)
        self.renderer.update_time_signal.connect(self.update_slider)
        # if the renderer wants to pause itself (eg when the playback hits the
        # end of the replay), we kick it back to us (the `Interface`) so we can
        # also update the pause button's state.
        self.renderer.pause_signal.connect(self.toggle_pause)

        # we want to give `VisualizerControls` the union of all the replay's
        # mods
        mods = Mod.NM
        for replay in replays:
            mods += replay.mods

        self.controls = VisualizerControls(start_speed, mods, replays)
        self.controls.pause_button.clicked.connect(self.toggle_pause)
        self.controls.play_reverse_button.clicked.connect(self.play_reverse)
        self.controls.play_normal_button.clicked.connect(self.play_normal)
        self.controls.next_frame_button.clicked.connect(
            lambda: self.change_frame(reverse=False))
        self.controls.previous_frame_button.clicked.connect(
            lambda: self.change_frame(reverse=True))
        self.controls.speed_up_button.clicked.connect(self.increase_speed)
        self.controls.speed_down_button.clicked.connect(self.lower_speed)
        self.controls.copy_to_clipboard_button.clicked.connect(
            self.copy_to_clipboard)
        self.controls.time_slider.sliderMoved.connect(self.renderer.seek_to)
        self.controls.time_slider.setRange(self.renderer.playback_start,
                                           self.renderer.playback_end)

        self.controls.raw_view_changed.connect(self.renderer.raw_view_changed)
        self.controls.only_color_keydowns_changed.connect(
            self.renderer.only_color_keydowns_changed)
        self.controls.hitobjects_changed.connect(
            self.renderer.hitobjects_changed)
        self.controls.approach_circles_changed.connect(
            self.renderer.approach_circles_changed)
        self.controls.num_frames_changed.connect(
            self.renderer.num_frames_changed)
        self.controls.draw_hit_error_bar_changed.connect(
            self.renderer.draw_hit_error_bar_changed)
        self.controls.circle_size_mod_changed.connect(
            self.renderer.circle_size_mod_changed)
        self.controls.show_info_for_replay.connect(self.show_info_panel)

        self.splitter = QSplitter()
        # splitter lays widgets horizontally by default, so combine renderer and
        # controls into one single widget vertically
        self.splitter.addWidget(
            Combined([self.renderer, self.controls], Qt.Vertical))

        layout = QGridLayout()
        layout.addWidget(self.splitter, 1, 0, 1, 1)
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)

    def play_normal(self):
        self.unpause()
        self.renderer.play_direction = 1
        self.update_speed(abs(self.renderer.clock.current_speed))

    def update_slider(self, value):
        self.controls.time_slider.setValue(value)

    def change_by(self, delta):
        self.pause()
        self.renderer.seek_to(self.renderer.clock.time_counter + delta)

    def play_reverse(self):
        self.unpause()
        self.renderer.play_direction = -1
        self.update_speed(abs(self.renderer.clock.current_speed))

    def update_speed(self, speed):
        self.renderer.clock.change_speed(speed * self.renderer.play_direction)

    def change_frame(self, reverse):
        self.pause()
        self.renderer.search_nearest_frame(reverse=reverse)

    def toggle_pause(self):
        if self.renderer.paused:
            self.unpause()
        else:
            self.pause()

    def pause(self):
        self.controls.set_paused_state(True)
        self.renderer.pause()

    def unpause(self):
        self.controls.set_paused_state(False)
        self.renderer.resume()

    def lower_speed(self):
        index = self.speeds.index(abs(self.renderer.clock.current_speed))
        if index == 0:
            return
        speed = self.speeds[index - 1]
        self.controls.speed_label.setText(str(speed) + "x")
        self.update_speed(speed)

    def increase_speed(self):
        index = self.speeds.index(abs(self.renderer.clock.current_speed))
        if index == len(self.speeds) - 1:
            return
        speed = self.speeds[index + 1]
        self.controls.speed_label.setText(str(speed) + "x")
        self.update_speed(speed)

    def copy_to_clipboard(self):
        timestamp = int(self.renderer.clock.get_time())
        clipboard = QApplication.clipboard()

        # TODO accomodate arbitrary numbers of replays (including 0 replays)
        r1 = self.replays[0]
        if len(self.replays) == 2:
            r2 = self.replays[1]
            user_str = f"u={r1.user_id}&m1={r1.mods.short_name()}&u2={r2.user_id}&m2={r2.mods.short_name()}"
        else:
            user_str = f"u={r1.user_id}&m1={r1.mods.short_name()}"

        clipboard.setText(
            f"circleguard://m={r1.map_id}&{user_str}&t={timestamp}")

    def show_info_panel(self, replay):
        """
        Shows an info panel containing stats about the replay to the left of the
        renderer. The visualizer window will expand to accomodate for this extra
        space.
        """
        if replay in self.replay_info_cache:
            replay_info = self.replay_info_cache[replay]
            replay_info.show()
        else:
            ur, frametime, snaps, judgments = self.replay_statistics_precalculated[
                replay]
            replay_info = ReplayInfo(replay, self.library.path, ur, frametime,
                                     snaps, judgments, self.snaps_args)
            replay_info.seek_to.connect(self.seek_to)

        # don't show two of the same info panels at once
        if self.current_replay_info is not None:
            # if they're the same, don't change anything
            if replay_info == self.current_replay_info:
                return
            # Otherwise, close the current one and show the new one.
            # simulate a "close" button press
            self.current_replay_info.close_button_clicked.emit()

        def remove_replay_info():
            replay_info.hide()
            self.current_replay_info = None

        replay_info.close_button_clicked.connect(remove_replay_info)
        self.splitter.insertWidget(0, replay_info)
        self.current_replay_info = replay_info
        self.replay_info_cache[replay] = replay_info

    def seek_to(self, time):
        self.pause()
        self.renderer.seek_to(time)

    def calculate_cg_statistics(self):
        cg = KeylessCircleguard()
        for replay in self.replays:
            ur = None
            judgments = None
            if cg.map_available(replay):
                ur = cg.ur(replay)
                judgments = cg.judgments(replay)

            frametime = cg.frametime(replay)
            snaps = cg.snaps(replay, **self.snaps_args)
            self.replay_statistics_precalculated[replay] = (ur, frametime,
                                                            snaps, judgments)