Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ build/
*.o
*.obj
CMakeUserPresets.json
TODO.md
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,15 @@ set(PLUGIN_SOURCES
"src/effects/cabin/EngineStartStopEffect.cpp"
"src/effects/manual/ManualZoomEffect.cpp"
"src/effects/manual/BlindspotViewerEffect.cpp"
"src/effects/manual/cabin_walk/CabinWalkEffect.cpp"
"src/effects/manual/cabin_walk/CabinLayouts.cpp"
"src/effects/manual/cabin_walk/CabinPresets.cpp"
"src/effects/manual/cabin_walk/CabinWalkSounds.cpp"
"src/effects/manual/cabin_walk/InteriorCameraOverride.cpp"
"src/ui/SettingsWindow.cpp"
"src/ui/Widgets.cpp"
"src/ui/EffectTabs.cpp"
"src/ui/CabinLayoutPanel.cpp"
"src/ui/SettingsTab.cpp"
"src/ui/AboutTab.cpp"
"src/ui/OpenUrl.cpp"
Expand Down
6 changes: 6 additions & 0 deletions CONTRIBUTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ game's `plugins/spfPlugins/MotionCab/`.
- Derive a new effect from `ConfigurableEffect`
(`src/effects/ConfigurableEffect.hpp`), which handles its `enabled`
toggle and reads its settings by name.
- If an effect only makes sense in the driver's seat, override
`NeedsDriverSeat()` so it fades out while Cabin Walk has the player
elsewhere in the cabin.
- Declare a new keybind action once in `src/core/Keybinds.hpp` (default
key and localization key) and add it to `kAllActions`, plus
`kPolledActions` if the effect polls it.
- Reuse the shared helpers (`src/math/Units.hpp`,
`src/effects/TelemetryUtil.hpp`, `src/effects/road/Surface.hpp`,
`src/core/Keybinds.hpp`) rather than duplicating their logic in an
Expand Down
37 changes: 25 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ camera control, keybind rebinding, and the in-game settings UI.
dies
- **Manual Zoom** — smooth zoom effect
- **Blindspot Viewer** — leans forward to see traffic lights hidden by the cab
- **Cabin Walk** — walks around the parked cabin and sits in the passenger
seat or on the bunk
- **Profiles** — create, save, and switch between named presets
- **Localized** — available in 15 languages

Expand All @@ -68,22 +70,23 @@ active while in the interior (cabin) camera view.

## Controls

| Action | Default key | Behavior |
| ---------------- | ----------- | ---------------------------------------------------------- |
| Look Left | Numpad / | Manual Look — hold or toggle to look left |
| Look Right | Numpad * | Manual Look — hold or toggle to look right |
| Zoom | Numpad - | Manual Zoom — hold to zoom in, release to return to normal |
| Blindspot Viewer | F10 | Toggle or hold to lean forward and look up |
| Toggle Window | F9 | Shows/hides the MotionCab Quick Settings window |

All keys are fully rebindable in SPF's in-game Keybinds drawer.
| Action | Default key | Behavior |
| ------------------- | ----------- | ------------------------------------------------------------------------------------- |
| Toggle Window | F9 | Shows/hides the MotionCab Quick Settings window |
| Look Left | Numpad / | Manual Look — hold or toggle to look left |
| Look Right | Numpad * | Manual Look — hold or toggle to look right |
| Zoom | Numpad - | Manual Zoom — hold to zoom in, release to return to normal |
| Blindspot Viewer | F10 | Blindspot Viewer — Toggle or hold to lean forward and look up |
| Stand Up / Sit Down | Page Up | Cabin Walk — get up; tap to sit at the seat you look at, hold to go back to the wheel |
| Walk | W/S/A/D | Cabin Walk — walk forward, back and sideways while standing |
| Crouch | C | Cabin Walk — crouch while standing |

## Configuration

All parameters are tuned live either through SPF's native in-game settings
window, or through MotionCab's own **Quick Settings** window, which mirrors
the same settings and adds a few conveniences: right-click any slider to reset
it to default, and the **Settings** tab has a button to reset every effect at once.
the same settings and adds a few conveniences: right-click any slider to reset it
to default, and the **Settings** tab has a button to reset every effect at once.
Values are persisted by the framework to `plugins/spfPlugins/MotionCab/config/settings.json`.

| Setting | Default | Description |
Expand Down Expand Up @@ -149,6 +152,8 @@ Values are persisted by the framework to `plugins/spfPlugins/MotionCab/config/se
| `fov_offset_deg` | 10.0 | How much the field of view widens while peeking. Negative = narrower. |
| `smoothing_time` | 0.6 | How smooth the lean-and-return motion is. |
| `toggle_mode` | true | On: press once to peek, press again to sit back. Off: peek while held, let go to sit back. |
| **Cabin Walk** | | |
| `require_parking_brake` | true | On: needs the parking brake, releasing it sends you back. Off: stopping is enough. |

Every effect also has its own `enabled` toggle, defaulting to on.

Expand Down Expand Up @@ -201,15 +206,23 @@ game's `plugins/spfPlugins/MotionCab/`.
│ │ │ Surface (ground classification)
│ │ ├── cabin/ IdleBreathing, EngineVibration, EngineStartStop
│ │ └── manual/ MirrorCheck, ManualLook, ManualZoom, BlindspotViewer
│ │ └── cabin_walk/
│ │ ├── CabinWalkEffect.* The effect: standing up, walking, sitting down
│ │ ├── CabinLayouts.* Per-truck cabin layouts
│ │ ├── CabinPresets.* Shipped layouts, one per truck model
│ │ ├── CabinWalkSounds.* Footsteps (FMOD bank)
│ │ └── InteriorCameraOverride.* Swaps the camera's limits while out of the seat
│ └── ui/
│ ├── SettingsWindow.cpp / .hpp MotionCab Quick Settings window
│ ├── EffectTabs.cpp / .hpp Driving/Road/Cabin/Manual tabs
│ ├── CabinLayoutPanel.cpp/.hpp Cabin Walk's layout panel (Advanced)
│ ├── SettingsTab.cpp / .hpp Settings tab: keybind, profiles, reset
│ ├── AboutTab.cpp / .hpp About tab
│ ├── Widgets.cpp / .hpp Shared look and widgets
│ └── OpenUrl.cpp / .hpp Opens a URL in the system browser
├── data/
│ └── logo.png About tab logo
│ ├── logo.png About tab logo
│ └── sounds/ Cabin Walk's FMOD bank
├── localization/
│ └── <lang>.json UI text, one file per language
├── SPF_API/ SPF Framework SDK headers
Expand Down
81 changes: 81 additions & 0 deletions SPF_API/SPF_Environment_API.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ extern "C" {
*/
typedef struct SPF_Environment_Handle SPF_Environment_Handle;

/**
* @brief Describes one entry from the game's VFS mount tables (Section 8).
*/
typedef struct SPF_VfsMountInfo {
char vpath[256]; /**< Virtual path (e.g. "/spf/MyPlugin", "/home"). */
char physical_path[512]; /**< Physical disk path backing the mount. */
int pool_index; /**< 0 = core, 1 = user, 2 = mod, 3 = scs, 4 = root. */
int order; /**< Mount order inside the pool (higher = resolved earlier). */
} SPF_VfsMountInfo;

/**
* @struct SPF_Environment_API
* @brief Table of function pointers to access environment data.
Expand Down Expand Up @@ -364,6 +374,77 @@ typedef struct SPF_Environment_API {
*/
int (*Env_GetActiveProfileType)(SPF_Environment_Handle* h, char* out_buffer, int buffer_size);

// =============================================================================================
// Section 7: Plugin VFS Mounting
// =============================================================================================

/**
* @brief Mounts a physical directory into the game VFS under "/spf/<PluginName>".
* @details The game resolves files under the virtual path to the plugin's physical
* directory. The mount is idempotent: calling again with the same directory
* returns the same virtual path. All mounts are automatically removed when
* the plugin is disabled or unloaded, even if the plugin forgets to unmount.
* @param h The context handle obtained from Env_GetContext.
* @param physical_path Physical directory to mount (e.g. from Env_GetPluginDir).
* @param pool_index VFS pool: -1 = default (user), 0 = core, 1 = user, 2 = mod, 3 = scs.
* Pool precedence is fixed (highest first): core > user > mod > scs; the
* first pool holding the file wins. Mount recency does not determine the
* winner: a newer mount does not override an older one — even if a mod
* folder is mounted later, base content registered earlier may still
* shadow it.
* @param order Mount order inside the pool: higher value = resolved earlier.
* Only matters when two mounts in the same pool overlap in virtual path
* prefixes (e.g. "/spf/A" vs "/spf/A/sub"); unrelated prefixes never
* compete. The game itself uses 100000 (base), 159-190 (DLC), 1001+
* (mods), 650 (home), 0 (steam). Pick a value below the game content of
* the target pool (e.g. 650 for user/mod pools) if the plugin content
* must lose to it. Any int is accepted; no game-side limits.
* @param out_vpath Buffer to receive the virtual path (e.g. "/spf/MyPlugin").
* @param buffer_size Size of the output buffer.
* @return true if the directory is mounted (or already was).
* @note Must be called from the game thread (OnActivated / OnGameWorldReady).
*/
bool (*Env_VfsMount)(SPF_Environment_Handle* h, const char* physical_path, int pool_index, int order, char* out_vpath, int buffer_size);

/**
* @brief Unmounts all VFS mounts created by this plugin.
* @param h The context handle obtained from Env_GetContext.
* @return true.
*/
bool (*Env_VfsUnmount)(SPF_Environment_Handle* h);

// =============================================================================================
// Section 8: VFS Mount Enumeration
// =============================================================================================

/**
* @brief Gets the total number of mounts across all VFS pools.
* @param h The context handle obtained from Env_GetContext.
* @return Total mount count in the game's live tables (includes game and mod
* mounts, not only this plugin's), or 0 when the finder is not ready.
* @note Call from the game thread. The count comes from a snapshot cache that
* is invalidated only when the game's mount tables change.
*/
int (*Env_VfsGetMountCount)(SPF_Environment_Handle* h);

/**
* @brief Reads one mount entry by flat index (pools in index order).
* @details On success fills all SPF_VfsMountInfo fields from the game's live
* mount tables: vpath (virtual path), physical_path (disk path),
* pool_index and order. Entries cover every mount in the game,
* including game and mod mounts, not only this plugin's.
* @param h The context handle obtained from Env_GetContext.
* @param index Zero-based index in [0, Env_VfsGetMountCount).
* @param out_info Struct receiving the mount entry. Only valid when this
* function returns true; on false its contents are untouched.
* @return true if the index was valid and out_info was filled.
* @note Strings may be empty when the backing game memory could not be read;
* pool_index and order are always set on success. Call from the game
* thread. Iterate: count = Env_VfsGetMountCount(h); for (i = 0; i <
* count; ++i) Env_VfsGetMountAt(h, i, &info).
*/
bool (*Env_VfsGetMountAt)(SPF_Environment_Handle* h, int index, SPF_VfsMountInfo* out_info);

} SPF_Environment_API;

#ifdef __cplusplus
Expand Down
8 changes: 8 additions & 0 deletions SPF_API/SPF_KeyBinds_API.h
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ typedef struct SPF_KeyBinds_API {
* @details This function is only effective if the action's 'consume' policy is set to 'manual'
* in the framework settings. It allows the plugin to decide at runtime when a
* shared key (e.g., WASD) should be captured by the plugin or passed to the game.
* The new state applies from the next press of the key, and `Kbind_GetActionValue`
* keeps reporting the input while it is blocked from the game.
*
* @param h The context handle.
* @param actionName The logical name of the action (e.g., "Movement.Forward").
Expand All @@ -288,6 +290,12 @@ typedef struct SPF_KeyBinds_API {
* It **ignores** logical behaviors such as 'toggle', 'hold', or 'press_type' (short/long).
* If you need to react to these logical events, use `Kbind_Register` instead.
*
* ### Blocking and UI Capture:
* - An input blocked from the game with `Kbind_SetBlockState` is still reported here.
* - While the framework UI captures the keyboard (a text field being edited, a modal
* popup open, a widget held with the mouse), keyboard inputs read as released,
* just as they don't trigger `Kbind_Register` callbacks.
*
* ### Expected Value Ranges:
* - **Digital Buttons (Keyboard/Gamepad):** Returns either `0.0` (released) or `1.0` (pressed).
* - **Analog Triggers:** Returns `0.0` to `1.0`.
Expand Down
Loading
Loading