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 32 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
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,13 @@ For the former, run the anticheat.py file with some or all of the following flag
| -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

Expand Down
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.
52 changes: 31 additions & 21 deletions osu-ac/anticheat.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
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, WHITELIST, VERSION

class Anticheat:

Expand All @@ -36,27 +36,42 @@ def __init__(self, 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 PATH_REPLAYS]

threshold = args.threshold
stddevs = args.stddevs
Expand All @@ -72,18 +87,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 +115,9 @@ def _run_map(self):
return

if __name__ == '__main__':
anticheat = Anticheat(argparser.parse_args())
args = argparser.parse_args()
if(args.version):
print("osu!anticheat {}".format(VERSION))
sys.exit(0)
anticheat = Anticheat(args)
anticheat.run()
17 changes: 9 additions & 8 deletions osu-ac/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
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
27 changes: 15 additions & 12 deletions osu-ac/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
12 changes: 4 additions & 8 deletions osu-ac/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,10 @@
from secret import API_KEY

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

# names of replays to check
PATH_REPLAYS_USER_STUB = join(PATH_REPLAYS, "user")
PATH_REPLAYS_CHECK_STUB = join(PATH_REPLAYS, "compare") # path of replays to check against

PATH_REPLAYS_USER = [join(PATH_REPLAYS_USER_STUB, f) for f in os.listdir(PATH_REPLAYS_USER_STUB) if isfile(join(PATH_REPLAYS_USER_STUB, f)) and f != ".DS_Store"]
# get all replays in path to check against
PATH_REPLAYS_CHECK = [join(PATH_REPLAYS_CHECK_STUB, f) for f in os.listdir(PATH_REPLAYS_CHECK_STUB) if isfile(join(PATH_REPLAYS_CHECK_STUB, f)) and f != ".DS_Store"]

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"]

API_BASE = "https://osu.ppy.sh/api/"
API_REPLAY = API_BASE + "get_replay?k=" + API_KEY + "&m=0&b={}&u={}"
Expand All @@ -25,3 +19,5 @@
WHITELIST = ["124493", "6304246", "2558286", "2562987", "2757689"]

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

VERSION = "1.0d"
2 changes: 2 additions & 0 deletions osu-ac/draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ def run(self):
plot1 = plt.plot('x', 'y', "red", animated=True, label=self.replay1.player_name)[0]
plot2 = plt.plot('', '', "blue", animated=True, label=self.replay2.player_name)[0]

fig.legend()

def init():
ax.set_xlim(0, 512)
ax.set_ylim(0, 384)
Expand Down
7 changes: 4 additions & 3 deletions osu-ac/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

# strings taken from osu api error responses
class Error(Enum):
NO_REPLAY = "Replay not available."
RATELIMITED = "Requesting too fast! Slow your operation, cap'n!"
UNKOWN = "Unkown error."
NO_REPLAY = "Replay not available."
RATELIMITED = "Requesting too fast! Slow your operation, cap'n!"
RETRIEVAL_FAILED = "Replay retrieval failed."
UNKOWN = "Unkown error."

class Mod(Enum):
NoMod = 0
Expand Down
8 changes: 8 additions & 0 deletions osu-ac/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class AnticheatException(Exception):
"""Base class for exceptions in the anticheat program."""

class InvalidArgumentsException(AnticheatException):
"""Indicates an invalid argument was passed to one of the flags."""

class APIException(AnticheatException):
"""Indicates some error on the API's end that we were not prepared to handle."""
12 changes: 5 additions & 7 deletions osu-ac/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ def run():

_number = num.get()
_cache = cache.get()
_single = single.get()
_silent = silent.get()

_silent = True # Visualizations do very very bad things when not called from the main thread, so when using gui, we just...force ignore them
_verify = verify.get()

def run_anticheat():
anticheat = Anticheat(SimpleNamespace(map_id=_map_id, user_id=_user_id, local=_local, threshold=_threshold, stddevs=_stddevs,
number=_number, cache=_cache, single=_single, silent=_silent))
number=_number, cache=_cache, silent=_silent, verify=_verify))
anticheat.run()

thread = threading.Thread(target=run_anticheat)
Expand Down Expand Up @@ -56,8 +55,7 @@ def run_anticheat():
cache = tkinter.BooleanVar(value=False)

# unimplemented
single = tkinter.BooleanVar(value=False)
silent = tkinter.BooleanVar(value=False)
verify = tkinter.BooleanVar(value=False)

# Make visual elements for main frame
map_label = ttk.Label(main, text="Map id:")
Expand Down Expand Up @@ -98,7 +96,7 @@ def run_anticheat():
top_plays_label1.grid(row=0, column=1)
top_plays_entry = ttk.Entry(top_x_plays, width=4, textvariable=num)
top_plays_entry.grid(row=0, column=2)
top_plays_label2 = ttk.Label(top_x_plays, text="leaderboard plays?\n(Between 1 and 100 inclusive)")
top_plays_label2 = ttk.Label(top_x_plays, text="leaderboard plays?\n(Between 2 and 100 inclusive)")
top_plays_label2.grid(row=0, column=3)

auto_threshold = ttk.Frame(options)
Expand Down
Loading