Farm Tablet v2 is a client-side HUD mod built on a clean layered architecture. This document explains how all the pieces fit together.
main.lua
└── FarmTabletManager ← top-level coordinator, owns everything
├── Settings ← user preferences value object
├── SettingsManager ← XML serialisation
├── SettingsGUI ← console commands
├── SettingsUI ← pause-menu injection
├── FarmTabletSystem ← data + state (no rendering)
│ ├── AppRegistry ← app catalogue + auto-detection
│ ├── FT_DataProvider ← cached FS25 game-data queries
│ └── bucket state ← bucket tracker live state
├── FarmTabletUI ← all rendering + input
│ └── FT_Renderer ← overlay + text drawing API
└── InputHandler ← key polling
The FS25 mod entry point. Hooks into Mission00.load, Mission00.loadMission00Finished, and FSBaseMission.delete using Utils.prependedFunction / Utils.appendedFunction. Also appends to FSBaseMission.update to drive the per-frame update loop.
The g_dedicatedServer guard prevents any initialisation on dedicated servers — the mod is client-only.
The root object. Created once per mission load. Owns all subsystems and delegates to them. Exposes the public API used by console commands and the global g_FarmTablet.
Key responsibilities:
- Constructs all subsystems in the correct order
- Injects into the pause-menu settings frame
- Dispatches the per-frame
update(dt)to all subsystems - Shows the welcome HUD notification
Pure data/state layer — no rendering. Safe to construct on all network contexts (listen-server, client peer, etc.).
Key responsibilities:
- Holds the
AppRegistryandFT_DataProvider - Tracks
currentApp,isTabletOpen,workshopSelectedVehicle,soilSelectedField - Manages the bucket tracker state and update loop
- Resets stale selections in
onTabletClosed()
The entire rendering and interaction layer. Only constructed when mission:getIsClient() is true.
Key responsibilities:
- Builds the tablet layout from
FT.LAYOUTconstants - Manages the
FT_Rendererinstance - Dispatches to app-specific drawer functions from
_appDrawers - Handles mouse input and hit-testing
- Implements scroll wheel for sidebar and content
- Implements Edit Mode (drag/resize)
- Manages the
_clockTimerfor topbar refresh
All visual output goes through this class. Wraps the FS25 g_overlayManager API. Maintains separate layers for chrome (persistent), app content (cleared on switch), and cover strips (drawn on top to clip overflow).
Single access point for all FS25 game data. Uses a simple time-based cache (TTL per key) to avoid polling the game's APIs every frame. Invalidated on tablet close and after any state-changing action (e.g. vehicle repair).
Maintains the catalogue of all installed apps. Built-in apps are registered at construction time. Companion-mod apps are added dynamically by autoDetect() after mission load.
Settings is a plain value object holding all user preferences. SettingsManager handles XML serialisation to/from the per-savegame config file.
Polls Input.isKeyPressed(keyConstant) each frame and calls toggleTablet() on rising edge (key down, not held).
FS25 loads the mod
→ source() calls in main.lua register all classes
Mission00.load fires
→ FarmTabletManager.new() is called
→ Settings loaded from XML
→ FarmTabletSystem constructed
→ FarmTabletUI constructed (client only)
→ InputHandler constructed (client only)
→ SettingsUI injected into pause menu (client + g_gui)
→ SettingsGUI registers console commands (client only)
Mission00.loadMission00Finished fires
→ FarmTabletManager:onMissionLoaded()
→ FarmTabletSystem:initialize()
→ AppRegistry:autoDetect() — companion mods checked here
→ InputHandler:registerKeyBinding()
→ Welcome HUD notification shown
Per-frame FSBaseMission.update fires
→ FarmTabletManager:update(dt)
→ InputHandler:update(dt) — key polling
→ FarmTabletSystem:update(dt) — bucket tracker
→ FarmTabletUI:update(dt) — clock refresh, scroll polling, digging auto-refresh
FSBaseMission.delete fires
→ FarmTabletManager:delete()
→ Settings:save()
→ InputHandler:unregisterKeyBinding()
→ FarmTabletUI:delete() — destroys all overlays
Apps are not classes — they are drawer functions registered on the FarmTabletUI class table:
FarmTabletUI:registerDrawer(FT.APP.WEATHER, function(self)
-- self is the FarmTabletUI instance
-- Draw the app content here using self.r:appRect(), self.r:appText(), etc.
end)When the user switches to an app, FarmTabletUI:_drawContent() calls the registered function inside a pcall. If it errors, _drawError() renders the error message instead.
The drawer function receives self (the FarmTabletUI instance) and should use the layout helper methods:
self:contentInner()— padded content area boundsself:drawAppHeader()— standard title + dividerself:drawRow(),self:drawSection(),self:drawRule(),self:drawBar()— standard row/section primitivesself:drawButton(),self:drawButtonPair()— action buttons that register hit regionsself:drawInfoIcon()/self:drawHelpPage()— shared help overlay system
See writing-an-app.md for a full walkthrough.
FS25 uses a normalised screen space where:
x = 0.0is the left edge of the screenx = 1.0is the right edgey = 0.0is the bottom of the screeny = 1.0is the top
All drawing coordinates in FT_Renderer and app drawers are in this space.
The FT.px(v) and FT.py(v) helpers convert reference pixel values (designed at 1080p) to normalised units using the scale factors set by FarmTabletUI:_build(). Always use these instead of hardcoding normalised values — they handle scaling transparently.
Important:
FT.px()andFT.py()return0until_build()runs. Never call them at module-load time (i.e. outside a function body).
Each frame where the tablet is open:
FarmTabletUI:draw()is called by FS25 (registered as a drawable)- It calls
self.r:flush(clipY, clipH)which renders in this order:- Persistent overlays (
_overlays) — chrome body, background - App-layer overlays (
_appLayer) — current app content, clipped to content bounds - Cover overlays (
_coverLayer) — solid strips that hide overflow at content edges - Persistent text (
_texts) — topbar, sidebar labels - App-layer text (from
_buttonswhere_isText == true) — app content text
- Persistent overlays (
_drawEditOverlay()renders the Edit Mode pulsing border on top of everything (nativerenderOverlaycalls, bypassesFT_Renderer)
- The tablet is client-only.
FarmTabletSystemis safe everywhere butFarmTabletUIandInputHandlerare only created whenmission:getIsClient(). - Each player has their own independent tablet instance. There is no synchronisation between players.
- Data comes from
FT_DataProviderwhich reads from shared game globals (g_farmManager,g_currentMission, etc.) — so all players see the same farm data for their own farm. - The Workshop app's REPAIR action sends a
VehicleRepairEventnetwork event when the local client is not also the server, ensuring the repair is applied correctly in multiplayer. - Dedicated servers: the
g_dedicatedServerguard inmain.luaskips all initialisation entirely.