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 beyondConfig.UnstreamDistance(120m), hard capConfig.MaxActivePerClient(30). - Map blip per poster (single sprite, single name) â toggle with
Config.UseBlips. /postersopens 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 to0to disable. - Owners renew from the inspect screen â adds
Config.RenewExtensionfrom now, so frequent renewals don't accumulate. Per-identifier rate limit viaConfig.RenewCooldown. - Server runs an expiry sweep every
Config.ExpirySweepTick(default 1h) and broadcasts removal to all clients.
Admin moderation â
/posterreportsopens the report queue (server gates by ACEConfig.AdminAce, defaultns-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 toserver/data/. Autosaved on dirty withConfig.AutoSaveInterval(60s).
- oxmysql (default) â
- Cross-framework via the ns-lib SQL adapter (works against the framework's configured DB resource).
Requirements â
| Resource | Required | Purpose |
|---|---|---|
| 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) |
oxmysql | optional | Only if Config.UseMySQL = true |
| Node.js + npm | optional | Only for rebuilding the NUI |
Installation â
Drop the
ns-posterfolder into your server'sresources/directory.Add to
server.cfgafter ns-lib + object_gizmo:ensure ns-lib ensure object_gizmo ensure ns-poster(Optional) grant admin moderation access:
add_ace group.admin ns-poster.admin allow(Optional, MySQL mode â default)
Config.UseMySQL = truerequires thens_poster_*tables. Two install paths:- Auto: tables auto-create on first boot via
CREATE TABLE IF NOT EXISTSinserver/db.lua. No action needed. - Manual (recommended for production): import
sql/install.sqlbefore starting the resource.
- Auto: tables auto-create on first boot via
(Optional) populate
Config.AllowedImageDomainsâ empty by default, so only the built-in asset library is usable until you whitelist hosts.
Commands & keys â
| Action | How | Notes |
|---|---|---|
| Open designer | /poster | |
| Open posters list (with GPS waypoint) | /posters | |
| Open admin report queue | /posterreports | Server gates by ACE; non-admins get a silent no-op |
| Inspect a poster | Walk up, press G | Like / report / renew from this view |
| Placement gizmo (after pressing Place in designer) | W move, R rotate, LAlt snap to ground, Esc confirm, Backspace cancel | Driven by object_gizmo |
Configuration â
All settings live in config.lua. Highlights:
Storage â
| Key | Default | Notes |
|---|---|---|
UseMySQL | true | false â JSON files in server/data/ |
AutoSaveInterval | 60 | seconds; JSON mode autosave when dirty |
Streaming + limits â
| Key | Default | Notes |
|---|---|---|
StreamDistance / UnstreamDistance | 100 / 120 | meters (hysteresis) |
StreamTick | 500 | ms between stream checks |
MaxActivePerClient | 30 | hard cap on simultaneous spawned props |
PlaceCooldown | 60 | seconds between placements (per player) |
MaxPostersPerPlayer | 5 | cap per identifier |
MaxTextElements | 12 | per design |
MaxImageElements | 6 | per design |
MaxTextLength | 280 | per text element |
MaxPayloadBytes | 32 KB | serialized JSON size limit |
MaxPresetsPerPlayer | 10 | personal saved presets |
Placement â
| Key | Default | Notes |
|---|---|---|
DefaultProp | p_cs_advertposter01x | |
AllowedProps | 3 props | p_cs_advertposter01x, p_gen_posterwanted05x, p_poster_troub01x_lrg |
PlacementMaxDistance | 5.0 | server-side anti-teleport clamp |
PlacementMinDistance | 0.5 | minimum distance from player |
PlacementStartOffset | 2.0 | ghost spawn distance in front of player |
Blip â
| Key | Default | Notes |
|---|---|---|
UseBlips | true | false â no blips; players discover posters by walking into them |
BlipSpriteName | blip_wanted_poster | resolved with GetHashKey at runtime |
BlipName | 'Poster' | |
BlipScale | 0.2 |
Interact â
| Key | Default | Notes |
|---|---|---|
InteractKey | 0x760A9C6F | INPUT_INTERACT_OPTION1 = G |
InteractDistance | 2.5 | meters |
InteractTick | 250 | ms between proximity checks |
Expiration + renew â
| Key | Default | Notes |
|---|---|---|
PosterTtl | 7 days | 0 = posters never expire |
RenewExtension | 7 days | added from now per renew click |
RenewCooldown | 30 | seconds per identifier |
ExpirySweepTick | 3600 | seconds between cleanup sweeps |
Social â
| Key | Default | Notes |
|---|---|---|
AllowSelfLike | false | owners can/can't like their own posters |
LikeCooldown | 5 | seconds per identifier |
ReportCooldown | 180 | seconds per identifier between any two reports |
MaxReportReasonLength | 200 | chars |
Permission + whitelist â
| Key | Default | Notes |
|---|---|---|
AdminAce | ns-poster.admin | ACE that overrides ownership for removal + report queue |
AllowCustomSignature | false | true â 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) â
| Key | Default | Notes |
|---|---|---|
DiscordWebhook | '' | Empty disables all webhook logging |
DiscordUsername | 'ns-poster' | |
DiscordAvatarUrl | '' | Optional |
DiscordLogPlace | true | |
DiscordLogRemove | false | Noisy on busy servers |
DiscordLogReport | true | |
DiscordLogExpired | false | Noisy |
DiscordLogAdminTeleport | true | Logged 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 buildThe 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.svgThese 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.luavalidation, payload-size cap, image-URL whitelist, and (for remove/renew) ownership/ACE check. - Placement coordinates are clamped to within
Config.PlacementMaxDistanceof the requesting player (anti-teleport). - React renders all user text via
{value}(notdangerouslySetInnerHTML), so HTML injection cannot reach the DOM. - Image URLs must be
https://and the host must matchConfig.AllowedImageDomains. Subdomain wildcards: prefix entry with.(e.g..imgur.commatchesi.imgur.com). - All ACE checks and ownership comparisons run server-side;
/posterreportsis 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.
-- 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'