From 0a880bc4d4a4819041eccfe588b90fd330821ee9 Mon Sep 17 00:00:00 2001 From: "smx.pusha" <139338836+smxpusha@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:23:14 +0200 Subject: [PATCH] BLD - add standalone phone development workflow --- .agents/skills/ask-matt/SKILL.md | 78 +++ .agents/skills/ask-matt/agents/openai.yaml | 5 + .agents/skills/code-review/SKILL.md | 89 +++ .agents/skills/code-review/agents/openai.yaml | 3 + .agents/skills/codebase-design/DEEPENING.md | 37 ++ .../skills/codebase-design/DESIGN-IT-TWICE.md | 44 ++ .agents/skills/codebase-design/SKILL.md | 114 ++++ .../skills/codebase-design/agents/openai.yaml | 3 + .agents/skills/domain-modeling/ADR-FORMAT.md | 47 ++ .../skills/domain-modeling/CONTEXT-FORMAT.md | 60 ++ .agents/skills/domain-modeling/SKILL.md | 74 +++ .../skills/domain-modeling/agents/openai.yaml | 3 + .agents/skills/esx-framework/SKILL.md | 47 ++ .../skills/esx-framework/best-practices.md | 456 +++++++++++++ .../skills/esx-framework/client-functions.md | 256 +++++++ .agents/skills/esx-framework/core-concepts.md | 217 ++++++ .../skills/esx-framework/events-callbacks.md | 450 +++++++++++++ .../skills/esx-framework/reference-links.md | 137 ++++ .../skills/esx-framework/server-functions.md | 464 +++++++++++++ .../skills/esx-framework/xplayer-methods.md | 624 ++++++++++++++++++ .agents/skills/fivem-basics/SKILL.md | 31 + .agents/skills/fivem-basics/client-server.md | 33 + .agents/skills/fivem-basics/debugging.md | 17 + .agents/skills/fivem-basics/events.md | 104 +++ .agents/skills/fivem-basics/exports.md | 48 ++ .agents/skills/fivem-basics/fxmanifest.md | 45 ++ .agents/skills/fivem-basics/optimization.md | 296 +++++++++ .../skills/fivem-basics/reference-links.md | 16 + .agents/skills/fivem-basics/structure.md | 65 ++ .agents/skills/fivem-dev/SKILL.md | 146 ++++ .agents/skills/fivem-nui/SKILL.md | 26 + .agents/skills/fivem-nui/best-practices.md | 472 +++++++++++++ .agents/skills/fivem-nui/fullscreen-nui.md | 205 ++++++ .agents/skills/fivem-nui/nui-callbacks.md | 309 +++++++++ .agents/skills/fivem-nui/reference-links.md | 128 ++++ .agents/skills/fivem-nui/setup.md | 385 +++++++++++ .agents/skills/fivem-security/SKILL.md | 25 + .agents/skills/fivem-security/events.md | 73 ++ .agents/skills/grill-me/SKILL.md | 7 + .agents/skills/grill-me/agents/openai.yaml | 5 + .agents/skills/grilling/SKILL.md | 12 + .agents/skills/grilling/agents/openai.yaml | 3 + .agents/skills/lua-basics/SKILL.md | 29 + .agents/skills/lua-basics/conditionals.md | 59 ++ .agents/skills/lua-basics/errors.md | 61 ++ .agents/skills/lua-basics/functions.md | 157 +++++ .agents/skills/lua-basics/reference-links.md | 17 + .agents/skills/lua-basics/tables.md | 124 ++++ .agents/skills/lua-basics/variables.md | 58 ++ .agents/skills/oxlib/SKILL.md | 45 ++ .agents/skills/oxlib/addCommand.md | 30 + .agents/skills/oxlib/callback.md | 61 ++ .agents/skills/oxlib/init.md | 34 + .agents/skills/oxlib/interface.md | 60 ++ .agents/skills/oxlib/zones.md | 68 ++ .agents/skills/oxmysql/SKILL.md | 42 ++ .agents/skills/oxmysql/insert.md | 26 + .agents/skills/oxmysql/placeholders.md | 11 + .agents/skills/oxmysql/prepare.md | 29 + .agents/skills/oxmysql/query.md | 37 ++ .agents/skills/oxmysql/rawExecute.md | 19 + .agents/skills/oxmysql/scalar.md | 27 + .agents/skills/oxmysql/single.md | 29 + .agents/skills/oxmysql/transaction.md | 35 + .agents/skills/oxmysql/update.md | 25 + .agents/skills/research/SKILL.md | 12 + .agents/skills/research/agents/openai.yaml | 3 + .../skills/resolving-merge-conflicts/SKILL.md | 14 + .../agents/openai.yaml | 3 + .agents/skills/to-spec/SKILL.md | 75 +++ .agents/skills/to-spec/agents/openai.yaml | 5 + frontend/pnpm-workspace.yaml | 3 +- 72 files changed, 6856 insertions(+), 1 deletion(-) create mode 100644 .agents/skills/ask-matt/SKILL.md create mode 100644 .agents/skills/ask-matt/agents/openai.yaml create mode 100644 .agents/skills/code-review/SKILL.md create mode 100644 .agents/skills/code-review/agents/openai.yaml create mode 100644 .agents/skills/codebase-design/DEEPENING.md create mode 100644 .agents/skills/codebase-design/DESIGN-IT-TWICE.md create mode 100644 .agents/skills/codebase-design/SKILL.md create mode 100644 .agents/skills/codebase-design/agents/openai.yaml create mode 100644 .agents/skills/domain-modeling/ADR-FORMAT.md create mode 100644 .agents/skills/domain-modeling/CONTEXT-FORMAT.md create mode 100644 .agents/skills/domain-modeling/SKILL.md create mode 100644 .agents/skills/domain-modeling/agents/openai.yaml create mode 100644 .agents/skills/esx-framework/SKILL.md create mode 100644 .agents/skills/esx-framework/best-practices.md create mode 100644 .agents/skills/esx-framework/client-functions.md create mode 100644 .agents/skills/esx-framework/core-concepts.md create mode 100644 .agents/skills/esx-framework/events-callbacks.md create mode 100644 .agents/skills/esx-framework/reference-links.md create mode 100644 .agents/skills/esx-framework/server-functions.md create mode 100644 .agents/skills/esx-framework/xplayer-methods.md create mode 100644 .agents/skills/fivem-basics/SKILL.md create mode 100644 .agents/skills/fivem-basics/client-server.md create mode 100644 .agents/skills/fivem-basics/debugging.md create mode 100644 .agents/skills/fivem-basics/events.md create mode 100644 .agents/skills/fivem-basics/exports.md create mode 100644 .agents/skills/fivem-basics/fxmanifest.md create mode 100644 .agents/skills/fivem-basics/optimization.md create mode 100644 .agents/skills/fivem-basics/reference-links.md create mode 100644 .agents/skills/fivem-basics/structure.md create mode 100644 .agents/skills/fivem-dev/SKILL.md create mode 100644 .agents/skills/fivem-nui/SKILL.md create mode 100644 .agents/skills/fivem-nui/best-practices.md create mode 100644 .agents/skills/fivem-nui/fullscreen-nui.md create mode 100644 .agents/skills/fivem-nui/nui-callbacks.md create mode 100644 .agents/skills/fivem-nui/reference-links.md create mode 100644 .agents/skills/fivem-nui/setup.md create mode 100644 .agents/skills/fivem-security/SKILL.md create mode 100644 .agents/skills/fivem-security/events.md create mode 100644 .agents/skills/grill-me/SKILL.md create mode 100644 .agents/skills/grill-me/agents/openai.yaml create mode 100644 .agents/skills/grilling/SKILL.md create mode 100644 .agents/skills/grilling/agents/openai.yaml create mode 100644 .agents/skills/lua-basics/SKILL.md create mode 100644 .agents/skills/lua-basics/conditionals.md create mode 100644 .agents/skills/lua-basics/errors.md create mode 100644 .agents/skills/lua-basics/functions.md create mode 100644 .agents/skills/lua-basics/reference-links.md create mode 100644 .agents/skills/lua-basics/tables.md create mode 100644 .agents/skills/lua-basics/variables.md create mode 100644 .agents/skills/oxlib/SKILL.md create mode 100644 .agents/skills/oxlib/addCommand.md create mode 100644 .agents/skills/oxlib/callback.md create mode 100644 .agents/skills/oxlib/init.md create mode 100644 .agents/skills/oxlib/interface.md create mode 100644 .agents/skills/oxlib/zones.md create mode 100644 .agents/skills/oxmysql/SKILL.md create mode 100644 .agents/skills/oxmysql/insert.md create mode 100644 .agents/skills/oxmysql/placeholders.md create mode 100644 .agents/skills/oxmysql/prepare.md create mode 100644 .agents/skills/oxmysql/query.md create mode 100644 .agents/skills/oxmysql/rawExecute.md create mode 100644 .agents/skills/oxmysql/scalar.md create mode 100644 .agents/skills/oxmysql/single.md create mode 100644 .agents/skills/oxmysql/transaction.md create mode 100644 .agents/skills/oxmysql/update.md create mode 100644 .agents/skills/research/SKILL.md create mode 100644 .agents/skills/research/agents/openai.yaml create mode 100644 .agents/skills/resolving-merge-conflicts/SKILL.md create mode 100644 .agents/skills/resolving-merge-conflicts/agents/openai.yaml create mode 100644 .agents/skills/to-spec/SKILL.md create mode 100644 .agents/skills/to-spec/agents/openai.yaml diff --git a/.agents/skills/ask-matt/SKILL.md b/.agents/skills/ask-matt/SKILL.md new file mode 100644 index 0000000..70b807b --- /dev/null +++ b/.agents/skills/ask-matt/SKILL.md @@ -0,0 +1,78 @@ +--- +name: ask-matt +description: Ask which skill or flow fits your situation. A router over the skills in this repo. +disable-model-invocation: true +--- + +# Ask Matt + +You don't remember every skill, so ask. + +A **flow** is a path through the skills. Most paths run along one **main flow**, and two **on-ramps** merge onto it. Everything else is standalone, or a vocabulary layer that runs underneath. + +## The main flow: idea → ship + +The route most work travels. You have an idea and want it built. + +1. **`/grill-with-docs`** — sharpen the idea by interview. Start here when you **have a codebase**: it's stateful, retaining what it learns in `CONTEXT.md` and ADRs. (No codebase? Use `/grill-me` — see Standalone. Both run the same `/grilling` primitive; `grill-with-docs` is the one that leaves a paper trail.) +2. **Branch — can you settle every question in conversation?** If a question needs a runnable answer (state, business logic, a UI you have to see), detour through a prototype, bridged by **`/handoff`** in both directions (see Crossing sessions): + - **`/handoff`** out, then open a fresh session against that file, + - **`/prototype`** to answer the question with throwaway code, + - **`/handoff`** back what you learned, and reference it from the original idea thread. +3. **Branch — is this a multi-session build?** + - **Yes** → **`/to-spec`** (turn the thread into a spec), then **`/to-tickets`** to split it into tracer-bullet tickets, each declaring its **blocking edges**. On a local tracker that's one file per ticket under `.scratch//issues/`, worked blockers-first by hand; on a real tracker the edges become native blocking links, so any ticket whose blockers are done can be grabbed — kick off **`/implement`** per ticket, **clearing context between each one**. + - **No** → **`/implement`** right here, in the same context window. + + Either way, **`/implement`** builds each issue by driving **`/tdd`** internally — one red-green slice at a time — then closes out by running **`/code-review`**, a two-axis review (Standards + Spec) of the diff, before committing. Reach for **`/tdd`** on its own when you just want to build a concrete behaviour test-first without a full spec, and **`/code-review`** on its own whenever you want to review a branch or PR against a fixed point. + +### Context hygiene + +Keep steps 1–3 in **one unbroken context window** — don't compact or clear until after `/to-tickets` — so the grilling, spec, and tickets all build on the same thinking. Each `/implement` then starts fresh, working from the ticket. + +The limit on this is the **[smart zone](https://www.aihero.dev/ai-coding-dictionary/smart-zone)**: the window (~120k tokens on state-of-the-art models) within which the model still reasons sharply. If a session approaches it before `/to-tickets`, don't push on degraded — `/handoff` and continue in a fresh thread. + +## On-ramps + +A starting situation that generates work, then merges onto the main flow. + +- **Bugs and requests piling up** → **`/triage`**. It moves issues through triage roles and produces agent-ready issues, which **`/implement`** later picks up. + + Triage is only for issues **you didn't create** — bug reports, incoming feature requests, anything that arrives raw. Tickets that `/to-tickets` produced are already agent-ready, so **don't triage them**. + +- **Something's broken** → **`/diagnosing-bugs`**. For the hard ones: the bug that resists a first glance, the intermittent flake, the regression that crept in between two known-good states. It refuses to theorise until it has a **tight feedback loop** — one command that already goes red on *this* bug — then fixes with a regression test. Its post-mortem hands off to **`/improve-codebase-architecture`** when the real finding is that there's no good seam to lock the bug down. + +- **A huge, foggy effort — a greenfield project or a huge feature build, too big for one session** → **`/wayfinder`**, the most cognitively demanding flow here. When the way from here to the destination isn't visible yet, it charts a **shared map** of **decision tickets** on the issue tracker and resolves them one at a time — producing **decisions, not deliverables** — until the fog is pushed back and the way is clear. Where **`/grill-with-docs`** sharpens an idea you can hold in one session, wayfinder is for the idea you can't — and it's slower and denser, so save it for exactly that, never a well-scoped feature. + + When the map clears, **it hands off, it doesn't build**: merge onto the main flow at **`/to-spec`**, which collapses the map's linked decisions into a buildable plan, then `/to-tickets` and `/implement` as usual. Looping the map straight into `/implement` skips that collapse and throws the linked detail away — go straight to `/implement` only when the effort turned out genuinely small. + +## Codebase health + +Not feature work — upkeep. + +- **`/improve-codebase-architecture`** — run whenever you have a spare moment to keep the codebase good for agents to operate in. It surfaces **deepening opportunities**; picking one _generates an idea_ you can take into the main flow at `/grill-with-docs`. It's the survey that finds the candidates; **`/codebase-design`** (below) is the bench you design the chosen one on. + +## Vocabulary underneath + +Two model-invoked references that run *beneath* the other skills — each the single source of truth for its vocabulary. Reach for them directly when the **words**, not the process, are the problem; or let the skills above pull them in. + +- **`/domain-modeling`** — sharpen the project's *domain* language: challenge a fuzzy term, resolve an overloaded word ("account" doing three jobs), record a hard-to-reverse decision as an ADR. It's the active discipline `/grill-with-docs` drives to keep `CONTEXT.md` a clean glossary. +- **`/codebase-design`** — the deep-module vocabulary (module, interface, depth, seam, adapter, leverage, locality) for designing a module's *shape*: a lot of behaviour behind a small interface at a clean seam. `/tdd` and `/improve-codebase-architecture` both speak it. + +## Crossing sessions + +- **`/handoff`** — when a thread is full or you need to branch off (e.g. into a `/prototype` session), this compacts the conversation into a markdown file. You don't continue in place — you **open a new session and reference that file** to carry the context across. It's the bridge between context windows, in either direction. Use it when you want a **fresh session** but need the **current conversation preserved**. +- **`/compact`** (built-in) — stay in the **same conversation**, letting the earlier turns be summarized. Use it at **intentional breaks between phases**, when you don't mind losing the verbatim history. Don't compact mid-phase — the agent can lose its way. `/handoff` forks; `/compact` continues. + +## Standalone + +Off the main flow entirely. + +- **`/grill-me`** — the same relentless interview as `/grill-with-docs`, but for when you have **no codebase**. Stateless: it saves nothing locally, builds no `CONTEXT.md`. Reach for it to sharpen any plan or design that doesn't live in a repo. +- **`/prototype`** — a small, throwaway program that answers one design question: does this state model feel right, or what should this UI look like. Throwaway from day one — keep the answer, delete the code. It's the detour in step 2 of the main flow, but reach for it any time a design question is hard to settle on paper. +- **`/research`** — delegate reading legwork to a **background agent**: it investigates a question against **primary sources**, then leaves a cited Markdown file in the repo. Keep working while it reads. The file it produces is something to take *into* the main flow at `/grill-with-docs` — research feeds the thinking, it doesn't replace it. +- **`/teach`** — learn a concept over multiple sessions, using the current directory as a stateful workspace. +- **`/writing-great-skills`** — reference for writing and editing skills well. + +## Precondition + +**`/setup-matt-pocock-skills`** — run before your first engineering flow to configure the issue tracker, triage labels, and doc layout the other skills assume. Custom issue trackers also work. diff --git a/.agents/skills/ask-matt/agents/openai.yaml b/.agents/skills/ask-matt/agents/openai.yaml new file mode 100644 index 0000000..5c60d51 --- /dev/null +++ b/.agents/skills/ask-matt/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Ask Matt" + short_description: "Find the right skill or workflow" +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/code-review/SKILL.md b/.agents/skills/code-review/SKILL.md new file mode 100644 index 0000000..2a0b524 --- /dev/null +++ b/.agents/skills/code-review/SKILL.md @@ -0,0 +1,89 @@ +--- +name: code-review +description: Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes — Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/PRD asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to "review since X". +--- + +Two-axis review of the diff between `HEAD` and a fixed point the user supplies: + +- **Standards** — does the code conform to this repo's documented coding standards? +- **Spec** — does the code faithfully implement the originating issue / PRD / spec? + +Both axes run as **parallel sub-agents** so they don't pollute each other's context, then this skill aggregates their findings. + +The issue tracker should have been provided to you — run `/setup-matt-pocock-skills` if `docs/agents/issue-tracker.md` is missing. + +## Process + +### 1. Pin the fixed point + +Whatever the user said is the fixed point — a commit SHA, branch name, tag, `main`, `HEAD~5`, etc. If they didn't specify one, ask for it. + +Capture the diff command once: `git diff ...HEAD` (three-dot, so the comparison is against the merge-base). Also note the list of commits via `git log ..HEAD --oneline`. + +Before going further, confirm the fixed point resolves (`git rev-parse `) and the diff is non-empty. A bad ref or empty diff should fail here — not inside two parallel sub-agents. + +### 2. Identify the spec source + +Look for the originating spec, in this order: + +1. Issue references in the commit messages (`#123`, `Closes #45`, GitLab `!67`, etc.) — fetch via the workflow in `docs/agents/issue-tracker.md`. +2. A path the user passed as an argument. +3. A PRD/spec file under `docs/`, `specs/`, or `.scratch/` matching the branch name or feature. +4. If nothing is found, ask the user where the spec is. If they say there isn't one, the **Spec** sub-agent will skip and report "no spec available". + +### 3. Identify the standards sources + +Anything in the repo that documents how code should be written, such as `CODING_STANDARDS.md` or `CONTRIBUTING.md`. + +On top of whatever the repo documents, the Standards axis always carries the **smell baseline** below — a fixed set of Fowler code smells (_Refactoring_, ch.3) that applies even when a repo documents nothing. Two rules bind it: + +- **The repo overrides.** A documented repo standard always wins; where it endorses something the baseline would flag, suppress the smell. +- **Always a judgement call.** Each smell is a labelled heuristic ("possible Feature Envy"), never a hard violation — and, like any standard here, skip anything tooling already enforces. + +Each smell reads *what it is* → *how to fix*; match it against the diff: + +- **Mysterious Name** — a function, variable, or type whose name doesn't reveal what it does or holds. → rename it; if no honest name comes, the design's murky. +- **Duplicated Code** — the same logic shape appears in more than one hunk or file in the change. → extract the shared shape, call it from both. +- **Feature Envy** — a method that reaches into another object's data more than its own. → move the method onto the data it envies. +- **Data Clumps** — the same few fields or params keep travelling together (a type wanting to be born). → bundle them into one type, pass that. +- **Primitive Obsession** — a primitive or string standing in for a domain concept that deserves its own type. → give the concept its own small type. +- **Repeated Switches** — the same `switch`/`if`-cascade on the same type recurs across the change. → replace with polymorphism, or one map both sites share. +- **Shotgun Surgery** — one logical change forces scattered edits across many files in the diff. → gather what changes together into one module. +- **Divergent Change** — one file or module is edited for several unrelated reasons. → split so each module changes for one reason. +- **Speculative Generality** — abstraction, parameters, or hooks added for needs the spec doesn't have. → delete it; inline back until a real need shows. +- **Message Chains** — long `a.b().c().d()` navigation the caller shouldn't depend on. → hide the walk behind one method on the first object. +- **Middle Man** — a class or function that mostly just delegates onward. → cut it, call the real target direct. +- **Refused Bequest** — a subclass or implementer that ignores or overrides most of what it inherits. → drop the inheritance, use composition. + +### 4. Spawn both sub-agents in parallel + +Send a single message with two `Agent` tool calls. Use the `general-purpose` subagent for both. + +**Standards sub-agent prompt** — include: + +- The full diff command and commit list. +- The list of standards-source files you found in step 3, **plus the smell baseline from step 3** pasted in full — the sub-agent has no other access to it. +- The brief: "Report — per file/hunk where relevant — (a) every place the diff violates a documented standard: cite the standard (file + the rule); and (b) any baseline smell you spot: name it and quote the hunk. Distinguish hard violations from judgement calls — documented-standard breaches can be hard, but baseline smells are always judgement calls, and a documented repo standard overrides the baseline. Skip anything tooling enforces. Under 400 words." + +**Spec sub-agent prompt** — include: + +- The diff command and commit list. +- The path or fetched contents of the spec. +- The brief: "Report: (a) requirements the spec asked for that are missing or partial; (b) behaviour in the diff that wasn't asked for (scope creep); (c) requirements that look implemented but where the implementation looks wrong. Quote the spec line for each finding. Under 400 words." + +If the spec is missing, skip the Spec sub-agent and note this in the final report. + +### 5. Aggregate + +Present the two reports under `## Standards` and `## Spec` headings, verbatim or lightly cleaned. Do **not** merge or rerank findings — the two axes are deliberately separate (see _Why two axes_). + +End with a one-line summary: total findings per axis, and the worst issue _within each axis_ (if any). Don't pick a single winner across axes — that's the reranking the separation exists to prevent. + +## Why two axes + +A change can pass one axis and fail the other: + +- Code that follows every standard but implements the wrong thing → **Standards pass, Spec fail.** +- Code that does exactly what the issue asked but breaks the project's conventions → **Spec pass, Standards fail.** + +Reporting them separately stops one axis from masking the other. diff --git a/.agents/skills/code-review/agents/openai.yaml b/.agents/skills/code-review/agents/openai.yaml new file mode 100644 index 0000000..9076774 --- /dev/null +++ b/.agents/skills/code-review/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Code Review" + short_description: "Review a diff on standards and spec" diff --git a/.agents/skills/codebase-design/DEEPENING.md b/.agents/skills/codebase-design/DEEPENING.md new file mode 100644 index 0000000..3938457 --- /dev/null +++ b/.agents/skills/codebase-design/DEEPENING.md @@ -0,0 +1,37 @@ +# Deepening + +How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [SKILL.md](SKILL.md) — **module**, **interface**, **seam**, **adapter**. + +## Dependency categories + +When assessing a candidate for deepening, classify its dependencies. The category determines how the deepened module is tested across its seam. + +### 1. In-process + +Pure computation, in-memory state, no I/O. Always deepenable — merge the modules and test through the new interface directly. No adapter needed. + +### 2. Local-substitutable + +Dependencies that have local test stand-ins (PGLite for Postgres, in-memory filesystem). Deepenable if the stand-in exists. The deepened module is tested with the stand-in running in the test suite. The seam is internal; no port at the module's external interface. + +### 3. Remote but owned (Ports & Adapters) + +Your own services across a network boundary (microservices, internal APIs). Define a **port** (interface) at the seam. The deep module owns the logic; the transport is injected as an **adapter**. Tests use an in-memory adapter. Production uses an HTTP/gRPC/queue adapter. + +Recommendation shape: *"Define a port at the seam, implement an HTTP adapter for production and an in-memory adapter for testing, so the logic sits in one deep module even though it's deployed across a network."* + +### 4. True external (Mock) + +Third-party services (Stripe, Twilio, etc.) you don't control. The deepened module takes the external dependency as an injected port; tests provide a mock adapter. + +## Seam discipline + +- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a port unless at least two adapters are justified (typically production + test). A single-adapter seam is just indirection. +- **Internal seams vs external seams.** A deep module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface. Don't expose internal seams through the interface just because tests use them. + +## Testing strategy: replace, don't layer + +- Old unit tests on shallow modules become waste once tests at the deepened module's interface exist — delete them. +- Write new tests at the deepened module's interface. The **interface is the test surface**. +- Tests assert on observable outcomes through the interface, not internal state. +- Tests should survive internal refactors — they describe behaviour, not implementation. If a test has to change when the implementation changes, it's testing past the interface. diff --git a/.agents/skills/codebase-design/DESIGN-IT-TWICE.md b/.agents/skills/codebase-design/DESIGN-IT-TWICE.md new file mode 100644 index 0000000..49a7c42 --- /dev/null +++ b/.agents/skills/codebase-design/DESIGN-IT-TWICE.md @@ -0,0 +1,44 @@ +# Design It Twice + +When the user wants to explore alternative interfaces for a chosen deepening candidate, use this parallel sub-agent pattern. Based on "Design It Twice" (Ousterhout) — your first idea is unlikely to be the best. + +Uses the vocabulary in [SKILL.md](SKILL.md) — **module**, **interface**, **seam**, **adapter**, **leverage**. + +## Process + +### 1. Frame the problem space + +Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate: + +- The constraints any new interface would need to satisfy +- The dependencies it would rely on, and which category they fall into (see [DEEPENING.md](DEEPENING.md)) +- A rough illustrative code sketch to ground the constraints — not a proposal, just a way to make the constraints concrete + +Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel. + +### 2. Spawn sub-agents + +Spawn 3+ sub-agents in parallel using the Agent tool. Each must produce a **radically different** interface for the deepened module. + +Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [DEEPENING.md](DEEPENING.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint: + +- Agent 1: "Minimize the interface — aim for 1–3 entry points max. Maximise leverage per entry point." +- Agent 2: "Maximise flexibility — support many use cases and extension." +- Agent 3: "Optimise for the most common caller — make the default case trivial." +- Agent 4 (if applicable): "Design around ports & adapters for cross-seam dependencies." + +Include both [SKILL.md](SKILL.md) vocabulary and CONTEXT.md vocabulary in the brief so each sub-agent names things consistently with the architecture language and the project's domain language. + +Each sub-agent outputs: + +1. Interface (types, methods, params — plus invariants, ordering, error modes) +2. Usage example showing how callers use it +3. What the implementation hides behind the seam +4. Dependency strategy and adapters (see [DEEPENING.md](DEEPENING.md)) +5. Trade-offs — where leverage is high, where it's thin + +### 3. Present and compare + +Present designs sequentially so the user can absorb each one, then compare them in prose. Contrast by **depth** (leverage at the interface), **locality** (where change concentrates), and **seam placement**. + +After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid. Be opinionated — the user wants a strong read, not a menu. diff --git a/.agents/skills/codebase-design/SKILL.md b/.agents/skills/codebase-design/SKILL.md new file mode 100644 index 0000000..16620c2 --- /dev/null +++ b/.agents/skills/codebase-design/SKILL.md @@ -0,0 +1,114 @@ +--- +name: codebase-design +description: Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary. +--- + +# Codebase Design + +Design **deep modules**: a lot of behaviour behind a small interface, placed at a clean seam, testable through that interface. Use this language and these principles wherever code is being designed or restructured. The aim is leverage for callers, locality for maintainers, and testability for everyone. + +## Glossary + +Use these terms exactly — don't substitute "component," "service," "API," or "boundary." Consistent language is the whole point. + +**Module** — anything with an interface and an implementation. Deliberately scale-agnostic: a function, class, package, or tier-spanning slice. _Avoid_: unit, component, service. + +**Interface** — everything a caller must know to use the module correctly: the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics. _Avoid_: API, signature (too narrow — they refer only to the type-level surface). + +**Implementation** — what's inside a module, its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for "adapter" when the seam is the topic; "implementation" otherwise. + +**Depth** — leverage at the interface: the amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface, **shallow** when the interface is nearly as complex as the implementation. + +**Seam** _(Michael Feathers)_ — a place where you can alter behaviour without editing in that place; the *location* at which a module's interface lives. Where to put the seam is its own design decision, distinct from what goes behind it. _Avoid_: boundary (overloaded with DDD's bounded context). + +**Adapter** — a concrete thing that satisfies an interface at a seam. Describes *role* (what slot it fills), not substance (what's inside). + +**Leverage** — what callers get from depth: more capability per unit of interface they learn. One implementation pays back across N call sites and M tests. + +**Locality** — what maintainers get from depth: change, bugs, knowledge, and verification concentrate in one place rather than spreading across callers. Fix once, fixed everywhere. + +## Deep vs shallow + +**Deep module** = small interface + lots of implementation: + +``` +┌─────────────────────┐ +│ Small Interface │ ← Few methods, simple params +├─────────────────────┤ +│ │ +│ Deep Implementation│ ← Complex logic hidden +│ │ +└─────────────────────┘ +``` + +**Shallow module** = large interface + little implementation (avoid): + +``` +┌─────────────────────────────────┐ +│ Large Interface │ ← Many methods, complex params +├─────────────────────────────────┤ +│ Thin Implementation │ ← Just passes through +└─────────────────────────────────┘ +``` + +When designing an interface, ask: + +- Can I reduce the number of methods? +- Can I simplify the parameters? +- Can I hide more complexity inside? + +## Principles + +- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable parts — they just aren't part of the interface. A module can have **internal seams** (private to its implementation, used by its own tests) as well as the **external seam** at its interface. +- **The deletion test.** Imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep. +- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test *past* the interface, the module is probably the wrong shape. +- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it. + +## Designing for testability + +Good interfaces make testing natural: + +1. **Accept dependencies, don't create them.** + + ```typescript + // Testable + function processOrder(order, paymentGateway) {} + + // Hard to test + function processOrder(order) { + const gateway = new StripeGateway(); + } + ``` + +2. **Return results, don't produce side effects.** + + ```typescript + // Testable + function calculateDiscount(cart): Discount {} + + // Hard to test + function applyDiscount(cart): void { + cart.total -= discount; + } + ``` + +3. **Small surface area.** Fewer methods = fewer tests needed. Fewer params = simpler test setup. + +## Relationships + +- A **Module** has exactly one **Interface** (the surface it presents to callers and tests). +- **Depth** is a property of a **Module**, measured against its **Interface**. +- A **Seam** is where a **Module**'s **Interface** lives. +- An **Adapter** sits at a **Seam** and satisfies the **Interface**. +- **Depth** produces **Leverage** for callers and **Locality** for maintainers. + +## Rejected framings + +- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead. +- **"Interface" as the TypeScript `interface` keyword or a class's public methods**: too narrow — interface here includes every fact a caller must know. +- **"Boundary"**: overloaded with DDD's bounded context. Say **seam** or **interface**. + +## Going deeper + +- **Deepening a cluster given its dependencies** — see [DEEPENING.md](DEEPENING.md): dependency categories, seam discipline, and replace-don't-layer testing. +- **Exploring alternative interfaces** — see [DESIGN-IT-TWICE.md](DESIGN-IT-TWICE.md): spin up parallel sub-agents to design the interface several radically different ways, then compare on depth, locality, and seam placement. diff --git a/.agents/skills/codebase-design/agents/openai.yaml b/.agents/skills/codebase-design/agents/openai.yaml new file mode 100644 index 0000000..3180715 --- /dev/null +++ b/.agents/skills/codebase-design/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Codebase Design" + short_description: "Vocabulary for deep-module design" diff --git a/.agents/skills/domain-modeling/ADR-FORMAT.md b/.agents/skills/domain-modeling/ADR-FORMAT.md new file mode 100644 index 0000000..da7e78e --- /dev/null +++ b/.agents/skills/domain-modeling/ADR-FORMAT.md @@ -0,0 +1,47 @@ +# ADR Format + +ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc. + +Create the `docs/adr/` directory lazily — only when the first ADR is needed. + +## Template + +```md +# {Short title of the decision} + +{1-3 sentences: what's the context, what did we decide, and why.} +``` + +That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections. + +## Optional sections + +Only include these when they add genuine value. Most ADRs won't need them. + +- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited +- **Considered Options** — only when the rejected alternatives are worth remembering +- **Consequences** — only when non-obvious downstream effects need to be called out + +## Numbering + +Scan `docs/adr/` for the highest existing number and increment by one. + +## When to offer an ADR + +All three of these must be true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing." + +### What qualifies + +- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres." +- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP." +- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out. +- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s. +- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate. +- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract." +- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months. diff --git a/.agents/skills/domain-modeling/CONTEXT-FORMAT.md b/.agents/skills/domain-modeling/CONTEXT-FORMAT.md new file mode 100644 index 0000000..eaf2a18 --- /dev/null +++ b/.agents/skills/domain-modeling/CONTEXT-FORMAT.md @@ -0,0 +1,60 @@ +# CONTEXT.md Format + +## Structure + +```md +# {Context Name} + +{One or two sentence description of what this context is and why it exists.} + +## Language + +**Order**: +{A one or two sentence description of the term} +_Avoid_: Purchase, transaction + +**Invoice**: +A request for payment sent to a customer after delivery. +_Avoid_: Bill, payment request + +**Customer**: +A person or organization that places orders. +_Avoid_: Client, buyer, account +``` + +## Rules + +- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others under `_Avoid_`. +- **Keep definitions tight.** One or two sentences max. Define what it IS, not what it does. +- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs. +- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine. + +## Single vs multi-context repos + +**Single context (most repos):** One `CONTEXT.md` at the repo root. + +**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other: + +```md +# Context Map + +## Contexts + +- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders +- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments +- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping + +## Relationships + +- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking +- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices +- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money` +``` + +The skill infers which structure applies: + +- If `CONTEXT-MAP.md` exists, read it to find contexts +- If only a root `CONTEXT.md` exists, single context +- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved + +When multiple contexts exist, infer which one the current topic relates to. If unclear, ask. diff --git a/.agents/skills/domain-modeling/SKILL.md b/.agents/skills/domain-modeling/SKILL.md new file mode 100644 index 0000000..d0f7e1a --- /dev/null +++ b/.agents/skills/domain-modeling/SKILL.md @@ -0,0 +1,74 @@ +--- +name: domain-modeling +description: Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, record an architectural decision, or when another skill needs to maintain the domain model. +--- + +# Domain Modeling + +Actively build and sharpen the project's domain model as you design. This is the *active* discipline — challenging terms, inventing edge-case scenarios, and writing the glossary and decisions down the moment they crystallise. (Merely *reading* `CONTEXT.md` for vocabulary is not this skill — that's a one-line habit any skill can do. This skill is for when you're changing the model, not just consuming it.) + +## File structure + +Most repos have a single context: + +``` +/ +├── CONTEXT.md +├── docs/ +│ └── adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives: + +``` +/ +├── CONTEXT-MAP.md +├── docs/ +│ └── adr/ ← system-wide decisions +├── src/ +│ ├── ordering/ +│ │ ├── CONTEXT.md +│ │ └── docs/adr/ ← context-specific decisions +│ └── billing/ +│ ├── CONTEXT.md +│ └── docs/adr/ +``` + +Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed. + +## During the session + +### Challenge against the glossary + +When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?" + +### Sharpen fuzzy language + +When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things." + +### Discuss concrete scenarios + +When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts. + +### Cross-reference with code + +When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?" + +### Update CONTEXT.md inline + +When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md). + +`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else. + +### Offer ADRs sparingly + +Only offer to create an ADR when all three are true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will wonder "why did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md). diff --git a/.agents/skills/domain-modeling/agents/openai.yaml b/.agents/skills/domain-modeling/agents/openai.yaml new file mode 100644 index 0000000..7f1522d --- /dev/null +++ b/.agents/skills/domain-modeling/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Domain Modeling" + short_description: "Build and sharpen a domain model" diff --git a/.agents/skills/esx-framework/SKILL.md b/.agents/skills/esx-framework/SKILL.md new file mode 100644 index 0000000..6c1f940 --- /dev/null +++ b/.agents/skills/esx-framework/SKILL.md @@ -0,0 +1,47 @@ +--- +name: esx-framework +description: ESX Legacy Framework for FiveM - Player management, jobs, economy, inventory, weapons. Use when creating ESX resources or working with xPlayer, PlayerData, ESX functions. +author: germanfndez +version: 1.0.0 +mcp-server: projecthub +--- + +# ESX Framework Development + +Complete guide for developing with ESX Legacy Framework — the most trusted FiveM roleplay framework since 2017. + +## When to use + +- Creating or editing ESX resources/scripts +- Working with player data (xPlayer, PlayerData) +- Implementing jobs, economy, inventory, or weapon systems +- Using ESX client/server functions, callbacks, or events +- Questions about ESX best practices and optimization + +## How to use + +Read individual rule files for detailed explanations and examples: + +- **rules/core-concepts.md** — ESX architecture, PlayerData, xPlayer object, framework initialization +- **rules/client-functions.md** — Client-side ESX functions, UI systems, player state management +- **rules/server-functions.md** — Server-side functions, player retrieval, callbacks, triggers +- **rules/xplayer-methods.md** — xPlayer object methods: money, items, weapons, inventory, jobs, metadata +- **rules/jobs-economy.md** — Job system, salaries, accounts (money/bank), society management +- **rules/inventory-items.md** — Inventory system, item management, usable items, weight calculations +- **rules/weapons-loadout.md** — Weapon system, loadout, components, ammo, tints +- **rules/events-callbacks.md** — ESX events, server callbacks, client callbacks, secure net events +- **rules/best-practices.md** — ESX coding standards, optimization, security, naming conventions + +- **rules/reference-links.md** — Official ESX documentation links + +## Key principles + +1. **Always check for nil** — `if xPlayer then ... end` before using xPlayer +2. **Use ESX.GetPlayerFromId** — Standard player retrieval: `local xPlayer = ESX.GetPlayerFromId(source)` +3. **Wait for player load** — Check `ESX.IsPlayerLoaded()` on client before accessing PlayerData +4. **Never trust client** — Validate all data server-side, use SecureNetEvent for client events +5. **Follow ESX patterns** — Use ESX functions instead of reinventing (callbacks, notifications, etc.) +6. **Optimize loops** — Cache player objects, avoid unnecessary GetPlayerFromId calls +7. **Use camelCase** — Follow Lua naming: `myVariable`, `MyGlobalFunction`, `MY_CONSTANT` +8. **Minimal globals** — Keep variables local unless they need global scope +9. **Use ox_lib for UI** — Prefer ox_lib for menus, dialogs, notifications, progress bars instead of ESX UI diff --git a/.agents/skills/esx-framework/best-practices.md b/.agents/skills/esx-framework/best-practices.md new file mode 100644 index 0000000..8fe6a53 --- /dev/null +++ b/.agents/skills/esx-framework/best-practices.md @@ -0,0 +1,456 @@ +# ESX Best Practices + +Based on official ESX documentation: https://docs.esx-framework.org/en/tutorial/coding_practices + +## Naming Conventions + +### camelCase for local variables and functions + +```lua +local myVariable = 10 +local playerCount = 0 + +local function calculateDistance(pos1, pos2) + return #(pos1 - pos2) +end +``` + +### PascalCase for global functions + +```lua +function MyGlobalFunction() + print('This is global') +end +``` + +### UPPERCASE for constants + +```lua +local MAX_DISTANCE = 10.0 -- Lua 5.4 +local INTERACTION_KEY = 38 +local DEFAULT_SALARY = 500 +``` + +## Use Local Variables + +**ALWAYS prefer local over global** — locals are faster and prevent scope pollution. + +```lua +-- GOOD +local playerPed = PlayerPedId() +local playerCoords = GetEntityCoords(playerPed) + +-- BAD (global variables) +playerPed = PlayerPedId() +playerCoords = GetEntityCoords(playerPed) +``` + +## Caching + +Cache frequently accessed values to improve performance. + +```lua +-- GOOD: Cache once +local playerPed = PlayerPedId() +local playerCoords = GetEntityCoords(playerPed) + +CreateThread(function() + while condition do + -- Use cached values + local distance = #(playerCoords - targetCoords) + Wait(1000) + end +end) + +-- Listen for ped changes +AddEventHandler('esx:playerPedChanged', function(newPed) + playerPed = newPed +end) + +-- BAD: Call every iteration +CreateThread(function() + while condition do + local distance = #(GetEntityCoords(PlayerPedId()) - targetCoords) + Wait(1000) + end +end) +``` + +### Common caching patterns + +```lua +local playerId = PlayerId() +local serverId = GetPlayerServerId(playerId) +local playerPed = PlayerPedId() + +-- Update on ped change +AddEventHandler('esx:playerPedChanged', function(newPed) + playerPed = newPed +end) +``` + +## Avoid Infinite Loops + +Always include a condition to exit loops. + +```lua +-- BAD: True infinite loop +CreateThread(function() + while true do + Wait(0) + if IsControlJustPressed(0, 38) then + -- Do something + end + end +end) + +-- GOOD: Conditional loop +local function startPoliceThread() + CreateThread(function() + while ESX.PlayerData.job and ESX.PlayerData.job.name == 'police' do + Wait(0) + if IsControlJustPressed(0, 38) then + -- Do something + end + end + end) +end + +-- Start thread when job changes +AddEventHandler('esx:setJob', function(job) + if job.name == 'police' then + startPoliceThread() + end +end) + +-- Check on resource start +if ESX.PlayerData.job and ESX.PlayerData.job.name == 'police' then + startPoliceThread() +end +``` + +## Proper Wait Times + +Don't use `Wait(0)` unless absolutely necessary — adjust based on your needs. + +```lua +-- BAD: Unnecessary high frequency check +CreateThread(function() + while true do + Wait(0) -- Runs every frame! + local health = GetEntityHealth(playerPed) + end +end) + +-- GOOD: Reasonable frequency +CreateThread(function() + while true do + Wait(1000) -- Check every second + local health = GetEntityHealth(playerPed) + end +end) +``` + +## Modern Natives & Methods + +Use modern alternatives over outdated functions. + +```lua +-- Use PlayerPedId() instead of GetPlayerPed(-1) +local ped = PlayerPedId() + +-- Use vector math for distance +local distance = #(vector3(x1, y1, z1) - vector3(x2, y2, z2)) +-- NOT: GetDistanceBetweenCoords(x1, y1, z1, x2, y2, z2, true) + +-- Use joaat() for hashes +local hash = joaat('adder') +-- NOT: GetHashKey('adder') + +-- Use table insertion shortcut +table[#table + 1] = value +-- NOT: table.insert(table, value) + +-- Use direct nil assignment +table[index] = nil +-- NOT: table.remove(table, index) +``` + +## DRY (Don't Repeat Yourself) + +Write reusable code. + +```lua +-- BAD: Repeated code +RegisterNetEvent('myResource:giveItem1') +AddEventHandler('myResource:giveItem1', function() + local xPlayer = ESX.GetPlayerFromId(source) + if not xPlayer then return end + + if xPlayer.canCarryItem('bread', 1) then + xPlayer.addInventoryItem('bread', 1) + xPlayer.showNotification('Received bread', 'success') + end +end) + +RegisterNetEvent('myResource:giveItem2') +AddEventHandler('myResource:giveItem2', function() + local xPlayer = ESX.GetPlayerFromId(source) + if not xPlayer then return end + + if xPlayer.canCarryItem('water', 1) then + xPlayer.addInventoryItem('water', 1) + xPlayer.showNotification('Received water', 'success') + end +end) + +-- GOOD: Reusable function +local function giveItem(playerId, item, count, label) + local xPlayer = ESX.GetPlayerFromId(playerId) + if not xPlayer then return end + + if xPlayer.canCarryItem(item, count) then + xPlayer.addInventoryItem(item, count) + xPlayer.showNotification('Received ' .. label, 'success') + return true + else + xPlayer.showNotification('Inventory full', 'error') + return false + end +end + +RegisterNetEvent('myResource:giveItem') +AddEventHandler('myResource:giveItem', function(item, count, label) + giveItem(source, item, count, label) +end) +``` + +## Comments & Documentation + +Use clear comments for complex logic. + +```lua +---@param playerId number The player's server ID +---@param item string The item name +---@param count number The amount to give +---@return boolean success Whether the item was given +local function giveItem(playerId, item, count) + local xPlayer = ESX.GetPlayerFromId(playerId) + if not xPlayer then return false end + + -- Check if player has inventory space + if not xPlayer.canCarryItem(item, count) then + xPlayer.showNotification('Inventory full', 'error') + return false + end + + xPlayer.addInventoryItem(item, count) + return true +end +``` + +## Security + +### Never trust client data + +```lua +-- BAD: Client sends price +RegisterNetEvent('shop:buyItem') +AddEventHandler('shop:buyItem', function(item, price) + local xPlayer = ESX.GetPlayerFromId(source) + xPlayer.removeMoney(price) -- Client controls price! +end) + +-- GOOD: Server validates price +RegisterNetEvent('shop:buyItem') +AddEventHandler('shop:buyItem', function(item) + local xPlayer = ESX.GetPlayerFromId(source) + if not xPlayer then return end + + local price = Config.Items[item].price -- Server controls price + if xPlayer.getMoney() >= price then + xPlayer.removeMoney(price, 'Bought ' .. item) + xPlayer.addInventoryItem(item, 1) + end +end) +``` + +### Use SecureNetEvent for client events + +```lua +-- CLIENT: Can only be triggered by server +ESX.SecureNetEvent('myResource:rewardPlayer', function(reward) + -- Safe to use, server-only trigger + print('Received reward:', reward) +end) + +-- SERVER: Trigger the secure event +xPlayer.triggerEvent('myResource:rewardPlayer', 1000) +``` + +### Validate everything server-side + +```lua +RegisterNetEvent('garage:takeVehicle') +AddEventHandler('garage:takeVehicle', function(vehiclePlate) + local xPlayer = ESX.GetPlayerFromId(source) + if not xPlayer then return end + + -- Validate player owns this vehicle + MySQL.Async.fetchScalar('SELECT 1 FROM owned_vehicles WHERE plate = @plate AND owner = @owner', { + ['@plate'] = vehiclePlate, + ['@owner'] = xPlayer.identifier + }, function(result) + if result then + -- Player owns vehicle, spawn it + spawnVehicle(source, vehiclePlate) + else + -- Cheater trying to spawn vehicle they don't own + xPlayer.kick('Attempted to spawn vehicle they don\'t own') + end + end) +end) +``` + +## Error Handling + +Always check for nil and handle errors. + +```lua +-- GOOD +local xPlayer = ESX.GetPlayerFromId(source) +if not xPlayer then + print('ERROR: xPlayer is nil for source ' .. source) + return +end + +local itemData = xPlayer.getInventoryItem('bread') +if not itemData then + print('ERROR: Item not found') + return +end + +if itemData.count >= 1 then + xPlayer.removeInventoryItem('bread', 1) +end +``` + +## Indentation + +Use **2 spaces** for indentation (ESX standard). + +```lua +if condition then + if anotherCondition then + doSomething() + end +end +``` + +## Lua 5.4 Features + +If you have Lua 5.4 enabled in your fxmanifest: + +```lua +lua54 'yes' +``` + +You can use: + +```lua +-- Constants +local MAX_DISTANCE = 10.0 + +-- Compound operators +count += 1 +count -= 1 +count *= 2 +count /= 2 + +-- to-be-closed variables +local file = io.open('file.txt', 'r') +``` + +## Folder Structure + +Organize your resource properly: + +``` +myresource/ +├── fxmanifest.lua +├── config.lua +├── client/ +│ ├── main.lua +│ └── menu.lua +├── server/ +│ ├── main.lua +│ └── callbacks.lua +└── shared/ + └── utils.lua +``` + +## Resource Optimization Checklist + +- [ ] Use `local` for all variables unless they must be global +- [ ] Cache frequently accessed values (PlayerPedId, coords, etc.) +- [ ] Use appropriate Wait times (avoid Wait(0) when possible) +- [ ] Use modern natives and methods +- [ ] Avoid infinite loops (add exit conditions) +- [ ] Minimize database queries (batch when possible) +- [ ] Don't create threads unnecessarily +- [ ] Use `exports` over triggers when possible +- [ ] Validate all client data server-side +- [ ] Use ESX.SecureNetEvent for client events +- [ ] Check for nil before using objects +- [ ] Use descriptive variable/function names +- [ ] Add comments for complex logic +- [ ] Follow ESX naming conventions + +## Performance Tips + +1. **Cache ESX object once**: + ```lua + -- Resource start + ESX = exports.es_extended.getSharedObject() + + -- NOT in every function + ``` + +2. **Minimize GetPlayerFromId calls**: + ```lua + -- GOOD + local xPlayer = ESX.GetPlayerFromId(source) + local money = xPlayer.getMoney() + local job = xPlayer.getJob() + + -- BAD + local money = ESX.GetPlayerFromId(source).getMoney() + local job = ESX.GetPlayerFromId(source).getJob() + ``` + +3. **Use GetExtendedPlayers with filters**: + ```lua + -- GOOD + local police = ESX.GetExtendedPlayers('job', 'police') + + -- BAD (loops all players) + local police = {} + for _, xPlayer in pairs(ESX.GetExtendedPlayers()) do + if xPlayer.job.name == 'police' then + table.insert(police, xPlayer) + end + end + ``` + +4. **Batch database operations**: + ```lua + -- GOOD: One query + MySQL.Async.execute('INSERT INTO logs (player, action) VALUES (?, ?), (?, ?)', { + player1, action1, + player2, action2 + }) + + -- BAD: Multiple queries + MySQL.Async.execute('INSERT INTO logs (player, action) VALUES (?, ?)', {player1, action1}) + MySQL.Async.execute('INSERT INTO logs (player, action) VALUES (?, ?)', {player2, action2}) + ``` diff --git a/.agents/skills/esx-framework/client-functions.md b/.agents/skills/esx-framework/client-functions.md new file mode 100644 index 0000000..be53815 --- /dev/null +++ b/.agents/skills/esx-framework/client-functions.md @@ -0,0 +1,256 @@ +# ESX Client Functions + +All functions available on **CLIENT side** via `ESX` object. + +## Player State + +### ESX.IsPlayerLoaded() + +Returns if player has successfully loaded. + +```lua +if ESX.IsPlayerLoaded() then + print('Player is loaded and ready') +end + +-- Common pattern: wait for load +while not ESX.IsPlayerLoaded() do + Wait(250) +end +``` + +### ESX.GetPlayerData() + +Returns `ESX.PlayerData` (same as accessing it directly). + +```lua +local playerData = ESX.GetPlayerData() +print('Player job:', playerData.job.name) +``` + +### ESX.SetPlayerData(key, value) + +Sets player data locally (will be overwritten by server updates). + +```lua +ESX.SetPlayerData('customKey', 'customValue') +print(ESX.PlayerData.customKey) -- 'customValue' +``` + +## Secure Events + +### ESX.SecureNetEvent(name, callback) + +Registers a client event that can ONLY be triggered by server (prevents cheaters). + +```lua +ESX.SecureNetEvent('myResource:giveReward', function(amount) + -- Only server can trigger this + print('Received reward:', amount) +end) + +-- Server side: +TriggerClientEvent('myResource:giveReward', playerId, 1000) +``` + +## Inventory + +### ESX.SearchInventory(items, count) + +Searches player inventory for items. + +```lua +-- Search single item +local breadItem = ESX.SearchInventory('bread') +if breadItem and breadItem.count > 0 then + print('You have', breadItem.count, 'bread') +end + +-- Search multiple items +local items = ESX.SearchInventory({'bread', 'water'}, true) +for itemName, itemData in pairs(items) do + print(itemName, ':', itemData.count) +end +``` + +## Notifications + +ESX has its own notification system, but **USE OX_LIB** for notifications instead: + +```lua +-- Use ox_lib for notifications (preferred) +lib.notify({ + title = 'Bank', + description = 'You received $500', + type = 'success' +}) +``` + +## Input & Controls + +### ESX.RegisterInput(command, label, inputGroup, key, onPress, onRelease) + +Registers a keybind. + +```lua +ESX.RegisterInput('openInventory', 'Open Inventory', 'keyboard', 'f2', + function() + -- Key pressed + print('Opening inventory') + end, + function() + -- Key released (optional) + print('Closed inventory') + end +) +``` + +### ESX.HashString(str) + +Gets input hash/mapping for display (wrongly named, returns input label). + +```lua +local inputLabel = ESX.HashString('openInventory') +ESX.ShowHelpNotification('Press ' .. inputLabel .. ' to open inventory', false, true, 3000) +``` + +## Spawn Management + +### ESX.DisableSpawnManager() + +Disables FiveM's default spawn manager. + +```lua +ESX.DisableSpawnManager() +``` + +### ESX.SpawnPlayer(coords, heading, cb) + +Spawns player at coords with optional callback. + +```lua +local spawnCoords = vector4(100.0, 200.0, 50.0, 90.0) +ESX.SpawnPlayer(spawnCoords, function() + print('Player spawned') +end) +``` + +## Coords & Position + +### ESX.GetAccount(accountName) + +Returns player's account data. + +```lua +local bankAccount = ESX.GetAccount('bank') +print('Bank balance:', bankAccount.money) +``` + +## Vehicle Functions + +### ESX.GetVehicleTypeClient(model) + +Returns vehicle type for model. + +```lua +local vehicleType = ESX.GetVehicleTypeClient('t20') +print('Vehicle type:', vehicleType) -- 'automobile', 'bike', 'boat', 'heli', etc. +``` + +## UI Components (Use ox_lib) + +ESX has its own UI resources, but **USE OX_LIB** for all UI components: + +```lua +-- Progress bars +if lib.progressBar({ + duration = 5000, + label = 'Repairing vehicle...', + useWhileDead = false, + canCancel = true, + disable = { + car = true, + move = true + }, + anim = { + dict = 'mini@repair', + clip = 'fixing_a_player' + } +}) then + print('Repair complete') +end + +-- Context menus +lib.registerContext({ + id = 'player_menu', + title = 'Player Menu', + options = { + { + title = 'Give Money', + icon = 'dollar-sign', + onSelect = function() + -- Handle give money + end + }, + { + title = 'Check ID', + icon = 'id-card', + onSelect = function() + -- Handle check ID + end + } + } +}) + +lib.showContext('player_menu') + +-- Text UI +lib.showTextUI('[E] - Interact', { + position = "right-center" +}) + +lib.hideTextUI() +``` + +## Best Practices + +1. **Always check player loaded before using PlayerData**: + ```lua + if not ESX.IsPlayerLoaded() then return end + ``` + +2. **Cache PlayerData locally when needed**: + ```lua + local job = ESX.PlayerData.job + if job.name == 'police' then + -- Do something + end + ``` + +3. **Use SecureNetEvent for events that modify player state**: + ```lua + -- CLIENT + ESX.SecureNetEvent('myResource:serverAction', function(data) + -- Safe to use, only server can trigger + end) + ``` + +4. **Search inventory before assuming item exists**: + ```lua + local item = ESX.SearchInventory('bread') + if item and item.count > 0 then + -- Player has bread + end + ``` + +5. **Use ox_lib for all UI**: + ```lua + -- Notifications + lib.notify({title = 'Success', description = 'Action completed', type = 'success'}) + + -- Progress bars + lib.progressBar({duration = 5000, label = 'Working...'}) + + -- Context menus + lib.showContext('my_menu') + ``` diff --git a/.agents/skills/esx-framework/core-concepts.md b/.agents/skills/esx-framework/core-concepts.md new file mode 100644 index 0000000..a85ec3b --- /dev/null +++ b/.agents/skills/esx-framework/core-concepts.md @@ -0,0 +1,217 @@ +# ESX Core Concepts + +## Framework Architecture + +ESX Legacy is a **modular framework** that provides: +- Player management and persistence +- Job and economy systems +- Inventory and weapon management +- UI components (menus, notifications, progress bars) +- Database integration (MySQL/MariaDB) + +## Getting ESX Object + +### Client Side + +```lua +-- RECOMMENDED: Using CreateThread +CreateThread(function() + while not ESX do + Wait(100) + end + + while not ESX.IsPlayerLoaded() do + Wait(100) + end + + -- ESX is now available and player is loaded + print('Player loaded:', ESX.PlayerData.firstName, ESX.PlayerData.lastName) +end) +``` + +### Server Side + +```lua +-- ESX is immediately available on server +ESX = exports['es_extended']:getSharedObject() + +-- Or if using newer exports pattern +ESX = exports.es_extended.getSharedObject() +``` + +## PlayerData Structure + +**Available on CLIENT only** via `ESX.PlayerData`: + +```lua +ESX.PlayerData = { + coords = vector3(x, y, z), -- Last known position + ped = PlayerPedId(), -- Player ped handle + group = "user", -- Permission group (user/admin/superadmin) + identifier = "char1:license...", -- Character identifier + ssn = "123-45-6789", -- Social Security Number + inventory = {}, -- Items (table with item name as key) + job = {}, -- Job data (name, label, grade, salary, etc.) + loadout = {}, -- Weapons + name = "John Doe", -- Player name (Steam/FiveM) + playerId = 1, -- Server ID + source = 1, -- Server ID + variables = {}, -- Custom variables set by server + weight = 12, -- Current inventory weight + maxWeight = 24, -- Maximum inventory weight + metadata = {}, -- Custom metadata + admin = false, -- Is admin (based on group) + license = "license:...", -- Rockstar license + dateofbirth = "01/01/2000", -- Character DOB + height = 181, -- Character height + dead = false, -- Is player dead + firstName = "John", -- Character first name + lastName = "Doe", -- Character last name + sex = "m", -- Character gender (m/f) + money = 187, -- Cash amount (use accounts instead) + accounts = { -- Money accounts + money = 187, + bank = 5000, + black_money = 0 + } +} +``` + +## xPlayer Object (Server) + +**Available on SERVER only** — represents a player with methods: + +### Getting xPlayer + +```lua +-- Standard way +local xPlayer = ESX.GetPlayerFromId(source) + +-- By identifier +local xPlayer = ESX.GetPlayerFromIdentifier("license:abc123...") +``` + +### xPlayer contains same data as PlayerData PLUS server-only methods: + +```lua +xPlayer.identifier -- Player identifier +xPlayer.name -- Player name +xPlayer.job -- Job data +xPlayer.accounts -- Money accounts +xPlayer.inventory -- Items +xPlayer.loadout -- Weapons +xPlayer.group -- Permission group +xPlayer.coords -- Last known coords +-- ... many methods (see xplayer-methods.md) +``` + +## Framework Initialization + +### Client Startup Flow + +1. ESX object becomes available +2. Player connects to server +3. Server creates xPlayer +4. Client receives PlayerData via `esx:playerLoaded` event +5. `ESX.PlayerLoaded` becomes true +6. `ESX.PlayerData` is populated + +### Server Startup Flow + +1. ESX loads from database +2. Jobs are loaded (`ESX.Jobs`) +3. Items are loaded (`ESX.Items`) +4. Resources can now use ESX + +## Important Events + +### Client Events + +```lua +-- Player loaded (character selected) +AddEventHandler('esx:playerLoaded', function(playerData) + ESX.PlayerData = playerData +end) + +-- Player data updated +AddEventHandler('esx:updatePlayerData', function(key, value) + ESX.PlayerData[key] = value +end) + +-- Job changed +AddEventHandler('esx:setJob', function(job) + ESX.PlayerData.job = job +end) + +-- Player died +AddEventHandler('esx:onPlayerDeath', function(data) + -- Handle death +end) + +-- Player spawned +AddEventHandler('esx:onPlayerSpawn', function() + -- Handle spawn +end) +``` + +### Server Events + +```lua +-- Player joined (before character selection) +AddEventHandler('esx:onPlayerJoined', function() + local _source = source + -- Player connected +end) + +-- Player loaded (character selected) +AddEventHandler('esx:playerLoaded', function(playerId, xPlayer) + -- xPlayer is now available +end) + +-- Player dropped +AddEventHandler('esx:playerDropped', function(playerId, reason) + -- Player left server +end) +``` + +## Constants and Configuration + +```lua +-- Use UPPERCASE for constants +local MAX_DISTANCE = 10.0 -- Lua 5.4 const +local INTERACTION_KEY = 38 -- E key + +-- Use Config for resource settings +Config = {} +Config.MaxWeight = 24 +Config.EnableSocieties = true +``` + +## Best Practices + +1. **Always wait for player load on client**: + ```lua + while not ESX.IsPlayerLoaded() do + Wait(100) + end + ``` + +2. **Check for nil on server**: + ```lua + local xPlayer = ESX.Player(source) + if not xPlayer then return end + ``` + +3. **Cache ESX object**: + ```lua + -- Do this once at resource start + ESX = exports.es_extended.getSharedObject() + + -- NOT in every function/event + ``` + +4. **Use ESX.GetPlayerFromId**: + ```lua + local xPlayer = ESX.GetPlayerFromId(source) + if not xPlayer then return end + ``` diff --git a/.agents/skills/esx-framework/events-callbacks.md b/.agents/skills/esx-framework/events-callbacks.md new file mode 100644 index 0000000..04ecbfd --- /dev/null +++ b/.agents/skills/esx-framework/events-callbacks.md @@ -0,0 +1,450 @@ +# ESX Events & Callbacks + +## Server Callbacks + +Server callbacks allow the client to request data from the server. + +### Registering a Server Callback + +```lua +-- SERVER +ESX.RegisterServerCallback('myResource:getPlayerData', function(source, cb, additionalParam) + local xPlayer = ESX.GetPlayerFromId(source) + if not xPlayer then return cb(nil) end + + local data = { + money = xPlayer.getMoney(), + job = xPlayer.job.name, + grade = xPlayer.job.grade, + param = additionalParam + } + + cb(data) +end) +``` + +### Calling a Server Callback + +```lua +-- CLIENT +ESX.TriggerServerCallback('myResource:getPlayerData', function(data) + if data then + print('Money:', data.money) + print('Job:', data.job) + end +end, 'extraParam') +``` + +### Common Patterns + +```lua +-- Check if player can afford something +ESX.RegisterServerCallback('shop:canAfford', function(source, cb, itemName) + local xPlayer = ESX.GetPlayerFromId(source) + if not xPlayer then return cb(false) end + + local price = Config.Items[itemName].price + cb(xPlayer.getMoney() >= price) +end) + +-- CLIENT usage +ESX.TriggerServerCallback('shop:canAfford', function(canAfford) + if canAfford then + -- Show buy menu + else + lib.notify({title = 'Shop', description = 'Not enough money', type = 'error'}) + end +end, 'bread') +``` + +## Client Callbacks + +**WARNING**: Client callbacks should NEVER be used for sensitive operations! Client can fake any data. + +### Registering a Client Callback + +```lua +-- CLIENT +ESX.RegisterClientCallback('myResource:getVehicleModel', function(cb, vehicle) + local model = GetEntityModel(vehicle) + cb(model) +end) +``` + +### Calling a Client Callback (Server) + +```lua +-- SERVER +ESX.TriggerClientCallback(source, 'myResource:getVehicleModel', function(model) + print('Vehicle model:', model) +end, vehicleNetId) +``` + +## ESX Events + +### Client Events + +#### esx:playerLoaded + +Fired when player's character loads. + +```lua +-- CLIENT +AddEventHandler('esx:playerLoaded', function(playerData) + ESX.PlayerData = playerData + print('Player loaded:', playerData.firstName, playerData.lastName) + + -- Initialize your resource + startClientScripts() +end) +``` + +#### esx:updatePlayerData + +Fired when any PlayerData is updated. + +```lua +-- CLIENT +AddEventHandler('esx:updatePlayerData', function(key, value) + ESX.PlayerData[key] = value + + if key == 'job' then + print('Job changed to:', value.name) + elseif key == 'money' then + print('Money updated:', value) + end +end) +``` + +#### esx:setJob + +Fired when player's job changes. + +```lua +-- CLIENT +AddEventHandler('esx:setJob', function(job) + ESX.PlayerData.job = job + print('New job:', job.name, 'Grade:', job.grade) + + -- Start/stop job-specific systems + if job.name == 'police' then + startPoliceBlips() + else + stopPoliceBlips() + end +end) +``` + +#### esx:setAccountMoney + +Fired when player's account money changes. + +```lua +-- CLIENT +AddEventHandler('esx:setAccountMoney', function(account) + print('Account updated:', account.name, account.money) +end) +``` + +#### esx:addInventoryItem + +Fired when player receives an item. + +```lua +-- CLIENT +AddEventHandler('esx:addInventoryItem', function(item, count) + print('Received:', count, 'x', item.label) +end) +``` + +#### esx:removeInventoryItem + +Fired when player loses an item. + +```lua +-- CLIENT +AddEventHandler('esx:removeInventoryItem', function(item, count) + print('Removed:', count, 'x', item.label) +end) +``` + +#### esx:onPlayerDeath + +Fired when player dies. + +```lua +-- CLIENT +AddEventHandler('esx:onPlayerDeath', function(data) + print('Player died') + print('Killer:', data.killerServerId) + + -- Respawn logic +end) +``` + +#### esx:onPlayerSpawn + +Fired when player spawns. + +```lua +-- CLIENT +AddEventHandler('esx:onPlayerSpawn', function() + print('Player spawned') +end) +``` + +#### esx:playerPedChanged + +Fired when player ped changes (e.g., after model change). + +```lua +-- CLIENT +local playerPed = PlayerPedId() + +AddEventHandler('esx:playerPedChanged', function(newPed) + playerPed = newPed + print('Ped changed:', newPed) +end) +``` + +### Server Events + +#### esx:onPlayerJoined + +Fired when player connects (before character selection). + +```lua +-- SERVER +AddEventHandler('esx:onPlayerJoined', function() + local _source = source + print('Player connected:', _source) +end) +``` + +#### esx:playerLoaded + +Fired when player's character loads. + +```lua +-- SERVER +AddEventHandler('esx:playerLoaded', function(playerId, xPlayer) + print('Player loaded:', xPlayer.getName()) + + -- Give welcome bonus + if xPlayer.getMeta('firstTime') == nil then + xPlayer.addMoney(5000, 'Welcome bonus') + xPlayer.setMeta('firstTime', false) + end +end) +``` + +#### esx:playerDropped + +Fired when player disconnects. + +```lua +-- SERVER +AddEventHandler('esx:playerDropped', function(playerId, reason) + print('Player ' .. playerId .. ' left:', reason) +end) +``` + +#### esx:setJob + +Fired when player's job changes (server-side). + +```lua +-- SERVER +AddEventHandler('esx:setJob', function(playerId, job, lastJob) + local xPlayer = ESX.GetPlayerFromId(playerId) + if not xPlayer then return end + + print(xPlayer.getName() .. ' changed from ' .. lastJob.name .. ' to ' .. job.name) + + -- Log job change + MySQL.Async.execute('INSERT INTO job_changes (identifier, old_job, new_job) VALUES (@identifier, @old, @new)', { + ['@identifier'] = xPlayer.identifier, + ['@old'] = lastJob.name, + ['@new'] = job.name + }) +end) +``` + +## Secure Net Events + +Use SecureNetEvent for client events that should only be triggered by server. + +### Registering Secure Net Event + +```lua +-- CLIENT +ESX.SecureNetEvent('myResource:giveReward', function(amount, reason) + -- Only server can trigger this + print('Received reward:', amount, reason) + + lib.notify({ + title = 'Reward', + description = 'You received $' .. amount .. ' for ' .. reason, + type = 'success' + }) +end) +``` + +### Triggering Secure Net Event + +```lua +-- SERVER +local xPlayer = ESX.GetPlayerFromId(source) +xPlayer.triggerEvent('myResource:giveReward', 500, 'completing mission') +``` + +## Custom Events + +### Triggering Client Event from Server + +```lua +-- SERVER +local xPlayer = ESX.GetPlayerFromId(source) +xPlayer.triggerEvent('myResource:openMenu', menuData) + +-- Or using TriggerClientEvent +TriggerClientEvent('myResource:openMenu', source, menuData) + +-- Or for multiple players +local officers = ESX.GetExtendedPlayers('job', 'police') +for i, xPlayer in ipairs(officers) do + xPlayer.triggerEvent('myResource:alert', 'Code 3 at Legion Square') +end +``` + +### Triggering Server Event from Client + +```lua +-- CLIENT +TriggerServerEvent('myResource:buyItem', 'bread') +``` + +### Receiving Custom Events + +```lua +-- CLIENT +RegisterNetEvent('myResource:openMenu') +AddEventHandler('myResource:openMenu', function(menuData) + -- Open menu with data +end) + +-- SERVER +RegisterNetEvent('myResource:buyItem') +AddEventHandler('myResource:buyItem', function(itemName) + local xPlayer = ESX.GetPlayerFromId(source) + if not xPlayer then return end + + -- Validate and process purchase + local price = Config.Items[itemName].price + if xPlayer.getMoney() >= price then + xPlayer.removeMoney(price, 'Bought ' .. itemName) + xPlayer.addInventoryItem(itemName, 1) + end +end) +``` + +## Best Practices + +1. **Always validate server-side**: + ```lua + -- BAD: Trust client data + RegisterNetEvent('shop:buy') + AddEventHandler('shop:buy', function(price) + local xPlayer = ESX.GetPlayerFromId(source) + xPlayer.removeMoney(price) -- Client controls price! + end) + + -- GOOD: Server validates + RegisterNetEvent('shop:buy') + AddEventHandler('shop:buy', function(itemName) + local xPlayer = ESX.GetPlayerFromId(source) + if not xPlayer then return end + + local price = Config.Items[itemName].price + if xPlayer.getMoney() >= price then + xPlayer.removeMoney(price, 'Bought ' .. itemName) + xPlayer.addInventoryItem(itemName, 1) + end + end) + ``` + +2. **Use callbacks for data requests**: + ```lua + -- GOOD: Use callback + ESX.TriggerServerCallback('shop:canAfford', function(canAfford) + if canAfford then + -- Do something + end + end, 'bread') + + -- BAD: Use event + TriggerServerEvent('shop:checkAfford', 'bread') + RegisterNetEvent('shop:affordResult') + AddEventHandler('shop:affordResult', function(canAfford) + -- Client can fake this event + end) + ``` + +3. **Use SecureNetEvent for important client events**: + ```lua + -- CLIENT + ESX.SecureNetEvent('police:giveArmor', function() + SetPedArmour(PlayerPedId(), 100) + end) + + -- SERVER (validated) + local xPlayer = ESX.GetPlayerFromId(source) + if xPlayer.job.name == 'police' then + xPlayer.triggerEvent('police:giveArmor') + end + ``` + +4. **Always check for nil**: + ```lua + RegisterNetEvent('myResource:action') + AddEventHandler('myResource:action', function() + local xPlayer = ESX.GetPlayerFromId(source) + if not xPlayer then return end + + -- Safe to use xPlayer + end) + ``` + +5. **Use proper event naming**: + ```lua + -- GOOD: Descriptive names + 'myResource:openShopMenu' + 'myResource:buyItem' + 'myResource:sellItem' + + -- BAD: Vague names + 'myResource:event1' + 'myResource:action' + 'openMenu' + ``` + +6. **Listen to ESX events for state changes**: + ```lua + -- CLIENT: React to job changes + AddEventHandler('esx:setJob', function(job) + if job.name == 'police' then + startPoliceFeatures() + else + stopPoliceFeatures() + end + end) + + -- Check on resource start too + CreateThread(function() + while not ESX.IsPlayerLoaded() do Wait(100) end + + if ESX.PlayerData.job.name == 'police' then + startPoliceFeatures() + end + end) + ``` diff --git a/.agents/skills/esx-framework/reference-links.md b/.agents/skills/esx-framework/reference-links.md new file mode 100644 index 0000000..13773a1 --- /dev/null +++ b/.agents/skills/esx-framework/reference-links.md @@ -0,0 +1,137 @@ +# ESX Framework Reference Links + +## Official Documentation + +- **Main Documentation**: https://docs.esx-framework.org/en +- **GitHub Repository**: https://github.com/esx-framework/esx_core +- **Discord Community**: https://discord.esx-framework.org + +## Core Documentation + +### Getting Started +- **Introduction**: https://docs.esx-framework.org/en +- **ESX Core Overview**: https://docs.esx-framework.org/en/esx_core +- **Best Coding Practices**: https://docs.esx-framework.org/en/tutorial/coding_practices +- **Developing a Script**: https://docs.esx-framework.org/en/tutorial/developing + +### Client-Side +- **Client Functions**: https://docs.esx-framework.org/en/esx_core/es_extended/client/functions +- **Client Events**: https://docs.esx-framework.org/en/esx_core/es_extended/events/client +- **Client Modules**: https://docs.esx-framework.org/en/esx_core/es_extended/client + - **Callback**: https://docs.esx-framework.org/en/esx_core/es_extended/client/modules/callback + - **ESX.Game**: https://docs.esx-framework.org/en/esx_core/es_extended/client/modules/game + - **Streaming**: https://docs.esx-framework.org/en/esx_core/es_extended/client/modules/streaming + - **Scaleform**: https://docs.esx-framework.org/en/esx_core/es_extended/client/modules/scaleform + +### Server-Side +- **Server Functions**: https://docs.esx-framework.org/en/esx_core/es_extended/server/functions +- **Server Events**: https://docs.esx-framework.org/en/esx_core/es_extended/events/server +- **xPlayer Functions**: https://docs.esx-framework.org/en/esx_core/es_extended/server/xplayer +- **OneSync**: https://docs.esx-framework.org/en/esx_core/es_extended/server/onesync + +### Shared +- **PlayerData Structure**: https://docs.esx-framework.org/en/esx_core/es_extended/playerdata +- **Shared Functions**: https://docs.esx-framework.org/en/esx_core/es_extended/shared + - **Math**: https://docs.esx-framework.org/en/esx_core/es_extended/shared/math + - **Table**: https://docs.esx-framework.org/en/esx_core/es_extended/shared/table + - **Timeout**: https://docs.esx-framework.org/en/esx_core/es_extended/shared/timeout + +### Configuration +- **Main Config**: https://docs.esx-framework.org/en/esx_core/es_extended/config/main +- **Discord Logs**: https://docs.esx-framework.org/en/esx_core/es_extended/config/logs +- **Weapon Config**: https://docs.esx-framework.org/en/esx_core/es_extended/config/weapon +- **Adjustments**: https://docs.esx-framework.org/en/esx_core/es_extended/config/adjustments + +### Commands +- **Commands Documentation**: https://docs.esx-framework.org/en/esx_core/es_extended/commands + +## UI Components + +- **esx_context**: https://docs.esx-framework.org/en/esx_core/esx_context +- **esx_notify**: https://docs.esx-framework.org/en/esx_core/esx_notify +- **esx_progressbar**: https://docs.esx-framework.org/en/esx_core/esx_progressbar +- **esx_textui**: https://docs.esx-framework.org/en/esx_core/esx_textui +- **esx_menu_default**: https://docs.esx-framework.org/en/esx_core/esx_menu_default +- **esx_menu_dialog**: https://docs.esx-framework.org/en/esx_core/esx_menu_dialog +- **esx_menu_list**: https://docs.esx-framework.org/en/esx_core/esx_menu_list + +## Player Systems + +- **esx_identity**: https://docs.esx-framework.org/en/esx_core/esx_identity +- **esx_multicharacter**: https://docs.esx-framework.org/en/esx_core/esx_multicharacter +- **esx_skin**: https://docs.esx-framework.org/en/esx_core/esx_skin +- **skinchanger**: https://docs.esx-framework.org/en/esx_core/skinchanger + +## Popular Addons + +### Jobs +- **Police Job**: https://docs.esx-framework.org/en/esx_addons/esx_policejob +- **Ambulance Job**: https://docs.esx-framework.org/en/esx_addons/esx_ambulancejob +- **Mechanic Job**: https://docs.esx-framework.org/en/esx_addons/esx_mechanicjob +- **Taxi Job**: https://docs.esx-framework.org/en/esx_addons/esx_taxijob +- **Generic Jobs**: https://docs.esx-framework.org/en/esx_addons/esx_jobs + +### Economy & Shops +- **Banking**: https://docs.esx-framework.org/en/esx_addons/esx_banking +- **Shops**: https://docs.esx-framework.org/en/esx_addons/esx_shops +- **Weapon Shop**: https://docs.esx-framework.org/en/esx_addons/esx_weaponshop +- **Vehicle Shop**: https://docs.esx-framework.org/en/esx_addons/esx_vehicleshop +- **Clothe Shop**: https://docs.esx-framework.org/en/esx_addons/esx_clotheshop +- **LS Custom**: https://docs.esx-framework.org/en/esx_addons/esx_lscustom +- **Barbershop**: https://docs.esx-framework.org/en/esx_addons/esx_barbershop + +### Systems +- **Billing**: https://docs.esx-framework.org/en/esx_addons/esx_billing +- **License**: https://docs.esx-framework.org/en/esx_addons/esx_license +- **Society**: https://docs.esx-framework.org/en/esx_addons/esx_society +- **Datastore**: https://docs.esx-framework.org/en/esx_addons/esx_datastore +- **Service**: https://docs.esx-framework.org/en/esx_addons/esx_service +- **Status**: https://docs.esx-framework.org/en/esx_addons/esx_status +- **Basic Needs**: https://docs.esx-framework.org/en/esx_addons/esx_basicneeds + +### Vehicles & Property +- **Garage**: https://docs.esx-framework.org/en/esx_addons/esx_garage +- **Property**: https://docs.esx-framework.org/en/esx_addons/esx_property +- **DMV School**: https://docs.esx-framework.org/en/esx_addons/esx_dmvschool + +### Other +- **HUD**: https://docs.esx-framework.org/en/esx_addons/esx_hud +- **Animations**: https://docs.esx-framework.org/en/esx_addons/esx_animations +- **RP Chat**: https://docs.esx-framework.org/en/esx_addons/esx_rpchat +- **Job Listing**: https://docs.esx-framework.org/en/esx_addons/esx_joblisting + +## Troubleshooting + +- **Common Issues**: https://docs.esx-framework.org/en/troubleshoot +- **GitHub Issues**: https://github.com/esx-framework/esx_core/issues + +## Community Resources + +- **Discord**: https://discord.esx-framework.org (13,000+ members) +- **GitHub Organization**: https://github.com/esx-framework +- **Official Website**: https://esx-framework.org + +## Related Documentation + +### FiveM Documentation +- **Natives Reference**: https://docs.fivem.net/natives/ +- **Scripting Reference**: https://docs.fivem.net/docs/scripting-reference/ +- **Resource Manifest**: https://docs.fivem.net/docs/scripting-reference/resource-manifest/resource-manifest/ + +### Database (oxmysql) +- **oxmysql Documentation**: https://overextended.dev/oxmysql + +### UI Library (ox_lib) +- **ox_lib Documentation**: https://overextended.dev/ox_lib + +## Quick Access + +When helping developers, reference these pages for: + +- **Getting Started**: https://docs.esx-framework.org/en/tutorial/developing +- **Client Functions**: https://docs.esx-framework.org/en/esx_core/es_extended/client/functions +- **Server Functions**: https://docs.esx-framework.org/en/esx_core/es_extended/server/functions +- **xPlayer Methods**: https://docs.esx-framework.org/en/esx_core/es_extended/server/xplayer +- **PlayerData Structure**: https://docs.esx-framework.org/en/esx_core/es_extended/playerdata +- **Best Practices**: https://docs.esx-framework.org/en/tutorial/coding_practices +- **Coding Annotations**: https://docs.esx-framework.org/en/tutorial/coding_practices/annotations diff --git a/.agents/skills/esx-framework/server-functions.md b/.agents/skills/esx-framework/server-functions.md new file mode 100644 index 0000000..9d9e9bb --- /dev/null +++ b/.agents/skills/esx-framework/server-functions.md @@ -0,0 +1,464 @@ +# ESX Server Functions + +All functions available on **SERVER side** via `ESX` object. + +## Player Retrieval + +### ESX.GetPlayerFromId(source) + +Returns xPlayer object for player. + +```lua +local xPlayer = ESX.GetPlayerFromId(source) +if not xPlayer then return end + +print('Player name:', xPlayer.getName()) +``` + +### ESX.GetPlayerFromIdentifier(identifier) + +Returns xPlayer object by identifier. + +```lua +local xPlayer = ESX.GetPlayerFromIdentifier('license:abc123...') +if not xPlayer then return end +``` + +### ESX.GetExtendedPlayers(key, value) + +Returns multiple players matching filter. + +```lua +-- Get all police officers +local policeOfficers = ESX.GetExtendedPlayers('job', 'police') +for i, xPlayer in ipairs(policeOfficers) do + print('Officer:', xPlayer.getName()) +end + +-- Get ALL players (no filter) +local allPlayers = ESX.GetExtendedPlayers() +for i, xPlayer in ipairs(allPlayers) do + print(xPlayer.getName()) +end +``` + +### ESX.GetNumPlayers(key, value) + +Returns number of players matching filter. + +```lua +local policeCount = ESX.GetNumPlayers('job', 'police') +print('Police online:', policeCount) + +local totalPlayers = ESX.GetNumPlayers() +print('Total players:', totalPlayers) +``` + +## Player Identification + +### ESX.GetIdentifier(playerId) + +Returns player's identifier (license with char prefix). + +```lua +local identifier = ESX.GetIdentifier(source) +print('Player identifier:', identifier) +-- Output: "char1:license:abc123..." +``` + +## Callbacks + +### ESX.RegisterServerCallback(name, cb) + +Registers a server callback that client can trigger. + +```lua +ESX.RegisterServerCallback('myResource:getData', function(source, cb, param1) + local xPlayer = ESX.GetPlayerFromId(source) + if not xPlayer then return cb(nil) end + + -- Do server logic + local data = { + money = xPlayer.getMoney(), + job = xPlayer.job.name + } + + cb(data) +end) + +-- Client calls it: +-- ESX.TriggerServerCallback('myResource:getData', function(data) +-- print(data.money, data.job) +-- end, 'param1') +``` + +### ESX.TriggerClientCallback(playerId, name, cb, ...) + +Triggers a client callback and waits for response. + +**WARNING**: Never trust client data for sensitive operations! + +```lua +ESX.TriggerClientCallback(source, 'esx:getVehicleType', function(vehicleType) + print('Vehicle type:', vehicleType) +end, 'bati') +``` + +### ESX.AwaitClientCallback(playerId, name, ...) + +Triggers client callback and waits (blocking). + +**WARNING**: Never trust client data for sensitive operations! + +```lua +local vehicleType = ESX.AwaitClientCallback(source, 'esx:getVehicleType', 'bati') +print('Vehicle type:', vehicleType) +``` + +## Commands + +### ESX.RegisterCommand(name, group, cb, allowConsole, suggestion) + +Registers a command with permission check. + +```lua +ESX.RegisterCommand('heal', 'admin', function(xPlayer, args, showError) + xPlayer.triggerEvent('esx_ambulancejob:heal', 'full') +end, false, { + help = 'Heal yourself', + arguments = {} +}) + +-- With arguments +ESX.RegisterCommand('givemoney', 'admin', function(xPlayer, args, showError) + local targetPlayer = ESX.GetPlayerFromId(args.playerId) + if not targetPlayer then + return showError('Player not found') + end + + targetPlayer.addMoney(args.amount, 'Admin gave money') + xPlayer.showNotification('Gave $' .. args.amount .. ' to ' .. targetPlayer.getName()) +end, false, { + help = 'Give money to player', + arguments = { + {name = 'playerId', help = 'Player ID', type = 'playerId'}, + {name = 'amount', help = 'Amount', type = 'number'} + } +}) +``` + +## Jobs + +### ESX.GetJobs() + +Returns all registered jobs. + +```lua +local jobs = ESX.GetJobs() +for jobName, jobData in pairs(jobs) do + print('Job:', jobName, jobData.label) + for grade, gradeData in pairs(jobData.grades) do + print(' Grade:', gradeData.label, 'Salary:', gradeData.salary) + end +end +``` + +### ESX.DoesJobExist(job, grade) + +Checks if job and grade exist. + +```lua +if ESX.DoesJobExist('police', 4) then + print('Police chief grade exists') +end +``` + +### ESX.CreateJob(name, label, grades) + +Creates a new job and inserts into database. + +```lua +ESX.CreateJob('baker', 'Baker', { + {grade = 0, name = 'apprentice', label = 'Apprentice', salary = 320}, + {grade = 1, name = 'employee', label = 'Employee', salary = 470}, + {grade = 2, name = 'manager', label = 'Manager', salary = 610}, + {grade = 3, name = 'boss', label = 'Boss', salary = 910} +}) +``` + +### ESX.RefreshJobs() + +Reloads jobs from database. + +```lua +-- After manually editing jobs in database +ESX.RefreshJobs() +``` + +## Items + +### ESX.GetItems() + +Returns all registered items. + +```lua +local items = ESX.GetItems() +for itemName, itemData in pairs(items) do + print('Item:', itemName, itemData.label, 'Weight:', itemData.weight) +end +``` + +### ESX.GetItemLabel(item) + +Returns item label. + +```lua +local label = ESX.GetItemLabel('bread') +print('Item label:', label) -- 'Bread' +``` + +### ESX.AddItems(items) + +Adds new items to database and ESX.Items (only if using default inventory). + +```lua +ESX.AddItems({ + {name = 'energy_drink', label = 'Energy Drink', weight = 1, rare = false, canRemove = true}, + {name = 'diamond_ring', label = 'Diamond Ring', weight = 2, rare = true} +}) +``` + +### ESX.RefreshItems() + +Reloads items from database (only if using default inventory). + +```lua +ESX.RefreshItems() +``` + +### ESX.RegisterUsableItem(item, cb) + +Registers an item as usable. + +```lua +ESX.RegisterUsableItem('bread', function(playerId) + local xPlayer = ESX.GetPlayerFromId(playerId) + if not xPlayer then return end + + xPlayer.removeInventoryItem('bread', 1) + + -- Heal player or do something + TriggerClientEvent('esx_status:add', playerId, 'hunger', 200000) + xPlayer.showNotification('You ate bread', 'success') +end) +``` + +### ESX.UseItem(source, item, ...) + +Forces player to use item. + +```lua +ESX.UseItem(source, 'bread') +``` + +### ESX.GetUsableItems() + +Returns all usable items. + +```lua +local usableItems = ESX.GetUsableItems() +for itemName, isUsable in pairs(usableItems) do + if isUsable then + print('Usable:', itemName) + end +end +``` + +## Vehicle Functions + +### ESX.GetVehicleType(model, playerId, cb) + +Returns vehicle type (server must ask client). + +```lua +-- With callback +ESX.GetVehicleType('t20', source, function(vehicleType) + print('Vehicle type:', vehicleType) +end) + +-- With promise (blocking) +local vehicleType = ESX.GetVehicleType('t20', source) +print('Vehicle type:', vehicleType) +``` + +## Pickups (Default Inventory Only) + +### ESX.CreatePickup(type, name, count, label, playerId, components, tintIndex) + +Creates a pickup at player's position. + +```lua +-- Item pickup +ESX.CreatePickup('item_standard', 'bread', 5, 'Bread', source) + +-- Money pickup +ESX.CreatePickup('item_money', 'money', 500, 'Cash', source) + +-- Weapon pickup +ESX.CreatePickup('item_weapon', 'WEAPON_PISTOL', 50, 'Pistol', source, {}, 0) +``` + +## Events + +### ESX.TriggerClientEvent(eventName, playerIds, ...) + +Triggers event for one or multiple players. + +```lua +-- Single player +ESX.TriggerClientEvent('myResource:notify', source, 'Hello!') + +-- Multiple players +local officers = ESX.GetExtendedPlayers('job', 'police') +local officerIds = {} +for i, xPlayer in ipairs(officers) do + table.insert(officerIds, xPlayer.source) +end + +ESX.TriggerClientEvent('myResource:alert', officerIds, 'Code 3!') +``` + +## Discord Logs + +### ESX.DiscordLog(webhookName, title, color, message) + +Sends simple Discord log. + +```lua +ESX.DiscordLog('UserActions', 'Player Joined', 'green', 'John Doe joined the server') +``` + +### ESX.DiscordLogFields(webhookName, title, color, fields) + +Sends Discord log with fields. + +```lua +ESX.DiscordLogFields('AdminActions', '/givemoney Used', 'orange', { + {name = 'Admin', value = xPlayer.getName(), inline = true}, + {name = 'Target', value = targetPlayer.getName(), inline = true}, + {name = 'Amount', value = '$5000', inline = true} +}) +``` + +## Player Function Overrides + +### ESX.RegisterPlayerFunctionOverrides(index, overrides) + +Adds custom functions to xPlayer object. + +```lua +local leoJobs = {'police', 'sheriff', 'fbi'} +local medicJobs = {'ambulance', 'doctor', 'firefighter'} + +ESX.RegisterPlayerFunctionOverrides('customFunctions', { + isLeo = function(self) + return table.contains(leoJobs, self.job.name) + end, + isMedic = function(self) + return table.contains(medicJobs, self.job.name) + end +}) + +-- Now you can use: +-- if xPlayer.isLeo() then ... end +``` + +### ESX.SetPlayerFunctionOverride(index) + +Switches active override set. + +```lua +ESX.SetPlayerFunctionOverride('customFunctions') +``` + +## Debug + +### ESX.Trace(msg) + +Prints trace message when Debug is enabled in config. + +```lua +ESX.Trace('Player ' .. xPlayer.getName() .. ' opened inventory') +``` + +## Best Practices + +1. **Always check for nil**: + ```lua + local xPlayer = ESX.GetPlayerFromId(source) + if not xPlayer then return end + ``` + +2. **Use GetExtendedPlayers for filtered player lists**: + ```lua + -- GOOD - Get specific job + local officers = ESX.GetExtendedPlayers('job', 'police') + + -- BAD - Loop through all then filter + for _, xPlayer in pairs(ESX.GetExtendedPlayers()) do + if xPlayer.job.name == 'police' then + -- Less efficient + end + end + ``` + +3. **Never trust client data**: + ```lua + -- BAD + RegisterNetEvent('myResource:buyItem') + AddEventHandler('myResource:buyItem', function(price) + -- Client controls price! Bad! + local xPlayer = ESX.GetPlayerFromId(source) + xPlayer.removeMoney(price) + end) + + -- GOOD + RegisterNetEvent('myResource:buyItem') + AddEventHandler('myResource:buyItem', function(itemName) + local xPlayer = ESX.GetPlayerFromId(source) + if not xPlayer then return end + + local price = Config.Items[itemName].price -- Server controls price + if xPlayer.getMoney() >= price then + xPlayer.removeMoney(price, 'Bought ' .. itemName) + xPlayer.addInventoryItem(itemName, 1) + end + end) + ``` + +4. **Use callbacks for client-to-server data requests**: + ```lua + ESX.RegisterServerCallback('myResource:canBuy', function(source, cb, itemName) + local xPlayer = ESX.GetPlayerFromId(source) + if not xPlayer then return cb(false) end + + local price = Config.Items[itemName].price + cb(xPlayer.getMoney() >= price) + end) + ``` + +5. **Cache xPlayer when using multiple times**: + ```lua + -- GOOD + local xPlayer = ESX.GetPlayerFromId(source) + if not xPlayer then return end + + local money = xPlayer.getMoney() + local job = xPlayer.job.name + local inventory = xPlayer.inventory + + -- BAD (calls GetPlayerFromId 3 times) + local money = ESX.GetPlayerFromId(source).getMoney() + local job = ESX.GetPlayerFromId(source).job.name + local inventory = ESX.GetPlayerFromId(source).inventory + ``` diff --git a/.agents/skills/esx-framework/xplayer-methods.md b/.agents/skills/esx-framework/xplayer-methods.md new file mode 100644 index 0000000..9055506 --- /dev/null +++ b/.agents/skills/esx-framework/xplayer-methods.md @@ -0,0 +1,624 @@ +# xPlayer Methods (Server Only) + +The xPlayer object represents a player on the **SERVER** with many useful methods. + +## Getting xPlayer + +```lua +local xPlayer = ESX.GetPlayerFromId(source) +if not xPlayer then return end +``` + +## Basic Info + +### xPlayer.getName() + +Returns player's name. + +```lua +print('Player name:', xPlayer.getName()) +``` + +### xPlayer.getIdentifier() + +Returns player's identifier (with char prefix). + +```lua +print('Identifier:', xPlayer.getIdentifier()) +-- Output: "char1:license:abc123..." +``` + +### xPlayer.getSSN() + +Returns player's Social Security Number. + +```lua +print('SSN:', xPlayer.getSSN()) +-- Output: "123-45-6789" +``` + +### xPlayer.setName(name) + +Sets player's name. + +```lua +xPlayer.setName('John Doe') +``` + +## Coordinates + +### xPlayer.getCoords(vector) + +Returns player's last known coordinates. + +```lua +-- As table +local coords = xPlayer.getCoords() +print(coords.x, coords.y, coords.z, coords.heading) + +-- As vector3 +local coords = xPlayer.getCoords(true) +local distance = #(coords - vector3(0, 0, 0)) +``` + +### xPlayer.setCoords(coords) + +Teleports player to coordinates. + +```lua +xPlayer.setCoords(vector3(100.0, 200.0, 50.0)) + +-- Or vector4 with heading +xPlayer.setCoords(vector4(100.0, 200.0, 50.0, 90.0)) +``` + +### xPlayer.kick(reason) + +Kicks player from server. + +```lua +xPlayer.kick('You have been kicked') +``` + +## Job Management + +### xPlayer.getJob() + +Returns player's job data. + +```lua +local job = xPlayer.getJob() +print('Job:', job.name) +print('Grade:', job.grade) +print('Label:', job.label) +print('Salary:', job.grade_salary) +print('On Duty:', job.onDuty) +``` + +### xPlayer.setJob(name, grade, onDuty) + +Sets player's job. + +```lua +-- Set job with default duty state +xPlayer.setJob('police', 4) + +-- Set job and duty state +xPlayer.setJob('police', 4, true) -- On duty +xPlayer.setJob('police', 4, false) -- Off duty +``` + +## Money Management + +### xPlayer.getMoney() + +Returns cash amount. + +```lua +local cash = xPlayer.getMoney() +print('Cash:', cash) +``` + +### xPlayer.addMoney(amount, reason) + +Adds cash. + +```lua +xPlayer.addMoney(500, 'Sold apples') +``` + +### xPlayer.removeMoney(amount, reason) + +Removes cash. + +```lua +if xPlayer.getMoney() >= 500 then + xPlayer.removeMoney(500, 'Bought item') +end +``` + +### xPlayer.setMoney(amount) + +Sets cash to exact amount. + +```lua +xPlayer.setMoney(1000) +``` + +## Account Management + +### xPlayer.getAccounts(minimal) + +Returns all accounts. + +```lua +-- Full data +local accounts = xPlayer.getAccounts() +print('Bank:', accounts.bank.money) +print('Cash:', accounts.money.money) + +-- Minimal (just amounts) +local accounts = xPlayer.getAccounts(true) +print('Bank:', accounts.bank) +print('Cash:', accounts.money) +``` + +### xPlayer.getAccount(accountName) + +Returns specific account. + +```lua +local bankAccount = xPlayer.getAccount('bank') +print('Bank balance:', bankAccount.money) +``` + +### xPlayer.addAccountMoney(account, amount, reason) + +Adds money to account. + +```lua +xPlayer.addAccountMoney('bank', 5000, 'Paycheck received') +``` + +### xPlayer.removeAccountMoney(account, amount, reason) + +Removes money from account. + +```lua +if xPlayer.getAccount('bank').money >= 2000 then + xPlayer.removeAccountMoney('bank', 2000, 'Paid bills') +end +``` + +### xPlayer.setAccountMoney(account, amount, reason) + +Sets account to exact amount. + +```lua +xPlayer.setAccountMoney('bank', 10000, 'Admin action') +``` + +## Paycheck Management + +### xPlayer.togglePaycheck(toggle) + +Enable/disable paycheck. + +```lua +xPlayer.togglePaycheck(false) -- Disable paycheck +xPlayer.togglePaycheck(true) -- Enable paycheck +``` + +### xPlayer.isPaycheckEnabled() + +Check if paycheck is enabled. + +```lua +if xPlayer.isPaycheckEnabled() then + print('Paycheck is enabled') +end +``` + +## Inventory (Default ESX Inventory) + +### xPlayer.getInventory(minimal) + +Returns player inventory. + +```lua +-- Full data +local inventory = xPlayer.getInventory() +for itemName, itemData in pairs(inventory) do + print(itemName, itemData.count, itemData.weight) +end + +-- Minimal (just counts) +local inventory = xPlayer.getInventory(true) +for itemName, count in pairs(inventory) do + print(itemName, count) +end +``` + +### xPlayer.getInventoryItem(item) + +Returns specific item data. + +```lua +local breadItem = xPlayer.getInventoryItem('bread') +print('Bread count:', breadItem.count) +print('Bread weight:', breadItem.weight) +``` + +### xPlayer.addInventoryItem(item, count) + +Adds item to inventory. + +```lua +xPlayer.addInventoryItem('bread', 5) +``` + +### xPlayer.removeInventoryItem(item, count) + +Removes item from inventory. + +```lua +xPlayer.removeInventoryItem('bread', 2) +``` + +### xPlayer.setInventoryItem(item, count) + +Sets item to exact count. + +```lua +xPlayer.setInventoryItem('bread', 10) +``` + +### xPlayer.hasItem(item) + +Checks if player has item. + +```lua +if xPlayer.hasItem('bread') then + print('Player has bread') +end +``` + +### xPlayer.getWeight() + +Returns current inventory weight. + +```lua +print('Current weight:', xPlayer.getWeight()) +``` + +### xPlayer.getMaxWeight() + +Returns max inventory weight. + +```lua +print('Max weight:', xPlayer.getMaxWeight()) +``` + +### xPlayer.setMaxWeight(weight) + +Sets max inventory weight. + +```lua +xPlayer.setMaxWeight(50) -- Backpack equipped +``` + +### xPlayer.canCarryItem(item, count) + +Checks if player can carry item. + +```lua +if xPlayer.canCarryItem('bread', 5) then + xPlayer.addInventoryItem('bread', 5) +else + xPlayer.showNotification('Inventory full', 'error') +end +``` + +### xPlayer.canSwapItem(firstItem, firstCount, secondItem, secondCount) + +Checks if items can be swapped. + +```lua +if xPlayer.canSwapItem('bread', 5, 'water', 3) then + xPlayer.removeInventoryItem('bread', 5) + xPlayer.addInventoryItem('water', 3) +end +``` + +## Weapon Management (Default ESX) + +### xPlayer.getLoadout(minimal) + +Returns player's weapons. + +```lua +-- Full data +local loadout = xPlayer.getLoadout() +for weaponName, weaponData in pairs(loadout) do + print(weaponName, weaponData.ammo, weaponData.components) +end + +-- Minimal (just ammo and components) +local loadout = xPlayer.getLoadout(true) +for weaponName, weaponData in pairs(loadout) do + print(weaponName, weaponData.ammo) +end +``` + +### xPlayer.getWeapon(weaponName) + +Returns specific weapon data. + +```lua +local pistol = xPlayer.getWeapon('WEAPON_PISTOL') +if pistol then + print('Ammo:', pistol.ammo) + print('Components:', json.encode(pistol.components)) +end +``` + +### xPlayer.hasWeapon(weaponName) + +Checks if player has weapon. + +```lua +if xPlayer.hasWeapon('WEAPON_PISTOL') then + print('Player has pistol') +end +``` + +### xPlayer.addWeapon(weaponName, ammo) + +Gives weapon to player. + +```lua +xPlayer.addWeapon('WEAPON_PISTOL', 250) +``` + +### xPlayer.removeWeapon(weaponName) + +Removes weapon from player. + +```lua +xPlayer.removeWeapon('WEAPON_PISTOL') +``` + +### xPlayer.addWeaponAmmo(weaponName, ammo) + +Adds ammo to weapon. + +```lua +xPlayer.addWeaponAmmo('WEAPON_PISTOL', 50) +``` + +### xPlayer.removeWeaponAmmo(weaponName, ammo) + +Removes ammo from weapon. + +```lua +xPlayer.removeWeaponAmmo('WEAPON_PISTOL', 25) +``` + +### xPlayer.updateWeaponAmmo(weaponName, ammo) + +Sets weapon ammo to exact amount. + +```lua +xPlayer.updateWeaponAmmo('WEAPON_PISTOL', 100) +``` + +### xPlayer.addWeaponComponent(weaponName, component) + +Adds component to weapon. + +```lua +xPlayer.addWeaponComponent('WEAPON_PISTOL', 'suppressor') +``` + +### xPlayer.removeWeaponComponent(weaponName, component) + +Removes component from weapon. + +```lua +xPlayer.removeWeaponComponent('WEAPON_PISTOL', 'suppressor') +``` + +### xPlayer.hasWeaponComponent(weaponName, component) + +Checks if weapon has component. + +```lua +if xPlayer.hasWeaponComponent('WEAPON_PISTOL', 'suppressor') then + print('Pistol has suppressor') +end +``` + +### xPlayer.setWeaponTint(weaponName, tintIndex) + +Sets weapon tint. + +```lua +xPlayer.setWeaponTint('WEAPON_PISTOL', 2) -- Gold tint +``` + +### xPlayer.getWeaponTint(weaponName) + +Gets weapon tint. + +```lua +local tint = xPlayer.getWeaponTint('WEAPON_PISTOL') +print('Tint index:', tint) +``` + +## Permissions + +### xPlayer.getGroup() + +Returns player's permission group. + +```lua +local group = xPlayer.getGroup() +print('Group:', group) -- 'user', 'admin', 'superadmin' +``` + +### xPlayer.setGroup(group) + +Sets player's permission group. + +```lua +xPlayer.setGroup('admin') +``` + +## Variables & Metadata + +### xPlayer.set(key, value) + +Sets custom variable. + +```lua +xPlayer.set('lastLocation', 'LS Airport') +``` + +### xPlayer.get(key) + +Gets custom variable. + +```lua +local lastLocation = xPlayer.get('lastLocation') +print('Last location:', lastLocation) +``` + +### xPlayer.setMeta(key, value, subKey) + +Sets metadata (persisted to database). + +```lua +xPlayer.setMeta('title', 'Dr.') +xPlayer.setMeta('licenses', 'driver', true) -- With subkey +``` + +### xPlayer.getMeta(key, subKey) + +Gets metadata. + +```lua +local title = xPlayer.getMeta('title') +print('Title:', title) + +local hasDriver = xPlayer.getMeta('licenses', 'driver') +``` + +### xPlayer.clearMeta(key, subKey) + +Clears metadata. + +```lua +xPlayer.clearMeta('title') +xPlayer.clearMeta('licenses', 'driver') -- Clear subkey +``` + +## Client Communication + +### xPlayer.triggerEvent(eventName, ...) + +Triggers client event for this player. + +```lua +xPlayer.triggerEvent('myResource:showMenu', {title = 'Shop', items = {}}) +``` + +### xPlayer.showNotification(msg, type, length, title, position) + +Shows notification to player. + +```lua +xPlayer.showNotification('You received $500', 'success', 3000) +``` + +### xPlayer.showAdvancedNotification(sender, subject, msg, textureDict, iconType, flash, saveToBrief, hudColorIndex) + +Shows GTA-style notification. + +```lua +xPlayer.showAdvancedNotification('Police', 'Dispatch', 'Code 3', 'CHAR_CALL911', 1) +``` + +### xPlayer.showHelpNotification(msg, thisFrame, beep, duration) + +Shows help notification. + +```lua +xPlayer.showHelpNotification('Press E to interact', false, true, 3000) +``` + +## Utility + +### xPlayer.getPlayTime() + +Returns total playtime in seconds. + +```lua +local playtime = xPlayer.getPlayTime() +local hours = math.floor(playtime / 3600) +print('Playtime:', hours, 'hours') +``` + +### xPlayer.executeCommand(command) + +Executes command as player. + +```lua +xPlayer.executeCommand('dv 5') +``` + +## Best Practices + +1. **Always check if xPlayer exists**: + ```lua + local xPlayer = ESX.GetPlayerFromId(source) + if not xPlayer then return end + ``` + +2. **Check before removing**: + ```lua + if xPlayer.getMoney() >= price then + xPlayer.removeMoney(price, 'Bought item') + else + xPlayer.showNotification('Not enough money', 'error') + end + ``` + +3. **Check inventory space**: + ```lua + if xPlayer.canCarryItem('bread', 5) then + xPlayer.addInventoryItem('bread', 5) + else + xPlayer.showNotification('Inventory full', 'error') + end + ``` + +4. **Always provide reasons**: + ```lua + -- GOOD + xPlayer.addMoney(500, 'Sold apples') + + -- BAD (no reason, harder to debug) + xPlayer.addMoney(500) + ``` + +5. **Cache xPlayer reference**: + ```lua + -- GOOD + local xPlayer = ESX.GetPlayerFromId(source) + xPlayer.addMoney(100) + xPlayer.setJob('police', 0) + + -- BAD (calls GetPlayerFromId twice) + ESX.GetPlayerFromId(source).addMoney(100) + ESX.GetPlayerFromId(source).setJob('police', 0) + ``` diff --git a/.agents/skills/fivem-basics/SKILL.md b/.agents/skills/fivem-basics/SKILL.md new file mode 100644 index 0000000..2a3bc3f --- /dev/null +++ b/.agents/skills/fivem-basics/SKILL.md @@ -0,0 +1,31 @@ +--- +name: fivem-basics +description: FiveM resource structure, fxmanifest, client/server scripting, events. Use when creating or editing FiveM resources or Lua scripts, or when the user asks how FiveM works. +author: germanfndez +version: 1.0.0 +mcp-server: projecthub +--- + +# FiveM basics + +Best practices for FiveM — resources, manifest, client/server, events. Use this skill whenever you are dealing with FiveM code to obtain domain-specific knowledge. + +## When to use + +- User asks how FiveM resources or scripts work. +- Editing or creating `fxmanifest.lua`, `client_*.lua`, or `server.lua`. +- Questions about client/server, events, or exports. +- Need to look up natives or detailed docs → point to https://docs.fivem.net/natives/ and https://docs.fivem.net/docs/. + +## How to use + +Read individual rule files for detailed explanations and examples: + +- **rules/structure.md** — Resource structure and organization: scope, client/server separation, logical grouping, naming conventions. +- **rules/fxmanifest.md** — Resource manifest (fxmanifest.lua): fx_version, game, client_scripts, server_script, files, dependencies. +- **rules/client-server.md** — Client vs server scripts, shared code, communication patterns. +- **rules/events.md** — Events in Lua: RegisterNetEvent, TriggerServerEvent, TriggerClientEvent, naming conventions, security. +- **rules/exports.md** — Defining and consuming exports between resources. +- **rules/debugging.md** — Server vs client (F8) logs; when to ask the user for F8 logs if there's no server-side error. +- **rules/optimization.md** — Lua/FiveM optimization: locals, loops, natives (PlayerPedId, vector distance), state bags, security, readability, folder structure. +- **rules/reference-links.md** — Official docs and natives reference. diff --git a/.agents/skills/fivem-basics/client-server.md b/.agents/skills/fivem-basics/client-server.md new file mode 100644 index 0000000..532c793 --- /dev/null +++ b/.agents/skills/fivem-basics/client-server.md @@ -0,0 +1,33 @@ +# Client vs server + +## Client scripts + +- Run on **each player’s game**. +- Can use game natives: peds, vehicles, world, UI, drawing, etc. +- Have access to the local player and game state on that machine. + +## Server script + +- Runs **once** on the server. +- No direct access to game world or visuals. +- Use for: persistence, auth, shared data, validation, database, economy. +- Can target specific clients with `TriggerClientEvent(event, playerId, ...)`. + +## Shared scripts + +- **shared_scripts** run on both client and server. +- Put config, constants, or helper functions here. +- Be careful: no game natives in shared code unless you guard by environment. + +## Communication + +- **Events** — `TriggerServerEvent` / `TriggerClientEvent` (see rules/events.md). +- **Exports** — Call functions from other resources (see rules/exports.md). +- **State bags** — Shared key/value state (see docs). + +## Summary + +| Side | Runs on | Game natives | Use for | +|--------|-------------|---------------|-----------------------------------| +| Client | Each player | Yes | UI, gameplay, locals, rendering | +| Server | Once | No (server natives only) | Data, auth, validation, broadcast | diff --git a/.agents/skills/fivem-basics/debugging.md b/.agents/skills/fivem-basics/debugging.md new file mode 100644 index 0000000..936dfd7 --- /dev/null +++ b/.agents/skills/fivem-basics/debugging.md @@ -0,0 +1,17 @@ +# Debugging FiveM scripts + +## Server vs client logs + +- **Server console** (TxAdmin, terminal, or server window): errors and prints from `server_script` and server-side code. +- **F8 client console**: in-game console opened with **F8**. Shows errors and prints from `client_script` and client-side code. + +## When to ask for F8 logs + +If there is **no error on the server side** (server console is clean or the issue doesn’t show there), ask the user to **share the F8 logs** (client console). Many issues (client Lua errors, missing natives, UI or gameplay bugs) only appear in the client console. + +Tell the user to: +1. Reproduce the issue in-game. +2. Press **F8** to open the client console. +3. Copy the relevant output (errors in red, or the last lines) and share it. + +This helps distinguish server-side vs client-side problems. diff --git a/.agents/skills/fivem-basics/events.md b/.agents/skills/fivem-basics/events.md new file mode 100644 index 0000000..2359cb0 --- /dev/null +++ b/.agents/skills/fivem-basics/events.md @@ -0,0 +1,104 @@ +# Events (Lua) + +Events are the main way to communicate between client and server (or within the same side). + +## Register and listen + +```lua +RegisterNetEvent('myresource:client:itemReceived') +AddEventHandler('myresource:client:itemReceived', function(itemId, amount) + -- handle on client +end) +``` + +- **RegisterNetEvent** — Registers a **networked** event (can be triggered from the other side). +- **AddEventHandler** — Attaches the handler. Use the same event name. + +For **local-only** events (same side), use `AddEventHandler` only; no need for `RegisterNetEvent`. + +## Triggering + +| Function | Direction | Example | +|----------|-----------|--------| +| **TriggerServerEvent**(event, ...) | Client → Server | `TriggerServerEvent('myres:server:buyItem', itemId)` | +| **TriggerClientEvent**(event, playerId, ...) | Server → Client | `TriggerClientEvent('myres:client:notify', source, 'Done!')` | +| **TriggerEvent**(event, ...) | Local only (same side) | `TriggerEvent('myres:client:closeMenu')` | + +- On server, use **source** for the player who triggered the event. +- Use **-1** as playerId in `TriggerClientEvent` to send to all clients. + +## Naming conventions + +Event names should follow the format: `{resourceName}:{client/server}:{eventName}` + +This allows the reader to tell at a glance what resource the event is triggered from, and whether the event should be handled on the client or server. + +### Past Tense + +An event should describe something that has already happened, without prescribing the desired reaction. This pattern recognizes that many event handlers may exist for the same event, which each handle the event in a different way. Triggering an event should be thought of as the cause, whereas handling an event is the effect. The effect should not be in the event name. While an event name need not strictly be past tense, writing event names using past tense can help developers follow this principle. + +**BAD:** +```lua +local function sendMessage(message) + TriggerEvent('resourceName:server:checkProfanity', message) +end + +RegisterNetEvent('resourceName:server:checkProfanity', source, message) + checkProfanity(message) +end +``` + +**GOOD:** +```lua +local function sendMessage(message) + TriggerEvent('resourceName:server:sentMessage', message) +end + +RegisterNetEvent('resourceName:server:sentMessage', source, message) + checkProfanity(message) +end +``` + +## When to use events vs functions + +### Use a function instead of an event handler for single resource, non-networked events + +Events differ from functions in that one event can have many handler functions, versus a function call only executes one function. If the event is non-networked and only is intended to be handled by one resource, a function should be used instead. + +### Use callbacks when wanting to get data back across the network + +It's an anti-pattern to trigger and listen for a separate event to get data back across the network when triggering an event. Instead, use a callback. + +### Use AddEventHandler for non-networked events + +Keeping with the principle of limiting scope, if an event is triggered and handled on the client or server exclusively, do not register it as a net event. + +## Security + +### Secure Net Events + +GetInvokingResource will be nil if an event is triggered from the opposite side of the network that the event is registered on (client triggering a server event, or server triggering a client event). Restrict other ways to call the event to prevent exploits, unless the event is intended to be triggered by both the client and server. + +```lua +RegisterNetEvent('resourceName:client:eventName', function() + if GetInvokingResource() then return end + --- handle the event +end) +``` + +### Validate on server + +Always validate the sender with **GetInvokingResource()** to avoid exploits: + +```lua +RegisterNetEvent('shop:server:purchase') +AddEventHandler('shop:server:purchase', function(itemId) + if GetInvokingResource() then return end -- only allow from same resource or trusted + -- ... +end) +``` + +## Reference + +- Listening: https://docs.fivem.net/docs/scripting-manual/working-with-events/listening-for-events/ +- Triggering: https://docs.fivem.net/docs/scripting-manual/working-with-events/triggering-events/ diff --git a/.agents/skills/fivem-basics/exports.md b/.agents/skills/fivem-basics/exports.md new file mode 100644 index 0000000..2d4fb8f --- /dev/null +++ b/.agents/skills/fivem-basics/exports.md @@ -0,0 +1,48 @@ +# Exports + +Exports let one resource call functions exposed by another resource. + +## Defining exports (Lua) + +In your resource’s manifest: + +```lua +exports { 'getWidget', 'setWidget' } +``` + +In your script, define the globals (or use the runtime export API): + +```lua +local widget = nil + +function getWidget() + return widget +end + +function setWidget(value) + widget = value +end +``` + +## Consuming exports (Lua) + +```lua +local w = exports.myresource:getWidget() +exports.myresource:setWidget(42) +``` + +## Server exports + +Use **server_export** in the manifest and define the same on the server script. Other resources call them from server context: + +```lua +exports.myresource:getData() +``` + +## Notes + +- Prefer **exports** over manifest `export` when possible (e.g. `exports('resname', function() ... end)`). + +## Reference + +- Resource manifest exports: https://docs.fivem.net/docs/scripting-reference/resource-manifest/resource-manifest/#export diff --git a/.agents/skills/fivem-basics/fxmanifest.md b/.agents/skills/fivem-basics/fxmanifest.md new file mode 100644 index 0000000..747b552 --- /dev/null +++ b/.agents/skills/fivem-basics/fxmanifest.md @@ -0,0 +1,45 @@ +# fxmanifest.lua + +The resource manifest is a file named `fxmanifest.lua` (or legacy `__resource.lua`) at the root of the resource folder. It runs in a separate Lua runtime and uses semi-declarative syntax. + +## Essential entries + +| Entry | Description | +|-------|-------------| +| **fx_version** | Use `'cerulean'` (recommended). Alternatives: `'bodacious'`, `'adamant'`. | +| **game** | `'gta5'` (FiveM), `'rdr3'` (RedM), or `'common'` (no game-specific APIs). | +| **author**, **description**, **version** | Optional metadata. | +| **client_scripts** / **server_script** / **shared_scripts** | Arrays or single string. Support globbing. | +| **files** | Files sent to client (e.g. for `data_file` or other assets). | +| **dependency** / **dependencies** | Other resources that must start before this one. | +| **exports** | Client-side export names (Lua). | +| **server_export** | Server-side export names. | + +## Scripts + +Scripts are Lua (`.lua`). Use **client_scripts**, **server_script**, or **shared_scripts** to load them. + +## Globbing + +Script entries support glob patterns: + +- `'*.lua'` — all Lua in root (non-recursive) +- `'**/*.lua'` — all Lua files recursively +- `'client/cl_*.lua'` — client Lua in `client/` folder + +## Minimal example + +```lua +fx_version 'cerulean' +game 'gta5' +author 'Your Name' +description 'My resource' +version '1.0.0' + +client_scripts { 'client.lua' } +server_script 'server.lua' +``` + +## Full reference + +https://docs.fivem.net/docs/scripting-reference/resource-manifest/resource-manifest/ diff --git a/.agents/skills/fivem-basics/optimization.md b/.agents/skills/fivem-basics/optimization.md new file mode 100644 index 0000000..64fc9f9 --- /dev/null +++ b/.agents/skills/fivem-basics/optimization.md @@ -0,0 +1,296 @@ +# Lua / FiveM script optimization and best practices + +Use this rule when writing or reviewing FiveM Lua for performance, readability, or security. + +## General practices + +### Localize functions and variables + +Lua accesses local variables and functions faster than global ones. Prefer `local` unless the value must be global. + +```lua +-- Don't +myVariable = false +function someFunction() + print('Im a global function!') +end + +-- Do +local myVariable = false +local function someFunction() + print('Im a local function!') +end +``` + +### Prefer table indexing over table.insert + +Direct assignment is more efficient than `table.insert`. + +```lua +local t = {} +table.insert(t, {}) -- Don't +t[#t + 1] = {} -- Do +``` + +### Simplify conditionals + +Use `if something then` instead of `if something ~= nil then` when you want to treat both `nil` and `false` as falsy. + +```lua +if bool then -- true only when neither nil nor false + print('bool was neither nil or false!') +end +``` + +### Keep functions universal + +Write functions and events that accept parameters so they can handle multiple scenarios and stay reusable. + +```lua +local function someFunction(param1, param2, param3) + if param1 == 'something' then + -- ... + elseif param2 == 'somethingelse' then + -- ... + end +end + +RegisterNetEvent('someEvent', function(param1, param2, param3) + -- same idea +end) +``` + +### Short returns + +Exit early when conditions are not met to avoid deep nesting. + +```lua +local function someFunction(param1, param2, param3) + if not param1 then return end + -- ... + if not param2 then return end + -- ... +end +``` + +### Avoid re-creating tables in loops + +Initialize once and reuse. + +```lua +local reusableTable = {} +for i = 1, 10 do + reusableTable[i] = i -- reuse, don't create new table each iteration +end +``` + +### Free memory with nil + +Assign unused variables to `nil` so the garbage collector can reclaim memory. + +```lua +local largeData = { 1, 2, 3, 4 } +-- ... use it ... +largeData = nil +``` + +### Avoid hardcoding + +Centralize configurable values (coords, item names, amounts) in a `config.lua` or similar. + +```lua +Config = { + Zones = { + PoliceStation = vector3(441.1, -981.1, 30.7), + Hospital = vector3(1151.21, -1529.62, 34.84) + }, + Payments = { Police = 150, EMS = 120 } +} +``` + +### Logging and debugging + +Use a debug flag so you can enable/disable logs without removing code. + +```lua +local DEBUG = true +local function debugLog(message) + if DEBUG then print(message) end +end +``` + +### Track performance + +Use `os.clock()` or FiveM’s `GetGameTimer()` for performance-critical sections. + +```lua +local start = os.clock() +-- code to measure +print("Execution time:", os.clock() - start) + +-- or +local startTime = GetGameTimer() +Wait(1000) +print("Execution time (ms):", GetGameTimer() - startTime) +``` + +### Avoid overusing network events + +For frequent sync, prefer shared state (state bags, entity state) instead of spamming `TriggerServerEvent` / `TriggerClientEvent`. + +```lua +Entity(playerPed).state:set('exampleData', 123, true) +local data = Entity(playerPed).state.exampleData +``` + +### Optimize data transmission + +Send only the data you need, not whole tables or large payloads. + +```lua +TriggerServerEvent('exampleEvent', { x = 100, y = 200 }) -- minimal payload +``` + +## Code readability + +### Comment your code + +Explain non-obvious or complex logic. + +```lua +-- Check if the player is in range of the target zone +if #(playerCoords - targetCoords) < 10 then + print('Player is in range') +end +``` + +### Organize your script + +Group variables, functions, event handlers, and main logic into clear sections. + +```lua +-- Variables +local QBCore = exports['qb-core']:GetCoreObject() + +-- Functions +local function calculateDistance(pos1, pos2) + return #(pos1 - pos2) +end + +-- Events +RegisterNetEvent('exampleEvent', function() ... end) + +-- Main logic +CreateThread(function() ... end) +``` + +### Folder structure + +Split scripts into smaller files instead of one huge file. + +``` +my_script/ +├── client/ +│ ├── main.lua +│ ├── utils.lua +├── server/ +│ ├── main.lua +│ ├── events.lua +├── shared/ +│ ├── config.lua +└── fxmanifest.lua +``` + +## Native usage + +### Use PlayerPedId() instead of GetPlayerPed(-1) + +```lua +local ped = GetPlayerPed(-1) -- Don't +local ped = PlayerPedId() -- Do +``` + +### Use vector distance instead of GetDistanceBetweenCoords + +```lua +local dist = GetDistanceBetweenCoords(pCoords, coords, true) -- Don't +local dist = #(pCoords - coords) -- Do +if dist < 5 then ... end +``` + +## Loops and threads + +### Control when loops run + +Only run loops when needed (e.g. when player is in zone); turn them off when not. + +```lua +local listen = false +CreateThread(function() + while listen do + -- do work only when needed + Wait(0) + end +end) +-- set listen = true when entering zone, false when leaving +``` + +### Prefer variable Wait() over fixed Wait(0) + +Use a variable wait time: long when idle, short when active. + +```lua +CreateThread(function() + while true do + local sleep = 2500 + local inRange = #(GetEntityCoords(PlayerPedId()) - someCoords) < 10.0 + if inRange then + sleep = 0 + -- do something + end + Wait(sleep) + end +end) +``` + +### Restrict job-specific loops + +Only run job-specific logic for players who have that job. + +```lua +local job = QBCore.Functions.GetPlayerData().job.name +CreateThread(function() + while job == 'police' do + -- police-only logic + Wait(0) + end +end) +``` + +## Security + +- Multiple checks and validation are good; don’t shy away from extra ifs or passing tokens through events. +- **Never** handle money or item transactions on the client; always validate and apply on the server. + +## Event handlers + +Use handlers to update state so you don’t have to poll constantly. + +```lua +local isLoggedIn = false +local PlayerData = {} + +AddStateBagChangeHandler('isLoggedIn', nil, function(_, _, value) + if value then + isLoggedIn = true + PlayerData = QBCore.Functions.GetPlayerData() + else + isLoggedIn = false + PlayerData = {} + end +end) + +RegisterNetEvent('QBCore:Client:OnJobUpdate', function(JobInfo) + PlayerData.job = JobInfo +end) +``` diff --git a/.agents/skills/fivem-basics/reference-links.md b/.agents/skills/fivem-basics/reference-links.md new file mode 100644 index 0000000..22a9d80 --- /dev/null +++ b/.agents/skills/fivem-basics/reference-links.md @@ -0,0 +1,16 @@ +# Reference links + +Use these when you need detailed or up-to-date information. + +| Topic | URL | +|-------|-----| +| **Docs (home)** | https://docs.fivem.net/docs/ | +| **Resource manifest** | https://docs.fivem.net/docs/scripting-reference/resource-manifest/resource-manifest/ | +| **Natives (game API)** | https://docs.fivem.net/natives/ | +| **Scripting manual** | https://docs.fivem.net/docs/scripting-manual/ | +| **Getting started** | https://docs.fivem.net/docs/getting-started/ | +| **Lua client functions** | https://docs.fivem.net/docs/scripting-reference/runtimes/lua/client-functions/ | +| **Lua server functions** | https://docs.fivem.net/docs/scripting-reference/runtimes/lua/server-functions/ | +| **Events list** | https://docs.fivem.net/docs/scripting-reference/events/list/ | + +When the user needs a specific native or deeper reference, point them to the natives page or the scripting manual. diff --git a/.agents/skills/fivem-basics/structure.md b/.agents/skills/fivem-basics/structure.md new file mode 100644 index 0000000..5158af5 --- /dev/null +++ b/.agents/skills/fivem-basics/structure.md @@ -0,0 +1,65 @@ +# Structure/Scope + +Best practices for organizing FiveM resources and Lua code. + +## Prefer Limited Scope + +Variables and functions should be scoped to the smallest visibility needed. Prefer in order: +1. **local function** (most restricted) +2. **function** (module/file level) +3. **export function** (cross-resource) +4. **AddEventHandler** (event-based) +5. **RegisterNetEvent/Callback** (networked, least restricted) + +## Separate Client & Server Files + +Client and server specific files should be organized into their own folders. + +**Example structure:** +``` +my-resource/ +├── fxmanifest.lua +├── client/ +│ ├── main.lua +│ └── ui.lua +└── server/ + ├── main.lua + └── database.lua +``` + +## Use Logical Grouping + +It may make sense to place constructs of the same type together in a file. For example: +- All file scoped variables at the top of the file +- Followed by all local functions +- Followed by all global functions +- Followed by events + +This grouping structure makes it easier to understand the API at the server, resource, and file levels. + +Alternatively, local single use functions could be located directly above or as close as possible to the functions they are called from, and grouping can be based on call structure rather than construct type. + +## Use Files/Modules To Hide Local Functions + +If you have one resource scoped function which calls a few local single use functions, put them all in their own file or module. This keeps your code organized and maintains proper encapsulation. + +## Resource Naming + +Resources should be named with underscores "_" instead of spaces. Other special characters should be avoided so that exports work well. + +**Examples:** +- ✅ `my_awesome_resource` +- ✅ `vehicle_shop` +- ❌ `my awesome resource` (spaces) +- ❌ `vehicle-shop!` (special characters) + +## File Naming + +Files should be named all lower case without any spaces. Dashes "-" or underscores "_" can be used instead of spaces. + +**Examples:** +- ✅ `main.lua` +- ✅ `player_manager.lua` +- ✅ `vehicle-shop.lua` +- ❌ `PlayerManager.lua` (camelCase) +- ❌ `vehicle shop.lua` (spaces) diff --git a/.agents/skills/fivem-dev/SKILL.md b/.agents/skills/fivem-dev/SKILL.md new file mode 100644 index 0000000..256ff76 --- /dev/null +++ b/.agents/skills/fivem-dev/SKILL.md @@ -0,0 +1,146 @@ +--- +name: fivem-dev +description: FiveM development orchestrator for the standalone sky_phone resource, covering Lua, NUI, security, framework adapters, ox_lib, oxmysql, and optional external integrations. Use when creating or editing FiveM Lua, manifests, client/server events, NUI, SQL, or integrations in this repository. +allowed-tools: Read, Write, Edit, Glob, Grep, Bash, WebFetch +--- + +# FiveM development orchestrator for sky_phone + +Use this guide together with the root `AGENTS.md`. The phone is standalone: never introduce a +dependency or interaction with `sky_base`, `sky_jobs_base`, another `sky_*` resource, or their +globals, APIs, events, state, files, config, and database tables. + +## Core workflow + +1. Trace the complete client, server, NUI, config, persistence, and integration flow. +2. Verify natives and external APIs against authoritative documentation or installed source. +3. Identify the root cause and make the smallest coherent fix. +4. Preserve server authority and validate every client-controlled value. +5. Run focused syntax checks, tests, and the real frontend/resource build when present. +6. Inspect the final diff for accidental coupling, generated-file drift, and unrelated edits. + +## Standalone boundary + +- The resource namespace is `sky_phone`. +- Do not add `sky_base`, `sky_jobs_base`, or another Sky resource to `dependency`, `dependencies`, + `shared_script`, `server_script`, or `client_script` entries. +- Do not use `Sky`, `Sky_Jobs`, `Sky.FW`, `Sky.Cb`, `Sky.DB`, `Sky.Query`, Sky exports, or Sky event + namespaces. +- Do not probe whether those resources are started and do not add optional compatibility fallbacks. +- Build phone-owned framework, callback, persistence, notification, and logging adapters where the + existing phone architecture calls for them. +- Third-party integrations must be explicit, documented, isolated, and verified from their own + source/API. Never route them through another Sky resource. + +## Source verification + +Never invent a native, framework API, export, event, parameter, or return type. + +| Topic | Authoritative source | +|---|---| +| FiveM natives | https://docs.fivem.net/natives/ | +| FiveM events, manifests, NUI | https://docs.fivem.net/docs/ | +| ESX | https://docs.esx-framework.org/ | +| QBCore | https://docs.qbcore.org/ | +| Qbox | https://docs.qbox.re/ | +| ox_lib / oxmysql | https://coxdocs.dev/ | +| Fivemanage | https://docs.fivemanage.com/ | + +Before using a framework or library API, inspect `fxmanifest.lua`, configuration, lockfiles, and the +installed integration code to determine the actual version and context. Prefer the phone's existing +adapter over scattering direct framework calls across feature code. If an adapter is missing, add it +inside `sky_phone`; do not borrow one from another resource. + +## Lua and resource structure + +- Use 4-space indentation, double quotes, `snake_case` locals, and `PascalCase` classes when the + surrounding source does not establish a stronger convention. +- Prefer locals and guard clauses. Do not create wrappers that merely return another call. +- Use `joaat("...")`, `PlayerPedId()`, and vector distance (`#(a - b)`). +- Treat native results as truthy/falsy; avoid strict comparisons with `true` or `false`. +- Separate client, server, shared, config, locale, and NUI concerns. +- Use a local function before a resource export, local event, callback, or network event. +- Prefix public resource-owned events and callbacks with `sky_phone:`. +- Avoid per-frame loops unless rendering/input truly requires them; dynamically increase `Wait()` + while idle. + +A manifest may declare only dependencies that `sky_phone` actually uses and documents: + +```lua +fx_version "cerulean" +game "gta5" + +shared_scripts { "config.lua" } +client_scripts { "client/*.lua" } +server_scripts { "server/*.lua" } + +ui_page "html/index.html" +files { "html/index.html", "html/**/*" } +``` + +Add library imports such as ox_lib or oxmysql only when confirmed in the resource architecture. + +## Server authority and events + +- The client and NUI request; the server validates and applies. +- Re-check identity, authorization, ownership, distance, state, rate limits, and configured limits on + the server. +- Never accept client-provided balances, prices, rewards, item counts, roles, phone ownership, or + recipient identity as authoritative. +- Send minimal network payloads and use state bags for suitable replicated state rather than event + spam. +- Rate-limit sensitive and spammable actions with phone-owned server logic. +- Log rejected or suspicious actions without leaking secrets or personal data. + +```lua +RegisterNetEvent("sky_phone:server:updateSetting", function(setting_name, requested_value) + local src = source + if not isAllowedSetting(setting_name, requested_value) then + logSecurityEvent(src, "invalid phone setting update") + return + end + + updateOwnedPhoneSetting(src, setting_name, requested_value) +end) +``` + +The example names describe responsibilities, not guaranteed existing APIs. Resolve them to actual +phone-owned functions after inspecting the repository. + +## NUI + +- Declare the real `ui_page` and generated files in `fxmanifest.lua`. +- Lua to UI: use `SendNUIMessage`; manage focus with `SetNUIFocus` and always release it on close. +- UI to Lua: post to `https://${GetParentResourceName()}/callbackName` and handle it with + `RegisterNUICallback`. +- Treat every NUI payload as untrusted. Send consequential requests to the server and validate there. +- Call the NUI response callback on every reachable path so browser requests cannot hang. +- Keep user-facing strings in the phone's locale system; logs remain English-only. + +## SQL and persistence + +- Use the persistence layer already owned by `sky_phone`. +- Parameterize every value; never concatenate client input into SQL. +- Own phone schema and migrations under this repository and use collision-resistant `sky_phone` + table/key prefixes where appropriate. +- Never query or mutate tables owned by `sky_base`, `sky_jobs_base`, or another Sky resource. +- Use transactions for multi-step mutations that must succeed or fail together. +- Treat deployment/schema checks as static evidence, not proof of live server behavior. + +## Anti-patterns + +- Adding a Sky dependency, import, export, event, state probe, or database shortcut. +- Trusting NUI or client state for permissions or mutations. +- Hiding failures with broad guards, `pcall`, arbitrary waits, retries, or silent fallbacks. +- Hardcoding direct framework calls throughout feature code instead of using the phone's adapter. +- Re-fetching stable data every frame. +- Leaving NUI callbacks unanswered. +- Editing generated frontend output without updating its source and running the build. + +## Final verification + +- Search the diff for forbidden Sky dependencies and symbols. +- Validate manifest paths and dependency order. +- Run targeted Lua/JS/TS checks, tests, and builds available in the repository. +- Verify SQL/config/locale migrations are explicit. +- Clearly distinguish static/build validation from live FiveM runtime proof. diff --git a/.agents/skills/fivem-nui/SKILL.md b/.agents/skills/fivem-nui/SKILL.md new file mode 100644 index 0000000..2b69612 --- /dev/null +++ b/.agents/skills/fivem-nui/SKILL.md @@ -0,0 +1,26 @@ +--- +name: fivem-nui +description: FiveM NUI (New User Interface) development for creating graphical elements and user interfaces. Use when creating or editing NUI interfaces, HTML/CSS/JS for FiveM, or handling NUI callbacks. +--- + +# FiveM NUI Development + +Best practices for FiveM NUI development — fullscreen UIs, NUI callbacks, messaging, and UI integration. Use this skill whenever you are dealing with FiveM user interfaces to obtain domain-specific knowledge. + +## When to use + +- User asks how to create a UI for FiveM. +- Creating or editing HTML/CSS/JS files for FiveM resources. +- Setting up `ui_page` in fxmanifest.lua. +- Questions about NUI callbacks, SendNUIMessage, or SetNUIFocus. +- Need to look up natives → point to https://docs.fivem.net/natives/. + +## How to use + +Read individual rule files for detailed explanations and examples: + +- **rules/setup.md** — Setting up NUI in a resource: ui_page, files entry, folder structure. +- **rules/fullscreen-nui.md** — Creating fullscreen NUI pages: SEND_NUI_MESSAGE, SET_NUI_FOCUS, developer tools, referencing assets. +- **rules/nui-callbacks.md** — NUI callbacks: RegisterNUICallback, fetch requests, data handling, security. +- **rules/best-practices.md** — Best practices: performance, security, communication patterns, error handling. +- **rules/reference-links.md** — Official docs and natives reference. diff --git a/.agents/skills/fivem-nui/best-practices.md b/.agents/skills/fivem-nui/best-practices.md new file mode 100644 index 0000000..59a7722 --- /dev/null +++ b/.agents/skills/fivem-nui/best-practices.md @@ -0,0 +1,472 @@ +# NUI Best Practices + +Guidelines for building performant, secure, and maintainable NUI interfaces. + +## Performance + +### Minimize SendNUIMessage calls + +❌ **Bad:** +```lua +-- Sending updates every frame (terrible for performance!) +CreateThread(function() + while true do + Wait(0) + SendNUIMessage({ + type = 'updateSpeed', + speed = GetEntitySpeed(PlayerPedId()) + }) + end +end) +``` + +✅ **Good:** +```lua +-- Only send when value changes significantly +local lastSpeed = 0 +CreateThread(function() + while true do + Wait(100) -- Update every 100ms at most + local currentSpeed = math.floor(GetEntitySpeed(PlayerPedId()) * 3.6) -- km/h + + if math.abs(currentSpeed - lastSpeed) >= 1 then + lastSpeed = currentSpeed + SendNUIMessage({ + type = 'updateSpeed', + speed = currentSpeed + }) + end + end +end) +``` + +### Batch updates + +❌ **Bad:** +```lua +SendNUIMessage({type = 'updateHealth', health = health}) +SendNUIMessage({type = 'updateArmor', armor = armor}) +SendNUIMessage({type = 'updateStamina', stamina = stamina}) +``` + +✅ **Good:** +```lua +SendNUIMessage({ + type = 'updateStats', + data = { + health = health, + armor = armor, + stamina = stamina + } +}) +``` + +### Optimize DOM operations + +❌ **Bad:** +```js +// Updating DOM on every message +window.addEventListener('message', (event) => { + if (event.data.type === 'updateList') { + const list = document.getElementById('list'); + list.innerHTML = ''; // Clears entire list + + event.data.items.forEach(item => { + const div = document.createElement('div'); + div.textContent = item.name; + list.appendChild(div); // Multiple reflows + }); + } +}); +``` + +✅ **Good:** +```js +// Using document fragment for batch DOM updates +window.addEventListener('message', (event) => { + if (event.data.type === 'updateList') { + const list = document.getElementById('list'); + const fragment = document.createDocumentFragment(); + + event.data.items.forEach(item => { + const div = document.createElement('div'); + div.textContent = item.name; + fragment.appendChild(div); + }); + + list.innerHTML = ''; + list.appendChild(fragment); // Single reflow + } +}); +``` + +### Use CSS animations instead of JavaScript + +❌ **Bad:** +```js +function fadeIn(element) { + let opacity = 0; + const interval = setInterval(() => { + opacity += 0.1; + element.style.opacity = opacity; + if (opacity >= 1) clearInterval(interval); + }, 50); +} +``` + +✅ **Good:** +```css +.fade-in { + animation: fadeIn 0.5s ease-in; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} +``` + +```js +element.classList.add('fade-in'); +``` + +## Security + +### Always validate NUI callback data + +❌ **Bad:** +```lua +RegisterNUICallback('transfer', function(data, cb) + -- No validation! + TriggerServerEvent('bank:server:transfer', data.target, data.amount) + cb('ok') +end) +``` + +✅ **Good:** +```lua +RegisterNUICallback('transfer', function(data, cb) + -- Validate all inputs + if not data.target or type(data.target) ~= 'number' then + cb({success = false, error = 'Invalid target'}) + return + end + + if not data.amount or type(data.amount) ~= 'number' then + cb({success = false, error = 'Invalid amount'}) + return + end + + if data.amount <= 0 or data.amount > 1000000 then + cb({success = false, error = 'Amount out of range'}) + return + end + + -- Additional validation on server side + TriggerServerEvent('bank:server:transfer', data.target, data.amount) + cb({success = true}) +end) +``` + +### Never trust client-side data on server + +```lua +-- Server-side +RegisterNetEvent('shop:server:purchase') +AddEventHandler('shop:server:purchase', function(itemId, quantity, price) + local src = source + + -- DON'T trust the price from client! + -- Look it up server-side + local actualPrice = Items[itemId].price + local totalCost = actualPrice * quantity + + -- Validate and process... +end) +``` + +### Protect sensitive callbacks + +```lua +RegisterNUICallback('adminAction', function(data, cb) + -- Check if player has admin permissions server-side + TriggerServerEvent('admin:server:checkPermission', data.action) + cb('ok') +end) +``` + +## Communication patterns + +### State management + +Keep UI state synchronized: + +```lua +-- client.lua +local currentUI = { + visible = false, + page = 'home', + data = {} +} + +function updateUI(updates) + for k, v in pairs(updates) do + currentUI[k] = v + end + + SendNUIMessage({ + type = 'updateState', + state = currentUI + }) +end + +function openUI(page, data) + updateUI({ + visible = true, + page = page, + data = data + }) + SetNUIFocus(true, true) +end +``` + +### Event-driven updates + +```lua +-- Update UI when game events happen +AddEventHandler('playerSpawned', function() + SendNUIMessage({ + type = 'playerSpawned' + }) +end) + +RegisterNetEvent('inventory:client:itemAdded') +AddEventHandler('inventory:client:itemAdded', function(item, count) + SendNUIMessage({ + type = 'itemAdded', + item = item, + count = count + }) +end) +``` + +## Error handling + +### Always handle callback responses + +```lua +RegisterNUICallback('getData', function(data, cb) + local success, result = pcall(function() + return getSomeData(data.id) + end) + + if success then + cb({success = true, data = result}) + else + print('Error in getData:', result) + cb({success = false, error = 'Failed to get data'}) + end +end) +``` + +### Handle network errors in UI + +```js +async function callbackWithRetry(name, data, maxRetries = 3) { + for (let i = 0; i < maxRetries; i++) { + try { + const response = await fetch(`https://${GetParentResourceName()}/${name}`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(data) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error(`Attempt ${i + 1} failed:`, error); + + if (i === maxRetries - 1) { + throw error; // Final attempt failed + } + + // Wait before retry + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } +} +``` + +## Code organization + +### Separate concerns + +**client.lua:** +```lua +-- UI management only +local UI = {} + +function UI.open(page, data) + SetNUIFocus(true, true) + SendNUIMessage({type = 'open', page = page, data = data}) +end + +function UI.close() + SetNUIFocus(false, false) + SendNUIMessage({type = 'close'}) +end + +function UI.update(data) + SendNUIMessage({type = 'update', data = data}) +end + +return UI +``` + +**main.lua:** +```lua +local UI = require('client') + +-- Game logic +RegisterCommand('shop', function() + local items = getShopItems() + UI.open('shop', {items = items}) +end) + +RegisterNUICallback('buyItem', function(data, cb) + local success = purchaseItem(data.itemId) + cb({success = success}) +end) +``` + +### Use TypeScript for complex UIs + +**types.ts:** +```typescript +export interface PlayerData { + name: string; + id: number; + health: number; + armor: number; +} + +export interface NUIMessage { + type: 'show' | 'hide' | 'update'; + data?: any; +} + +export interface NUICallback { + success: boolean; + data?: T; + error?: string; +} +``` + +**app.ts:** +```typescript +import { PlayerData, NUIMessage, NUICallback } from './types'; + +window.addEventListener('message', (event: MessageEvent) => { + const { type, data } = event.data; + + switch (type) { + case 'show': + showUI(data as PlayerData); + break; + // ... + } +}); + +async function getData(endpoint: string, payload: any): Promise> { + const response = await fetch(`https://${GetParentResourceName()}/${endpoint}`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(payload) + }); + + return response.json(); +} +``` + +## Accessibility + +### Keyboard navigation + +```js +document.addEventListener('keydown', (event) => { + switch(event.key) { + case 'Escape': + closeUI(); + break; + case 'ArrowUp': + navigateUp(); + break; + case 'ArrowDown': + navigateDown(); + break; + case 'Enter': + selectCurrent(); + break; + } +}); +``` + +### Focus management + +```js +function showUI() { + const ui = document.getElementById('ui'); + ui.classList.add('visible'); + + // Focus first interactive element + const firstInput = ui.querySelector('input, button, [tabindex]'); + if (firstInput) { + firstInput.focus(); + } +} +``` + +## Testing + +### Mock mode for browser testing + +```js +// Check if running in game or browser +const isDevelopment = !window.invokeNative; + +if (isDevelopment) { + // Mock data for testing + setTimeout(() => { + window.dispatchEvent(new MessageEvent('message', { + data: { + type: 'show', + data: { + items: [ + { id: 1, name: 'Test Item 1', price: 100 }, + { id: 2, name: 'Test Item 2', price: 200 } + ] + } + } + })); + }, 1000); +} + +// Mock fetch for testing +function mockFetch(url, options) { + console.log('Mock fetch:', url, options); + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ success: true, data: {} }) + }); +} + +const fetchFn = isDevelopment ? mockFetch : fetch; +``` + +## Reference + +- Performance tips: https://docs.fivem.net/docs/scripting-manual/nui-development/ +- Security guide: https://docs.fivem.net/docs/developers/server-security/ diff --git a/.agents/skills/fivem-nui/fullscreen-nui.md b/.agents/skills/fivem-nui/fullscreen-nui.md new file mode 100644 index 0000000..86ae6ee --- /dev/null +++ b/.agents/skills/fivem-nui/fullscreen-nui.md @@ -0,0 +1,205 @@ +# Fullscreen NUI + +Fullscreen NUI pages are the most common type of user interface in FiveM. They overlay on top of the game and can have input focus for mouse/keyboard interaction. + +## Natives + +Key natives for fullscreen NUI: + +- **SEND_NUI_MESSAGE** / **SendNUIMessage** — Send data from Lua to the browser (JSON). +- **SET_NUI_FOCUS** — Control keyboard and mouse focus for the NUI page. + +## Sending messages to NUI + +Use `SendNUIMessage` to send data to your UI: + +```lua +-- Lua example +SendNUIMessage({ + type = 'openMenu', + data = { + title = 'Shop', + items = shopItems + } +}) +``` + +## Receiving messages in the browser + +In your HTML/JavaScript, listen for messages using the `message` event: + +```js +window.addEventListener('message', (event) => { + const data = event.data; + + if (data.type === 'openMenu') { + showMenu(data.data.title, data.data.items); + } +}); +``` + +## NUI Focus + +Control focus with `SetNUIFocus`: + +```lua +-- Enable both keyboard and mouse +SetNUIFocus(true, true) + +-- Enable keyboard only (no cursor) +SetNUIFocus(true, false) + +-- Disable focus completely +SetNUIFocus(false, false) +``` + +**Important:** +- The first parameter controls **keyboard focus**. +- The second parameter controls **mouse cursor** visibility and focus. +- Always disable focus when closing the UI to prevent input issues. + +## Focus stack + +FiveM maintains a focus stack for NUI resources: +- The most recently focused resource is on top. +- Resources are rendered as full-screen iframes. +- There's no click-through across resources. +- Only the current resource can control its own focus. + +## Referencing assets + +Use the `https://cfx-nui-{resourceName}/` protocol to reference resource files: + +```html + + + + + + + + +``` + +**Note:** The old `nui://` protocol is deprecated and no longer works in newer browser versions. Always use `https://cfx-nui-`. + +## Developer tools + +### Chrome DevTools + +Access CEF remote debugging tools at [http://localhost:13172/](http://localhost:13172/) while the game is running. Use any Chromium-based browser. + +### Console command + +Alternatively, use the `nui_devTools` command in the F8 console (requires developer mode enabled). + +## Example: Simple menu + +**Lua (client.lua):** +```lua +local menuOpen = false + +RegisterCommand('openmenu', function() + menuOpen = true + SetNUIFocus(true, true) + SendNUIMessage({ + type = 'show', + items = { + {id = 1, name = 'Item 1', price = 100}, + {id = 2, name = 'Item 2', price = 200} + } + }) +end) + +RegisterNUICallback('close', function(data, cb) + menuOpen = false + SetNUIFocus(false, false) + cb('ok') +end) + +RegisterNUICallback('buyItem', function(data, cb) + print('Buying item:', data.itemId) + -- Handle purchase logic + cb({success = true, message = 'Purchase successful'}) +end) +``` + +**HTML (ui/index.html):** +```html + + + + + + + + + + + +``` + +**JavaScript (ui/app.js):** +```js +window.addEventListener('message', (event) => { + if (event.data.type === 'show') { + showMenu(event.data.items); + } +}); + +function showMenu(items) { + const menu = document.getElementById('menu'); + const itemsDiv = document.getElementById('items'); + + itemsDiv.innerHTML = ''; + items.forEach(item => { + const div = document.createElement('div'); + div.className = 'item'; + div.textContent = `${item.name} - $${item.price}`; + div.onclick = () => buyItem(item.id); + itemsDiv.appendChild(div); + }); + + menu.style.display = 'block'; +} + +function closeMenu() { + document.getElementById('menu').style.display = 'none'; + fetch(`https://${GetParentResourceName()}/close`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({}) + }); +} + +function buyItem(itemId) { + fetch(`https://${GetParentResourceName()}/buyItem`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ itemId: itemId }) + }).then(resp => resp.json()).then(resp => { + if (resp.success) { + alert(resp.message); + } + }); +} + +function GetParentResourceName() { + return window.location.hostname.replace('cfx-nui-', ''); +} +``` + +## Reference + +- Fullscreen NUI: https://docs.fivem.net/docs/scripting-manual/nui-development/full-screen-nui/ +- SEND_NUI_MESSAGE: https://docs.fivem.net/natives/?_0x78608ACB +- SET_NUI_FOCUS: https://docs.fivem.net/natives/?_0x5B98AE30 diff --git a/.agents/skills/fivem-nui/nui-callbacks.md b/.agents/skills/fivem-nui/nui-callbacks.md new file mode 100644 index 0000000..6facff7 --- /dev/null +++ b/.agents/skills/fivem-nui/nui-callbacks.md @@ -0,0 +1,309 @@ +# NUI Callbacks + +NUI callbacks allow the browser (UI) to send data back to the game and receive responses. They work like HTTP endpoints that your UI can call. + +## Registering callbacks in Lua + +Use `RegisterNUICallback` to create a callback endpoint: + +```lua +RegisterNUICallback('getPlayerData', function(data, cb) + -- data contains the POST body parsed as JSON + local requestedData = data.dataType + + -- Perform logic + local playerData = { + name = GetPlayerName(PlayerId()), + health = GetEntityHealth(PlayerPedId()), + position = GetEntityCoords(PlayerPedId()) + } + + -- ALWAYS call the callback (cb) to prevent request stalling + cb(playerData) +end) +``` + +**Important:** +- The `data` parameter contains the POST body automatically parsed as JSON. +- The `cb` function must ALWAYS be called, even if just with `{}` or `{ok = true}`. +- Failing to call `cb` will cause the browser request to hang indefinitely. + +## Calling callbacks from the browser + +Use `fetch` to call a callback from your JavaScript: + +```js +// Get the current resource name +function GetParentResourceName() { + return window.location.hostname.replace('cfx-nui-', ''); +} + +// Call the callback +fetch(`https://${GetParentResourceName()}/getPlayerData`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: JSON.stringify({ + dataType: 'basic' + }) +}) +.then(resp => resp.json()) +.then(data => { + console.log('Player data:', data); + // Use the data in your UI + document.getElementById('player-name').textContent = data.name; +}) +.catch(err => { + console.error('Error fetching player data:', err); +}); +``` + +## Callback naming + +The callback name in the URL must match the name you registered: + +```lua +-- Lua +RegisterNUICallback('buyItem', function(data, cb) + -- ... +end) +``` + +```js +// Browser +fetch(`https://${GetParentResourceName()}/buyItem`, { + method: 'POST', + // ... +}) +``` + +## Complete example: Item purchase + +**Lua (client.lua):** +```lua +RegisterNUICallback('purchaseItem', function(data, cb) + local itemId = data.itemId + local quantity = data.quantity or 1 + + -- Validate input + if not itemId then + cb({success = false, error = 'Item ID required'}) + return + end + + -- Trigger server-side purchase + TriggerServerEvent('shop:server:purchase', itemId, quantity) + + -- Wait for server response (you'd typically use a callback pattern) + -- For this example, we'll return immediately + cb({ + success = true, + message = 'Purchase request sent', + itemId = itemId, + quantity = quantity + }) +end) + +-- Handle server response +RegisterNetEvent('shop:client:purchaseResult') +AddEventHandler('shop:client:purchaseResult', function(success, message) + SendNUIMessage({ + type = 'purchaseResult', + success = success, + message = message + }) +end) +``` + +**JavaScript (ui/shop.js):** +```js +function purchaseItem(itemId, quantity) { + fetch(`https://${GetParentResourceName()}/purchaseItem`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: JSON.stringify({ + itemId: itemId, + quantity: quantity + }) + }) + .then(resp => resp.json()) + .then(data => { + if (data.success) { + showNotification(data.message, 'success'); + } else { + showNotification(data.error, 'error'); + } + }) + .catch(err => { + console.error('Purchase error:', err); + showNotification('Purchase failed', 'error'); + }); +} + +// Listen for server response +window.addEventListener('message', (event) => { + if (event.data.type === 'purchaseResult') { + if (event.data.success) { + showNotification('Purchase successful!', 'success'); + } else { + showNotification(event.data.message, 'error'); + } + } +}); +``` + +## Async/await pattern + +For cleaner code, use async/await: + +```js +async function getPlayerData() { + try { + const response = await fetch(`https://${GetParentResourceName()}/getPlayerData`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: JSON.stringify({ + dataType: 'full' + }) + }); + + const data = await response.json(); + return data; + } catch (error) { + console.error('Failed to get player data:', error); + return null; + } +} + +// Usage +async function updateUI() { + const playerData = await getPlayerData(); + if (playerData) { + renderPlayerInfo(playerData); + } +} +``` + +## Error handling + +Always handle errors properly: + +**Lua:** +```lua +RegisterNUICallback('risky_operation', function(data, cb) + local success, result = pcall(function() + -- Your risky operation + return doSomethingRisky(data) + end) + + if success then + cb({success = true, data = result}) + else + cb({success = false, error = 'Operation failed: ' .. tostring(result)}) + end +end) +``` + +**JavaScript:** +```js +async function performRiskyOperation() { + try { + const response = await fetch(`https://${GetParentResourceName()}/risky_operation`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({}) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + if (!data.success) { + throw new Error(data.error || 'Operation failed'); + } + + return data.data; + } catch (error) { + console.error('Operation failed:', error); + showNotification('Something went wrong', 'error'); + return null; + } +} +``` + +## Security considerations + +### Always validate data + +```lua +RegisterNUICallback('setPlayerName', function(data, cb) + -- Validate input + if not data.name or type(data.name) ~= 'string' then + cb({success = false, error = 'Invalid name'}) + return + end + + -- Sanitize input + local name = data.name:gsub('[^%w%s]', '') -- Remove special characters + if #name < 3 or #name > 32 then + cb({success = false, error = 'Name must be 3-32 characters'}) + return + end + + -- Proceed with validated data + TriggerServerEvent('player:server:setName', name) + cb({success = true}) +end) +``` + +### Don't trust client data + +Always validate and verify on the server: + +```lua +-- Client +RegisterNUICallback('buyItem', function(data, cb) + TriggerServerEvent('shop:server:buyItem', data.itemId, data.quantity) + cb({ok = true}) +end) + +-- Server +RegisterNetEvent('shop:server:buyItem') +AddEventHandler('shop:server:buyItem', function(itemId, quantity) + local src = source + + -- Validate item exists + if not Items[itemId] then + return + end + + -- Validate quantity + if type(quantity) ~= 'number' or quantity < 1 or quantity > 100 then + return + end + + -- Check player has enough money + local player = GetPlayer(src) + local price = Items[itemId].price * quantity + + if player.getMoney() >= price then + player.removeMoney(price) + player.addItem(itemId, quantity) + TriggerClientEvent('shop:client:purchaseResult', src, true, 'Purchase successful') + else + TriggerClientEvent('shop:client:purchaseResult', src, false, 'Not enough money') + end +end) +``` + +## Reference + +- NUI Callbacks: https://docs.fivem.net/docs/scripting-manual/nui-development/nui-callbacks/ +- RegisterNUICallback: https://docs.fivem.net/docs/scripting-reference/runtimes/lua/functions/RegisterNUICallback/ diff --git a/.agents/skills/fivem-nui/reference-links.md b/.agents/skills/fivem-nui/reference-links.md new file mode 100644 index 0000000..82f899a --- /dev/null +++ b/.agents/skills/fivem-nui/reference-links.md @@ -0,0 +1,128 @@ +# Reference Links + +Official documentation and useful resources for FiveM NUI development. + +## Official FiveM Documentation + +### NUI Development +- **NUI Overview**: https://docs.fivem.net/docs/scripting-manual/nui-development/ +- **Fullscreen NUI**: https://docs.fivem.net/docs/scripting-manual/nui-development/full-screen-nui/ +- **NUI Callbacks**: https://docs.fivem.net/docs/scripting-manual/nui-development/nui-callbacks/ +- **Loading Screens**: https://docs.fivem.net/docs/scripting-manual/nui-development/loading-screens/ +- **Direct-rendered UI (DUI)**: https://docs.fivem.net/docs/scripting-manual/nui-development/dui/ + +### Natives Reference +- **SEND_NUI_MESSAGE**: https://docs.fivem.net/natives/?_0x78608ACB +- **SET_NUI_FOCUS**: https://docs.fivem.net/natives/?_0x5B98AE30 +- **All Natives**: https://docs.fivem.net/natives/ + +### Scripting Manual +- **Creating Your First Script**: https://docs.fivem.net/docs/scripting-manual/introduction/creating-your-first-script/ +- **Working with Events**: https://docs.fivem.net/docs/scripting-manual/working-with-events/ +- **Resource Manifest**: https://docs.fivem.net/docs/scripting-reference/resource-manifest/resource-manifest/ + +### Lua Functions +- **RegisterNUICallback**: https://docs.fivem.net/docs/scripting-reference/runtimes/lua/functions/RegisterNUICallback/ +- **SendNUIMessage**: https://docs.fivem.net/docs/scripting-reference/runtimes/lua/functions/SendNUIMessage/ + +## Security +- **Secure Your Events**: https://docs.fivem.net/docs/developers/server-security/ + +## Web Technologies + +### HTML/CSS/JavaScript +- **MDN Web Docs**: https://developer.mozilla.org/ +- **Fetch API**: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API +- **Window.postMessage**: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage + +### Modern Frameworks +- **React**: https://react.dev/ +- **Vue**: https://vuejs.org/ +- **Svelte**: https://svelte.dev/ +- **Vite**: https://vitejs.dev/ + +### CSS Frameworks +- **Tailwind CSS**: https://tailwindcss.com/ +- **Bootstrap**: https://getbootstrap.com/ + +### TypeScript +- **TypeScript Documentation**: https://www.typescriptlang.org/docs/ + +## Build Tools +- **Vite**: https://vitejs.dev/ +- **Webpack**: https://webpack.js.org/ +- **npm**: https://www.npmjs.com/ + +## Community Resources + +### Forums +- **FiveM Forums**: https://forum.cfx.re/ +- **FiveM Discord**: https://discord.gg/fivem + +### Code Examples +- **FiveM Cookbook**: https://docs.fivem.net/docs/cookbook/ +- **GitHub - FiveM**: https://github.com/citizenfx/fivem + +## Tools + +### Debugging +- **Chrome DevTools**: http://localhost:13172/ (when game is running) +- **F8 Console**: In-game developer console + +### Development +- **Visual Studio Code**: https://code.visualstudio.com/ +- **Browser DevTools**: Built into Chrome, Firefox, Edge + +## Additional Resources + +### Performance +- **Web Performance**: https://web.dev/performance/ +- **Chrome DevTools Performance**: https://developer.chrome.com/docs/devtools/performance/ + +### Accessibility +- **Web Accessibility**: https://www.w3.org/WAI/ +- **ARIA**: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA + +## Quick Reference Card + +### Key Lua Functions +```lua +-- Send message to UI +SendNUIMessage({type = 'action', data = value}) + +-- Set focus +SetNUIFocus(hasKeyboardFocus, hasMouseFocus) + +-- Register callback +RegisterNUICallback('callbackName', function(data, cb) + cb(responseData) +end) +``` + +### Key JavaScript Patterns +```js +// Listen for messages +window.addEventListener('message', (event) => { + // Handle event.data +}); + +// Call callback +fetch(`https://${GetParentResourceName()}/callbackName`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(data) +}); + +// Get resource name +function GetParentResourceName() { + return window.location.hostname.replace('cfx-nui-', ''); +} +``` + +### Asset References +```html + + + + +``` diff --git a/.agents/skills/fivem-nui/setup.md b/.agents/skills/fivem-nui/setup.md new file mode 100644 index 0000000..972f448 --- /dev/null +++ b/.agents/skills/fivem-nui/setup.md @@ -0,0 +1,385 @@ +# Setting up NUI in a Resource + +This guide covers the basic setup for adding a NUI interface to your FiveM resource. + +## Folder structure + +Recommended folder structure for a resource with NUI: + +``` +my-resource/ +├── fxmanifest.lua +├── client.lua +├── server.lua +└── ui/ + ├── index.html + ├── css/ + │ └── style.css + ├── js/ + │ └── app.js + └── images/ + └── logo.png +``` + +## fxmanifest.lua configuration + +You need to specify the `ui_page` and include all UI files in the `files` array: + +```lua +fx_version 'cerulean' +game 'gta5' + +author 'Your Name' +description 'Resource with NUI' +version '1.0.0' + +-- Client-side Lua script +client_script 'client.lua' + +-- Server-side Lua script +server_script 'server.lua' + +-- Specify the root UI page +ui_page 'ui/index.html' + +-- All UI files must be included in files array +files { + 'ui/index.html', + 'ui/css/style.css', + 'ui/js/app.js', + 'ui/images/logo.png' +} +``` + +### Using wildcards + +You can use wildcards for convenience: + +```lua +ui_page 'ui/index.html' + +files { + 'ui/**/*.*' -- Include all files recursively in ui folder +} +``` + +## External hosting + +You can also host the UI externally: + +```lua +ui_page 'https://ui-frontend.example.com/v1.0.0/index.html' + +-- No files array needed for external hosting +``` + +**Benefits:** +- Faster updates without resource restart +- Can use modern build tools and CI/CD +- Reduces resource download size + +**Considerations:** +- Requires external web hosting +- Players need internet connection +- Potential latency for initial load + +## Basic HTML structure + +**ui/index.html:** +```html + + + + + + My Resource UI + + + +
+ +
+ + + + +``` + +## Basic CSS + +**ui/css/style.css:** +```css +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + overflow: hidden; +} + +#app { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + display: none; /* Hidden by default */ +} + +#app.visible { + display: flex; + justify-content: center; + align-items: center; +} +``` + +## Basic JavaScript + +**ui/js/app.js:** +```js +// Helper to get resource name +function GetParentResourceName() { + return window.location.hostname.replace('cfx-nui-', ''); +} + +// Listen for messages from Lua +window.addEventListener('message', (event) => { + const data = event.data; + + switch(data.type) { + case 'show': + showUI(data.data); + break; + case 'hide': + hideUI(); + break; + case 'update': + updateUI(data.data); + break; + } +}); + +// Show the UI +function showUI(data) { + const app = document.getElementById('app'); + app.classList.add('visible'); + + // Populate UI with data + if (data) { + // Handle data... + } +} + +// Hide the UI +function hideUI() { + const app = document.getElementById('app'); + app.classList.remove('visible'); +} + +// Update UI content +function updateUI(data) { + // Update logic... +} + +// Close UI and notify Lua +function closeUI() { + hideUI(); + + fetch(`https://${GetParentResourceName()}/close`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: JSON.stringify({}) + }); +} + +// ESC key to close +document.addEventListener('keydown', (event) => { + if (event.key === 'Escape') { + closeUI(); + } +}); +``` + +## Basic Lua client script + +**client.lua:** +```lua +local uiOpen = false + +-- Command to open UI +RegisterCommand('openui', function() + openUI() +end) + +-- Open the UI +function openUI() + if uiOpen then return end + + uiOpen = true + SetNUIFocus(true, true) + + SendNUIMessage({ + type = 'show', + data = { + title = 'My UI', + content = 'Hello from FiveM!' + } + }) +end + +-- Close the UI +function closeUI() + if not uiOpen then return end + + uiOpen = false + SetNUIFocus(false, false) + + SendNUIMessage({ + type = 'hide' + }) +end + +-- Handle close callback from NUI +RegisterNUICallback('close', function(data, cb) + closeUI() + cb('ok') +end) + +-- Example: Update UI with server data +RegisterNetEvent('myresource:client:updateUI') +AddEventHandler('myresource:client:updateUI', function(newData) + SendNUIMessage({ + type = 'update', + data = newData + }) +end) +``` + +## Modern build tools + +For production applications, consider using modern build tools: + +### Using Vite + +**package.json:** +```json +{ + "name": "my-resource-ui", + "version": "1.0.0", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "devDependencies": { + "vite": "^5.0.0" + } +} +``` + +**vite.config.js:** +```js +import { defineConfig } from 'vite' + +export default defineConfig({ + base: './', + build: { + outDir: 'dist', + emptyOutDir: true, + assetsInlineLimit: 0 + } +}) +``` + +**fxmanifest.lua:** +```lua +ui_page 'ui/dist/index.html' + +files { + 'ui/dist/**/*' +} +``` + +### Using React + +```bash +npm create vite@latest ui -- --template react +cd ui +npm install +npm run build +``` + +### Using Vue + +```bash +npm create vite@latest ui -- --template vue +cd ui +npm install +npm run build +``` + +## Testing during development + +1. **Browser testing:** Open `ui/index.html` directly in your browser for quick UI testing. +2. **Mock data:** Create mock data in your JavaScript for testing without running the game. +3. **Hot reload:** Use Vite or similar tools for hot module replacement during development. +4. **DevTools:** Use `http://localhost:13172/` while the game is running to debug the live NUI. + +## Common mistakes + +### Forgetting to add files to manifest + +❌ **Wrong:** +```lua +ui_page 'ui/index.html' +-- Missing files array! +``` + +✅ **Correct:** +```lua +ui_page 'ui/index.html' +files { + 'ui/index.html', + 'ui/css/style.css', + 'ui/js/app.js' +} +``` + +### Wrong asset references + +❌ **Wrong:** +```html + + + +``` + +✅ **Correct:** +```html + +``` + +### Not disabling focus + +❌ **Wrong:** +```lua +-- User closes UI but focus is still active +-- Now they can't move or shoot! +``` + +✅ **Correct:** +```lua +RegisterNUICallback('close', function(data, cb) + SetNUIFocus(false, false) -- Always disable focus! + cb('ok') +end) +``` + +## Reference + +- Resource Manifest: https://docs.fivem.net/docs/scripting-reference/resource-manifest/resource-manifest/ +- Fullscreen NUI: https://docs.fivem.net/docs/scripting-manual/nui-development/full-screen-nui/ diff --git a/.agents/skills/fivem-security/SKILL.md b/.agents/skills/fivem-security/SKILL.md new file mode 100644 index 0000000..3d92cb3 --- /dev/null +++ b/.agents/skills/fivem-security/SKILL.md @@ -0,0 +1,25 @@ +--- +name: fivem-security +description: Best practices and rules for securing FiveM resources against cheaters and exploits. Use this skill when writing or reviewing server-side and client-side code to ensure malicious events, unauthorized entity creations, and client trust issues are prevented. Focuses on strict server authority and safe event handling. +--- + +# 🛡️ FiveM Security & Anti-Exploit Principles + +This skill provides architectural guidance for securing FiveM resources against common cheats, unauthorized event triggers, and malicious data manipulation. + +**Core Philosophy:** NEVER TRUST THE CLIENT. + +The client is in the hands of the user, which means it can be fully compromised. Every action that affects the game state, economy, or other players MUST be validated on the server. + +## 📂 Core Concepts & Rules + +Detailed rules are broken down into specific topics within the `rules/` directory: + +- **[events.md](rules/events.md)**: How to properly structure and validate `RegisterNetEvent` / `TriggerServerEvent` to prevent unauthorized execution. + +## ⚠️ The Golden Rules of FiveM Security + +1. **Server Authority**: The server dictates the truth. The client only requests actions. +2. **Never Trust Parameters**: Always validate arguments sent from the client (e.g., if a client says "give me $50", the server must check if the client *earned* it, not just blindly accept the amount). +3. **Distance Checks**: Always check the distance on the server side before allowing an interaction (e.g., looting, selling, entering a zone). +4. **Rate Limiting**: Prevent event spamming by implementing server-side cooldowns or debouncing for critical actions. \ No newline at end of file diff --git a/.agents/skills/fivem-security/events.md b/.agents/skills/fivem-security/events.md new file mode 100644 index 0000000..991dfa4 --- /dev/null +++ b/.agents/skills/fivem-security/events.md @@ -0,0 +1,73 @@ +# 🛡️ Secure Event Handling + +The single biggest vulnerability in FiveM development is trusting data sent from the client via `TriggerServerEvent`. **Hackers don't need complex menus; they just execute events with spoofed parameters.** + +## ❌ Bad Practice: Trusting the Client + +Never let the client dictate the outcome. + +```lua +-- CLIENT +TriggerServerEvent("job:payMe", 5000) -- The hacker just changes this to 5000000 + +-- SERVER +RegisterNetEvent("job:payMe", function(amount) + local src = source + local Player = QBCore.Functions.GetPlayer(src) + Player.Functions.AddMoney("cash", amount) -- Boom, economy ruined. +end) +``` + +## ✅ Good Practice: Server Authority + +The client **requests** an action; the server **calculates** the result. + +```lua +-- CLIENT +-- Client just says "I finished the job" +TriggerServerEvent("job:requestPayment") + +-- SERVER +RegisterNetEvent("job:requestPayment", function() + local src = source + local Player = QBCore.Functions.GetPlayer(src) + + -- The SERVER decides how much to pay based on server-side logic/config + local paymentAmount = Config.JobPayAmount + + -- Additional security: Are they actually clocked in? Did they wait the required time? + if not ServerSideJobState[src].isWorking then return end + + Player.Functions.AddMoney("cash", paymentAmount) +end) +``` + +## 📍 Distance Checks (Crucial) + +If a player triggers an event to "buy an item" or "harvest a plant," the server **MUST** check if they are actually physically near the location. Hackers can trigger events from across the map. + +```lua +-- SERVER +local sellPosition = vector3(100.0, 0.0, 0.0) + +RegisterNetEvent("packages:givePackage", function() + local src = source + local ped = GetPlayerPed(src) + local position = GetEntityCoords(ped) + + -- Server checks distance. 10 units is usually a safe margin for latency. + if #(position - sellPosition) >= 10.0 then + print(("Exploit attempt: %s tried to sell from too far away."):format(GetPlayerName(src))) + return + end + + -- Proceed with giving the item +end) +``` + +## 🛡️ Best Practices Summary + +1. **Client requests, Server decides.** Never send prices, amounts, or sensitive item names from the client if it can be avoided. +2. **Always perform Distance Checks** on the server using `GetEntityCoords(GetPlayerPed(source))`. +3. **Verify State.** If the event requires a specific job or item, verify it on the server *again*. +4. **Log suspicious activity.** If a distance check fails drastically, log it for admins. \ No newline at end of file diff --git a/.agents/skills/grill-me/SKILL.md b/.agents/skills/grill-me/SKILL.md new file mode 100644 index 0000000..9470cfc --- /dev/null +++ b/.agents/skills/grill-me/SKILL.md @@ -0,0 +1,7 @@ +--- +name: grill-me +description: A relentless interview to sharpen a plan or design. +disable-model-invocation: true +--- + +Run a `/grilling` session. diff --git a/.agents/skills/grill-me/agents/openai.yaml b/.agents/skills/grill-me/agents/openai.yaml new file mode 100644 index 0000000..4d6fb0c --- /dev/null +++ b/.agents/skills/grill-me/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Grill Me" + short_description: "Sharpen a plan through interview" +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/grilling/SKILL.md b/.agents/skills/grilling/SKILL.md new file mode 100644 index 0000000..52d8eb3 --- /dev/null +++ b/.agents/skills/grilling/SKILL.md @@ -0,0 +1,12 @@ +--- +name: grilling +description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases. +--- + +Interview me relentlessly about every aspect of this until we reach a shared understanding. Walk down each branch of the decision tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time, waiting for feedback on each question before continuing. Asking multiple questions at once is bewildering. + +If a *fact* can be found by exploring the environment (filesystem, tools, etc.), look it up rather than asking me. The *decisions*, though, are mine — put each one to me and wait for my answer. + +Do not act on it until I confirm we have reached a shared understanding. diff --git a/.agents/skills/grilling/agents/openai.yaml b/.agents/skills/grilling/agents/openai.yaml new file mode 100644 index 0000000..85b1260 --- /dev/null +++ b/.agents/skills/grilling/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Grilling" + short_description: "Stress-test thinking one question at a time" diff --git a/.agents/skills/lua-basics/SKILL.md b/.agents/skills/lua-basics/SKILL.md new file mode 100644 index 0000000..807063f --- /dev/null +++ b/.agents/skills/lua-basics/SKILL.md @@ -0,0 +1,29 @@ +--- +name: lua-basics +description: Effective Lua programming for FiveM - functions, tables, variables, conditionals, error handling. Use when writing or reviewing Lua code for FiveM resources. +author: germanfndez +version: 1.0.0 +mcp-server: projecthub +--- + +# Lua Basics + +Best practices for writing effective Lua code in FiveM. This skill covers fundamental Lua patterns, performance optimizations, and code quality guidelines specifically for FiveM development. + +## When to use + +- Writing or reviewing Lua code for FiveM resources. +- Questions about Lua best practices, naming conventions, or code structure. +- Optimizing Lua code for performance. +- Need guidance on functions, tables, variables, conditionals, or error handling in Lua. + +## How to use + +Read individual rule files for detailed explanations and examples: + +- **rules/functions.md** — Function best practices: size, naming, parameters, exports, guard clauses. +- **rules/tables.md** — Table operations: array indices, dereferencing, avoiding table.insert, iterations, array size. +- **rules/variables.md** — Variable naming conventions: constants, locals, globals, enums vs booleans. +- **rules/conditionals.md** — Conditional patterns: default values, boolean expressions, readability. +- **rules/errors.md** — Error handling: assertions, pre-conditions, errors as values, fail loudly. +- **rules/reference-links.md** — Official Lua and FiveM documentation resources. diff --git a/.agents/skills/lua-basics/conditionals.md b/.agents/skills/lua-basics/conditionals.md new file mode 100644 index 0000000..3e665c1 --- /dev/null +++ b/.agents/skills/lua-basics/conditionals.md @@ -0,0 +1,59 @@ +# Conditionals + +## Default Values + +Consider ternary operator 'or' instead of nil checks to improve readability. + +**BAD:** +```lua +if name then + return name +else + return "John Doe" +end +``` + +**GOOD:** +```lua +return name or "John Doe" +``` + +## Don't Write "if true then return true" + +When returning or setting a variable to the value of the conditional statement itself, don't use an if else block. + +**BAD:** +```lua +if name == "mark" or name == "stacy" then + return true +else + return false +end +``` + +**GOOD:** +```lua +return name == "mark" or name == "stacy" +``` + +## Prefer positive boolean expressions + +This makes the code easier to read. + +**BAD:** +```lua +if not isHappy then + return "sad" +else + return "happy" +end +``` + +**GOOD:** +```lua +if isHappy then + return "happy" +else + return "sad" +end +``` diff --git a/.agents/skills/lua-basics/errors.md b/.agents/skills/lua-basics/errors.md new file mode 100644 index 0000000..e366812 --- /dev/null +++ b/.agents/skills/lua-basics/errors.md @@ -0,0 +1,61 @@ +# Error Handling + +An unexpected state or condition within the code may cause a lua error to be thrown. However, these bad states can have other consequences when not so explicitly detected and handled. They may propagate bad state to other components of the system, or introduce unintended behavior. Checking for and handling unexpected state can make your code more robust to possible failures and vulnerabilities. + +## Use assert instead of 'if expression then error()' + +assert is a more succinct, readable way to throw an error on a condition not being met. + +**BAD:** +```lua +if not someVar then error("someVar is nil") end +``` + +**GOOD:** +```lua +assert(someVar ~= nil, "someVar is nil") +``` + +## Pre-condition check liberally + +When performing an operation, make a list of assumptions and then write pre-condition checks. + +## Fail loudly for unexpected state + +When writing pre-condition checks, failure should often result in a lua error with a message. Failing silently by just early returning from a function can be difficult to debug and may go undetected. + +**BAD:** +```lua +if not isPlayerDead() then return end +``` + +**GOOD:** +```lua +assert(isPlayerDead(), "player is not dead") +``` + +## Throwing an Error vs Logging + +Some states may be unexpected, but recoverable. In these cases, it may be preferable to log the state, but still allow the operation to proceed. An example of this would be a player selling items to an NPC. If some of the items were failing to sell, it would be better to log/print the error, while allowing the rest of the items to go through. + +Keep in mind what execution will be cancelled by throwing an error and use best judgment to decide whether throwing or logging is the better choice. + +## Assertions vs Errors as Values + +Assertions cause lua errors to propagate up the stack, forcing callers to handle them via protected calls. While assertions should always be used for unexpected or "impossible" cases, errors as values can be helpful for expected failure cases that we want the caller of the API to handle. This involves returning a success boolean, followed by an optional error code & message. Doing this makes the default behavior of our function fail silently, as it becomes the callers responsibility to decide to handle the error. This can provide a better experience for players as a silent failure may be preferable to loud error messages in cases where the player pressed the wrong button for example. + +Note that API functions should be idempotent when possible. A no-op result is still considered successful, if the state of the system is the one the caller expects after the function is ran. + +### Example Error as Value + +```lua +function add(a, b) + if not a or not b then + return nil, { + code = 'missing_required_params', + message = 'either a or b is nil', + } + end + return a + b +end +``` diff --git a/.agents/skills/lua-basics/functions.md b/.agents/skills/lua-basics/functions.md new file mode 100644 index 0000000..4811824 --- /dev/null +++ b/.agents/skills/lua-basics/functions.md @@ -0,0 +1,157 @@ +# Functions + +## Size & Scope + +- Functions should only do one thing, and should be small. If a function is not small, break it up into smaller functions. +- Avoid mixing high level code with low level code in the same function. + +## Naming + +- Local functions should be named in camelCase format to differentiate from natives and standard lua library functions +- Global functions should be named in PascalCase format +- Functions should also be named with a leading verb. + +**BAD:** +```lua +function player() +function playerDrop() +``` + +**GOOD:** +```lua +function getPlayerObject() +function dropPlayer() +``` + +## Parameters + +### Limit Number of Parameters + +If needing more than 3 or so parameters for a single function, that may be a sign that the parameters can be grouped within a table and passed as a object, rather than as individual arguments. + +**BAD:** +```lua +function createChar(name, age, height, birthday, nationality) + +end +``` + +**GOOD:** +```lua +function createChar(char) + +end +``` + +### Avoid boolean parameters in APIs + +Boolean parameters are a signal that a function is doing two things. Instead, call two different functions that each do one thing. While what is considered an API is ambiguous, a good rule of thumb is not to include boolean parameters in global or exported functions. + +**BAD:** +```lua +function PrintEmotionalState(isHappy) + if isHappy then + print("happy") + else + print("sad") + end +end +``` + +**GOOD:** +```lua +function PrintHappy() + print("happy") +end + +function PrintSad() + print("sad") +end +``` + +### Avoid passing implied functions as arguments + +Instead declare the function in a local variable and pass the variable as the argument. This has major performance improvements if the calling function is invoked more than once. Some functions such as CreateThread are often only invoked once, so there wouldn't be any performance improvement to localizing the argument function. However, it's still recommended anyway as a defensive measure to avoid the issue entirely if the code were to change in the future. + +**BAD:** +```lua +someFunction(function() + +end) +``` + +**GOOD:** +```lua +local function myFunction() + +end +someFunction(myFunction) +``` + +### Parameter Overloads + +Be careful overloading a function. Overloading can be a smell that a function is doing more than one thing. Overloads are useful as wrapper functions, providing different ways to call the same underlying function. + +### Optional Parameters + +Required parameters should come before optional ones. + +## Export Documentation + +Exports should have a lua-language-server annotation to declare the API: + +```lua +--- Puts a space between a first and last name +---@param first string first name +---@param last string last name +---@return string full name +local function formatName(first, last) + return first .. ' ' .. last +end + +exports('formatName', formatName) +``` + +### Keep returned values small in size + +Since returned values are passed-by-value, there can be a significant performance cost to returning large payloads. Benchmarks show it is more performant to have many export calls that return a small amount of data each, than few export calls that return large payloads, even if the total number of bytes transferred is the same. Providing accessor exports instead of direct access to tables also makes your API more flexible to future changes. + +**BAD:** +```lua +exports('GetTable', function() + return myTable +end) +``` + +**GOOD:** +```lua +exports('GetValue', function(key) + return myTable[key] +end) +``` + +## Use guard clauses + +Often before doing the "real" work of a function, certain pre-conditions must be met. Guard clauses are conditional statements that provide early returns to check certain conditions. This allows the reader to also exit early, rather than reading the entire function. + +Additionally, using guard clauses avoids nesting, which can make code difficult to read. Sometimes though, a simple if statement reads just fine. Use your best judgment. + +**BAD:** +```lua +local function getFullName(first, last) + if not nameHidden and first and last then + return first .. last + else + return nil + end +end +``` + +**GOOD:** +```lua +local function getFullName(first, last) + if nameHidden then return end + if not first or not last then return end + return first .. last +end +``` diff --git a/.agents/skills/lua-basics/reference-links.md b/.agents/skills/lua-basics/reference-links.md new file mode 100644 index 0000000..ba7473e --- /dev/null +++ b/.agents/skills/lua-basics/reference-links.md @@ -0,0 +1,17 @@ +# Reference Links + +## Official Documentation + +- **Effective FiveM Lua**: https://manason.github.io/effective-fivem-lua/ +- **Lua 5.4 Reference Manual**: https://www.lua.org/manual/5.4/ +- **Programming in Lua (book)**: https://www.lua.org/pil/ + +## Specific Topics + +- **Functions**: https://manason.github.io/effective-fivem-lua/functions/ +- **Tables**: https://manason.github.io/effective-fivem-lua/tables/ +- **Variables**: https://manason.github.io/effective-fivem-lua/variables/ +- **Conditionals**: https://manason.github.io/effective-fivem-lua/conditionals/ +- **Error Handling**: https://manason.github.io/effective-fivem-lua/errors/ +- **Structure/Scope**: https://manason.github.io/effective-fivem-lua/structure/ +- **Events**: https://manason.github.io/effective-fivem-lua/events/ diff --git a/.agents/skills/lua-basics/tables.md b/.agents/skills/lua-basics/tables.md new file mode 100644 index 0000000..2b9334f --- /dev/null +++ b/.agents/skills/lua-basics/tables.md @@ -0,0 +1,124 @@ +# Tables + +## Imply Array Indices + +Other languages don't allow declaring an array with explicit indices. Unless the keys have important meaning that needs to be made clear to the reader, they should be implied. + +**BAD:** +```lua +local myTable = { + [1] = "first index", + [2] = "second index", + [3] = "third index" +} +``` + +**GOOD:** +```lua +local myTable = { + "first index", + "second index", + "third index" +} +``` + +## Dereferencing + +### Prefer object access for constant keys, and array access for non-constant keys + +```lua +local company = { + boss = "Sam" +} +``` + +**BAD:** +```lua +local boss = company["boss"] +``` + +**GOOD:** +```lua +local boss = company.boss +``` + +### Extract duplicate table dereferences into local variables + +This is both a readability and performance boost. + +**BAD:** +```lua +local concatenation = myTable["key"] .. myTable["key"] +``` + +**GOOD:** +```lua +local myTableValue = myTable["key"] +local concatenation = myTableValue .. myTableValue +``` + +## Avoid table.insert() + +It has horrible performance. It should only be used if needing to insert into an array at a specific index that is not the last index. + +### Inserting at the end of a table + +**BAD:** +```lua +table.insert(myTable, "value") +``` + +**GOOD:** +```lua +myTable[#myTable + 1] = "value" +``` + +### Inserting/Overwriting a given key + +**BAD:** +```lua +table.insert(myTable, "key", "value") +``` + +**GOOD:** +```lua +myTable["key"] = "value" +``` + +## Use numeric for loops when iterating over an array + +This is a performance boost. + +**BAD:** +```lua +for k, v in pairs(myArray) do + print(k .. ", " .. v) +end +``` + +**GOOD:** +```lua +for i=1, #myArray do + print(i .. ", " .. myArray[i]) +end +``` + +## Maintain your own array size variable + +There is a significant performance difference for large arrays as #array is an O(n) operation. Note that sometimes iterating through the entire array to find the size is preferable, but a common pattern of starting with an empty array and populating it in a loop should use an array size variable. + +**BAD:** +```lua +for i = 1, 100 do + myArray[#myArray+1] = i +end +``` + +**GOOD:** +```lua +local myArraySize = 0 +for i = 1, 100 do + myArraySize += 1 + myArray[myArraySize] = i +end +``` diff --git a/.agents/skills/lua-basics/variables.md b/.agents/skills/lua-basics/variables.md new file mode 100644 index 0000000..31e1c46 --- /dev/null +++ b/.agents/skills/lua-basics/variables.md @@ -0,0 +1,58 @@ +# Variables + +## Naming + +### Name constants using ALL_CAPS + +```lua +local MY_CONSTANT = "constant value" +MY_GLOBAL_CONSTANT = "another constant value" +``` + +### camelCase non-constant local variable names + +```lua +local myVariable = "variable value" +``` + +### PascalCase non-constant global variable names + +```lua +MyGlobalVariable = "global variable value" +``` + +### Use underscore "_" as the name of a variable that cannot be deleted but is unused + +```lua +local function printValues(map) + for _, v in pairs(map) do + print(v) + end +end +``` + +### Enums Vs Booleans + +Enums should be used to reflect the state of something when more than two options exist. A common anti-pattern is using multiple booleans to reflect the state. This is confusing and problematic, because the code then needs to defend against impossible states, as the combination of booleans is able to represent more states than is desired. It also makes the code more opaque and harder to reason about. What does it mean if isWalking and isRunning are both false? That we don't know? Is the state idle? Or maybe swimming? + +**BAD:** +```lua +local isWalking = false +local isRunning = false +``` + +**GOOD:** +```lua +local MOVEMENT = { + UNKNOWN = 1, + WALKING = 2, + RUNNING = 3 +} +local movementState = MOVEMENT.UNKNOWN +``` + +Representing the state in an enum this way also makes it easier to modify in the future to add more states. Such as idle, swimming, flying, falling, etc. Adding an UNKNOWN field is useful when the enum isn't exhaustive, as a catch all to represent any other state. + +## Location + +Local variables within a function should be declared as close as possible to the place where they are used. This limits what the developer must keep in their head while reading the code. Local variables declared outside of a function should be declared at the top of the file, grouped together. Global variables should be declared at the top of the file grouped together. client/server global variables should only be declared within a single client/server file. This helps keep things organized instead of spreading random globals around the resource. diff --git a/.agents/skills/oxlib/SKILL.md b/.agents/skills/oxlib/SKILL.md new file mode 100644 index 0000000..fddbf33 --- /dev/null +++ b/.agents/skills/oxlib/SKILL.md @@ -0,0 +1,45 @@ +--- +name: oxlib +description: "Ox Lib for FiveM - UI (notify, alert, input, menu, progress), callbacks client-server, addCommand, zones (poly/box/sphere), keybinds, shared utilities. Use when writing resources that need ox_lib or when the user mentions notifications, dialogs, menus, zones/areas, or client-server communication." +author: germanfndez +version: "1.0.0" +mcp-server: projecthub +--- + +# Ox Lib + +Standalone library for FiveM: reusable UI, callbacks, commands, and shared modules. Used by many Ox resources (ox_inventory, ox_target, etc.). Always prefer ox_lib over custom NUI or legacy patterns when the user wants notifications, dialogs, menus, or client-server calls. + +## When to use + +- User asks for notifications, alert dialogs, input dialogs, menus, progress bars, or TextUI. +- Client needs to call server (or server calls client): use `lib.callback` / `lib.callback.await` and `lib.callback.register`. +- Registering server commands with help and params: use `lib.addCommand`. +- Keybinds, context menus, skill checks, locales, or shared helpers (lib.table, lib.string, etc.). +- **Zones**: “when player enters/leaves area”, “is player inside” — use `lib.zones.poly`, `lib.zones.box`, `lib.zones.sphere` (prefer client; server has limited support for onEnter/onExit/inside). + +## Setup + +- In `fxmanifest.lua`: `shared_scripts { '@ox_lib/init.lua' }`. Optional: `ox_libs { 'locale', 'callback', ... }` to preload modules. + +## Rules + +Read the rule that matches what you're doing: + +- **rules/init.md** — Adding ox_lib to fxmanifest, shared_script, ox_libs. +- **rules/callback.md** — Client↔server: `lib.callback`, `lib.callback.await`, `lib.callback.register`. +- **rules/interface.md** — UI: `lib.notify`, `lib.alertDialog`, `lib.inputDialog`; icons (Font Awesome 6). +- **rules/addCommand.md** — Server commands: `lib.addCommand` with help, params, restricted. +- **rules/zones.md** — Zones: `lib.zones.poly`, `lib.zones.box`, `lib.zones.sphere`; onEnter, onExit, inside; remove, contains, setDebug. + +## References (look up if not covered in the rules above) + +If something isn't covered in the rules above, check the official docs: + +- **Ox Lib (index):** https://coxdocs.dev/ox_lib +- **Interface (notify, alert, input, menu, progress, textui):** https://coxdocs.dev/ox_lib/Modules/Interface +- **Callback (client/server):** https://coxdocs.dev/ox_lib/Modules/Callback/Lua/Server and …/Client +- **AddCommand:** https://coxdocs.dev/ox_lib/Modules/AddCommand/Server +- **Zones:** https://coxdocs.dev/ox_lib/Modules/Zones/Shared +- **AddKeybind:** https://coxdocs.dev/ox_lib/Modules/AddKeybind/Client +- **Locale, Table, String, Math, etc.:** navigate from https://coxdocs.dev/ox_lib diff --git a/.agents/skills/oxlib/addCommand.md b/.agents/skills/oxlib/addCommand.md new file mode 100644 index 0000000..30e5741 --- /dev/null +++ b/.agents/skills/oxlib/addCommand.md @@ -0,0 +1,30 @@ +# addCommand — Server commands with validation and suggestions + +Use `lib.addCommand` (server-side) to register commands with help text, parameter validation, and optional permission (restricted). Prefer over raw `RegisterCommand` when you want typed args and chat suggestions. + +```lua +lib.addCommand('giveitem', { + help = 'Give an item to a player', + restricted = 'group.admin', -- or true (ace only) or array of permissions + params = { + { name = 'target', type = 'playerId', help = 'Target player server id' }, + { name = 'item', type = 'string', help = 'Item name' }, + { name = 'count', type = 'number', help = 'Amount', optional = true }, + }, +}, function(source, args, raw) + local target = args.target + local item = args.item + local count = args.count or 1 + -- ... +end) +``` + +**Param types:** `'number'`, `'playerId'`, `'string'`, `'longString'`. Use `optional = true` for optional params. + +**Multiple names for the same command:** + +```lua +lib.addCommand({'giveitem', 'gi'}, { help = '...', params = { ... } }, cb) +``` + +Docs: [AddCommand (Server) – coxdocs.dev](https://coxdocs.dev/ox_lib/Modules/AddCommand/Server). diff --git a/.agents/skills/oxlib/callback.md b/.agents/skills/oxlib/callback.md new file mode 100644 index 0000000..f94015a --- /dev/null +++ b/.agents/skills/oxlib/callback.md @@ -0,0 +1,61 @@ +# callback — Client ↔ Server communication + +Use `lib.callback` for request/response between client and server. Prefer this over TriggerServerCallback/RegisterNetCallback patterns. + +## Server: register a callback (handle client requests) + +```lua +-- Server: register handler. First arg is source (player id), then any args sent by client. +lib.callback.register('myresource:getData', function(source, key) + local value = GetStoredValue(key) + return value +end) +``` + +## Client: call server (callback style) + +```lua +lib.callback('myresource:getData', false, function(value) + if value then + print(value) + end +end, 'someKey') +``` + +Second argument is `delay` (number or `false`): cooldown in ms before the callback can be triggered again; use `false` for no limit. + +## Client: call server (await — yields until response) + +```lua +local value = lib.callback.await('myresource:getData', false, 'someKey') +if value then + print(value) +end +``` + +## Server: call client + +```lua +-- Server: trigger client callback +lib.callback('ox:getNearbyVehicles', source, function(vehicles) + for i = 1, #vehicles do + -- use vehicles[i] + end +end, radius) +``` + +```lua +-- Server: await client response +local vehicles = lib.callback.await('ox:getNearbyVehicles', source, radius) +``` + +## Client: register a callback (handle server requests) + +```lua +lib.callback.register('ox:getNearbyVehicles', function(radius) + local coords = GetEntityCoords(cache.ped) + return lib.getNearbyVehicles(coords, radius, true) +end) +``` + +Use a unique name (e.g. `resourcename:action`) to avoid clashes. Full docs: [Callback – coxdocs.dev](https://coxdocs.dev/ox_lib/Modules/Callback/Lua/Server) and [Client](https://coxdocs.dev/ox_lib/Modules/Callback/Lua/Client). diff --git a/.agents/skills/oxlib/init.md b/.agents/skills/oxlib/init.md new file mode 100644 index 0000000..e808b60 --- /dev/null +++ b/.agents/skills/oxlib/init.md @@ -0,0 +1,34 @@ +# init — Adding ox_lib to a resource + +Add ox_lib as a shared script so `lib` (and optionally `cache`, `require`) are available in client and server. + +**fxmanifest.lua** + +```lua +fx_version 'cerulean' +game 'gta5' + +shared_scripts { + '@ox_lib/init.lua', +} +-- Or if it's the only shared script: +-- shared_script '@ox_lib/init.lua' + +client_scripts { 'client.lua' } +server_scripts { 'server.lua' } +``` + +Optional: preload specific modules so they are available without `lib.require()`: + +```lua +ox_libs { + 'locale', + 'callback', + 'math', + 'table', +} +``` + +Modules can also be loaded dynamically with `lib.require('callback')` or by calling `lib.callback`, `lib.notify`, etc. (ox_lib loads them on first use). + +Ensure the resource `ox_lib` is started before your resource (e.g. in server.cfg or `ensure ox_lib`). diff --git a/.agents/skills/oxlib/interface.md b/.agents/skills/oxlib/interface.md new file mode 100644 index 0000000..9b357f5 --- /dev/null +++ b/.agents/skills/oxlib/interface.md @@ -0,0 +1,60 @@ +# interface — Notifications, alert dialog, input dialog + +The UI is shown on the **client** (player screen). You can invoke these functions from **client** or from **server**: from server, trigger the client (e.g. `TriggerClientEvent`) so the client runs `lib.notify` / `lib.alertDialog` / etc., or use ox_lib’s server-side exports when available (e.g. to send a notification to a specific player). Icons use Font Awesome 6; default type is `solid`. For brand icons use a table: `icon = {'fab', 'apple'}`. + +## lib.notify — Notifications + +```lua +lib.notify({ + title = 'Title', + description = 'Description (markdown supported)', + type = 'success', -- 'inform' | 'error' | 'success' | 'warning' + duration = 3000, + position = 'top-right', + icon = 'check', +}) +``` + +Optional: `id` (string) for a unique notification so it only shows once when spammed; `iconColor`, `style`, `sound`, etc. See [Notifications](https://coxdocs.dev/ox_lib/Modules/Interface/Client/notify). + +## lib.alertDialog — Simple alert / confirm + +Returns `'confirm'` or `'cancel'` (or `nil` if closed). Call from client, or from server by triggering the client. + +```lua +local result = lib.alertDialog({ + header = 'Title', + content = 'Body with **markdown** support.', + centered = true, + cancel = true, + labels = { cancel = 'Cancel', confirm = 'OK' }, +}) +if result == 'confirm' then + -- player pressed OK +end +``` + +## lib.inputDialog — Form inputs + +Takes a heading, rows (array of field definitions), and optional options. Returns a table (array) of values by index, or `nil` if cancelled. Call from client, or from server by triggering the client. + +```lua +local input = lib.inputDialog('Dialog title', { + { type = 'input', label = 'Name', placeholder = 'Your name', required = true }, + { type = 'number', label = 'Amount', min = 1, max = 100, default = 1 }, + { type = 'checkbox', label = 'Accept' }, +}) +if not input then return end +local name, amount, accepted = input[1], input[2], input[3] +``` + +Row types: `input`, `number`, `checkbox`, `select`, `multi-select`, `slider`, `color`, `date`, `time`, `textarea`. See [Input Dialog](https://coxdocs.dev/ox_lib/Modules/Interface/Client/input). + +## Other UI + +- **lib.progress** — progress bar with duration/cancel. +- **lib.showTextUI** / **lib.hideTextUI** — TextUI. +- **lib.context** — context menu. +- **lib.menu** — list menu. + +Full list: [Interface – coxdocs.dev](https://coxdocs.dev/ox_lib/Modules/Interface). diff --git a/.agents/skills/oxlib/zones.md b/.agents/skills/oxlib/zones.md new file mode 100644 index 0000000..ea04876 --- /dev/null +++ b/.agents/skills/oxlib/zones.md @@ -0,0 +1,68 @@ +# zones — Poly, box, and sphere zones (lib.zones) + +Faster alternative to PolyZone. Use for “when player enters/leaves area” or “is player inside area”. **Note:** Server-side zones have limited support: `onEnter`, `onExit`, and `inside` do not work on server; use client for full behavior. + +Reference: [Zones (Shared) – coxdocs.dev](https://coxdocs.dev/ox_lib/Modules/Zones/Shared). + +## lib.zones.poly — Polygon zone + +`points` = array of `vector3` defining the polygon; `thickness` = height (default `4`). + +```lua +local zone = lib.zones.poly({ + points = { + vec3(413.8, -1026.1, 29), + vec3(411.6, -1023.1, 29), + vec3(412.2, -1018.0, 29), + -- ... + }, + thickness = 2, + onEnter = function(self) print('entered', self.id) end, + onExit = function(self) print('exited', self.id) end, + inside = function(self) end, -- called every frame while inside + debug = true, +}) +``` + +## lib.zones.box — Box zone + +```lua +local zone = lib.zones.box({ + coords = vec3(442.5, -1017.6, 28.65), + size = vec3(2, 2, 2), -- default vec3(2, 2, 2) + rotation = 45, -- degrees, default 0 + onEnter = onEnter, + onExit = onExit, + inside = inside, + debug = true, +}) +``` + +## lib.zones.sphere — Sphere zone + +```lua +local zone = lib.zones.sphere({ + coords = vec3(442.5, -1017.6, 28.65), + radius = 2, -- default 2 + onEnter = onEnter, + onExit = onExit, + inside = inside, + debug = true, +}) +``` + +## Methods + +- **zone:remove()** — Removes the zone (data table can be reused later, e.g. with `lib.zones.poly(zone)`). +- **zone:contains(point)** — Returns `boolean` if `point` (vec3) is inside the zone. +- **zone:setDebug(true|false)** — Toggle debug draw; optional second arg: `vec4(r, g, b, a)` for color. + +## Utilities + +- **lib.zones.getAllZones()** — All registered zones. +- **lib.zones.getCurrentZones()** — Zones the player is currently inside (client). +- **lib.zones.getNearbyZones()** — Zones near the player (client). + +## Creating zones in-game + +Use the built-in zone creator: `/zone poly`, `/zone box`, or `/zone sphere`. Controls appear on the right; zones are saved to `ox_lib/created_zones.lua`. diff --git a/.agents/skills/oxmysql/SKILL.md b/.agents/skills/oxmysql/SKILL.md new file mode 100644 index 0000000..910ebd2 --- /dev/null +++ b/.agents/skills/oxmysql/SKILL.md @@ -0,0 +1,42 @@ +--- +name: oxmysql +description: "OxMySQL for FiveM - SQL integrations with MySQL/MariaDB. Use when writing or editing server-side database code: queries, inserts, updates, transactions, or any resource that uses oxmysql (query, insert, prepare, update, single, scalar, rawExecute, transaction)." +author: germanfndez +version: "1.0.0" +mcp-server: projecthub +--- + +# OxMySQL + +SQL integration for FiveM using OxMySQL (replacement for mysql-async / ghmattimysql). Server-side only. Use MariaDB over MySQL 8 for compatibility. + +## When to use + +- User asks for database queries, inserts, updates, or SQL in a FiveM resource. +- Editing or writing code that uses `MySQL.*` or `exports.oxmysql`. +- Designing tables, upserts, or transactions. + +## Setup +- Lua: `server_script '@oxmysql/lib/MySQL.lua'` in fxmanifest (above other scripts). + +## Rules + +Read the rule that matches what you're doing: + +- **rules/placeholders.md** — Safe parameters (`?` placeholders), avoid SQL injection. +- **rules/query.md** — `MySQL.query` / `MySQL.query.await`: SELECT returns rows; other statements return insertId/affectedRows. +- **rules/insert.md** — `MySQL.insert`: insert row, returns insert id. +- **rules/prepare.md** — `MySQL.prepare`: prepared statements, only `?` placeholders; faster for repeated queries. +- **rules/update.md** — `MySQL.update`: update rows, returns affected count. +- **rules/single.md** — `MySQL.single`: one row or nil. +- **rules/scalar.md** — `MySQL.scalar`: single value (one row, one column). +- **rules/rawExecute.md** — `MySQL.rawExecute`: raw execution, no automatic result shape. +- **rules/transaction.md** — `MySQL.transaction`: run multiple queries in a transaction. + +## References (look up if not covered in the rules above) + +If something isn't covered in the rules above, check the official docs: + +- **OxMySQL (index):** https://coxdocs.dev/oxmysql +- **Placeholders:** https://coxdocs.dev/oxmysql/placeholders +- **Functions (query, insert, prepare, update, single, scalar, rawExecute, transaction):** https://coxdocs.dev/oxmysql (Functions section) diff --git a/.agents/skills/oxmysql/insert.md b/.agents/skills/oxmysql/insert.md new file mode 100644 index 0000000..bf2eb23 --- /dev/null +++ b/.agents/skills/oxmysql/insert.md @@ -0,0 +1,26 @@ +# insert + +Inserts a row and returns the insert id (or nil/falsy on failure). + +**Lua (Promise)** + +```lua +local id = MySQL.insert.await('INSERT INTO `users` (identifier, firstname, lastname) VALUES (?, ?, ?)', { identifier, firstName, lastName }) +print(id) +``` + +**Lua (Callback)** + +```lua +MySQL.insert('INSERT INTO `users` (identifier, firstname, lastname) VALUES (?, ?, ?)', { identifier, firstName, lastName }, function(id) + print(id) +end) +``` + +**JavaScript** + +```js +const insertId = await MySQL.insert('INSERT INTO `users` (identifier, firstname, lastname) VALUES (?, ?, ?)', [identifier, firstName, lastName]); +``` + +Reference: [insert – coxdocs.dev](https://coxdocs.dev/oxmysql/Functions/insert). diff --git a/.agents/skills/oxmysql/placeholders.md b/.agents/skills/oxmysql/placeholders.md new file mode 100644 index 0000000..a094dac --- /dev/null +++ b/.agents/skills/oxmysql/placeholders.md @@ -0,0 +1,11 @@ +# Placeholders + +Use `?` for values; parameters as array or object. Prevents SQL injection. + +```lua +MySQL.scalar('SELECT `username` FROM `users` WHERE `identifier` = ? AND `group` = ?', { identifier, group }) +``` + +Named placeholders (`@name`) are deprecated; use positional `?` and pass array. For prepared statements use **rules/prepare.md** (only `?` and `??` for column names). + +Reference: [Placeholders – coxdocs.dev](https://coxdocs.dev/oxmysql/placeholders). diff --git a/.agents/skills/oxmysql/prepare.md b/.agents/skills/oxmysql/prepare.md new file mode 100644 index 0000000..729ee5f --- /dev/null +++ b/.agents/skills/oxmysql/prepare.md @@ -0,0 +1,29 @@ +# prepare + +Prepared statements: faster for repeated queries. **Only `?` (value) and `??` (column name) placeholders** — named placeholders throw. + +- Date does not return the datestring commonly used in FiveM. +- TINYINT(1) and BIT do not return boolean. +- SELECT result shape: column, row, or array of rows depending on columns/rows selected (unlike rawExecute). + +**Lua (Promise)** + +```lua +local response = MySQL.prepare.await('SELECT `firstname`, `lastname` FROM `users` WHERE `identifier` = ?', { identifier }) +``` + +**Lua (Callback)** + +```lua +MySQL.prepare('SELECT `firstname`, `lastname` FROM `users` WHERE `identifier` = ?', { identifier }, function(response) + -- use response +end) +``` + +**Upsert (insert or update on duplicate)** + +```lua +MySQL.prepare('INSERT INTO ox_inventory (owner, name, data) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE data = VALUES(data)', { owner, dbId, inventory }) +``` + +Reference: [prepare – coxdocs.dev](https://coxdocs.dev/oxmysql/Functions/prepare). diff --git a/.agents/skills/oxmysql/query.md b/.agents/skills/oxmysql/query.md new file mode 100644 index 0000000..3605b39 --- /dev/null +++ b/.agents/skills/oxmysql/query.md @@ -0,0 +1,37 @@ +# query + +SELECT returns all matching rows (array of rows). Other statements return insertId, affectedRows, etc. + +**Lua (Promise)** + +```lua +local response = MySQL.query.await('SELECT `firstname`, `lastname` FROM `users` WHERE `identifier` = ?', { identifier }) +if response then + for i = 1, #response do + local row = response[i] + print(row.firstname, row.lastname) + end +end +``` + +**Lua (Callback)** + +```lua +MySQL.query('SELECT `firstname`, `lastname` FROM `users` WHERE `identifier` = ?', { identifier }, function(response) + if response then + for i = 1, #response do + local row = response[i] + print(row.firstname, row.lastname) + end + end +end) +``` + +**JavaScript** + +```js +const rows = await MySQL.query('SELECT `firstname`, `lastname` FROM `users` WHERE `identifier` = ?', [identifier]); +// rows is array of objects +``` + +Reference: [query – coxdocs.dev](https://coxdocs.dev/oxmysql/Functions/query). diff --git a/.agents/skills/oxmysql/rawExecute.md b/.agents/skills/oxmysql/rawExecute.md new file mode 100644 index 0000000..5b2af64 --- /dev/null +++ b/.agents/skills/oxmysql/rawExecute.md @@ -0,0 +1,19 @@ +# rawExecute + +Executes raw SQL. Does not normalize result shape like query/prepare (SELECT returns raw result). Use when you need full control or non-standard result handling. + +**Lua (Promise)** + +```lua +local result = MySQL.rawExecute.await('DELETE FROM `sessions` WHERE `expires` < NOW()') +``` + +**Lua (Callback)** + +```lua +MySQL.rawExecute('DELETE FROM `sessions` WHERE `expires` < NOW()', {}, function(result) + -- raw result +end) +``` + +Prefer **query**, **insert**, **update**, **single**, **scalar**, or **prepare** when they match the use case; use rawExecute only when necessary. Reference: [rawExecute – coxdocs.dev](https://coxdocs.dev/oxmysql/Functions/rawExecute). diff --git a/.agents/skills/oxmysql/scalar.md b/.agents/skills/oxmysql/scalar.md new file mode 100644 index 0000000..010d71e --- /dev/null +++ b/.agents/skills/oxmysql/scalar.md @@ -0,0 +1,27 @@ +# scalar + +Returns a single value (one row, one column). Use for COUNT, one field, etc. + +**Lua (Promise)** + +```lua +local count = MySQL.scalar.await('SELECT COUNT(*) FROM `users` WHERE `group` = ?', { group }) +local name = MySQL.scalar.await('SELECT `username` FROM `users` WHERE `identifier` = ?', { identifier }) +``` + +**Lua (Callback)** + +```lua +MySQL.scalar('SELECT `username` FROM `users` WHERE `identifier` = ?', { identifier }, function(name) + if name then print(name) end +end) +``` + +**JavaScript** + +```js +const count = await MySQL.scalar('SELECT COUNT(*) FROM `users` WHERE `group` = ?', [group]); +const name = await MySQL.scalar('SELECT `username` FROM `users` WHERE `identifier` = ?', [identifier]); +``` + +Reference: [scalar – coxdocs.dev](https://coxdocs.dev/oxmysql/Functions/scalar). diff --git a/.agents/skills/oxmysql/single.md b/.agents/skills/oxmysql/single.md new file mode 100644 index 0000000..28c870e --- /dev/null +++ b/.agents/skills/oxmysql/single.md @@ -0,0 +1,29 @@ +# single + +Returns a single row (first match) or nil/null if none. + +**Lua (Promise)** + +```lua +local user = MySQL.single.await('SELECT * FROM `users` WHERE `identifier` = ?', { identifier }) +if user then + print(user.firstname, user.lastname) +end +``` + +**Lua (Callback)** + +```lua +MySQL.single('SELECT * FROM `users` WHERE `identifier` = ?', { identifier }, function(user) + if user then print(user.firstname) end +end) +``` + +**JavaScript** + +```js +const user = await MySQL.single('SELECT * FROM `users` WHERE `identifier` = ?', [identifier]); +// user is one object or null +``` + +Reference: [single – coxdocs.dev](https://coxdocs.dev/oxmysql/Functions/single). diff --git a/.agents/skills/oxmysql/transaction.md b/.agents/skills/oxmysql/transaction.md new file mode 100644 index 0000000..3e79eaa --- /dev/null +++ b/.agents/skills/oxmysql/transaction.md @@ -0,0 +1,35 @@ +# transaction + +Run multiple queries in a transaction. If any fails, the transaction is rolled back. + +**Lua (Promise)** + +```lua +MySQL.transaction.await({ + { query = 'UPDATE `accounts` SET `balance` = `balance` - ? WHERE `id` = ?', values = { amount, fromId } }, + { query = 'UPDATE `accounts` SET `balance` = `balance` + ? WHERE `id` = ?', values = { amount, toId } }, +}) +``` + +**Lua (Callback)** + +```lua +MySQL.transaction({ + { query = 'UPDATE `accounts` SET `balance` = `balance` - ? WHERE `id` = ?', values = { amount, fromId } }, + { query = 'UPDATE `accounts` SET `balance` = `balance` + ? WHERE `id` = ?', values = { amount, toId } }, +}, function(success) + if not success then -- rollback happened + end +end) +``` + +**JavaScript** + +```js +await MySQL.transaction([ + { query: 'UPDATE `accounts` SET `balance` = `balance` - ? WHERE `id` = ?', values: [amount, fromId] }, + { query: 'UPDATE `accounts` SET `balance` = `balance` + ? WHERE `id` = ?', values: [amount, toId] }, +]); +``` + +Reference: [transaction – coxdocs.dev](https://coxdocs.dev/oxmysql/Functions/transaction). diff --git a/.agents/skills/oxmysql/update.md b/.agents/skills/oxmysql/update.md new file mode 100644 index 0000000..f1ea186 --- /dev/null +++ b/.agents/skills/oxmysql/update.md @@ -0,0 +1,25 @@ +# update + +Updates rows; returns affected row count (or result object with affectedRows). + +**Lua (Promise)** + +```lua +local affected = MySQL.update.await('UPDATE `users` SET `lastname` = ? WHERE `identifier` = ?', { newLastName, identifier }) +``` + +**Lua (Callback)** + +```lua +MySQL.update('UPDATE `users` SET `lastname` = ? WHERE `identifier` = ?', { newLastName, identifier }, function(affected) + -- use affected +end) +``` + +**JavaScript** + +```js +const result = await MySQL.update('UPDATE `users` SET `lastname` = ? WHERE `identifier` = ?', [newLastName, identifier]); +``` + +Always use `?` placeholders for values. Reference: [update – coxdocs.dev](https://coxdocs.dev/oxmysql/Functions/update). diff --git a/.agents/skills/research/SKILL.md b/.agents/skills/research/SKILL.md new file mode 100644 index 0000000..0ba594a --- /dev/null +++ b/.agents/skills/research/SKILL.md @@ -0,0 +1,12 @@ +--- +name: research +description: Investigate a question against high-trust primary sources and capture the findings as a Markdown file in the repo. Use when the user wants a topic researched, docs or API facts gathered, or reading legwork delegated to a background agent. +--- + +Spin up a **background agent** to do the research, so you keep working while it reads. + +Its job: + +1. Investigate the question against **primary sources** — official docs, source code, specs, first-party APIs — not a secondary write-up of them. Follow every claim back to the source that owns it. +2. Write the findings to a single Markdown file, citing each claim's source. +3. Save it where the repo already keeps such notes; match the existing convention, and if there is none, put it somewhere sensible and say where. diff --git a/.agents/skills/research/agents/openai.yaml b/.agents/skills/research/agents/openai.yaml new file mode 100644 index 0000000..e18b96c --- /dev/null +++ b/.agents/skills/research/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Research" + short_description: "Research from high-trust sources" diff --git a/.agents/skills/resolving-merge-conflicts/SKILL.md b/.agents/skills/resolving-merge-conflicts/SKILL.md new file mode 100644 index 0000000..aadb3fc --- /dev/null +++ b/.agents/skills/resolving-merge-conflicts/SKILL.md @@ -0,0 +1,14 @@ +--- +name: resolving-merge-conflicts +description: "Use when you need to resolve an in-progress git merge/rebase conflict." +--- + +1. **See the current state** of the merge/rebase. Check git history, and the conflicting files. + +2. **Find the primary sources** for each conflict. Understand deeply why each change was made, and what the original intent was. Read the commit messages, check the PRs, check original issues/tickets. + +3. **Resolve each hunk.** Preserve both intents where possible. Where incompatible, pick the one matching the merge's stated goal and note the trade-off. Do **not** invent new behaviour. Always resolve; never `--abort`. + +4. Discover the project's **automated checks** and run them — typically typecheck, then tests, then format. Fix anything the merge broke. + +5. **Finish the merge/rebase.** Stage everything and commit. If rebasing, continue the rebase process until all commits are rebased. diff --git a/.agents/skills/resolving-merge-conflicts/agents/openai.yaml b/.agents/skills/resolving-merge-conflicts/agents/openai.yaml new file mode 100644 index 0000000..331ffb9 --- /dev/null +++ b/.agents/skills/resolving-merge-conflicts/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Resolving Merge Conflicts" + short_description: "Resolve merge and rebase conflicts" diff --git a/.agents/skills/to-spec/SKILL.md b/.agents/skills/to-spec/SKILL.md new file mode 100644 index 0000000..f3cca8d --- /dev/null +++ b/.agents/skills/to-spec/SKILL.md @@ -0,0 +1,75 @@ +--- +name: to-spec +description: Turn the current conversation into a spec and publish it to the project issue tracker — no interview, just synthesis of what you've already discussed. +disable-model-invocation: true +--- + +This skill takes the current conversation context and codebase understanding and produces a spec (you may know this document as a PRD). Do NOT interview the user — just synthesize what you already know. + +The issue tracker and triage label vocabulary should have been provided to you — run `/setup-matt-pocock-skills` if not. + +## Process + +1. Explore the repo to understand the current state of the codebase, if you haven't already. Use the project's domain glossary vocabulary throughout the spec, and respect any ADRs in the area you're touching. + +2. Sketch out the seams at which you're going to test the feature. Existing seams should be preferred to new ones. Use the highest seam possible. If new seams are needed, propose them at the highest point you can. The fewer seams across the codebase, the better - the ideal number is one. + +Check with the user that these seams match their expectations. + +3. Write the spec using the template below, then publish it to the project issue tracker. Apply the `ready-for-agent` triage label - no need for additional triage. + + + +## Problem Statement + +The problem that the user is facing, from the user's perspective. + +## Solution + +The solution to the problem, from the user's perspective. + +## User Stories + +A LONG, numbered list of user stories. Each user story should be in the format of: + +1. As an , I want a , so that + + +1. As a mobile bank customer, I want to see balance on my accounts, so that I can make better informed decisions about my spending + + +This list of user stories should be extremely extensive and cover all aspects of the feature. + +## Implementation Decisions + +A list of implementation decisions that were made. This can include: + +- The modules that will be built/modified +- The interfaces of those modules that will be modified +- Technical clarifications from the developer +- Architectural decisions +- Schema changes +- API contracts +- Specific interactions + +Do NOT include specific file paths or code snippets. They may end up being outdated very quickly. + +Exception: if a prototype produced a snippet that encodes a decision more precisely than prose can (state machine, reducer, schema, type shape), inline it within the relevant decision and note briefly that it came from a prototype. Trim to the decision-rich parts — not a working demo, just the important bits. + +## Testing Decisions + +A list of testing decisions that were made. Include: + +- A description of what makes a good test (only test external behavior, not implementation details) +- Which modules will be tested +- Prior art for the tests (i.e. similar types of tests in the codebase) + +## Out of Scope + +A description of the things that are out of scope for this spec. + +## Further Notes + +Any further notes about the feature. + + diff --git a/.agents/skills/to-spec/agents/openai.yaml b/.agents/skills/to-spec/agents/openai.yaml new file mode 100644 index 0000000..549e6f7 --- /dev/null +++ b/.agents/skills/to-spec/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "To Spec" + short_description: "Turn a conversation into a spec" +policy: + allow_implicit_invocation: false diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index c6f4a1e..09a02ca 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -1,3 +1,4 @@ +allowBuilds: + esbuild: true onlyBuiltDependencies: - esbuild -