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:
2026-09-04 02:14:56 +02:00
co-authored by Claude Opus 5
commit a33dba46a4
8 changed files with 8405 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
* text=auto eol=lf
*.db binary
*.jsonl text eol=lf
+3
View File
@@ -0,0 +1,3 @@
.cache/
__pycache__/
*.pyc
+141
View File
@@ -0,0 +1,141 @@
# fivem-natives-db
Offline-Nachschlagewerk für **FiveM Natives** und **docs.fivem.net** — als SQLite-Datenbank
mit Volltextsuche (FTS5), CLI und fertigem Claude-Code-Skill.
| | |
|---|---|
| Natives | **7.359** (6.416 GTA5 + 943 Cfx) |
| Doc-Seiten | **342** aus `citizenfx/fivem-docs` |
| Größe | ~11 MB SQLite + ~4 MB JSONL |
| Dependencies | keine — nur Python 3.8+ Stdlib |
## Installation als Claude-Code-Skill
Ins Skill-Verzeichnis klonen, dann findet Claude Code die `SKILL.md` automatisch:
```bash
git clone https://git.d4rkst3r.de/D4rkst3r/fivem-natives-db.git ~/.claude/skills/fivem-natives
```
Unter Windows (PowerShell):
```powershell
git clone https://git.d4rkst3r.de/D4rkst3r/fivem-natives-db.git "$env:USERPROFILE\.claude\skills\fivem-natives"
```
Danach greift Claude bei Fragen zu Natives, Hashes, Signaturen oder FiveM-Docs
automatisch auf die lokale DB zu, statt die Doku zu raten oder online zu fetchen.
Alternativ irgendwohin klonen und `fivem.py` direkt benutzen.
## CLI
```bash
python fivem.py ragdoll # kombinierte Suche (Natives + Docs)
python fivem.py show SetPedToRagdoll # volles Detail
python fivem.py show 0xAE99FB955581844A # ... auch per Hash oder jhash
python fivem.py search vehicle engine --apiset client --limit 20
python fivem.py ns # alle 45 Namespaces
python fivem.py ns VEHICLE # Natives eines Namespace
python fivem.py docs fxmanifest # Docs-Volltextsuche
python fivem.py doc scripting-reference/resource-manifest
python fivem.py stats
```
`show` liefert Signatur, Parameter mit Typ und Beschreibung, Rückgabewert,
Beschreibungstext und die offiziellen Codebeispiele:
```
## SetPedToRagdoll
`0xAE99FB955581844A` **PED** | **client** | **gta5**
Native-Name: `SET_PED_TO_RAGDOLL`
void SetPedToRagdoll(Ped ped, int minTime, int maxTime, int ragdollType,
BOOL bAbortIfInjured, BOOL bAbortIfDead, BOOL bForceScriptControl)
```
Mit `--json` gibt es maschinenlesbare Ausgabe.
## Schema
`data/natives.db`:
| Tabelle | Inhalt |
|---|---|
| `natives` | ein Eintrag pro Native — s.u. |
| `natives_fts` | FTS5 über `name`, `lua_name`, `ns`, `description`, `param_names`, `aliases`, `hash` |
| `docs` | `path`, `slug`, `section`, `title`, `weight`, `url`, `body` (Shortcodes aufgelöst), `raw` |
| `docs_fts` | FTS5 über `title`, `section`, `slug`, `body` |
| `meta` | `built_at`, `native_count`, `doc_count`, `sources`, `schema_version` |
Spalten in `natives`:
`hash`, `jhash`, `name` (Original, z.B. `SET_ENTITY_COORDS`), `lua_name`
(Aufrufname, z.B. `SetEntityCoords`), `ns`, `source` (`gta5`|`cfx`), `game`,
`apiset` (`client`|`server`|`shared`), `results`, `results_description`,
`description`, `signature`, `params_json`, `param_names`, `examples_json`,
`aliases`, `url`.
Direkt per SQL nutzbar:
```sql
SELECT n.lua_name, n.ns, n.signature
FROM natives_fts f JOIN natives n ON n.id = f.rowid
WHERE natives_fts MATCH 'vehicle AND engine'
AND n.apiset = 'client'
ORDER BY rank LIMIT 20;
```
`data/natives.jsonl` enthält dieselben Natives als eine JSON-Zeile pro Native —
praktisch für `grep`, wenn kein Python zur Hand ist.
## Rebuild
```bash
python build.py --refresh
```
Quellen:
- `https://runtime.fivem.net/doc/natives.json` — GTA5-Natives
- `https://runtime.fivem.net/doc/natives_cfx.json` — Cfx/FiveM-Natives
- `https://github.com/citizenfx/fivem-docs` — Markdown von docs.fivem.net
- `https://runtime.fivem.net/doc/events/{client,server}.html.json` — Event-Referenz
Ohne `--refresh` wird aus `.cache/` gebaut (nicht eingecheckt).
Beim Build werden die Hugo-Shortcodes der Docs aufgelöst, damit der Inhalt
durchsuchbar ist: `code` (bindet Beispieldateien ein), `native_link` (wird zum
Link auf docs.fivem.net), `alert` (wird zum Blockquote), `rmv`/`rmv2`, `events`
(zieht die Client-/Server-Event-Referenz), `youtube`/`video`/`forum_topic`.
## Namenskonvertierung
`lua_name` folgt exakt der Regel aus FiveM `ext/natives/codegen_out_lua.lua`:
```
name:lower():gsub('_(%a)', upper):gsub('^%l', upper)
```
Ein Unterstrich vor einer **Ziffer** bleibt deshalb erhalten:
| Native | Lua |
|---|---|
| `SET_ENTITY_COORDS` | `SetEntityCoords` |
| `_FORCE_VEHICLE_ENGINE_SYNTH` | `ForceVehicleEngineSynth` |
| `DRAW_SCALEFORM_MOVIE_3D_SOLID` | `DrawScaleformMovie_3dSolid` |
| `UI3DSCENE_IS_AVAILABLE` | `Ui3dsceneIsAvailable` |
| (unbenannt) | `N_0x<hash>` |
## Hinweis zur Repo-Größe
`data/` ist eingecheckt, damit ein Clone sofort einsatzbereit ist. Jeder Rebuild
legt ~15 MB in die Git-History. Bei häufigen Updates den Data-Commit amenden oder
die History gelegentlich squashen.
## Lizenz
Der Code hier ist frei verwendbar. Die Daten stammen von Cfx.re:
Natives-Metadaten und `citizenfx/fivem-docs` stehen unter den jeweiligen
Lizenzen der Upstream-Projekte.
+90
View File
@@ -0,0 +1,90 @@
---
name: fivem-natives
description: Nachschlagewerk für FiveM/GTA5 Natives und docs.fivem.net. Nutzen, sobald ein FiveM-Native, ein Hash (0x...), eine Native-Signatur, ein Parameter oder ein Rückgabewert geprüft werden soll, wenn unklar ist ob ein Native client-, server- oder shared-seitig ist, wenn ein Native über die Beschreibung gesucht wird ("wie setze ich einen Ped in Ragdoll"), oder wenn etwas aus den FiveM-Docs gebraucht wird (fxmanifest, Resource-Manifest-Einträge, Server-Convars, Events, Scripting-Runtimes, Server-Setup, Streaming/Assets, Game-Referenzen).
---
# FiveM Natives & Docs
Lokale SQLite-Datenbank mit **7.359 Natives** (GTA5 + Cfx) und **342 Doc-Seiten** von
docs.fivem.net. Offline, keine Dependencies außer Python 3.
**Immer diese Datenbank statt WebFetch/WebSearch verwenden** — sie ist schneller,
vollständig und enthält die Beschreibungen, Parametertypen und Beispiele aus den
offiziellen Quellen.
## Aufruf
Alle Kommandos relativ zum Skill-Verzeichnis (`$SKILL_DIR = Ordner dieser SKILL.md`):
```bash
python "$SKILL_DIR/fivem.py" <kommando> [args]
```
## Kommandos
| Kommando | Zweck |
|---|---|
| `fivem.py <freitext>` | Kombinierte Suche über Natives **und** Docs (Default) |
| `fivem.py show <Name\|Hash>` | Volles Detail: Signatur, Parameter, Rückgabe, Beschreibung, Beispiele |
| `fivem.py search <worte>` | Nur Natives, mit `--apiset client\|server\|shared`, `--ns VEHICLE`, `--limit N`, `--json` |
| `fivem.py ns` / `fivem.py ns PED` | Namespaces auflisten / alle Natives eines Namespace |
| `fivem.py docs <worte>` | Volltextsuche in docs.fivem.net (`--section scripting-reference`) |
| `fivem.py doc <slug>` | Komplette Doc-Seite ausgeben |
| `fivem.py stats` | Umfang / Build-Datum |
## Typische Abläufe
**Signatur eines bekannten Natives prüfen** — vor dem Schreiben von Native-Aufrufen:
```bash
python "$SKILL_DIR/fivem.py" show SetEntityCoords
python "$SKILL_DIR/fivem.py" show 0xAE99FB955581844A
```
Akzeptiert Lua-Name (`SetPedToRagdoll`), Original-Name (`SET_PED_TO_RAGDOLL`),
Hash und jhash — Groß-/Kleinschreibung egal.
**Native über die Funktion suchen**, wenn der Name unbekannt ist:
```bash
python "$SKILL_DIR/fivem.py" search vehicle engine health
python "$SKILL_DIR/fivem.py" search identifier --apiset server
```
**Server- vs. Client-Kontext klären** — das Feld `apiset` ist maßgeblich:
```bash
python "$SKILL_DIR/fivem.py" search player ping --apiset server
```
**Docs nachschlagen**:
```bash
python "$SKILL_DIR/fivem.py" docs fxmanifest
python "$SKILL_DIR/fivem.py" doc scripting-reference/resource-manifest
python "$SKILL_DIR/fivem.py" docs convar --section server-manual
```
## Wichtig beim Interpretieren
- **`apiset`** sagt, wo das Native läuft: `client`, `server` oder `shared`.
Alle GTA5-Natives sind `client`; Cfx-Natives können server- oder shared-seitig sein.
- **Gleicher Name, zwei Einträge**: viele Natives existieren zusätzlich als
server-seitiges RPC-Pendant (z.B. `SetPedToRagdoll` als `0xAE99FB955581844A`
client und `0x83CB5052` server). `show` gibt beide aus — den passenden auswählen.
- **`lua_name`** ist der Bezeichner, der in Lua/JS tatsächlich aufgerufen wird.
Achtung auf Sonderfälle wie `DrawScaleformMovie_3d` (Unterstrich vor Ziffern
bleibt) und `World3dToScreen2d`.
- **Unbenannte Natives** heißen `N_0x<hash>`; per `Citizen.InvokeNative(0x..., ...)`
aufrufbar.
- Parameter mit Namen wie `p4`, `p19` sind in den offiziellen Docs undokumentiert —
das im Code kenntlich machen statt zu raten.
## Fallback ohne Python
`data/natives.jsonl` enthält ein Native pro Zeile und ist direkt grep-bar:
```bash
grep -i '"lua_name": "SetPedToRagdoll"' "$SKILL_DIR/data/natives.jsonl"
```
## Daten aktualisieren
```bash
python "$SKILL_DIR/build.py" --refresh
```
Zieht `runtime.fivem.net/doc/natives*.json` und `github.com/citizenfx/fivem-docs` neu.
+434
View File
@@ -0,0 +1,434 @@
#!/usr/bin/env python3
"""
Baut data/natives.db (SQLite + FTS5) aus:
- https://runtime.fivem.net/doc/natives.json (GTA5 natives)
- https://runtime.fivem.net/doc/natives_cfx.json (Cfx/FiveM natives)
- https://github.com/citizenfx/fivem-docs (docs.fivem.net Markdown)
Nur Python-Stdlib. Aufruf: python build.py [--refresh]
"""
import json
import os
import re
import shutil
import sqlite3
import subprocess
import sys
import urllib.request
from datetime import datetime, timezone
from html.parser import HTMLParser
ROOT = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(ROOT, "data")
DB = os.path.join(DATA, "natives.db")
CACHE = os.path.join(ROOT, ".cache")
NATIVE_SOURCES = [
("gta5", "https://runtime.fivem.net/doc/natives.json"),
("cfx", "https://runtime.fivem.net/doc/natives_cfx.json"),
]
DOCS_REPO = "https://github.com/citizenfx/fivem-docs.git"
# ---------------------------------------------------------------- helpers
def log(*a):
print("[build]", *a, flush=True)
def fetch(url, dest):
os.makedirs(os.path.dirname(dest), exist_ok=True)
log("fetch", url)
req = urllib.request.Request(url, headers={"User-Agent": "fivem-natives-db/1.0"})
with urllib.request.urlopen(req, timeout=180) as r, open(dest, "wb") as f:
shutil.copyfileobj(r, f)
return dest
def pascal(name):
"""SET_ENTITY_COORDS -> SetEntityCoords.
Repliziert exakt die Regel aus FiveM ext/natives/codegen_out_lua.lua:
name:lower():gsub('_(%a)', upper):gsub('^%l', upper)
Wichtig: ein Unterstrich vor einer *Ziffer* bleibt erhalten, deshalb wird
DRAW_SCALEFORM_MOVIE_3D zu DrawScaleformMovie_3d (nicht ...Movie3D).
"""
if not name:
return ""
s = re.sub(r"_([a-z])", lambda m: m.group(1).upper(), name.lower())
return s[:1].upper() + s[1:]
def lua_name(nat):
n = nat.get("name")
if n:
return pascal(n)
# Unbenannte Natives werden von den Runtimes als N_0x<hash> exportiert
return "N_" + nat["hash"].lower()
def signature(nat, lname):
ps = ", ".join(
"%s %s" % (p.get("type", "Any"), p.get("name", "arg"))
for p in nat.get("params", [])
)
return "%s %s(%s)" % (nat.get("results") or "void", lname, ps)
# ---------------------------------------------------------------- natives
def load_natives(refresh=False):
rows = []
for source, url in NATIVE_SOURCES:
path = os.path.join(CACHE, os.path.basename(url))
if refresh or not os.path.exists(path):
fetch(url, path)
with open(path, "r", encoding="utf-8") as f:
doc = json.load(f)
for ns, natives in doc.items():
for h, nat in natives.items():
nat.setdefault("hash", h)
lname = lua_name(nat)
params = nat.get("params") or []
rows.append({
"hash": nat["hash"],
"jhash": nat.get("jhash") or "",
"name": nat.get("name") or "",
"lua_name": lname,
"ns": ns,
"source": source,
"game": nat.get("game") or ("gta5" if source == "gta5" else ""),
"apiset": nat.get("apiset") or ("client" if source == "gta5" else ""),
"results": nat.get("results") or "void",
"results_description": nat.get("resultsDescription") or "",
"description": (nat.get("description") or "").strip(),
"signature": signature(nat, lname),
"params_json": json.dumps(params, ensure_ascii=False),
"param_names": " ".join(p.get("name", "") for p in params),
"examples_json": json.dumps(nat.get("examples") or [], ensure_ascii=False),
"aliases": " ".join(nat.get("aliases") or []),
"url": "https://docs.fivem.net/natives/?_%s" % nat["hash"],
})
log("natives:", len(rows))
return rows
# ---------------------------------------------------------------- html -> text
class HtmlToText(HTMLParser):
"""Minimaler Konverter fuer die Event-Referenz (TypeDoc-HTML)."""
BLOCK = {"p", "div", "section", "li", "tr", "br", "ul", "ol", "table", "pre"}
HEAD = {"h1": "#", "h2": "##", "h3": "###", "h4": "####", "h5": "#####"}
def __init__(self):
super().__init__(convert_charrefs=True)
self.parts = []
self._skip = 0
self._pre = 0
def handle_starttag(self, tag, attrs):
if tag in ("script", "style"):
self._skip += 1
elif tag in self.HEAD:
self.parts.append("\n\n%s " % self.HEAD[tag])
elif tag == "li":
self.parts.append("\n- ")
elif tag in ("pre", "code"):
self._pre += 1
self.parts.append("`" if tag == "code" else "\n```\n")
elif tag in self.BLOCK:
self.parts.append("\n")
def handle_endtag(self, tag):
if tag in ("script", "style"):
self._skip = max(0, self._skip - 1)
elif tag in ("pre", "code"):
self._pre = max(0, self._pre - 1)
self.parts.append("`" if tag == "code" else "\n```\n")
elif tag in self.HEAD or tag in self.BLOCK:
self.parts.append("\n")
def handle_data(self, data):
if not self._skip:
self.parts.append(data if self._pre else re.sub(r"\s+", " ", data))
def text(self):
out = "".join(self.parts)
out = re.sub(r"``\s*``", "", out)
out = re.sub(r"\n{3,}", "\n\n", out)
return "\n".join(l.rstrip() for l in out.splitlines()).strip()
def html_to_text(html):
p = HtmlToText()
p.feed(html)
p.close()
return p.text()
# ---------------------------------------------------------------- shortcodes
FRONTMATTER = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.S)
PAIRED_SC = re.compile(
r"\{\{[%<]\s*(alert|steps)\b(.*?)[%>]\}\}(.*?)\{\{[%<]\s*/\s*\1\s*[%>]\}\}", re.S)
SINGLE_SC = re.compile(r"\{\{[%<]\s*([A-Za-z0-9_-]+)((?:[^%>]|%(?!\}\})|>(?!\}))*?)\s*[%>]\}\}", re.S)
ATTR = re.compile(r'([A-Za-z0-9_-]+)\s*=\s*"([^"]*)"')
POSITIONAL = re.compile(r'"([^"]*)"')
def parse_attrs(s):
named = dict(ATTR.findall(s))
pos = [v for v in POSITIONAL.findall(ATTR.sub("", s))]
return named, pos
class Shortcodes:
"""Loest die Hugo-Shortcodes der fivem-docs zu reinem Markdown auf."""
def __init__(self, repo_root, native_hash_by_name):
self.root = repo_root
self.hashes = native_hash_by_name
self._events = {}
self._rmv = self._read_static("/static/resource_manifest_version.txt")
self._rmv2 = self._read_static("/static/resource_manifest_version2.txt")
def _read_static(self, rel):
path = os.path.join(self.root, rel.lstrip("/").replace("/", os.sep))
try:
with open(path, "r", encoding="utf-8", errors="replace") as f:
return f.read().strip()
except OSError:
return ""
def _event_doc(self, kind):
if kind not in self._events:
cached = os.path.join(CACHE, "events_%s.json" % kind)
if not os.path.exists(cached):
try:
fetch("https://runtime.fivem.net/doc/events/%s.html.json" % kind, cached)
except Exception as exc: # Netzwerk optional
log("events %s nicht geladen: %s" % (kind, exc))
self._events[kind] = ""
return ""
with open(cached, "r", encoding="utf-8") as f:
self._events[kind] = html_to_text(json.load(f).get("content", ""))
return self._events[kind]
# -- einzelne Shortcodes ------------------------------------------------
def _sc(self, name, attrs, pos):
if name == "code":
body = self._read_static(attrs.get("file", "")).replace("{{RMV}}", self._rmv)
return "\n```%s\n%s\n```\n" % (attrs.get("language", ""), body)
if name == "rmv":
return self._rmv
if name == "rmv2":
return self._rmv2
if name == "native_link":
n = (pos[0] if pos else "").strip()
h = self.hashes.get(n)
return "[%s](https://docs.fivem.net/natives/?_%s)" % (n, h) if h else "`%s`" % n
if name == "youtube":
vid = attrs.get("id") or (pos[0] if pos else "")
return "\n[Video: https://www.youtube.com/watch?v=%s]\n" % vid
if name == "video":
return "\n[Video: %s]\n" % attrs.get("src", "")
if name == "events":
return "\n" + self._event_doc((pos[0] if pos else "client").strip()) + "\n"
if name == "forum_topic":
return "\n[Community-Post: https://forum.cfx.re/t/%s]\n" % (pos[0] if pos else "")
if name in ("article-nav", "steps"):
return ""
return ""
def _paired(self, m):
name, raw, inner = m.group(1), m.group(2), m.group(3)
inner = self.render(inner).strip()
if name == "steps":
return "\n" + inner + "\n"
attrs, _ = parse_attrs(raw)
title = attrs.get("title", "")
head = "> **%s**\n" % title if title else ""
body = "\n".join("> " + l for l in inner.splitlines())
return "\n%s%s\n" % (head, body)
def render(self, text):
prev = None
while prev != text:
prev = text
text = PAIRED_SC.sub(self._paired, text)
return SINGLE_SC.sub(
lambda m: self._sc(m.group(1), *parse_attrs(m.group(2))), text)
def load_docs(natives, refresh=False):
src = os.path.join(CACHE, "fivem-docs")
if not os.path.isdir(src):
os.makedirs(CACHE, exist_ok=True)
log("clone", DOCS_REPO)
subprocess.run(["git", "clone", "--depth", "1", "-q", DOCS_REPO, src], check=True)
elif refresh:
subprocess.run(["git", "-C", src, "pull", "-q"], check=False)
# native_link braucht Name -> Hash (inkl. Aliases)
hashes = {}
for n in natives:
if n["name"]:
hashes.setdefault(n["name"], n["hash"])
for a in n["aliases"].split():
hashes.setdefault(a, n["hash"])
shortcodes = Shortcodes(src, hashes)
base = os.path.join(src, "content", "docs")
rows = []
for dirpath, _dirs, files in os.walk(base):
for fn in sorted(files):
if not fn.endswith(".md"):
continue
full = os.path.join(dirpath, fn)
rel = os.path.relpath(full, base).replace("\\", "/")
with open(full, "r", encoding="utf-8", errors="replace") as f:
raw = f.read()
title, weight = "", 0
body = raw
m = FRONTMATTER.match(raw)
if m:
body = raw[m.end():]
for line in m.group(1).splitlines():
low = line.lower()
if low.startswith("title:"):
title = line.split(":", 1)[1].strip().strip('"').strip("'")
elif low.startswith("weight:"):
try:
weight = int(line.split(":", 1)[1].strip())
except ValueError:
pass
if not title:
title = os.path.splitext(fn)[0].replace("-", " ").title()
slug = rel[:-3]
if slug.endswith("/_index") or slug == "_index":
slug = slug[:-len("_index")].rstrip("/")
section = slug.split("/")[0] if slug else "root"
url = "https://docs.fivem.net/docs/" + (slug + "/" if slug else "")
rows.append({
"path": rel,
"slug": slug,
"section": section,
"title": title,
"weight": weight,
"url": url,
"body": shortcodes.render(body).strip(),
"raw": body.strip(),
})
log("docs:", len(rows))
return rows
# ---------------------------------------------------------------- schema
SCHEMA = """
DROP TABLE IF EXISTS natives_fts;
DROP TABLE IF EXISTS natives;
DROP TABLE IF EXISTS docs_fts;
DROP TABLE IF EXISTS docs;
DROP TABLE IF EXISTS meta;
CREATE TABLE natives (
id INTEGER PRIMARY KEY,
hash TEXT NOT NULL,
jhash TEXT,
name TEXT,
lua_name TEXT,
ns TEXT,
source TEXT, -- gta5 | cfx
game TEXT, -- gta5 | rdr3 | ny | ''
apiset TEXT, -- client | server | shared
results TEXT,
results_description TEXT,
description TEXT,
signature TEXT,
params_json TEXT,
param_names TEXT,
examples_json TEXT,
aliases TEXT,
url TEXT
);
CREATE INDEX idx_nat_hash ON natives(hash);
CREATE INDEX idx_nat_name ON natives(name);
CREATE INDEX idx_nat_lua ON natives(lua_name);
CREATE INDEX idx_nat_ns ON natives(ns);
CREATE INDEX idx_nat_api ON natives(apiset);
CREATE VIRTUAL TABLE natives_fts USING fts5(
name, lua_name, ns, description, param_names, aliases, hash,
content='natives', content_rowid='id',
tokenize="unicode61 tokenchars '_'"
);
CREATE TABLE docs (
id INTEGER PRIMARY KEY,
path TEXT, slug TEXT, section TEXT, title TEXT,
weight INTEGER, url TEXT, body TEXT, raw TEXT
);
CREATE INDEX idx_docs_slug ON docs(slug);
CREATE INDEX idx_docs_sect ON docs(section);
CREATE VIRTUAL TABLE docs_fts USING fts5(
title, section, slug, body,
content='docs', content_rowid='id',
tokenize="unicode61 tokenchars '_'"
);
CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT);
"""
def build(refresh=False):
os.makedirs(DATA, exist_ok=True)
natives = load_natives(refresh)
docs = load_docs(natives, refresh)
if os.path.exists(DB):
os.remove(DB)
con = sqlite3.connect(DB)
con.executescript(SCHEMA)
ncols = ["hash", "jhash", "name", "lua_name", "ns", "source", "game", "apiset",
"results", "results_description", "description", "signature",
"params_json", "param_names", "examples_json", "aliases", "url"]
con.executemany(
"INSERT INTO natives (%s) VALUES (%s)" % (",".join(ncols), ",".join("?" * len(ncols))),
[[r[c] for c in ncols] for r in natives],
)
con.execute("INSERT INTO natives_fts(natives_fts) VALUES('rebuild')")
dcols = ["path", "slug", "section", "title", "weight", "url", "body", "raw"]
con.executemany(
"INSERT INTO docs (%s) VALUES (%s)" % (",".join(dcols), ",".join("?" * len(dcols))),
[[r[c] for c in dcols] for r in docs],
)
con.execute("INSERT INTO docs_fts(docs_fts) VALUES('rebuild')")
con.executemany("INSERT INTO meta (key,value) VALUES (?,?)", [
("built_at", datetime.now(timezone.utc).isoformat(timespec="seconds")),
("native_count", str(len(natives))),
("doc_count", str(len(docs))),
("sources", json.dumps([u for _, u in NATIVE_SOURCES] + [DOCS_REPO])),
("schema_version", "1"),
])
con.commit()
con.execute("VACUUM")
con.close()
# Grep-freundlicher Export (eine Zeile pro Native)
out = os.path.join(DATA, "natives.jsonl")
with open(out, "w", encoding="utf-8", newline="\n") as f:
for r in sorted(natives, key=lambda x: (x["ns"], x["name"] or x["hash"])):
slim = {k: r[k] for k in ("hash", "name", "lua_name", "ns", "apiset", "game",
"signature", "description", "aliases", "url")}
slim["params"] = json.loads(r["params_json"])
f.write(json.dumps(slim, ensure_ascii=False) + "\n")
log("ok ->", DB, "%.1f MB" % (os.path.getsize(DB) / 1e6))
if __name__ == "__main__":
build(refresh="--refresh" in sys.argv)
BIN
View File
Binary file not shown.
+7359
View File
File diff suppressed because one or more lines are too long
+375
View File
@@ -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:]))