Skip to content

ns-poster ​

Designable, syncable in-world posters for RedM. Players open a NUI editor to compose a poster (text + images, presets, custom backgrounds), then place it as an interactive prop anywhere in the world. Nearby players see the prop, press G to inspect the full design, can like posters, report bad ones, and owners can renew them before they auto-expire. Admins get a moderation queue with one-click teleport. Cross-framework via ns-lib.

Discord: https://discord.gg/UyyngemnF8 — support, bug reports, feedback.


Features ​

Designer (NUI) ​

  • 4 built-in presets — Wanted Poster, Newspaper Page, Classified Ad, Custom (blank canvas).
  • Drag / resize / rotate text and image elements on a 600×800 (3:4) canvas.
  • 4 fonts shipped: Rye (western), IM Fell English (serif), Inter (sans), Special Elite (typewriter).
  • 8 image filters: None, Sepia, Wanted, Noir, B&W, Vintage, Faded, Cold.
  • Image sources: built-in asset library + whitelisted-domain URLs.
  • Custom backgrounds: library, https URL, or solid color.
  • Undo/redo via design history.
  • Save your own composition as a personal preset (Config.MaxPresetsPerPlayer).

Placement ​

  • Uses object_gizmo for full 3D position + rotation control while placing.
  • Ghost prop is local-only; the canonical networked prop is spawned by the server on confirm — no duplicates.
  • Cancel returns the player to the designer with the in-progress design intact.

World + sync ​

  • Distance-based streaming — props spawn at Config.StreamDistance (100m), despawn beyond Config.UnstreamDistance (120m), hard cap Config.MaxActivePerClient (30).
  • Map blip per poster (single sprite, single name) — toggle with Config.UseBlips.
  • /posters opens a list with GPS waypoint to any placed poster.
  • Walk up to a poster, press G to open the inspect view.

Social ​

  • Likes — one per player per poster, rate-limited (Config.LikeCooldown). Owners can be blocked from liking their own (Config.AllowSelfLike).
  • Reports — players flag offensive content with a reason. Per-identifier global cooldown (Config.ReportCooldown) plus a per-poster duplicate guard — you can't reopen a report on the same poster until an admin dismisses or resolves the previous one.

Expiration (TTL) ​

  • Posters auto-expire after Config.PosterTtl (default 7 days). Set to 0 to disable.
  • Owners renew from the inspect screen — adds Config.RenewExtension from now, so frequent renewals don't accumulate. Per-identifier rate limit via Config.RenewCooldown.
  • Server runs an expiry sweep every Config.ExpirySweepTick (default 1h) and broadcasts removal to all clients.

Admin moderation ​

  • /posterreports opens the report queue (server gates by ACE Config.AdminAce, default ns-poster.admin).
  • One-click teleport to the reported poster.
  • Dismiss / resolve / remove actions; report state is tracked in DB.
  • Optional Discord webhook log when an admin teleports.

Discord webhook (server-only) ​

  • Set Config.DiscordWebhook, then toggle per-event:
    • DiscordLogPlace — poster placed.
    • DiscordLogRemove — poster removed (off by default; noisy).
    • DiscordLogReport — new report submitted.
    • DiscordLogExpired — auto-expiry sweep (off by default; noisy).
    • DiscordLogAdminTeleport — admin teleported from the report queue.
  • Empty webhook URL disables all logging, regardless of toggles.

Persistence ​

  • Two backends, switched at runtime via Config.UseMySQL:
    • oxmysql (default) — Config.UseMySQL = true.
    • JSON files — Config.UseMySQL = false, written to server/data/. Autosaved on dirty with Config.AutoSaveInterval (60s).
  • Cross-framework via the ns-lib SQL adapter (works against the framework's configured DB resource).

Requirements ​

ResourceRequiredPurpose
ns-lib✅Framework adapter (VORP / RSG-Core / RedEM:RP auto-detect)
object_gizmo✅3D placement gizmo. FiveM resource — adapt to RedM first (set game 'rdr3' + add rdr3_warning in its fxmanifest)
oxmysqloptionalOnly if Config.UseMySQL = true
Node.js + npmoptionalOnly for rebuilding the NUI

Installation ​

  1. Drop the ns-poster folder into your server's resources/ directory.

  2. Add to server.cfg after ns-lib + object_gizmo:

    ensure ns-lib
    ensure object_gizmo
    ensure ns-poster
  3. (Optional) grant admin moderation access:

    add_ace group.admin ns-poster.admin allow
  4. (Optional, MySQL mode — default) Config.UseMySQL = true requires the ns_poster_* tables. Two install paths:

    • Auto: tables auto-create on first boot via CREATE TABLE IF NOT EXISTS in server/db.lua. No action needed.
    • Manual (recommended for production): import sql/install.sql before starting the resource.
  5. (Optional) populate Config.AllowedImageDomains — empty by default, so only the built-in asset library is usable until you whitelist hosts.


Commands & keys ​

ActionHowNotes
Open designer/poster
Open posters list (with GPS waypoint)/posters
Open admin report queue/posterreportsServer gates by ACE; non-admins get a silent no-op
Inspect a posterWalk up, press GLike / report / renew from this view
Placement gizmo (after pressing Place in designer)W move, R rotate, LAlt snap to ground, Esc confirm, Backspace cancelDriven by object_gizmo

Configuration ​

All settings live in config.lua. Highlights:

Storage ​

KeyDefaultNotes
UseMySQLtruefalse → JSON files in server/data/
AutoSaveInterval60seconds; JSON mode autosave when dirty

Streaming + limits ​

KeyDefaultNotes
StreamDistance / UnstreamDistance100 / 120meters (hysteresis)
StreamTick500ms between stream checks
MaxActivePerClient30hard cap on simultaneous spawned props
PlaceCooldown60seconds between placements (per player)
MaxPostersPerPlayer5cap per identifier
MaxTextElements12per design
MaxImageElements6per design
MaxTextLength280per text element
MaxPayloadBytes32 KBserialized JSON size limit
MaxPresetsPerPlayer10personal saved presets

Placement ​

KeyDefaultNotes
DefaultPropp_cs_advertposter01x
AllowedProps3 propsp_cs_advertposter01x, p_gen_posterwanted05x, p_poster_troub01x_lrg
PlacementMaxDistance5.0server-side anti-teleport clamp
PlacementMinDistance0.5minimum distance from player
PlacementStartOffset2.0ghost spawn distance in front of player

Blip ​

KeyDefaultNotes
UseBlipstruefalse → no blips; players discover posters by walking into them
BlipSpriteNameblip_wanted_posterresolved with GetHashKey at runtime
BlipName'Poster'
BlipScale0.2

Interact ​

KeyDefaultNotes
InteractKey0x760A9C6FINPUT_INTERACT_OPTION1 = G
InteractDistance2.5meters
InteractTick250ms between proximity checks

Expiration + renew ​

KeyDefaultNotes
PosterTtl7 days0 = posters never expire
RenewExtension7 daysadded from now per renew click
RenewCooldown30seconds per identifier
ExpirySweepTick3600seconds between cleanup sweeps

Social ​

KeyDefaultNotes
AllowSelfLikefalseowners can/can't like their own posters
LikeCooldown5seconds per identifier
ReportCooldown180seconds per identifier between any two reports
MaxReportReasonLength200chars

Permission + whitelist ​

KeyDefaultNotes
AdminAcens-poster.adminACE that overrides ownership for removal + report queue
AllowCustomSignaturefalsetrue → users can override the "Posted by" text
AllowedImageDomains{} (empty)https hosts allowed for image URLs. Prefix with . for subdomain wildcard (e.g. .imgur.com)

Discord webhook (server-only) ​

KeyDefaultNotes
DiscordWebhook''Empty disables all webhook logging
DiscordUsername'ns-poster'
DiscordAvatarUrl''Optional
DiscordLogPlacetrue
DiscordLogRemovefalseNoisy on busy servers
DiscordLogReporttrue
DiscordLogExpiredfalseNoisy
DiscordLogAdminTeleporttrueLogged with admin name + report id

Building the NUI ​

Pre-built output ships in html/. Only rebuild if you change anything under ui/.

cd scripts/ns-poster/ui
npm install
npm run build

The build emits to ../html/ and regenerates the library manifest from files in library/.

Asset library ​

The designer references SVGs (PNG also accepted) under:

library/backgrounds/  parchment.svg, newspaper.svg, wanted_aged.svg,
                      white_paper.svg, dark_wood.svg, vintage_yellow.svg,
                      wanted-poster-bg.svg
library/decorations/  skull.svg, star.svg, frame_ornate.svg, eagle.svg,
                      dollar.svg, wanted_stamp.svg

These are placeholders — drop your own art in. The picker entries are auto-generated by ui/scripts/gen-library-manifest.mjs on every build, so you only need to add files and run npm run build (or npm run gen-library manually).


Security model ​

  • Server is authoritative — every place / remove / save-preset / like / report / renew / resolve-report goes through shared/schema.lua validation, payload-size cap, image-URL whitelist, and (for remove/renew) ownership/ACE check.
  • Placement coordinates are clamped to within Config.PlacementMaxDistance of the requesting player (anti-teleport).
  • React renders all user text via {value} (not dangerouslySetInnerHTML), so HTML injection cannot reach the DOM.
  • Image URLs must be https:// and the host must match Config.AllowedImageDomains. Subdomain wildcards: prefix entry with . (e.g. .imgur.com matches i.imgur.com).
  • All ACE checks and ownership comparisons run server-side; /posterreports is a no-op for non-admins (server never replies).
  • Per-identifier rate limits on likes, reports, and renews prevent client hammering.

Exports ​

All exports take a real player source. source = 0 (console) is rejected so a buggy/malicious peer resource can't bypass ownership.

lua
-- Placement & lifecycle
exports['ns-poster']:placePoster(source, { design = {...}, world = { coords, heading, model } })
exports['ns-poster']:removePoster(source, posterId)
exports['ns-poster']:renewPoster(source, posterId)
exports['ns-poster']:getAllPosters()           -- read-only snapshot

-- Social
exports['ns-poster']:likePoster(source, posterId)
exports['ns-poster']:reportPoster(source, posterId, reason)

-- Moderation (require Config.AdminAce)
exports['ns-poster']:listReports(source, filterStatus)         -- 'open' | 'resolved' | 'dismissed' | nil (all)
exports['ns-poster']:resolveReport(source, reportId, action)   -- 'dismiss' | 'resolve' | 'remove'

Released under the MIT License.