> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dietrich-development.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Setup

> Install and configure DD Pinboard: board locations, collaboration, permissions, image hosts and showcase presets

## Requirements

* RedM with VORP or RSG, detected through the bundled framework bridge
* `oxmysql` and a configured MySQL / MariaDB database with JSON column support
* The built release package, including `ui/dist/`

No separate interaction framework is required. Keep the resource folder named `dd_pinboard`; the bundled image URLs and UI callbacks use that name.

## Installation

<Steps>
  <Step title="Install the resource">
    Extract the release's `dd_pinboard` folder into your server's resources directory.
  </Step>

  <Step title="Set the start order">
    Start your framework and `oxmysql` before `dd_pinboard` in `server.cfg`.

    ```text theme={null}
    ensure oxmysql
    ensure dd_pinboard
    ```
  </Step>

  <Step title="Configure locations">
    Edit `config/config.lua`. Keep each board's `key` unique and stable: changing it points to a different board. Set coordinates, labels, edit jobs and optional background or size restrictions.
  </Step>

  <Step title="Grant admin access">
    Add this permission for the server's admin group if it should clear or seed boards:

    ```text theme={null}
    add_ace group.admin dd_pinboard.admin allow
    ```
  </Step>

  <Step title="Start and check">
    Start the resource and walk to a configured location. Database tables are created automatically; a board row is created on first open or when seeded. Keep `Config.Debug = false` on a production server.
  </Step>
</Steps>

## Configuration

The following is the shipped `config/config.lua` for version 1.3.0. Restart the resource after changing Lua configuration.

<Accordion title="Full configuration">
  ```lua theme={null}
  Config = {}

  -- Verbose console logging via Bridge.Debug(). Also enables the /pinboard <key> dev command,
  -- which lets admins open a board from anywhere. Never ship a server with this on.
  Config.Debug = false

  -- Framework is auto-detected (VORP / RSG). Set to 'vorp' or 'rsg' to force it.
  Config.Framework = nil

  -- Language is set in config/language.lua (DDLanguage) or server-wide via `setr dd:language de`.

  -- ---------------------------------------------------------------------------
  -- Board locations
  -- ---------------------------------------------------------------------------
  -- One persistent board per entry. `key` is the database identifier of the board: keep it
  -- stable once players have used the board, renaming it creates a fresh empty board.
  --   key      unique id (letters, digits, underscore)
  --   label    board title shown in the UI header
  --   coords   world position of the interaction point
  --   radius   interaction range in metres (default Config.interactRadius)
  --   editJobs optional list of job names; only members may edit (everyone may still read).
  --            Omit or leave nil for "everyone may edit".
  --   bgStyle  optional fixed background: 'cork', 'parchment', 'dark', 'slate' or 'linen'. The
  --            board always shows it, the Background selector is gone from the toolbar, and a
  --            showcase preset's own background is ignored here. Omit to let the players choose.
  --   size     optional { w = <px>, h = <px> }: the board is a sheet of that size (in board
  --            pixels, at zoom 1) instead of endless, hung in a wooden frame with the game world
  --            visible around it. Cards stay on the sheet and the view cannot be panned off it.
  --            Omit for an endless board.
  Config.boardLocations = {
      {
          key    = 'saint_denis_precinct',
          label  = 'Saint Denis Precinct Board',
          coords = vector3(2496.9163, -1300.8125, 48.9569),
          -- editJobs = { 'police' },
      },
      {
          key     = 'valentine_saloon',
          label   = 'Valentine Saloon Board',
          coords  = vector3(-299.33, 802.11, 118.46),
          -- A saloon notice board: always cork, and only so big.
          bgStyle = 'cork',
          size    = { w = 2400, h = 1600 },
      },
      -- Ready-made demonstration board. Fill it with `/pinboard_seed showcase_case_board`
      -- (see Config.showcasePresets in config/showcase.lua) or delete this entry.
      {
          key    = 'showcase_case_board',
          label  = 'Sheriff\u{2019}s Case Board',
          coords = vector3(-278.05, 803.63, 119.34),
      },
  }

  -- ---------------------------------------------------------------------------
  -- Interaction
  -- ---------------------------------------------------------------------------
  Config.interactRadius = 2.0     -- metres, default per location
  Config.interactKey    = 'E'     -- 'E', 'G', 'R', 'SPACE' or a raw RedM control hash
  Config.interactHoldMs = nil     -- e.g. 500 for hold-to-open; nil = press

  -- ---------------------------------------------------------------------------
  -- Permissions (server-side)
  -- ---------------------------------------------------------------------------
  Config.Permissions = {
      -- May this player edit the given board location? Reading is always allowed.
      -- `location` is the entry from Config.boardLocations.
      edit = function(src, location)
          if not location.editJobs then return true end
          local job = Bridge.getCharacterJob(src)
          for _, allowed in ipairs(location.editJobs) do
              if job == allowed then return true end
          end
          return false
      end,

      -- Admin actions: the clear command and the /pinboard dev command.
      admin = function(src)
          return IsPlayerAceAllowed(src, 'dd_pinboard.admin')
      end,
  }

  -- ---------------------------------------------------------------------------
  -- Commands
  -- ---------------------------------------------------------------------------
  Config.Commands = {
      clear = 'pinboard_clear',   -- /pinboard_clear <key>          wipes every card on a board (admin)
      seed  = 'pinboard_seed',    -- /pinboard_seed <key> [preset]  fills a board from a showcase preset (admin)
      open  = 'pinboard',         -- /pinboard <key>                opens a board from anywhere (Config.Debug and ace dd_pinboard.admin)
  }

  -- ---------------------------------------------------------------------------
  -- Limits (server-enforced)
  -- ---------------------------------------------------------------------------
  Config.maxCardsPerBoard  = 200
  Config.maxEdgesPerBoard  = 400
  Config.maxCardPayloadLen = 32 * 1024   -- bytes of JSON per card (rich text, drawing strokes)
  Config.maxCardSize       = 10000       -- px per side; a note stops growing and a resize stops here
  Config.maxImageUrlLen    = 1024

  -- Image cards load their picture straight from the URL a player pastes. Leave the list empty
  -- to allow any host, or restrict it to hosts you trust (subdomains of a listed host are allowed).
  Config.allowedImageHosts = {
      -- 'i.imgur.com',
      -- 'cdn.discordapp.com',
      -- 'media.discordapp.net',
  }

  -- ---------------------------------------------------------------------------
  -- Live collaboration
  -- ---------------------------------------------------------------------------
  -- Everyone at a board who passes Config.Permissions.edit edits it at the same time. There is
  -- no board-wide lock: while a player is dragging, typing in or drawing on ONE card, that card
  -- is claimed for them and shows as busy to the others; every other card stays free.
  Config.autosaveDebounceMs = 2000   -- an editor sends its changes this long after its last edit

  -- A claim on a card is dropped this long after the last edit to it, so a player who wanders
  -- off mid-note never leaves a card blocked for the rest of the session.
  Config.cardClaimTimeoutSeconds = 30

  -- Draw the other players' mouse pointers on the board. Purely cosmetic: turning it off stops
  -- the position updates entirely, it does not change who may edit what.
  Config.showRemoteCursors = true
  Config.cursorUpdateHz    = 12      -- pointer updates per second (only while others are at the board)

  -- Hide who is at the board. With `true` the other players are shown as numbered visitors
  -- ("Visitor 2") instead of by character name — in the presence bar, on the card they hold, on
  -- their cursor and in the "X is working on that card" message. The real names never leave the
  -- server. Use it where players are not supposed to learn each other's names from a notice board.
  Config.anonymizeCollaborators = false

  -- Colours that identify the players at a board: presence bar, busy cards, cursors. Handed out
  -- in order as players open the board; they wrap around when more players than colours are there.
  Config.playerColors = {
      '#e6b422', '#5aa9e6', '#7ac74f', '#e8724c',
      '#b07de8', '#4fd1c5', '#f06292', '#d6c7a1',
  }

  -- Colours offered for notes, pins and threads ('#rrggbb').
  Config.cardColors = {
      '#d4a017', '#c0392b', '#2980b9', '#27ae60', '#8e44ad', '#e67e22',
      '#f9e49a', '#f4a0a0', '#a8ccf0', '#a8e4b8', '#cda8e8', '#f4c8a0',
  }
  ```
</Accordion>

### Board locations

`key` identifies persistent content; `label` names the board; `coords` sets the interaction point. `radius` overrides the default interaction distance.

* Omit `editJobs` to let everyone edit, or list the job names allowed to edit. Reading remains open to everyone.
* Set `bgStyle` to `cork`, `parchment`, `dark` (wood), `slate` or `linen` to fix the surface and hide its selector.
* Set `size` with `w` and `h` in board pixels to create a bounded board; omit it for an endless board.
* Moving coordinates while retaining the key keeps the board's content.

A fixed background overrides stored and preset backgrounds. Both the editor and server enforce board bounds.

### Permissions and collaboration

`Config.Permissions.edit` is a server-side callback and can replace the default job check. `Config.Permissions.admin` gates admin commands with `dd_pinboard.admin` by default.

Each editor sends changes after `autosaveDebounceMs` (2,000 ms by default). Card reservations expire after `cardClaimTimeoutSeconds` (30 seconds by default). There is no board-wide editing lock.

`showRemoteCursors` controls pointer visibility and traffic; `cursorUpdateHz` sets its rate. No cursor traffic is sent while a player is alone. `playerColors` identifies participants. `anonymizeCollaborators` replaces character names with numbered visitors in the collaboration UI. `cardColors` supplies the note, pin, thread and pencil palette.

### Limits and image hosts

Defaults allow 200 cards and 400 threads per board, 32 KiB of JSON per card, a maximum card side of 10,000 pixels and image URLs up to 1,024 characters. The server validates edits before accepting a batch.

An empty `allowedImageHosts` permits any HTTP or HTTPS host. Entries permit the named host and its subdomains. Images still need to be reachable from every player's client.

<Note>
  If you restrict image hosts and use the bundled investigation preset, include `cfx-nui-dd_pinboard` in the allow-list. Its pictures load from the resource itself.
</Note>

### Language

English and German are included in `config/language.lua`. Set `DDLanguage` there. To use the server-wide `setr dd:language de` setting instead, set `DDLanguage` to an empty string or `nil`; a non-empty per-resource value takes precedence. Add a translation table in that file for another language.

## Commands and presets

| Command                         | Purpose                                                                                          |
| ------------------------------- | ------------------------------------------------------------------------------------------------ |
| `/pinboard_clear <key>`         | Delete the board's cards and threads; requires admin permission                                  |
| `/pinboard_seed <key> [preset]` | Replace the board with a configured preset; defaults to `crime_board`; requires admin permission |
| `/pinboard <key>`               | Open from anywhere; registered only with debug enabled and requires admin permission             |

Clear and seed also work in the server console without the leading slash. Command names are configurable.

To load the bundled investigation board at the supplied showcase location:

```text theme={null}
/pinboard_seed showcase_case_board crime_board
```

Prepare your own layouts in `config/showcase.lua`. Presets contain cards, thread references, an optional background and viewport. Use the shipped `crime_board` data as the reference. The server applies the same card, image, payload and bounds validation used for player edits.

<Warning>
  Successful seeding replaces the target board's existing cards and threads and resets viewers to the new state. Clearing also resets viewers. Neither operation can be reversed with session Undo; back up database content you want to keep.
</Warning>

A rejected preset keeps existing content. Validation counts existing content while adding the preset, so a nearly full board can hit the card or thread cap. Prefer a fresh demonstration location, or back up and clear an existing board before seeding it. Unknown preset names are rejected. A location's fixed surface takes precedence over the preset's background.

## Troubleshooting

<AccordionGroup>
  <Accordion title="The board does not open">
    Confirm the resource, framework and oxmysql started successfully. Check the configured coordinates and radius, the interaction key, and that the release includes ui/dist. The remote-open command requires both debug mode and admin permission.
  </Accordion>

  <Accordion title="I can read but cannot edit">
    Check the character's job against editJobs or your custom permission callback. A card another player is using is reserved only for them; wait for release or its inactivity timeout.
  </Accordion>

  <Accordion title="A photo does not load">
    Check that its URL points directly to an image accessible from the client. Review allowedImageHosts. For bundled preset images, keep the resource name dd\_pinboard and permit cfx-nui-dd\_pinboard when an allow-list is enabled.
  </Accordion>

  <Accordion title="The background selector is missing or a card cannot move farther">
    The location may have a fixed bgStyle or bounded size. These restrictions apply to players and showcase presets.
  </Accordion>

  <Accordion title="Undo history disappeared">
    Undo is local to the open session. Closing or a full resync clears it; edits by other players invalidate conflicting steps. A rejected batch, admin clear or seed can cause a full resync.
  </Accordion>

  <Accordion title="Old content disappeared after changing a location">
    Check whether its key changed. Restore the original key or migrate the stored location\_key as described in the developer reference. Merely changing coordinates does not create a new board.
  </Accordion>
</AccordionGroup>

See the [developer reference](/scripts/pinboard/developers) for exports and database details.
