Compare commits

..
5 Commits
6 changed files with 149 additions and 36 deletions
+3 -2
View File
@@ -67,10 +67,11 @@ Complete guide to deploy and run the bot on a VPS with PostgreSQL secured over W
psql -U botuser -d mlbb_bot -h localhost
```
2. The bot automatically creates tables on first start (`src/db/schema.sql`).
3. Insert player records into `ref_player`:
3. Insert player records into `ref_player` (multiple IGNs can point to same discord_user_id):
```sql
INSERT INTO ref_player (in_game_name, discord_user_id) VALUES
('YourIGN', 123456789012345678),
('YourFirstIGN', 123456789012345678),
('Lilianna • Pristine', 123456789012345678),
('FriendIGN1', 234567890123456789),
('FriendIGN2', NULL);
```
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
set -e
echo "=== Deploying Epic MLBB Bot ==="
cd /home/sylph/epic
echo "1. Pulling latest changes from git..."
git pull origin main
echo "2. Syncing Python dependencies..."
/home/sylph/.hermes/bin/uv sync
echo "3. Restarting user systemd service..."
systemctl --user restart epic-bot.service
echo "4. Checking service status..."
sleep 2
systemctl --user is-active --quiet epic-bot.service && echo "✓ epic-bot is active and running" || (echo "✗ Service failed to start" && exit 1)
echo "=== Deployment Completed Successfully ==="
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
export XDG_RUNTIME_DIR=/run/user/1002
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1002/bus"
/home/sylph/epic/scripts/deploy.sh
+73 -10
View File
@@ -45,10 +45,16 @@ class MLBBCog(commands.Cog):
if not players_data:
return False, "No registered players detected in this match screenshot.", None
raw_match_id = extracted.get("match_id")
clean_match_id = str(raw_match_id).strip() if raw_match_id else None
if clean_match_id and clean_match_id.lower() in ("null", "none", "unknown", ""):
clean_match_id = None
match_result = extracted.get("match_result", "Unknown").capitalize()
game_mode = extracted.get("game_mode", "Ranked").capitalize()
inserted_records = []
skipped_records = []
async with self.bot.db.acquire() as conn:
async with conn.transaction():
for p in players_data:
@@ -65,6 +71,38 @@ class MLBBCog(commands.Cog):
assists = int(p.get("assists", 0))
damage = p.get("damage_dealt")
if clean_match_id:
existing = await conn.fetchval(
"SELECT 1 FROM match_records WHERE match_id = $1 AND player_id = $2",
clean_match_id,
player_info["id"],
)
if existing:
skipped_records.append(player_info["in_game_name"])
continue
await conn.execute(
"""
INSERT INTO match_records (
match_id, player_id, match_result, game_mode, hero_name, score,
medal, kills, deaths, assists, damage_dealt, recorded_by_discord_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT DO NOTHING
""",
clean_match_id,
player_info["id"],
match_result,
game_mode,
hero,
score,
medal,
kills,
deaths,
assists,
damage,
recorder_id,
)
else:
await conn.execute(
"""
INSERT INTO match_records (
@@ -84,6 +122,7 @@ class MLBBCog(commands.Cog):
damage,
recorder_id,
)
inserted_records.append(
{
"ign": player_info["in_game_name"],
@@ -95,12 +134,18 @@ class MLBBCog(commands.Cog):
)
if not inserted_records:
if skipped_records:
return False, f"Duplicate match detected ({clean_match_id}). Stats already recorded.", None
return False, "Extracted players could not be matched with `ref_player` database.", None
embed_color = discord.Color.green() if match_result == "Victory" else discord.Color.red()
desc = f"**Mode:** {game_mode} | **Party Size:** {len(inserted_records)}"
if clean_match_id:
desc = f"**Match ID:** `{clean_match_id}`\n" + desc
embed = discord.Embed(
title=f"⚔️ MLBB Match Recorded - {match_result}",
description=f"**Mode:** {game_mode} | **Party Size:** {len(inserted_records)}",
description=desc,
color=embed_color,
)
@@ -111,6 +156,9 @@ class MLBBCog(commands.Cog):
inline=False,
)
if skipped_records:
embed.set_footer(text=f"Duplicates skipped: {', '.join(skipped_records)}")
return True, "", embed
@app_commands.command(name="record", description="Record party stats from MLBB post-match scoreboard")
@@ -174,17 +222,25 @@ class MLBBCog(commands.Cog):
async with self.bot.db.acquire() as conn:
if ign:
player_row = await conn.fetchrow(
player_rows = await conn.fetch(
"SELECT id, in_game_name FROM ref_player WHERE LOWER(in_game_name) = LOWER($1)",
ign.strip(),
)
else:
player_row = await conn.fetchrow(
player_rows = await conn.fetch(
"SELECT id, in_game_name FROM ref_player WHERE discord_user_id = $1",
interaction.user.id,
)
if len(player_rows) > 1:
igns = ", ".join(f"`{r['in_game_name']}`" for r in player_rows)
await interaction.response.send_message(
f"You have multiple registered IGNs ({igns}). Please specify one: `/stats ign:<name>`",
ephemeral=True,
)
return
player_row = player_rows[0] if player_rows else None
if not player_row:
if not player_rows:
target_desc = f"**{ign}**" if ign else "your Discord account"
await interaction.response.send_message(
f"Player matching {target_desc} not found in `ref_player` database.",
@@ -192,6 +248,10 @@ class MLBBCog(commands.Cog):
)
return
player_ids = [row["id"] for row in player_rows]
ign_names = [row["in_game_name"] for row in player_rows]
title_name = " & ".join(ign_names) if len(ign_names) > 1 else ign_names[0]
stats_row = await conn.fetchrow(
"""
SELECT
@@ -206,14 +266,14 @@ class MLBBCog(commands.Cog):
COUNT(*) FILTER (WHERE medal = 'Silver') AS silver_count,
COUNT(*) FILTER (WHERE medal = 'Bronze') AS bronze_count
FROM match_records
WHERE player_id = $1
WHERE player_id = ANY($1::int[])
""",
player_row["id"],
player_ids,
)
if not stats_row or stats_row["total_matches"] == 0:
await interaction.response.send_message(
f"No recorded matches for **{player_row['in_game_name']}** yet.",
f"No recorded matches for **{title_name}** yet.",
ephemeral=True,
)
return
@@ -222,12 +282,12 @@ class MLBBCog(commands.Cog):
"""
SELECT hero_name, COUNT(*) AS games, AVG(score) AS hero_score
FROM match_records
WHERE player_id = $1
WHERE player_id = ANY($1::int[])
GROUP BY hero_name
ORDER BY games DESC, hero_score DESC
LIMIT 3
""",
player_row["id"],
player_ids,
)
total = stats_row["total_matches"]
@@ -238,9 +298,12 @@ class MLBBCog(commands.Cog):
kda_ratio = round(((stats_row["total_kills"] or 0) + (stats_row["total_assists"] or 0)) / max(deaths, 1), 2)
embed = discord.Embed(
title=f"📊 MLBB Stats: {player_row['in_game_name']}",
title=f"📊 MLBB Stats: {title_name}",
color=discord.Color.blurple(),
)
if len(ign_names) > 1:
embed.description = f"**Linked Accounts:** {', '.join(ign_names)}"
embed.add_field(name="Matches", value=f"{total} ({wins}W / {total - wins}L)", inline=True)
embed.add_field(name="Win Rate", value=f"{wr}%", inline=True)
embed.add_field(name="Avg Score", value=f"{avg_score}", inline=True)
+24 -1
View File
@@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS ref_player (
CREATE TABLE IF NOT EXISTS match_records (
id SERIAL PRIMARY KEY,
match_id VARCHAR(64),
player_id INT NOT NULL REFERENCES ref_player(id) ON DELETE CASCADE,
match_result VARCHAR(16) NOT NULL,
game_mode VARCHAR(32) NOT NULL DEFAULT 'Ranked',
@@ -21,5 +22,27 @@ CREATE TABLE IF NOT EXISTS match_records (
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_match_records_player ON match_records(player_id);
-- Migration safety for existing tables
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'ref_player' AND column_name = 'discord_user_id'
) THEN
ALTER TABLE ref_player DROP CONSTRAINT IF EXISTS ref_player_discord_user_id_key;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'match_records' AND column_name = 'match_id'
) THEN
ALTER TABLE match_records ADD COLUMN match_id VARCHAR(64);
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_ref_player_discord_id ON ref_player(discord_user_id);
CREATE INDEX IF NOT EXISTS idx_ref_player_ign_lower ON ref_player(LOWER(in_game_name));
CREATE INDEX IF NOT EXISTS idx_match_records_player ON match_records(player_id);
CREATE INDEX IF NOT EXISTS idx_match_records_match_id ON match_records(match_id);
CREATE UNIQUE INDEX IF NOT EXISTS uq_match_records_match_player ON match_records(match_id, player_id) WHERE match_id IS NOT NULL;
+6 -4
View File
@@ -33,13 +33,15 @@ class Router9Service:
You are an expert OCR and data extractor for Mobile Legends: Bang Bang (MLBB) post-match scoreboard screenshots.
Task:
1. Identify the match outcome: "Victory" or "Defeat".
2. Identify game mode: "Ranked", "Classic", "Brawl", or "Custom" (default to "Ranked" if unsure).
3. Extract player statistics ONLY for players whose in-game name matches one of these registered names (case-insensitive): [{allowed_names_str}].
4. Strictly ignore all random/other players not in the list.
1. Extract the Match ID if visible on the scoreboard (usually located at the bottom-left or bottom area of the MLBB post-match screen, e.g. "Match ID 123456789012345678" or numerical digits). If not visible or uncertain, return null.
2. Identify the match outcome: "Victory" or "Defeat".
3. Identify game mode: "Ranked", "Classic", "Brawl", or "Custom" (default to "Ranked" if unsure).
4. Extract player statistics ONLY for players whose in-game name matches one of these registered names (case-insensitive): [{allowed_names_str}].
5. Strictly ignore all random/other players not in the list.
Output strict JSON only with no markdown formatting or backticks:
{{
"match_id": "string digits/id or null",
"match_result": "Victory" | "Defeat",
"game_mode": "Ranked" | "Classic" | "Brawl" | "Custom",
"players": [