Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8a68dea4d | ||
|
|
c305af9900 |
@@ -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
|
psql -U botuser -d mlbb_bot -h localhost
|
||||||
```
|
```
|
||||||
2. The bot automatically creates tables on first start (`src/db/schema.sql`).
|
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
|
```sql
|
||||||
INSERT INTO ref_player (in_game_name, discord_user_id) VALUES
|
INSERT INTO ref_player (in_game_name, discord_user_id) VALUES
|
||||||
('YourIGN', 123456789012345678),
|
('YourFirstIGN', 123456789012345678),
|
||||||
|
('Lilianna • Pristine', 123456789012345678),
|
||||||
('FriendIGN1', 234567890123456789),
|
('FriendIGN1', 234567890123456789),
|
||||||
('FriendIGN2', NULL);
|
('FriendIGN2', NULL);
|
||||||
```
|
```
|
||||||
|
|||||||
+84
-29
@@ -45,10 +45,16 @@ class MLBBCog(commands.Cog):
|
|||||||
if not players_data:
|
if not players_data:
|
||||||
return False, "No registered players detected in this match screenshot.", None
|
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()
|
match_result = extracted.get("match_result", "Unknown").capitalize()
|
||||||
game_mode = extracted.get("game_mode", "Ranked").capitalize()
|
game_mode = extracted.get("game_mode", "Ranked").capitalize()
|
||||||
|
|
||||||
inserted_records = []
|
inserted_records = []
|
||||||
|
skipped_records = []
|
||||||
async with self.bot.db.acquire() as conn:
|
async with self.bot.db.acquire() as conn:
|
||||||
async with conn.transaction():
|
async with conn.transaction():
|
||||||
for p in players_data:
|
for p in players_data:
|
||||||
@@ -65,25 +71,58 @@ class MLBBCog(commands.Cog):
|
|||||||
assists = int(p.get("assists", 0))
|
assists = int(p.get("assists", 0))
|
||||||
damage = p.get("damage_dealt")
|
damage = p.get("damage_dealt")
|
||||||
|
|
||||||
await conn.execute(
|
if clean_match_id:
|
||||||
"""
|
existing = await conn.fetchval(
|
||||||
INSERT INTO match_records (
|
"SELECT 1 FROM match_records WHERE match_id = $1 AND player_id = $2",
|
||||||
player_id, match_result, game_mode, hero_name, score,
|
clean_match_id,
|
||||||
medal, kills, deaths, assists, damage_dealt, recorded_by_discord_id
|
player_info["id"],
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
)
|
||||||
""",
|
if existing:
|
||||||
player_info["id"],
|
skipped_records.append(player_info["in_game_name"])
|
||||||
match_result,
|
continue
|
||||||
game_mode,
|
|
||||||
hero,
|
await conn.execute(
|
||||||
score,
|
"""
|
||||||
medal,
|
INSERT INTO match_records (
|
||||||
kills,
|
match_id, player_id, match_result, game_mode, hero_name, score,
|
||||||
deaths,
|
medal, kills, deaths, assists, damage_dealt, recorded_by_discord_id
|
||||||
assists,
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||||
damage,
|
ON CONFLICT DO NOTHING
|
||||||
recorder_id,
|
""",
|
||||||
)
|
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 (
|
||||||
|
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)
|
||||||
|
""",
|
||||||
|
player_info["id"],
|
||||||
|
match_result,
|
||||||
|
game_mode,
|
||||||
|
hero,
|
||||||
|
score,
|
||||||
|
medal,
|
||||||
|
kills,
|
||||||
|
deaths,
|
||||||
|
assists,
|
||||||
|
damage,
|
||||||
|
recorder_id,
|
||||||
|
)
|
||||||
|
|
||||||
inserted_records.append(
|
inserted_records.append(
|
||||||
{
|
{
|
||||||
"ign": player_info["in_game_name"],
|
"ign": player_info["in_game_name"],
|
||||||
@@ -95,12 +134,18 @@ class MLBBCog(commands.Cog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not inserted_records:
|
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
|
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()
|
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(
|
embed = discord.Embed(
|
||||||
title=f"⚔️ MLBB Match Recorded - {match_result}",
|
title=f"⚔️ MLBB Match Recorded - {match_result}",
|
||||||
description=f"**Mode:** {game_mode} | **Party Size:** {len(inserted_records)}",
|
description=desc,
|
||||||
color=embed_color,
|
color=embed_color,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -111,6 +156,9 @@ class MLBBCog(commands.Cog):
|
|||||||
inline=False,
|
inline=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if skipped_records:
|
||||||
|
embed.set_footer(text=f"Duplicates skipped: {', '.join(skipped_records)}")
|
||||||
|
|
||||||
return True, "", embed
|
return True, "", embed
|
||||||
|
|
||||||
@app_commands.command(name="record", description="Record party stats from MLBB post-match scoreboard")
|
@app_commands.command(name="record", description="Record party stats from MLBB post-match scoreboard")
|
||||||
@@ -174,17 +222,17 @@ class MLBBCog(commands.Cog):
|
|||||||
|
|
||||||
async with self.bot.db.acquire() as conn:
|
async with self.bot.db.acquire() as conn:
|
||||||
if ign:
|
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)",
|
"SELECT id, in_game_name FROM ref_player WHERE LOWER(in_game_name) = LOWER($1)",
|
||||||
ign.strip(),
|
ign.strip(),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
player_row = await conn.fetchrow(
|
player_rows = await conn.fetch(
|
||||||
"SELECT id, in_game_name FROM ref_player WHERE discord_user_id = $1",
|
"SELECT id, in_game_name FROM ref_player WHERE discord_user_id = $1",
|
||||||
interaction.user.id,
|
interaction.user.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not player_row:
|
if not player_rows:
|
||||||
target_desc = f"**{ign}**" if ign else "your Discord account"
|
target_desc = f"**{ign}**" if ign else "your Discord account"
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
f"Player matching {target_desc} not found in `ref_player` database.",
|
f"Player matching {target_desc} not found in `ref_player` database.",
|
||||||
@@ -192,6 +240,10 @@ class MLBBCog(commands.Cog):
|
|||||||
)
|
)
|
||||||
return
|
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(
|
stats_row = await conn.fetchrow(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
@@ -206,14 +258,14 @@ class MLBBCog(commands.Cog):
|
|||||||
COUNT(*) FILTER (WHERE medal = 'Silver') AS silver_count,
|
COUNT(*) FILTER (WHERE medal = 'Silver') AS silver_count,
|
||||||
COUNT(*) FILTER (WHERE medal = 'Bronze') AS bronze_count
|
COUNT(*) FILTER (WHERE medal = 'Bronze') AS bronze_count
|
||||||
FROM match_records
|
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:
|
if not stats_row or stats_row["total_matches"] == 0:
|
||||||
await interaction.response.send_message(
|
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,
|
ephemeral=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -222,12 +274,12 @@ class MLBBCog(commands.Cog):
|
|||||||
"""
|
"""
|
||||||
SELECT hero_name, COUNT(*) AS games, AVG(score) AS hero_score
|
SELECT hero_name, COUNT(*) AS games, AVG(score) AS hero_score
|
||||||
FROM match_records
|
FROM match_records
|
||||||
WHERE player_id = $1
|
WHERE player_id = ANY($1::int[])
|
||||||
GROUP BY hero_name
|
GROUP BY hero_name
|
||||||
ORDER BY games DESC, hero_score DESC
|
ORDER BY games DESC, hero_score DESC
|
||||||
LIMIT 3
|
LIMIT 3
|
||||||
""",
|
""",
|
||||||
player_row["id"],
|
player_ids,
|
||||||
)
|
)
|
||||||
|
|
||||||
total = stats_row["total_matches"]
|
total = stats_row["total_matches"]
|
||||||
@@ -238,9 +290,12 @@ class MLBBCog(commands.Cog):
|
|||||||
kda_ratio = round(((stats_row["total_kills"] or 0) + (stats_row["total_assists"] or 0)) / max(deaths, 1), 2)
|
kda_ratio = round(((stats_row["total_kills"] or 0) + (stats_row["total_assists"] or 0)) / max(deaths, 1), 2)
|
||||||
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=f"📊 MLBB Stats: {player_row['in_game_name']}",
|
title=f"📊 MLBB Stats: {title_name}",
|
||||||
color=discord.Color.blurple(),
|
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="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="Win Rate", value=f"{wr}%", inline=True)
|
||||||
embed.add_field(name="Avg Score", value=f"{avg_score}", inline=True)
|
embed.add_field(name="Avg Score", value=f"{avg_score}", inline=True)
|
||||||
|
|||||||
+24
-1
@@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS ref_player (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS match_records (
|
CREATE TABLE IF NOT EXISTS match_records (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
|
match_id VARCHAR(64),
|
||||||
player_id INT NOT NULL REFERENCES ref_player(id) ON DELETE CASCADE,
|
player_id INT NOT NULL REFERENCES ref_player(id) ON DELETE CASCADE,
|
||||||
match_result VARCHAR(16) NOT NULL,
|
match_result VARCHAR(16) NOT NULL,
|
||||||
game_mode VARCHAR(32) NOT NULL DEFAULT 'Ranked',
|
game_mode VARCHAR(32) NOT NULL DEFAULT 'Ranked',
|
||||||
@@ -21,5 +22,27 @@ CREATE TABLE IF NOT EXISTS match_records (
|
|||||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
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_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
@@ -33,13 +33,15 @@ class Router9Service:
|
|||||||
You are an expert OCR and data extractor for Mobile Legends: Bang Bang (MLBB) post-match scoreboard screenshots.
|
You are an expert OCR and data extractor for Mobile Legends: Bang Bang (MLBB) post-match scoreboard screenshots.
|
||||||
|
|
||||||
Task:
|
Task:
|
||||||
1. Identify the match outcome: "Victory" or "Defeat".
|
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 game mode: "Ranked", "Classic", "Brawl", or "Custom" (default to "Ranked" if unsure).
|
2. Identify the match outcome: "Victory" or "Defeat".
|
||||||
3. Extract player statistics ONLY for players whose in-game name matches one of these registered names (case-insensitive): [{allowed_names_str}].
|
3. Identify game mode: "Ranked", "Classic", "Brawl", or "Custom" (default to "Ranked" if unsure).
|
||||||
4. Strictly ignore all random/other players not in the list.
|
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:
|
Output strict JSON only with no markdown formatting or backticks:
|
||||||
{{
|
{{
|
||||||
|
"match_id": "string digits/id or null",
|
||||||
"match_result": "Victory" | "Defeat",
|
"match_result": "Victory" | "Defeat",
|
||||||
"game_mode": "Ranked" | "Classic" | "Brawl" | "Custom",
|
"game_mode": "Ranked" | "Classic" | "Brawl" | "Custom",
|
||||||
"players": [
|
"players": [
|
||||||
|
|||||||
Reference in New Issue
Block a user