-
Notifications
You must be signed in to change notification settings - Fork 0
For Mod Developers
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.
- Introduction
- Detection
- Public API
- Events System
- Vehicle DNA Access
- Credit Score Integration
- Malfunction System
- Code Examples
- FS25 Coding Patterns
- Testing & Console Commands
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)
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
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")
endif 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))
endif UsedPlusAPI then
local features = UsedPlusAPI.getFeatureAvailability()
if features.financeEnabled then
-- Finance system is active
end
if features.maintenanceEnabled then
-- Maintenance system is active
end
endif 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
endThe UsedPlus API is exposed via the global UsedPlusAPI table. All functions are safe to call (return nil/false if data unavailable).
Returns the API version string (e.g., "1.0.0").
Returns the UsedPlus mod version (e.g., "2.7.1").
Returns true if UsedPlus is fully initialized and ready to use.
Example:
if UsedPlusAPI and UsedPlusAPI.isReady() then
-- Safe to use all API functions
endGet credit score for a farm (300-850 scale).
Parameters:
-
farmId(number) - Farm ID
Returns:
- Credit score (number, 300-850), or
nilif 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
endGet 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
endGet 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)
endCheck 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))
endGet 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
endGet 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()
endGet 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))
endUsedPlus 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)
Get vehicle DNA value.
Parameters:
-
vehicle(table) - Vehicle object
Returns:
- DNA value (number, 0.0-1.0), or
nilif 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
endCheck if vehicle is a workhorse (DNA >= 0.65).
Returns: boolean
Check if vehicle is a legendary workhorse (DNA >= 0.90).
Legendary workhorses are immune to repair degradation in RVB integration.
Returns: boolean
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")
endGet DNA classification as human-readable string.
Returns:
-
"Legendary Workhorse","Workhorse","Average", or"Lemon", ornil
Example:
local classification = UsedPlusAPI.getDNAClassification(vehicle)
if classification then
print("Vehicle Quality: " .. classification)
endGet 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
endGet 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
endGet 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
endGet 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
endCheck 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
endGet 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
endGet 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
endGet 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
endGet 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
endGet 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
endGet total assets for a farm.
Returns: Total asset value (number)
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))
endThis 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.
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, ornilon 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
endReport an on-time payment. This improves the player's credit score.
Parameters:
-
externalDealId(string) - ID fromregisterExternalDeal -
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!")
endReport a missed or late payment. This hurts the player's credit score.
Parameters:
-
externalDealId(string) - ID fromregisterExternalDeal -
isLate(boolean) -trueif paid late,falseif 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")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)Close an external deal.
Parameters:
-
externalDealId(string) - ID fromregisterExternalDeal -
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")Get all active external deals for a farm.
Returns: Array of deal objects
Get total debt from external deals.
Returns: Total external debt (number)
Get total monthly obligations from external deals.
Returns: Total external monthly payments (number)
UsedPlus fires events that your mod can subscribe to for real-time notifications.
| 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 |
-- 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)-- 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)
endlocal myCallback = function(farmId, newScore, oldScore)
print("Credit changed")
end
UsedPlusAPI.subscribe("onCreditScoreChanged", myCallback)
-- Later, when your mod unloads:
UsedPlusAPI.unsubscribe("onCreditScoreChanged", myCallback)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-- 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
endfunction 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
endfunction 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
endfunction 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
endUsedPlus does not currently expose a public API to trigger malfunctions externally. However, you can subscribe to malfunction events to react to them.
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
endUsedPlusAPI.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)-- 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-- 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-- 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
endUsedPlus 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.
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)
endReference: patterns/events.md
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
endReference: patterns/managers.md
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")
endReference: patterns/gui-dialogs.md
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
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
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.currentDay2. 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
end3. 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
For comprehensive documentation on all patterns, see:
- FS25 AI Coding Reference - Complete pattern library
- GUI Dialogs - Dialog creation
- Events - Multiplayer sync
- Managers - Singleton managers
- Save/Load - Persistence
- Extensions - Hooking game classes
- What Doesn't Work - Common mistakes
- Edit
%USERPROFILE%\Documents\My Games\FarmingSimulator2025\game.xml - Find
<development>section - Set
<controls>true</controls> - In-game, press
~(tilde) to open console
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")if UsedPlusAPI and UsedPlusAPI.isReady() then
-- Use API
else
-- Fallback behavior
endlocal score = UsedPlusAPI.getCreditScore(farmId)
if score then
-- Use score
else
-- Use default behavior
endlocal myCallback = function(...) end
UsedPlusAPI.subscribe("onCreditScoreChanged", myCallback)
-- When your mod unloads:
UsedPlusAPI.unsubscribe("onCreditScoreChanged", myCallback)Subscribe only to events you need - don't subscribe to everything.
Always test your integration in multiplayer mode to ensure events sync correctly.
Your mod should work (with reduced functionality) if UsedPlus is not installed.
Let your users know that your mod integrates with UsedPlus and what benefits they get.
When integrating your mod with UsedPlus:
- Check
UsedPlusAPIavailability before using - Subscribe to relevant events
- Register external deals if you're a finance mod
- Report payments to credit bureau
- Handle
nilreturns 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
- UsedPlus GitHub: FS25_UsedPlus Repository
- FS25 AI Coding Reference: Pattern Library
- FS25 Community LUADOC: API Documentation
- FS25 Lua Source: Raw Source Archive
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.