7359 Natives (GTA5 + Cfx) und 342 Doc-Seiten von docs.fivem.net in einer SQLite-DB mit FTS5-Volltextsuche, dazu ein dependency-freies Python-CLI und eine SKILL.md fuer Claude Code. - build.py zieht runtime.fivem.net/doc/natives*.json und citizenfx/fivem-docs und loest die Hugo-Shortcodes der Docs auf (code, native_link, alert, events) - lua_name folgt exakt der Regel aus FiveM ext/natives/codegen_out_lua.lua - fivem.py: show / search / ns / docs / doc / stats, optional --json - data/natives.jsonl als grep-barer Fallback Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
376 lines
13 KiB
Python
376 lines
13 KiB
Python
#!/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 <Name>")
|
|
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 <slug>")
|
|
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 <Name> | python fivem.py doc <slug>")
|
|
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:]))
|