Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Developement #45

Merged
merged 40 commits into from
Feb 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
726eb19
implement --verify flag
tybug Feb 1, 2019
7e3f60e
catch retrieval failed api response, refactor enum checks
tybug Feb 1, 2019
a866127
move replay filtering to comparer. Fixes #29
tybug Feb 1, 2019
7132fcd
Merge pull request #33 from osu-anticheat/compare-error
tybug Feb 1, 2019
e300cb5
Merge pull request #34 from osu-anticheat/dl-error
tybug Feb 1, 2019
463cba7
Merge pull request #30 from osu-anticheat/verify-redo
tybug Feb 1, 2019
78107f2
highlight last replay not first
tybug Feb 1, 2019
76d5843
print username for online replays instead of id
tybug Feb 1, 2019
12a29f9
Merge pull request #35 from osu-anticheat/usernames
tybug Feb 1, 2019
b6d01be
don't compare when iterator has no elements. Fixes #36
tybug Feb 1, 2019
f73e310
Merge branch 'developement' of https://github.com/osu-anticheat/osu-a…
tybug Feb 1, 2019
38ae884
remove debug statement
tybug Feb 2, 2019
239be6e
fix --single flag
tybug Feb 2, 2019
486d1b7
make --single default behavior for -l, force --silent on gui
tybug Feb 2, 2019
1175752
require -n to be at least 2
tybug Feb 2, 2019
e99d09d
remove debugging code
tybug Feb 2, 2019
6c5a87c
add --version flag
tybug Feb 2, 2019
82bb2f6
Merge pull request #38 from osu-anticheat/restrict-mods
tybug Feb 2, 2019
74cb983
change default threshold to 18
tybug Feb 2, 2019
f440762
Merge branch 'developement' of https://github.com/osu-anticheat/osu-a…
tybug Feb 2, 2019
e0e671c
reinsert visualization legend
samuelhklumpers Feb 2, 2019
a00cbd0
fix top n leaderboard label
samuelhklumpers Feb 2, 2019
9ecdfd6
fix fetch <2 or >100 error message
samuelhklumpers Feb 2, 2019
dbf904a
Merge pull request #39 from osu-anticheat/textfixes
samuelhklumpers Feb 2, 2019
ac6fc09
raise properly subclassed exception types
tybug Feb 2, 2019
f75fe9f
fix loading from cache
tybug Feb 2, 2019
30c2c6e
use specific versions instead of master branches for pip
tybug Feb 2, 2019
72b9f05
pass replay_id when creating Replay from cache
tybug Feb 3, 2019
ad55e54
only attempt to revalidate users actually stored in db
tybug Feb 3, 2019
209a4db
Merge pull request #42 from osu-anticheat/compare-fix
tybug Feb 3, 2019
553787f
remove debugging
tybug Feb 3, 2019
5dae8c8
Merge pull request #44 from osu-anticheat/compare-fix
tybug Feb 3, 2019
b120098
bump version
tybug Feb 3, 2019
4874c00
Merge branch 'developement' of https://github.com/osu-anticheat/osu-a…
tybug Feb 3, 2019
6f15c77
update dependencies for org name change
tybug Feb 3, 2019
c771258
update org name to circleguard in readme
tybug Feb 3, 2019
bc277ec
missed one...
tybug Feb 3, 2019
7e068d3
remove any mention of anticheat in code
tybug Feb 3, 2019
4af63c5
re-ignore secret.py
tybug Feb 3, 2019
96f24bd
delete secret.py (oops)
tybug Feb 3, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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