Skip to content

For Mod Developers

XelaNull edited this page Feb 3, 2026 · 2 revisions

For Mod Developers: FS25_UsedPlus Integration Guide

Version: 2.7.1+ | API Version: 1.0.0

Welcome to the UsedPlus developer integration guide. This document shows you how to integrate your mod with UsedPlus's finance, credit, and vehicle systems.


Table of Contents

  1. Introduction
  2. Detection
  3. Public API
  4. Events System
  5. Vehicle DNA Access
  6. Credit Score Integration
  7. Malfunction System
  8. Code Examples
  9. FS25 Coding Patterns
  10. Testing & Console Commands

Introduction

Why Integrate with UsedPlus?

UsedPlus provides a comprehensive financial and vehicle management system for FS25. By integrating with it, your mod can:

  • Query credit scores to adjust loan terms, interest rates, or unlock features
  • Access vehicle DNA to determine if a vehicle is a workhorse or lemon
  • Register external loans with the Credit Bureau to affect player credit scores
  • Subscribe to financial events (payments, credit changes, malfunctions)
  • Read vehicle condition (fluids, reliability, active malfunctions)
  • Leverage proven FS25 patterns from a production codebase (122 Lua files, 33 dialogs, 135k+ lines)

What's Possible?

Finance Mod Integration:

  • Register your mod's loans with UsedPlus Credit Bureau
  • Player's payment behavior on YOUR loans affects their credit score
  • Display your loans in the unified Finance Manager
  • Query credit scores to adjust your interest rates dynamically

Vehicle Mod Integration:

  • Check if a custom vehicle is a "Legendary Workhorse" (DNA >= 0.90)
  • Trigger malfunctions based on your mod's logic
  • Read fluid levels, reliability states, and active breakdowns
  • Subscribe to repair/maintenance events

Production/Economy Mod Integration:

  • Query total debt and monthly obligations for financial pressure
  • Check farm collateral for production chain financing
  • React to credit score changes to adjust production costs

Detection

Check if UsedPlus is Loaded

Always check if the UsedPlus API is available before using it:

-- Basic check
if UsedPlusAPI then
    -- API is available
    local score = UsedPlusAPI.getCreditScore(farmId)
end

-- Full check (recommended)
if UsedPlusAPI and UsedPlusAPI.isReady() then
    -- API is fully initialized and ready
    local score = UsedPlusAPI.getCreditScore(farmId)
else
    -- Fallback behavior
    print("[MyMod] UsedPlus API not available - using defaults")
end

Version Checking

if UsedPlusAPI then
    local apiVersion = UsedPlusAPI.getVersion()      -- "1.0.0"
    local modVersion = UsedPlusAPI.getModVersion()   -- "2.7.1"

    print(string.format("[MyMod] UsedPlus detected - API v%s, Mod v%s",
                        apiVersion, modVersion))
end

Feature Detection

if UsedPlusAPI then
    local features = UsedPlusAPI.getFeatureAvailability()

    if features.financeEnabled then
        -- Finance system is active
    end

    if features.maintenanceEnabled then
        -- Maintenance system is active
    end
end

Compatible Mods Detection

if UsedPlusAPI then
    local mods = UsedPlusAPI.getCompatibleMods()

    if mods.rvbInstalled then
        -- Real Vehicle Breakdowns is installed
        -- UsedPlus is deeply integrated with it
    end

    if mods.uytInstalled then
        -- Use Your Tires is installed
        -- Quality tiers affect tire wear
    end

    if mods.enhancedLoanSystemInstalled then
        -- ELS is installed
        -- UsedPlus loan system is disabled (defers to ELS)
    end
end

Public API

The UsedPlus API is exposed via the global UsedPlusAPI table. All functions are safe to call (return nil/false if data unavailable).

Version & Availability

UsedPlusAPI.getVersion()

Returns the API version string (e.g., "1.0.0").

UsedPlusAPI.getModVersion()

Returns the UsedPlus mod version (e.g., "2.7.1").

UsedPlusAPI.isReady()

Returns true if UsedPlus is fully initialized and ready to use.

Example:

if UsedPlusAPI and UsedPlusAPI.isReady() then
    -- Safe to use all API functions
end

Credit System API

UsedPlusAPI.getCreditScore(farmId)

Get credit score for a farm (300-850 scale).

Parameters:

  • farmId (number) - Farm ID

Returns:

  • Credit score (number, 300-850), or nil if unavailable

Example:

local farmId = g_currentMission:getFarmId()
local score = UsedPlusAPI.getCreditScore(farmId)

if score then
    if score >= 720 then
        print("Excellent credit - offering best rates!")
    elseif score < 580 then
        print("Poor credit - higher interest rates apply")
    end
end

UsedPlusAPI.getCreditRating(farmId)

Get credit rating tier for a farm.

Parameters:

  • farmId (number) - Farm ID

Returns:

  • rating (string) - "Excellent", "Good", "Fair", "Poor", or "Very Poor"
  • level (number) - 1-5 (1 = best)

Example:

local rating, level = UsedPlusAPI.getCreditRating(farmId)

if rating == "Excellent" then
    -- Unlock premium features
elseif level >= 4 then
    -- Restrict features for poor credit
end

UsedPlusAPI.getInterestAdjustment(farmId)

Get interest rate adjustment based on credit score.

Parameters:

  • farmId (number) - Farm ID

Returns:

  • Percentage points to add to base rate (number, -1.5 to +3.0)

Example:

local baseRate = 0.08  -- 8% base interest
local adjustment = UsedPlusAPI.getInterestAdjustment(farmId)

if adjustment then
    local finalRate = baseRate + (adjustment / 100)
    print(string.format("Final interest rate: %.2f%%", finalRate * 100))
    -- Example output: "Final interest rate: 9.50%" (if adjustment = +1.5)
end

UsedPlusAPI.canFinance(farmId, financeType)

Check if a farm qualifies for specific financing.

Parameters:

  • farmId (number) - Farm ID
  • financeType (string) - One of:
    • "REPAIR" - Vehicle repairs
    • "VEHICLE_FINANCE" - Vehicle financing
    • "VEHICLE_LEASE" - Vehicle leasing
    • "LAND_FINANCE" - Land financing
    • "CASH_LOAN" - Cash loans

Returns:

  • canFinance (boolean) - Whether farm qualifies
  • minRequired (number) - Minimum credit score required
  • currentScore (number) - Farm's current credit score

Example:

local canFinance, minRequired, currentScore =
    UsedPlusAPI.canFinance(farmId, "VEHICLE_FINANCE")

if not canFinance then
    local shortfall = minRequired - currentScore
    print(string.format("Credit score too low. Need %d more points.", shortfall))
end

UsedPlusAPI.getPaymentStats(farmId)

Get payment history statistics for a farm.

Returns:

  • Table with:
    • totalPayments (number) - Total payments made
    • onTimePayments (number) - Payments made on time
    • latePayments (number) - Late payments
    • missedPayments (number) - Missed payments
    • currentStreak (number) - Current on-time payment streak
    • longestStreak (number) - Longest on-time payment streak

Example:

local stats = UsedPlusAPI.getPaymentStats(farmId)

if stats then
    local reliability = (stats.onTimePayments / stats.totalPayments) * 100
    print(string.format("Payment reliability: %.1f%%", reliability))

    if stats.currentStreak >= 12 then
        -- Reward long-term good behavior
        print("12+ month streak - bonus unlocked!")
    end
end

UsedPlusAPI.getOnTimePaymentRate(farmId)

Get on-time payment rate as a percentage.

Returns:

  • Percentage (number, 0-100)

Example:

local rate = UsedPlusAPI.getOnTimePaymentRate(farmId)

if rate >= 95 then
    -- Excellent payment history
    applyPremiumBenefits()
end

UsedPlusAPI.getCreditHistory(farmId, limit)

Get recent credit history events.

Parameters:

  • farmId (number) - Farm ID
  • limit (number, optional) - Max entries to return (default: all)

Returns:

  • Array of history entry tables (newest first)

Example:

local history = UsedPlusAPI.getCreditHistory(farmId, 10)

for _, entry in ipairs(history) do
    print(string.format("%s: %s (%+d points)",
                        entry.date, entry.description, entry.scoreChange))
end

Vehicle DNA API

UsedPlus assigns each vehicle a "DNA" value (0.0 to 1.0) representing its inherent quality:

  • 0.0-0.35: Lemon (breaks down more often)
  • 0.36-0.64: Average
  • 0.65-0.89: Workhorse (more reliable)
  • 0.90-1.0: Legendary Workhorse (immune to repair degradation)

UsedPlusAPI.getVehicleDNA(vehicle)

Get vehicle DNA value.

Parameters:

  • vehicle (table) - Vehicle object

Returns:

  • DNA value (number, 0.0-1.0), or nil if unavailable

Example:

local vehicle = g_currentMission.controlledVehicle

if vehicle then
    local dna = UsedPlusAPI.getVehicleDNA(vehicle)

    if dna then
        print(string.format("Vehicle DNA: %.2f", dna))

        if dna >= 0.90 then
            -- This vehicle is nearly immortal!
            applyWorkhorseBonus()
        end
    end
end

UsedPlusAPI.isWorkhorse(vehicle)

Check if vehicle is a workhorse (DNA >= 0.65).

Returns: boolean

UsedPlusAPI.isLegendaryWorkhorse(vehicle)

Check if vehicle is a legendary workhorse (DNA >= 0.90).

Legendary workhorses are immune to repair degradation in RVB integration.

Returns: boolean

UsedPlusAPI.isLemon(vehicle)

Check if vehicle is a lemon (DNA <= 0.35).

Returns: boolean

Example:

if UsedPlusAPI.isLegendaryWorkhorse(vehicle) then
    print("This machine is a legend - built to last!")
elseif UsedPlusAPI.isLemon(vehicle) then
    print("Warning: This vehicle has poor reliability")
end

UsedPlusAPI.getDNAClassification(vehicle)

Get DNA classification as human-readable string.

Returns:

  • "Legendary Workhorse", "Workhorse", "Average", or "Lemon", or nil

Example:

local classification = UsedPlusAPI.getDNAClassification(vehicle)

if classification then
    print("Vehicle Quality: " .. classification)
end

UsedPlusAPI.getDNALifetimeMultiplier(vehicle)

Get DNA-based lifetime multiplier for RVB parts integration.

Returns:

  • Multiplier (number, 0.6 to 1.4)

Example:

local multiplier = UsedPlusAPI.getDNALifetimeMultiplier(vehicle)

if multiplier then
    local baseLifetime = 1000  -- hours
    local adjustedLifetime = baseLifetime * multiplier
    print(string.format("Part lifetime: %d hours", adjustedLifetime))
    -- Lemon (0.6): 600 hours
    -- Average (1.0): 1000 hours
    -- Workhorse (1.4): 1400 hours
end

Maintenance State API

UsedPlusAPI.getFluidLevels(vehicle)

Get fluid levels for a vehicle.

Returns:

  • Table with:
    • oilLevel (number, 0.0-1.0)
    • hydraulicFluidLevel (number, 0.0-1.0)

Example:

local fluids = UsedPlusAPI.getFluidLevels(vehicle)

if fluids then
    if fluids.oilLevel < 0.2 then
        print("WARNING: Oil level critically low!")
    end

    if fluids.hydraulicFluidLevel < 0.3 then
        print("WARNING: Hydraulic fluid low!")
    end
end

UsedPlusAPI.getReliability(vehicle)

Get reliability values for a vehicle.

Returns:

  • Table with:
    • engine (number, 0.0-1.0)
    • electrical (number, 0.0-1.0)
    • hydraulic (number, 0.0-1.0)
    • overall (number, 0.0-1.0)

Example:

local reliability = UsedPlusAPI.getReliability(vehicle)

if reliability then
    if reliability.overall < 0.5 then
        print("Vehicle is in poor condition - repairs recommended")
    end

    -- Check specific systems
    if reliability.engine < 0.3 then
        print("Engine reliability critical!")
    end
end

UsedPlusAPI.getActiveMalfunctions(vehicle)

Get currently active malfunctions.

Returns:

  • Table with malfunction states:
    • runaway - { active = bool, startTime = number }
    • hydraulicSurge - { active = bool, endTime = number }
    • implementStuckDown - { active = bool, endTime = number }
    • implementStuckUp - { active = bool, endTime = number }
    • implementPull - { active = bool, direction = number }
    • implementDrag - { active = bool }
    • electricalCutout - { active = bool, endTime = number }
    • steeringPull - { active = bool, strength = number, direction = number }

Example:

local malfunctions = UsedPlusAPI.getActiveMalfunctions(vehicle)

if malfunctions then
    if malfunctions.runaway and malfunctions.runaway.active then
        print("EMERGENCY: Runaway engine!")
        -- Trigger emergency response in your mod
    end

    if malfunctions.hydraulicSurge and malfunctions.hydraulicSurge.active then
        print("Hydraulic surge in progress")
    end
end

UsedPlusAPI.hasActiveMalfunction(vehicle)

Check if vehicle has any active malfunction.

Returns: boolean

Example:

if UsedPlusAPI.hasActiveMalfunction(vehicle) then
    print("Vehicle is experiencing a malfunction!")
    -- Disable certain features in your mod
end

UsedPlusAPI.getProgressiveDegradation(vehicle)

Get progressive degradation information.

Returns:

  • Table with:
    • maxEngineReliability (number, 0.0-1.0) - Current reliability cap
    • maxElectricalReliability (number, 0.0-1.0)
    • maxHydraulicReliability (number, 0.0-1.0)
    • repairCount (number) - Number of repairs performed
    • breakdownCount (number) - Number of breakdowns
    • rvbTotalDegradation (number) - RVB degradation (if RVB installed)
    • rvbRepairCount (number)
    • rvbBreakdownCount (number)

Example:

local degradation = UsedPlusAPI.getProgressiveDegradation(vehicle)

if degradation then
    if degradation.repairCount > 10 then
        print("This vehicle has been repaired many times")
    end

    if degradation.maxEngineReliability < 0.8 then
        print("Engine can no longer reach full reliability")
    end
end

UsedPlusAPI.getTireInfo(vehicle)

Get tire information.

Returns:

  • Array of tire data objects:
    • index (number) - Tire index
    • condition (number, 0.0-1.0) - Tire condition
    • tier (number, 1-3) - 1=Retread, 2=Normal, 3=Quality
    • isFlat (boolean) - Whether tire is flat
    • uytWear (number, optional) - UYT wear level if UYT mod installed

Example:

local tires = UsedPlusAPI.getTireInfo(vehicle)

if tires then
    for _, tire in ipairs(tires) do
        if tire.isFlat then
            print(string.format("Tire %d is FLAT!", tire.index))
        elseif tire.condition < 0.2 then
            print(string.format("Tire %d is badly worn", tire.index))
        end
    end
end

Finance Deals API

UsedPlusAPI.getActiveDeals(farmId)

Get all active finance deals for a farm.

Returns:

  • Array of deal objects with:
    • id (string) - Deal ID
    • dealType (string) - Deal type
    • itemName (string) - Description
    • originalAmount (number) - Starting balance
    • currentBalance (number) - Current balance
    • monthlyPayment (number) - Monthly payment amount
    • interestRate (number) - Interest rate (decimal)
    • termMonths (number) - Total term in months
    • monthsPaid (number) - Months paid so far
    • remainingMonths (number) - Remaining months
    • missedPayments (number) - Missed payment count

Example:

local deals = UsedPlusAPI.getActiveDeals(farmId)

if deals then
    for _, deal in ipairs(deals) do
        print(string.format("%s: $%d remaining",
                            deal.itemName, deal.currentBalance))
    end
end

UsedPlusAPI.getTotalDebt(farmId)

Get total debt for a farm (all active UsedPlus deals + vanilla loan).

Returns: Total debt amount (number)

Example:

local totalDebt = UsedPlusAPI.getTotalDebt(farmId)
local totalAssets = UsedPlusAPI.getTotalAssets(farmId)

if totalDebt and totalAssets then
    local debtToAssetRatio = totalDebt / totalAssets

    if debtToAssetRatio > 0.8 then
        print("WARNING: Debt-to-asset ratio is dangerously high!")
    end
end

UsedPlusAPI.getMonthlyObligations(farmId)

Get monthly payment obligations.

Returns:

  • Table with:
    • usedPlusTotal (number) - Total from UsedPlus deals
    • externalTotal (number) - Total from external mods (ELS, HP, Employment)
    • grandTotal (number) - Total monthly obligations

Example:

local obligations = UsedPlusAPI.getMonthlyObligations(farmId)

if obligations then
    print(string.format("Monthly obligations: $%d", obligations.grandTotal))

    -- Check cash flow
    local monthlyIncome = calculateMonthlyIncome()
    if obligations.grandTotal > monthlyIncome * 0.5 then
        print("WARNING: Over 50% of income goes to debt payments!")
    end
end

UsedPlusAPI.getTotalAssets(farmId)

Get total assets for a farm.

Returns: Total asset value (number)


UsedPlusAPI.getResaleValue(vehicle)

Get condition-adjusted resale value for a vehicle.

Factors in reliability, DNA, damage, and wear.

Returns: Adjusted sale value (number)

Example:

local resaleValue = UsedPlusAPI.getResaleValue(vehicle)
local storePrice = StoreItemUtil.getConfigPrice(vehicle.configFileName)

if resaleValue and storePrice then
    local depreciation = 1 - (resaleValue / storePrice)
    print(string.format("Depreciation: %.1f%%", depreciation * 100))
end

Credit Bureau API

This is the most powerful integration point! External finance mods can register their deals with UsedPlus to affect credit scores.

When players make on-time payments on YOUR mod's loans, their UsedPlus credit score improves. When they miss payments, it drops. This creates a unified credit system across multiple mods.

UsedPlusAPI.registerExternalDeal(modName, dealId, farmId, dealData)

Register an external deal with the UsedPlus credit bureau.

Parameters:

  • modName (string) - Your mod's unique identifier (e.g., "MyFinanceMod")
  • dealId (string) - Your internal deal ID
  • farmId (number) - Farm ID the deal belongs to
  • dealData (table) - Deal information:
    • dealType (string, required) - "loan", "lease", "finance", or "credit"
    • itemName (string, required) - Description (e.g., "Equipment Loan")
    • originalAmount (number, required) - Starting balance
    • currentBalance (number, optional) - Current balance
    • monthlyPayment (number, required) - Expected monthly payment
    • interestRate (number, optional) - Interest rate as decimal
    • termMonths (number, optional) - Total term in months

Returns:

  • externalDealId (string) - ID for future calls, or nil on failure

Example:

-- When your mod creates a loan
local externalDealId = UsedPlusAPI.registerExternalDeal(
    "MyFinanceMod",           -- Your mod name
    "loan_12345",             -- Your internal ID
    farmId,                   -- Farm ID
    {
        dealType = "loan",
        itemName = "Equipment Financing",
        originalAmount = 50000,
        monthlyPayment = 2500,
        interestRate = 0.08,  -- 8%
        termMonths = 24,
    }
)

if externalDealId then
    -- Store this ID - you'll need it for reporting payments
    myLoan.usedPlusId = externalDealId
end

UsedPlusAPI.reportExternalPayment(externalDealId, amount)

Report an on-time payment. This improves the player's credit score.

Parameters:

  • externalDealId (string) - ID from registerExternalDeal
  • amount (number) - Payment amount

Returns: boolean success

Example:

-- When player makes their monthly payment
local success = UsedPlusAPI.reportExternalPayment(myLoan.usedPlusId, 2500)

if success then
    print("Payment reported to UsedPlus - credit score improved!")
end

UsedPlusAPI.reportExternalDefault(externalDealId, isLate)

Report a missed or late payment. This hurts the player's credit score.

Parameters:

  • externalDealId (string) - ID from registerExternalDeal
  • isLate (boolean) - true if paid late, false if missed entirely

Returns: boolean success

Example:

-- When player misses a payment
UsedPlusAPI.reportExternalDefault(myLoan.usedPlusId, false)  -- Missed
print("Missed payment reported - credit score decreased")

-- When player pays late
UsedPlusAPI.reportExternalDefault(myLoan.usedPlusId, true)   -- Late
print("Late payment reported - minor credit score decrease")

UsedPlusAPI.updateExternalDealBalance(externalDealId, newBalance)

Update the current balance (e.g., after interest accrual).

Returns: boolean success

Example:

-- Monthly interest accrual
local interest = myLoan.balance * (myLoan.interestRate / 12)
myLoan.balance = myLoan.balance + interest

UsedPlusAPI.updateExternalDealBalance(myLoan.usedPlusId, myLoan.balance)

UsedPlusAPI.closeExternalDeal(externalDealId, reason)

Close an external deal.

Parameters:

  • externalDealId (string) - ID from registerExternalDeal
  • reason (string) - "paid_off", "cancelled", "defaulted", or "transferred"

Returns: boolean success

Example:

-- When loan is paid off
UsedPlusAPI.closeExternalDeal(myLoan.usedPlusId, "paid_off")

-- When loan defaults
UsedPlusAPI.closeExternalDeal(myLoan.usedPlusId, "defaulted")

UsedPlusAPI.getExternalDeals(farmId)

Get all active external deals for a farm.

Returns: Array of deal objects


UsedPlusAPI.getExternalDebt(farmId)

Get total debt from external deals.

Returns: Total external debt (number)


UsedPlusAPI.getExternalMonthlyPayments(farmId)

Get total monthly obligations from external deals.

Returns: Total external monthly payments (number)


Events System

UsedPlus fires events that your mod can subscribe to for real-time notifications.

Available Events

Event Parameters When Fired
onCreditScoreChanged farmId, newScore, oldScore, newRating, oldRating Credit score changes
onPaymentMade farmId, dealId, amount, dealType Payment successfully made
onPaymentMissed farmId, dealId, dealType Payment was missed
onDealCreated farmId, deal New finance deal created
onDealCompleted farmId, deal Finance deal paid off/closed
onMalfunctionTriggered vehicle, type, message Malfunction started
onMalfunctionEnded vehicle, type Malfunction ended
onVehicleRepaired vehicle Vehicle was repaired

Subscribing to Events

-- Subscribe to credit score changes
UsedPlusAPI.subscribe("onCreditScoreChanged", function(farmId, newScore, oldScore, newRating, oldRating)
    print(string.format("Farm %d: Credit changed from %d (%s) to %d (%s)",
                        farmId, oldScore, oldRating, newScore, newRating))

    if newScore < 600 then
        -- Show warning to player
        MyMod.showCreditWarning()
    end
end)

-- Subscribe to malfunction events
UsedPlusAPI.subscribe("onMalfunctionTriggered", function(vehicle, malfType, message)
    if malfType == "runaway" then
        print("EMERGENCY: Runaway engine on " .. vehicle:getName())
        -- Trigger emergency response in your mod
    end
end)

-- Subscribe to payment events
UsedPlusAPI.subscribe("onPaymentMade", function(farmId, dealId, amount, dealType)
    print(string.format("Payment made: $%d on %s", amount, dealType))
    -- Track payment history in your mod
end)

Subscribing with Context (Self)

-- In your mod class
function MyMod:init()
    UsedPlusAPI.subscribe("onCreditScoreChanged", self.onCreditChanged, self)
end

function MyMod:onCreditChanged(farmId, newScore, oldScore)
    -- 'self' is your mod instance
    self:handleCreditChange(farmId, newScore)
end

Unsubscribing

local myCallback = function(farmId, newScore, oldScore)
    print("Credit changed")
end

UsedPlusAPI.subscribe("onCreditScoreChanged", myCallback)

-- Later, when your mod unloads:
UsedPlusAPI.unsubscribe("onCreditScoreChanged", myCallback)

Vehicle DNA Access

Reading DNA Data

local vehicle = g_currentMission.controlledVehicle

if vehicle and UsedPlusAPI then
    local dna = UsedPlusAPI.getVehicleDNA(vehicle)

    if dna then
        -- DNA ranges from 0.0 (lemon) to 1.0 (workhorse)
        print(string.format("Vehicle DNA: %.2f", dna))

        -- Classification helpers
        if UsedPlusAPI.isLegendaryWorkhorse(vehicle) then
            -- DNA >= 0.90 - Nearly immortal
            grantWorkhorseBonus(vehicle)
        elseif UsedPlusAPI.isWorkhorse(vehicle) then
            -- DNA >= 0.65 - More reliable than average
            reduceMaintenanceCosts(vehicle)
        elseif UsedPlusAPI.isLemon(vehicle) then
            -- DNA <= 0.35 - Breaks down more often
            increaseMaintenanceCosts(vehicle)
        end
    end
end

Using DNA in Your Mod Logic

-- Example: Adjust part lifetime based on DNA
function MyMod:calculatePartLifetime(vehicle, baseLi fetime)
    local multiplier = 1.0

    if UsedPlusAPI then
        multiplier = UsedPlusAPI.getDNALifetimeMultiplier(vehicle) or 1.0
        -- Returns 0.6 for lemons, 1.4 for workhorses
    end

    return baseLifetime * multiplier
end

-- Example: Adjust fuel efficiency based on DNA
function MyMod:getFuelEfficiency(vehicle)
    local baseEfficiency = 1.0

    if UsedPlusAPI then
        local dna = UsedPlusAPI.getVehicleDNA(vehicle)
        if dna then
            -- Workhorses use fuel more efficiently
            -- Lemons waste fuel
            baseEfficiency = 0.8 + (dna * 0.4)  -- 0.8 to 1.2 range
        end
    end

    return baseEfficiency
end

Credit Score Integration

Checking Credit for Feature Unlocks

function MyMod:canAccessPremiumFeatures(farmId)
    if not UsedPlusAPI then
        return true  -- Fallback: allow access
    end

    local score = UsedPlusAPI.getCreditScore(farmId)

    if not score then
        return true  -- No credit data, allow access
    end

    -- Require 700+ credit for premium features
    return score >= 700
end

Dynamic Interest Rates

function MyMod:calculateInterestRate(farmId)
    local baseRate = 0.08  -- 8% base rate

    if UsedPlusAPI then
        local adjustment = UsedPlusAPI.getInterestAdjustment(farmId)
        if adjustment then
            baseRate = baseRate + (adjustment / 100)
        end
    end

    return baseRate
end

Credit-Based Loan Limits

function MyMod:getMaxLoanAmount(farmId)
    local baseLimit = 100000

    if UsedPlusAPI then
        local score = UsedPlusAPI.getCreditScore(farmId)

        if score then
            if score >= 750 then
                return baseLimit * 2.0  -- Excellent credit: double limit
            elseif score >= 650 then
                return baseLimit * 1.5  -- Good credit: 1.5x limit
            elseif score >= 550 then
                return baseLimit        -- Fair credit: base limit
            else
                return baseLimit * 0.5  -- Poor credit: half limit
            end
        end
    end

    return baseLimit
end

Malfunction System

Triggering Malfunctions

UsedPlus does not currently expose a public API to trigger malfunctions externally. However, you can subscribe to malfunction events to react to them.

Detecting Active Malfunctions

function MyMod:checkVehicleStatus(vehicle)
    if not UsedPlusAPI then return end

    if UsedPlusAPI.hasActiveMalfunction(vehicle) then
        local malfunctions = UsedPlusAPI.getActiveMalfunctions(vehicle)

        if malfunctions.runaway and malfunctions.runaway.active then
            -- Runaway engine - disable certain features
            self:disableAutopilot(vehicle)
        end

        if malfunctions.hydraulicSurge and malfunctions.hydraulicSurge.active then
            -- Hydraulic surge - warn player
            self:showHydraulicWarning(vehicle)
        end
    end
end

Reacting to Malfunction Events

UsedPlusAPI.subscribe("onMalfunctionTriggered", function(vehicle, malfType, message)
    if malfType == "runaway" then
        -- Emergency response
        MyMod.handleRunawayEngine(vehicle)
    elseif malfType == "hydraulicSurge" then
        -- Disable hydraulic-dependent features
        MyMod.disableHydraulics(vehicle)
    end
end)

UsedPlusAPI.subscribe("onMalfunctionEnded", function(vehicle, malfType)
    if malfType == "runaway" then
        MyMod.restoreNormalOperation(vehicle)
    end
end)

Code Examples

Complete Finance Mod Integration

-- MyFinanceMod - Complete integration with UsedPlus Credit Bureau

MyFinanceMod = {}
MyFinanceMod.loans = {}

function MyFinanceMod:createLoan(farmId, amount, termMonths)
    local dealId = self:generateDealId()
    local monthlyPayment = self:calculatePayment(amount, termMonths)

    -- Create internal loan
    local loan = {
        id = dealId,
        farmId = farmId,
        amount = amount,
        balance = amount,
        termMonths = termMonths,
        monthlyPayment = monthlyPayment,
        monthsPaid = 0,
    }

    -- Register with UsedPlus credit bureau
    if UsedPlusAPI and UsedPlusAPI.isReady() then
        -- Adjust interest based on credit score
        local baseRate = 0.08
        local adjustment = UsedPlusAPI.getInterestAdjustment(farmId)
        loan.interestRate = baseRate + ((adjustment or 0) / 100)

        -- Register the deal
        loan.usedPlusId = UsedPlusAPI.registerExternalDeal(
            "MyFinanceMod",
            dealId,
            farmId,
            {
                dealType = "loan",
                itemName = "MyFinanceMod Equipment Loan",
                originalAmount = amount,
                monthlyPayment = monthlyPayment,
                interestRate = loan.interestRate,
                termMonths = termMonths,
            }
        )

        if loan.usedPlusId then
            print("[MyFinanceMod] Loan registered with UsedPlus Credit Bureau")
        end
    end

    self.loans[dealId] = loan
    return loan
end

function MyFinanceMod:processPayment(loan, amount)
    loan.balance = loan.balance - amount
    loan.monthsPaid = loan.monthsPaid + 1

    -- Report to UsedPlus
    if loan.usedPlusId and UsedPlusAPI then
        UsedPlusAPI.reportExternalPayment(loan.usedPlusId, amount)
        print("[MyFinanceMod] Payment reported - credit score improved!")
    end

    if loan.balance <= 0 then
        self:closeLoan(loan, "paid_off")
    end
end

function MyFinanceMod:missedPayment(loan)
    -- Report default to UsedPlus (hurts credit!)
    if loan.usedPlusId and UsedPlusAPI then
        UsedPlusAPI.reportExternalDefault(loan.usedPlusId, false)
        print("[MyFinanceMod] Missed payment reported - credit score decreased!")
    end
end

function MyFinanceMod:closeLoan(loan, reason)
    -- Close with UsedPlus
    if loan.usedPlusId and UsedPlusAPI then
        UsedPlusAPI.closeExternalDeal(loan.usedPlusId, reason)
    end

    self.loans[loan.id] = nil
end

function MyFinanceMod:generateDealId()
    return "LOAN_" .. g_currentMission.environment.currentDay .. "_" .. math.random(10000, 99999)
end

function MyFinanceMod:calculatePayment(amount, termMonths)
    local rate = 0.08 / 12  -- Monthly rate
    return amount * (rate * (1 + rate)^termMonths) / ((1 + rate)^termMonths - 1)
end

Vehicle Mod Integration

-- MyVehicleMod - Integration with UsedPlus DNA system

MyVehicleMod = {}

function MyVehicleMod:onVehicleEnter(vehicle)
    if not UsedPlusAPI then return end

    -- Check DNA classification
    local classification = UsedPlusAPI.getDNAClassification(vehicle)

    if classification then
        self:showDNANotification(vehicle, classification)
    end

    -- Adjust features based on DNA
    if UsedPlusAPI.isLegendaryWorkhorse(vehicle) then
        -- Grant bonuses for legendary workhorses
        self:applyWorkhorseBonus(vehicle)
    elseif UsedPlusAPI.isLemon(vehicle) then
        -- Apply penalties for lemons
        self:applyLemonPenalty(vehicle)
    end
end

function MyVehicleMod:applyWorkhorseBonus(vehicle)
    -- Increase fuel efficiency
    if vehicle.spec_motorized then
        vehicle.spec_motorized.fuelUsageMultiplier = 0.9  -- 10% better
    end

    print("[MyVehicleMod] Workhorse bonus applied - improved fuel efficiency")
end

function MyVehicleMod:applyLemonPenalty(vehicle)
    -- Decrease fuel efficiency
    if vehicle.spec_motorized then
        vehicle.spec_motorized.fuelUsageMultiplier = 1.1  -- 10% worse
    end

    print("[MyVehicleMod] Lemon penalty applied - reduced fuel efficiency")
end

function MyVehicleMod:showDNANotification(vehicle, classification)
    local message = string.format("Vehicle Quality: %s", classification)
    g_currentMission:showBlinkingWarning(message, 3000)
end

-- Subscribe to malfunction events
if UsedPlusAPI then
    UsedPlusAPI.subscribe("onMalfunctionTriggered", function(vehicle, malfType, message)
        if malfType == "runaway" then
            MyVehicleMod:handleRunaway(vehicle)
        end
    end)
end

function MyVehicleMod:handleRunaway(vehicle)
    -- Disable autopilot during runaway
    if vehicle.spec_aiVehicle then
        vehicle:stopAIVehicle()
    end

    g_currentMission:showBlinkingWarning("RUNAWAY ENGINE! STOP VEHICLE!", 5000)
end

Production Chain Integration

-- MyProductionMod - Integration with UsedPlus financial data

MyProductionMod = {}

function MyProductionMod:canAffordProduction(farmId, productionCost)
    if not UsedPlusAPI then
        -- Fallback: just check cash
        local farm = g_farmManager:getFarmById(farmId)
        return farm.money >= productionCost
    end

    -- Check cash flow capacity
    local obligations = UsedPlusAPI.getMonthlyObligations(farmId)
    local farm = g_farmManager:getFarmById(farmId)

    if obligations and farm then
        local availableCash = farm.money - productionCost
        local monthlyBuffer = obligations.grandTotal * 2  -- 2 months buffer

        if availableCash < monthlyBuffer then
            print("[MyProductionMod] Warning: Low cash reserves after production cost")
            return false
        end
    end

    return true
end

function MyProductionMod:getCreditBasedPricing(farmId, basePrice)
    if not UsedPlusAPI then
        return basePrice
    end

    local score = UsedPlusAPI.getCreditScore(farmId)

    if score then
        if score >= 750 then
            return basePrice * 0.95  -- 5% discount for excellent credit
        elseif score < 550 then
            return basePrice * 1.1   -- 10% markup for poor credit
        end
    end

    return basePrice
end

function MyProductionMod:checkFinancialHealth(farmId)
    if not UsedPlusAPI then return true end

    local totalDebt = UsedPlusAPI.getTotalDebt(farmId)
    local totalAssets = UsedPlusAPI.getTotalAssets(farmId)

    if totalDebt and totalAssets and totalAssets > 0 then
        local debtRatio = totalDebt / totalAssets

        if debtRatio > 0.8 then
            print("[MyProductionMod] WARNING: High debt-to-asset ratio - consider reducing production")
            return false
        end
    end

    return true
end

FS25 Coding Patterns

UsedPlus is built using battle-tested patterns from 83 Lua files and 30+ custom dialogs. Below are the most important patterns for mod developers.

For complete documentation, see the FS25 AI Coding Reference.

Network Events (Multiplayer Sync)

All state-modifying actions must use network events for multiplayer compatibility.

Pattern:

-- MyActionEvent.lua
MyActionEvent = {}
local MyActionEvent_mt = Class(MyActionEvent, Event)

InitEventClass(MyActionEvent, "MyActionEvent")

function MyActionEvent.emptyNew()
    return Event.new(MyActionEvent_mt)
end

function MyActionEvent.new(farmId, data)
    local self = MyActionEvent.emptyNew()
    self.farmId = farmId
    self.data = data
    return self
end

-- Key pattern: Check g_server to execute locally or send to server
function MyActionEvent.sendToServer(farmId, data)
    if g_server ~= nil then
        -- Server or single-player: execute directly
        MyActionEvent.execute(farmId, data)
    else
        -- Client: send to server
        g_client:getServerConnection():sendEvent(MyActionEvent.new(farmId, data))
    end
end

function MyActionEvent:writeStream(streamId, connection)
    streamWriteInt32(streamId, self.farmId)
    streamWriteString(streamId, self.data)
end

function MyActionEvent:readStream(streamId, connection)
    self.farmId = streamReadInt32(streamId)
    self.data = streamReadString(streamId)
    self:run(connection)
end

function MyActionEvent.execute(farmId, data)
    -- Business logic here (server-side)
    if g_myManager then
        g_myManager:doSomething(farmId, data)
    end
end

function MyActionEvent:run(connection)
    if not connection:getIsServer() then return end
    MyActionEvent.execute(self.farmId, self.data)
end

Reference: patterns/events.md


Manager Singleton Pattern

Managers hold global state and process time-based updates.

Pattern:

-- MyManager.lua
MyManager = {}
local MyManager_mt = Class(MyManager)

g_myManager = nil  -- Global singleton

function MyManager.new()
    local self = setmetatable({}, MyManager_mt)

    self.items = {}
    self.isServer = false

    return self
end

function MyManager:loadMapFinished()
    self.isServer = g_currentMission:getIsServer()

    if self.isServer then
        -- Subscribe to hourly updates
        g_messageCenter:subscribe(MessageType.HOUR_CHANGED, self.onHourChanged, self)
    end
end

function MyManager:onHourChanged()
    if not self.isServer then return end

    -- Process all farms
    for farmId, farm in pairs(g_farmManager:getFarms()) do
        self:processItemsForFarm(farmId, farm)
    end
end

function MyManager:delete()
    if self.isServer then
        g_messageCenter:unsubscribe(MessageType.HOUR_CHANGED, self)
    end
end

-- In main.lua
function MyMod:loadMap(filename)
    g_myManager = MyManager.new()
end

function MyMod:loadMapFinished()
    if g_myManager then
        g_myManager:loadMapFinished()
    end
end

function MyMod:deleteMap()
    if g_myManager then
        g_myManager:delete()
        g_myManager = nil
    end
end

Reference: patterns/managers.md


GUI Dialogs (MessageDialog Pattern)

CRITICAL: Always use MessageDialog as base class, NOT DialogElement.

XML (gui/MyDialog.xml):

<?xml version="1.0" encoding="utf-8" standalone="no" ?>
<GUI onOpen="onOpen" onClose="onClose">
    <GuiElement profile="newLayer"/>
    <Bitmap profile="dialogFullscreenBg"/>

    <GuiElement profile="dialogBg" id="dialogElement" size="800px 600px">
        <ThreePartBitmap profile="fs25_dialogBgMiddle"/>
        <ThreePartBitmap profile="fs25_dialogBgTop"/>
        <ThreePartBitmap profile="fs25_dialogBgBottom"/>

        <GuiElement profile="fs25_dialogContentContainer">
            <Text profile="fs25_dialogTitle" text="My Dialog" position="0px -30px"/>
            <Text profile="fs25_dialogText" id="messageText" position="0px -80px" text="Content"/>
        </GuiElement>

        <BoxLayout profile="fs25_dialogButtonBox">
            <Button profile="buttonOK" text="OK" onClick="onClickOk"/>
            <Bitmap profile="fs25_dialogButtonBoxSeparator"/>
            <Button profile="buttonBack" text="Cancel" onClick="onClickCancel"/>
        </BoxLayout>
    </GuiElement>
</GUI>

Lua (src/gui/MyDialog.lua):

MyDialog = {}
local MyDialog_mt = Class(MyDialog, MessageDialog)  -- Use MessageDialog!

function MyDialog.new(target, custom_mt)
    local self = MessageDialog.new(target, custom_mt or MyDialog_mt)
    self.messageText = nil
    self.callbackFunc = nil
    return self
end

function MyDialog:onGuiSetupFinished()
    MyDialog:superClass().onGuiSetupFinished(self)
    self.messageText = self.target:getFirstDescendant("messageText")
end

function MyDialog:onOpen()
    MyDialog:superClass().onOpen(self)
end

function MyDialog:setData(message, callback)
    self.callbackFunc = callback
    if self.messageText then
        self.messageText:setText(message)
    end
end

function MyDialog:onClickOk()
    if self.callbackFunc then
        self.callbackFunc(true)
    end
    self:close()
end

function MyDialog:onClickCancel()
    if self.callbackFunc then
        self.callbackFunc(false)
    end
    self:close()
end

function MyDialog:close()
    g_gui:closeDialog(self)
end

-- Dynamic loading
MyDialog.INSTANCE = nil

function MyDialog.show(message, callback)
    if MyDialog.INSTANCE == nil then
        MyDialog.INSTANCE = MyDialog.new()
        g_gui:loadGui(g_currentModDirectory .. "gui/MyDialog.xml", "MyDialog", MyDialog.INSTANCE)
    end

    MyDialog.INSTANCE:setData(message, callback)
    g_gui:showDialog("MyDialog")
end

Reference: patterns/gui-dialogs.md


Save/Load Integration

Farm Extension Pattern:

-- Extend Farm class to add custom data
FarmExtension = {}

function FarmExtension.new(isServer, superFunc, isClient, spectator, customMt, ...)
    local farm = superFunc(isServer, isClient, spectator, customMt, ...)

    -- Add custom data
    farm.myModItems = {}

    return farm
end

Farm.new = Utils.overwrittenFunction(Farm.new, FarmExtension.new)

-- Save data
function FarmExtension:saveToXMLFile(xmlFile, key)
    if self.myModItems == nil then
        self.myModItems = {}
    end

    for i, item in ipairs(self.myModItems) do
        local itemKey = string.format("%s.myMod.items.item(%d)", key, i - 1)
        xmlFile:setString(itemKey .. "#id", item.id)
        xmlFile:setInt(itemKey .. "#value", item.value)
    end
end

Farm.saveToXMLFile = Utils.appendedFunction(Farm.saveToXMLFile, FarmExtension.saveToXMLFile)

-- Load data
function FarmExtension:loadFromXMLFile(superFunc, xmlFile, key)
    local result = superFunc(self, xmlFile, key)

    self.myModItems = {}
    xmlFile:iterate(key .. ".myMod.items.item", function(_, itemKey)
        local item = {
            id = xmlFile:getString(itemKey .. "#id", ""),
            value = xmlFile:getInt(itemKey .. "#value", 0),
        }
        table.insert(self.myModItems, item)
    end)

    return result
end

Farm.loadFromXMLFile = Utils.overwrittenFunction(Farm.loadFromXMLFile, FarmExtension.loadFromXMLFile)

Reference: patterns/save-load.md


Extending Game Classes

Utils Extension Functions:

  • Utils.appendedFunction(original, yourFunc) - Runs your code AFTER original
  • Utils.prependedFunction(original, yourFunc) - Runs your code BEFORE original
  • Utils.overwrittenFunction(original, yourFunc) - Replaces original but gives you access to call it

Example:

-- Add method to existing class
function BuyVehicleData:setMyModData(data)
    self.myModData = data
end

-- Hook into network stream
BuyVehicleData.writeStream = Utils.appendedFunction(
    BuyVehicleData.writeStream,
    function(self, streamId, connection)
        streamWriteBool(streamId, self.myModData ~= nil)
        if self.myModData then
            streamWriteString(streamId, self.myModData)
        end
    end
)

BuyVehicleData.readStream = Utils.appendedFunction(
    BuyVehicleData.readStream,
    function(self, streamId, connection)
        if streamReadBool(streamId) then
            self.myModData = streamReadString(streamId)
        end
    end
)

Reference: patterns/extensions.md


Common Pitfalls

1. Using os.time() or os.date() - NOT AVAILABLE

-- WRONG - Will crash!
local timestamp = os.time()

-- CORRECT
local timestamp = g_currentMission.time  -- Game time in ms
local currentDay = g_currentMission.environment.currentDay

2. Using goto statements - Lua 5.1 doesn't support them

-- WRONG - Lua 5.2+ only
for i, item in pairs(items) do
    if skip then goto continue end
    process(item)
    ::continue::
end

-- CORRECT
for i, item in pairs(items) do
    if not skip then
        process(item)
    end
end

3. Using Slider widgets - Unreliable

-- WRONG - Events don't fire
<Slider profile="fs25_slider" onChange="onChanged"/>

-- CORRECT - Use MultiTextOption
<MultiTextOption profile="fs25_multiTextOption" id="selector" onClick="onChange"/>

4. Using DialogElement base class - Broken

-- WRONG - Rendering issues
local MyDialog_mt = Class(MyDialog, DialogElement)

-- CORRECT
local MyDialog_mt = Class(MyDialog, MessageDialog)

5. Using g_gui:showYesNoDialog() - Doesn't exist

-- WRONG - Method doesn't exist!
g_gui:showYesNoDialog({ title = "Confirm", text = "Sure?", callback = fn })

-- CORRECT
YesNoDialog.show(fn, nil, "Sure?", "Confirm")

Reference: pitfalls/what-doesnt-work.md


Full Pattern Documentation

For comprehensive documentation on all patterns, see:


Testing & Console Commands

Enabling Developer Console

  1. Edit %USERPROFILE%\Documents\My Games\FarmingSimulator2025\game.xml
  2. Find <development> section
  3. Set <controls>true</controls>
  4. In-game, press ~ (tilde) to open console

UsedPlus Console Commands

Credit System:

-- Set credit score
UsedPlus.setCreditScore(farmId, score)
UsedPlus.setCreditScore(1, 850)  -- Max score

-- Add credit history
UsedPlus.addCreditHistory(farmId, description, scoreChange)
UsedPlus.addCreditHistory(1, "Test Payment", 10)

Finance:

-- Clear all deals
UsedPlus.clearAllDeals(farmId)

-- Force payment
UsedPlus.forcePayment(dealId)

-- Get deal info
UsedPlus.getDealInfo(dealId)

Vehicle DNA:

-- Set vehicle DNA
UsedPlus.setVehicleDNA(vehicle, dnaValue)
-- Example: Make controlled vehicle a legendary workhorse
UsedPlus.setVehicleDNA(g_currentMission.controlledVehicle, 0.95)

-- Check DNA
UsedPlus.getVehicleDNA(g_currentMission.controlledVehicle)

Maintenance:

-- Set fluid levels
UsedPlus.setOilLevel(vehicle, level)  -- 0.0-1.0
UsedPlus.setHydraulicFluidLevel(vehicle, level)

-- Set reliability
UsedPlus.setEngineReliability(vehicle, level)  -- 0.0-1.0

-- Trigger malfunction
UsedPlus.triggerMalfunction(vehicle, "runaway")
UsedPlus.triggerMalfunction(vehicle, "hydraulicSurge")

Testing Integration:

-- Test credit score changes
UsedPlus.setCreditScore(1, 450)  -- Poor credit
UsedPlus.setCreditScore(1, 850)  -- Excellent credit

-- Test vehicle DNA
local v = g_currentMission.controlledVehicle
UsedPlus.setVehicleDNA(v, 0.1)   -- Make it a lemon
UsedPlus.setVehicleDNA(v, 0.95)  -- Make it legendary

-- Test malfunctions
UsedPlus.triggerMalfunction(v, "runaway")
UsedPlus.endMalfunction(v, "runaway")

Best Practices

1. Always Check API Availability

if UsedPlusAPI and UsedPlusAPI.isReady() then
    -- Use API
else
    -- Fallback behavior
end

2. Handle nil Returns Gracefully

local score = UsedPlusAPI.getCreditScore(farmId)
if score then
    -- Use score
else
    -- Use default behavior
end

3. Clean Up Subscriptions

local myCallback = function(...) end
UsedPlusAPI.subscribe("onCreditScoreChanged", myCallback)

-- When your mod unloads:
UsedPlusAPI.unsubscribe("onCreditScoreChanged", myCallback)

4. Use Specific Event Subscriptions

Subscribe only to events you need - don't subscribe to everything.

5. Test in Multiplayer

Always test your integration in multiplayer mode to ensure events sync correctly.

6. Provide Fallback Behavior

Your mod should work (with reduced functionality) if UsedPlus is not installed.

7. Document Your Integration

Let your users know that your mod integrates with UsedPlus and what benefits they get.


Example Integration Checklist

When integrating your mod with UsedPlus:

  • Check UsedPlusAPI availability before using
  • Subscribe to relevant events
  • Register external deals if you're a finance mod
  • Report payments to credit bureau
  • Handle nil returns gracefully
  • Provide fallback behavior if UsedPlus not installed
  • Test in single-player
  • Test in multiplayer (dedicated server)
  • Clean up subscriptions on mod unload
  • Document integration in your mod description

Support & Resources


Compatibility Notes

See COMPATIBILITY.md for detailed information on how UsedPlus integrates with other popular mods:

  • Real Vehicle Breakdowns (RVB) - Deeply integrated (DNA affects part lifetimes)
  • Use Up Your Tyres (UYT) - Deeply integrated (Quality affects wear rate)
  • EnhancedLoanSystem (ELS) - Integrated (Displays ELS loans in Finance Manager)
  • HirePurchasing (HP) - Integrated (Displays HP leases)
  • Employment - Integrated (Worker wages in monthly obligations)

Last Updated: 2026-01-27 API Version: 1.0.0 Mod Version: 2.7.1+


Built with Claude AI as part of the FS25_UsedPlus project. 100% AI-written, production-tested in a 30,000+ line codebase.

Clone this wiki locally