Skip to content

Commit

Permalink
Merge pull request #46 from agentsofthesystem/develop
Browse files Browse the repository at this point in the history
Support Game Update Feature
  • Loading branch information
jreed1701 committed Feb 22, 2024
2 parents 2e91a28 + 5c59f73 commit 8108bc1
Show file tree
Hide file tree
Showing 27 changed files with 541 additions and 21 deletions.
6 changes: 4 additions & 2 deletions application/alembic/versions/database_v3.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Add updates to the game server table.
Revision ID: database_v1
Revision ID: database_v3
Revises:
Create Date: 2023-10-08 14:10:31.088339
Expand All @@ -26,7 +26,9 @@ def upgrade():
sa.String(length=25),
default=GameStates.NOT_STATE.value,
nullable=False,
)
),
insert_after="",
insert_before="",
)

# ### end Alembic commands ###
Expand Down
77 changes: 77 additions & 0 deletions application/alembic/versions/database_v4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Add actions table for tracking game state
Revision ID: database_v4
Revises:
Create Date: 2023-10-08 14:10:31.088339
"""
from alembic import op
import sqlalchemy as sa

from datetime import datetime

# revision identifiers, used by Alembic.
revision = "database_v4"
down_revision = "database_v3"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"actions",
sa.Column("action_id", sa.Integer(), nullable=False),
sa.Column("game_id", sa.Integer(), nullable=False),
sa.Column("type", sa.String(length=100), nullable=False),
sa.Column("result", sa.String(length=100), nullable=True),
sa.Column("owner", sa.String(length=100), nullable=False, default="NONE"),
sa.Column("timestamp", sa.DateTime(), nullable=False, default=datetime.utcnow),
sa.Column("spare", sa.String(length=100), nullable=True),
sa.PrimaryKeyConstraint("action_id"),
)

with op.batch_alter_table("games", schema=None) as batch_op:
batch_op.create_foreign_key(
op.f("fk_actions_game_game_id"),
"actions",
["game_id"],
["game_id"],
)

batch_op.add_column(
sa.Column(
"game_steam_build_branch",
sa.String(length=256),
default="public",
nullable=False,
),
insert_after="public",
insert_before="public",
)

batch_op.add_column(
sa.Column(
"game_steam_build_id",
sa.Integer(),
default=-1,
nullable=False,
),
insert_after=-1,
insert_before=-1,
)

op.create_index("ix_actions_action_id", "actions", ["action_id"], unique=False)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("actions")

op.drop_constraint("fk_actions_game_game_id", "games", type_="foreignkey")

with op.batch_alter_table("games", schema=None) as batch_op:
batch_op.drop_column("game_steam_build_id")
batch_op.drop_column("game_steam_build_branch")
# ### end Alembic commands ###
61 changes: 58 additions & 3 deletions application/api/controllers/games.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from flask import request
from sqlalchemy import desc

from application.common import toolbox
from application.common import logger, toolbox
from application.extensions import DATABASE
from application.models.actions import Actions
from application.models.games import Games


Expand All @@ -13,9 +16,25 @@ def get_all_games(add_server_status=False):
Games.query, page, per_page, "game.get_all_games"
)

# For a server with 2-5 games, this for loop is not a problem. The author knows that
# a join is more optimal for database items.
game_items = games_dict["items"]

# Mixin other items, like actions
for game in game_items:
actions_qry = Actions.query.filter_by(game_id=game["game_id"]).order_by(
desc(Actions.timestamp)
)

# This wil get the first per_page items (10 by default)
actions_dict = Actions.to_collection_dict(
actions_qry, page, per_page, "game.get_all_games"
)
game["actions"] = actions_dict["items"]

if add_server_status:
game_items = games_dict["items"]
for game in game_items:
# From .py file not db.
game_obj = toolbox._get_supported_game_object(game["game_name"])
game["game_exe"] = game_obj._game_executable
has_pid = True if game["game_pid"] != "null" else False
Expand All @@ -30,10 +49,26 @@ def get_all_games(add_server_status=False):
@staticmethod
def get_game_by_name(game_name):
game_query = Games.query.filter_by(game_name=game_name)
return Games.to_collection_dict(
game_dict = Games.to_collection_dict(
game_query, 1, 1, "game.get_game_by_name", game_name=game_name
)

if len(game_dict["items"]) == 0:
return game_dict

game_item = game_dict["items"][0]
actions_qry = Actions.query.filter_by(game_id=game_item["game_id"]).order_by(
desc(Actions.timestamp)
)

# This wil get the first per_page items (10 by default)
actions_dict = Actions.to_collection_dict(
actions_qry, 1, 10, "game.get_game_by_name", game_name=game_name
)
game_dict["actions"] = actions_dict["items"]

return game_dict


@staticmethod
def get_games_schema():
Expand Down Expand Up @@ -76,3 +111,23 @@ def get_game_server_status(game_name: str) -> str:
)

return response


def update_game(game_id: int, request) -> bool:
payload = request.json

game_qry = Games.query.filter_by(game_id=game_id)
game_obj = game_qry.first()

if game_obj is None:
logger.error(f"Error: Game ID: {game_id} does not exist")
return False

try:
game_qry.update(payload)
DATABASE.session.commit()
except Exception:
logger.critical("Error: update_game - Unable to update database.")
return False

return True
12 changes: 12 additions & 0 deletions application/api/v1/blueprints/architect.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from application.common import logger
from application.common.decorators import authorization_required
from application.managers.steam_manager import SteamUpdateManager
from application.api.controllers import architect as architect_controller
from application.api.controllers import games as games_controller

Expand All @@ -25,13 +26,24 @@ def health_secure():
@architect.route("/agent/info", methods=["GET"])
@authorization_required
def agent_info():
steam_mgr = SteamUpdateManager()
info = {}

platform_dict = architect_controller.get_platform_info()

games = games_controller.get_all_games(add_server_status=True)
games = games["items"]

# TODO - Tech Debt: Update agent info page to get this info over websocket. Works for now
# but does not scale.
for game in games:
update_dict = steam_mgr.is_update_required(
game["game_steam_build_id"],
game["game_steam_build_branch"],
game["game_steam_id"],
)
game["update_required"] = update_dict["is_required"]

info: dict = platform_dict
info.update({"games": games})

Expand Down
39 changes: 39 additions & 0 deletions application/api/v1/blueprints/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from application.common.exceptions import InvalidUsage
from application.common.game_base import BaseGame
from application.extensions import DATABASE
from application.managers.steam_manager import SteamUpdateManager
from application.models.games import Games
from application.models.game_arguments import GameArguments

Expand Down Expand Up @@ -40,6 +41,15 @@ def get_game_schema():
return jsonify(games_controller.get_games_schema())


@game.route("/game/<int:game_id>", methods=["PATCH"])
@authorization_required
def update_game_by_id(game_id: int):
if not games_controller.update_game(game_id, request):
raise InvalidUsage("Error, Unable to update game data", status_code=500)

return "Success", 200


@game.route("/game/status/<string:game_name>", methods=["GET"])
@authorization_required
def get_game_server_status(game_name):
Expand Down Expand Up @@ -125,6 +135,35 @@ def game_uninstall(game_name):
raise InvalidUsage(message, status_code=500)


@game.route("/game/update/check/<int:game_id>", methods=["GET"])
@authorization_required
def game_check_for_update(game_id: int):
logger.info("Checking if Game Requireds an update")

game_obj = Games.query.filter_by(game_id=game_id).first()

if game_obj is None:
message = f"Game Server Update Check: Game ID {game_id} does not exist!"
logger.critical(message)
raise InvalidUsage(message, status_code=400)

steam_mgr = SteamUpdateManager()

try:
app_info = steam_mgr.is_update_required(
game_obj.game_steam_build_id,
game_obj.game_steam_build_branch,
game_obj.game_steam_id,
)
except Exception as error:
logger.critical(error)
raise InvalidUsage(
"Unable to determine if update is required.", status_code=500
)

return jsonify(app_info)


class GameArgumentsApi(MethodView):
def __init__(self, model):
self.model = model
Expand Down
66 changes: 65 additions & 1 deletion application/api/v1/blueprints/steam.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
from datetime import datetime
from flask import Blueprint, request, jsonify

from application.common import logger
from application.common.constants import GameActionTypes
from application.common.decorators import authorization_required
from application.common.exceptions import InvalidUsage
from application.extensions import DATABASE
from application.managers.steam_manager import SteamManager
from application.models.actions import Actions
from application.models.games import Games

steam = Blueprint("steam", __name__, url_prefix="/v1")

Expand Down Expand Up @@ -51,6 +56,22 @@ def steam_app_install():
payload["password"],
)

game_obj = Games.query.filter_by(
game_steam_id=steam_id, game_install_dir=payload["install_dir"]
).first()

try:
new_action = Actions()
new_action.type = GameActionTypes.INSTALLING.value
new_action.game_id = game_obj.game_id
new_action.result = install_thread.native_id
DATABASE.session.add(new_action)
DATABASE.session.commit()
except Exception:
message = "SteamManager: install_steam_app -> Error: Failed to update database."
logger.critical(message)
raise InvalidUsage(message, status_code=500)

payload.pop("steam_install_path")
payload.pop("steam_id")
payload.pop("install_dir")
Expand Down Expand Up @@ -104,6 +125,24 @@ def steam_app_update():
payload["password"],
)

game_qry = Games.query.filter_by(
game_steam_id=steam_id, game_install_dir=payload["install_dir"]
)
game_obj = game_qry.first()

try:
new_action = Actions()
new_action.type = GameActionTypes.UPDATING.value
new_action.game_id = game_obj.game_id
new_action.result = update_thread.native_id
game_obj.game_last_update = datetime.now()
DATABASE.session.add(new_action)
DATABASE.session.commit()
except Exception:
message = "SteamManager: install_steam_app -> Error: Failed to update database."
logger.critical(message)
raise InvalidUsage(message, status_code=500)

payload.pop("steam_install_path")
payload.pop("steam_id")
payload.pop("install_dir")
Expand All @@ -121,9 +160,34 @@ def steam_app_update():
)


# TODO - Implement this functionality.
# TODO - Deprecate this - Was implemented in game_base. No longer needed.
@steam.route("/steam/app/remove", methods=["POST"])
@authorization_required
def steam_app_remove():
logger.info("Remote uninstalls of game servers Not Yet Implemented")
return "Success"


@steam.route("/steam/app/build/id", methods=["POST"])
@authorization_required
def steam_app_get_build_id():
logger.info("Getting Steam Application Manifest Build ID")

payload = request.json

required_data = ["steam_install_path", "game_install_path", "steam_id"]

if not set(required_data).issubset(set(list(payload.keys()))):
message = "Error: Missing Required Data"
logger.error(message)
logger.info(payload.keys())
raise InvalidUsage(message, status_code=400)

steam_id = payload["steam_id"]
steam_install_path = payload["steam_install_path"]
game_install_path = payload["game_install_path"]

steam_mgr = SteamManager(steam_install_path, force_steam_install=False)
app_info = steam_mgr.get_build_id_from_app_manifest(game_install_path, steam_id)

return jsonify(app_info)
Loading

0 comments on commit 8108bc1

Please sign in to comment.