#!/usr/bin/env python3 """ fivem.py - Nachschlagewerk fuer FiveM Natives + docs.fivem.net. Liest data/natives.db (SQLite + FTS5). Nur Python-Stdlib, keine Dependencies. python fivem.py ragdoll # Freitextsuche (Natives + Docs) python fivem.py show SetPedToRagdoll # volles Detail zu einem Native python fivem.py show 0xAE99FB955581844A # ... auch per Hash python fivem.py search vehicle engine --apiset client --limit 20 python fivem.py ns VEHICLE # alle Natives eines Namespace python fivem.py ns # alle Namespaces python fivem.py docs fxmanifest # Docs-Volltextsuche python fivem.py doc scripting-reference/resource-manifest/resource-manifest python fivem.py stats """ import argparse import json import os import re import sqlite3 import sys DB = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "natives.db") # Docs/Natives enthalten UTF-8; Windows-Konsolen sind oft cp1252. for _stream in (sys.stdout, sys.stderr): if hasattr(_stream, "reconfigure"): try: _stream.reconfigure(encoding="utf-8", errors="replace") except (ValueError, OSError): pass SUBCOMMANDS = {"show", "search", "ns", "docs", "doc", "stats", "lookup"} # ------------------------------------------------------------------ db def connect(): if not os.path.exists(DB): sys.exit("natives.db fehlt - erst 'python build.py' ausfuehren (%s)" % DB) con = sqlite3.connect("file:%s?mode=ro" % DB.replace("\\", "/"), uri=True) con.row_factory = sqlite3.Row return con def fts_query(text): """Freitext -> sichere FTS5-Query mit Prefix-Matching.""" tokens = [t for t in re.split(r"[^0-9A-Za-z_]+", text) if t] if not tokens: return None return " AND ".join('"%s"*' % t.replace('"', "") for t in tokens) # ------------------------------------------------------------------ render def render_native(r, examples=True): out = [] title = r["lua_name"] or r["hash"] out.append("## %s" % title) tags = [r["ns"], r["apiset"] or "?"] if r["game"]: tags.append(r["game"]) if r["source"] == "cfx": tags.append("cfx") out.append("`%s` %s" % (r["hash"], " | ".join("**%s**" % t for t in tags))) if r["name"] and r["name"] != r["lua_name"]: out.append("Native-Name: `%s`" % r["name"]) if r["jhash"]: out.append("jhash: `%s`" % r["jhash"]) if r["aliases"]: out.append("Aliases: %s" % ", ".join("`%s`" % a for a in r["aliases"].split())) out.append("") out.append("```") out.append(r["signature"]) out.append("```") params = json.loads(r["params_json"] or "[]") if params: out.append("") out.append("### Parameter") for p in params: desc = (p.get("description") or "").replace("\n", " ").strip() out.append("- `%s` (%s)%s" % (p.get("name", "?"), p.get("type", "?"), " - " + desc if desc else "")) if r["results"] and r["results"] != "void": rd = r["results_description"] or "" out.append("") out.append("### Rueckgabe") out.append("`%s`%s" % (r["results"], " - " + rd.replace("\n", " ") if rd else "")) if r["description"]: out.append("") out.append("### Beschreibung") out.append(r["description"]) if examples: ex = json.loads(r["examples_json"] or "[]") if ex: out.append("") out.append("### Beispiele") for e in ex: out.append("```%s" % (e.get("lang") or "")) out.append((e.get("code") or "").replace("\r\n", "\n").rstrip()) out.append("```") out.append("") out.append("Docs: %s" % r["url"]) return "\n".join(out) def summarize(description): """Erste sinnvolle Zeile - Code-Fences und Leerzeilen ueberspringen.""" in_fence = False for line in (description or "").splitlines(): line = line.strip() if line.startswith("```"): in_fence = not in_fence continue if in_fence or not line: continue return line[:140] return "(keine Beschreibung)" def render_native_line(r): flags = r["apiset"] or "?" if r["source"] == "cfx": flags += ",cfx" return "- **%s** `%s` [%s/%s] - %s" % ( r["lua_name"], r["hash"], r["ns"], flags, summarize(r["description"]), ) def render_doc_line(r): return "- **%s** `%s`\n %s" % (r["title"], r["slug"], r["url"]) # ------------------------------------------------------------------ cmds def cmd_show(con, args): key = args.query.strip() rows = con.execute( """SELECT * FROM natives WHERE lua_name = ?1 COLLATE NOCASE OR name = ?1 COLLATE NOCASE OR hash = ?1 COLLATE NOCASE OR jhash = ?1 COLLATE NOCASE ORDER BY (source='cfx') DESC""", (key,), ).fetchall() if not rows: # Fallback: 0x-Praefix vergessen / Teiltreffer rows = con.execute( "SELECT * FROM natives WHERE lua_name LIKE ?1 OR name LIKE ?1 LIMIT 25", ("%" + key + "%",), ).fetchall() if len(rows) != 1: if not rows: print("Kein Native '%s' gefunden. Versuch: fivem.py search %s" % (key, key)) return 1 print("Mehrdeutig - %d Treffer:\n" % len(rows)) print("\n".join(render_native_line(r) for r in rows)) return 0 if args.json: print(json.dumps([dict(r) for r in rows], ensure_ascii=False, indent=2)) return 0 print("\n\n---\n\n".join(render_native(r, examples=not args.no_examples) for r in rows)) return 0 def cmd_search(con, args): q = fts_query(" ".join(args.query)) if not q: return 1 sql = ["SELECT n.* FROM natives_fts f JOIN natives n ON n.id = f.rowid", "WHERE natives_fts MATCH ?"] params = [q] if args.apiset: sql.append("AND n.apiset = ?") params.append(args.apiset) if args.ns: sql.append("AND n.ns = ?") params.append(args.ns.upper()) sql.append("ORDER BY rank LIMIT ?") params.append(args.limit) rows = con.execute(" ".join(sql), params).fetchall() if args.json: print(json.dumps([dict(r) for r in rows], ensure_ascii=False, indent=2)) return 0 if not rows: print("Keine Natives gefunden fuer: %s" % " ".join(args.query)) return 0 print("# Natives (%d)\n" % len(rows)) print("\n".join(render_native_line(r) for r in rows)) print("\nDetails: python fivem.py show ") return 0 def cmd_ns(con, args): if not args.name: rows = con.execute( "SELECT ns, COUNT(*) c, SUM(apiset='server') srv FROM natives GROUP BY ns ORDER BY ns" ).fetchall() print("# Namespaces (%d)\n" % len(rows)) for r in rows: extra = " (%d server)" % r["srv"] if r["srv"] else "" print("- **%s** - %d Natives%s" % (r["ns"], r["c"], extra)) return 0 rows = con.execute( "SELECT * FROM natives WHERE ns = ? ORDER BY lua_name LIMIT ?", (args.name.upper(), args.limit), ).fetchall() if not rows: print("Namespace '%s' unbekannt. Liste: python fivem.py ns" % args.name) return 1 print("# %s (%d gezeigt)\n" % (args.name.upper(), len(rows))) print("\n".join(render_native_line(r) for r in rows)) return 0 def cmd_docs(con, args): q = fts_query(" ".join(args.query)) if not q: return 1 sql = ["SELECT d.*, snippet(docs_fts, 3, '**', '**', ' ... ', 24) AS snip", "FROM docs_fts f JOIN docs d ON d.id = f.rowid WHERE docs_fts MATCH ?"] params = [q] if args.section: sql.append("AND d.section = ?") params.append(args.section) sql.append("ORDER BY rank LIMIT ?") params.append(args.limit) rows = con.execute(" ".join(sql), params).fetchall() if not rows: print("Keine Docs gefunden fuer: %s" % " ".join(args.query)) return 0 print("# Docs (%d)\n" % len(rows)) for r in rows: print("- **%s** `%s`" % (r["title"], r["slug"])) print(" %s" % r["snip"].replace("\n", " ")) print(" %s" % r["url"]) print("\nVolltext: python fivem.py doc ") return 0 def cmd_doc(con, args): slug = args.slug.strip().strip("/") r = con.execute("SELECT * FROM docs WHERE slug = ? OR path = ?", (slug, slug)).fetchone() if not r: rows = con.execute("SELECT * FROM docs WHERE slug LIKE ? LIMIT 15", ("%" + slug + "%",)).fetchall() if len(rows) == 1: r = rows[0] else: print("Kein eindeutiges Dokument '%s'." % slug) if rows: print("\n" + "\n".join(render_doc_line(x) for x in rows)) return 1 print("# %s\n" % r["title"]) print("Quelle: %s\n" % r["url"]) print(r["raw"] if args.raw else r["body"]) return 0 def cmd_stats(con, args): meta = dict(con.execute("SELECT key, value FROM meta").fetchall()) print("# fivem-natives-db\n") print("- Gebaut: %s" % meta.get("built_at")) print("- Natives: %s" % meta.get("native_count")) print("- Doc-Seiten: %s" % meta.get("doc_count")) print("\n## Natives nach apiset") for r in con.execute("SELECT apiset, COUNT(*) c FROM natives GROUP BY apiset ORDER BY c DESC"): print("- %s: %d" % (r["apiset"] or "?", r["c"])) print("\n## Doc-Sektionen") for r in con.execute("SELECT section, COUNT(*) c FROM docs GROUP BY section ORDER BY section"): print("- %s: %d" % (r["section"], r["c"])) return 0 def cmd_lookup(con, args): """Kombinierte Suche: exakter Native, sonst Natives + Docs.""" key = " ".join(args.query) exact = con.execute( """SELECT * FROM natives WHERE lua_name = ?1 COLLATE NOCASE OR name = ?1 COLLATE NOCASE OR hash = ?1 COLLATE NOCASE""", (key.strip(),), ).fetchall() if exact: print("\n\n---\n\n".join(render_native(r) for r in exact)) return 0 q = fts_query(key) if not q: return 1 nat = con.execute( "SELECT n.* FROM natives_fts f JOIN natives n ON n.id=f.rowid " "WHERE natives_fts MATCH ? ORDER BY rank LIMIT ?", (q, args.limit) ).fetchall() doc = con.execute( "SELECT d.* FROM docs_fts f JOIN docs d ON d.id=f.rowid " "WHERE docs_fts MATCH ? ORDER BY rank LIMIT ?", (q, max(3, args.limit // 2)) ).fetchall() if nat: print("# Natives (%d)\n" % len(nat)) print("\n".join(render_native_line(r) for r in nat)) if doc: print("\n# Docs (%d)\n" % len(doc)) print("\n".join(render_doc_line(r) for r in doc)) if not nat and not doc: print("Nichts gefunden fuer: %s" % key) else: print("\nDetails: python fivem.py show | python fivem.py doc ") return 0 # ------------------------------------------------------------------ main def main(argv): p = argparse.ArgumentParser(prog="fivem.py", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) sub = p.add_subparsers(dest="cmd") s = sub.add_parser("show", help="Volles Detail zu einem Native (Name oder Hash)") s.add_argument("query") s.add_argument("--json", action="store_true") s.add_argument("--no-examples", action="store_true") s.set_defaults(fn=cmd_show) s = sub.add_parser("search", help="Natives durchsuchen") s.add_argument("query", nargs="+") s.add_argument("--apiset", choices=["client", "server", "shared"]) s.add_argument("--ns") s.add_argument("--limit", type=int, default=25) s.add_argument("--json", action="store_true") s.set_defaults(fn=cmd_search) s = sub.add_parser("ns", help="Namespaces auflisten / Natives eines Namespace") s.add_argument("name", nargs="?") s.add_argument("--limit", type=int, default=400) s.set_defaults(fn=cmd_ns) s = sub.add_parser("docs", help="docs.fivem.net durchsuchen") s.add_argument("query", nargs="+") s.add_argument("--section") s.add_argument("--limit", type=int, default=10) s.set_defaults(fn=cmd_docs) s = sub.add_parser("doc", help="Doc-Seite komplett ausgeben") s.add_argument("slug") s.add_argument("--raw", action="store_true", help="Hugo-Shortcodes beibehalten") s.set_defaults(fn=cmd_doc) s = sub.add_parser("stats", help="Umfang der Datenbank") s.set_defaults(fn=cmd_stats) s = sub.add_parser("lookup", help="Kombinierte Suche (Default)") s.add_argument("query", nargs="+") s.add_argument("--limit", type=int, default=15) s.set_defaults(fn=cmd_lookup) # Ohne Subcommand: alles als lookup behandeln if argv and argv[0] not in SUBCOMMANDS and not argv[0].startswith("-"): argv = ["lookup"] + argv args = p.parse_args(argv) if not getattr(args, "fn", None): p.print_help() return 1 con = connect() try: return args.fn(con, args) finally: con.close() if __name__ == "__main__": sys.exit(main(sys.argv[1:]))