FiveM Natives + Docs Nachschlagewerk
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>
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
#!/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:]))
|
||||
Reference in New Issue
Block a user