Version: 1.0.4.1 Last Updated: 2026-02-14
- Getting Started
- Architecture Overview
- Adding New Crops
- Adding New Fertilizers
- Adding New Settings
- Network Synchronization
- Hook System
- HUD System
- Testing Your Changes
- Common Gotchas
- Build & Release
-
Clone Repository
git clone https://github.com/TheCodingDad-TisonK/FS25_SoilFertilizer.git cd FS25_SoilFertilizer -
Install to FS25
- Copy entire folder to
%USERPROFILE%\Documents\My Games\FarmingSimulator2025\mods\ - Or create symbolic link for live development
- Copy entire folder to
-
Enable Developer Console
- Edit
game.xmlin FS25 root - Set
<development>to<controls>true</controls> - Press
~in-game to open console
- Edit
-
Enable Logging
- Use
SoilDebugconsole command - Check
log.txtin FS25 documents folder
- Use
FS25_SoilFertilizer/
├── modDesc.xml # Mod manifest & translations
├── icon.dds # Mod icon
├── CLAUDE.md # Project architecture guide
├── DEVELOPMENT.md # This file
├── TESTING.md # Testing procedures
├── src/
│ ├── main.lua # Entry point & lifecycle hooks
│ ├── SoilFertilityManager.lua # Central coordinator
│ ├── SoilFertilitySystem.lua # Core soil simulation logic
│ ├── config/
│ │ ├── Constants.lua # All tunable values
│ │ └── SettingsSchema.lua # Settings definitions
│ ├── settings/
│ │ ├── Settings.lua # Settings domain object
│ │ ├── SettingsManager.lua # XML save/load
│ │ ├── SoilSettingsUI.lua # In-game UI generation
│ │ └── SoilSettingsGUI.lua # Console commands
│ ├── hooks/
│ │ └── HookManager.lua # Game engine hooks
│ ├── network/
│ │ └── NetworkEvents.lua # Multiplayer sync
│ ├── ui/
│ │ └── SoilHUD.lua # Always-on HUD overlay
│ └── utils/
│ ├── Logger.lua # Centralized logging
│ ├── AsyncRetryHandler.lua # Retry pattern utility
│ └── UIHelper.lua # UI element creation
main.lua loads modules in strict dependency order (see CLAUDE.md for details):
- Utilities & Config: Logger, Constants, SettingsSchema
- Core Systems: HookManager, SoilFertilitySystem, SoilFertilityManager
- Settings: SettingsManager, Settings, SoilSettingsGUI
- UI: UIHelper, SoilSettingsUI, SoilHUD
- Network: NetworkEvents
Important: Respect this order when adding new modules.
SoilFertilityManager (exposed as g_SoilFertilityManager) owns all subsystems:
g_SoilFertilityManager
├── settings : Settings instance
├── settingsManager : SettingsManager instance
├── soilSystem : SoilFertilitySystem instance
├── soilHUD : SoilHUD instance
└── Network events registered globallyHarvest Event:
FruitUtil.fruitPickupEventfires (FS25)HookManagerintercepts via hook- Calls
SoilFertilitySystem:onHarvest(fieldId, fruitType, liters) - System calculates nutrient depletion
- Updates
fieldData[fieldId]internal state - If multiplayer server: broadcasts
SoilFieldUpdateEventto clients
Crops have different nutrient extraction rates. To add a new crop:
Edit src/config/Constants.lua:
SoilConstants.CROP_EXTRACTION = {
-- Existing crops...
-- Add your new crop here
["yourcrop"] = { -- Must match FS25 fruit type name (lowercase)
N = 15, -- Nitrogen extraction per 1000L harvested
P = 8, -- Phosphorus extraction
K = 10, -- Potassium extraction
},
}Calibration Guidelines:
- High N crops: Wheat, Barley, Corn (leafy growth) - N: 15-20
- High P crops: Corn, Soybeans (energy/seeds) - P: 8-12
- High K crops: Potatoes, Sugar Beets (roots/tubers) - K: 12-18
- Nitrogen-fixing: Soybeans, Peas (legumes) - N: 5-8 (they fix their own)
- Plant your crop in FS25
- Note field nutrients before harvest:
SoilFieldInfo <fieldId> - Harvest the crop
- Check nutrients after:
SoilFieldInfo <fieldId> - Verify depletion matches your rates × difficulty multiplier
No code changes needed - the system automatically picks up crops from Constants!
Fertilizers restore nutrients with different ratios.
Edit src/config/Constants.lua:
SoilConstants.FERTILIZER_PROFILES = {
-- Existing fertilizers...
-- Add your new fertilizer here
["YOURFERTILIZER"] = { -- Must match FS25 fill type name (UPPERCASE)
N = 25, -- Nitrogen added per 1000L applied
P = 15, -- Phosphorus added
K = 10, -- Potassium added
pH = 0, -- pH change (positive increases, negative decreases)
OM = 0, -- Organic matter change
},
}Common Fertilizer Types:
- Liquid Fertilizer: High N (25-30), Moderate P/K (10-15)
- Solid Fertilizer: Balanced N/P/K (15-20 each)
- Manure: Moderate N/P/K (10-15), adds OM (0.5-1.0)
- Slurry: Moderate N/P/K (12-18), adds OM (0.3-0.5)
- Digestate: High N (20-25), moderate P/K, adds OM (0.4)
- Lime: No N/P/K, raises pH (+0.2 to +0.5)
- Note field nutrients:
SoilFieldInfo <fieldId> - Apply your fertilizer in FS25
- Check nutrients after:
SoilFieldInfo <fieldId> - Verify nutrients increased by your rates
The mod uses a schema-driven settings system. One definition in SettingsSchema.lua auto-generates:
- XML save/load
- Default values
- In-game UI
- Console commands
- Network sync
Edit src/config/SettingsSchema.lua:
SettingsSchema.definitions = {
-- Existing settings...
-- Add your new setting here
{
id = "yourSetting", -- Internal ID (camelCase)
type = "boolean", -- "boolean" or "number"
default = true, -- Default value
min = 1, -- (Optional) Min value for numbers
max = 10, -- (Optional) Max value for numbers
uiId = "sf_your_setting", -- UI/translation key (snake_case)
pfProtected = false, -- true = disabled when Precision Farming active
},
}Edit modDesc.xml in the <l10n> section:
<!-- Short label for UI toggle -->
<text name="sf_your_setting_short">
<en>Your Setting</en>
<de>Deine Einstellung</de>
<!-- ... other languages -->
</text>
<!-- Long description/tooltip -->
<text name="sf_your_setting_long">
<en>Enable/disable your new feature</en>
<de>Aktiviere/deaktiviere deine neue Funktion</de>
<!-- ... other languages -->
</text>Languages: en, de, fr, pl, es, it, cz, br, uk, ru, hu (11 total)
The setting is now automatically available:
-- Access anywhere via g_SoilFertilityManager.settings
if g_SoilFertilityManager.settings.yourSetting then
-- Your feature code here
endThat's it! The UI, save/load, network sync, and console commands are all auto-generated.
- Server-authoritative: Server owns soil data and settings
- Client sync: Clients receive updates via network events
- Admin-only: Only admin users can change settings
| Event | Direction | Purpose |
|---|---|---|
SoilSettingChangeEvent |
Client → Server | Request setting change (admin validated) |
SoilSettingSyncEvent |
Server → Clients | Broadcast setting change |
SoilRequestFullSyncEvent |
Client → Server | Request full state on join |
SoilFullSyncEvent |
Server → Client | Send all settings + field data |
SoilFieldUpdateEvent |
Server → Clients | Update specific field after harvest/fertilize |
- Client joins server
- Client sends
SoilRequestFullSyncEvent - Server responds with
SoilFullSyncEventcontaining:- All settings
- All field data
- Client applies received data
- If sync fails, client retries (3 attempts, 5-second intervals)
If you need to sync new data types:
-
Add to SoilFullSyncEvent (
NetworkEvents.lua):-- In writeStream: streamWriteString(streamId, tostring(self.yourNewData)) -- In readStream: self.yourNewData = streamReadString(streamId)
-
Broadcast changes:
if g_server and g_currentMission.missionDynamicInfo.isMultiplayer then g_server:broadcastEvent(YourUpdateEvent.new(data)) end
The mod intercepts FS25 game events using Utils.appendedFunction:
-- Original FS25 function
FruitUtil.fruitPickupEvent = function(...)
-- FS25's original code runs first
end
-- Our hook wraps it
FruitUtil.fruitPickupEvent = Utils.appendedFunction(
FruitUtil.fruitPickupEvent, -- Original function
function(...) -- Our code runs AFTER original
-- Our soil depletion logic
end
)| Hook | Target | Triggers On | Handler |
|---|---|---|---|
| Harvest | FruitUtil.fruitPickupEvent |
Crop harvested | SoilFertilitySystem:onHarvest() |
| Fertilizer | Sprayer.spray |
Fertilizer applied | SoilFertilitySystem:onFertilizerApplied() |
| Plowing | Cultivator.processCultivatorArea |
Field plowed | SoilFertilitySystem:onPlowing() |
| Ownership | g_farmlandManager.fieldOwnershipChanged |
Field bought/sold | SoilFertilitySystem:onFieldOwnershipChanged() |
| Weather | g_currentMission.environment.update |
Every frame | SoilFertilitySystem:onEnvironmentUpdate() |
- Add hook installation in
HookManager.lua:
function HookManager:installYourHook()
if not YourGameClass or not YourGameClass.yourMethod then
print("[SoilFertilizer WARNING] Could not install your hook")
return
end
local original = YourGameClass.yourMethod
YourGameClass.yourMethod = Utils.appendedFunction(
original,
function(self, param1, param2, ...)
if not g_SoilFertilityManager or
not g_SoilFertilityManager.settings.enabled then
return
end
local success, errorMsg = pcall(function()
g_SoilFertilityManager.soilSystem:onYourEvent(param1, param2)
end)
if not success then
print("[SoilFertilizer ERROR] Your hook failed: " .. tostring(errorMsg))
end
end
)
self:register(YourGameClass, "yourMethod", original, "YourGameClass.yourMethod")
print("[SoilFertilizer] Your hook installed")
end- Call from installAll():
function HookManager:installAll(soilSystem)
-- ... existing hooks
self:installYourHook()
end- Add handler in
SoilFertilitySystem.lua:
function SoilFertilitySystem:onYourEvent(param1, param2)
-- Your logic here
endImportant: Always use pcall() to prevent crashes from propagating to FS25!
The HUD is an always-on overlay (like Precision Farming's HUD):
- SoilHUD (
src/ui/SoilHUD.lua): Renders overlay - Position: User-configurable (5 presets)
- Visibility: Settings-based + F8 runtime toggle + context-aware
The HUD hides when:
- Mod disabled (
settings.enabled = false) - Show HUD setting off (
settings.showHUD = false) - F8 toggled off (
self.visible = false) - Menu/dialog open
- Large map open
- Tutorial messages visible
- Construction mode active
- Special camera modes
Edit SoilHUD:drawPanel() in src/ui/SoilHUD.lua:
function SoilHUD:drawPanel(fieldId)
-- ... existing drawing code
-- Add your new element
setTextColor(1.0, 1.0, 1.0, 1.0)
renderText(x, y, 0.012, string.format("Your Data: %d", yourValue))
y = y - lineHeight -- Move down for next line
endImportant: FS25's Giants Engine does NOT provide explicit Z-order/layer APIs for Overlays (no setRenderOrder(), no Z-index).
Overlays render in call order within a frame phase:
- Game initializes core UI (menus, HUD elements)
- Mods render overlays during their
draw()callbacks - Order depends on: mod load order + callback registration order
- Last rendered = top-most on screen
Timing: We render during standard FSBaseMission.update() callback, which executes AFTER game UI initialization but BEFORE debug overlays.
Defensive Visibility Checks (prevent rendering over critical UI):
- Game menus/dialogs (
g_gui:getIsGuiVisible()) - Large map overlay (
IngameMap.STATE_LARGE_MAP) - Construction mode (placeable placement)
- Tutorial messages
- Context help display
- Courseplay full HUD mode (if detected)
Position Flexibility: Users can choose from 5 HUD presets if conflicts occur with other mods.
Load these popular UI mods and verify no overlap:
- Courseplay: HUD should not overlap course UI
- AutoDrive: HUD should not overlap route display
- GPS Mod: HUD should not overlap guidance lines UI
- Precision Farming: HUD disabled (PF read-only mode)
If conflict detected:
- Check logs for which mod loaded first
- Adjust HUD position via settings (Top Right → Top Left, etc.)
- Enable compact mode to reduce vertical space
- Report incompatibility if settings can't resolve it
Enable debug mode and check render timing:
if self.settings.debugMode then
SoilLogger.info("[HUD] Rendering at position (%0.3f, %0.3f)", self.panelX, self.panelY)
endCheck log.txt to see when HUD renders relative to other mod messages.
See TESTING.md for comprehensive manual testing procedures.
- Load mod in clean savegame - no errors in log
- Harvest crops - nutrients deplete correctly
- Apply fertilizer - nutrients restore correctly
- Toggle settings - changes take effect
- Save/load - data persists
- Multiplayer - server/client sync works
- With Precision Farming - read-only mode activates
-- Add to your code for debugging
if self.settings.debugMode then
SoilLogger.info("Your debug message: %s", tostring(value))
endEnable in-game: SoilDebug
FS25 uses Lua 5.1 (not 5.2+):
- ❌ No
gotoorcontinue - ❌ No
os.time()oros.date()- Useg_currentMission.time - ❌ No bitwise operators - Use
bitAND,bitOR, etc. - ✅ Use guard clauses instead of
continue:-- Bad (doesn't work) for i, v in ipairs(list) do if v == skip then continue end end -- Good for i, v in ipairs(list) do if v ~= skip then -- your code end end
Use module prefixes for global functions:
-- Bad
function RequestSync() -- Pollutes global namespace
end
-- Good
function SoilNetworkEvents_RequestSync() -- Namespaced
endAlways uninstall hooks on mod unload:
-- HookManager tracks all hooks
self:register(TargetClass, "method", originalFunction, "name")
-- Cleanup in HookManager:uninstallAll()
for _, hook in ipairs(self.hooks) do
hook.target[hook.key] = hook.original -- Restore original
end- Always run soil changes on server only
- Always broadcast updates to clients
- Never modify soil data on clients directly
-- Good pattern
if g_server then
-- Modify data
field.nitrogen = newValue
-- Broadcast to clients
if g_currentMission.missionDynamicInfo.isMultiplayer then
g_server:broadcastEvent(SoilFieldUpdateEvent.new(fieldId, field))
end
endSettingsSchema.definitions order affects:
- UI display order
- Network sync order
- XML save/load order
Don't reorder existing settings after release - it breaks saves!
-
UI IDs must have
_shortand_longvariants:sf_your_setting_short- Label in UIsf_your_setting_long- Tooltip text
-
Multi-option settings need option labels:
sf_your_option_1,sf_your_option_2, etc.
- Field ID: Specific field polygon (unique)
- Farmland ID: Purchasable land parcel (may contain multiple fields)
Use g_fieldManager:getFieldAtWorldPosition(x, z) for precise field lookup.
-
Update version in modDesc.xml:
<version>1.0.5.0</version>
-
Update version in source file headers:
- Update headers in
SoilHUD.lua,UIHelper.lua,Settings.lua, etc. - Update
CLAUDE.mdProject Overview section
- Update headers in
-
Test thoroughly:
- Run full regression test checklist (see
TESTING.md) - Test in multiplayer
- Test with Precision Farming
- Run full regression test checklist (see
-
Update CHANGELOG:
- Document all changes since last version
- Group by: Added, Changed, Fixed, Removed
-
Create ZIP:
# From mod root directory zip -r FS25_SoilFertilizer.zip . -x "*.git*" -x "*.md"
-
Commit & Tag:
git add . git commit -m "Release v1.0.5.0" git tag v1.0.5.0 git push origin development git push origin v1.0.5.0
-
Create Pull Request from
developmenttomain
- Icon: 256×256 DDS file
- ZIP name: Must match modDesc
<modName> - No external dependencies: All code must be self-contained
- Translations: All 11 languages required
- Testing: Must work in both SP and MP
- File size: Keep under 50MB
- FS25 Scripting Documentation: https://gdn.giants-software.com/
- CLAUDE.md: Project architecture and conventions
- TESTING.md: Manual testing procedures
- CODEBASE_AUDIT.md: Known issues and tech debt
Questions? Open an issue on GitHub!
Happy Modding! 🚜🌾