From 1dfc17f9d6495b3ce5414d9b9de8a375e43b0d79 Mon Sep 17 00:00:00 2001 From: "Flemming N. Larsen" Date: Sun, 1 Nov 2026 15:33:52 +0100 Subject: [PATCH] Support HIDENAMES and CLEARDATA as rumble settings, and accept client 1.12.0 --- HandleQueuedResults.py | 15 ++++++++- structures.py | 4 ++- test_e2e.py | 69 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/HandleQueuedResults.py b/HandleQueuedResults.py index 5075983..91bd425 100644 --- a/HandleQueuedResults.py +++ b/HandleQueuedResults.py @@ -15,6 +15,11 @@ import structures from structures import global_dict, load_blob +def upload_setting(results, key): + # An upload without the setting means it is off. The client ends each upload + # with a line break, which stays on the last field. + return results.get(key, "").strip() == "YES" + def handle_queued_results(): out = [] results = json.loads(request.get_data(as_text=True)) @@ -44,6 +49,8 @@ def handle_queued_results(): Name=rumble, Rounds=int(results["rounds"]), Field=results["field"], Melee=results["melee"] == "YES", Teams=results["teams"] == "YES", TotalUploads=0, + HideNames=upload_setting(results, "hidenames"), + ClearData=upload_setting(results, "cleardata"), MeleeSize=10) out.append("CREATED NEW GAME TYPE " + rumble + "\n") logging.info("Created new game type: " + rumble) @@ -52,7 +59,9 @@ def handle_queued_results(): rounds = (game.Rounds == int(results["rounds"])) teams = game.Teams == (results["teams"] == "YES") melee = game.Melee == (results["melee"] == "YES") - allowed = field and rounds and teams and melee + hidenames = game.HideNames == upload_setting(results, "hidenames") + cleardata = game.ClearData == upload_setting(results, "cleardata") + allowed = field and rounds and teams and melee and hidenames and cleardata if not allowed: errstr = "OK. ERROR. Incorrect " + rumble + " config: " errorReasons = [] @@ -64,6 +73,10 @@ def handle_queued_results(): errorReasons.append("teams ") if not melee: errorReasons.append("melee ") + if not hidenames: + errorReasons.append("hidenames ") + if not cleardata: + errorReasons.append("cleardata ") logging.error(errstr + ", ".join(errorReasons) + " User: " + results["user"]) return "".join(out) diff --git a/structures.py b/structures.py index 12ded99..0fda990 100644 --- a/structures.py +++ b/structures.py @@ -71,7 +71,7 @@ def load_blob(blob, default): total = "TOTAL" participants = "PARTICIPANTS" sync = "SYNCHRONIZE" -allowed_clients = ["1.11.2"] +allowed_clients = ["1.11.2", "1.12.0"] allowed_versions = ["1"] global_dict = {} @@ -130,6 +130,8 @@ class Rumble(db.Model): MeleeSize = db.IntegerProperty(indexed = False) Rounds = db.IntegerProperty(indexed = False) Field = db.StringProperty(indexed = False) + HideNames = db.BooleanProperty(indexed = False, default = False) + ClearData = db.BooleanProperty(indexed = False, default = False) Name = db.StringProperty(indexed = False) # key_name TotalUploads = db.IntegerProperty(indexed = False) Participants = db.StringListProperty(indexed = False) diff --git a/test_e2e.py b/test_e2e.py index dac6f65..04a1d5e 100644 --- a/test_e2e.py +++ b/test_e2e.py @@ -19,6 +19,7 @@ import wsgi_local # noqa: E402 (imports gae_shim first, starts background threads) import background # noqa: E402 +import store # noqa: E402 import structures # noqa: E402 from google.appengine.api import taskqueue # noqa: E402 (the shim) @@ -28,10 +29,10 @@ GAME = "TestRumble" -def make_battle(fname, sname, fscore, sscore): +def make_battle(fname, sname, fscore, sscore, game=GAME, settings=""): """A minimal valid /UploadedResults body (a 1v1 battle, A-vs-B).""" fields = { - "client": "1.11.2", "version": "1", "game": GAME, + "client": "1.12.0", "version": "1", "game": game, "rounds": "35", "field": "1000x1000", "melee": "NO", "teams": "NO", "fname": fname, "sname": sname, "fscore": str(fscore), "sscore": str(sscore), @@ -40,17 +41,25 @@ def make_battle(fname, sname, fscore, sscore): "ssurvival": "0" if fscore >= sscore else "35", "user": "tester", "time": str(int(time.time() * 1000)), } - return "&".join("%s=%s" % kv for kv in fields.items()) + return "&".join("%s=%s" % kv for kv in fields.items()) + settings -def upload(fname, sname, fscore, sscore): - r = client.post("/UploadedResults", data=make_battle(fname, sname, fscore, sscore)) +def upload(fname, sname, fscore, sscore, game=GAME, settings=""): + r = client.post("/UploadedResults", data=make_battle(fname, sname, fscore, sscore, game, settings)) body = r.get_data(as_text=True) assert r.status_code == 200 and "OK" in body, (r.status_code, body) assert "added to queue" in body, "upload not accepted: " + body return body +def battles(game): + """The battles of the Alpha-Bravo pairing, which unlike a bot's total never drops when an opponent retires.""" + for pairing in store.load_pairings(game, "Alpha"): + if pairing.Name == "Bravo": + return int(pairing.Battles) + return 0 + + def get_json(path): r = client.get(path) assert r.status_code == 200, (path, r.status_code) @@ -277,6 +286,56 @@ def first_bot(path): assert without_header == 403, without_header assert with_header == 200, with_header + # === Scenario: a rumble stored before these settings existed has them off === + # GIVEN a stored rumble without the settings + # WHEN it is read + old = structures.Rumble._from_row("OldRumble", {"Name": "OldRumble"}) + # THEN both settings are off + assert old.HideNames is False and old.ClearData is False, old + + # === Scenario: an upload without the settings works as before === + # GIVEN a rumble with the settings off + before = battles(GAME) + # WHEN an upload without the settings arrives + upload("Alpha", "Bravo", 60, 40) + background.drain() + # THEN it counts + assert battles(GAME) == before + 1, battles(GAME) + + # === Scenario: a rumble with the settings off rejects results with them on === + # GIVEN the same rumble + before = battles(GAME) + # WHEN uploads with either setting on arrive, the first as the client sends it + upload("Alpha", "Bravo", 60, 40, settings="&hidenames=YES\n") + upload("Alpha", "Bravo", 60, 40, settings="&cleardata=YES") + background.drain() + # THEN neither counts + assert battles(GAME) == before, battles(GAME) + + # === Scenario: a new rumble takes the settings from its first upload === + # GIVEN no rumble called HiddenRumble + # WHEN the first upload has both settings on, as the client sends it + both = "&cleardata=YES&hidenames=YES\r\n" + upload("Alpha", "Bravo", 60, 40, game="HiddenRumble", settings=both) + background.drain() + # THEN the rumble has them on, and the battle counts + hidden = structures.Rumble.get_by_key_name("HiddenRumble") + assert hidden.HideNames is True and hidden.ClearData is True, hidden + assert battles("HiddenRumble") == 1, battles("HiddenRumble") + + # === Scenario: a rumble with the settings on only takes results with them on === + # GIVEN that rumble + # WHEN uploads with no settings, or only one of them, arrive + upload("Alpha", "Bravo", 60, 40, game="HiddenRumble") + upload("Alpha", "Bravo", 60, 40, game="HiddenRumble", settings="&hidenames=YES\n") + upload("Alpha", "Bravo", 60, 40, game="HiddenRumble", settings="&cleardata=YES") + background.drain() + # THEN they do not count, but one with both on does + assert battles("HiddenRumble") == 1, battles("HiddenRumble") + upload("Alpha", "Bravo", 61, 39, game="HiddenRumble", settings=both) + background.drain() + assert battles("HiddenRumble") == 2, battles("HiddenRumble") + print("test_e2e OK")