--[[
    Sky Phone configuration

    General settings live in this file. Media providers and upload limits live
    in config/media.lua, while translations remain in config/locales/*.lua.
    Sections marked "SERVER ONLY" are guarded with IsDuplicityVersion(), so
    passwords, API keys, migration settings, and server-owned locations are not
    applied by game clients.

    Keep option names unchanged. Restart sky_phone after editing this file.
]]

-- CONFIG_DEFAULT_EXCLUDE_START
-- When enabled, the active configuration is loaded from SQL and managed through
-- /phonepanel. Frontend builds snapshot the shipped defaults from config.lua and
-- media.lua into source/shared/config_default.lua.
Config.PhoneConfigurator = {
    Enabled = true,
}

-- Fixed server permissions. These values remain authoritative even while the
-- Phone Configurator is enabled and are intentionally not shown in its panel.
-- Group names use the active framework's permissions. On Qbox they also match
-- ACE objects such as "admin" from the standard permissions.cfg.
Config.CommandPermissions = {
    phonepanel = { "god", "superadmin", "admin" },
    phonetestdata = { "god", "superadmin", "admin" },
    fliptokverify = { "god", "superadmin", "admin" },
    picstagramverify = { "god", "superadmin", "admin" },
    picstagramadmin = { "god", "superadmin", "admin" },
}
-- CONFIG_DEFAULT_EXCLUDE_END

-- =============================================================================
-- Core, framework and device
-- =============================================================================

Config.Bridge = {
    Framework = "auto", -- auto, esx, qbox, qb
    -- auto, ak47, codem, core, jaksam, jpr, lj, mf, one, origen, ox, ps, qb, qs, smx, tgiann, hex, esx
    -- Compatibility aliases: qb-inv -> qb, qbox -> ox
    Inventory = "auto",
    Locale = "en",
    CallbackTimeout = 15000,
    Debug = false, -- true: show debug/info output; warnings and errors are always shown
}

Config.Command = "phone"

Config.Phone = {
    Item = "phone",
    Unique = true, -- true: data follows each phone item; false: one persistent phone per character; forced false for metadata-free inventories
    Keybind = "F1", -- false disables the configurable phone key mapping
    AllowMovement = true, -- true: game input stays active while the mobile phone is open
    HoldToLook = {
        Enabled = true, -- hold the configured control to hide the cursor and look around; independent of AllowMovement
        Control = 19, -- INPUT_CHARACTER_WHEEL (Left Alt by default)
    },
    DevelopmentCommand = true,
    DeviceName = "iFruit Phone",
}

Config.TestData = {
    Enabled = false, -- development/test servers only; keep disabled in production
    Command = "phonetestdata",
    AdminOnly = false, -- enable only on development servers; every run is scoped to the executing player's phone
}

Config.CustomApps = {
    Enabled = true,
    BundledApps = true,
    ExternalApps = true,
    Debug = false, -- detailed client traces for exports, registration, catalog sync and lifecycle events
    ReadyTimeoutMs = 8000,
    MaximumMessageBytes = 65536,
    MaximumStorageBytesPerApp = 262144,
    MaximumStorageValueBytes = 65536, -- Bridge v1 ceiling; lower values tighten the server policy.
    MaximumStorageKeyLength = 64, -- Bridge v1 ceiling; lower values tighten the server policy.
    MaximumStorageKeysPerApp = 128,
    StorageRequestsPerMinute = 120,
    TrustedAdapters = {},
}

Config.Security = {
    MaximumAttempts = 5,
    LockSeconds = 30,
    AttemptsPerMinute = 12,
}

Config.AdminPanel = {
    Enabled = true,
    Command = "phonepanel",
    MaximumPlayers = 128,
    ReadRequestsPerMinute = 60,
    ActionRequestsPerMinute = 30,
    CredentialRevealsPerMinute = 6,
    AuditLimit = 40,
    ActivityLimit = 40,
}

Config.Sim = {
    Enabled = true, -- false: devices receive a persistent random number automatically; forced false for metadata-free inventories
    RegisteredItem = "sky_phone_sim_registered",
    AnonymousItem = "sky_phone_sim_anonymous",
    NumberLength = 10, -- total number of digits, including NumberPrefix
    NumberPrefix = "", -- digits only; use "555", not "555-"
    NumberGroups = { 3, 3, 4 }, -- display groups separated by spaces
}

-- =============================================================================
-- Calls, voice and radio
-- =============================================================================

Config.Speaker = {
    Enabled = true, -- global phone and radio speaker controls
}

Config.Calls = {
    VoiceProvider = "pma", -- yaca (alias: yaca-voice), pma (alias: pma-voice), saltychat (alias: salty)
    RingSeconds = 30,
    ContactNameMaxLength = 80,
    ContactNotesMaxLength = 500,
    RecentPageSize = 100,
}

Config.Payphones = {
    Enabled = true,
    -- Existing world props with these models are detected automatically.
    Props = {
        "prop_phonebox_01b",
        "p_phonebox_01b_s",
        "prop_phonebox_01a",
        "prop_phonebox_04",
    },
    -- Additional booths are spawned only at these custom positions.
    CustomProp = "prop_phonebox_01b",
    CustomLocations = {
        -- vector4(x, y, z, heading),
    },
    ReplacementProp = "sf_prop_sf_phonebox_01b_s",
    PricePerSecond = 1,
    PaymentAccount = "cash", -- cash or bank
    Currency = "$",
    NoAnswerTimeoutSeconds = 30,
    CallerNumber = "PAYPHONE",
    InteractionDistance = 1.8,
    ServerValidationDistance = 3.0,
    MaximumCallDistance = 4.0,
    ScanDistance = 25.0,
    ScanIntervalMs = 1000,
    ModelLoadTimeoutMs = 5000,
    Animation = {
        Dictionary = "anim@scripted@payphone_hits@male@",
        PedClip = "FXFR_PAV_1_INTRO_MALE",
        PropClip = "FXFR_PAV_1_INTRO_PHONE",
        HangupDurationMs = 2000,
    },
}

Config.Radio = {
    VoiceProvider = "auto", -- auto, yaca, pma, saltychat
    DefaultVolume = 50,
    HistoryLimit = 8,
    FrequencyMin = 0.1,
    FrequencyMax = 999.9,
    FrequencyDecimals = 1,
    AllowSecondary = true,
    Notifications = false,
    AutoRejoin = false,
    DisplayName = {
        Enabled = true,
        MaxLength = 32,
        AllowedJobs = { -- Job name = minimum grade. Unlisted jobs cannot set a radio display name.
            police = 0,
            sheriff = 0,
            fib = 0,
            army = 0,
            ambulance = 0,
        },
    },
    Hud = {
        Enabled = true,
        SpeakerPersistMilliseconds = 3000,
        Position = {
            Horizontal = "right", -- left or right
            Vertical = "top", -- top or bottom
            HorizontalOffset = 2.0, -- vh
            VerticalOffset = 30.0, -- vh
        },
    },
    Badge = {
        Enabled = true,
        MaxLength = 8,
        ForbiddenPatterns = { "88", "1488", "18", "14", "28", "198" },
    },
    LockedChannels = {
        {
            range = { 0.1, 100.0 },
            jobs = {
                police = true,
                sheriff = true,
                fib = true,
                army = true,
                ambulance = true,
            },
        },
    },
}

-- =============================================================================
-- Phone presentation and animations
-- =============================================================================

Config.Animations = {
    Enabled = true,
    PropModel = "prop_npc_phone_02",
    PropBone = 28422,
    LoadTimeoutMs = 5000,
    ContextPollMs = 250,
    Dictionaries = {
        OnFoot = "cellphone@",
        Driver = "anim@cellphone@in_car@ds",
        Passenger = "anim@cellphone@in_car@ps",
        Camera = "cellphone@self",
    },
    Clips = {
        TextIn = "cellphone_text_in",
        TextRead = "cellphone_text_read_base",
        TextOut = "cellphone_text_out",
        TextToCall = "cellphone_text_to_call",
        CallListen = "cellphone_call_listen_base",
        CallToText = "cellphone_call_to_text",
        CallOut = "cellphone_call_out",
        Camera = "selfie",
    },
    Transforms = {
        Portrait = {
            position = vector3(0.0, 0.0, 0.0),
            rotation = vector3(0.0, 0.0, 0.0),
        },
        Landscape = {
            position = vector3(0.0, 0.0, 0.0),
            rotation = vector3(0.0, 0.0, 90.0),
        },
    },
}

-- =============================================================================
-- Built-in applications
-- =============================================================================

Config.Messages = {
    BodyMaxLength = 2000,
    ConversationScanLimit = 1000,
    ThreadPageSize = 200,
    SendsPerMinute = 30,
    MediaLoadsPerMinute = 120,
    VoiceMaxDurationMs = 30000,
    VoiceMaxBase64Length = 180000,
    VoiceWaveformSamples = 48,
    VideoMaxDurationMs = 30000,
    DeleteBatchSize = 20,
}

Config.Memos = {
    MaximumCount = 100,
    MaximumDurationMs = 300000,
    MaximumBytes = 2 * 1024 * 1024,
    MaximumWaveformSamples = 96,
    TitleMaxLength = 120,
    NoteMaxLength = 2000,
    UploadsPerMinute = 10,
    WritesPerMinute = 60,
    ListsPerMinute = 60,
    MaximumPendingUploads = 1,
    UploadSessionTimeoutMs = 60000,
}

Config.EasyShare = {
    Enabled = true,
    DefaultVisibility = "everyone", -- everyone, contacts or hidden
    MaximumDistance = 15.0,
    HistoryLimit = 50,
    PendingSeconds = 30,
    TransferDurationMs = 3000,
    RequestsPerMinute = 12,
    BootstrapRequestsPerMinute = 30,
    VisibilityUpdatesPerMinute = 10,
    ActionsPerMinute = 30,
    PayloadMaxBytes = 24000,
}

Config.DarkChat = {
    AliasMaxLength = 32,
    BodyMaxLength = 2000,
    ThreadPageSize = 200,
    SendsPerMinute = 30,
    ActionsPerMinute = 60,
    ReportsPerDay = 10,
    VoiceMaxDurationMs = 60000,
    VoiceMaxBase64Length = 360000,
    VoiceWaveformSamples = 48,
    CleanupIntervalSeconds = 30,
    AllowedDisappearTimers = {
        [0] = true,
        [-1] = true, -- after reading
        [60] = true,
        [300] = true,
        [3600] = true,
        [86400] = true,
        [604800] = true,
    },
}

Config.Mail = {
    Domain = "ifruit.com",
    LocalPartMinLength = 3,
    LocalPartMaxLength = 32,
    PasswordMinLength = 6,
    PasswordMaxLength = 64,
    SubjectMaxLength = 120,
    BodyMaxLength = 20000,
    MaxRecipients = 10,
    PageSize = 50,
    AuthAttemptsPerMinute = 5,
    MailboxNameMaxLength = 50,
    MaxMailboxes = 20,
    MailboxRequestsPerMinute = 12,
    DeleteBatchSize = 50,
    DeleteRequestsPerMinute = 12,
}

Config.Banking = {
    Currency = "$",
    MinimumAmount = 1,
    MaximumAmount = 1000000,
    ActionsPerMinute = 12,
    HistoryLimit = 50,
}

Config.Health = {
    DailyStepGoal = 8000,
    SampleIntervalMs = 500,
    ReportIntervalSeconds = 30,
    ReportsPerMinute = 4,
    MaximumSpeedMetersPerSecond = 12.0,
    EmergencyNumber = "911",
    ProfileTextMaxLength = 500,
}

Config.Billing = {
    Enabled = true,
    Currency = "$",
    PaymentAccount = "bank", -- bank or cash
    MinimumAmount = 1,
    MaximumAmount = 1000000,
    PageSize = 30,
    UrgentLimit = 5,
    ActionsPerMinute = 8,
    AllowDisputes = true,
    DefaultDueDays = 7,
    MaximumTitleLength = 160,
    MaximumDescriptionLength = 1000,
}

Config.Garage = {
    System = "auto", -- auto, custom, esx, qb, qbox, ak47, bp, cd, codem, ds-servercreator, hex, jg, my, okok, op, quasar, rx, vms, ws, zyke_garages
    MaximumVehicles = 250,
    RequestsPerMinute = 30,
    VehicleImages = {
        Enabled = true,
        UrlTemplate = "https://cdn.sky-systems.net/vehicles/{model}.png",
        -- Optional spawn-name overrides for garages that only store model hashes.
        -- Example: [970598228] = "sultan",
        ModelNames = {},
    },
    Custom = {
        Table = "",
        OwnerColumn = "",
    },
    Valet = {
        Enabled = true,
        Price = 750,
        Account = "bank", -- bank or cash
        RequestsPerMinute = 3,
        CooldownSeconds = 60,
        TimeoutSeconds = 180,
        SpawnDistance = 110.0,
        ArrivalDistance = 14.0,
        DriveSpeed = 20.0,
        DrivingStyle = 786603,
        DriverModel = "s_m_m_autoshop_01",
        -- The current valet driver route supports road vehicles. Keep non-road types disabled.
        VehicleTypes = {
            car = true,
            bike = true,
            boat = false,
            plane = false,
            helicopter = false,
        },
    },
}

Config.Housing = {
    System = "auto", -- auto, rtx, quasar, vms, rx, nolag, sn, esx_property, qbx_properties
    AutoPriority = { "esx_property", "qbx_properties", "rtx", "quasar", "vms", "rx", "nolag", "sn" },
    MaximumProperties = 50,
    OverviewRequestsPerMinute = 30,
    ActionsPerMinute = 12,
    Camera = {
        HeightAboveEntrance = 2.5,
        FieldOfView = 55.0,
        MinimumFieldOfView = 20.0,
        MaximumFieldOfView = 75.0,
        RotateSpeed = 0.65,
        ExitControl = 177, -- Backspace
        NightVisionControl = 38, -- E
        ZoomInControl = 241, -- Mouse wheel up
        ZoomOutControl = 242, -- Mouse wheel down
        LeftControl = 174,
        RightControl = 175,
        UpControl = 172,
        DownControl = 173,
    },
}

Config.Marketplace = {
    PageSize = 20,
    MessagePageSize = 50,
    OfferHistorySize = 50,
    MaxActiveListings = 15,
    MaxImages = 6,
    TitleMinLength = 5,
    TitleMaxLength = 70,
    DescriptionMinLength = 20,
    DescriptionMaxLength = 2000,
    MessageMaxLength = 1000,
    MaximumPrice = 100000000,
    ListingLifetimeDays = 7,
    Categories = {
        "vehicles",
        "property",
        "electronics",
        "clothing",
        "tools",
        "leisure",
        "services",
        "jobs",
        "wanted",
        "other",
    },
    Districts = {
        "los_santos",
        "vinewood",
        "vespucci",
        "south_los_santos",
        "sandy_shores",
        "paleto_bay",
        "blaine_county",
    },
    PhotoGradients = {
        "linear-gradient(145deg, #ff9a62, #5f2c82 58%, #141e30)",
        "linear-gradient(160deg, #67d5b5, #26648e 55%, #0b132b)",
        "linear-gradient(135deg, #fbc2eb, #a6c1ee 48%, #302b63)",
        "linear-gradient(150deg, #f6d365, #fda085 45%, #512b58)",
        "linear-gradient(145deg, #ff6b6b, #845ec2 52%, #0f2027)",
        "linear-gradient(150deg, #00c9a7, #4d8076 46%, #1f3a5f)",
        "linear-gradient(135deg, #ffc75f, #f96d80 48%, #4b4453)",
    },
}

Config.LocalPages = {
    PageSize = 20,
    MaxImages = 6,
    ProfileHandleMinLength = 3,
    ProfileHandleMaxLength = 24,
    ProfileBioMaxLength = 160,
    TitleMinLength = 5,
    TitleMaxLength = 80,
    BodyMinLength = 10,
    BodyMaxLength = 1500,
    Categories = { "recommendation", "wanted", "service", "event", "place", "community" },
    CityMarktSharesPerDay = 1,
}

-- =============================================================================
-- Social applications
-- =============================================================================

Config.FlipTok = {
    PageSize = 12,
    CaptionMaxLength = 500,
    CommentMaxLength = 300,
    BioMaxLength = 160,
    PasswordMinLength = 8,
    PasswordMaxLength = 72,
    MaxVideoDurationMs = 300000,
    MaxPostMedia = 10,
    MusicTracks = {},
    VerifyCommand = "fliptokverify",
    ReportWebhookConvar = "sky_phone_fliptok_report_webhook",
}

Config.Picstagram = {
    PageSize = 12,
    CommentPageSize = 100,
    MaxPostMedia = 5,
    CaptionMaxLength = 800,
    CommentMaxLength = 300,
    BioMaxLength = 160,
    LocationMaxLength = 80,
    StoryTextMaxLength = 160,
    StoryLifetimeSeconds = 24 * 60 * 60,
    PasswordMinLength = 8,
    PasswordMaxLength = 72,
    PostsPerMinute = 6,
    StoriesPerMinute = 6,
    CommentsPerMinute = 20,
    ReportDetailsMaxLength = 500,
    ReportReasons = { "spam", "harassment", "dangerous", "illegal", "other" },
    VerifyCommand = "picstagramverify",
}

Config.Feather = {
    PageSize = 20,
    TextMaxLength = 360,
    BioMaxLength = 160,
    DisplayNameMaxLength = 50,
    HandleMaxLength = 30,
    SearchMaxLength = 80,
    HashtagMaxLength = 64,
    TrendingTopicLimit = 10,
    ReportDetailsMaxLength = 500,
    MaxImages = 4,
    PostsPerMinute = 8,
    ActionsPerMinute = 90,
    ReportsPerDay = 10,
}

-- =============================================================================
-- Utilities, groups and transport
-- =============================================================================

Config.MapMarkers = {
    MaximumMarkers = 50,
    LabelMaxLength = 40,
    ActionsPerMinute = 60,
}

Config.CrewLink = {
    UsernameMinLength = 3,
    UsernameMaxLength = 20,
    PasswordMinLength = 8,
    PasswordMaxLength = 72,
    GroupNameMinLength = 3,
    GroupNameMaxLength = 32,
    MaximumGroupsPerProfile = 5,
    MaximumMembersPerGroup = 16,
    InviteCodeLength = 8,
    InviteLifetimeSeconds = 30,
    NearbyInviteDistance = 5.0,
    NearbyScanLimit = 12,
    PingLabelMaxLength = 48,
    PingLifetimeSeconds = 300,
    MaximumActivePings = 12,
    ActionsPerMinute = 30,
    LiveRequestsPerMinute = 120,
    OverheadRefreshMilliseconds = 3000,
    OverheadDistance = 50.0,
    ExternalPingResources = {
        -- ["example_resource"] = true,
    },
}

Config.Calendar = {
    TitleMaxLength = 120,
    NoteMaxLength = 2000,
    MaximumDurationSeconds = 7 * 24 * 60 * 60,
    MaximumQuerySeconds = 370 * 24 * 60 * 60,
    PastEditSeconds = 365 * 24 * 60 * 60,
    FutureSeconds = 5 * 365 * 24 * 60 * 60,
    ReminderPollSeconds = 15,
}

Config.SkyRide = {
    PaymentAccount = "bank",
    Currency = "$",
    ProfileNameMinLength = 2,
    ProfileNameMaxLength = 50,
    DistanceUnit = "kilometer", -- kilometer or mile
    QuoteLifetimeSeconds = 120,
    HistoryLimit = 50,
    ReadsPerMinute = 120,
    ActionsPerMinute = 30,
    QuotesPerMinute = 20,
    RecoveryIntervalSeconds = 15,
    PaymentPendingRecoverySeconds = 30,
    PickupSelectionRadius = 150.0,
    ArrivalRadius = 40.0,
    CompletionRadius = 80.0,
    MinimumDistanceMeters = 250,
    MaximumDistanceMeters = 40000,
    RouteDistanceMultiplier = 1.25,
    AverageSpeedMetersPerSecond = 12.0,
    DriverPayoutPercent = 85,
    RatingCommentMaxLength = 300,
    MaximumTip = 10000,
    CustomFare = {
        Enabled = true,
        MinimumPrice = 5,
        MaximumPrice = 100000,
        MinimumCalculatedMultiplier = 0.5,
        MaximumCalculatedMultiplier = 3.0,
    },
    DriverJobs = {
        taxi = 0,
    },
    Services = {
        {
            Id = "taxi",
            Seats = 4,
            EtaMinutes = 3,
            BaseFare = 12,
            PricePerKilometer = 18,
            PricePerMile = 29,
            PricePerMinute = 1,
            MinimumFare = 15,
        },
        {
            Id = "comfort",
            Seats = 4,
            EtaMinutes = 5,
            BaseFare = 16,
            PricePerKilometer = 24,
            PricePerMile = 39,
            PricePerMinute = 2,
            MinimumFare = 22,
        },
        {
            Id = "xl",
            Seats = 6,
            EtaMinutes = 7,
            BaseFare = 20,
            PricePerKilometer = 30,
            PricePerMile = 48,
            PricePerMinute = 2,
            MinimumFare = 28,
        },
        {
            Id = "premium",
            Seats = 4,
            EtaMinutes = 8,
            BaseFare = 28,
            PricePerKilometer = 40,
            PricePerMile = 64,
            PricePerMinute = 3,
            MinimumFare = 38,
        },
    },
    QuickLocations = {
        {
            Id = "legion-square",
            Label = "Legion Square",
            Coords = vector3(-265.1, -960.2, 31.2),
        },
        {
            Id = "diamond-casino",
            Label = "Diamond Casino",
            Coords = vector3(925.2, 46.4, 81.1),
        },
        {
            Id = "airport",
            Label = "Los Santos Airport",
            Coords = vector3(-1037.7, -2737.8, 20.2),
        },
        {
            Id = "vinewood",
            Label = "Vinewood",
            Coords = vector3(-594.4, -929.9, 23.9),
        },
    },
}

Config.Crypto = {
    Enabled = true,
    Currency = "$",
    BankAccount = "bank",
    AssetScale = 1000000,
    PriceScale = 100,
    QuoteLifetimeSeconds = 8,
    PriceTickMinimumSeconds = 7,
    PriceTickMaximumSeconds = 12,
    MarketsPerTickMinimum = 8,
    MarketsPerTickMaximum = 12,
    TickMovementDivisor = 10,
    RandomImpulseDivisor = 3,
    MomentumDecayBasisPoints = 8000,
    MomentumImpulseBasisPoints = 2000,
    MeanReversionBasisPoints = 55,
    CycleDurationMinimumTicks = 14,
    CycleDurationMaximumTicks = 30,
    CycleStrengthMinimumBasisPoints = 2800,
    CycleStrengthMaximumBasisPoints = 5200,
    CycleTransitionBasisPoints = 1800,
    GlobalCycleMinimumTicks = 24,
    GlobalCycleMaximumTicks = 54,
    GlobalTrendMinimumBasisPoints = 8,
    GlobalTrendMaximumBasisPoints = 24,
    MarketShockChanceBasisPoints = 100,
    MarketShockMinimumMultiplier = 2,
    MarketShockMaximumMultiplier = 3,
    MaximumMovementMultiplier = 3,
    HistoryRetentionTicks = 4096,
    SparklinePoints = 48,
    SessionSeconds = 30 * 60,
    RecentAuthenticationSeconds = 5 * 60,
    PasswordMinLength = 8,
    PasswordMaxLength = 72,
    HandleMinLength = 3,
    HandleMaxLength = 20,
    FeeBasisPoints = 75,
    MinimumFee = 1,
    MinimumSettlement = 10,
    MaximumSettlement = 250000,
    MaximumTradeNotional = 100000,
    MaximumPositionQuantity = 100000,
    MaximumTransferQuantity = 100000,
    DailyDepositLimit = 500000,
    DailyWithdrawalLimit = 250000,
    DailyTradeLimit = 500000,
    ActionsPerMinute = 30,
    LoginAttempts = 5,
    LockoutSeconds = 5 * 60,
    TreasuryCash = 5000000,
    Markets = {
        {
            Id = "aurora",
            Symbol = "AUR",
            Name = "Aurora",
            Color = "#25d9ad",
            Logo = "◈",
            InitialPrice = 12850,
            MinimumPrice = 3000,
            MaximumPrice = 80000,
            VolatilityBasisPoints = 180,
            IssuedSupply = 1000000,
            TreasuryInventory = 850000,
        },
        {
            Id = "vertex",
            Symbol = "VTX",
            Name = "Vertex",
            Color = "#4d8cff",
            Logo = "◆",
            InitialPrice = 4275,
            MinimumPrice = 800,
            MaximumPrice = 30000,
            VolatilityBasisPoints = 260,
            IssuedSupply = 2500000,
            TreasuryInventory = 2100000,
        },
        {
            Id = "ember",
            Symbol = "EMB",
            Name = "Ember",
            Color = "#ff9d54",
            Logo = "✦",
            InitialPrice = 980,
            MinimumPrice = 100,
            MaximumPrice = 12000,
            VolatilityBasisPoints = 340,
            IssuedSupply = 8000000,
            TreasuryInventory = 6800000,
        },
        {
            Id = "titan",
            Symbol = "TTN",
            Name = "Titan",
            Color = "#8f7cff",
            Logo = "⬢",
            InitialPrice = 425000000,
            MinimumPrice = 100000000,
            MaximumPrice = 900000000,
            VolatilityBasisPoints = 65,
            IssuedSupply = 21000,
            TreasuryInventory = 17500,
        },
        {
            Id = "crown",
            Symbol = "CRN",
            Name = "Crown",
            Color = "#f6c453",
            Logo = "♛",
            InitialPrice = 128000000,
            MinimumPrice = 25000000,
            MaximumPrice = 350000000,
            VolatilityBasisPoints = 85,
            IssuedSupply = 64000,
            TreasuryInventory = 51000,
        },
        {
            Id = "atlas",
            Symbol = "ATL",
            Name = "Atlas",
            Color = "#e66f9e",
            Logo = "▲",
            InitialPrice = 12000000,
            MinimumPrice = 3000000,
            MaximumPrice = 30000000,
            VolatilityBasisPoints = 110,
            IssuedSupply = 250000,
            TreasuryInventory = 205000,
        },
        {
            Id = "zenith",
            Symbol = "ZNT",
            Name = "Zenith",
            Color = "#6b8cff",
            Logo = "✥",
            InitialPrice = 68000000,
            MinimumPrice = 18000000,
            MaximumPrice = 160000000,
            VolatilityBasisPoints = 95,
            IssuedSupply = 125000,
            TreasuryInventory = 99000,
        },
        {
            Id = "celestium",
            Symbol = "CLS",
            Name = "Celestium",
            Color = "#8ad8ff",
            Logo = "✧",
            InitialPrice = 8450000,
            MinimumPrice = 1500000,
            MaximumPrice = 22000000,
            VolatilityBasisPoints = 125,
            IssuedSupply = 420000,
            TreasuryInventory = 340000,
        },
        {
            Id = "orbit",
            Symbol = "ORB",
            Name = "Orbit",
            Color = "#57c7ff",
            Logo = "◎",
            InitialPrice = 1244000,
            MinimumPrice = 250000,
            MaximumPrice = 3500000,
            VolatilityBasisPoints = 145,
            IssuedSupply = 900000,
            TreasuryInventory = 735000,
        },
        {
            Id = "helix",
            Symbol = "HLX",
            Name = "Helix",
            Color = "#c47cff",
            Logo = "⌁",
            InitialPrice = 582000,
            MinimumPrice = 90000,
            MaximumPrice = 1600000,
            VolatilityBasisPoints = 175,
            IssuedSupply = 1400000,
            TreasuryInventory = 1140000,
        },
        {
            Id = "nova",
            Symbol = "NVA",
            Name = "Nova",
            Color = "#ff6f91",
            Logo = "✺",
            InitialPrice = 325000,
            MinimumPrice = 50000,
            MaximumPrice = 900000,
            VolatilityBasisPoints = 210,
            IssuedSupply = 2200000,
            TreasuryInventory = 1800000,
        },
        {
            Id = "prism",
            Symbol = "PRM",
            Name = "Prism",
            Color = "#5ee1c4",
            Logo = "◇",
            InitialPrice = 74500,
            MinimumPrice = 12000,
            MaximumPrice = 240000,
            VolatilityBasisPoints = 195,
            IssuedSupply = 3800000,
            TreasuryInventory = 3100000,
        },
        {
            Id = "quantum",
            Symbol = "QTM",
            Name = "Quantum",
            Color = "#7a9cff",
            Logo = "◉",
            InitialPrice = 21400,
            MinimumPrice = 3500,
            MaximumPrice = 85000,
            VolatilityBasisPoints = 230,
            IssuedSupply = 5200000,
            TreasuryInventory = 4250000,
        },
        {
            Id = "drift",
            Symbol = "DRF",
            Name = "Drift",
            Color = "#4ecdc4",
            Logo = "≋",
            InitialPrice = 7600,
            MinimumPrice = 900,
            MaximumPrice = 30000,
            VolatilityBasisPoints = 280,
            IssuedSupply = 12000000,
            TreasuryInventory = 9800000,
        },
        {
            Id = "pulse",
            Symbol = "PLS",
            Name = "Pulse",
            Color = "#ff5f7f",
            Logo = "◍",
            InitialPrice = 1840,
            MinimumPrice = 200,
            MaximumPrice = 9000,
            VolatilityBasisPoints = 320,
            IssuedSupply = 18000000,
            TreasuryInventory = 14800000,
        },
        {
            Id = "moss",
            Symbol = "MOS",
            Name = "Moss",
            Color = "#64c987",
            Logo = "♧",
            InitialPrice = 460,
            MinimumPrice = 60,
            MaximumPrice = 2400,
            VolatilityBasisPoints = 360,
            IssuedSupply = 36000000,
            TreasuryInventory = 29500000,
        },
        {
            Id = "flux",
            Symbol = "FLX",
            Name = "Flux",
            Color = "#a777e3",
            Logo = "ϟ",
            InitialPrice = 238,
            MinimumPrice = 35,
            MaximumPrice = 1400,
            VolatilityBasisPoints = 390,
            IssuedSupply = 44000000,
            TreasuryInventory = 36000000,
        },
        {
            Id = "tide",
            Symbol = "TDE",
            Name = "Tide",
            Color = "#46a6ff",
            Logo = "≈",
            InitialPrice = 125,
            MinimumPrice = 20,
            MaximumPrice = 700,
            VolatilityBasisPoints = 410,
            IssuedSupply = 60000000,
            TreasuryInventory = 49000000,
        },
        {
            Id = "spark",
            Symbol = "SPK",
            Name = "Spark",
            Color = "#ffb84d",
            Logo = "✹",
            InitialPrice = 74,
            MinimumPrice = 12,
            MaximumPrice = 420,
            VolatilityBasisPoints = 430,
            IssuedSupply = 85000000,
            TreasuryInventory = 70000000,
        },
        {
            Id = "pebble",
            Symbol = "PBL",
            Name = "Pebble",
            Color = "#9aa8b8",
            Logo = "●",
            InitialPrice = 22,
            MinimumPrice = 4,
            MaximumPrice = 160,
            VolatilityBasisPoints = 470,
            IssuedSupply = 150000000,
            TreasuryInventory = 124000000,
        },
        {
            Id = "nano",
            Symbol = "NNO",
            Name = "Nano",
            Color = "#5bd6ff",
            Logo = "η",
            InitialPrice = 9,
            MinimumPrice = 1,
            MaximumPrice = 85,
            VolatilityBasisPoints = 520,
            IssuedSupply = 260000000,
            TreasuryInventory = 214000000,
        },
        {
            Id = "shard",
            Symbol = "SHD",
            Name = "Shard",
            Color = "#70e0d0",
            Logo = "△",
            InitialPrice = 5,
            MinimumPrice = 1,
            MaximumPrice = 55,
            VolatilityBasisPoints = 560,
            IssuedSupply = 420000000,
            TreasuryInventory = 345000000,
        },
        {
            Id = "pixel",
            Symbol = "PXL",
            Name = "Pixel",
            Color = "#ff78c8",
            Logo = "▦",
            InitialPrice = 3,
            MinimumPrice = 1,
            MaximumPrice = 32,
            VolatilityBasisPoints = 610,
            IssuedSupply = 700000000,
            TreasuryInventory = 580000000,
        },
        {
            Id = "dust",
            Symbol = "DST",
            Name = "Dust",
            Color = "#c4b5a5",
            Logo = "·",
            InitialPrice = 1,
            MinimumPrice = 1,
            MaximumPrice = 18,
            VolatilityBasisPoints = 680,
            IssuedSupply = 1200000000,
            TreasuryInventory = 990000000,
        },
    },
}

-- =============================================================================
-- Server-only configuration
-- =============================================================================

if IsDuplicityVersion() then
    -- -------------------------------------------------------------------------
    -- Server secrets
    -- -------------------------------------------------------------------------

    Config.Server = {
        -- Keep these values private and stable. Changing a pepper invalidates existing app passwords.
        PasscodePepper = "f626581802800478346266e66414d8e6f2c28050214a593c25901904c162bffe",
        CrewLinkPasswordPepper = "2751e5729aee4955a529c1a87104b58fbe1d7280b18b66264236028caa06eb47",
        FlipTokPasswordPepper = "a85ea307680f1205a4fda03be8af18ecf7edf16ffc83450b90cac7e41a9719a7",
        PicstagramPasswordPepper = "653e41dd19aba5ef750a668ad1886273ba7d0e0420bd5c745748df80a6e22676",
    }

    -- -------------------------------------------------------------------------
    -- Company directory
    -- -------------------------------------------------------------------------

    Config.Companies = {
        Enabled = true,
        PageSize = 20,
        MaximumPageSize = 50,
        MaximumOpenRequestsPerSim = 5,
        MaximumServices = 25,
        MaximumRequestMedia = 3,
        SubjectMaxLength = 120,
        RequestBodyMaxLength = 2000,
        MessageMaxLength = 2000,
        ProfileDescriptionMaxLength = 1000,
        DistrictMaxLength = 80,
        AddressMaxLength = 160,
        ServiceTitleMaxLength = 80,
        ServiceDescriptionMaxLength = 500,
        ServicePriceMaxLength = 80,
        AnnouncementTitleMaxLength = 120,
        AnnouncementBodyMaxLength = 1000,
        AvailabilityMaximumSeconds = 24 * 60 * 60,
        AnnouncementMaximumSeconds = 30 * 24 * 60 * 60,
        RetentionDays = 180,
        RateLimits = {
            Read = 120,
            Search = 60,
            CreateRequest = 5,
            Message = 30,
            RequestAction = 30,
            Profile = 12,
            CallAvailability = 30,
        },
        CallRouting = {
            MaxAttempts = 3,
            RingSeconds = 10,
        },
        Categories = {
            "public_services",
            "vehicles",
            "transport",
        },
        Statuses = {
            new = true,
            assigned = true,
            in_progress = true,
            waiting_customer = true,
            completed = true,
            cancelled = true,
        },
        AvailabilityStatuses = {
            available = true,
            busy = true,
            closed = true,
        },
        Definitions = {
            police = {
                Job = "police",
                Name = "Los Santos Police Department",
                Category = "public_services",
                Public = true,
                Emergency = true,
                Verified = true,
                Icon = "shield",
                LogoUrl = "https://picsum.photos/seed/companies-police-logo/180/180",
                Description = "Public safety, emergency response, and police services.",
                DefaultAvailability = "closed",
                AcceptsRequests = false,
                District = "Mission Row",
                LocationLabel = "Mission Row Police Station",
                Address = "Mission Row Police Station",
                Location = vector3(425.1, -979.5, 30.7),
                ServiceLine = {
                    Number = "911",
                    AutoContact = true,
                    CanCall = true,
                    CanMessage = false,
                    Routing = "round_robin",
                    MinimumGrade = 0,
                },
                Permissions = {
                    WorkQueue = 0,
                    Availability = 1,
                    Assign = 2,
                    Profile = 3,
                    Hours = 3,
                    Services = 3,
                    Announcement = 3,
                },
                Services = {},
            },
            ambulance = {
                Job = "ambulance",
                Name = "Emergency Medical Services",
                Category = "public_services",
                Public = true,
                Emergency = true,
                Verified = true,
                Icon = "medical",
                LogoUrl = "https://picsum.photos/seed/companies-ems-logo/180/180",
                Description = "Emergency medical response and patient care.",
                DefaultAvailability = "closed",
                AcceptsRequests = false,
                District = "Pillbox Hill",
                LocationLabel = "Pillbox Hill Medical Center",
                Address = "Pillbox Hill Medical Center",
                Location = vector3(307.2, -595.3, 43.3),
                ServiceLine = {
                    Number = "912",
                    AutoContact = true,
                    CanCall = true,
                    CanMessage = false,
                    Routing = "round_robin",
                    MinimumGrade = 0,
                },
                Permissions = {
                    WorkQueue = 0,
                    Availability = 1,
                    Assign = 2,
                    Profile = 3,
                    Hours = 3,
                    Services = 3,
                    Announcement = 3,
                },
                Services = {},
            },
            fire = {
                Job = "fire",
                Name = "Los Santos Fire Department",
                Category = "public_services",
                Public = true,
                Emergency = true,
                Verified = true,
                Icon = "flame",
                LogoUrl = "https://picsum.photos/seed/companies-fire-logo/180/180",
                Description = "Fire response, rescue, and public safety services.",
                DefaultAvailability = "closed",
                AcceptsRequests = false,
                District = "El Burro Heights",
                LocationLabel = "Capital Boulevard Fire Station",
                Address = "Capital Boulevard Fire Station",
                Location = vector3(1200.6, -1473.6, 34.9),
                ServiceLine = {
                    Number = "913",
                    AutoContact = true,
                    CanCall = true,
                    CanMessage = false,
                    Routing = "round_robin",
                    MinimumGrade = 0,
                },
                Permissions = {
                    WorkQueue = 0,
                    Availability = 1,
                    Assign = 2,
                    Profile = 3,
                    Hours = 3,
                    Services = 3,
                    Announcement = 3,
                },
                Services = {},
            },
            mechanic = {
                Job = "mechanic",
                Name = "Los Santos Customs",
                Category = "vehicles",
                Public = true,
                Emergency = false,
                Verified = true,
                Icon = "wrench",
                LogoUrl = "https://picsum.photos/seed/companies-mechanic-logo/180/180",
                Description = "Vehicle diagnostics, repairs, and roadside assistance.",
                DefaultAvailability = "closed",
                AcceptsRequests = true,
                District = "Burton",
                LocationLabel = "Los Santos Customs",
                Address = "Carcer Way",
                Location = vector3(-337.3, -136.9, 39.0),
                ServiceLine = {
                    Number = "5550101000",
                    AutoContact = true,
                    CanCall = true,
                    CanMessage = false,
                    Routing = "round_robin",
                    MinimumGrade = 0,
                },
                Permissions = {
                    WorkQueue = 0,
                    Availability = 1,
                    Assign = 2,
                    Profile = 3,
                    Hours = 3,
                    Services = 3,
                    Announcement = 3,
                },
                Services = {
                    {
                        Id = "mechanic-repair",
                        Title = "Vehicle repair",
                        Description = "Diagnostics and repairs for road vehicles.",
                        Price = "Price after inspection",
                        RequestsEnabled = true,
                    },
                    {
                        Id = "mechanic-towing",
                        Title = "Roadside assistance",
                        Description = "Assistance for disabled vehicles.",
                        Price = "Price by distance",
                        RequestsEnabled = true,
                    },
                },
            },
            taxi = {
                Job = "taxi",
                Name = "Downtown Cab Co.",
                Category = "transport",
                Public = true,
                Emergency = false,
                Verified = true,
                Icon = "car",
                LogoUrl = "https://picsum.photos/seed/companies-taxi-logo/180/180",
                Description = "Staffed taxi rides throughout Los Santos and Blaine County.",
                DefaultAvailability = "closed",
                AcceptsRequests = true,
                District = "East Vinewood",
                LocationLabel = "Downtown Cab Co.",
                Address = "Tangerine Street",
                Location = vector3(895.0, -179.2, 74.7),
                ServiceLine = {
                    Number = "5550102000",
                    AutoContact = true,
                    CanCall = true,
                    CanMessage = false,
                    Routing = "round_robin",
                    MinimumGrade = 0,
                },
                Permissions = {
                    WorkQueue = 0,
                    Availability = 1,
                    Assign = 2,
                    Profile = 3,
                    Hours = 3,
                    Services = 3,
                    Announcement = 3,
                },
                Services = {
                    {
                        Id = "taxi-ride",
                        Title = "Taxi ride",
                        Description = "Request a staffed taxi service.",
                        Price = "Metered fare",
                        RequestsEnabled = true,
                    },
                },
            },
        },
    }

    -- -------------------------------------------------------------------------
    -- Music library
    -- -------------------------------------------------------------------------

    Config.Music = {
        -- Files are found recursively in config/music by their Id. For example,
        -- Id = "night-drive" finds night-drive.ogg/mp3 and optional artwork with
        -- the same name (.webp/.png/.jpg/.jpeg), even inside subdirectories.
        Tracks = {
            -- {
            --     Id = "night-drive",
            --     Title = "Night Drive",
            --     Artist = "Sky Records",
            -- },
        },

        MaximumPersonalSongs = 100,
        MaximumPlaylists = 50,
        MaximumPlaylistSongs = 250,
        PlaylistNameMaxLength = 80,
        ActionsPerMinute = 30,
        MetadataTimeoutMs = 4500,
    }

    -- -------------------------------------------------------------------------
    -- Weazel News
    -- -------------------------------------------------------------------------

    Config.WeazelNews = {
        Enabled = true,
        PageSize = 20,
        MaximumOffset = 10000,
        MaximumImages = 6,
        SearchMaxLength = 80,
        DraftTitleMinLength = 1,
        DraftBodyMinLength = 1,
        TitleMinLength = 1,
        TitleMaxLength = 160,
        BodyMinLength = 1,
        BodyMaxLength = 12000,
        ExcerptMaxLength = 240,
        RateLimits = {
            Read = 120,
            Write = 20,
        },
        Categories = {
            "official",
            "events",
            "jobs",
            "news",
            "business",
        },
        -- Framework job name = minimum grade. Every listed job can manage every article.
        AllowedJobs = {
            weazel = 0,
            reporter = 0,
        },
    }

    -- -------------------------------------------------------------------------
    -- CityWarn population warnings
    -- -------------------------------------------------------------------------

    Config.CityWarn = {
        Enabled = true,
        PageSize = 30,
        MaximumActiveAlerts = 20,
        TitleMaxLength = 120,
        BodyMaxLength = 2000,
        InstructionsMaxLength = 2000,
        UpdateMaxLength = 2000,
        AreaLabelMaxLength = 120,
        MinimumRadius = 100,
        MaximumRadius = 10000,
        DefaultDurationMinutes = 60,
        MaximumDurationMinutes = 1440,
        RequireDuty = true,
        RateLimits = {
            Read = 180,
            Write = 20,
        },
        Publishers = {
            police = {
                MinimumGrade = 2,
                MaximumSeverity = "extreme",
                CityWide = true,
                Categories = { "public_safety", "police", "infrastructure", "evacuation" },
            },
            fire = {
                MinimumGrade = 2,
                MaximumSeverity = "extreme",
                CityWide = true,
                Categories = { "public_safety", "fire", "infrastructure", "evacuation" },
            },
            ambulance = {
                MinimumGrade = 2,
                MaximumSeverity = "danger",
                CityWide = false,
                Categories = { "public_safety", "medical", "evacuation" },
            },
            government = {
                MinimumGrade = 2,
                MaximumSeverity = "extreme",
                CityWide = true,
                Categories = { "public_safety", "police", "fire", "medical", "infrastructure", "evacuation" },
            },
        },
    }

    -- -------------------------------------------------------------------------
    -- Database migrations
    -- -------------------------------------------------------------------------

    Config.Migrations = Config.Migrations or {}

    -- Manual, non-destructive import from LB Phone's `phone_*` database tables.
    -- The migration never starts during a resource/server restart. Completed
    -- domains are recorded in `sky_phone_migrations` for idempotent retries.
    -- Import: skyphone:migrate lb-phone
    -- Console preview: skyphone:migrate lb-phone dry
    -- Idempotent retry: skyphone:migrate lb-phone force
    -- Remove imported Sky data: skyphone:migrate lb-phone remove
    Config.Migrations.LbPhone = {
        SourcePrefix = "phone_",

        -- auto: direct framework identifier first, then an unambiguous license match.
        -- identifier: only match phone_phones.owner_id to the framework identifier.
        -- license: only resolve phone_phones.owner_id through the framework license.
        IdentifierMode = "auto",

        Domains = {
            devices = true,
            settings = true,
            alarms = true,
            contacts = true,
            blocked = true,
            calls = true,
            messages = true,
            photos = true,
            notes = true,
            wallet = true,
            voiceMemos = true,
            picstagram = true,
            mail = true,
            mapMarkers = true,
            darkChat = true,
            flipTok = true,
            feather = true,
            flare = true,
        },
    }
end
