Compare commits
5
Commits
b75cc8bc37
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50917f84f6 | ||
|
|
0226df3fd0 | ||
|
|
08c6ac009b | ||
|
|
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
|
||||
```
|
||||
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);
|
||||
```
|
||||
|
||||
Executable
+20
@@ -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 ==="
|
||||
@@ -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
@@ -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
@@ -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
@@ -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": [
|
||||
|
||||
Reference in New Issue
Block a user