mirror of
https://github.com/bombsquad-community/plugin-manager.git
synced 2026-08-15 13:03:08 +00:00
Merge d1ebef1def into d442c44ea8
This commit is contained in:
commit
8fe7686959
2 changed files with 637 additions and 0 deletions
|
|
@ -2583,6 +2583,31 @@
|
||||||
"md5sum": "3ead338c768fbdffe022aadcbdc8c1d8"
|
"md5sum": "3ead338c768fbdffe022aadcbdc8c1d8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"friend_manager": {
|
||||||
|
"description": "Build a real friend list while playing online! Click any player's name in a party to add them as a friend - their account ID and every profile name they've played under gets saved automatically, so you'll recognize them even if they switch profiles later. Get notified when a friend joins your party. Find your full friend list anytime from Settings -> Friends.",
|
||||||
|
"external_url": "",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "ItzHacker101",
|
||||||
|
"email": "",
|
||||||
|
"discord": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"versions": {
|
||||||
|
"1.0.1": {
|
||||||
|
"api_version": 9,
|
||||||
|
"commit_sha": "755a5eb",
|
||||||
|
"released_on": "21-06-2026",
|
||||||
|
"md5sum": "f107e7e0ec6e3bbaba19a840255681f6"
|
||||||
|
},
|
||||||
|
"1.0.0": {
|
||||||
|
"api_version": 9,
|
||||||
|
"commit_sha": "5cf2ebd",
|
||||||
|
"released_on": "21-06-2026",
|
||||||
|
"md5sum": "e8bbf07e9f8546aea2be25e7398f6559"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
612
plugins/utilities/friend_manager.py
Normal file
612
plugins/utilities/friend_manager.py
Normal file
|
|
@ -0,0 +1,612 @@
|
||||||
|
# ba_meta require api 9
|
||||||
|
"""
|
||||||
|
Friend Manager
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
Add players you meet in online parties to a persistent friend list, see
|
||||||
|
their known profile names, and get notified when they join a party with you.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
1. In any party (e.g. the Advanced Party Window), click/tap a player's
|
||||||
|
name to open their menu.
|
||||||
|
2. Select "Add Friend" (or "View Profile" if already a friend).
|
||||||
|
3. Open the full list anytime from Settings -> the "Friends" button.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
import babase
|
||||||
|
import bauiv1 as bui
|
||||||
|
import bascenev1 as bs
|
||||||
|
import bauiv1lib.popup
|
||||||
|
import bauiv1lib.settings.allsettings
|
||||||
|
import bauiv1lib.party
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
plugman = dict(
|
||||||
|
plugin_name="friend_manager",
|
||||||
|
description=(
|
||||||
|
"Build a real friend list while playing online! Click any player's "
|
||||||
|
"name in a party to add them as a friend - their account ID and every "
|
||||||
|
"profile name they've played under gets saved automatically, so you'll "
|
||||||
|
"recognize them even if they switch profiles later. Get notified when "
|
||||||
|
"a friend joins your party. Find your full friend list anytime from "
|
||||||
|
"Settings -> Friends."
|
||||||
|
),
|
||||||
|
external_url="",
|
||||||
|
authors=[{"name": "ItzHacker101", "email": "", "discord": ""}],
|
||||||
|
version="1.0.1",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Data layer
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class FriendManager:
|
||||||
|
"""Stores and persists the friend list as a local JSON file."""
|
||||||
|
|
||||||
|
friends: dict = {}
|
||||||
|
_file_path: str | None = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_file_path(cls) -> str:
|
||||||
|
if cls._file_path is None:
|
||||||
|
try:
|
||||||
|
base_dir = babase.app.env.python_directory_user
|
||||||
|
except Exception:
|
||||||
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
# Keep our data in its own folder (not loose in the mods root)
|
||||||
|
# so the whole "friend_manager_data" folder can simply be copied
|
||||||
|
# to another device/install and friends will carry over.
|
||||||
|
data_dir = os.path.join(base_dir, "friend_manager_data")
|
||||||
|
try:
|
||||||
|
os.makedirs(data_dir, exist_ok=True)
|
||||||
|
except Exception:
|
||||||
|
data_dir = base_dir # fall back rather than crash
|
||||||
|
|
||||||
|
cls._file_path = os.path.join(data_dir, "friend_list.json")
|
||||||
|
return cls._file_path
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls):
|
||||||
|
path = cls._get_file_path()
|
||||||
|
if os.path.exists(path):
|
||||||
|
try:
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
cls.friends = data if isinstance(data, dict) else {}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[FriendManager] Error loading friend list: {e}")
|
||||||
|
cls.friends = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def save(cls):
|
||||||
|
path = cls._get_file_path()
|
||||||
|
try:
|
||||||
|
tmp_path = path + ".tmp"
|
||||||
|
with open(tmp_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(cls.friends, f, indent=4, ensure_ascii=False)
|
||||||
|
os.replace(tmp_path, path) # atomic-ish write
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[FriendManager] Error saving friend list: {e}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add(cls, pb_id: str, v2_name: str, profiles: list, is_stable_id: bool):
|
||||||
|
if not pb_id:
|
||||||
|
return
|
||||||
|
profiles = [p for p in (profiles or []) if p]
|
||||||
|
now = str(datetime.datetime.now().strftime("%Y-%m-%d %H:%M"))
|
||||||
|
|
||||||
|
if pb_id in cls.friends:
|
||||||
|
entry = cls.friends[pb_id]
|
||||||
|
existing = entry.setdefault("profiles", [])
|
||||||
|
for p in profiles:
|
||||||
|
if p not in existing:
|
||||||
|
existing.append(p)
|
||||||
|
if v2_name:
|
||||||
|
entry["v2_name"] = v2_name
|
||||||
|
entry["last_seen"] = now
|
||||||
|
entry["stable_id"] = is_stable_id
|
||||||
|
else:
|
||||||
|
cls.friends[pb_id] = {
|
||||||
|
"v2_name": v2_name or "Unknown",
|
||||||
|
"profiles": profiles,
|
||||||
|
"added": now,
|
||||||
|
"last_seen": now,
|
||||||
|
"stable_id": is_stable_id,
|
||||||
|
}
|
||||||
|
cls.save()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def remove(cls, pb_id: str):
|
||||||
|
if pb_id in cls.friends:
|
||||||
|
del cls.friends[pb_id]
|
||||||
|
cls.save()
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _safe_roster() -> list:
|
||||||
|
"""get_game_roster() can raise or return None outside an active session."""
|
||||||
|
try:
|
||||||
|
roster = bs.get_game_roster()
|
||||||
|
return roster if roster else []
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_player(entry: dict) -> tuple[str, str, list, bool]:
|
||||||
|
"""
|
||||||
|
Pulls a pb_id, display name, profile-name list, and an "is this ID
|
||||||
|
actually stable" flag out of a roster entry.
|
||||||
|
|
||||||
|
account_id (the V2 account hash) is the only truly stable identifier.
|
||||||
|
If a player isn't signed into a V2 account, there's no persistent ID
|
||||||
|
the game gives us at all - in that case we fall back to their current
|
||||||
|
display name, but flag it as unstable so the UI can warn about it
|
||||||
|
instead of silently pretending it's reliable.
|
||||||
|
"""
|
||||||
|
client_id = entry.get('client_id')
|
||||||
|
account_id = entry.get('account_id')
|
||||||
|
v2_name = entry.get('display_string') or 'Unknown'
|
||||||
|
is_stable = bool(account_id)
|
||||||
|
pb_id = account_id or v2_name or f"client_{client_id}"
|
||||||
|
profiles = [pl.get('name') for pl in entry.get('players', []) if pl.get('name')]
|
||||||
|
return pb_id, v2_name, profiles, is_stable
|
||||||
|
|
||||||
|
|
||||||
|
def _format_relative_time(timestamp_str: str) -> str:
|
||||||
|
"""Turns '2026-06-19 10:42' into something like 'Yesterday' or '3h ago'."""
|
||||||
|
if not timestamp_str:
|
||||||
|
return "Unknown"
|
||||||
|
try:
|
||||||
|
then = datetime.datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M")
|
||||||
|
except Exception:
|
||||||
|
return timestamp_str
|
||||||
|
|
||||||
|
delta = datetime.datetime.now() - then
|
||||||
|
seconds = delta.total_seconds()
|
||||||
|
|
||||||
|
if seconds < 0:
|
||||||
|
return then.strftime("%Y-%m-%d")
|
||||||
|
if seconds < 60:
|
||||||
|
return "Just now"
|
||||||
|
if seconds < 3600:
|
||||||
|
m = int(seconds // 60)
|
||||||
|
return f"{m} minute{'s' if m != 1 else ''} ago"
|
||||||
|
if seconds < 86400:
|
||||||
|
h = int(seconds // 3600)
|
||||||
|
return f"{h} hour{'s' if h != 1 else ''} ago"
|
||||||
|
days = delta.days
|
||||||
|
if days == 1:
|
||||||
|
return "Yesterday"
|
||||||
|
if days < 7:
|
||||||
|
return f"{days} days ago"
|
||||||
|
if days < 30:
|
||||||
|
weeks = days // 7
|
||||||
|
return f"{weeks} week{'s' if weeks != 1 else ''} ago"
|
||||||
|
return then.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# UI
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class FriendProfileWindow(bui.Window):
|
||||||
|
def __init__(self, pb_id: str):
|
||||||
|
self.pb_id = pb_id
|
||||||
|
self.data = FriendManager.friends.get(pb_id, {})
|
||||||
|
self._width = 450
|
||||||
|
self._height = 440
|
||||||
|
uiscale = bui.app.ui_v1.uiscale
|
||||||
|
|
||||||
|
super().__init__(root_widget=bui.containerwidget(
|
||||||
|
size=(self._width, self._height),
|
||||||
|
transition='in_scale',
|
||||||
|
scale=1.5 if uiscale is babase.UIScale.SMALL else 1.2
|
||||||
|
))
|
||||||
|
|
||||||
|
self._back_btn = bui.buttonwidget(
|
||||||
|
parent=self._root_widget,
|
||||||
|
position=(20, self._height - 60),
|
||||||
|
size=(50, 50),
|
||||||
|
label=bui.charstr(bui.SpecialChar.BACK),
|
||||||
|
button_type='backSmall',
|
||||||
|
on_activate_call=self._back
|
||||||
|
)
|
||||||
|
bui.containerwidget(edit=self._root_widget, cancel_button=self._back_btn)
|
||||||
|
|
||||||
|
bui.textwidget(parent=self._root_widget, position=(self._width / 2, self._height - 40),
|
||||||
|
text="🪪 Friend Profile", h_align='center', v_align='center',
|
||||||
|
scale=1.3, color=(0.4, 1.0, 0.4))
|
||||||
|
|
||||||
|
v = self._height - 100
|
||||||
|
bui.textwidget(parent=self._root_widget, position=(self._width / 2, v),
|
||||||
|
text=f"Name: {self.data.get('v2_name', 'Unknown')}",
|
||||||
|
h_align='center', scale=1.1)
|
||||||
|
v -= 28
|
||||||
|
bui.textwidget(parent=self._root_widget, position=(self._width / 2, v),
|
||||||
|
text=f"ID: {self.pb_id}", h_align='center', scale=0.55,
|
||||||
|
color=(0.7, 0.7, 0.7))
|
||||||
|
v -= 24
|
||||||
|
added = self.data.get('added', 'Unknown')
|
||||||
|
last_seen_raw = self.data.get('last_seen', added)
|
||||||
|
bui.textwidget(parent=self._root_widget, position=(self._width / 2, v),
|
||||||
|
text=f"Friends since {added} | Last seen {_format_relative_time(last_seen_raw)}",
|
||||||
|
h_align='center', scale=0.55, color=(0.6, 0.6, 0.6))
|
||||||
|
v -= 26
|
||||||
|
|
||||||
|
if not self.data.get('stable_id', True):
|
||||||
|
bui.textwidget(parent=self._root_widget, position=(self._width / 2, v),
|
||||||
|
text="⚠️ Not signed into a V2 account - ID may change later",
|
||||||
|
h_align='center', scale=0.55, color=(1.0, 0.7, 0.2))
|
||||||
|
v -= 26
|
||||||
|
|
||||||
|
bui.textwidget(parent=self._root_widget, position=(self._width / 2, v),
|
||||||
|
text="Known Profiles:", h_align='center', color=(0.4, 0.8, 1.0))
|
||||||
|
|
||||||
|
v -= 20
|
||||||
|
self._scroll = bui.scrollwidget(parent=self._root_widget, position=(40, 90),
|
||||||
|
size=(self._width - 80, v - 100))
|
||||||
|
self._column = bui.columnwidget(parent=self._scroll)
|
||||||
|
|
||||||
|
profiles = self.data.get('profiles') or []
|
||||||
|
if profiles:
|
||||||
|
for prof in profiles:
|
||||||
|
bui.textwidget(parent=self._column, text=f"• {prof}", h_align='left',
|
||||||
|
scale=0.8, size=(self._width - 100, 25))
|
||||||
|
else:
|
||||||
|
bui.textwidget(parent=self._column, text="No known profile names yet.",
|
||||||
|
h_align='left', scale=0.7, color=(0.6, 0.6, 0.6),
|
||||||
|
size=(self._width - 100, 25))
|
||||||
|
|
||||||
|
self._remove_btn = bui.buttonwidget(
|
||||||
|
parent=self._root_widget, position=(self._width / 2 - 100, 20), size=(200, 50),
|
||||||
|
label="🗑️ Remove Friend", color=(0.8, 0.2, 0.2), textcolor=(1, 1, 1),
|
||||||
|
on_activate_call=self._confirm_remove)
|
||||||
|
self._confirm_armed = False
|
||||||
|
self._confirm_timer = None
|
||||||
|
|
||||||
|
def _confirm_remove(self):
|
||||||
|
# Two-step confirm: first tap arms it, second tap (within 3s) removes.
|
||||||
|
if not self._confirm_armed:
|
||||||
|
self._confirm_armed = True
|
||||||
|
if self._remove_btn and self._remove_btn.exists():
|
||||||
|
bui.buttonwidget(edit=self._remove_btn, label="⚠️ Tap again to confirm")
|
||||||
|
self._confirm_timer = babase.AppTimer(3.0, babase.CallStrict(self._disarm))
|
||||||
|
return
|
||||||
|
self._disarm()
|
||||||
|
self._remove()
|
||||||
|
|
||||||
|
def _disarm(self):
|
||||||
|
self._confirm_armed = False
|
||||||
|
if self._remove_btn and self._remove_btn.exists():
|
||||||
|
bui.buttonwidget(edit=self._remove_btn, label="🗑️ Remove Friend")
|
||||||
|
|
||||||
|
def _remove(self):
|
||||||
|
name = self.data.get('v2_name', 'Friend')
|
||||||
|
FriendManager.remove(self.pb_id)
|
||||||
|
bui.screenmessage(f"🗑️ {name} removed from friends.", color=(1, 0.4, 0.4))
|
||||||
|
self._back()
|
||||||
|
|
||||||
|
def _back(self):
|
||||||
|
if self._root_widget and self._root_widget.exists():
|
||||||
|
bui.containerwidget(edit=self._root_widget, transition='out_scale')
|
||||||
|
|
||||||
|
|
||||||
|
class FriendListWindow(bui.Window):
|
||||||
|
def __init__(self, origin_widget=None):
|
||||||
|
self._width = 650
|
||||||
|
self._height = 450
|
||||||
|
uiscale = bui.app.ui_v1.uiscale
|
||||||
|
|
||||||
|
offset = origin_widget.get_screen_space_center() if origin_widget else (0, 0)
|
||||||
|
|
||||||
|
super().__init__(root_widget=bui.containerwidget(
|
||||||
|
size=(self._width, self._height),
|
||||||
|
transition='in_scale',
|
||||||
|
scale=1.4 if uiscale is babase.UIScale.SMALL else 1.0,
|
||||||
|
scale_origin_stack_offset=offset
|
||||||
|
))
|
||||||
|
|
||||||
|
self._back_btn = bui.buttonwidget(
|
||||||
|
parent=self._root_widget,
|
||||||
|
position=(40, self._height - 65),
|
||||||
|
size=(60, 60),
|
||||||
|
label=bui.charstr(bui.SpecialChar.BACK),
|
||||||
|
button_type='backSmall',
|
||||||
|
on_activate_call=self._back
|
||||||
|
)
|
||||||
|
bui.containerwidget(edit=self._root_widget, cancel_button=self._back_btn)
|
||||||
|
|
||||||
|
self._title_text = bui.textwidget(
|
||||||
|
parent=self._root_widget, position=(self._width * 0.5, self._height - 40),
|
||||||
|
size=(0, 0), text="📋 Friends List", h_align='center', v_align='center',
|
||||||
|
scale=1.3, color=(0.4, 1.0, 0.4))
|
||||||
|
|
||||||
|
self._scroll = bui.scrollwidget(parent=self._root_widget, position=(40, 40),
|
||||||
|
size=(self._width - 80, self._height - 110))
|
||||||
|
self._column = bui.columnwidget(parent=self._scroll)
|
||||||
|
self._refresh()
|
||||||
|
|
||||||
|
def _refresh(self):
|
||||||
|
for child in self._column.get_children():
|
||||||
|
child.delete()
|
||||||
|
|
||||||
|
if self._title_text and self._title_text.exists():
|
||||||
|
bui.textwidget(edit=self._title_text,
|
||||||
|
text=f"📋 Friends List ({len(FriendManager.friends)})")
|
||||||
|
|
||||||
|
if not FriendManager.friends:
|
||||||
|
bui.textwidget(parent=self._column,
|
||||||
|
text="No friends added yet.\n"
|
||||||
|
"Click a player's name in a party to add one!",
|
||||||
|
h_align='left', scale=0.8, color=(0.6, 0.6, 0.6),
|
||||||
|
size=(self._width - 140, 60))
|
||||||
|
return
|
||||||
|
|
||||||
|
online_ids = {_resolve_player(entry)[0] for entry in _safe_roster()}
|
||||||
|
|
||||||
|
sorted_friends = sorted(
|
||||||
|
FriendManager.friends.items(),
|
||||||
|
key=lambda kv: kv[1].get('v2_name', '').lower()
|
||||||
|
)
|
||||||
|
|
||||||
|
for pb_id, data in sorted_friends:
|
||||||
|
is_online = pb_id in online_ids
|
||||||
|
row = bui.containerwidget(
|
||||||
|
parent=self._column, size=(self._width - 100, 60), background=True,
|
||||||
|
color=(0.25, 0.4, 0.25) if is_online else (0.25, 0.25, 0.25))
|
||||||
|
bui.textwidget(parent=row, position=(15, 32), size=(0, 0),
|
||||||
|
text=data.get('v2_name', 'Unknown'), scale=0.9,
|
||||||
|
h_align='left', v_align='center', color=(1, 1, 1))
|
||||||
|
status = "🟢 Online now" if is_online else \
|
||||||
|
f"Last seen: {_format_relative_time(data.get('last_seen'))}"
|
||||||
|
bui.textwidget(parent=row, position=(15, 12), size=(0, 0), text=status,
|
||||||
|
scale=0.5, h_align='left', v_align='center',
|
||||||
|
color=(0.4, 1.0, 0.4) if is_online else (0.6, 0.6, 0.6))
|
||||||
|
|
||||||
|
bui.buttonwidget(parent=row, position=(self._width - 240, 10), size=(120, 40),
|
||||||
|
label="👤 Profile", color=(0.2, 0.6, 0.8), textcolor=(1, 1, 1),
|
||||||
|
on_activate_call=babase.CallPartial(FriendProfileWindow, pb_id))
|
||||||
|
|
||||||
|
def _back(self):
|
||||||
|
if self._root_widget and self._root_widget.exists():
|
||||||
|
bui.containerwidget(edit=self._root_widget, transition='out_scale')
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Background polling: keeps friend data fresh + notifies when a friend joins
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_previously_online_friend_ids: set[str] = set()
|
||||||
|
_auto_update_first_run = True
|
||||||
|
|
||||||
|
|
||||||
|
def auto_update_friends():
|
||||||
|
global _previously_online_friend_ids, _auto_update_first_run
|
||||||
|
|
||||||
|
roster = _safe_roster()
|
||||||
|
if not roster:
|
||||||
|
return
|
||||||
|
|
||||||
|
now = str(datetime.datetime.now().strftime("%Y-%m-%d %H:%M"))
|
||||||
|
current_online_friend_ids = set()
|
||||||
|
updated = False
|
||||||
|
|
||||||
|
for entry in roster:
|
||||||
|
try:
|
||||||
|
pb_id, v2_name, profiles, is_stable = _resolve_player(entry)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if pb_id not in FriendManager.friends:
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_online_friend_ids.add(pb_id)
|
||||||
|
friend = FriendManager.friends[pb_id]
|
||||||
|
|
||||||
|
if v2_name and v2_name != friend.get('v2_name'):
|
||||||
|
friend['v2_name'] = v2_name
|
||||||
|
updated = True
|
||||||
|
|
||||||
|
existing = friend.setdefault('profiles', [])
|
||||||
|
for pr in profiles:
|
||||||
|
if pr not in existing:
|
||||||
|
existing.append(pr)
|
||||||
|
updated = True
|
||||||
|
|
||||||
|
friend['last_seen'] = now
|
||||||
|
friend['stable_id'] = is_stable
|
||||||
|
updated = True
|
||||||
|
|
||||||
|
# Notify only for friends who *newly* appeared since the last check
|
||||||
|
# (and never on the very first scan after launch, to avoid spamming
|
||||||
|
# "joined" messages for people already in the party when you start).
|
||||||
|
if not _auto_update_first_run and pb_id not in _previously_online_friend_ids:
|
||||||
|
bui.screenmessage(f"👋 Your friend {v2_name} just joined the party!",
|
||||||
|
color=(0.3, 0.9, 1.0))
|
||||||
|
try:
|
||||||
|
bui.getsound('shieldUp').play()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
_previously_online_friend_ids = current_online_friend_ids
|
||||||
|
_auto_update_first_run = False
|
||||||
|
|
||||||
|
if updated:
|
||||||
|
FriendManager.save()
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Monkey patches
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def apply_friend_manager_patches():
|
||||||
|
# 1. Inject a "Friends" button into the settings window.
|
||||||
|
_old_allsettings_init = bauiv1lib.settings.allsettings.AllSettingsWindow.__init__
|
||||||
|
|
||||||
|
def _new_allsettings_init(self, transition='in_right', origin_widget=None):
|
||||||
|
_old_allsettings_init(self, transition, origin_widget)
|
||||||
|
|
||||||
|
def inject_btn():
|
||||||
|
if self._root_widget and self._root_widget.exists():
|
||||||
|
try:
|
||||||
|
w = getattr(self, '_width', 800)
|
||||||
|
bui.buttonwidget(
|
||||||
|
parent=self._root_widget,
|
||||||
|
position=(w - 180, 40),
|
||||||
|
size=(140, 50),
|
||||||
|
label="👥 Friends",
|
||||||
|
color=(0.2, 0.6, 0.2),
|
||||||
|
textcolor=(1, 1, 1),
|
||||||
|
scale=0.9,
|
||||||
|
on_activate_call=babase.CallPartial(
|
||||||
|
lambda widget: FriendListWindow(origin_widget=widget),
|
||||||
|
self._root_widget)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[FriendManager] Couldn't inject Friends button: {e}")
|
||||||
|
|
||||||
|
babase.apptimer(0.05, inject_btn)
|
||||||
|
|
||||||
|
bauiv1lib.settings.allsettings.AllSettingsWindow.__init__ = _new_allsettings_init
|
||||||
|
|
||||||
|
# 2. Remember which client_id was clicked in the party window, so the
|
||||||
|
# popup-menu patch below knows which player it's dealing with.
|
||||||
|
_old_on_party_member_press = bauiv1lib.party.PartyWindow._on_party_member_press
|
||||||
|
|
||||||
|
def _pre_party_member_press(self, client_id: int, is_host: bool, widget: bui.Widget):
|
||||||
|
self._friendmgr_target_id = client_id
|
||||||
|
_old_on_party_member_press(self, client_id, is_host, widget)
|
||||||
|
|
||||||
|
bauiv1lib.party.PartyWindow._on_party_member_press = _pre_party_member_press
|
||||||
|
|
||||||
|
# 3. Patch the popup menu that appears for a clicked player, adding our
|
||||||
|
# "Add Friend" / "View Profile" entry.
|
||||||
|
_old_popup_init = bauiv1lib.popup.PopupMenuWindow.__init__
|
||||||
|
|
||||||
|
def _new_popup_init(self, *args, **kwargs):
|
||||||
|
choices = kwargs.get('choices')
|
||||||
|
choices_from_kwargs = 'choices' in kwargs
|
||||||
|
if choices is None and len(args) > 1:
|
||||||
|
choices = args[1]
|
||||||
|
|
||||||
|
choices_display = kwargs.get('choices_display')
|
||||||
|
choices_display_from_kwargs = 'choices_display' in kwargs
|
||||||
|
if choices_display is None and len(args) > 8:
|
||||||
|
choices_display = args[8]
|
||||||
|
|
||||||
|
delegate = kwargs.get('delegate')
|
||||||
|
if delegate is None and len(args) > 3:
|
||||||
|
delegate = args[3]
|
||||||
|
|
||||||
|
# The presence of this attribute (set by _pre_party_member_press just
|
||||||
|
# above) is itself the signal that this popup belongs to a player row.
|
||||||
|
client_id = getattr(delegate, '_friendmgr_target_id', None)
|
||||||
|
if delegate is not None and hasattr(delegate, '_friendmgr_target_id'):
|
||||||
|
delattr(delegate, '_friendmgr_target_id')
|
||||||
|
|
||||||
|
if client_id is not None and delegate is not None and choices is not None:
|
||||||
|
pb_id = None
|
||||||
|
v2_name = "Unknown"
|
||||||
|
profiles: list = []
|
||||||
|
is_stable = False
|
||||||
|
|
||||||
|
for entry in _safe_roster():
|
||||||
|
if entry.get('client_id') == client_id:
|
||||||
|
pb_id, v2_name, profiles, is_stable = _resolve_player(entry)
|
||||||
|
break
|
||||||
|
|
||||||
|
if pb_id:
|
||||||
|
choices = list(choices)
|
||||||
|
if choices_display is None:
|
||||||
|
choices_display = [babase.Lstr(value=c) for c in choices]
|
||||||
|
else:
|
||||||
|
choices_display = list(choices_display)
|
||||||
|
|
||||||
|
if pb_id in FriendManager.friends:
|
||||||
|
choices.append('friendmgr_view_profile')
|
||||||
|
choices_display.append(babase.Lstr(value='👤 View Profile'))
|
||||||
|
else:
|
||||||
|
choices.append('friendmgr_add_friend')
|
||||||
|
choices_display.append(babase.Lstr(value='➕ Add Friend'))
|
||||||
|
|
||||||
|
if choices_from_kwargs:
|
||||||
|
kwargs['choices'] = choices
|
||||||
|
elif len(args) > 1:
|
||||||
|
args = list(args)
|
||||||
|
args[1] = choices
|
||||||
|
args = tuple(args)
|
||||||
|
else:
|
||||||
|
kwargs['choices'] = choices
|
||||||
|
|
||||||
|
if choices_display_from_kwargs:
|
||||||
|
kwargs['choices_display'] = choices_display
|
||||||
|
elif len(args) > 8:
|
||||||
|
args = list(args)
|
||||||
|
args[8] = choices_display
|
||||||
|
args = tuple(args)
|
||||||
|
else:
|
||||||
|
kwargs['choices_display'] = choices_display
|
||||||
|
|
||||||
|
self._friendmgr_target_id = client_id
|
||||||
|
|
||||||
|
cls = delegate.__class__
|
||||||
|
if not getattr(cls, '_friendmgr_patched', False):
|
||||||
|
orig_choice = cls.popup_menu_selected_choice
|
||||||
|
|
||||||
|
def _new_choice(self_del, popup_win, choice):
|
||||||
|
target_cid = getattr(popup_win, '_friendmgr_target_id', None)
|
||||||
|
|
||||||
|
if choice in ('friendmgr_add_friend', 'friendmgr_view_profile') \
|
||||||
|
and target_cid is not None:
|
||||||
|
for entry in _safe_roster():
|
||||||
|
if entry.get('client_id') == target_cid:
|
||||||
|
p_pb_id, p_v2, p_profiles, p_stable = _resolve_player(entry)
|
||||||
|
if choice == 'friendmgr_add_friend':
|
||||||
|
FriendManager.add(p_pb_id, p_v2, p_profiles, p_stable)
|
||||||
|
bui.screenmessage(
|
||||||
|
f"✅ {p_v2} added to your friends!",
|
||||||
|
color=(0, 1, 0))
|
||||||
|
try:
|
||||||
|
bui.getsound('shieldUp').play()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
FriendProfileWindow(p_pb_id)
|
||||||
|
return
|
||||||
|
return
|
||||||
|
|
||||||
|
orig_choice(self_del, popup_win, choice)
|
||||||
|
|
||||||
|
cls.popup_menu_selected_choice = _new_choice
|
||||||
|
cls._friendmgr_patched = True
|
||||||
|
|
||||||
|
_old_popup_init(self, *args, **kwargs)
|
||||||
|
|
||||||
|
bauiv1lib.popup.PopupMenuWindow.__init__ = _new_popup_init
|
||||||
|
|
||||||
|
|
||||||
|
# ba_meta export babase.Plugin
|
||||||
|
class FriendManagerPlugin(babase.Plugin):
|
||||||
|
def on_app_running(self):
|
||||||
|
FriendManager.load()
|
||||||
|
babase.apptimer(1.0, babase.CallStrict(self.delayed_startup))
|
||||||
|
|
||||||
|
def delayed_startup(self):
|
||||||
|
try:
|
||||||
|
apply_friend_manager_patches()
|
||||||
|
bui.screenmessage("🤝 Friend Manager activated!", color=(0.2, 1.0, 0.2))
|
||||||
|
try:
|
||||||
|
bui.getsound('shieldUp').play()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[FriendManager] Setup error: {e}")
|
||||||
|
|
||||||
|
self._timer = babase.AppTimer(5.0, babase.CallStrict(auto_update_friends), repeat=True)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue