#!/usr/bin/env python3 """获取Minecraft玩家活动信息(JSON输出)""" import sys, os, re, json from datetime import datetime sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from mc_config import MC_SERVER_DIR, PLAYTIME_DB def get_recent_events(limit=5): log_path = os.path.join(MC_SERVER_DIR, "logs", "latest.log") if not os.path.exists(log_path): return [] events = [] patterns = [ (re.compile(r'(\w+)\s+joined\s+the\s+game'), 'join'), (re.compile(r'(\w+)\s+left\s+the\s+game'), 'leave'), ] skip = {"Server", "RCON", "Thread", "Generic", "World"} try: with open(log_path, "r", errors="ignore") as f: lines = f.readlines() for line in lines[-5000:]: for pattern, etype in patterns: m = pattern.search(line) if m: name = m.group(1) if name in skip: continue events.append({"type": etype, "player": name}) break seen = set() unique = [] for e in reversed(events): key = (e["player"], e["type"]) if key not in seen: seen.add(key) unique.append(e) if len(unique) >= limit: break return list(reversed(unique)) except Exception: return [] def get_playtime_db(): if os.path.exists(PLAYTIME_DB): try: with open(PLAYTIME_DB, "r") as f: return json.load(f) except Exception: pass return {"players": {}, "last_log_pos": 0} def save_playtime_db(db): try: with open(PLAYTIME_DB, "w") as f: json.dump(db, f, indent=2, ensure_ascii=False) except Exception: pass def update_playtime(): db = get_playtime_db() log_path = os.path.join(MC_SERVER_DIR, "logs", "latest.log") if not os.path.exists(log_path): return db try: with open(log_path, "rb") as f: f.seek(0, 2) size = f.tell() start = min(db.get("last_log_pos", 0), size) if start > size: start = 0 with open(log_path, "r", errors="ignore") as f: f.seek(start) lines = f.readlines() now = datetime.now() join_p = re.compile(r'joined the game') leave_p = re.compile(r'left the game') name_p = re.compile(r'\]:\s+(\w+)\s+(?:joined|left)') for line in lines: m = name_p.search(line) if not m: continue player = m.group(1) if join_p.search(line): if player not in db["players"]: db["players"][player] = {"total_seconds": 0, "last_join": None} db["players"][player]["last_join"] = now.isoformat() elif leave_p.search(line): if player in db["players"] and db["players"][player].get("last_join"): join_time = datetime.fromisoformat(db["players"][player]["last_join"]) sec = (now - join_time).total_seconds() if 0 < sec < 86400: db["players"][player]["total_seconds"] += sec db["players"][player]["last_join"] = None db["last_log_pos"] = size save_playtime_db(db) except Exception: pass return db def format_time(seconds): h = seconds // 3600 m = (seconds % 3600) // 60 if h >= 24: d = h // 24 h = h % 24 return f"{d}天{h}小时{m}分" elif h > 0: return f"{h}小时{m}分" return f"{m}分钟" def get_playtime_leaderboard(top_n=10): db = update_playtime() players = db.get("players", {}) lb = [] for name, data in players.items(): total = data.get("total_seconds", 0) if data.get("last_join"): try: jt = datetime.fromisoformat(data["last_join"]) s = (datetime.now() - jt).total_seconds() if 0 < s < 86400: total += s except Exception: pass if total > 0: lb.append({"name": name, "seconds": int(total), "formatted": format_time(int(total))}) lb.sort(key=lambda x: -x["seconds"]) return lb[:top_n] def _load_uuid_to_name(): """从 usercache.json 加载 UUID->名字映射,回退 ops.json/whitelist.json""" mapping = {} # 主源:usercache.json(包含所有登录过的玩家) for src_file in ["usercache.json", "ops.json", "whitelist.json"]: path = os.path.join(MC_SERVER_DIR, src_file) try: with open(path, "r") as f: for entry in json.load(f): if 'uuid' in entry and 'name' in entry: mapping[entry['uuid']] = entry['name'] except Exception: pass return mapping def get_death_leaderboard(top_n=10): uuid_to_name = _load_uuid_to_name() stats_dir = os.path.join(MC_SERVER_DIR, "world", "stats") if not os.path.exists(stats_dir): return [] deaths = {} for fname in os.listdir(stats_dir): if not fname.endswith('.json'): continue uuid = fname.replace('.json', '') fpath = os.path.join(stats_dir, fname) try: with open(fpath, "r") as f: data = json.load(f) dc = data.get('stats', {}).get('minecraft:custom', {}).get('minecraft:deaths', 0) if dc > 0: name = uuid_to_name.get(uuid, uuid[:8]) deaths[name] = dc except Exception: continue return [{"name": n, "deaths": d} for n, d in sorted(deaths.items(), key=lambda x: -x[1])[:top_n]] def get_achievement_leaderboard(top_n=10): uuid_to_name = _load_uuid_to_name() adv_dir = os.path.join(MC_SERVER_DIR, "world", "advancements") if not os.path.exists(adv_dir): return [] ach = {} for fname in os.listdir(adv_dir): if not fname.endswith('.json'): continue uuid = fname.replace('.json', '') fpath = os.path.join(adv_dir, fname) try: with open(fpath, "r") as f: data = json.load(f) name = uuid_to_name.get(uuid) if not name: name = uuid[:8] # 名字查不到就用 UUID 前缀 done = 0 for k, v in data.items(): if not isinstance(v, dict): continue if 'recipes/' in k or ':recipes/' in k: continue if v.get('done'): done += 1 if done > 0: ach[name] = done except Exception: continue return [{"name": n, "achievements": a} for n, a in sorted(ach.items(), key=lambda x: -x[1])[:top_n]] if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument("--top", type=int, default=10) args = parser.parse_args() result = { "recent_events": get_recent_events(5), "playtime": get_playtime_leaderboard(args.top), "deaths": get_death_leaderboard(args.top), "achievements": get_achievement_leaderboard(args.top), } print(json.dumps(result, ensure_ascii=False, indent=2))