diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index af6eb4e1..a6a319d9 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -24,7 +24,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: config-file: ./.github/codeql/codeql-config.yml # Override language selection by uncommenting this and choosing your languages @@ -34,7 +34,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -48,4 +48,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/alembic/versions/fe5e615ae090_add_game_history_table.py b/alembic/versions/fe5e615ae090_add_game_history_table.py new file mode 100644 index 00000000..373afb73 --- /dev/null +++ b/alembic/versions/fe5e615ae090_add_game_history_table.py @@ -0,0 +1,57 @@ +"""add game history table + +Revision ID: fe5e615ae090 +Revises: de5d615ae090 +Create Date: 2023-02-28 19:33:02.808038 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.engine.reflection import Inspector +from sqlalchemy.sql.expression import func + +conn = op.get_bind() +inspector = Inspector.from_engine(conn) +tables = inspector.get_table_names() + +# revision identifiers, used by Alembic. +revision = "fe5e615ae090" +down_revision = "de5d615ae090" +branch_labels = None +depends_on = None + + +def _table_has_column(table, column): + has_column = False + for col in inspector.get_columns(table): + if column not in col["name"]: + continue + has_column = True + return has_column + + +def _has_table(table_name): + tables = inspector.get_table_names() + return table_name in tables + + +def upgrade(): + if not _has_table("game_history"): + op.create_table( + "game_history", + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("created", sa.DateTime, nullable=True), + sa.Column( + "team_id", + sa.Integer, + sa.ForeignKey("team.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column("_type", sa.VARCHAR(length=20), nullable=False), + sa.Column("_value", sa.Integer, default=0, nullable=False), + ) + + +def downgrade(): + if _has_table("game_history"): + op.drop_table("game_history") diff --git a/alembic/versions/ffe623ae412_delete_snapshot_tables.py b/alembic/versions/ffe623ae412_delete_snapshot_tables.py new file mode 100644 index 00000000..c42cc4d4 --- /dev/null +++ b/alembic/versions/ffe623ae412_delete_snapshot_tables.py @@ -0,0 +1,119 @@ +"""delete snapshot table + +Revision ID: ffe623ae412 +Revises: fe5e615ae090 +Create Date: 2023-03-11 19:33:02.808038 + +""" +import sqlalchemy as sa +from datetime import datetime +from alembic import op +from sqlalchemy.engine.reflection import Inspector +from sqlalchemy.sql.expression import func + +conn = op.get_bind() +inspector = Inspector.from_engine(conn) +tables = inspector.get_table_names() + +# revision identifiers, used by Alembic. +revision = "ffe623ae412" +down_revision = "fe5e615ae090" +branch_labels = None +depends_on = None + +history = {} +flag_count = {} + +def _table_has_column(table, column): + has_column = False + for col in inspector.get_columns(table): + if column not in col["name"]: + continue + has_column = True + return has_column + + +def _has_table(table_name): + tables = inspector.get_table_names() + return table_name in tables + +def add_history(created, team_id, reason, value): + conn = op.get_bind() + conn.execute(f"INSERT INTO game_history (created, team_id, _type, _value) VALUES ('{created}', {team_id}, '{reason}', {value});") + +def check_flag(item): + team_id = item[0] + created = datetime.now() + if str(team_id) in flag_count: + flag_count[str(team_id)] += 1 + else: + add_history(created, team_id, "flag_count", 0) + flag_count[str(team_id)] = 1 + add_history(created, team_id, "flag_count", flag_count[str(team_id)]) + +def check_history(item): + created = item[1] + team_id = item[2] + money = item[3] + bots = item[4] + if str(team_id) in history: + team = history[str(team_id)] + if bots != team["bots"]: + history[str(team_id)]["bots"] = bots + add_history(created, team_id, "bot_count", bots) + elif money != team["money"]: + history[str(team_id)]["money"] = money + add_history(created, team_id, "score", money) + return + else: + add_history(created, team_id, "bot_count", 0) + add_history(created, team_id, "start", 0) + if bots > 0: + add_history(created, team_id, "bot_count", 0) + if money > 0: + add_history(created, team_id, "score", money) + history[str(team_id)] = {"money": money, "bots": bots} + +def upgrade(): + try: + conn = op.get_bind() + res = conn.execute("SELECT * FROM snapshot_team;") + results = res.fetchall() + i = 0 + for item in results: + check_history(item) + i += 1 + if i > 0: + conn.execute("COMMIT;") + + except Exception as e: + print("Failed to import prior snapshot data into game history: %s" % str(e)) + print("Continuing...") + try: + res = conn.execute("SELECT * FROM team_to_flag;") + results = res.fetchall() + i = 0 + for item in results: + check_flag(item) + i += 1 + if i > 0: + conn.execute("COMMIT;") + except Exception as e: + print("Failed to import prior flag count into game history: %s" % str(e)) + print("Continuing...") + + if _has_table("snapshot_to_snapshot_team"): + op.drop_table("snapshot_to_snapshot_team") + if _has_table("snapshot_team_to_game_level"): + op.drop_table("snapshot_team_to_game_level") + if _has_table("snapshot_team_to_flag"): + op.drop_table("snapshot_team_to_flag") + if _has_table("snapshot_team"): + op.drop_table("snapshot_team") + if _has_table("snapshot"): + op.drop_table("snapshot") + + +def downgrade(): + print("No downgrade for this") + pass diff --git a/handlers/AdminHandlers/AdminGameHandlers.py b/handlers/AdminHandlers/AdminGameHandlers.py index 53f13b29..b1d7b782 100644 --- a/handlers/AdminHandlers/AdminGameHandlers.py +++ b/handlers/AdminHandlers/AdminGameHandlers.py @@ -40,8 +40,6 @@ from models.Team import Team from models.Theme import Theme from models.Penalty import Penalty -from models.Snapshot import Snapshot -from models.SnapshotTeam import SnapshotTeam from models.SourceCode import SourceCode from models.Corporation import Corporation from models.Category import Category @@ -52,9 +50,8 @@ from libs.StringCoding import encode, decode from libs.ValidationError import ValidationError from libs.ConfigHelpers import save_config -from libs.GameHistory import GameHistory from libs.ConsoleColors import * -from libs.Scoreboard import score_bots +from libs.Scoreboard import score_bots, Scoreboard from handlers.BaseHandlers import BaseHandler from string import printable from setup.xmlsetup import import_xml @@ -644,14 +641,6 @@ def refresh_app_config(self): logging.info("Starting botnet callback function") self.application.settings["score_bots_callback"].start() - logging.info("Restarting history callback function") - game_history = GameHistory.instance() - self.application.settings["history_callback"].stop() - self.application.history_callback = PeriodicCallback( - game_history.take_snapshot, options.history_snapshot_interval - ) - self.application.settings["history_callback"].start() - class AdminResetHandler(BaseHandler): @restrict_ip_address @@ -676,10 +665,11 @@ def post(self, *args, **kwargs): user.money = 0 teams = Team.all() for team in teams: + team.game_history = [] if options.banking: - team.money = options.starting_team_money + team.set_score("start", options.starting_team_money) else: - team.money = 0 + team.set_score("start", 0) team.flags = [] team.hints = [] team.boxes = [] @@ -706,16 +696,6 @@ def post(self, *args, **kwargs): for swat in swats: self.dbsession.delete(swat) self.dbsession.commit() - snapshot = Snapshot.all() - for snap in snapshot: - self.dbsession.delete(snap) - self.dbsession.commit() - snapshot_team = SnapshotTeam.all() - for snap in snapshot_team: - self.dbsession.delete(snap) - self.dbsession.commit() - game_history = GameHistory.instance() - game_history.take_snapshot() # Take starting snapshot flags = Flag.all() for flag in flags: # flag.value = flag.value allows a fallback to when original_value was used @@ -725,6 +705,7 @@ def post(self, *args, **kwargs): self.dbsession.add(flag) self.dbsession.commit() self.dbsession.flush() + Scoreboard.update_gamestate(self) self.event_manager.push_score_update() self.flush_memcached() success = "Successfully Reset Game" @@ -769,22 +750,12 @@ def post(self, *args, **kwargs): for swat in swats: self.dbsession.delete(swat) self.dbsession.commit() - snapshot = Snapshot.all() - for snap in snapshot: - self.dbsession.delete(snap) - self.dbsession.commit() - snapshot_team = SnapshotTeam.all() - for snap in snapshot_team: - self.dbsession.delete(snap) - self.dbsession.commit() for user in users: self.dbsession.delete(user) self.dbsession.commit() for team in teams: self.dbsession.delete(team) self.dbsession.commit() - game_history = GameHistory.instance() - game_history.take_snapshot() # Take starting snapshot flags = Flag.all() for flag in flags: # flag.value = flag.value allows a fallback to when original_value was used @@ -794,6 +765,7 @@ def post(self, *args, **kwargs): self.dbsession.add(flag) self.dbsession.commit() self.dbsession.flush() + Scoreboard.update_gamestate(self) self.event_manager.push_score_update() self.flush_memcached() if options.teams: diff --git a/handlers/AdminHandlers/AdminGameObjectHandlers.py b/handlers/AdminHandlers/AdminGameObjectHandlers.py index 68703b42..12a35d80 100644 --- a/handlers/AdminHandlers/AdminGameObjectHandlers.py +++ b/handlers/AdminHandlers/AdminGameObjectHandlers.py @@ -123,7 +123,9 @@ def create_team(self): team.name = self.get_argument("team_name", "") team.motto = self.get_argument("motto", "") if not self.config.banking: - team.money = 0 + team.set_score("start", 0) + else: + team.set_score("start", options.starting_team_money) level_0 = GameLevel.by_number(0) if not level_0: level_0 = GameLevel.all()[0] @@ -429,7 +431,7 @@ def post(self, *args, **kwargs): if penalty: value = penalty.cost() if value > 0: - team.money += value + team.set_score("penalty", value + team.money) if user: user.money += value self.dbsession.add(user) @@ -451,15 +453,15 @@ def post(self, *args, **kwargs): for item in Flag.team_captures(flag.id): tm = Team.by_id(item[0]) deduction = flag.dynamic_value(tm) - flag_value - tm.money = int(tm.money - deduction) + tm.set_score("decay", int(tm.money - deduction)) self.dbsession.add(tm) self.event_manager.flag_decayed(tm, flag) - team.money += flag_value + team.set_score("flag", flag_value + team.money) if user: user.money += flag_value user.flags.append(flag) self.dbsession.add(user) - team.flags.append(flag) + team.add_flag(flag) self.dbsession.add(team) self.dbsession.commit() BoxHandler.success_capture(self, user, flag, flag_value) diff --git a/handlers/AdminHandlers/AdminUserHandlers.py b/handlers/AdminHandlers/AdminUserHandlers.py index 1cb78c49..8f7ca444 100644 --- a/handlers/AdminHandlers/AdminUserHandlers.py +++ b/handlers/AdminHandlers/AdminUserHandlers.py @@ -64,11 +64,11 @@ def post(self, *args, **kwargs): if group == "all": teams = Team.all() for team in teams: - team.money += value + team.set_score("admin", value + team.money) self.dbsession.add(team) else: team = Team.by_uuid(group) - team.money += value + team.set_score("admin", value + team.money) self.dbsession.add(team) self.dbsession.commit() self.event_manager.admin_score_update(team, message, value) @@ -96,7 +96,7 @@ def edit_team(self): raise ValidationError("Team does not exist") team.name = self.get_argument("name", team.name) team.motto = self.get_argument("motto", team.motto) - team.money = self.get_argument("money", team.money) + team.set_score("admin", self.get_argument("money", team.money)) team.notes = self.get_argument("notes", "") if hasattr(self.request, "files") and "avatarfile" in self.request.files: team.avatar = self.request.files["avatarfile"][0]["body"] @@ -217,9 +217,9 @@ def create_team(self, user): team.motto = "" team._avatar = identicon(team.name, 6) if self.config.banking: - team.money = self.config.starting_team_money + team.set_score("start", self.config.starting_team_money) else: - team.money = 0 + team.set_score("start", 0) level_0 = GameLevel.by_number(0) if not level_0: level_0 = GameLevel.all()[0] diff --git a/handlers/BaseHandlers.py b/handlers/BaseHandlers.py index 137ae91c..b06a196c 100644 --- a/handlers/BaseHandlers.py +++ b/handlers/BaseHandlers.py @@ -255,7 +255,6 @@ def start_game(self): if not self.application.settings["game_started"]: logging.info("The game is about to begin, good hunting!") self.application.settings["game_started"] = True - self.application.settings["history_callback"].start() if self.config.use_bots: self.application.settings["score_bots_callback"].start() # Fire game start webhook @@ -266,8 +265,6 @@ def stop_game(self): if self.application.settings["game_started"]: logging.info("The game is stopping ...") self.application.settings["game_started"] = False - if self.application.settings["history_callback"]._running: - self.application.settings["history_callback"].stop() if self.application.settings["score_bots_callback"]._running: self.application.settings["score_bots_callback"].stop() # Fire game stop webhook diff --git a/handlers/MarketHandlers.py b/handlers/MarketHandlers.py index 46d5b361..83f1055c 100644 --- a/handlers/MarketHandlers.py +++ b/handlers/MarketHandlers.py @@ -91,7 +91,7 @@ def post(self, *args, **kwargs): def purchase_item(self, team, item): """Conducts the actual purchase of an item""" - team.money -= abs(item.price) + team.set_score("purchase_market", team.money - abs(item.price)) team.items.append(item) self.dbsession.add(team) self.dbsession.commit() diff --git a/handlers/MissionsHandler.py b/handlers/MissionsHandler.py index f65a1519..14565617 100644 --- a/handlers/MissionsHandler.py +++ b/handlers/MissionsHandler.py @@ -294,7 +294,7 @@ def success_capture(self, user, flag, old_reward=None): box = flag.box if box.is_complete(user): if box.value > 0: - user.team.money += box.value + user.team.set_score("box", box.value + user.team.money) self.dbsession.add(user.team) self.dbsession.flush() self.dbsession.commit() @@ -317,7 +317,7 @@ def success_capture(self, user, flag, old_reward=None): if level_progress == 1.0 and level not in user.team.game_levels: reward_dialog = "" if level._reward > 0: - user.team.money += level._reward + user.team.set_score("level", level._reward + user.team.money) self.dbsession.add(user.team) self.dbsession.flush() self.dbsession.commit() @@ -390,7 +390,7 @@ def failed_capture(self, flag, submission): "%s (%s) capture failed '%s' - lost %s" % (user.handle, user.team.name, flag.name, penalty) ) - user.team.money -= penalty + user.team.set_score("penalty", user.team.money - penalty) user.money -= penalty self.dbsession.add(user.team) self.dbsession.flush() @@ -417,12 +417,12 @@ def attempt_capture(self, flag, submission): for item in Flag.team_captures(flag.id): tm = Team.by_id(item[0]) deduction = flag.dynamic_value(tm) - flag_value - tm.money = int(tm.money - deduction) + tm.set_score("decay", int(tm.money - deduction)) self.dbsession.add(tm) self.event_manager.flag_decayed(tm, flag) - team.money += flag_value + team.set_score("flag", flag_value + team.money) user.money += flag_value - team.flags.append(flag) + team.add_flag(flag) user.flags.append(flag) self.dbsession.add(user) self.dbsession.add(team) @@ -517,7 +517,7 @@ def _purchase_hint(self, hint, team): """Add hint to team object""" if hint not in team.hints: user = self.get_current_user() - team.money -= abs(hint.price) + team.set_score("purchase_hint", team.money - abs(hint.price)) team.hints.append(hint) self.dbsession.add(team) self.dbsession.commit() @@ -571,7 +571,7 @@ def buyout(self): % (user.handle, user.team.name, level.name) ) user.team.game_levels.append(level) - user.team.money -= level.buyout + user.team.set_score("level_buyout", user.team.money - level.buyout) self.dbsession.add(user.team) self.dbsession.commit() self.event_manager.level_unlocked(user, level) diff --git a/handlers/PublicHandlers.py b/handlers/PublicHandlers.py index adb55147..76516af7 100644 --- a/handlers/PublicHandlers.py +++ b/handlers/PublicHandlers.py @@ -550,9 +550,9 @@ def create_team(self): team.motto = self.get_argument("motto", "") team._avatar = identicon(team.name, 6) if self.config.banking: - team.money = self.config.starting_team_money + team.set_score("start", self.config.starting_team_money) else: - team.money = 0 + team.set_score("start", 0) levels = GameLevel.all() for level in levels: if level.type == "none": @@ -571,7 +571,9 @@ def create_team(self): if len(filter_avatars("team")) == 0: team._avatar = identicon(team.name, 6) if not self.config.banking: - team.money = 0 + team.set_score("start", 0) + else: + team.set_score("start", self.config.starting_team_money) level_0 = GameLevel.by_number(0) if not level_0: level_0 = GameLevel.all()[0] diff --git a/handlers/ScoreboardHandlers.py b/handlers/ScoreboardHandlers.py index 479617f7..1bd5b367 100644 --- a/handlers/ScoreboardHandlers.py +++ b/handlers/ScoreboardHandlers.py @@ -31,10 +31,10 @@ from tornado.websocket import WebSocketHandler from handlers.BaseHandlers import BaseHandler from libs.SecurityDecorators import use_black_market, item_allowed -from libs.GameHistory import GameHistory from libs.Scoreboard import Scoreboard from builtins import str from math import ceil +from models import dbsession from models.Team import Team from models.User import User from models.Box import Box @@ -43,6 +43,7 @@ from datetime import datetime, timedelta from tornado.options import options from collections import OrderedDict +from itertools import islice class ScoreboardDataSocketHandler(WebSocketHandler): @@ -50,9 +51,6 @@ class ScoreboardDataSocketHandler(WebSocketHandler): connections = set() - def initialize(self): - self.last_message = datetime.now() - def open(self): """When we receive a new websocket connect""" self.connections.add(self) @@ -66,8 +64,7 @@ def on_message(self, message): Scoreboard.update_gamestate(self) if self.application.settings["hide_scoreboard"]: self.write_message("pause") - elif datetime.now() - self.last_message > timedelta(seconds=3): - self.last_message = datetime.now() + else: self.write_message(Scoreboard.now(self)) def on_close(self): @@ -129,6 +126,7 @@ def get(self, *args, **kargs): "mvp": self.mvp_table, "timer": self.timediff, "feed": self.json_feed, + "history": self.history_data, } if len(args) and args[0] in uri: uri[args[0]]() @@ -242,6 +240,50 @@ def team_skills(self): self.write({"error": "Team does not exist"}) self.finish() + def history_data(self): + """Send history in JSON""" + top = int(self.get_argument("top", 10)) + teams = self.application.settings["scoreboard_state"]["teams"] + score_teams = [ + team["uuid"] + for team in sorted(teams.values(), key=lambda d: d["money"], reverse=True)[ + 0:top + ] + ] + flag_teams = [ + team["uuid"] + for team in sorted( + teams.values(), key=lambda d: len(d["flags"]), reverse=True + )[0:top] + ] + bot_teams = [ + team["uuid"] + for team in sorted( + teams.values(), key=lambda d: d["bot_count"], reverse=True + )[0:top] + ] + history = { + "history": { + "bot_count": {}, + "flag_count": {}, + "score_count": {}, + } + } + for uuid in flag_teams: + team = Team.by_uuid(uuid) + if team: + history["history"]["flag_count"][team.name] = team.get_history("flags") + for uuid in score_teams: + team = Team.by_uuid(uuid) + if team: + history["history"]["score_count"][team.name] = team.get_history("score") + for uuid in bot_teams: + team = Team.by_uuid(uuid) + if team: + history["history"]["bot_count"][team.name] = team.get_history("bots") + self.write(json.dumps(history)) + self.finish() + class ScoreboardHistoryHandler(BaseHandler): def get(self, *args, **kwargs): @@ -268,35 +310,22 @@ def get(self, *args, **kwargs): class ScoreboardHistorySocketHandler(WebSocketHandler): connections = set() - game_history = GameHistory.instance() - - def initialize(self): - self.game_history._load() - self.last_message = datetime.now() def open(self): """When we receive a new websocket connect""" self.connections.add(self) - history_length = int(self.get_argument("length", 29)) - self.write_message(self.get_history(history_length)) def on_message(self, message): """We ignore messages if there are more than 1 every 3 seconds""" if self.application.settings["hide_scoreboard"]: self.write_message("pause") - elif datetime.now() - self.last_message > timedelta(seconds=3): - self.last_message = datetime.now() - self.write_message(self.get_history(1)) + else: + self.write_message("update") def on_close(self): """Lost connection to client""" self.connections.remove(self) - def get_history(self, length=29): - """Send history in JSON""" - length = abs(length) + 1 - return json.dumps({"history": self.game_history[(-1 * length) :]}) - class ScoreboardWallOfSheepHandler(BaseHandler): @use_black_market @@ -365,9 +394,11 @@ def get(self, *args, **kwargs): end_count = display * page start_count = end_count - display teams = [] - for i, team in enumerate(ranks): + for i, team_name in enumerate(ranks): if i >= start_count and i < end_count: - teams.append(Team.by_uuid(ranks[team].get("uuid"))) + team = Team.by_uuid(ranks[team_name].get("uuid")) + if team: + teams.append(team) elif i >= end_count: break diff --git a/handlers/UpgradeHandlers.py b/handlers/UpgradeHandlers.py index 39599557..97f7b35d 100644 --- a/handlers/UpgradeHandlers.py +++ b/handlers/UpgradeHandlers.py @@ -76,7 +76,9 @@ def post(self, *args, **kwargs): elif user.team.money < self.config.password_upgrade_cost: self.render_page(["You cannot afford to upgrade your hash"]) elif len(passwd) <= self.config.max_password_length: - user.team.money -= self.config.password_upgrade_cost + user.team.set_score( + "upgrade", user.team.money - self.config.password_upgrade_cost + ) self.dbsession.add(user.team) self.dbsession.commit() self.event_manager.push_score_update() @@ -239,9 +241,9 @@ def transfer(self): def theft(self, victim, destination, amount, preimage): """Successfully cracked a password""" - victim.team.money -= abs(amount) + victim.team.set_score("theft", victim.team.money - abs(amount)) value = int(abs(amount) * 0.85) - destination.money += value + destination.set_score("theft", destination.money + value) self.dbsession.add(destination) self.dbsession.add(victim.team) user = self.get_current_user() @@ -284,7 +286,7 @@ def purchase_code(self, box): """Modify the database to reflect purchase""" team = self.get_current_user().team source_code = SourceCode.by_box_id(box.id) - team.money -= abs(source_code.price) + team.set_score("purchase_code", team.money - abs(source_code.price)) team.purchased_source_code.append(source_code) logging.info( "%s purchased '%s' from the source code market." @@ -385,7 +387,7 @@ def create_swat(self, user, target): """Create Swat request object in database""" price = Swat.get_price(target) assert 0 < price - user.team.money -= price + user.team.set_score("purchase_swat", user.team.money - price) swat = Swat(user_id=user.id, target_id=target.id, paid=price) self.dbsession.add(swat) self.dbsession.add(user.team) diff --git a/handlers/__init__.py b/handlers/__init__.py index 3cbb35e2..2b7c8513 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -44,7 +44,6 @@ from modules.AppTheme import AppTheme from libs.ConsoleColors import * from libs.Scoreboard import Scoreboard, score_bots -from libs.GameHistory import GameHistory from libs.DatabaseConnection import DatabaseConnection from libs.StringCoding import encode from handlers.BotnetHandlers import * @@ -71,7 +70,6 @@ # Singletons io_loop = IOLoop.instance() -game_history = GameHistory.instance() def get_cookie_secret(): @@ -251,9 +249,6 @@ def get_cookie_secret(): temp_global_notifications=None, # Callback functions score_bots_callback=PeriodicCallback(score_bots, options.bot_reward_interval), - history_callback=PeriodicCallback( - game_history.take_snapshot, options.history_snapshot_interval - ), # Scoreboard Highlights scoreboard_history={}, scoreboard_state={}, @@ -284,10 +279,6 @@ def update_db(update=True): command.stamp(alembic_cfg, "head") -def load_history(): - game_history._load() - - # Main entry point def start_server(): """Main entry point for the application""" @@ -295,7 +286,6 @@ def start_server(): locale.load_translations("locale") if options.autostart_game: app.settings["game_started"] = True - app.settings["history_callback"].start() if options.use_bots: app.settings["score_bots_callback"].start() # Setup server object diff --git a/libs/BotManager.py b/libs/BotManager.py index fedd0895..4617873b 100644 --- a/libs/BotManager.py +++ b/libs/BotManager.py @@ -153,6 +153,8 @@ def count_by_team_uuid(self, tuuid): def add_bot(self, bot_wsocket): if not self.is_duplicate(bot_wsocket): + from models.Team import Team + bot = Bot( wsock_uuid=str(bot_wsocket.uuid), team_name=str(bot_wsocket.team_name), @@ -161,11 +163,14 @@ def add_bot(self, bot_wsocket): box_uuid=str(bot_wsocket.box_uuid), remote_ip=str(bot_wsocket.remote_ip), ) + team = Team.by_uuid(bot.team_uuid) bot.dbsession = self.dbsession self.botdb.add(bot) self.botdb.flush() self.botnet[bot_wsocket.uuid] = bot_wsocket - self.notify_monitors(bot.team_name) + + team.set_bot(self.count_by_team_uuid(team.uuid)) + self.notify_monitors(team.name) return True else: return False @@ -178,12 +183,15 @@ def save_bot(self, bot): def remove_bot(self, bot_wsocket): bot = self.botdb.query(Bot).filter_by(wsock_uuid=str(bot_wsocket.uuid)).first() if bot is not None: + from models.Team import Team + + team = Team.by_uuid(bot.team_uuid) logging.debug("Removing bot '%s' at %s" % (bot.team_uuid, bot.remote_ip)) - team = bot.team_name self.botnet.pop(bot_wsocket.uuid, None) self.botdb.delete(bot) self.botdb.flush() - self.notify_monitors(team) + team.set_bot(self.count_by_team_uuid(team.uuid)) + self.notify_monitors(team.name) else: logging.warning( "Failed to remove bot '%s' does not exist in manager" % bot_wsocket.uuid diff --git a/libs/ConfigHelpers.py b/libs/ConfigHelpers.py index a210e505..e16f8a2b 100644 --- a/libs/ConfigHelpers.py +++ b/libs/ConfigHelpers.py @@ -75,7 +75,7 @@ def create_demo_user(): team = Team() team.name = "player" team.motto = "Don't hate the player" - team.money = 0 + team.set_score("start", 0) team.game_levels.append(GameLevel.all()[0]) team.members.append(user) dbsession.add(user) diff --git a/libs/GameHistory.py b/libs/GameHistory.py deleted file mode 100644 index 43c26bb3..00000000 --- a/libs/GameHistory.py +++ /dev/null @@ -1,170 +0,0 @@ -""" -Created on Nov 11, 2012 - -@author: moloch - - Copyright 2012 Root the Box - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" -# pylint: disable=no-member - - -import logging - -from models import dbsession -from models.Team import Team -from models.Snapshot import Snapshot -from models.SnapshotTeam import SnapshotTeam -from sqlalchemy import desc -from libs.BotManager import BotManager -from libs.EventManager import EventManager -from libs.Sessions import MemcachedConnect -from libs.Singleton import Singleton -from tornado.options import options -from builtins import object, range - - -@Singleton -class GameHistory(object): - """ - List-like object to store game history, with cache to avoid - multiple large database reads. - """ - - def __init__(self): - self.config = options - self.dbsession = dbsession - self.cache = MemcachedConnect() - self.epoch = None # Date/time of first snapshot - self.event_manager = EventManager.instance() - - def _load(self): - """Moves snapshots from db into the cache""" - logging.info("Loading game history from database ...") - snaps = Snapshot.all() - if len(snaps) > 0: - snap = snaps[0] - else: - snap = self.__now__() # Take starting snapshot - self.epoch = snap.created - try: - max_index = len(self) - start_index = snap.id if len(self) <= (snap.id + 9) else max_index - 9 - for index in range(start_index, max_index + 1): - snapshot = Snapshot.by_id(index) - if snapshot and self.cache.get(snapshot.key) is None: - logging.info( - "Cached snapshot (%d of %d)" % (snapshot.id, max_index) - ) - self.cache.set(snapshot.key, snapshot.to_dict()) - logging.info("History load complete.") - except TypeError: - logging.error("Error Loading Cache (try to restart memcached)") - except KeyboardInterrupt: - logging.info("History load stopped by user.") - - def take_snapshot(self, *args): - """Take a snapshot of the current game data""" - snapshot = self.__now__() - if snapshot is not None: - self.cache.set(snapshot.key, snapshot.to_dict()) - self.event_manager.push_history(snapshot.to_dict()) - - def get_flag_history_by_name(self, name, start, stop=None): - """Retrieves flag capture history for a team""" - snapshots = self[start:] if stop is None else self[start:stop] - series = [] - for snapshot in snapshots: - if name in snapshot["scoreboard"]: - flags = snapshot["scoreboard"][name]["flags"] - series.append((snapshot["timestamp"], len(flags))) - return series - - def get_money_history_by_name(self, name, start, stop=None): - """Retrieves money history for a team""" - snapshots = self[start:] if stop is None else self[start:stop] - series = [] - for snapshot in snapshots: - if name in snapshot["scoreboard"]: - money = snapshot["scoreboard"][name]["money"] - series.append((snapshot["timestamp"], money)) - return series - - def get_bot_history_by_name(self, name, start, stop=None): - """Retrieves money history for a team""" - snapshots = self[start:] if stop is None else self[start:stop] - series = [] - for snapshot in snapshots: - if name in snapshot["scoreboard"]: - bots = snapshot["scoreboard"][name]["bots"] - series.append((snapshot["timestamp"], bots)) - return series - - def __now__(self): - """Returns snapshot object it as a dict""" - snapshot = Snapshot() - bot_manager = BotManager.instance() - # self.dbsession = DBSession() - for team in Team.all(): - if not team.locked: - snapshot_team = SnapshotTeam( - team_id=team.id, - money=team.money, - bots=bot_manager.count_by_team(team.name), - ) - snapshot_team.game_levels = team.game_levels - snapshot_team.flags = team.flags - self.dbsession.add(snapshot_team) - self.dbsession.flush() - snapshot.teams.append(snapshot_team) - self.dbsession.add(snapshot) - self.dbsession.commit() - return snapshot - - def __iter__(self): - for snapshot in self: - yield snapshot.to_dict() - - def __contains__(self, index): - return True if Snapshot.by_id(index) is not None else False - - def __len__(self): - """Return length of the game history""" - return self.dbsession.query(Snapshot).order_by(desc(Snapshot.id)).first().id - - def __getitem__(self, key): - """Implements slices and indices""" - if isinstance(key, slice): - ls = [self[index] for index in range(*key.indices(len(self)))] - return [item for item in ls if item is not None] - elif isinstance(key, int): - if key < 0: # Handle negative indices - key += len(self) - if key >= len(self): - raise IndexError("The index (%d) is out of range." % key) - return self.__at__(key) - else: - raise TypeError("Invalid index argument to GameHistory") - - def __at__(self, index): - """Get snapshot at specific index""" - key = Snapshot.to_key(index + 1) - if self.cache.get(key) is not None: - return self.cache.get(key) - else: - snapshot = Snapshot.by_id(index + 1) - if snapshot is not None: - self.cache.set(snapshot.key, snapshot.to_dict()) - return snapshot.to_dict() - return None diff --git a/libs/Scoreboard.py b/libs/Scoreboard.py index 24ad0a45..0e03bb0a 100644 --- a/libs/Scoreboard.py +++ b/libs/Scoreboard.py @@ -83,13 +83,15 @@ def update_gamestate(cls, app): for item in highlights: value = team.get_score(item) game_state["teams"][team.name][item] = value - game_history = app.settings["scoreboard_history"] - if team.name in game_history: - prev = game_history[team.name][item] + scoreboard_history = app.settings["scoreboard_history"] + if team.name in scoreboard_history: + prev = scoreboard_history[team.name][item] if prev < value: highlights[item] = millis else: - highlights[item] = game_history[team.name]["highlights"][item] + highlights[item] = scoreboard_history[team.name]["highlights"][ + item + ] highlights["now"] = millis game_state["teams"][team.name]["highlights"] = highlights app.settings["scoreboard_history"][team.name] = game_state["teams"].get( @@ -164,7 +166,7 @@ def score_bots(): ) bot_manager.add_rewards(team.name, options.bot_reward) bot_manager.notify_monitors(team.name) - team.money += reward + team.set_score("bot", reward + team.money) dbsession.add(team) dbsession.flush() event_manager.bot_scored(team, message) diff --git a/models/GameHistory.py b/models/GameHistory.py new file mode 100644 index 00000000..47a89030 --- /dev/null +++ b/models/GameHistory.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +""" +Created on Mar 3, 2023 + +@author: ElJefe + + Copyright 2023 Root the Box + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +# pylint: disable=no-member + +import json +from models import dbsession +from sqlalchemy import Column, ForeignKey +from sqlalchemy.types import Unicode, String, Integer +from models.BaseModels import DatabaseObject +from builtins import str +from tornado.options import options + + +class GameHistory(DatabaseObject): + + """Game History definition""" + + team_id = Column(Integer, ForeignKey("team.id"), nullable=False) + _type = Column(Unicode(20), nullable=False, default=str("none")) + _value = Column(Integer, default=0, nullable=False) + + @classmethod + def all(cls): + """Returns a list of all objects in the database""" + return dbsession.query(cls).all() + + @classmethod + def by_type(cls, _type): + """Return the game history object based on the _type""" + return dbsession.query(cls).filter_by(_type=_type).all() + + @property + def type(self): + return self._type + + @type.setter + def type(self, _type): + self._type = str(_type) + + @property + def value(self): + return self._value + + @value.setter + def value(self, _value): + self._value = int(_value) + + def to_dict(self): + from models.Team import Team + + team = Team.by_id(self.team_id) + """Returns public data as a dict""" + return { + "timestamp": self.created.strftime("%s"), + "team_name": team.name, + "value": self._value, + } + + def __repr__(self): + return "" % ( + self.team_id, + self._type, + self._value, + ) + + def __str__(self): + return json.dumps( + { + "created": self.created.strftime("%s"), + "team_uuid": team.uuid, + "type": self._type, + "value": self._value, + } + ) diff --git a/models/Relationships.py b/models/Relationships.py index d7417808..fabf116f 100644 --- a/models/Relationships.py +++ b/models/Relationships.py @@ -108,51 +108,3 @@ nullable=False, ), ) - -snapshot_to_snapshot_team = Table( - "snapshot_to_snapshot_team", - DatabaseObject.metadata, - Column( - "snapshot_id", - Integer, - ForeignKey("snapshot.id", ondelete="CASCADE"), - nullable=False, - ), - Column( - "snapshot_team_id", - Integer, - ForeignKey("snapshot_team.id", ondelete="CASCADE"), - nullable=False, - ), -) - -snapshot_team_to_flag = Table( - "snapshot_team_to_flag", - DatabaseObject.metadata, - Column( - "snapshot_team_id", - Integer, - ForeignKey("snapshot_team.id", ondelete="CASCADE"), - nullable=False, - ), - Column( - "flag_id", Integer, ForeignKey("flag.id", ondelete="CASCADE"), nullable=False - ), -) - -snapshot_team_to_game_level = Table( - "snapshot_team_to_game_level", - DatabaseObject.metadata, - Column( - "snapshot_team_id", - Integer, - ForeignKey("snapshot_team.id", ondelete="CASCADE"), - nullable=False, - ), - Column( - "gam_level_id", - Integer, - ForeignKey("game_level.id", ondelete="CASCADE"), - nullable=False, - ), -) diff --git a/models/Snapshot.py b/models/Snapshot.py deleted file mode 100644 index 73844856..00000000 --- a/models/Snapshot.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mar 11, 2012 - -@author: moloch - - Copyright 2012 Root the Box - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" - -import datetime - -from sqlalchemy.orm import relationship, backref -from models import dbsession -from models.Relationships import snapshot_to_snapshot_team -from models.BaseModels import DatabaseObject -from builtins import str - -### Constants ### -# Ignore time zone for now -UNIX_EPOCH = datetime.datetime(year=1970, month=1, day=1) - - -class Snapshot(DatabaseObject): - """Snapshot of game data""" - - # Has many 'SnapshotTeam' objects - teams = relationship( - "SnapshotTeam", - secondary=snapshot_to_snapshot_team, - backref=backref("snapshot", lazy="select"), - ) - - @classmethod - def all(cls): - """Returns a list of all objects in the database""" - return dbsession.query(cls).all() - - @classmethod - def by_id(cls, identifier): - """Returns a the object with id of identifier""" - return dbsession.query(cls).filter_by(id=identifier).first() - - @classmethod - def to_key(cls, val): - return "snapshot.%d" % val - - @property - def key(self): - return self.to_key(self.id) - - def to_dict(self): - data = {} - for team in self.teams: - data[str(team.name)] = { - "bots": team.bots, - "money": team.money, - "game_levels": [str(level) for level in team.game_levels], - "flags": [flag.name for flag in team.flags], - } - unix_time = self.created - UNIX_EPOCH - return {"timestamp": unix_time.total_seconds(), "scoreboard": data} diff --git a/models/SnapshotTeam.py b/models/SnapshotTeam.py deleted file mode 100644 index c18f5cad..00000000 --- a/models/SnapshotTeam.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mar 11, 2012 - -@author: moloch - - Copyright 2012 Root the Box - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" - - -from sqlalchemy import Column, ForeignKey -from sqlalchemy.orm import relationship, backref -from sqlalchemy.types import Integer -from models import dbsession -from models.Team import Team -from models.Relationships import snapshot_team_to_flag, snapshot_team_to_game_level -from models.BaseModels import DatabaseObject - - -class SnapshotTeam(DatabaseObject): - - """ - Used by game history; snapshot of a single team in history - """ - - team_id = Column(Integer, ForeignKey("team.id", ondelete="CASCADE"), nullable=False) - money = Column(Integer, nullable=False) - bots = Column(Integer, nullable=False) - - game_levels = relationship( - "GameLevel", - secondary=snapshot_team_to_game_level, - backref=backref("snapshot_team", lazy="select"), - ) - - flags = relationship( - "Flag", - secondary=snapshot_team_to_flag, - backref=backref("snapshot_team", lazy="select"), - ) - - @property - def name(self): - return dbsession.query(Team._name).filter_by(id=self.team_id).first()[0] - - @classmethod - def all(cls): - """Returns a list of all objects in the database""" - return dbsession.query(cls).all() diff --git a/models/Team.py b/models/Team.py index 6f8eb2e2..7b90adc6 100644 --- a/models/Team.py +++ b/models/Team.py @@ -34,6 +34,7 @@ from models import dbsession from models.BaseModels import DatabaseObject from models.User import User +from models.GameHistory import GameHistory from models.Relationships import ( team_to_box, team_to_item, @@ -110,6 +111,12 @@ class Team(DatabaseObject): "GameLevel", secondary=team_to_game_level, back_populates="teams", lazy="select" ) + game_history = relationship( + "GameHistory", + backref=backref("team", lazy="select"), + cascade="all,delete,delete-orphan", + ) + @classmethod def all(cls): """Returns a list of all objects in the database""" @@ -152,17 +159,44 @@ def count(cls): def name(self): return self._name - def get_score(self, item): - if item == "money": + def get_score(self, _type): + if _type == "money": return self.money - elif item == "flag": + elif _type == "flag": return len(self.flags) - elif item == "hint": + elif _type == "hint": return len(self.hints) - elif item == "bot": + elif _type == "bot": return self.bot_count return 0 + def set_score(self, _type, _money): + score_update = GameHistory(type=_type, value=_money) + self.game_history.append(score_update) + self.money = _money + + def set_bot(self, botcount): + bot_update = GameHistory(type="bot_count", value=botcount) + self.game_history.append(bot_update) + + def add_flag(self, flag): + self.flags.append(flag) + add_flag = GameHistory(type="flag_count", value=len(self.flags)) + self.game_history.append(add_flag) + + def get_history(self, _type=None): + history = [] + for item in self.game_history: + if _type == "bots": + if item.type == "bot_count": + history.append(item.to_dict()) + elif _type == "flags": + if item.type == "flag_count": + history.append(item.to_dict()) + elif item.type != "flag_count" and item.type != "bot_count": + history.append(item.to_dict()) + return history + @name.setter def name(self, value): if not 3 <= len(value) <= 24: diff --git a/models/__init__.py b/models/__init__.py index fa56f694..9bea75b5 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -129,8 +129,6 @@ def cxt_dbsession(): from models.RegistrationToken import RegistrationToken from models.MarketItem import MarketItem from models.IpAddress import IpAddress -from models.Snapshot import Snapshot -from models.SnapshotTeam import SnapshotTeam from models.SourceCode import SourceCode from models.Swat import Swat from models.Hint import Hint diff --git a/rootthebox.py b/rootthebox.py index c62d8dae..d903bdff 100644 --- a/rootthebox.py +++ b/rootthebox.py @@ -57,9 +57,7 @@ def start(): os._exit(1) """ Starts the application """ - from handlers import start_server, load_history - - load_history() + from handlers import start_server prefix = "https://" if options.ssl else "http://" # TODO For docker, it would be nice to grab the mapped docker port @@ -1006,14 +1004,6 @@ def help(): ) # I/O Loop Settings -define( - "history_snapshot_interval", - default=int(60000 * 5), - group="game", - help="interval to create history snapshots (milliseconds)", - type=int, -) - define( "bot_reward_interval", default=int(60000 * 15), diff --git a/setup/__init__.py b/setup/__init__.py index aa8a0b38..09547cdc 100644 --- a/setup/__init__.py +++ b/setup/__init__.py @@ -1,2 +1,2 @@ # -*- coding: utf-8 -*- -__version__ = "3.12.5" +__version__ = "3.13.0" diff --git a/setup/create_database.py b/setup/create_database.py index 7f360ee6..2c2ebec6 100644 --- a/setup/create_database.py +++ b/setup/create_database.py @@ -56,8 +56,6 @@ def create_tables(sqla_engine, sqla_metadata, echo=False): from models.RegistrationToken import RegistrationToken from models.MarketItem import MarketItem from models.IpAddress import IpAddress -from models.Snapshot import Snapshot -from models.SnapshotTeam import SnapshotTeam from models.SourceCode import SourceCode from models.Swat import Swat from models.Hint import Hint diff --git a/static/css/pages/public/summary.css b/static/css/pages/public/summary.css index 7e3574a3..9e88f290 100644 --- a/static/css/pages/public/summary.css +++ b/static/css/pages/public/summary.css @@ -1,15 +1,6 @@ .summarycolumn { padding-bottom: 0px !important; } -.graphhr { - text-align:right !important; - margin: -10px 0 2px !important; - padding-right: 4px !important; -} -.graphtoggle { - font-size: large; - vertical-align: middle; -} .glow { color: #fff; font-weight: bold; diff --git a/static/js/libs/markdown-toolbar.js b/static/js/libs/markdown-toolbar.js index 7c5f2740..5ef40da1 100644 --- a/static/js/libs/markdown-toolbar.js +++ b/static/js/libs/markdown-toolbar.js @@ -52,6 +52,7 @@ ) { rendered = emoji.replace_colons(rendered); } + rendered = rendered.replaceAll('' + htmlEncode(this.series.name) + '
' + htmlEncode(this.y) + ' flag(s)'; } }, + colors: ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE', '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92'], plotOptions: { line: { dataLabels: { @@ -285,6 +306,9 @@ function drawFlagGraph(state) { }, series: state, }); + for (item in chart.series) { + extendData(chart.series[item], chart.xAxis[0]); + } return chart; } @@ -297,155 +321,74 @@ function getSeriesIndexByName(state, teamName) { return undefined; } -/* Updates the local graph state */ -function updateFlagState(flagState, update) { - timestamp = update['timestamp'] * 1000; - for (var teamName in update['scoreboard']) { - flagCount = update['scoreboard'][teamName]['flags'].length; - seriesIndex = getSeriesIndexByName(flagState, teamName); - if (seriesIndex !== undefined) { - /* Add to existing series' data array */ - flagState[seriesIndex].data.push([timestamp, flagCount]); - } else { - //console.log("Create flag series: " + teamName); - newSeries = { - name: teamName, - data: [ - [timestamp, flagCount], - ] - } - flagState.push(newSeries); - } - } -} - -/* Called if the Flag graph is currently displayed */ -function liveFlagUpdate(chart, update) { - timestamp = update['timestamp'] * 1000; - for (var teamName in update['scoreboard']) { - //console.log("Updating: " + teamName); - flagCount = update['scoreboard'][teamName]['flags'].length; - index = getSeriesIndexByName(chart.series, teamName); - if (index !== undefined) { - var shift = (30 <= chart.series[index].data.length); - var scores = [timestamp, flagCount]; - chart.series[index].addPoint(scores, true, shift); - } else { - create_series = { - name: teamName, - data: [ - [timestamp, flagCount], - ] - } - chart.addSeries(create_series); - } - } -} - -function updateMoneyState(moneyState, update) { - timestamp = update['timestamp'] * 1000; - for (var teamName in update['scoreboard']) { - money = update['scoreboard'][teamName]['money']; - seriesIndex = getSeriesIndexByName(moneyState, teamName); - if (seriesIndex !== undefined) { - /* Add to existing series' data array */ - moneyState[seriesIndex].data.push([timestamp, money]); - } else { - //console.log("Create money series: " + teamName); - newSeries = { - name: teamName, - data: [ - [timestamp, money], - ] +function updateState(state, updates) { + for (var teamName in updates) { + teamdata = updates[teamName] + for(index = 0; index < teamdata.length; ++index) { + update = teamdata[index] + timestamp = update['timestamp'] * 1000; + teamname = update['team_name']; + value = update['value']; + seriesIndex = getSeriesIndexByName(state, teamname); + if (seriesIndex !== undefined) { + // Add to existing series' data array + state[seriesIndex].data.push([timestamp, value]); + } else { + //console.log("Create flag series: " + teamname); + newSeries = { + name: teamname, + data: [ + [timestamp, value], + ] + } + state.push(newSeries); } - moneyState.push(newSeries); } } } -function liveMoneyUpdate(chart, update) { - timestamp = update['timestamp'] * 1000; - for (var teamName in update['scoreboard']) { - //console.log("Updating: " + teamName); - money = update['scoreboard'][teamName]['money']; - index = getSeriesIndexByName(chart.series, teamName); - if (index !== undefined) { - var shift = (30 <= chart.series[index].data.length); - var scores = [timestamp, money]; - chart.series[index].addPoint(scores, true, shift); - } else { - create_series = { - name: teamName, - data: [ - [timestamp, money], - ] - } - chart.addSeries(create_series); - } - } +function update_graph_state(msg) { + /* Default graph is flags, init that first */ + flagState = []; + moneyState = []; + botState = []; + updateState(flagState, msg['history']['flag_count']); + updateState(moneyState, msg['history']['score_count']); + updateState(botState, msg['history']['bot_count']); + set_graph_chart(); } -function updateBotState(botState, update) { - timestamp = update['timestamp'] * 1000; - for (var teamName in update['scoreboard']) { - bots = update['scoreboard'][teamName]['bots']; - seriesIndex = getSeriesIndexByName(botState, teamName); - if (seriesIndex !== undefined) { - /* Add to existing series' data array */ - botState[seriesIndex].data.push([timestamp, bots]); - } else { - //console.log("Create bot series: " + teamName); - newSeries = { - name: teamName, - data: [ - [timestamp, bots], - ] - } - botState.push(newSeries); - } +function set_graph_chart() { + /* get the correct state */ + if (history_option == "flag") { + drawFlagGraph(flagState); + } else if (history_option == "bot") { + drawFlagGraph(botState); + } else { + drawFlagGraph(moneyState); } } -function liveBotUpdate(chart, update) { - timestamp = update['timestamp'] * 1000; - for (var teamName in update['scoreboard']) { - //console.log("Updating: " + teamName); - bots = update['scoreboard'][teamName]['bots']; - index = getSeriesIndexByName(chart.series, teamName); - if (index !== undefined) { - var shift = (30 <= chart.series[index].data.length); - var scores = [timestamp, bots]; - chart.series[index].addPoint(scores, true, shift); - } else { - create_series = { - name: teamName, - data: [ - [timestamp, bots], - ] - } - chart.addSeries(create_series); +function trigger_update() { + let top = $("#datapoints").find(":selected").val(); + $.get("/scoreboard/ajax/history?top=" + top, function(data) { + msg = jQuery.parseJSON(data); + if ('error' in msg) { + console.log("ERROR: " + msg.toString()); + } else if ('history' in msg) { + update_graph_state(msg) } - } -} - -function initializeState(updater, state, updates) { - for(index = 0; index < updates.length; ++index) { - updater(state, updates[index]); - } + $("body").css("cursor", "default"); + }); } -function initializeSocket(length) { +function initializeSocket() { $("body").css("cursor", "progress"); - window.history_ws = new WebSocket(wsUrl() + "/scoreboard/wsocket/game_history?length=" + length); - var chart = undefined; - var flagState = []; // List of Highchart series - var moneyState = []; - var botState = []; - var liveUpdateCallback = undefined; - + window.history_ws = new WebSocket(wsUrl() + "/scoreboard/wsocket/game_history"); history_ws.onopen = function() { $("#activity-monitor").removeClass("fa-eye-slash"); $("#activity-monitor").addClass("fa-refresh fa-spin"); + trigger_update(); } history_ws.onclose = function() { @@ -457,45 +400,47 @@ function initializeSocket(length) { if (evt.data === "pause") { location.reload(); } else { - msg = jQuery.parseJSON(evt.data); - //console.log(msg); - if ('error' in msg) { - console.log("ERROR: " + msg.toString()); - } else if ('history' in msg) { - /* Default graph is flags, init that first */ - initializeState(updateFlagState, flagState, msg['history']); - chart = drawFlagGraph(flagState); - liveUpdateCallback = liveFlagUpdate; - /* Init other states */ - initializeState(updateMoneyState, moneyState, msg['history']); - initializeState(updateBotState, botState, msg['history']); - } else if ('update' in msg) { - /* Update graph states */ - updateFlagState(flagState, msg['update']); - updateMoneyState(moneyState, msg['update']); - updateBotState(botState, msg['update']); - /* Update the live chart */ - liveUpdateCallback(chart, msg['update']); - } - $("body").css("cursor", "default"); + trigger_update(); + } + }; + + window.scoreboard_ws = new WebSocket(wsUrl() + "/scoreboard/wsocket/game_data"); + scoreboard_ws.onmessage = function(event) { + if (event.data === "pause") { + location.reload(); + } else { + game_data = jQuery.parseJSON(event.data); + + /* Update Money */ + let i = 0; + var money_ls = []; + $.each(game_data, function(index, item) { + if (item.money > 0) { + money_ls.push([index.toString(), item.money]); + i = i+1; + if (i > 9) { + return false; + } + } + }); + money_pie_chart.series[0].setData(money_ls, true); + + /* Update Flags */ + i = 0; + var flag_ls = []; + $.each(game_data, function(index, item) { + flag_count = item.flags.length; + if (flag_count > 0) { + flag_ls.push([index.toString(), flag_count]); + i = i+1; + if (i > 9) { + return false; + } + } + }); + flag_pie_chart.series[0].setData(flag_ls, true); } }; - $("#flags-history-button").off(); - $("#flags-history-button").click(function() { - chart = drawFlagGraph(flagState); - liveUpdateCallback = liveFlagUpdate; - }); - $("#money-history-button").off(); - $("#money-history-button").click(function() { - chart = drawMoneyGraph(moneyState); - liveUpdateCallback = liveMoneyUpdate; - }); - $("#bots-history-button").off(); - $("#bots-history-button").click(function() { - chart = drawBotGraph(botState); - liveUpdateCallback = liveBotUpdate; - }); - } $(document).ready(function() { @@ -511,9 +456,9 @@ $(document).ready(function() { } } } else { - initializeSocket(29); + initializeSocket(); $("#datapoints").change(function(){ - initializeSocket(this.value); + trigger_update(); }); if ($("#timercount").length > 0) { $.get("/scoreboard/ajax/timer", function(distance) { @@ -521,47 +466,20 @@ $(document).ready(function() { setTimer(distance, ""); }); } - window.scoreboard_ws = new WebSocket(wsUrl() + "/scoreboard/wsocket/game_data"); - scoreboard_ws.onmessage = function(event) { - if (event.data === "pause") { - location.reload(); - } else { - game_data = jQuery.parseJSON(event.data); - - /* Update Money */ - let i = 0; - var money_ls = []; - $.each(game_data, function(index, item) { - money_ls.push([index.toString(), item.money]); - i = i+1; - if (i > 9) { - return false; - } - }); - money_chart.series[0].setData(money_ls, true); - - /* Update Flags */ - i = 0; - var flag_ls = []; - $.each(game_data, function(index, item) { - flag_ls.push([index.toString(), item.flags.length]); - i = i+1; - if (i > 9) { - return false; - } - }); - flag_chart.series[0].setData(flag_ls, true); - - } - }; - - $("#graphtext").click(function(){ - $("#pie_graphs").toggle(); - if ($("#pie_graphs").is(":visible")) { - $("#graphtext").html('  Charts '); - } else { - $("#graphtext").html('  Charts '); - } + $("#flags-history-button").off(); + $("#flags-history-button").click(function() { + history_option = "flag" + set_graph_chart() + }); + $("#money-history-button").off(); + $("#money-history-button").click(function() { + history_option = "money" + set_graph_chart() + }); + $("#bots-history-button").off(); + $("#bots-history-button").click(function() { + history_option = "bot" + set_graph_chart() }); } }); diff --git a/templates/scoreboard/history.html b/templates/scoreboard/history.html index fa71bcbb..b72dd46d 100644 --- a/templates/scoreboard/history.html +++ b/templates/scoreboard/history.html @@ -24,26 +24,8 @@

{{ _("Charts") }}

-
-   {{ _("Charts") }}  -

-
-
-
- -
-
-
-
- -
-
-
- -
+
+
+
+
+ +
+
+
+
+ +
+
+
+
{% else %}

{% end %}