Files
D4rkst3randClaude Opus 5 a33dba46a4 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>
2026-09-04 02:14:56 +02:00

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:]))