> For the complete documentation index, see [llms.txt](https://fantasy-scripts.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://fantasy-scripts.gitbook.io/docs/weazel-news/configuration-file.md).

# Configuration File

```lua
--[[
    fs-weazel / config/config.lua

    Server-owner facing configuration. Anything a server owner would
    reasonably want to change lives here or in config/permissions.lua.
    Protocol values (statuses, transitions) live in shared/constants.lua
    and are deliberately NOT configurable.
]]

Config = Config or {}

-- ─────────────────────────────────────────────────────────────────────
--  Core
-- ─────────────────────────────────────────────────────────────────────

--- 'auto' | 'esx' | 'qbcore' | 'qbox'
Config.Framework = 'auto'

--- Verbose server logging, /wn_debug, and the /wn_testtv screen spawner.
--- Turn OFF in production — /wn_testtv spawns objects for any player.
Config.Debug = true

--- Locale key, must match a file in locales/.
Config.Locale = 'en'

-- ─────────────────────────────────────────────────────────────────────
--  Stations
--
--  Every entry here is a whole media company: its own job, its own staff,
--  its own stories, photos, adverts, television channels and books.
--  Nothing crosses between them. A reporter at one paper cannot see
--  another paper's drafts, tips, sources or accounts.
--
--  ── One job, one station ─────────────────────────────────────────────
--  A character's framework job decides which newsroom they work for, and
--  a character holds one job at a time. There is nothing to switch and
--  no way to be in two newsrooms at once. Give somebody the other job and
--  they are simply staff at the other station.
--
--  ── Adding a station ─────────────────────────────────────────────────
--    1. Create the job in your framework — see docs/jobs.md
--    2. Copy a block below, change `slug`, `job` and the branding
--    3. restart fs-weazel
--
--  The station, its sections and its television channels are created on
--  the next start. Nothing to import.
--
--  ── Removing one ─────────────────────────────────────────────────────
--  Set `active = false` rather than deleting the block. The station stops
--  appearing and its staff lose access, but its stories, books and audit
--  trail stay where they are. Deleting the block leaves rows pointing at
--  a station nothing describes.
--
--  ── Anything omitted is inherited ────────────────────────────────────
--  `theme`, `categories` and `gradeRoles` fall back to the defaults
--  further down this file. A second station that only wants a different
--  name and colour needs four lines, not forty.
-- ─────────────────────────────────────────────────────────────────────

Config.Stations = {
    {
        --- Internal name. Never shown. Changing it after go-live orphans
        --- that station's content, so pick one and leave it.
        slug      = 'weazel',
        --- The framework job. Must match your jobs list exactly.
        job       = 'weazel',
        active    = true,

        label     = 'Weazel News',
        shortName = 'WEAZEL',
        tagline   = 'Los Santos and Blaine County',
        website   = 'weazelnews.lsc',
        logo      = 'web/images/weazel-logo.png',

        --- Where ad revenue lands. Usually the same as the job name.
        society   = 'weazel',

        --- Overrides Config.Theme for this station only. Staff see their
        --- own colours; so does anyone reading their paper.
        theme     = { accent = '#eb1000', accentHi = '#FF4A3A', onAccent = '#FFF2F0' },

        --- Television channels this station broadcasts on. A TV prop
        --- tuned to one of these shows this station's bulletin.
        channels  = {
            { slug = 'main', label = 'Weazel One' },
            { slug = 'rolling', label = 'Weazel 24' },
        },
    },

    --  ── A second station ───────────────────────────────────────────
    --  Delete this block if you only run one newspaper. It is here as a
    --  worked example: note how little it needs to say, because
    --  everything it leaves out is inherited.
    {
        slug      = 'bcpost',
        job       = 'bcpost',
        active    = true,          -- ← set true once the job exists

        label     = 'Blaine County Post',
        shortName = 'BC POST',
        tagline   = 'North of the Alamo Sea',
        website   = 'bcpost.lsc',
        logo      = '',             -- falls back to the shortName wordmark

        society   = 'bcpost',

        theme     = { accent = '#2E9E5B', accentHi = '#5BD489', onAccent = '#07160D' },

        channels  = {
            { slug = 'main', label = 'BC Post TV' },
        },
    },
}

--- Derived from Config.Stations — do not edit.
---
--- The employment gate is "does this character hold a job that belongs to
--- an active station". Building it here rather than asking the station
--- list on every check keeps that a single table lookup on a hot path.
Config.Jobs = {}
for _, station in ipairs(Config.Stations) do
    if station.active ~= false then Config.Jobs[station.job] = true end
end

--- Auto-create an employee record the first time someone with a news job
--- opens the newsroom. Assigned the role below.
Config.AutoEnrollEmployees = true
Config.DefaultRole = 'intern'

-- ─────────────────────────────────────────────────────────────────────
--  Rank → role sync
--
--  With this on, the in-game job grade decides the newsroom role. Set
--  someone's rank with /setjob and their permissions follow on their next
--  newsroom action — no database edit, no Staff screen, one place to
--  manage people.
--
--  The trade-off, so it is a choice rather than a surprise: the sync
--  OVERWRITES the role on every resolve, so changes made on the Staff
--  screen are reverted the next time that person does anything. A grade
--  is a single number, so it can only express a ladder — the lateral
--  roles (photographer, camera_operator, anchor, radio_host,
--  social_media) become unreachable unless you map a rank to one.
--
--  Per-employee permission overrides are NOT touched by this. They are
--  stored separately and still apply on top, so the Staff screen remains
--  the way to give one person one extra permission without inventing a
--  rank for it.
--
--  Set false to manage roles purely from the Staff screen instead.
-- ─────────────────────────────────────────────────────────────────────

Config.SyncRoleWithGrade = true

--- Job grade -> role id. Grades absent from this table leave the role
--- alone, so you can map only the ranks you care about.
--- Role ids come from config/permissions.lua.
Config.GradeRoles = {
    [0] = 'intern',
    [1] = 'journalist',
    [2] = 'senior_journalist',
    [3] = 'producer',
    [4] = 'editor',
    [5] = 'senior_editor',
    [6] = 'news_director',
    [7] = 'editor_in_chief',
}

-- ─────────────────────────────────────────────────────────────────────
--  Branding — used by the newsroom, website, newspaper and broadcast
--  graphics so a server can re-skin the whole thing from one place.
-- ─────────────────────────────────────────────────────────────────────

Config.Branding = {
    organisation = 'Weazel News',
    shortName    = 'WEAZEL',
    tagline      = 'LOS Santos and Blaine County',
    website      = 'discord.gg/code',
    --- Paths from the resource root, and they must also be listed in the
    --- manifest's `files` block or CEF is not allowed to load them.
    --- Leave a path empty to fall back to the `shortName` wordmark.
    --- The logo replaces the wordmark in the newsroom rail and on the
    --- newspaper masthead, so a wide, transparent PNG suits it best.
    logo         = 'web/images/weazel-logo.png',
    watermark    = '',
    --- The accent colour used to live here and coloured only the masthead
    --- and the newspaper rule. It is Config.Theme.accent now, below, and
    --- colours the whole interface.
}

-- ─────────────────────────────────────────────────────────────────────
--  Theme
--
--  The interface palette. Every colour in the newsroom, the newspaper
--  reader, the publish card, the teleprompter and the television graphics
--  comes from here — there is nothing else to edit.
--
--  ── Most of the kit is derived, not listed ───────────────────────────
--  Change `accent` and the soft fills, focus rings, glows, scrollbar
--  thumb, drop cap, active nav item and every "published" badge follow it
--  automatically. That is why the list below is short: listing each of
--  those separately is how a palette drifts out of tune with itself.
--
--  So the two accent values worth thinking about are:
--    accentHi  a lighter accent, used for hover. Same hue, brighter.
--    onAccent  text drawn ON TOP of a solid accent fill. It has to stay
--              readable against `accent` — dark text on a light accent,
--              light text on a dark one. Get this wrong and buttons
--              become unreadable while everything else looks fine.
--
--  ── Applies at page load ─────────────────────────────────────────────
--  The palette is handed to the interface when it boots, so a change here
--  needs `restart fs-weazel` and, for a player already connected, one
--  reconnect — an open page keeps the palette it started with.
-- ─────────────────────────────────────────────────────────────────────

Config.Theme = {
    --- Brand colour. Carried by roughly a dozen derived values.
    accent   = '#eb1000',
    accentHi = '#FF4A3A',
    onAccent = '#FFF2F0',

    --- The dark the glass is made of. Near-black reads best over the game.
    bg    = '#0d0d0c',
    --- Body text, then two steps quieter for secondary and hint text.
    text  = '#F6F5F2',
    dim   = '#BAB9B3',
    faint = '#9E9D96',

    --- Meaning colours. These say what a thing IS — a danger badge should
    --- stay red even on a red-accented server — so they are separate from
    --- the accent on purpose.
    info    = '#7DD3FC',
    warn    = '#FFC629',
    danger  = '#FF7A70',
    success = '#6EE7A0',
    violet  = '#C9A9FF',
    pink    = '#FF9BEF',

    --- 0 to 1. How opaque the glass is; the game shows through below it.
    --- Lower is more transparent. Under about 0.6 text starts to fight
    --- with whatever is behind the panel.
    solidity = 0.78,

    --- Multiplier on every corner radius. 1 is the designed value; 0 gives
    --- square corners, 1.6 or so gives a soft, rounded look.
    radius = 1,

    --- Font stacks. Both families are loaded in web/build/index.html — a
    --- family named here that is not loaded there falls back to the next
    --- entry in the stack.
    mono = "'Chakra Petch', ui-monospace, monospace",
    sans = 'Sora, system-ui, sans-serif',
}

-- ─────────────────────────────────────────────────────────────────────
--  Article rules
-- ─────────────────────────────────────────────────────────────────────

Config.Articles = {
    --- Articles per page in newsroom and website listings.
    perPage = 20,
    --- How many historical versions to keep per article. 0 = unlimited.
    maxVersions = 25,
    --- Require the review workflow, or let publishers go straight through.
    requireReview = true,
    --- Seconds a scheduled article may be queued ahead of time.
    maxScheduleAhead = 60 * 60 * 24 * 30,
    --- Minimum seconds between two publishes by the same employee.
    publishCooldown = 15,
}

--- Default categories. Stored in article_categories on first migration;
--- editable afterwards through the admin UI.
Config.Categories = {
    { slug = 'breaking',       label = 'Breaking News',  colour = '#FF7A70', order = 1 },
    { slug = 'crime',          label = 'Crime',          colour = '#FF9BEF', order = 2 },
    { slug = 'politics',       label = 'Politics',       colour = '#7DD3FC', order = 3 },
    { slug = 'business',       label = 'Business',       colour = '#6EE7A0', order = 4 },
    { slug = 'sports',         label = 'Sports',         colour = '#FFC629', order = 5 },
    { slug = 'weather',        label = 'Weather',        colour = '#93C5FD', order = 6 },
    { slug = 'entertainment',  label = 'Entertainment',  colour = '#C9A9FF', order = 7 },
    { slug = 'lifestyle',      label = 'Lifestyle',      colour = '#FDBA74', order = 8 },
    { slug = 'traffic',        label = 'Traffic',        colour = '#FCD34D', order = 9 },
    { slug = 'government',     label = 'Government',     colour = '#A5B4FC', order = 10 },
    { slug = 'opinion',        label = 'Opinion',        colour = '#BAB9B3', order = 11 },
    { slug = 'investigations', label = 'Investigations', colour = '#F87171', order = 12 },
}

-- ─────────────────────────────────────────────────────────────────────
--  Media storage
--  The database stores metadata + a locator, never binary blobs.
-- ─────────────────────────────────────────────────────────────────────

Config.Media = {
    --- 'local' | 'url' | 'external'
    provider = 'url',
    --- For provider 'local': folder inside this resource served to the NUI.
    localPath = 'web/media',
    --- For provider 'external': an export called as
    --- exports[resource][method](identifier, payload) -> url
    externalResource = nil,
    externalMethod = nil,
    --- Hard caps.
    maxPhotosPerEmployee = 500,
    maxVideosPerEmployee = 100,
}

-- ─────────────────────────────────────────────────────────────────────
--  Televisions
--
--  Every prop listed here shows the broadcast. Texture replacement is
--  global — it swaps the texture on every instance of that model in the
--  world at once — so you are listing *model types*, not places. Adding a
--  model here is all it takes; no database row, no coordinates.
--
--  `txd` and `txn` are the prop's own texture dictionary and the texture
--  inside it that is the screen. They are usually the model name and
--  `script_rt_tvscreen`, but not always — if a TV stays on colour bars,
--  a wrong `txn` is almost always why.
--
--  wn_tv_screens still exists for the unusual case: one specific prop
--  tuned to a different channel from everything else.
-- ─────────────────────────────────────────────────────────────────────

Config.TV = {
    models = {
        { model = 'prop_tv_flat_01',   txd = 'prop_tv_flat_01',   txn = 'script_rt_tvscreen' },
        { model = 'prop_tv_flat_02',   txd = 'prop_tv_flat_02',   txn = 'script_rt_tvscreen' },
        { model = 'prop_tv_flat_03',   txd = 'prop_tv_flat_03',   txn = 'script_rt_tvscreen' },
        { model = 'prop_tv_flat_michael', txd = 'prop_tv_flat_michael', txn = 'script_rt_tvscreen' },
        { model = 'prop_trev_tv_01',   txd = 'prop_trev_tv_01',   txn = 'script_rt_tvscreen' },
        { model = 'prop_tv_06',        txd = 'prop_tv_06',        txn = 'script_rt_tvscreen' },
    },

    --- Channel slug these show. nil means whatever is live anywhere.
    defaultChannel = 'weazel-one',

    --- Model `/wn_testtv` spawns. Must be one of the models above, or the
    --- test set will show nothing.
    testModel = 'prop_tv_flat_01',
}

-- ─────────────────────────────────────────────────────────────────────
--  Field kit
--
--  The gear a crew carries: a prop in the hand and an animation to hold
--  it with. Every value here is a real GTA asset, but animation dictionary
--  names are the sort of thing that changes between game builds — so the
--  client loads each one with a timeout and falls back to `scenario` if
--  it never arrives, rather than leaving somebody standing empty-handed
--  with no explanation.
--
--  `bone` 28422 is the right hand. `offset` and `rotation` are how the
--  prop sits in it; nudge them if a prop looks wrong rather than changing
--  the bone.
--
--  ── Do not tune these by hand ────────────────────────────────────────
--  A model's origin sits wherever its artist left it, so no amount of
--  reasoning tells you where a prop lands in a fist. Set Config.Debug and
--  use `/newstune <gear>` in game: it moves the prop live and prints a
--  block in exactly this shape to paste back over the one below.
--
--  ── Animation and prop have to match ─────────────────────────────────
--  Rockstar authored some animations *around* a specific prop. When a
--  pair matches, the prop sits right at zero offset because the animator
--  placed the hand for it — `prop_v_cam_01` with the finale cameraman
--  loop is one of those. When it does not match, no offset will save it:
--  a shoulder camera pinned to the paparazzi's eye-level stills pose ends
--  up hanging in mid-air in front of the ped, and an arms-folded idle
--  buries whatever is in that hand inside the chest. Prefer changing the
--  animation over fighting the numbers.
-- ─────────────────────────────────────────────────────────────────────

Config.Field = {
    gear = {
        mic = {
            label    = 'Microphone',
            item     = 'weazel_mic',
            prop     = 'p_ing_microphonel_01',
            --- The left hand, not the right: the phone-holding animation
            --- below raises the left, which leaves the right free to
            --- gesture the way a reporter's does.
            bone     = 60309,
            offset   = { x = 0.055, y = 0.05, z = 0.0 },
            rotation = { x = 240.0, y = 0.0, z = 0.0 },
            dict     = 'missheistdocksprep1hold_cellphone',
            anim     = 'hold_cellphone',
            scenario = 'WORLD_HUMAN_STAND_IMPATIENT',
        },
        camera = {
            label    = 'News camera',
            item     = 'weazel_camera',
            prop     = 'prop_v_cam_01',
            bone     = 28422,
            --- Zero on purpose. The finale cameraman loop below was
            --- animated holding this exact model, so the hand is already
            --- in the right place; an offset would only pull it out of it.
            offset   = { x = 0.0, y = 0.0, z = 0.0 },
            rotation = { x = 0.0, y = 0.0, z = 0.0 },
            dict     = 'missfinale_c2mcs_1',
            anim     = 'fin_c2_mcs_1_camman',
            scenario = 'WORLD_HUMAN_PAPARAZZI',
        },
        boom = {
            label    = 'Boom mic',
            item     = 'weazel_boom',
            prop     = 'prop_v_bmike_01',
            bone     = 28422,
            --- A film crew's boom operator, animated holding this pole.
            offset   = { x = -0.08, y = 0.0, z = 0.0 },
            rotation = { x = 0.0, y = 0.0, z = 0.0 },
            dict     = 'missfra1',
            anim     = 'mcs2_crew_idle_m_boom',
            scenario = 'WORLD_HUMAN_STAND_IMPATIENT',
        },
    },

    -- ─────────────────────────────────────────────────────────────────
    --  The camera feed
    --
    --  What actually reaches a television, and the one part of this
    --  resource limited by the game rather than by design.
    --
    --  **Real-time video between players is not possible in FiveM.** GTA
    --  only loads the world around your own character, so a television in
    --  Paleto Bay has no Vinewood geometry, peds or traffic in memory —
    --  there is nothing for it to render. No API hands one player's
    --  rendered frame to another.
    --
    --  So the feed is relayed screenshots: whoever holds the `camera`
    --  gear has their view captured every `interval` and shown on the
    --  televisions. At two seconds that is half a frame per second — a
    --  satellite feed rather than video, which suits a news bulletin
    --  better than it sounds.
    --
    --  Whatever the operator is looking at IS the shot. They frame it by
    --  aiming, exactly like a real camera operator, and nothing hijacks
    --  their view to do it.
    --
    --  Needs `screenshot-basic`, which is not installed by default:
    --      https://github.com/citizenfx/screenshot-basic
    -- ─────────────────────────────────────────────────────────────────

    feed = {
        --- With this off, a live shot still works — the television shows
        --- the branded card with LIVE FROM and the reporter's name
        --- instead of a picture.
        enabled = true,

        resource = 'screenshot-basic',

        --- Milliseconds between frames. Below about 1500 the bandwidth
        --- stops being worth the extra smoothness.
        interval = 2000,

        --- The only lever on frame size. screenshot-basic captures at the
        --- operator's own game resolution and offers no way to resize, so
        --- someone on a 1440p monitor produces a far larger frame than
        --- someone on 1080p — and quality is all that stands between you
        --- and that difference.
        quality = 0.25,

        --- How a frame reaches viewers.
        ---
        ---   'upload' POST the frame to `uploadUrl` and relay only the
        ---            link it returns. A few hundred bytes per frame.
        ---            **Use this.**
        ---
        ---   'relay'  base64 through the server. screenshot-basic's own
        ---            README says "please don't send this through any
        ---            server events", and it is right: a full-resolution
        ---            JPEG is tens to hundreds of kilobytes, sent to
        ---            every client, several times a minute. It is here
        ---            for testing with one or two people and nothing
        ---            else.
        transport = 'upload',

        --- Required for 'upload': an endpoint that takes multipart form
        --- data and returns a URL. A Discord webhook works, as does any
        --- image host or something of your own.
        uploadUrl = 'WEBHOOK_HERE',
        uploadField = 'file',

        --- Frames above this are dropped rather than sent. Deliberately
        --- tight, because it is the only thing standing between a
        --- high-resolution operator and the network.
        maxFrameBytes = 100000,

        -- ─────────────────────────────────────────────────────────────
        --  Sound from the scene
        --
        --  Anyone standing near a television hears the reporter who is
        --  on air, one-way, however far away they are.
        --
        --  There is no audio stream to send. What voice resources give
        --  you is a channel — two players on one hear each other at any
        --  distance — so the reporter is put on a channel and viewers
        --  join it as listeners with their own transmit disabled.
        --
        --  Needs a voice resource. pma-voice and mumble-voip are
        --  implemented in bridge/voice.lua; SaltyChat is detected and
        --  deliberately declined, because its model is different enough
        --  that a naive mapping would misbehave. With none installed the
        --  picture still works and the sound simply does not.
        -- ─────────────────────────────────────────────────────────────

        voice = true,

        --- Radio channel used for the broadcast. Pick something no
        --- in-game radio uses, or police will hear the anchor.
        voiceChannel = 6180,

        --- How close to a television you must be to hear it.
        voiceRadius = 12.0,
    },

    --- How far a live shot may drift from where it was set up before the
    --- server drops it. Stops a "live from Vinewood" that is now in Sandy
    --- Shores.
    maxDrift = 60.0,

    --- Seconds a live shot survives without its reporter checking in.
    --- Covers a crash or a disconnect mid-broadcast.
    staleAfter = 90,
}

-- ─────────────────────────────────────────────────────────────────────
--  Reputation
--
--  Earned by doing the job, not by being online. Every entry is a row in
--  wn_reputation with a reason, and the running total is kept on the
--  employee — so a number can always be explained by the ledger behind
--  it rather than being a mystery score.
--
--  Set any value to 0 to stop awarding for that event.
-- ─────────────────────────────────────────────────────────────────────

Config.Reputation = {
    --- Your story went out.
    articlePublished = 5,
    --- ...and it was the splash.
    articleBreaking = 10,
    --- Someone read it. Awarded in blocks, see readsPerAward.
    articleRead = 1,
    readsPerAward = 25,
    --- An assignment you were on was completed.
    assignmentCompleted = 3,
    --- A tip you handled became a story.
    tipPublished = 2,
    --- An investigation you led reached publication.
    investigationPublished = 15,
    --- An editor sent your copy back.
    changesRequested = -2,
    --- Your story was rejected outright.
    articleRejected = -5,
}

-- ─────────────────────────────────────────────────────────────────────
--  Press credentials
-- ─────────────────────────────────────────────────────────────────────

Config.Credentials = {
    --- Days a new pass is valid for. 0 never expires.
    validForDays = 0,
    --- Prefix on the printed serial.
    serialPrefix = 'WN',
    --- Issue one automatically when somebody is enrolled.
    autoIssue = true,
}

-- ─────────────────────────────────────────────────────────────────────
--  The public paper
-- ─────────────────────────────────────────────────────────────────────

Config.Reader = {
    --- Who gets the on-screen card when an ordinary story publishes:
    ---   'everyone'    every player, every time
    ---   'subscribers' only players who followed the paper with /news
    ---   'staff'       only employees
    ---
    --- Breaking news always goes to everyone regardless — that is what
    --- makes it breaking. Move this to 'subscribers' if a busy newsroom
    --- starts feeling like spam.
    announce = 'everyone',

    --- The channel the reader's follow toggle writes to.
    channel = 'breaking',
}

-- ─────────────────────────────────────────────────────────────────────
--  The newspaper
--
--  A page is a grid, not a canvas. Elements carry a column position and
--  a span (grid_x / grid_y / grid_w / grid_h), which is what lets a page
--  laid out on one screen still look right on another — absolute pixel
--  positions would not survive a different resolution.
-- ─────────────────────────────────────────────────────────────────────

Config.Newspaper = {
    --- Columns across a page. 12 divides cleanly into halves, thirds and
    --- quarters, which is why layout systems keep choosing it.
    columns = 12,
    --- Rows a single page may grow to.
    rows = 16,
    --- Pages one edition may hold.
    maxPages = 16,
    --- Society account ad revenue is paid into, without the prefix the
    --- framework adds. Set nil to record revenue without moving money.
    society = 'weazel',
}

-- ─────────────────────────────────────────────────────────────────────
--  Advertising
-- ─────────────────────────────────────────────────────────────────────

Config.Advertising = {
    --- Suggested price per placement. The newsroom can charge whatever it
    --- likes — this only pre-fills the budget when a campaign is created.
    rates = {
        newspaper = 2500,
        tv        = 5000,
        website   = 1500,
        radio     = 2000,
    },
    --- Charge the advertiser's own bank account when a campaign is marked
    --- paid, rather than only recording it. Needs them to be online.
    chargeAdvertiser = false,

    -- ─── the public board ────────────────────────────────────────────
    --  What ordinary players see. A campaign reaches the board when it
    --  starts running and leaves it when it ends or is paused — the board
    --  reads the dates as well as the status, so an advert that ran out
    --  last week disappears whether or not anyone moved it on.
    --
    --  Nothing about money is ever sent to a player. Budgets, whether the
    --  advertiser paid, and who approved the campaign are not selected by
    --  the query that builds the board.

    --- Let any player open the board. Off hides the command entirely.
    publicBoard = true,

    --- The command that opens it. Change it if something else on your
    --- server already owns the name.
    boardCommand = 'weazelads',

    --- Tell the city when a campaign starts running.
    ---   'everyone'    a card on every screen, like a published story
    ---   'subscribers' only players following the paper
    ---   'off'         no announcement; the board is the only way in
    announce = 'everyone',

    --- Show the rate card on the board, so a business knows what
    --- advertising costs before they come and ask. These are the same
    --- `rates` above.
    showRates = true,
}

-- ─────────────────────────────────────────────────────────────────────
--  News tips
--  The only part of the resource ordinary citizens interact with, so it
--  is also the only part reachable by someone who is not an employee.
-- ─────────────────────────────────────────────────────────────────────

Config.Tips = {
    --- Let any player submit a tip with /newstip. Turn off to accept tips
    --- only from other resources through the integration export.
    allowPublic = true,
    --- Allow tips that do not record who sent them. When false, a
    --- submitter's identifier is always stored (staff still cannot see it
    --- without news.view_confidential).
    allowAnonymous = true,
    --- Paid to the submitter when a tip they sent leads to a published
    --- article. 0 disables payment entirely.
    rewardOnPublish = 0,
    --- Account the reward is paid into: 'cash' or 'bank'.
    rewardAccount = 'bank',
    --- Tips a single player may have open at once, to stop spam.
    maxOpenPerPlayer = 5,
}

-- ─────────────────────────────────────────────────────────────────────
--  Interviews
-- ─────────────────────────────────────────────────────────────────────

Config.Interviews = {
    --- Questions one interview may carry.
    maxQuestions = 20,
    --- How long a target has to answer a request before it lapses.
    requestTimeout = 120,
    --- How close the journalist must be to send a request. 0 disables the
    --- check, allowing remote (phone) interviews.
    maxDistance = 12.0,
}

-- ─────────────────────────────────────────────────────────────────────
--  Locations. Nothing is hard-coded to a single spot.
-- ─────────────────────────────────────────────────────────────────────

Config.Locations = {
    --- Newsroom terminals — where staff open the CMS.
    --- coords is the interaction point; heading is used for the marker.
    newsroom = {
        { label = 'Weazel Newsroom', coords = vec3(-598.42, -929.32, 23.87), heading = 88.0, radius = 1.5 },
    },
    --- Studio sets used by the broadcast system. Each entry supports an
    --- anchor desk, a camera position and a producer station.
    studios = {
        {
            id = 'main',
            label = 'Studio A',
            anchor   = vec3(-1050.12, -232.10, 44.02),
            camera   = vec3(-1052.40, -231.60, 44.02),
            producer = vec3(-1047.80, -234.40, 44.02),
            radius = 2.0,
        },
    },
    --- Newspaper stands where the public can pick up an edition.
    standLocations = {
        { label = 'Legion Square Stand', coords = vec3(215.76, -810.44, 30.79), radius = 1.2 },
    },
}

-- ─────────────────────────────────────────────────────────────────────
--  Rate limiting. Keys map to {max requests, window seconds}.
-- ─────────────────────────────────────────────────────────────────────

Config.RateLimits = {
    default        = { 30, 10 },
    articleSave    = { 10, 10 },
    articlePublish = { 5, 30 },
    tipSubmit      = { 3, 60 },
    mediaCreate    = { 20, 60 },
    search         = { 20, 10 },
    interviewSend  = { 5, 60 },
    assignmentSave = { 15, 30 },
    --- Camera frames. Generous because a feed at the default
    --- two-second interval is 30 a minute by design, but still
    --- capped so a modified client cannot flood it.
    fieldFrame     = { 60, 60 },
}

-- ─────────────────────────────────────────────────────────────────────
--  Integrations. All optional; nothing is hard-coded to one resource.
-- ─────────────────────────────────────────────────────────────────────

Config.Integrations = {
    --- Which inventory to register usable items through.
    ---   'ox_inventory'  the ox_inventory:usedItem event
    ---   'qb-inventory'  QBCore.Functions.CreateUseableItem
    ---   'esx'           ESX.RegisterUsableItem
    ---   'none'          no items at all
    ---
    --- Only ONE path is ever taken. ox_inventory honours the ESX and
    --- QBCore registrations for compatibility *and* fires its own event,
    --- so using both would run every handler twice and a toggle would
    --- equip and unequip in the same keystroke. ox_inventory is detected
    --- and preferred when it is running, whatever this says.
    inventory = 'ox_inventory',

    --- Item that opens the public portal on the front page. Anyone
    --- holding one can read it — that is what a newspaper is.
    newspaperItem = 'newspaper',

    --- The field kit items are NOT listed here. Each one lives on its
    --- gear entry in Config.Field.gear as `item`, so a piece of kit is
    --- defined in one place — prop, animation, offsets and item name
    --- together. Add a fourth piece of gear and it becomes usable with
    --- no change to this block.

    --- Phone adapter resource. nil disables phone push entirely.
    phoneResource = nil,
    --- Use ox_lib for notifications/context where available.
    useOxLib = true,
}

return Config

```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://fantasy-scripts.gitbook.io/docs/weazel-news/configuration-file.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
