Skip to content

Commit 88181ce

Browse files
authored
Merge pull request #1174 from MarechJ/feat/soldier-and-account-tables
Persisting and extending player profile details
2 parents 0acd058 + 7c89446 commit 88181ce

62 files changed

Lines changed: 4008 additions & 847 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Add PlayerAccount and PlayerSoldier tables
2+
3+
Revision ID: 0ac19ea4739e
4+
Revises: 78098bd1bbb0
5+
Create Date: 2025-10-28 15:08:24.036663
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
from sqlalchemy import text
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = '0ac19ea4739e'
15+
down_revision = '78098bd1bbb0'
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# Create the player_account table
22+
op.create_table(
23+
'player_account',
24+
sa.Column('id', sa.Integer(), nullable=False),
25+
sa.Column('playersteamid_id', sa.Integer(), nullable=False),
26+
sa.Column('name', sa.String(), nullable=True),
27+
sa.Column('discord_id', sa.String(), nullable=True),
28+
sa.Column('is_member', sa.Boolean(), nullable=False, default=sa.false()),
29+
sa.Column('country', sa.String(2), nullable=True),
30+
sa.Column('lang', sa.String(2), nullable=False, default='en'),
31+
sa.Column('updated', sa.TIMESTAMP(timezone=True), nullable=False, default=sa.func.now()),
32+
sa.ForeignKeyConstraint(['playersteamid_id'], ['steam_id_64.id'], ),
33+
sa.PrimaryKeyConstraint('id'),
34+
sa.UniqueConstraint('playersteamid_id', name='unique_player_account')
35+
)
36+
37+
# Create the player_soldier table
38+
op.create_table(
39+
'player_soldier',
40+
sa.Column('id', sa.Integer(), nullable=False),
41+
sa.Column('playersteamid_id', sa.Integer(), nullable=False),
42+
sa.Column('name', sa.String(), nullable=True),
43+
sa.Column('level', sa.Integer(), nullable=False, default=0),
44+
sa.Column('platform', sa.String(), nullable=True),
45+
sa.Column('clan_tag', sa.String(), nullable=True),
46+
sa.Column('updated', sa.TIMESTAMP(timezone=True), nullable=False, default=sa.func.now()),
47+
sa.ForeignKeyConstraint(['playersteamid_id'], ['steam_id_64.id'], ),
48+
sa.PrimaryKeyConstraint('id'),
49+
sa.UniqueConstraint('playersteamid_id', name='unique_player_soldier')
50+
)
51+
52+
# Populate the player_account & player_soldier tables with data from existing tables
53+
connection = op.get_bind()
54+
55+
connection.execute(text("""
56+
INSERT INTO player_account (playersteamid_id, country, is_member, lang, updated)
57+
SELECT
58+
p.id as playersteamid_id,
59+
CASE
60+
WHEN si.country = 'private' THEN NULL
61+
ELSE si.country
62+
END as country,
63+
false as is_member,
64+
'en' as lang,
65+
NOW() as updated
66+
FROM steam_id_64 p
67+
LEFT JOIN steam_info si ON si.playersteamid_id = p.id
68+
"""))
69+
70+
connection.execute(text("""
71+
INSERT INTO player_soldier (playersteamid_id, name, level, platform, updated)
72+
SELECT
73+
p.id as playersteamid_id,
74+
pn.name as name,
75+
COALESCE(ps_max.level, 0) as level,
76+
CASE
77+
WHEN LENGTH(p.steam_id_64) = 17 AND p.steam_id_64 ~ '^[0-9]+$' THEN 'steam'
78+
ELSE NULL
79+
END as platform,
80+
NOW() as updated
81+
FROM steam_id_64 p
82+
LEFT JOIN LATERAL (
83+
SELECT name
84+
FROM player_names pn
85+
WHERE pn.playersteamid_id = p.id
86+
ORDER BY pn.last_seen DESC
87+
LIMIT 1
88+
) pn ON true
89+
LEFT JOIN LATERAL (
90+
SELECT MAX(level) as level
91+
FROM player_stats ps
92+
WHERE ps.playersteamid_id = p.id
93+
AND ps.level > 0
94+
) ps_max ON true
95+
"""))
96+
97+
98+
def downgrade():
99+
# Drop the player_profile table
100+
op.drop_table('player_account')
101+
op.drop_table('player_soldier')
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Add level column to PlayerStats
2+
3+
Revision ID: 78098bd1bbb0
4+
Revises: f80b6dc2833a
5+
Create Date: 2025-08-12 12:37:30.321976
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '78098bd1bbb0'
14+
down_revision = 'f80b6dc2833a'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
op.add_column('player_stats', sa.Column('level', sa.Integer, nullable=False, server_default='0'))
21+
22+
23+
def downgrade():
24+
op.drop_column('player_stats', 'level')
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Add EOS ID column
2+
3+
Revision ID: 89a3502370a0
4+
Revises: 0ac19ea4739e
5+
Create Date: 2025-11-20 12:57:44.985471
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
from sqlalchemy.dialects import postgresql
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '89a3502370a0'
14+
down_revision = '0ac19ea4739e'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.add_column('player_soldier', sa.Column('eos_id', sa.String(), nullable=True))
22+
# ### end Alembic commands ###
23+
24+
25+
def downgrade():
26+
# ### commands auto generated by Alembic - please adjust! ###
27+
op.drop_column('player_soldier', 'eos_id')
28+
# ### end Alembic commands ###
6.47 KB
Loading
6.79 KB
Loading
9.02 KB
Loading

rcon/api_commands.py

Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2004,27 +2004,109 @@ def disband_squad_by_name(self, team_name: str, squad_name: str, reason: str):
20042004
team_name, squad_name = team_name.lower(), squad_name.lower()
20052005

20062006
if team_name != "allies" and team_name != "axis":
2007-
raise HLLCommandFailedError("Invalid team_name argument. It must be either 'axis' or 'allies'.")
2007+
raise HLLCommandFailedError(
2008+
"Invalid team_name argument. It must be either 'axis' or 'allies'."
2009+
)
20082010
if squad_name == "" or squad_name == "unassigned":
2009-
raise HLLCommandFailedError("Invalid squad_name argument. It cannot be an empty value or 'unassigned'.")
2011+
raise HLLCommandFailedError(
2012+
"Invalid squad_name argument. It cannot be an empty value or 'unassigned'."
2013+
)
20102014

20112015
online_players = self.get_detailed_players()["players"]
20122016

20132017
squad_players = list(
2014-
online_players[id]
2015-
for id in online_players
2016-
if online_players[id]["team"] == team_name
2017-
and online_players[id]["unit_name"] == squad_name
2018-
)
2018+
online_players[id]
2019+
for id in online_players
2020+
if online_players[id]["team"] == team_name
2021+
and online_players[id]["unit_name"] == squad_name
2022+
)
20192023

20202024
if not squad_players:
2021-
raise HLLCommandFailedError(f"Squad {squad_name} was not found in team {team_name}. It might have been disbanded already.")
2022-
2025+
raise HLLCommandFailedError(
2026+
f"Squad {squad_name} was not found in team {team_name}. It might have been disbanded already."
2027+
)
2028+
20232029
for player in squad_players:
20242030
super().remove_player_from_squad(player["player_id"], reason)
20252031

20262032
return {
20272033
"team_name": team_name,
20282034
"squad_name": squad_name,
2029-
"msg": f"Successfully disbaned {squad_name} squad in team {team_name}"
2035+
"msg": f"Successfully disbanded {squad_name} squad in team {team_name}",
20302036
}
2037+
2038+
def edit_player_soldier(
2039+
self,
2040+
player_id: str,
2041+
name: str | None = None,
2042+
level: int = 0,
2043+
platform: str | None = None,
2044+
clan_tag: str | None = None,
2045+
eos_id: str | None = None,
2046+
):
2047+
"""
2048+
Update NULL fields only in PlayerSoldier for the given player_id.
2049+
Does not overwrite any existing non-null values.
2050+
Returns the updated soldier as dict or None if not found.
2051+
"""
2052+
from rcon.models import enter_session, PlayerSoldier
2053+
2054+
with enter_session() as sess:
2055+
soldier_db, changed = PlayerSoldier.update_missing_fields(
2056+
sess,
2057+
player_id,
2058+
name=name,
2059+
level=level,
2060+
platform=platform,
2061+
clan_tag=clan_tag,
2062+
eos_id=eos_id,
2063+
)
2064+
if not soldier_db:
2065+
return HLLCommandFailedError(
2066+
f"Player {player_id} was not found. The soldier could not be updated."
2067+
)
2068+
if not changed:
2069+
return {
2070+
"soldier": soldier_db.to_dict(),
2071+
"msg": "Success!\nSome fields have not been saved as only null values are allowed to be modified.",
2072+
}
2073+
return {
2074+
"soldier": soldier_db.to_dict(),
2075+
"msg": "Successfully updated soldier details.",
2076+
}
2077+
2078+
def edit_player_account(
2079+
self,
2080+
player_id: str,
2081+
name: str | None = None,
2082+
discord_id: str | None = None,
2083+
is_member: bool = False,
2084+
country: str | None = None,
2085+
lang: str = "en",
2086+
):
2087+
"""
2088+
Update PlayerAccount fields for the given player_id.
2089+
All fields can be updated, including setting them to null.
2090+
Returns dict with account data and success message.
2091+
"""
2092+
from rcon.models import enter_session, PlayerAccount
2093+
2094+
with enter_session() as sess:
2095+
account_db = PlayerAccount.update_account(
2096+
sess,
2097+
player_id,
2098+
name=name,
2099+
discord_id=discord_id,
2100+
is_member=is_member,
2101+
country=country,
2102+
lang=lang,
2103+
)
2104+
if not account_db:
2105+
return HLLCommandFailedError(
2106+
f"Player {player_id} was not found. The account could not be updated."
2107+
)
2108+
2109+
return {
2110+
"account": account_db.to_dict(),
2111+
"msg": "Successfully updated account details.",
2112+
}

rcon/commands.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from rcon.connection import HLLCommandError, HLLConnection, Handle, Response
1010
from rcon.maps import LAYERS, MAPS, UNKNOWN_MAP_NAME, Environment, GameMode, LayerType
11-
from rcon.types import MapRotationResponse, MapSequenceResponse, ServerInfoType, SlotsType, VipId, GameStateType, AdminType
11+
from rcon.types import MapRotationResponse, MapSequenceResponse, PlayerInfoType, ServerInfoType, SlotsType, VipId, GameStateType, AdminType
1212
from rcon.utils import exception_in_chain
1313

1414
logger = logging.getLogger(__name__)
@@ -310,15 +310,12 @@ def get_maps(self) -> list[str]:
310310
return parameters[0]["valueMember"].split(",")
311311

312312
def get_player_ids(self) -> dict[str, str]:
313-
# TODO: Updated function signatures
314313
return {x["name"]: x["iD"] for x in self.exchange("GetServerInformation", 2, {"Name": "players", "Value": ""}).content_dict["players"]}
315314

316-
def get_all_player_info(self) -> list[dict[str, Any]]:
317-
# TODO: Updated function signatures
315+
def get_all_player_info(self) -> list[PlayerInfoType]:
318316
return self.exchange("GetServerInformation", 2, {"Name": "players", "Value": ""}).content_dict["players"]
319317

320-
def get_player_info(self, player_id: str) -> dict[str, Any] | None:
321-
# TODO: Updated function signatures
318+
def get_player_info(self, player_id: str) -> PlayerInfoType | None:
322319
return self.exchange("GetServerInformation", 2, {"Name": "player", "Value": player_id}).content_dict
323320

324321
def get_admin_ids(self) -> list[AdminType]:

rcon/hooks.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
)
3030
from rcon.maps import UNKNOWN_MAP_NAME, parse_layer
3131
from rcon.message_variables import format_message_string, populate_message_variables
32-
from rcon.models import PlayerID, enter_session, GameLayout
32+
from rcon.models import PlayerID, PlayerSoldier, enter_session, GameLayout
3333
from rcon.player_history import (
3434
_get_set_player,
3535
get_player,
@@ -519,6 +519,12 @@ def handle_on_connect(
519519
timestamp=int(struct_log["timestamp_ms"]) / 1000,
520520
)
521521

522+
try:
523+
if (player := rcon.get_detailed_player_info(player_id)):
524+
PlayerSoldier.update(player)
525+
except Exception:
526+
logger.exception("Unable to update soldier info for %s", player_id)
527+
522528
blacklisted = ban_if_blacklisted(rcon, player_id, struct_log["player_name_1"])
523529
if blacklisted:
524530
# We don't need the player potentially blacklisted a second

rcon/logs/loop.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,13 +287,16 @@ def record_player_stats(self, players: dict[str, GetDetailedPlayer]):
287287
p_defense=0,
288288
support=player["support"],
289289
p_support=0,
290+
level=player["level"],
290291
),
291292
)
292293
for stat in ["combat", "offense", "defense", "support"]:
293294
if player[stat] < p[stat]:
294295
p["p_" + stat] = p["p_" + stat] + p[stat]
295296

296297
p[stat] = player[stat]
298+
299+
p["level"] = player["level"]
297300
map_players[player_id] = p
298301
maps.update(0, m)
299302

0 commit comments

Comments
 (0)