Skip to content

Commit

Permalink
Merge pull request #45 from circleguard/developement
Browse files Browse the repository at this point in the history
Developement
  • Loading branch information
tybug authored Feb 4, 2019
2 parents c6c68ec + 96f24bd commit c49a278
Show file tree
Hide file tree
Showing 23 changed files with 168 additions and 141 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*.DS_Store
.spyproject
osu-ac/secret.py
circleguard/secret.py

## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
Expand Down Expand Up @@ -268,4 +269,4 @@ __pycache__/
*.pyc

# pip nonsense
src/*
src/*
30 changes: 12 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
**PLEASE JOIN OUR DISCORD AND REPORT CHEATERS THERE. Public reporting to /r/osureport will allow for cheaters to overwrite their stolen score before staff can get to them. The link to our discord can be found below**

https://discord.gg/wanBtNY


# osu!anticheat
# Circleguard

This project ultimately aims to create a comprehensive, player-run anticheat. A by no means complete list of cheats includes replay stealing, relax, replay editing, and timewarp.

As of the v1.0 release, we only attempt to detect the first item in that list - replay stealing.

**Disclaimer: Neither the osu!ac organization nor any of the osu!anticheat devs are associated with osu! or the official osu! staff in any way.**
**Disclaimer: Neither the Circleguard organization nor any of the circleguard devs are associated with osu! or the official osu! staff in any way.**

## Getting Started

Expand All @@ -28,39 +23,38 @@ There are two ways to use the program - purely through the CLI, or through a GUI

### CLI

For the former, run the anticheat.py file with some or all of the following flags:
For the former, run the circleguard.py file with some or all of the following flags:

| Flag | Usage |
| --- | --- |
| -h, --help | displays the messages below |
| -m, --map | checks the leaderboard on the given beatmap id against each other |
| -u, --user | checks only the given user against the other leaderboard replays. Must be set with -m |
| -l, --local | compare scores under the user/ directory to a beatmap leaderboard (if set with just -m), a score set by a user on a beatmap (if set with -m and -u) or other locally saved replays (default behavior) |
| -l, --local | compare scores under the replays/ directory to a beatmap leaderboard (if set with -m), a score set by a user on a beatmap (if set with -m and -u) or the other scores in the folder (default behavior) |
| -t, --threshold | sets the similarity threshold to print comparisons that score under it. Defaults to 20 |
| -a, --auto-threshold | sets the number of standard deviations from the average similarity the threshold will automatically be set to. Overrides -t **Note: If more than ![formula](https://latex.codecogs.com/gif.latex?\frac{1}{2}&space;-&space;\frac{1}{2}&space;\mathbf{erf}\frac{a}{\sqrt{2}}) of the input is stolen this may cause false negatives** |
| -n, --number | how many replays to get from a beatmap. No effect if not set with -m. Defaults to 50. **Note: the time complexity of the comparisons scales with O(n^2)** |
| -c, --cache | if set, locally caches replays so they don't have to be redownloaded when checking the same map multiple times |
| --single | compare all replays under user/ with all other replays under user/. No effect if not set with -l |
| -s, --silent | if set, you will not be prompted for a visualization of comparisons under the threshold. Results will still be printed |

| -v, --verify | Takes 3 positional arguments - map id, user1 id and user2 id. Verifies that the scores are steals of each other |

#### Some Examples

```bash
# compares https://osu.ppy.sh/u/1019489's replay on https://osu.ppy.sh/b/1776628 with the 49 other leaderboard replays
$ python anticheat.py -m 1776628 -u 1019489
$ python circleguard.py -m 1776628 -u 1019489

# compares the top 57 leaderboard replays against the other top 57 replays (57 choose 2 comparisons)
$ python anticheat.py -m 1618546 -n 57
$ python circleguard.py -m 1618546 -n 57

# compares the top 50 leaderboard replays against the other top 50 replays (50 choose 2 comparisons) and sets the threshold to be one standard deviation below the average similarity.
$ python anticheat.py -m 1618546 -n 50 -a 1.0
$ python circleguard.py -m 1618546 -n 50 -a 1.0

# compares all replays under user/ with the top 50 scores on https://osu.ppy.sh/b/1611251
$ python anticheat.py -l -m 1611251
$ python circleguard.py -l -m 1611251

# compares all replays under user/ with all replays under compare/
$ python anticheat.py -l
$ python circleguard.py -l
```

This means that if you have a replay from a player and want to see if it's stolen, you should place it in the user/ directory and run with the -l and -m flags.
Expand Down Expand Up @@ -88,7 +82,7 @@ When you click 'run' in the gui, keep an eye on the command line you started the
## Methodology
This program compares the cursor positions of two replays to determine average distance between them. Since the times rarely match up perfectly between replays, the coordinates from one replay are interpolated from its previous and next position to estimate its position at a time identical to the other replay. By doing this we force all timestamps to be identical for easy comparison, at the cost of some precision.

If run with -c (or with the appropriate option checked in the GUI), downloaded replays will be lossily compressed to roughly half their original size with [wtc compression](https://github.com/osu-anticheat/wtc-lzma-compressor). This reduces the need to wait for API ratelimits if run again.
If run with -c (or with the appropriate option checked in the GUI), downloaded replays will be lossily compressed to roughly half their original size with [wtc compression](https://github.com/circleguard/wtc-lzma-compressor) and then stored in a local databsae. This reduces the need to wait for API ratelimits if run again.

## Developement

Expand All @@ -98,4 +92,4 @@ If you have feedback on the program, are interested in contributing, or just wan

## Credits

Thanks to [kszlim](https://github.com/kszlim), whose [replay parser](https://github.com/kszlim/osu-replay-parser) formed the basis of [our modified replay parser](https://github.com/osu-anticheat/osu-replay-parser).
Thanks to [kszlim](https://github.com/kszlim), whose [replay parser](https://github.com/kszlim/osu-replay-parser) formed the basis of [our modified replay parser](https://github.com/circleguard/osu-replay-parser).
8 changes: 4 additions & 4 deletions STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def method(arg1, arg2):
[arg type] [arg name]: [description of arg]
Returns:
[Description of return value]
[Description of return value]
Raises:
[error name]: [description of when this error is raised]
Expand All @@ -56,8 +56,8 @@ Classes documentation is a little different. Classes follow all the same guideli
```python
class Comparer:
"""
A class for managing a set of replay comparisons.
A class for managing a set of replay comparisons.
Attributes:
List replays1: A list of Replay instances to compare against replays2.
List replays2: A list of Replay instances to be compared against.
Expand Down Expand Up @@ -116,4 +116,4 @@ Follow general git conventions when committing:

Pull Requests with messy history may be converted to a squash merge at a mantainer's discretion.

Finally, thou shalt not commit directly to master.
Finally, thou shalt not commit directly to master.
17 changes: 9 additions & 8 deletions osu-ac/argparser.py → circleguard/argparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,23 @@
argparser.add_argument("-u", "--user", dest="user_id",
help="checks only the given user against the other leaderboard replays. Must be set with -m")

argparser.add_argument("-l", "--local", help=("compare scores under the user/ directory to a beatmap leaderboard (if set with -m), "
"a score set by a user on a beatmap (if set with -m and -u) or other locally "
"saved replays (default behavior)"), action="store_true")
argparser.add_argument("-l", "--local", help=("compare scores under the replays/ directory to a beatmap leaderboard (if set with -m), "
"a score set by a user on a beatmap (if set with -m and -u) or the other scores in the folder "
"(default behavior)"), action="store_true")

argparser.add_argument("-t", "--threshold", help="sets the similarity threshold to print results that score under it. Defaults to 20", type=int, default=20)
argparser.add_argument("-t", "--threshold", help="sets the similarity threshold to print results that score under it. Defaults to 20", type=int, default=18)

argparser.add_argument("-a", "--auto", help="Sets the threshold to a number of standard deviations below the average similarity", type=float, dest="stddevs")

argparser.add_argument("-n", "--number", help="how many replays to get from a beatmap. No effect if not set with -m. Must be between 1 and 100 inclusive,"
argparser.add_argument("-n", "--number", help="how many replays to get from a beatmap. No effect if not set with -m. Must be between 2 and 100 inclusive,"
"defaults to 50. NOTE: THE TIME COMPLEXITY OF THE COMPARISONS WILL SCALE WITH O(n^2).", type=int, default=50)

argparser.add_argument("-c", "--cache", help="If set, locally caches replays so they don't have to be redownloaded when checking the same map multiple times.",
action="store_true")

argparser.add_argument("--single", help="Compare all replays under user/ with all other replays under user/. No effect if not set with -l",
action="store_true")

argparser.add_argument("-s", "--silent", help="If set, you will not be prompted for a visualization of comparisons under the threshold",
action="store_true")

argparser.add_argument("-v", "--verify", help="Takes 3 positional arguments - map id, user1 id and user2 id. Verifies that the scores are steals of each other", nargs=3)

argparser.add_argument("--version", help="Prints the program version", action="store_true")
16 changes: 12 additions & 4 deletions osu-ac/cacher.py → circleguard/cacher.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,22 @@ def cache(self, map_id, user_id, lzma_bytes, replay_id):
else: # else just insert
self.write("INSERT INTO replays VALUES(?, ?, ?, ?)", [map_id, user_id, compressed_bytes, replay_id])

def revalidate(self, map_id, user_to_replay):
def revalidate(self, map_id, user_info):
"""
Re-caches a stored replay if one of the given users has overwritten their score on the given map since it was cached.
Args:
String map_id: The map to revalidate.
Dictionary user_to_replay: The up tp date mapping of user_id to replay_id to revalidate.
Dictionary user_info: The up to date mapping of user_id to [username, replay_id, enabled_mods] to revalidate.
Only contains information for a single map.
"""

result = self.cursor.execute("SELECT user_id, replay_id FROM replays WHERE map_id=?", [map_id]).fetchall()

# filter result to only contain entries also in user_info
result = [info for info in result if info[0] in user_info.keys()]
for user_id, local_replay_id in result:
online_replay_id = user_to_replay[user_id]
online_replay_id = user_info[user_id][1]
if(local_replay_id != online_replay_id): # local (outdated) id does not match online (updated) id
print("replay outdated, redownloading...", end="")
# this **could** conceivable be the source of a logic error by Loader.replay_data returning None and the cache storing None,
Expand All @@ -85,7 +89,11 @@ def check_cache(self, map_id, user_id):
"""

result = self.cursor.execute("SELECT replay_data FROM replays WHERE map_id=? AND user_id=?", [map_id, user_id]).fetchone()
return wtc.decompress(result[0]) if result else None
if(result):
print("Loading replay by {} from cache".format(user_id))
return wtc.decompress(result[0])

return None

def write(self, statement, args):
"""
Expand Down
63 changes: 39 additions & 24 deletions osu-ac/anticheat.py → circleguard/circleguard.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

import sys
import itertools
import os
from os.path import isfile, join

from argparser import argparser
from draw import Draw
Expand All @@ -18,45 +20,63 @@
from comparer import Comparer
from investigator import Investigator
from cacher import Cacher
from config import PATH_REPLAYS_USER, PATH_REPLAYS_CHECK, WHITELIST
from config import PATH_REPLAYS_STUB, WHITELIST, VERSION

class Anticheat:
class Circleguard:

def __init__(self, args):
"""
Initializes an Anticheat instance.
Initializes a Circleguard instance.
[SimpleNamespace or argparse.Namespace] args:
A namespace-like object representing how and what to compare. An example may look like
`Namespace(cache=False, local=False, map_id=None, number=50, threshold=20, user_id=None)`
"""

# get all replays in path to check against. Load this per circleguard instance or users moving files around while the gui is open doesn't work.
self.PATH_REPLAYS = [join(PATH_REPLAYS_STUB, f) for f in os.listdir(PATH_REPLAYS_STUB) if isfile(join(PATH_REPLAYS_STUB, f)) and f != ".DS_Store"]

self.cacher = Cacher(args.cache)
self.args = args
if(args.map_id):
self.users_info = Loader.users_info(args.map_id, args.number)
if(args.user_id and args.map_id):
user_info = Loader.user_info(args.map_id, args.user_id)
self.replays_check = [OnlineReplay.from_map(self.cacher, args.map_id, args.user_id, user_info[args.user_id][0], user_info[args.user_id][1])]
user_info = Loader.user_info(args.map_id, args.user_id)[args.user_id] # should be guaranteed to only be a single mapping of user_id to a list
self.replays_check = [OnlineReplay.from_map(self.cacher, args.map_id, args.user_id, user_info[0], user_info[1], user_info[2])]

def run(self):
"""
Starts loading and detecting replays based on the args passed through the command line.
"""

if(self.args.local):
if(self.args.verify):
self._run_verify()
elif(self.args.local):
self._run_local()
elif(self.args.map_id):
self._run_map()
else:
print("Please set either --local (-l) or --map (-m)! ")
sys.exit(1)
print("Please set either --local (-l), --map (-m), or --verify (-v)! ")

def _run_verify(self):
args = self.args

map_id = self.args.verify[0]
user1_id = self.args.verify[1]
user2_id = self.args.verify[2]

user1_info = Loader.user_info(map_id, user1_id)
user2_info = Loader.user_info(map_id, user2_id)
replay1 = OnlineReplay.from_user_info(self.cacher, map_id, user1_info)
replay2 = OnlineReplay.from_user_info(self.cacher, map_id, user2_info)

comparer = Comparer(args.threshold, args.silent, replay1, replays2=replay2, stddevs=args.stddevs)
comparer.compare(mode="double")

def _run_local(self):

args = self.args
# get all local user replays (used in every --local case)
replays1 = [LocalReplay.from_path(osr_path) for osr_path in PATH_REPLAYS_USER]
# get all local replays (used in every --local case)
replays1 = [LocalReplay.from_path(osr_path) for osr_path in self.PATH_REPLAYS]

threshold = args.threshold
stddevs = args.stddevs
Expand All @@ -72,18 +92,9 @@ def _run_local(self):
comparer = Comparer(threshold, args.silent, replays1, replays2=replays2, stddevs=stddevs)
comparer.compare(mode="double")
return

if(args.single):
# checks every replay listed in PATH_REPLAYS_USER against every other replay there
comparer = Comparer(threshold, stddevs, args.silent, replays1)
comparer.compare(mode="single")
return
else:
# checks every replay listed in PATH_REPLAYS_USER against every replay listed in PATH_REPLAYS_CHECK
replays2 = [LocalReplay.from_path(osr_path) for osr_path in PATH_REPLAYS_CHECK]
comparer = Comparer(threshold, args.silent, replays1, replays2=replays2, stddevs=stddevs)
comparer.compare(mode="double")
return
comparer = Comparer(threshold, args.silent, replays1, stddevs=stddevs)
comparer.compare(mode="single")

def _run_map(self):

Expand All @@ -109,5 +120,9 @@ def _run_map(self):
return

if __name__ == '__main__':
anticheat = Anticheat(argparser.parse_args())
anticheat.run()
args = argparser.parse_args()
if(args.version):
print("Circleguard {}".format(VERSION))
sys.exit(0)
circleguard = Circleguard(args)
circleguard.run()
27 changes: 15 additions & 12 deletions osu-ac/comparer.py → circleguard/comparer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from draw import Draw
from replay import Replay
from config import WHITELIST

from exceptions import InvalidArgumentsException
class Comparer:
"""
A class for managing a set of replay comparisons.
Expand Down Expand Up @@ -42,9 +42,11 @@ def __init__(self, threshold, silent, replays1, replays2=None, stddevs=None):
self.stddevs = stddevs
self.silent = silent

self.replays1 = replays1
self.replays2 = replays2
# filter beatmaps we had no data for - see Loader.replay_data and OnlineReplay.from_map
self.replays1 = [replay for replay in replays1 if replay is not None]

if(replays2):
self.replays2 = [replay for replay in replays2 if replay is not None]

def compare(self, mode):
"""
Expand All @@ -56,17 +58,18 @@ def compare(self, mode):
String mode: One of either "double" or "single", determining how to choose which replays to compare.
"""

if(not self.replays1): # if this is empty, bad things
print("No comparisons could be made. Make sure replay data is available for your args")
return

if(mode == "double"):
print("comparing first set of replays to second set of replays")
iterator = itertools.product(self.replays1, self.replays2)
elif (mode == "single"):
print("comparing first set of replays to itself")
iterator = itertools.combinations(self.replays1, 2)
else:
raise Exception("`mode` must be one of 'double' or 'single'")


raise InvalidArgumentsException("`mode` must be one of 'double' or 'single'")

print("Starting to compare replays")
# automatically determine threshold based on standard deviations of similarities if stddevs is set
if(self.stddevs):
results = {}
Expand Down Expand Up @@ -126,13 +129,13 @@ def _print_result(self, result, replay1, replay2):
if(mean > self.threshold):
return

# if they were both set online, we don't get dates from
first_score = None
# if they were both set locally, we don't get replay ids to compare
last_score = None
if(replay1.replay_id and replay2.replay_id):
first_score = replay1.player_name if(replay1.replay_id < replay2.replay_id) else replay2.player_name
last_score = replay1.player_name if(replay1.replay_id > replay2.replay_id) else replay2.player_name

print("{:.1f} similarity, {:.1f} std deviation ({} vs {}{})"
.format(mean, sigma, replay1.player_name, replay2.player_name, " - {} set first".format(first_score) if first_score else ""))
.format(mean, sigma, replay1.player_name, replay2.player_name, " - {} set later".format(last_score) if last_score else ""))

if(self.silent):
return
Expand Down
18 changes: 18 additions & 0 deletions circleguard/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import pathlib

from secret import API_KEY

PATH_ROOT = pathlib.Path(__file__).parent
PATH_REPLAYS_STUB = PATH_ROOT / "replays"

API_BASE = "https://osu.ppy.sh/api/"
API_REPLAY = API_BASE + "get_replay?k=" + API_KEY + "&m=0&b={}&u={}"
API_SCORES_ALL = API_BASE + "get_scores?k=" + API_KEY + "&m=0&b={}&limit={}"
API_SCORES_USER = API_BASE + "get_scores?k=" + API_KEY + "&m=0&b={}&u={}"

# cookiezi, ryuk, rafis, azr8, toy,
WHITELIST = ["124493", "6304246", "2558286", "2562987", "2757689"]

PATH_DB = PATH_ROOT / "db" / "cache.db" # /absolute/path/db/cache.db

VERSION = "1.1"
File renamed without changes.
Loading

0 comments on commit c49a278

Please sign in to comment.