ENH - update SMS branch from dev

This commit is contained in:
Eichenholz
2026-08-06 19:42:54 +02:00
219 changed files with 29678 additions and 1311 deletions
+78
View File
@@ -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/<feature>/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 13 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.
@@ -0,0 +1,5 @@
interface:
display_name: "Ask Matt"
short_description: "Find the right skill or workflow"
policy:
allow_implicit_invocation: false
+89
View File
@@ -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 <fixed-point>...HEAD` (three-dot, so the comparison is against the merge-base). Also note the list of commits via `git log <fixed-point>..HEAD --oneline`.
Before going further, confirm the fixed point resolves (`git rev-parse <fixed-point>`) 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.
@@ -0,0 +1,3 @@
interface:
display_name: "Code Review"
short_description: "Review a diff on standards and spec"
@@ -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.
@@ -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 13 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.
+114
View File
@@ -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.
@@ -0,0 +1,3 @@
interface:
display_name: "Codebase Design"
short_description: "Vocabulary for deep-module design"
@@ -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.
@@ -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.
+74
View File
@@ -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).
@@ -0,0 +1,3 @@
interface:
display_name: "Domain Modeling"
short_description: "Build and sharpen a domain model"
+47
View File
@@ -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
@@ -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 <const> = 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 <const> = 10.0
-- Compound operators
count += 1
count -= 1
count *= 2
count /= 2
-- to-be-closed variables
local file <close> = 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})
```
@@ -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')
```
@@ -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 <const> = 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
```
@@ -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)
```
@@ -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
@@ -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
```
@@ -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)
```
+31
View File
@@ -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.
@@ -0,0 +1,33 @@
# Client vs server
## Client scripts
- Run on **each players 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 |
+17
View File
@@ -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 doesnt 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.
+104
View File
@@ -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/
+48
View File
@@ -0,0 +1,48 @@
# Exports
Exports let one resource call functions exposed by another resource.
## Defining exports (Lua)
In your resources 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
+45
View File
@@ -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/
+296
View File
@@ -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 FiveMs `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; dont 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 dont 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)
```
@@ -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.
+65
View File
@@ -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)
+146
View File
@@ -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.
+26
View File
@@ -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.
+472
View File
@@ -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<T = any> {
success: boolean;
data?: T;
error?: string;
}
```
**app.ts:**
```typescript
import { PlayerData, NUIMessage, NUICallback } from './types';
window.addEventListener('message', (event: MessageEvent<NUIMessage>) => {
const { type, data } = event.data;
switch (type) {
case 'show':
showUI(data as PlayerData);
break;
// ...
}
});
async function getData<T>(endpoint: string, payload: any): Promise<NUICallback<T>> {
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/
+205
View File
@@ -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
<!-- Reference a JavaScript file in your resource -->
<script type="text/javascript" src="https://cfx-nui-my-resource/build/app.js"></script>
<!-- Reference a CSS file -->
<link rel="stylesheet" href="https://cfx-nui-my-resource/styles/main.css">
<!-- Reference an image -->
<img src="https://cfx-nui-my-resource/images/logo.png">
```
**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
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial; }
#menu { display: none; background: rgba(0,0,0,0.8); color: white; padding: 20px; }
.item { padding: 10px; cursor: pointer; }
.item:hover { background: rgba(255,255,255,0.1); }
</style>
</head>
<body>
<div id="menu">
<h2>Shop</h2>
<div id="items"></div>
<button onclick="closeMenu()">Close</button>
</div>
<script src="https://cfx-nui-my-resource/ui/app.js"></script>
</body>
</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
+309
View File
@@ -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/
+128
View File
@@ -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
<!-- Use https://cfx-nui-{resourceName}/ protocol -->
<script src="https://cfx-nui-my-resource/js/app.js"></script>
<link href="https://cfx-nui-my-resource/css/style.css" rel="stylesheet">
<img src="https://cfx-nui-my-resource/images/logo.png">
```
+385
View File
@@ -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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Resource UI</title>
<link rel="stylesheet" href="https://cfx-nui-my-resource/ui/css/style.css">
</head>
<body>
<div id="app">
<!-- Your UI content here -->
</div>
<script src="https://cfx-nui-my-resource/ui/js/app.js"></script>
</body>
</html>
```
## 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
<script src="./app.js"></script>
<script src="/ui/app.js"></script>
<script src="nui://my-resource/ui/app.js"></script>
```
✅ **Correct:**
```html
<script src="https://cfx-nui-my-resource/ui/app.js"></script>
```
### 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/
+25
View File
@@ -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.
+73
View File
@@ -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.
+7
View File
@@ -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.
@@ -0,0 +1,5 @@
interface:
display_name: "Grill Me"
short_description: "Sharpen a plan through interview"
policy:
allow_implicit_invocation: false
+12
View File
@@ -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.
@@ -0,0 +1,3 @@
interface:
display_name: "Grilling"
short_description: "Stress-test thinking one question at a time"
+29
View File
@@ -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.
+59
View File
@@ -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
```
+61
View File
@@ -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
```
+157
View File
@@ -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
```
@@ -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/
+124
View File
@@ -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
```
+58
View File
@@ -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.
+45
View File
@@ -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
+30
View File
@@ -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).
+61
View File
@@ -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).
+34
View File
@@ -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`).
+60
View File
@@ -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_libs 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).
+68
View File
@@ -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`.
+42
View File
@@ -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)
+26
View File
@@ -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).
+11
View File
@@ -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).
+29
View File
@@ -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).
+37
View File
@@ -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).
+19
View File
@@ -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).
+27
View File
@@ -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).
+29
View File
@@ -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).
+35
View File
@@ -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).
+25
View File
@@ -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).
+12
View File
@@ -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.
@@ -0,0 +1,3 @@
interface:
display_name: "Research"
short_description: "Research from high-trust sources"
@@ -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.
@@ -0,0 +1,3 @@
interface:
display_name: "Resolving Merge Conflicts"
short_description: "Resolve merge and rebase conflicts"
+75
View File
@@ -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.
<spec-template>
## 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 <actor>, I want a <feature>, so that <benefit>
<user-story-example>
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
</user-story-example>
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.
</spec-template>
@@ -0,0 +1,5 @@
interface:
display_name: "To Spec"
short_description: "Turn a conversation into a spec"
policy:
allow_implicit_invocation: false
+9 -1
View File
@@ -7,11 +7,14 @@ An iFruit account is optional. Unlinked devices retain local settings, alarms, m
## Requirements
- ESX Legacy (`es_extended`), Qbox (`qbx_core`), or QBCore (`qb-core`). The bridge selects a running supported framework when `Config.Bridge.Framework` is set to `"auto"`.
- A supported metadata inventory: `ox_inventory`, `qb-inventory`, `lj-inventory`, `qs-inventory`, `codem-inventory`, `core_inventory`, or `mf-inventory`. The bridge auto-detects a running provider and normalizes `metadata`/`info`, slots, counts, item mutations, and usable-item callbacks. `mf-inventory` requires ESX.
- A supported inventory: `ox_inventory`, `qb-inventory`, `lj-inventory`, `qs-inventory`, `codem-inventory`, `core_inventory`, `mf-inventory`, or `smx-inventory`. The bridge auto-detects a running provider and normalizes metadata, slots, counts, item mutations, and usable-item callbacks. `mf-inventory` and `smx-inventory` require ESX. Because SMX stores standard ESX items as stacks, its adapter persists one active Phone/SIM metadata record per player and item type in ESX player metadata.
- A non-stackable inventory item named `sky_phone`.
- Two unique, non-stackable inventory items named `sky_phone_sim_registered` and `sky_phone_sim_anonymous`. Their metadata is initialized automatically on first use, so shops and crafting recipes add plain items without supplying a number.
- `oxmysql` with MySQL/MariaDB.
- `pma-voice` when `Config.Calls.VoiceProvider` is set to `"pma"`.
- A FiveManage V3 Media API token for Camera photo/video uploads and Gallery deletion. Set the
server-only `Config.Media.FiveManage.ApiKey` in `sky_phone/config/media.lua`; the token is never
sent to NUI because clients receive temporary presigned upload URLs instead.
## Messages GIF provider
@@ -28,6 +31,11 @@ until their dedicated implementation is available.
Database migrations run automatically. Existing `sky_phone_mail_accounts` installations are renamed to `sky_phone_accounts` while preserving account IDs and mail foreign keys. iFruit passwords are intentional in-character credentials and remain plaintext `VARCHAR(64)` values; registration screens warn players never to reuse a real password.
Camera and Gallery media is stored in `sky_phone_media`. Signed-out captures belong to the current
IMEI; linking an iFruit account moves those rows into the account gallery so every linked phone sees
them. Signing out hides cloud media without deleting it. Factory reset removes device-local media
and attempts to delete its remote FiveManage files, while account-owned media remains in the cloud.
For a fresh manual database installation, import `sky_phone/sql/install.sql`. It contains the complete current table, key, index, collation, and foreign-key schema. Runtime migrations remain authoritative for upgrading an existing installation and must stay enabled.
Framework, inventory, callback, notification, and database integrations live under `sky_phone/source/bridge`. The resource has no dependency on any other Sky resource.
+8
View File
@@ -15,6 +15,14 @@
},
"dependencies": {
"emoji-picker-element-data": "^1.8.0",
"@tiptap/core": "^3.29.2",
"@tiptap/extension-placeholder": "^3.29.2",
"@tiptap/markdown": "^3.29.2",
"@tiptap/pm": "^3.29.2",
"@tiptap/starter-kit": "^3.29.2",
"@tiptap/vue-3": "^3.29.2",
"dompurify": "^3.4.13",
"fix-webm-duration": "^1.0.6",
"konsta": "~5.2.0",
"lucide-vue-next": "^0.525.0",
"pinia": "^3.0.3",
+628 -115
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -1,2 +1,4 @@
allowBuilds:
esbuild: true
onlyBuiltDependencies:
- esbuild
+135 -19
View File
@@ -11,6 +11,7 @@ import {
import { useRoute, useRouter } from 'vue-router'
import PhoneHomeIndicator from '@/components/PhoneHomeIndicator.vue'
import PhoneMediaCapture from '@/components/PhoneMediaCapture.vue'
import PhoneLockScreen from '@/components/PhoneLockScreen.vue'
import PhoneNotifications from '@/components/PhoneNotifications.vue'
import NotificationPhonePreview from '@/components/NotificationPhonePreview.vue'
@@ -20,11 +21,14 @@ import SimPhonePicker, {
} from '@/components/SimPhonePicker.vue'
import { PHONE_FRAME_IMAGES } from '@/config/appearance'
import { useClockStore } from '@/stores/clock'
import { useGamesStore } from '@/features/games/store'
import { useCallsStore } from '@/stores/calls'
import { useAccountStore } from '@/stores/account'
import { useMailStore } from '@/stores/mail'
import { useMessagesStore } from '@/stores/messages'
import { useMediaStore } from '@/stores/media'
import { useMarketplaceStore } from '@/stores/marketplace'
import { useAppStoreStore } from '@/stores/app-store'
import { useNotesStore } from '@/stores/notes'
import { useWeatherStore } from '@/stores/weather'
import {
@@ -34,6 +38,7 @@ import {
import { usePhoneStore, type PhoneOpenPayload } from '@/stores/phone'
import type { PhoneNotificationDevicePayload } from '@/types/device'
import type { MailCounts } from '@/types/mail'
import type { MarketplaceCounts } from '@/types/marketplace'
import type { PhoneCall } from '@/types/phone'
import { nuiCall } from '@/utils/nui'
import { formatTimer } from '@/utils/clock'
@@ -43,7 +48,9 @@ import SpringboardView from '@/views/SpringboardView.vue'
type AppMessage = {
type?: string
data?:
| CalendarReminderData
| MailEventData
| MarketplaceEventData
| MessagesEventData
| PhoneCall
| PhoneNotificationInput
@@ -72,6 +79,24 @@ type MessagesEventData = {
title?: string
}
type MarketplaceEventData = {
counts?: MarketplaceCounts
device?: PhoneNotificationDevicePayload
inquiryId?: string
listingId?: string
sender?: string
text?: string
title?: string
}
type CalendarReminderData = {
device?: PhoneNotificationDevicePayload
eventId?: string
eventTitle?: string
startsAt?: number
text?: string
title?: string
}
const REFERENCE_VIEWPORT_WIDTH = 1920
const REFERENCE_VIEWPORT_HEIGHT = 1080
const PHONE_BASE_SCALE = 0.69
@@ -80,16 +105,22 @@ const isDevelopment = import.meta.env.DEV
const phone = usePhoneStore()
const account = useAccountStore()
const clock = useClockStore()
const games = useGamesStore()
const calls = useCallsStore()
const mail = useMailStore()
const messages = useMessagesStore()
const media = useMediaStore()
const marketplace = useMarketplaceStore()
const appStore = useAppStoreStore()
const notes = useNotesStore()
const weather = useWeatherStore()
const notifications = useNotificationsStore()
const route = useRoute()
const router = useRouter()
const isAppRoute = computed(() => route.name === 'app')
const appTransitionName = computed(() =>
route.query.transition === 'app-switch' ? 'app-switch' : 'app-window',
)
const isLocked = ref(false)
const isUnlocking = ref(false)
const simPicker = ref<SimPickerPayload | null>(null)
@@ -120,15 +151,45 @@ function hydratePhone(payload: PhoneOpenPayload): void {
account.hydrate(payload.account ?? null)
notes.hydrate(payload.notes ?? [])
clock.hydrate(payload.device?.data.alarms?.payload)
games.hydrate(payload.device?.data.games?.payload)
media.hydrate(payload.device?.data.media?.payload)
appStore.hydrate(payload.device?.data.apps?.payload)
void mail.bootstrap(payload.account?.email ?? '')
if (payload.account?.email) void marketplace.loadCounts()
else marketplace.setCounts({ active: 0, unread: 0 })
void calls.bootstrap()
void messages.loadConversations()
}
async function hydrateDevelopmentPhone(): Promise<void> {
const response = await nuiCall<PhoneOpenPayload>('development:bootstrap')
if (response.success && response.data) {
hydratePhone(response.data)
return
}
hydratePhone({
account: null,
device: {
data: {},
imei: '356938035643809',
name: 'iFruit Phone',
sim: {
id: 'development-sim',
number: '5551234567',
registered: true,
type: 'registered',
},
},
notes: [],
token: 'development',
})
}
function onMessage(event: MessageEvent<AppMessage>): void {
if (event.data?.type === 'app:open') {
hydratePhone(event.data.data as PhoneOpenPayload)
void nuiCall('ui:opened')
} else if (event.data?.type === 'device:updated') {
hydratePhone(event.data.data as PhoneOpenPayload)
} else if (event.data?.type === 'app:close') {
@@ -166,6 +227,61 @@ function onMessage(event: MessageEvent<AppMessage>): void {
}
}
notifications.show(notification)
} else if (event.data?.type === 'marketplace:changed' && event.data.data) {
const data = event.data.data as MarketplaceEventData
if (data.counts) marketplace.setCounts(data.counts)
} else if (
event.data?.type === 'marketplace:new-message' &&
event.data.data
) {
const data = event.data.data as MarketplaceEventData
const notification: PhoneNotificationInput = {
appId: 'citymarkt',
subtitle: data.sender,
text:
data.text ??
phone.t('Apps.citymarkt.newMessage', { sender: data.sender ?? '' }),
title: data.title ?? phone.t('Apps.citymarkt.name'),
}
if (
data.device &&
(!phone.isOpen || data.device.imei !== phone.device?.imei)
) {
notification.device = {
imei: data.device.imei,
name: data.device.name,
preferences: parsePhonePreferences(data.device.settings ?? null),
}
}
notifications.show(notification)
void marketplace.loadCounts()
} else if (event.data?.type === 'calendar:reminder' && event.data.data) {
const data = event.data.data as CalendarReminderData
const startsAt = Number(data.startsAt) || Date.now()
const notification: PhoneNotificationInput = {
appId: 'calendar',
subtitle: new Intl.DateTimeFormat(phone.lang, {
hour: '2-digit',
minute: '2-digit',
}).format(startsAt),
text:
data.text ??
phone.t('Apps.calendar.reminderNotification', {
title: data.eventTitle ?? '',
}),
title: data.title ?? phone.t('Apps.calendar.name'),
}
if (
data.device &&
(!phone.isOpen || data.device.imei !== phone.device?.imei)
) {
notification.device = {
imei: data.device.imei,
name: data.device.name,
preferences: parsePhonePreferences(data.device.settings ?? null),
}
}
notifications.show(notification)
} else if (event.data?.type === 'contacts:changed') {
void calls.loadContacts()
} else if (event.data?.type === 'messages:changed') {
@@ -239,6 +355,11 @@ function unlockPhone(): void {
}, 720)
}
function unlockCamera(): void {
unlockPhone()
window.setTimeout(() => void router.push('/apps/camera'), 0)
}
onMounted(() => {
window.addEventListener('message', onMessage)
window.addEventListener('keydown', onKeydown)
@@ -274,22 +395,7 @@ onMounted(() => {
}
}, 1000)
if (isDevelopment) {
hydratePhone({
account: null,
device: {
data: {},
imei: '356938035643809',
name: 'iFruit Phone',
sim: {
id: 'development-sim',
number: '5551234567',
registered: true,
type: 'registered',
},
},
notes: [],
token: 'development',
})
void hydrateDevelopmentPhone()
if (new URLSearchParams(window.location.search).has('simPickerPreview')) {
simPicker.value = {
choices: [
@@ -350,6 +456,7 @@ onBeforeUnmount(() => {
</script>
<template>
<PhoneMediaCapture />
<SimPhonePicker
v-if="simPicker"
:choices="simPicker.choices"
@@ -367,6 +474,7 @@ onBeforeUnmount(() => {
class="phone-stage"
:class="{
'phone-stage--dev': isDevelopment,
'phone-stage--landscape': phone.cameraLandscape,
'phone-stage--peek': notifications.isPeeking,
}"
:style="phoneResolutionStyle"
@@ -406,13 +514,21 @@ onBeforeUnmount(() => {
<PhoneStatusBar v-if="!isLocked" />
<SpringboardView />
<RouterView v-slot="{ Component }">
<Transition name="app-window">
<component :is="Component" v-if="isAppRoute" />
<Transition :name="appTransitionName">
<component
:is="Component"
v-if="isAppRoute"
:key="route.path"
/>
</Transition>
</RouterView>
<PhoneHomeIndicator v-if="!isLocked" />
<Transition name="lock-screen">
<PhoneLockScreen v-if="isLocked" @unlock="unlockPhone" />
<PhoneLockScreen
v-if="isLocked"
@camera="unlockCamera"
@unlock="unlockPhone"
/>
</Transition>
<PhoneNotifications
:notification="notifications.current"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="Calendar">
<defs>
<linearGradient id="bg" x1="32" y1="20" x2="224" y2="236" gradientUnits="userSpaceOnUse">
<stop stop-color="#ff765f"/>
<stop offset="1" stop-color="#e93147"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="150%">
<feDropShadow dx="0" dy="10" stdDeviation="10" flood-color="#8c1021" flood-opacity=".32"/>
</filter>
</defs>
<rect width="256" height="256" rx="58" fill="url(#bg)"/>
<g filter="url(#shadow)">
<rect x="42" y="50" width="172" height="164" rx="28" fill="#fff"/>
<path d="M42 78c0-15.5 12.5-28 28-28h116c15.5 0 28 12.5 28 28v26H42V78Z" fill="#f8e9eb"/>
<rect x="77" y="36" width="14" height="42" rx="7" fill="#fff"/>
<rect x="165" y="36" width="14" height="42" rx="7" fill="#fff"/>
<g fill="#d5d8df">
<rect x="67" y="124" width="26" height="21" rx="7"/><rect x="104" y="124" width="26" height="21" rx="7"/><rect x="141" y="124" width="26" height="21" rx="7"/>
<rect x="67" y="157" width="26" height="21" rx="7"/><rect x="141" y="157" width="26" height="21" rx="7"/>
</g>
<rect x="101" y="153" width="32" height="29" rx="9" fill="#ff4259"/>
<path d="m109 167 7 7 11-14" fill="none" stroke="#fff" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<circle cx="201" cy="198" r="27" fill="#272b36" stroke="#fff" stroke-width="6"/>
<path d="M201 183v16l10 7" fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Some files were not shown because too many files have changed in this diff Show More