diff --git a/.gitignore b/.gitignore index 1bed516..38038df 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build/ *.o *.obj CMakeUserPresets.json +TODO.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 4cd3192..085c619 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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" diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md index 32f34d8..3c3f670 100644 --- a/CONTRIBUTE.md +++ b/CONTRIBUTE.md @@ -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 diff --git a/README.md b/README.md index 7514db1..cd07ac8 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 | @@ -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. @@ -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/ │ └── .json UI text, one file per language ├── SPF_API/ SPF Framework SDK headers diff --git a/SPF_API/SPF_Environment_API.h b/SPF_API/SPF_Environment_API.h index b7fd638..919935e 100644 --- a/SPF_API/SPF_Environment_API.h +++ b/SPF_API/SPF_Environment_API.h @@ -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. @@ -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/". + * @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 diff --git a/SPF_API/SPF_KeyBinds_API.h b/SPF_API/SPF_KeyBinds_API.h index e4555b0..2a9f259 100644 --- a/SPF_API/SPF_KeyBinds_API.h +++ b/SPF_API/SPF_KeyBinds_API.h @@ -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"). @@ -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`. diff --git a/SPF_API/SPF_Sound_API.h b/SPF_API/SPF_Sound_API.h index 1e30a4d..081a5bc 100644 --- a/SPF_API/SPF_Sound_API.h +++ b/SPF_API/SPF_Sound_API.h @@ -2,12 +2,55 @@ /** * @file SPF_Sound_API.h - * @brief C-style API for the FMOD sound system, exposed to plugins. + * @brief C-style API for the sound system, exposed to plugins. * - * @details This header provides complete access to the game's FMOD Studio sound system. - * Plugins can enumerate sound banks, events, buses, and VCAs; control playback; - * adjust bus and global parameters; manage listeners; and override FMOD parameters - * at the hook level. + * @details This header provides access to the game's sound system through TWO + * independent layers. Choose the layer by answering one question: + * WHO should drive the sound — the plugin, or the game? + * + * ================================================================================================ + * ACCESS LAYERS + * ================================================================================================ + * + * [FMOD layer] — direct FMOD Studio access (implemented via FmodApi / FmodStudioHook). + * The plugin talks straight to FMOD: loads banks, enumerates events, creates + * instances, starts/stops playback, sets parameters, mixes buses and VCAs, + * controls listeners. THE GAME KNOWS NOTHING about these sounds — no game + * logic triggers, updates or stops them. Use this layer when: + * - the sound is entirely plugin-owned (custom music, plugin UI sounds, + * notifications, footsteps of a custom character); + * - the event does not exist in the game at all (e.g. a footsteps event + * the game never had); + * - the plugin must control playback timing itself. + * Banks loaded via SND_LoadBankFile / SND_LoadBankMemory go directly into FMOD: + * the game cannot see or manage such a bank — it never appears in the game's + * bank list and the game will not unload it. The plugin owns its lifetime. + * + * [Game layer] — SoundRef rebinding (".soundref" -> "bank#event"). + * The game maps its own sounds through ".soundref" files and activates them + * itself (UI clicks, horn, engine, world sounds). Use this layer when the + * plugin wants to REPLACE a sound the game plays, while the game keeps + * triggering, timing and stopping it exactly as before: + * 1. SND_RegisterSoundRefOverride("/sound/ui/ui_click.soundref", + * "my_bank#ui/click_v2"); + * 2. the game — including already-created events — switches to the new + * "bank#event"; + * 3. SND_UnregisterSoundRefOverride() / SND_ClearSoundRefOverrides() + * restores the originals. + * No plugin-side playback control is involved — the game remains the caller. + * + * Decision rule: + * - the game should keep calling the sound -> SoundRef (game layer) + * - the plugin drives playback / event not in the game -> FMOD (direct layer) + * + * ================================================================================================ + * THREAD SAFETY + * ================================================================================================ + * + * FMOD-layer functions must be called from the game thread (FMOD's command queue + * is processed there only; calling from other threads corrupts FMOD state). + * SoundRef (game layer) functions may be called from any thread; rebinds are + * applied on the game tick. * * ================================================================================================ * LIFECYCLE @@ -29,11 +72,18 @@ * 2. Control: SND_SetBusVolume() / SND_SetVCAVolume() / SND_SetBusMute() * * Bank management: - * 1. Load: SND_LoadBankFile() + * 1. Load: SND_LoadBankFile() / SND_LoadBankMemory() * 2. Query: SND_GetBankLoadingState() / SND_GetBankEventCount() * 3. Discover events: SND_GetBankEventGuid() + SND_FindEventIndexByGuid() * 4. Unload: SND_UnloadBank() * + * SoundRef rebinding (game layer): + * 1. Enumerate: SND_GetSoundRefCount() + SND_GetSoundRefPath() / SND_GetSoundRefSource() + * 2. Find: SND_FindSoundRefIndex() / SND_FindSoundRefBySource() + * 3. Rebind: SND_RegisterSoundRefOverride() + * 4. Inspect: SND_GetSoundRefOverride() / SND_IsSoundRefActive() + * 5. Restore: SND_UnregisterSoundRefOverride() / SND_ClearSoundRefOverrides() + * * ================================================================================================ * ABI STABILITY * ================================================================================================ @@ -119,7 +169,7 @@ typedef bool (*SPF_SND_AreAllOffsetsFound_t)(); typedef bool (*SPF_SND_RefreshOffsets_t)(); // ================================================================================================= -// BUS ENUMERATION & CONTROL +// BUS ENUMERATION & CONTROL [FMOD LAYER] // ================================================================================================= /** @@ -207,7 +257,7 @@ typedef bool (*SPF_SND_GetBusPause_t)(int index); typedef bool (*SPF_SND_SetBusPause_t)(int index, bool paused); // ================================================================================================= -// VCA (VOLUME CONTROL ASSOCIATION) +// VCA (VOLUME CONTROL ASSOCIATION) [FMOD LAYER] // ================================================================================================= /** @@ -250,7 +300,7 @@ typedef float (*SPF_SND_GetVCAVolume_t)(int index); typedef bool (*SPF_SND_SetVCAVolume_t)(int index, float volume); // ================================================================================================= -// GLOBAL PARAMETERS +// GLOBAL PARAMETERS [FMOD LAYER] // ================================================================================================= /** @@ -300,7 +350,7 @@ typedef float (*SPF_SND_GetGlobalParamValue_t)(const char* param_name); typedef bool (*SPF_SND_SetGlobalParamValue_t)(const char* param_name, float value); // ================================================================================================= -// EVENT ENUMERATION +// EVENT ENUMERATION [FMOD LAYER] // ================================================================================================= /** @@ -467,7 +517,7 @@ typedef int (*SPF_SND_GetEventLiveInstanceCount_t)(int event_index); typedef void* (*SPF_SND_GetEventLiveInstance_t)(int event_index, int instance_index); // ================================================================================================= -// EVENT PLAYBACK +// EVENT PLAYBACK [FMOD LAYER] // ================================================================================================= /** @@ -532,7 +582,7 @@ typedef int (*SPF_SND_GetEventPlaybackState_t)(void* instance); typedef void (*SPF_SND_ReleaseEvent_t)(void* instance); // ================================================================================================= -// EVENT INSTANCE PROPERTIES +// EVENT INSTANCE PROPERTIES [FMOD LAYER] // ================================================================================================= /** @@ -692,7 +742,7 @@ typedef int (*SPF_SND_GetEventLoopCount_t)(void* instance); typedef bool (*SPF_SND_SetEventCallback_t)(void* instance, SPF_SND_EventCallbackFn callback, uint32_t callback_mask); // ================================================================================================= -// LISTENER CONTROL +// LISTENER CONTROL [FMOD LAYER] // ================================================================================================= /** @@ -755,7 +805,7 @@ typedef bool (*SPF_SND_GetListenerAttributes_t)(int index, float* out_pos_x, flo typedef bool (*SPF_SND_SetListenerAttributes_t)(int index, float pos_x, float pos_y, float pos_z, float vel_x, float vel_y, float vel_z, float fwd_x, float fwd_y, float fwd_z, float up_x, float up_y, float up_z); // ================================================================================================= -// BANK MANAGEMENT +// BANK MANAGEMENT [FMOD LAYER] // ================================================================================================= /** @@ -770,12 +820,30 @@ typedef bool (*SPF_SND_SetListenerAttributes_t)(int index, float pos_x, float po * The dictionary enables path-based event lookup (SND_FindEventIndexByPath) * for plugin-bank events that FMOD cannot resolve internally. * + * FMOD layer: the game cannot see or manage this bank — it does not + * appear in the game bank list and the game will not unload it. + * * @param bank_path Filesystem path to the .bank file. * @param guids_path Path to the GUIDs dictionary file, or null for auto-discovery. * @return Opaque bank pointer, or NULL on failure. */ typedef void* (*SPF_SND_LoadBankFile_t)(const char* bank_path, const char* guids_path); +/** + * @brief Loads a bank from a memory buffer and optionally resolves event GUIDs from a dictionary. + * + * @details FMOD layer: the bank is loaded directly into FMOD — the game does not + * see it, does not list it and will not unload it. The plugin owns the + * buffer lifetime and the bank lifetime (unload with SND_UnloadBank()). + * Same GUID dictionary semantics as SND_LoadBankFile(). + * + * @param data Pointer to the raw .bank file bytes. + * @param size Size of the buffer in bytes. + * @param guids_path Path to the GUIDs dictionary file, or null for auto-discovery. + * @return Opaque bank pointer, or NULL on failure. + */ +typedef void* (*SPF_SND_LoadBankMemory_t)(const void* data, uint32_t size, const char* guids_path); + /** * @brief Returns the loading state of a bank. * @@ -849,7 +917,7 @@ typedef int (*SPF_SND_GetBankPath_t)(int index, char* out_buffer, int buffer_siz typedef bool (*SPF_SND_UnloadBank_t)(void* bank); // ================================================================================================= -// EVENT DESCRIPTION INTROSPECTION +// EVENT DESCRIPTION INTROSPECTION [FMOD LAYER] // ================================================================================================= /** @@ -918,7 +986,7 @@ typedef uint32_t (*SPF_SND_GetEventSoundSize_t)(int event_index); typedef int (*SPF_SND_GetEventSampleLoadingState_t)(int event_index); // ================================================================================================= -// FMOD HOOK OVERRIDES +// FMOD HOOK OVERRIDES [FMOD LAYER — INTERCEPTS THE GAME'S OWN FMOD READS] // ================================================================================================= /** @@ -980,13 +1048,447 @@ typedef void (*SPF_SND_Reset3DToOriginal_t)(const char* event_path); typedef bool (*SPF_SND_HasOverrides_t)(); /** - * @brief Removes ALL active FMOD hook overrides (parameters and 3D). + * @brief Removes ALL active FMOD hook overrides (parameters, 3D, volume, pitch). * - * @details After this call, all events revert to the game's original parameter - * and 3D attribute values. + * @details After this call, all events revert to the game's original parameter, + * 3D attribute, volume and pitch values. Suppression prefixes + * (SND_SuppressEventPlayback) are NOT affected — remove them explicitly. */ typedef void (*SPF_SND_RemoveAllOverrides_t)(); +/** + * @brief Forces the FMOD volume of every instance of an event path (FMOD hook layer). + * + * @details Intercepts EventInstance::setVolume for instances whose resolved + * event path equals @p event_path and replaces the value the game + * requested. The game's own volume logic keeps running — its writes + * are just replaced at the FMOD boundary. Remove the override to let + * the game's values through again. + * + * @param event_path Exact FMOD event path (e.g. "event:/horn/truck"). + * @param volume Forced volume (0.0 = silent, 1.0 = full). + */ +typedef void (*SPF_SND_OverrideEventVolume_t)(const char* event_path, float volume); + +/** + * @brief Removes a volume override installed by SND_OverrideEventVolume. + * + * @param event_path Exact FMOD event path the override was installed for. + */ +typedef void (*SPF_SND_RemoveEventVolumeOverride_t)(const char* event_path); + +/** + * @brief Forces the FMOD pitch of every instance of an event path (FMOD hook layer). + * + * @details Same interception model as SND_OverrideEventVolume, on + * EventInstance::setPitch. + * + * @param event_path Exact FMOD event path. + * @param pitch Forced pitch multiplier (1.0 = unchanged). + */ +typedef void (*SPF_SND_OverrideEventPitch_t)(const char* event_path, float pitch); + +/** + * @brief Removes a pitch override installed by SND_OverrideEventPitch. + * + * @param event_path Exact FMOD event path the override was installed for. + */ +typedef void (*SPF_SND_RemoveEventPitchOverride_t)(const char* event_path); + +// ================================================================================================= +// FMOD INTERCEPTION [SUPPRESSION / ACTIVITY OBSERVATION] (FMOD hook layer) +// ================================================================================================= + +// Activity codes delivered to SPF_SND_ActivityCallback_t. +#define SPF_ACTIVITY_EVENT_CREATED 1 // EventDescription::createInstance returned a new instance +#define SPF_ACTIVITY_EVENT_START_SUPPRESSED 2 // EventInstance::start was blocked by SND_SuppressEventPlayback +#define SPF_ACTIVITY_EVENT_STARTED 3 // EventInstance::start executed successfully +#define SPF_ACTIVITY_EVENT_STOPPED 4 // EventInstance::stop called +#define SPF_ACTIVITY_EVENT_PAUSED 5 // EventInstance::setPaused(true) called +#define SPF_ACTIVITY_EVENT_UNPAUSED 6 // EventInstance::setPaused(false) called +#define SPF_ACTIVITY_EVENT_RELEASED 7 // EventInstance::release called (path resolved before release) +#define SPF_ACTIVITY_EVENT_PARAM_SET 8 // setParameterByName/ByID called (param_name/param_value valid) +#define SPF_ACTIVITY_BANK_LOADED 9 // System::loadBank* succeeded (path = bank path) +#define SPF_ACTIVITY_BANK_UNLOADING 10 // Bank::unload called (path = bank path) + +/** + * @brief Callback observing FMOD activity — the interception layer's event feed. + * + * @details Invoked synchronously from inside the intercepting detour, on the + * thread that made the FMOD call (normally the game thread) — there is + * no queuing. @p path and @p param_name are valid only for the + * duration of the call; copy what you need. Nested delivery is + * suppressed (activity fired from inside your own callback is + * dropped), but calling SND_* FMOD functions from the callback is + * still unsafe — buffer the data and act on it later (e.g. in your + * OnUpdate). + * + * @param user_data Pointer passed to SND_SetActivityCallback. + * @param activity SPF_ACTIVITY_* code. + * @param path Event path, or bank path for SPF_ACTIVITY_BANK_*; "" if unresolvable. + * @param instance FMOD instance involved (NULL for bank activities). + * @param param_name Parameter name for SPF_ACTIVITY_EVENT_PARAM_SET, else NULL. + * @param param_value Effective value passed to FMOD for SPF_ACTIVITY_EVENT_PARAM_SET, else 0. + */ +typedef void (*SPF_SND_ActivityCallback_t)(void* user_data, int activity, const char* path, + void* instance, const char* param_name, float param_value); + +/** + * @brief Registers (or with NULL, unregisters) the FMOD activity callback. + * + * @details One callback per process; a new registration replaces the previous + * one. Survives until replaced or the hook is uninstalled. + * + * @param callback Callback function, or NULL to unregister. + * @param user_data Opaque pointer delivered back to @p callback. + */ +typedef void (*SPF_SND_SetActivityCallback_t)(SPF_SND_ActivityCallback_t callback, void* user_data); + +/** + * @brief Blocks playback start for every event path under a prefix (FMOD hook layer). + * + * @details Installs a prefix rule on the EventInstance::start detour: any start + * call for an event path beginning with @p path_prefix returns FMOD_OK + * without playing — the game's horn never reaches the speakers, from + * the first sample. The blocked start still counts: watch + * SND_GetSuppressedStartCount for press edges. The game may keep + * calling setParameter on the silent instance (e.g. "play") — those + * writes pass through untouched. Matching is case-sensitive prefix + * match; the rule applies to instances created before or after the + * call. Suppression rules survive until SND_UnsuppressEventPlayback. + * + * @param path_prefix Event path prefix (e.g. "event:/horn/"). + * @return true when the start hook is active and the rule installed; false on + * empty prefix or when the hook is unavailable. + */ +typedef bool (*SPF_SND_SuppressEventPlayback_t)(const char* path_prefix); + +/** + * @brief Removes a suppression rule installed by SND_SuppressEventPlayback. + * + * @details Instances already blocked stay silent until their next start call, + * which now plays normally. + * + * @param path_prefix The prefix rule to remove. + * @return true when a rule was found and removed; false otherwise. + */ +typedef bool (*SPF_SND_UnsuppressEventPlayback_t)(const char* path_prefix); + +/** + * @brief Returns how many start attempts were suppressed under a prefix. + * + * @details The counter resets when the prefix rule is removed or the hook + * reinstalled. Compare against a stored baseline to detect presses — + * the value only grows while the rule exists. + * + * @param path_prefix The suppression prefix. + * @return Suppressed-start attempt count, or 0 when no rule exists. + */ +typedef uint64_t (*SPF_SND_GetSuppressedStartCount_t)(const char* path_prefix); + +/** + * @brief Returns the FMOD instance of the most recent suppressed start under a prefix. + * + * @details Use it to read per-instance state the game keeps on the silent + * instance (e.g. the "play" parameter) to detect release. The pointer + * becomes NULL once the game releases that instance; never cache it + * across frames — call this every frame instead. The instance is + * stopped (its start was blocked) but alive and queryable. + * + * @param path_prefix The suppression prefix. + * @return FMOD instance pointer, or NULL when none / already released. + */ +typedef void* (*SPF_SND_GetLastSuppressedInstance_t)(const char* path_prefix); + +// ================================================================================================= +// GAME sound_event CONTROL [GAME LAYER] +// ================================================================================================= + +/** + * @brief Returns the number of live game sound_event_t objects. + * + * @details Walks per-bank event lists and builds a fresh snapshot on every + * call. Handles obtained via SND_GetGameEventAt stay valid while the + * event exists — prefer fetching handles once over repeated counts. + * + * @return Event count, or 0 if the sound system is not ready. + */ +typedef int (*SPF_SND_GetGameEventCount_t)(); + +/** + * @brief Returns the sound_event_t handle at @p index within a fresh snapshot. + * + * @param index Zero-based index (0 to SND_GetGameEventCount()-1 of the same snapshot). + * @return Opaque handle, or NULL when out of range. + */ +typedef void* (*SPF_SND_GetGameEventAt_t)(int index); + +/** + * @brief Finds the first game sound_event whose path equals @p path. + * + * @param path Exact event path string (game's own path field). + * @return Opaque handle, or NULL if no event matches. + */ +typedef void* (*SPF_SND_FindGameEventByPath_t)(const char* path); + +/** + * @brief Finds the first game sound_event whose source equals @p source. + * + * @param source Exact "bank#event" source string. + * @return Opaque handle, or NULL if no event matches. + */ +typedef void* (*SPF_SND_FindGameEventBySource_t)(const char* source); + +/** + * @brief Finds the game sound_event that owns @p instance. + * + * Reverse FMOD→game mapping: matches the instance field of every live + * sound_event against @p instance. + * + * @param instance FMOD EventInstance* handle. + * @return Opaque game event handle, or NULL if no game event owns it + * (plugin-created / foreign instances resolve to NULL). + */ +typedef void* (*SPF_SND_FindGameEventByInstance_t)(void* instance); + +/** + * @brief Reads the playback state dword of a game sound_event. + * + * @param event Handle from SND_FindGameEventByPath/BySource/At. + * @return SPF_SOUND_EVENT_STATE_* value; 0 if unavailable. + */ +typedef uint32_t (*SPF_SND_GetGameEventPlaybackState_t)(void* event); + +/** + * @brief Reports whether the game event is currently bound (state == 2). + * + * @param event Handle from SND_FindGameEventByPath/BySource/At. + * @return true when bound, false otherwise. + */ +typedef bool (*SPF_SND_IsGameEventBound_t)(void* event); + +/** + * @brief Recreates the event's EventInstance from its current source. + * + * @details Stops and releases the previous instance first (never leaks), then + * runs the game's own activate path. Does NOT auto-start playback — + * call SND_GameEvent_Start for that. Game thread only. + * + * @param event Handle from SND_FindGameEventByPath/BySource/At. + * @return true when activate succeeded. + */ +typedef bool (*SPF_SND_GameEvent_Activate_t)(void* event); + +/** + * @brief Starts (or resumes) a game sound_event through its vtable PlaybackControl. + * + * @param event Handle from SND_FindGameEventByPath/BySource/At. + * @return true on success. + */ +typedef bool (*SPF_SND_GameEvent_Start_t)(void* event); + +/** + * @brief Stops a game sound_event through its vtable Stop; instance is kept + * until the next SND_GameEvent_Activate. + * + * @param event Handle from SND_FindGameEventByPath/BySource/At. + * @return true on success. + */ +typedef bool (*SPF_SND_GameEvent_Stop_t)(void* event); + +/** + * @brief Pauses or resumes a game sound_event through its vtable SetPaused. + * + * @param event Handle from SND_FindGameEventByPath/BySource/At. + * @param paused true to pause, false to resume. + * @return true on success. + */ +typedef bool (*SPF_SND_GameEvent_SetPaused_t)(void* event, bool paused); + +/** + * @brief Sets event volume through its vtable SetVolume (also cached by the game at +0x80). + * + * @param event Handle from SND_FindGameEventByPath/BySource/At. + * @param volume New volume (game convention 0..1). + * @return true on success. + */ +typedef bool (*SPF_SND_GameEvent_SetVolume_t)(void* event, float volume); + +/** + * @brief Sets event pitch through its vtable SetPitch (cached by the game at +0x84). + * + * @param event Handle from SND_FindGameEventByPath/BySource/At. + * @param pitch New pitch multiplier (1.0 = normal). + * @return true on success. + */ +typedef bool (*SPF_SND_GameEvent_SetPitch_t)(void* event, float pitch); + +/** + * @brief Sets a raw FMOD event property through its vtable SetProperty (cached at +0x88). + * + * @param event Handle from SND_FindGameEventByPath/BySource/At. + * @param property_id FMOD event property id (0 = frequency modulation depth, ...). + * @param value New property value. + * @return true on success. + */ +typedef bool (*SPF_SND_GameEvent_SetProperty_t)(void* event, int property_id, float value); + +/** + * @brief Sets 3D position through its vtable Set3DAttributes. + * + * @param event Handle from SND_FindGameEventByPath/BySource/At. + * @param pos_x,pos_y,pos_z World position; velocity is zeroed by this overload. + * @return true on success. + */ +typedef bool (*SPF_SND_GameEvent_Set3DAttributes_t)(void* event, float pos_x, float pos_y, float pos_z); + +/** + * @brief Sets a parameter on a game sound_event by its 16-byte FMOD ID. + * + * @param event Handle from SND_FindGameEventByPath/BySource/At. + * @param id 16-byte parameter GUID (same layout as SPD_GET_PARAMETER_ID_GUID). + * @param value New parameter value. + * @return true on success. + */ +typedef bool (*SPF_SND_GameEvent_SetParameterByID_t)(void* event, const uint8_t id[16], float value); + +// SoundEvent playback state (sound_event_t+0x30) — SPF_SND_GetGameEventPlaybackState values +#define SPF_SOUND_EVENT_STATE_STOPPED 0u // stopped manually / never started +#define SPF_SOUND_EVENT_STATE_PLAYING 1u +#define SPF_SOUND_EVENT_STATE_PAUSED 3u +#define SPF_SOUND_EVENT_STATE_ENDED 4u // finished on its own + +// ================================================================================================= +// SOUNDREF CATALOG & REBINDING [GAME LAYER] +// ================================================================================================= + +/** + * @brief Returns the number of known .soundref paths. + * + * @details The catalog merges all sources: static game tables (UI, voice-nav), + * ".soundref" path references found in the game binary, files enumerated + * from the game VFS, and currently live sound events. Entries with a live + * event additionally expose their current "bank#event" source. + * + * @return SoundRef count, or 0 if the sound system is not ready. + */ +typedef int (*SPF_SND_GetSoundRefCount_t)(); + +/** + * @brief Copies the .soundref path of a catalog entry into the provided buffer. + * + * @param index Zero-based catalog index (0 to SND_GetSoundRefCount()-1). + * @param out_buffer Buffer to receive the path string. + * @param buffer_size Size of the output buffer in bytes. + * @return The full path length excluding the null terminator, or -1 if the index is invalid. + */ +typedef int (*SPF_SND_GetSoundRefPath_t)(int index, char* out_buffer, int buffer_size); + +/** + * @brief Copies the current "bank#event" source of a catalog entry. + * + * @details Non-empty only when the game has a live sound_event for this path + * (SND_IsSoundRefActive). Reflects any registered override once applied. + * + * @param index Zero-based catalog index. + * @param out_buffer Buffer to receive the source string. + * @param buffer_size Size of the output buffer in bytes. + * @return The full source length excluding the null terminator, or -1 if the index is invalid. + */ +typedef int (*SPF_SND_GetSoundRefSource_t)(int index, char* out_buffer, int buffer_size); + +/** + * @brief Searches the catalog for a ".soundref" path. + * + * @param soundref_path Full .soundref path (e.g. "/sound/ui/ui_click.soundref"). + * @return Zero-based catalog index, or -1 if not found. + */ +typedef int (*SPF_SND_FindSoundRefIndex_t)(const char* soundref_path); + +/** + * @brief Searches for the first catalog entry whose live source equals the given "bank#event". + * + * @details Useful to discover which soundref currently points at a given event, + * e.g. before rebinding everything that plays "music/main_menu" from a + * specific bank. + * + * @param source "bank#event" string (e.g. "sound/ui/ui.bank#click"). + * @return Zero-based catalog index, or -1 if not found. + */ +typedef int (*SPF_SND_FindSoundRefBySource_t)(const char* source); + +/** + * @brief Returns whether the game currently has a live sound_event for the path. + * + * @param soundref_path Full .soundref path. + * @return true if a live event exists, false otherwise. + */ +typedef bool (*SPF_SND_IsSoundRefActive_t)(const char* soundref_path); + +/** + * @brief PURE registration of a ".soundref" -> "bank#event" override (map only). + * + * @details No side effects at call time: nothing is applied to live events, no + * bank is loaded, no instance is stopped or started. The override is + * consulted inside the game's SoundRef load pipeline, so it takes + * effect on the soundref's NEXT activation by the game. For immediate + * audible replacement use SND_SoundRef_Replace. + * + * @param soundref_path Full .soundref path to rebind. + * @param source New source in "bank#event" format (e.g. "my_bank#ui/click_v2"). + * @return true if the override was registered, false on invalid arguments. + */ +typedef bool (*SPF_SND_RegisterSoundRefOverride_t)(const char* soundref_path, const char* source); + +/** + * @brief AUTOMATIC full-cycle replacement — the one-stop "swap my sound" call. + * + * @details Single call does everything, in order: + * 1) registers the override (same map as SND_RegisterSoundRefOverride); + * 2) rewrites the source field of every LIVE game event with this path; + * 3) queues a rebind executed on the game thread: stop current instance + * -> release it -> activate from the new source -> start again if the + * event was playing/paused before. During activate the GAME loads the + * target bank itself through its VFS (part of source before '#'), so + * the bank's VFS directory must be mounted first (Env_VfsMount). + * Events not live right now pick the override up automatically on their + * next activation (same as pure SND_RegisterSoundRefOverride). + * + * @param soundref_path Full .soundref path to rebind. + * @param source New source in "bank#event" format; before '#' is the VFS bank path. + * @return true if registered and the apply pass ran, false on invalid arguments. + */ +typedef bool (*SPF_SND_SoundRef_Replace_t)(const char* soundref_path, const char* source); + +/** + * @brief Removes one override, restoring the original "bank#event" source. + * + * @details Live events with this path are restored to their original source and + * rebound on the game thread with the same stop -> activate -> resume + * lifecycle, so a playing replacement stops and the original resumes. + * A bank that was auto-loaded by SND_SoundRef_Replace stays loaded + * (silent); unload it explicitly with SND_UnloadBank if needed. + * + * @param soundref_path Full .soundref path. + * @return true if an override existed and was removed, false otherwise. + */ +typedef bool (*SPF_SND_UnregisterSoundRefOverride_t)(const char* soundref_path); + +/** + * @brief Removes ALL registered SoundRef overrides, restoring every original source. + */ +typedef void (*SPF_SND_ClearSoundRefOverrides_t)(); + +/** + * @brief Copies the currently registered override source for a path. + * + * @param soundref_path Full .soundref path. + * @param out_buffer Buffer to receive the "bank#event" source. + * @param buffer_size Size of the output buffer in bytes. + * @return The source length (0 if no override is registered), or -1 on invalid arguments. + */ +typedef int (*SPF_SND_GetSoundRefOverride_t)(const char* soundref_path, char* out_buffer, int buffer_size); + // ================================================================================================= // API STRUCTURE // ================================================================================================= @@ -1015,7 +1517,7 @@ typedef struct SPF_Sound_API { /** @} */ /** - * @brief Bus enumeration and control. + * @brief Bus enumeration and control (FMOD layer). * @{ */ SPF_SND_GetBusCount_t SND_GetBusCount; @@ -1029,7 +1531,7 @@ typedef struct SPF_Sound_API { /** @} */ /** - * @brief VCA (Volume Control Association) enumeration and control. + * @brief VCA (Volume Control Association) enumeration and control (FMOD layer). * @{ */ SPF_SND_GetVCACount_t SND_GetVCACount; @@ -1039,7 +1541,7 @@ typedef struct SPF_Sound_API { /** @} */ /** - * @brief Global parameter enumeration and control. + * @brief Global parameter enumeration and control (FMOD layer). * @{ */ SPF_SND_GetGlobalParamCount_t SND_GetGlobalParamCount; @@ -1050,7 +1552,7 @@ typedef struct SPF_Sound_API { /** @} */ /** - * @brief Event enumeration (read-only metadata). + * @brief Event enumeration — bank catalog metadata (FMOD layer). * @{ */ SPF_SND_GetEventCount_t SND_GetEventCount; @@ -1072,7 +1574,7 @@ typedef struct SPF_Sound_API { /** @} */ /** - * @brief Event instance playback control. + * @brief Event instance playback control — plugin-driven playback (FMOD layer). * @{ */ SPF_SND_CreateEventInstance_t SND_CreateEventInstance; @@ -1084,7 +1586,7 @@ typedef struct SPF_Sound_API { /** @} */ /** - * @brief Event instance property control. + * @brief Event instance property control (FMOD layer). * @{ */ SPF_SND_SetEventVolume_t SND_SetEventVolume; @@ -1103,7 +1605,7 @@ typedef struct SPF_Sound_API { /** @} */ /** - * @brief Listener control. + * @brief Listener control (FMOD layer). * @{ */ SPF_SND_GetNumListeners_t SND_GetNumListeners; @@ -1113,10 +1615,11 @@ typedef struct SPF_Sound_API { /** @} */ /** - * @brief Bank management. + * @brief Bank management — banks loaded here are invisible to the game (FMOD layer). * @{ */ SPF_SND_LoadBankFile_t SND_LoadBankFile; + SPF_SND_LoadBankMemory_t SND_LoadBankMemory; SPF_SND_GetBankLoadingState_t SND_GetBankLoadingState; SPF_SND_GetBankEventCount_t SND_GetBankEventCount; SPF_SND_GetBankEventGuid_t SND_GetBankEventGuid; @@ -1127,7 +1630,7 @@ typedef struct SPF_Sound_API { /** @} */ /** - * @brief FMOD hook overrides (parameter and 3D attribute injection). + * @brief FMOD hook overrides — intercept the game's own FMOD reads (FMOD hook layer). * @{ */ SPF_SND_OverrideParameter_t SND_OverrideParameter; @@ -1137,10 +1640,25 @@ typedef struct SPF_Sound_API { SPF_SND_Reset3DToOriginal_t SND_Reset3DToOriginal; SPF_SND_HasOverrides_t SND_HasOverrides; SPF_SND_RemoveAllOverrides_t SND_RemoveAllOverrides; + SPF_SND_OverrideEventVolume_t SND_OverrideEventVolume; + SPF_SND_RemoveEventVolumeOverride_t SND_RemoveEventVolumeOverride; + SPF_SND_OverrideEventPitch_t SND_OverrideEventPitch; + SPF_SND_RemoveEventPitchOverride_t SND_RemoveEventPitchOverride; + /** @} */ + + /** + * @brief FMOD interception — suppression and activity observation (FMOD hook layer). + * @{ + */ + SPF_SND_SetActivityCallback_t SND_SetActivityCallback; + SPF_SND_SuppressEventPlayback_t SND_SuppressEventPlayback; + SPF_SND_UnsuppressEventPlayback_t SND_UnsuppressEventPlayback; + SPF_SND_GetSuppressedStartCount_t SND_GetSuppressedStartCount; + SPF_SND_GetLastSuppressedInstance_t SND_GetLastSuppressedInstance; /** @} */ /** - * @brief Event description introspection (parameters, user properties, sample state). + * @brief Event description introspection — parameters, user properties, sample state (FMOD layer). * @{ */ SPF_SND_GetEventParameterCount_t SND_GetEventParameterCount; @@ -1150,6 +1668,48 @@ typedef struct SPF_Sound_API { SPF_SND_GetEventSoundSize_t SND_GetEventSoundSize; SPF_SND_GetEventSampleLoadingState_t SND_GetEventSampleLoadingState; /** @} */ + + /** + * @brief Game sound_event control — direct handles into the game's own events (game layer). + * @details GAME THREAD ONLY: these calls reach FMOD internals and the game's + * vtable dispatch. Handles are raw game pointers — valid while the + * event exists; bank unload destroys its events, re-query after. + * @{ + */ + SPF_SND_GetGameEventCount_t SND_GetGameEventCount; + SPF_SND_GetGameEventAt_t SND_GetGameEventAt; + SPF_SND_FindGameEventByPath_t SND_FindGameEventByPath; + SPF_SND_FindGameEventBySource_t SND_FindGameEventBySource; + SPF_SND_FindGameEventByInstance_t SND_FindGameEventByInstance; + SPF_SND_GetGameEventPlaybackState_t SND_GetGameEventPlaybackState; + SPF_SND_IsGameEventBound_t SND_IsGameEventBound; + SPF_SND_GameEvent_Activate_t SND_GameEvent_Activate; + SPF_SND_GameEvent_Start_t SND_GameEvent_Start; + SPF_SND_GameEvent_Stop_t SND_GameEvent_Stop; + SPF_SND_GameEvent_SetPaused_t SND_GameEvent_SetPaused; + SPF_SND_GameEvent_SetVolume_t SND_GameEvent_SetVolume; + SPF_SND_GameEvent_SetPitch_t SND_GameEvent_SetPitch; + SPF_SND_GameEvent_SetProperty_t SND_GameEvent_SetProperty; + SPF_SND_GameEvent_Set3DAttributes_t SND_GameEvent_Set3DAttributes; + SPF_SND_GameEvent_SetParameterByID_t SND_GameEvent_SetParameterByID; + /** @} */ + + /** + * @brief SoundRef catalog & rebinding — the game keeps driving the sound (game layer). + * @{ + */ + SPF_SND_GetSoundRefCount_t SND_GetSoundRefCount; + SPF_SND_GetSoundRefPath_t SND_GetSoundRefPath; + SPF_SND_GetSoundRefSource_t SND_GetSoundRefSource; + SPF_SND_FindSoundRefIndex_t SND_FindSoundRefIndex; + SPF_SND_FindSoundRefBySource_t SND_FindSoundRefBySource; + SPF_SND_IsSoundRefActive_t SND_IsSoundRefActive; + SPF_SND_RegisterSoundRefOverride_t SND_RegisterSoundRefOverride; + SPF_SND_SoundRef_Replace_t SND_SoundRef_Replace; + SPF_SND_UnregisterSoundRefOverride_t SND_UnregisterSoundRefOverride; + SPF_SND_ClearSoundRefOverrides_t SND_ClearSoundRefOverrides; + SPF_SND_GetSoundRefOverride_t SND_GetSoundRefOverride; + /** @} */ } SPF_Sound_API; #ifdef __cplusplus diff --git a/data/sounds/MotionCab.bank b/data/sounds/MotionCab.bank new file mode 100644 index 0000000..b632d53 Binary files /dev/null and b/data/sounds/MotionCab.bank differ diff --git a/data/sounds/MotionCab.bank.guids b/data/sounds/MotionCab.bank.guids new file mode 100644 index 0000000..49acab2 --- /dev/null +++ b/data/sounds/MotionCab.bank.guids @@ -0,0 +1,6 @@ +{f6a678f3-22d5-4e9c-99e6-35d3de4b62a8} bank:/Master +{6ebb5827-53ae-02de-1faf-bf2d2fb86430} bank:/Master.strings +{ef6ac634-b9a5-43a2-9121-a924aa160fce} bank:/MotionCab +{7fe623a8-a2f0-4ba8-ad2a-9cad68432851} bus:/ +{72fc9579-78f7-401e-9f25-fe3fd10c52bf} bus:/Reverb +{5eb466cb-1df2-4a59-a9e4-f61fc1adaebe} event:/motioncab/footsteps diff --git a/localization/cs.json b/localization/cs.json index cbb62fe..40ccd2e 100644 --- a/localization/cs.json +++ b/localization/cs.json @@ -43,6 +43,32 @@ "blindspot_viewer": { "title": "Pohled do mrtvého úhlu", "desc": "Plynule se nakloní, aby bylo vidět za sloupek čelního skla." + }, + "cabin_walk": { + "stand_sit": { + "title": "Vstát / Sednout", + "desc": "Vstane ze sedadla. Klepnutím si sednete na místo, kam se díváte, podržením se vrátíte za volant." + }, + "forward": { + "title": "Jít dopředu", + "desc": "Jde dopředu, když stojíte v kabině." + }, + "back": { + "title": "Jít dozadu", + "desc": "Jde dozadu, když stojíte v kabině." + }, + "left": { + "title": "Krok doleva", + "desc": "Udělá úkrok doleva, když stojíte v kabině." + }, + "right": { + "title": "Krok doprava", + "desc": "Udělá úkrok doprava, když stojíte v kabině." + }, + "crouch": { + "title": "Přikrčit se", + "desc": "Přikrčí se vestoje." + } } }, "settings": { @@ -240,7 +266,7 @@ }, "manual": { "title": "Ruční", - "desc": "Efekty pohledu a přiblížení, které spouštíte sami.", + "desc": "Efekty rozhlížení, přiblížení a chůze, které spouštíte sami.", "mirror_check": { "title": "Kontrola zrcátek", "desc": "Podívá se do příslušného zrcátka, dokud svítí blinkr, a po jeho vypnutí se vrátí na střed.", @@ -347,6 +373,18 @@ "title": "Režim přepínání", "desc": "Zapnuto: jedním stiskem se nakloníte, dalším se vrátíte. Vypnuto: nakloníte se, dokud klávesu držíte, uvolněním se vrátíte." } + }, + "cabin_walk": { + "title": "Chůze po kabině", + "desc": "Vstaňte ze sedadla řidiče a volně se procházejte po kabině, když kamion stojí zaparkovaný.", + "hint": "Výchozí rozvržení jsou nastavena pro nejmenší kabinu s lůžkem každého tahače. Jiné kabiny může být nutné upravit.", + "enabled": { + "title": "Zapnout chůzi po kabině" + }, + "require_parking_brake": { + "title": "Vyžadovat parkovací brzdu", + "desc": "Zapnuto: nutná parkovací brzda, její uvolnění vás vrátí zpět. Vypnuto: stačí zastavit." + } } } }, @@ -431,6 +469,61 @@ "speed_system": "metric", "kmh": "km/h", "mph": "mph" + }, + "cabin_walk": { + "needs_parking_brake": "Zatáhněte parkovací brzdu, abyste mohli opustit sedadlo.", + "needs_stop": "Zastavte kamion, abyste mohli opustit sedadlo.", + "back_to_wheel": "Zpět za volant: kamion už není zaparkovaný.", + "layout": { + "no_truck": "Nastupte do kamionu, abyste mohli nastavit jeho kabinu.", + "truck": "Tahač", + "tip": { + "floor_z_min": "Jak daleko můžete jít dopředu, k čelnímu sklu.", + "floor_z_max": "Jak daleko můžete jít dozadu, k zadní stěně.", + "floor_x_min": "Jak daleko můžete jít ke dveřím řidiče.", + "floor_x_max": "Jak daleko můžete jít ke dveřím spolujezdce.", + "stand_y": "Výška vašich očí ve stoje.", + "crouch_depth": "O kolik klesne hlava, když se přikrčíte.", + "passenger_dx": "Do strany od zrcadlového obrazu vašeho sedadla.", + "passenger_dy": "Výš nebo níž než vaše sedadlo.", + "passenger_dz": "Víc vpředu nebo vzadu než vaše sedadlo.", + "bunk_sit_x": "Kde sedíte podél lůžka.", + "bunk_sit_y": "Výška vašich očí při sezení na lůžku.", + "bunk_sit_z": "Jak daleko vzadu je okraj lůžka.", + "yaw": "Kam se díváte vsedě.", + "pitch": "Jak moc se vsedě díváte nahoru nebo dolů.", + "has_passenger": "Zrušte, pokud se sedadlo spolujezdce sklápí nebo chybí.", + "has_bunk": "Zrušte u krátké kabiny bez lůžka." + }, + "group": { + "walkway": "Prostor pro chůzi", + "heights": "Výška vestoje", + "passenger": "Sedadlo spolujezdce", + "bunk": "Lůžko" + }, + "go": "Přejít tam", + "usable": "Použitelné", + "advanced": "Pokročilé", + "reset": "Obnovit uspořádání", + "reset_title": "Obnovit uspořádání kabiny", + "reset_message": "Vrátit se k výchozímu uspořádání pro {truck}?", + "reset_confirm": "Ano, obnovit", + "floor_x_min": "Levá hranice", + "floor_x_max": "Pravá hranice", + "floor_z_min": "Přední hranice", + "floor_z_max": "Zadní hranice", + "crouch_depth": "Hloubka přikrčení", + "passenger_dx": "Do strany", + "passenger_dy": "Výška", + "passenger_dz": "Hloubka", + "passenger_yaw": "Otočení", + "passenger_pitch": "Náklon", + "bunk_sit_x": "Do strany", + "bunk_sit_y": "Výška", + "bunk_sit_z": "Hloubka", + "bunk_sit_yaw": "Otočení", + "bunk_sit_pitch": "Náklon" + } } } } diff --git a/localization/de.json b/localization/de.json index bc2de7e..13dfd8b 100644 --- a/localization/de.json +++ b/localization/de.json @@ -43,6 +43,32 @@ "blindspot_viewer": { "title": "Toter-Winkel-Sicht", "desc": "Beugt sich sanft vor, um hinter die Scheibensäule zu sehen." + }, + "cabin_walk": { + "stand_sit": { + "title": "Aufstehen / Hinsetzen", + "desc": "Steht vom Sitz auf. Tippen, um sich auf den angeschauten Platz zu setzen, halten, um zurück ans Steuer zu gehen." + }, + "forward": { + "title": "Vorwärts gehen", + "desc": "Geht vorwärts, während du in der Kabine stehst." + }, + "back": { + "title": "Rückwärts gehen", + "desc": "Geht rückwärts, während du in der Kabine stehst." + }, + "left": { + "title": "Schritt nach links", + "desc": "Macht einen Seitschritt nach links, während du in der Kabine stehst." + }, + "right": { + "title": "Schritt nach rechts", + "desc": "Macht einen Seitschritt nach rechts, während du in der Kabine stehst." + }, + "crouch": { + "title": "Ducken", + "desc": "Duckt sich im Stehen." + } } }, "settings": { @@ -240,7 +266,7 @@ }, "manual": { "title": "Manuell", - "desc": "Blick- und Zoomeffekte, die du selbst auslöst.", + "desc": "Blick-, Zoom- und Laufeffekte, die du selbst auslöst.", "mirror_check": { "title": "Spiegelblick", "desc": "Schaut zum passenden Spiegel, solange ein Blinker an ist, und zentriert wieder, wenn er aus ist.", @@ -347,6 +373,18 @@ "title": "Umschaltmodus", "desc": "An: einmal drücken zum Vorbeugen, erneut drücken zum Zurücklehnen. Aus: vorbeugen, solange die Taste gehalten wird, loslassen zum Zurücklehnen." } + }, + "cabin_walk": { + "title": "Kabinenrundgang", + "desc": "Steh vom Fahrersitz auf und bewege dich im geparkten Lkw frei durch die Kabine.", + "hint": "Die Standardlayouts sind auf die kleinste Schlafkabine jedes Lkw abgestimmt. Andere Kabinen müssen eventuell angepasst werden.", + "enabled": { + "title": "Kabinenrundgang aktivieren" + }, + "require_parking_brake": { + "title": "Feststellbremse erforderlich", + "desc": "An: Feststellbremse nötig, Lösen schickt dich zurück. Aus: Stillstand genügt." + } } } }, @@ -431,6 +469,61 @@ "speed_system": "metric", "kmh": "km/h", "mph": "mph" + }, + "cabin_walk": { + "needs_parking_brake": "Zieh die Feststellbremse an, um den Sitz zu verlassen.", + "needs_stop": "Halte den Lkw an, um den Sitz zu verlassen.", + "back_to_wheel": "Zurück ans Steuer: Der Lkw ist nicht mehr geparkt.", + "layout": { + "no_truck": "Steig in einen Lkw, um seine Kabine einzurichten.", + "truck": "Lkw", + "tip": { + "floor_z_min": "Wie weit du nach vorn gehen kannst, zur Windschutzscheibe.", + "floor_z_max": "Wie weit du nach hinten gehen kannst, zur Rückwand.", + "floor_x_min": "Wie weit du zur Fahrertür gehen kannst.", + "floor_x_max": "Wie weit du zur Beifahrertür gehen kannst.", + "stand_y": "Deine Augenhöhe im Stehen.", + "crouch_depth": "Wie weit dein Kopf beim Ducken absinkt.", + "passenger_dx": "Seitlich vom Spiegelbild deines Sitzes.", + "passenger_dy": "Höher oder tiefer als dein Sitz.", + "passenger_dz": "Weiter vorn oder hinten als dein Sitz.", + "bunk_sit_x": "Wo du entlang der Liege sitzt.", + "bunk_sit_y": "Deine Augenhöhe beim Sitzen auf der Liege.", + "bunk_sit_z": "Wie weit hinten die Kante der Liege ist.", + "yaw": "Wohin du im Sitzen schaust.", + "pitch": "Wie weit du im Sitzen nach oben oder unten schaust.", + "has_passenger": "Abwählen, wenn der Beifahrersitz hochgeklappt ist oder fehlt.", + "has_bunk": "Abwählen für ein Fahrerhaus ohne Liege." + }, + "group": { + "walkway": "Laufbereich", + "heights": "Stehhöhe", + "passenger": "Beifahrersitz", + "bunk": "Liege" + }, + "go": "Hingehen", + "usable": "Nutzbar", + "advanced": "Erweitert", + "reset": "Aufteilung zurücksetzen", + "reset_title": "Kabinenaufteilung zurücksetzen", + "reset_message": "Zur Standardaufteilung für {truck} zurückkehren?", + "reset_confirm": "Ja, zurücksetzen", + "floor_x_min": "Linke Grenze", + "floor_x_max": "Rechte Grenze", + "floor_z_min": "Vordere Grenze", + "floor_z_max": "Hintere Grenze", + "crouch_depth": "Ducktiefe", + "passenger_dx": "Seite", + "passenger_dy": "Höhe", + "passenger_dz": "Tiefe", + "passenger_yaw": "Drehung", + "passenger_pitch": "Neigung", + "bunk_sit_x": "Seite", + "bunk_sit_y": "Höhe", + "bunk_sit_z": "Tiefe", + "bunk_sit_yaw": "Drehung", + "bunk_sit_pitch": "Neigung" + } } } } diff --git a/localization/en.json b/localization/en.json index f931a29..3802c39 100644 --- a/localization/en.json +++ b/localization/en.json @@ -43,6 +43,32 @@ "blindspot_viewer": { "title": "Blindspot Viewer", "desc": "Smoothly leans forward to see past the windshield pillar." + }, + "cabin_walk": { + "stand_sit": { + "title": "Stand Up / Sit Down", + "desc": "Gets up from the seat. Tap to sit at the seat you look at, hold to go back to the wheel." + }, + "forward": { + "title": "Walk Forward", + "desc": "Walks forward while standing in the cabin." + }, + "back": { + "title": "Walk Backward", + "desc": "Walks backward while standing in the cabin." + }, + "left": { + "title": "Step Left", + "desc": "Steps to the left while standing in the cabin." + }, + "right": { + "title": "Step Right", + "desc": "Steps to the right while standing in the cabin." + }, + "crouch": { + "title": "Crouch", + "desc": "Crouches while standing." + } } }, "settings": { @@ -240,7 +266,7 @@ }, "manual": { "title": "Manual", - "desc": "Look and zoom effects you trigger yourself.", + "desc": "Look, zoom and walking effects you trigger yourself.", "mirror_check": { "title": "Mirror Check", "desc": "Looks toward the corresponding mirror while a turn signal is on, and recenters when it's off.", @@ -347,6 +373,18 @@ "title": "Toggle Mode", "desc": "On: press once to peek, press again to sit back. Off: peek while held, let go to sit back." } + }, + "cabin_walk": { + "title": "Cabin Walk", + "desc": "Get up from the driver's seat and walk freely around the cabin while parked.", + "hint": "Default layouts are set for each truck's smallest sleeper cabin. Other cabins may need adjustments.", + "enabled": { + "title": "Enable Cabin Walk" + }, + "require_parking_brake": { + "title": "Require Parking Brake", + "desc": "On: needs the parking brake, releasing it sends you back. Off: stopping is enough." + } } } }, @@ -431,6 +469,61 @@ "speed_system": "imperial", "kmh": "km/h", "mph": "mph" + }, + "cabin_walk": { + "needs_parking_brake": "Engage the parking brake to leave the seat.", + "needs_stop": "Stop the truck to leave the seat.", + "back_to_wheel": "Back to the wheel: the truck is no longer parked.", + "layout": { + "no_truck": "Get in a truck to set up its cabin.", + "truck": "Truck", + "tip": { + "floor_z_min": "How far forward you can walk, toward the windshield.", + "floor_z_max": "How far back you can walk, toward the back wall.", + "floor_x_min": "How far you can walk toward the driver's door.", + "floor_x_max": "How far you can walk toward the passenger door.", + "stand_y": "Your eye height when standing.", + "crouch_depth": "How far your head drops when crouching.", + "passenger_dx": "Sideways from your own seat's mirror image.", + "passenger_dy": "Up or down from your own seat's height.", + "passenger_dz": "Forward or back from your own seat's depth.", + "bunk_sit_x": "Where you sit along the bunk.", + "bunk_sit_y": "Your eye height when sitting on the bunk.", + "bunk_sit_z": "How far back the bunk's edge is.", + "yaw": "Where you look once seated.", + "pitch": "How far up or down you look once seated.", + "has_passenger": "Untick if the passenger seat folds up or is missing.", + "has_bunk": "Untick for a day cab without a bunk." + }, + "group": { + "walkway": "Walkway", + "heights": "Standing Height", + "passenger": "Passenger Seat", + "bunk": "Bunk" + }, + "go": "Go There", + "usable": "Usable", + "advanced": "Advanced", + "reset": "Reset Layout", + "reset_title": "Reset Cabin Layout", + "reset_message": "Go back to the default layout for {truck}?", + "reset_confirm": "Yes, Reset", + "floor_x_min": "Left Limit", + "floor_x_max": "Right Limit", + "floor_z_min": "Front Limit", + "floor_z_max": "Back Limit", + "crouch_depth": "Crouch Depth", + "passenger_dx": "Side", + "passenger_dy": "Height", + "passenger_dz": "Depth", + "passenger_yaw": "Turn", + "passenger_pitch": "Tilt", + "bunk_sit_x": "Side", + "bunk_sit_y": "Height", + "bunk_sit_z": "Depth", + "bunk_sit_yaw": "Turn", + "bunk_sit_pitch": "Tilt" + } } } } diff --git a/localization/es.json b/localization/es.json index ca4d721..836eaea 100644 --- a/localization/es.json +++ b/localization/es.json @@ -43,6 +43,32 @@ "blindspot_viewer": { "title": "Visor de ángulo muerto", "desc": "Se inclina suavemente para ver detrás del pilar del parabrisas." + }, + "cabin_walk": { + "stand_sit": { + "title": "Levantarse / Sentarse", + "desc": "Te levanta del asiento. Pulsa para sentarte en el sitio que miras, mantén para volver al volante." + }, + "forward": { + "title": "Caminar hacia delante", + "desc": "Camina hacia delante mientras estás de pie en la cabina." + }, + "back": { + "title": "Caminar hacia atrás", + "desc": "Camina hacia atrás mientras estás de pie en la cabina." + }, + "left": { + "title": "Paso a la izquierda", + "desc": "Da un paso lateral a la izquierda mientras estás de pie en la cabina." + }, + "right": { + "title": "Paso a la derecha", + "desc": "Da un paso lateral a la derecha mientras estás de pie en la cabina." + }, + "crouch": { + "title": "Agacharse", + "desc": "Te agacha de pie." + } } }, "settings": { @@ -240,7 +266,7 @@ }, "manual": { "title": "Manual", - "desc": "Efectos de mirada y zoom que activas tú mismo.", + "desc": "Efectos de mirada, zoom y caminar que activas tú mismo.", "mirror_check": { "title": "Mirar los espejos", "desc": "Mira hacia el espejo correspondiente mientras un intermitente está encendido y vuelve al centro cuando se apaga.", @@ -347,6 +373,18 @@ "title": "Modo alternar", "desc": "Activado: pulsa una vez para inclinarte y otra vez para volver. Desactivado: te inclinas mientras mantienes pulsado y sueltas para volver." } + }, + "cabin_walk": { + "title": "Caminar por la cabina", + "desc": "Levántate del asiento del conductor y camina libremente por la cabina con el camión aparcado.", + "hint": "Las distribuciones predeterminadas están ajustadas a la cabina litera más pequeña de cada camión. Las demás cabinas pueden necesitar ajustes.", + "enabled": { + "title": "Activar Caminar por la cabina" + }, + "require_parking_brake": { + "title": "Exigir freno de estacionamiento", + "desc": "Activado: requiere el freno de estacionamiento, quitarlo te devuelve. Desactivado: basta con parar." + } } } }, @@ -431,6 +469,61 @@ "speed_system": "metric", "kmh": "km/h", "mph": "mph" + }, + "cabin_walk": { + "needs_parking_brake": "Pon el freno de estacionamiento para dejar el asiento.", + "needs_stop": "Detén el camión para dejar el asiento.", + "back_to_wheel": "De vuelta al volante: el camión ya no está aparcado.", + "layout": { + "no_truck": "Sube a un camión para configurar su cabina.", + "truck": "Camión", + "tip": { + "floor_z_min": "Hasta dónde puedes avanzar, hacia el parabrisas.", + "floor_z_max": "Hasta dónde puedes retroceder, hacia la pared trasera.", + "floor_x_min": "Hasta dónde puedes ir hacia la puerta del conductor.", + "floor_x_max": "Hasta dónde puedes ir hacia la puerta del acompañante.", + "stand_y": "La altura de tus ojos de pie.", + "crouch_depth": "Cuánto baja la cabeza al agacharte.", + "passenger_dx": "Hacia un lado desde el reflejo de tu asiento.", + "passenger_dy": "Más arriba o abajo que tu asiento.", + "passenger_dz": "Más adelante o atrás que tu asiento.", + "bunk_sit_x": "Dónde te sientas a lo largo de la litera.", + "bunk_sit_y": "La altura de tus ojos sentado en la litera.", + "bunk_sit_z": "Lo atrás que está el borde de la litera.", + "yaw": "Hacia dónde miras una vez sentado.", + "pitch": "Cuánto miras arriba o abajo una vez sentado.", + "has_passenger": "Desmárcalo si el asiento del acompañante se pliega o no existe.", + "has_bunk": "Desmárcalo para una cabina corta sin litera." + }, + "group": { + "walkway": "Zona de paso", + "heights": "Altura de pie", + "passenger": "Asiento del acompañante", + "bunk": "Litera" + }, + "go": "Ir allí", + "usable": "Utilizable", + "advanced": "Avanzado", + "reset": "Restablecer distribución", + "reset_title": "Restablecer distribución de la cabina", + "reset_message": "¿Volver a la distribución por defecto para {truck}?", + "reset_confirm": "Sí, restablecer", + "floor_x_min": "Límite izquierdo", + "floor_x_max": "Límite derecho", + "floor_z_min": "Límite delantero", + "floor_z_max": "Límite trasero", + "crouch_depth": "Profundidad al agacharse", + "passenger_dx": "Lado", + "passenger_dy": "Altura", + "passenger_dz": "Profundidad", + "passenger_yaw": "Giro", + "passenger_pitch": "Inclinación", + "bunk_sit_x": "Lado", + "bunk_sit_y": "Altura", + "bunk_sit_z": "Profundidad", + "bunk_sit_yaw": "Giro", + "bunk_sit_pitch": "Inclinación" + } } } } diff --git a/localization/fr.json b/localization/fr.json index 30e7323..f1e13f7 100644 --- a/localization/fr.json +++ b/localization/fr.json @@ -43,6 +43,32 @@ "blindspot_viewer": { "title": "Vue angle mort", "desc": "Se penche doucement pour voir derrière le montant de pare-brise." + }, + "cabin_walk": { + "stand_sit": { + "title": "Se lever / S'asseoir", + "desc": "Quitte le siège. Appuyer pour s'asseoir à la place regardée, maintenir pour revenir au volant." + }, + "forward": { + "title": "Avancer", + "desc": "Avance quand vous êtes debout dans la cabine." + }, + "back": { + "title": "Reculer", + "desc": "Recule quand vous êtes debout dans la cabine." + }, + "left": { + "title": "Pas à gauche", + "desc": "Fait un pas de côté vers la gauche quand vous êtes debout dans la cabine." + }, + "right": { + "title": "Pas à droite", + "desc": "Fait un pas de côté vers la droite quand vous êtes debout dans la cabine." + }, + "crouch": { + "title": "S'accroupir", + "desc": "S'accroupit debout." + } } }, "settings": { @@ -240,7 +266,7 @@ }, "manual": { "title": "Manuel", - "desc": "Effets de regard et de zoom que vous déclenchez vous-même.", + "desc": "Effets de regard, de zoom et de marche que vous déclenchez vous-même.", "mirror_check": { "title": "Contrôle des rétroviseurs", "desc": "Regarde vers le rétroviseur correspondant tant qu'un clignotant est allumé, et se recentre quand il s'éteint.", @@ -347,6 +373,18 @@ "title": "Mode bascule", "desc": "Activé : appuyer une fois pour se pencher, une seconde fois pour revenir. Désactivé : se pencher tant que la touche est maintenue, relâcher pour revenir." } + }, + "cabin_walk": { + "title": "Marche en cabine", + "desc": "Quittez le siège conducteur et marchez librement dans la cabine une fois garé.", + "hint": "Les dispositions par défaut sont réglées sur la plus petite cabine couchette de chaque camion. Les autres cabines peuvent nécessiter un ajustement.", + "enabled": { + "title": "Activer la marche en cabine" + }, + "require_parking_brake": { + "title": "Exiger le frein de parc", + "desc": "Activé : frein de parc requis, le desserrer vous ramène au volant. Désactivé : l'arrêt suffit." + } } } }, @@ -431,6 +469,61 @@ "speed_system": "metric", "kmh": "km/h", "mph": "mi/h" + }, + "cabin_walk": { + "needs_parking_brake": "Serrez le frein de parc pour quitter le siège.", + "needs_stop": "Arrêtez le camion pour quitter le siège.", + "back_to_wheel": "Retour au volant : le camion n'est plus garé.", + "layout": { + "no_truck": "Montez dans un camion pour configurer sa cabine.", + "truck": "Camion", + "tip": { + "floor_z_min": "Jusqu'où vous pouvez avancer, vers le pare-brise.", + "floor_z_max": "Jusqu'où vous pouvez reculer, vers la paroi arrière.", + "floor_x_min": "Jusqu'où vous pouvez aller vers la portière conducteur.", + "floor_x_max": "Jusqu'où vous pouvez aller vers la portière passager.", + "stand_y": "La hauteur de vos yeux debout.", + "crouch_depth": "De combien la tête descend quand vous vous accroupissez.", + "passenger_dx": "Sur le côté, depuis le reflet de votre siège.", + "passenger_dy": "Plus haut ou plus bas que votre siège.", + "passenger_dz": "Plus en avant ou en arrière que votre siège.", + "bunk_sit_x": "Où vous vous asseyez le long de la couchette.", + "bunk_sit_y": "La hauteur de vos yeux assis sur la couchette.", + "bunk_sit_z": "La distance du bord de la couchette vers l'arrière.", + "yaw": "Où vous regardez une fois assis.", + "pitch": "Vers le haut ou le bas une fois assis.", + "has_passenger": "Décochez si le siège passager se replie ou n'existe pas.", + "has_bunk": "Décochez pour une cabine courte sans couchette." + }, + "group": { + "walkway": "Zone de marche", + "heights": "Hauteur debout", + "passenger": "Siège passager", + "bunk": "Couchette" + }, + "go": "Y aller", + "usable": "Utilisable", + "advanced": "Avancé", + "reset": "Réinitialiser l'agencement", + "reset_title": "Réinitialiser l'agencement de la cabine", + "reset_message": "Revenir à l'agencement par défaut pour {truck} ?", + "reset_confirm": "Oui, réinitialiser", + "floor_x_min": "Limite gauche", + "floor_x_max": "Limite droite", + "floor_z_min": "Limite avant", + "floor_z_max": "Limite arrière", + "crouch_depth": "Profondeur d'accroupissement", + "passenger_dx": "Côté", + "passenger_dy": "Hauteur", + "passenger_dz": "Profondeur", + "passenger_yaw": "Rotation", + "passenger_pitch": "Inclinaison", + "bunk_sit_x": "Côté", + "bunk_sit_y": "Hauteur", + "bunk_sit_z": "Profondeur", + "bunk_sit_yaw": "Rotation", + "bunk_sit_pitch": "Inclinaison" + } } } } diff --git a/localization/it.json b/localization/it.json index 496e9cf..883c772 100644 --- a/localization/it.json +++ b/localization/it.json @@ -43,6 +43,32 @@ "blindspot_viewer": { "title": "Visuale angolo cieco", "desc": "Si sporge dolcemente per vedere dietro il montante del parabrezza." + }, + "cabin_walk": { + "stand_sit": { + "title": "Alzati / Siediti", + "desc": "Ti alza dal sedile. Premi per sederti nel posto che guardi, tieni premuto per tornare al volante." + }, + "forward": { + "title": "Cammina avanti", + "desc": "Cammina in avanti quando sei in piedi nella cabina." + }, + "back": { + "title": "Cammina indietro", + "desc": "Cammina all'indietro quando sei in piedi nella cabina." + }, + "left": { + "title": "Passo a sinistra", + "desc": "Fa un passo laterale a sinistra quando sei in piedi nella cabina." + }, + "right": { + "title": "Passo a destra", + "desc": "Fa un passo laterale a destra quando sei in piedi nella cabina." + }, + "crouch": { + "title": "Accovacciati", + "desc": "Ti accovacci in piedi." + } } }, "settings": { @@ -240,7 +266,7 @@ }, "manual": { "title": "Manuale", - "desc": "Effetti di sguardo e zoom che attivi tu.", + "desc": "Effetti di sguardo, zoom e camminata che attivi tu stesso.", "mirror_check": { "title": "Controllo specchietti", "desc": "Guarda verso lo specchietto corrispondente mentre un indicatore di direzione è acceso e torna al centro quando si spegne.", @@ -347,6 +373,18 @@ "title": "Modalità alternata", "desc": "Attiva: premi una volta per sporgerti, di nuovo per tornare. Disattiva: sporgiti finché tieni premuto, rilascia per tornare." } + }, + "cabin_walk": { + "title": "Camminata in cabina", + "desc": "Alzati dal sedile del conducente e muoviti liberamente nella cabina a camion parcheggiato.", + "hint": "Le disposizioni predefinite sono impostate sulla cabina con cuccetta più piccola di ogni camion. Le altre cabine potrebbero richiedere una regolazione.", + "enabled": { + "title": "Attiva Camminata in cabina" + }, + "require_parking_brake": { + "title": "Richiedi freno di stazionamento", + "desc": "Attivo: serve il freno di stazionamento, toglierlo ti riporta indietro. Disattivo: basta fermarsi." + } } } }, @@ -431,6 +469,61 @@ "speed_system": "metric", "kmh": "km/h", "mph": "mph" + }, + "cabin_walk": { + "needs_parking_brake": "Inserisci il freno di stazionamento per lasciare il sedile.", + "needs_stop": "Ferma il camion per lasciare il sedile.", + "back_to_wheel": "Di nuovo al volante: il camion non è più parcheggiato.", + "layout": { + "no_truck": "Sali su un camion per configurarne la cabina.", + "truck": "Camion", + "tip": { + "floor_z_min": "Quanto puoi avanzare, verso il parabrezza.", + "floor_z_max": "Quanto puoi arretrare, verso la parete posteriore.", + "floor_x_min": "Quanto puoi andare verso la portiera del conducente.", + "floor_x_max": "Quanto puoi andare verso la portiera del passeggero.", + "stand_y": "L'altezza dei tuoi occhi in piedi.", + "crouch_depth": "Quanto scende la testa quando ti accovacci.", + "passenger_dx": "Di lato rispetto al riflesso del tuo sedile.", + "passenger_dy": "Più in alto o in basso del tuo sedile.", + "passenger_dz": "Più avanti o indietro del tuo sedile.", + "bunk_sit_x": "Dove ti siedi lungo la cuccetta.", + "bunk_sit_y": "L'altezza dei tuoi occhi seduto sulla cuccetta.", + "bunk_sit_z": "Quanto è indietro il bordo della cuccetta.", + "yaw": "Dove guardi una volta seduto.", + "pitch": "Quanto guardi in alto o in basso una volta seduto.", + "has_passenger": "Deseleziona se il sedile del passeggero si ripiega o manca.", + "has_bunk": "Deseleziona per una cabina corta senza cuccetta." + }, + "group": { + "walkway": "Area calpestabile", + "heights": "Altezza in piedi", + "passenger": "Sedile passeggero", + "bunk": "Cuccetta" + }, + "go": "Vai lì", + "usable": "Utilizzabile", + "advanced": "Avanzate", + "reset": "Ripristina disposizione", + "reset_title": "Ripristina disposizione della cabina", + "reset_message": "Tornare alla disposizione predefinita per {truck}?", + "reset_confirm": "Sì, ripristina", + "floor_x_min": "Limite sinistro", + "floor_x_max": "Limite destro", + "floor_z_min": "Limite anteriore", + "floor_z_max": "Limite posteriore", + "crouch_depth": "Profondità accovacciata", + "passenger_dx": "Lato", + "passenger_dy": "Altezza", + "passenger_dz": "Profondità", + "passenger_yaw": "Rotazione", + "passenger_pitch": "Inclinazione", + "bunk_sit_x": "Lato", + "bunk_sit_y": "Altezza", + "bunk_sit_z": "Profondità", + "bunk_sit_yaw": "Rotazione", + "bunk_sit_pitch": "Inclinazione" + } } } } diff --git a/localization/ja.json b/localization/ja.json index fb7924b..9b57b23 100644 --- a/localization/ja.json +++ b/localization/ja.json @@ -43,6 +43,32 @@ "blindspot_viewer": { "title": "死角ビュー", "desc": "なめらかに身を乗り出して、フロントガラス横の柱の先を確認します。" + }, + "cabin_walk": { + "stand_sit": { + "title": "立つ / 座る", + "desc": "席から立ち上がります。押すと見ている席に座り、長押しすると運転席に戻ります。" + }, + "forward": { + "title": "前へ歩く", + "desc": "キャビン内で立っているときに前へ歩きます。" + }, + "back": { + "title": "後ろへ歩く", + "desc": "キャビン内で立っているときに後ろへ歩きます。" + }, + "left": { + "title": "左へステップ", + "desc": "キャビン内で立っているときに左へ横歩きします。" + }, + "right": { + "title": "右へステップ", + "desc": "キャビン内で立っているときに右へ横歩きします。" + }, + "crouch": { + "title": "しゃがむ", + "desc": "立っているときにしゃがみます。" + } } }, "settings": { @@ -240,7 +266,7 @@ }, "manual": { "title": "手動", - "desc": "自分で操作する視線とズームのエフェクトです。", + "desc": "自分で操作する視線・ズーム・歩行のエフェクト。", "mirror_check": { "title": "ミラー確認", "desc": "ウインカーが点いている間は対応するミラーを見て、消えると正面に戻ります。", @@ -347,6 +373,18 @@ "title": "トグルモード", "desc": "オン:1 回押すと身を乗り出し、もう一度押すと戻ります。オフ:押している間だけ身を乗り出し、離すと戻ります。" } + }, + "cabin_walk": { + "title": "キャビン歩行", + "desc": "駐車中に運転席から立ち上がり、キャビン内を自由に歩き回ります。", + "hint": "デフォルトのレイアウトは各トラックの最も小さいスリーパーキャブに合わせてあります。その他のキャビンは調整が必要な場合があります。", + "enabled": { + "title": "キャビン歩行を有効にする" + }, + "require_parking_brake": { + "title": "パーキングブレーキを必須にする", + "desc": "オン:パーキングブレーキが必要で、解除すると戻ります。オフ:停車していれば十分です。" + } } } }, @@ -431,6 +469,61 @@ "speed_system": "metric", "kmh": "km/h", "mph": "mph" + }, + "cabin_walk": { + "needs_parking_brake": "席を離れるにはパーキングブレーキをかけてください。", + "needs_stop": "席を離れるにはトラックを停止してください。", + "back_to_wheel": "運転席に戻りました:トラックが駐車状態ではなくなりました。", + "layout": { + "no_truck": "キャビンを設定するにはトラックに乗ってください。", + "truck": "トラック", + "tip": { + "floor_z_min": "前にどこまで歩けるか(フロントガラス方向)。", + "floor_z_max": "後ろにどこまで歩けるか(後壁方向)。", + "floor_x_min": "運転席側のドアにどこまで近づけるか。", + "floor_x_max": "助手席側のドアにどこまで近づけるか。", + "stand_y": "立っているときの目の高さ。", + "crouch_depth": "しゃがんだときに頭が下がる量。", + "passenger_dx": "自分の座席を反転した位置からの横方向のずれ。", + "passenger_dy": "自分の座席より上か下か。", + "passenger_dz": "自分の座席より前か後ろか。", + "bunk_sit_x": "寝台のどこに座るか。", + "bunk_sit_y": "寝台に座ったときの目の高さ。", + "bunk_sit_z": "寝台の縁がどれだけ後ろにあるか。", + "yaw": "座ったときに見る方向。", + "pitch": "座ったときに上下どれだけ見るか。", + "has_passenger": "助手席が折りたためる、または無い場合はチェックを外します。", + "has_bunk": "寝台の無いデイキャブではチェックを外します。" + }, + "group": { + "walkway": "歩行エリア", + "heights": "立ったときの高さ", + "passenger": "助手席", + "bunk": "寝台" + }, + "go": "そこへ移動", + "usable": "使用可能", + "advanced": "詳細設定", + "reset": "レイアウトをリセット", + "reset_title": "キャビンレイアウトのリセット", + "reset_message": "{truck} をデフォルトのレイアウトに戻しますか?", + "reset_confirm": "はい、リセット", + "floor_x_min": "左の境界", + "floor_x_max": "右の境界", + "floor_z_min": "前の境界", + "floor_z_max": "後ろの境界", + "crouch_depth": "しゃがむ深さ", + "passenger_dx": "左右", + "passenger_dy": "高さ", + "passenger_dz": "前後", + "passenger_yaw": "回転", + "passenger_pitch": "傾き", + "bunk_sit_x": "左右", + "bunk_sit_y": "高さ", + "bunk_sit_z": "前後", + "bunk_sit_yaw": "回転", + "bunk_sit_pitch": "傾き" + } } } } diff --git a/localization/ko.json b/localization/ko.json index d2bc199..88ea06b 100644 --- a/localization/ko.json +++ b/localization/ko.json @@ -43,6 +43,32 @@ "blindspot_viewer": { "title": "사각지대 뷰", "desc": "부드럽게 몸을 숙여 앞유리 옆 기둥 너머를 봅니다." + }, + "cabin_walk": { + "stand_sit": { + "title": "일어서기 / 앉기", + "desc": "자리에서 일어납니다. 누르면 바라보는 자리에 앉고, 길게 누르면 운전석으로 돌아갑니다." + }, + "forward": { + "title": "앞으로 걷기", + "desc": "캐빈 안에 서 있을 때 앞으로 걷습니다." + }, + "back": { + "title": "뒤로 걷기", + "desc": "캐빈 안에 서 있을 때 뒤로 걷습니다." + }, + "left": { + "title": "왼쪽으로 이동", + "desc": "캐빈 안에 서 있을 때 왼쪽으로 옆걸음합니다." + }, + "right": { + "title": "오른쪽으로 이동", + "desc": "캐빈 안에 서 있을 때 오른쪽으로 옆걸음합니다." + }, + "crouch": { + "title": "웅크리기", + "desc": "서 있을 때 웅크립니다." + } } }, "settings": { @@ -240,7 +266,7 @@ }, "manual": { "title": "수동", - "desc": "직접 작동하는 시선 및 확대 효과입니다.", + "desc": "직접 작동하는 시선, 줌, 걷기 효과입니다.", "mirror_check": { "title": "미러 확인", "desc": "방향지시등이 켜져 있는 동안 해당 미러를 바라보고, 꺼지면 정면으로 돌아옵니다.", @@ -347,6 +373,18 @@ "title": "토글 모드", "desc": "켜짐: 한 번 누르면 숙이고, 다시 누르면 돌아옵니다. 꺼짐: 누르고 있는 동안 숙이고, 놓으면 돌아옵니다." } + }, + "cabin_walk": { + "title": "캐빈 걷기", + "desc": "주차 중에 운전석에서 일어나 캐빈 안을 자유롭게 걸어 다닙니다.", + "hint": "기본 레이아웃은 각 트럭의 가장 작은 슬리퍼 캐빈에 맞춰져 있습니다. 다른 캐빈은 조정이 필요할 수 있습니다.", + "enabled": { + "title": "캐빈 걷기 사용" + }, + "require_parking_brake": { + "title": "주차 브레이크 필요", + "desc": "켜짐: 주차 브레이크가 필요하며, 풀면 돌아갑니다. 꺼짐: 정차만 하면 됩니다." + } } } }, @@ -431,6 +469,61 @@ "speed_system": "metric", "kmh": "km/h", "mph": "mph" + }, + "cabin_walk": { + "needs_parking_brake": "자리를 떠나려면 주차 브레이크를 채우세요.", + "needs_stop": "자리를 떠나려면 트럭을 멈추세요.", + "back_to_wheel": "운전석으로 복귀: 트럭이 더 이상 주차 상태가 아닙니다.", + "layout": { + "no_truck": "캐빈을 설정하려면 트럭에 탑승하세요.", + "truck": "트럭", + "tip": { + "floor_z_min": "앞쪽으로 얼마나 걸어갈 수 있는지(앞유리 방향).", + "floor_z_max": "뒤쪽으로 얼마나 걸어갈 수 있는지(뒷벽 방향).", + "floor_x_min": "운전석 문 쪽으로 얼마나 갈 수 있는지.", + "floor_x_max": "조수석 문 쪽으로 얼마나 갈 수 있는지.", + "stand_y": "서 있을 때 눈높이.", + "crouch_depth": "웅크릴 때 머리가 내려가는 정도입니다.", + "passenger_dx": "내 좌석을 반대편에 비춘 위치에서 옆으로 이동.", + "passenger_dy": "내 좌석보다 높거나 낮게.", + "passenger_dz": "내 좌석보다 앞이나 뒤로.", + "bunk_sit_x": "침대 위 어디에 앉는지.", + "bunk_sit_y": "침대에 앉았을 때 눈높이.", + "bunk_sit_z": "침대 가장자리가 얼마나 뒤에 있는지.", + "yaw": "앉았을 때 보는 방향.", + "pitch": "앉았을 때 위아래로 보는 정도.", + "has_passenger": "조수석이 접히거나 없으면 체크를 해제하세요.", + "has_bunk": "침대가 없는 데이 캡이면 체크를 해제하세요." + }, + "group": { + "walkway": "보행 영역", + "heights": "서 있을 때 높이", + "passenger": "조수석", + "bunk": "침대" + }, + "go": "그곳으로 이동", + "usable": "사용 가능", + "advanced": "고급", + "reset": "배치 초기화", + "reset_title": "캐빈 배치 초기화", + "reset_message": "{truck}의 배치를 기본값으로 되돌릴까요?", + "reset_confirm": "예, 초기화", + "floor_x_min": "왼쪽 경계", + "floor_x_max": "오른쪽 경계", + "floor_z_min": "앞쪽 경계", + "floor_z_max": "뒤쪽 경계", + "crouch_depth": "웅크리기 깊이", + "passenger_dx": "좌우", + "passenger_dy": "높이", + "passenger_dz": "앞뒤", + "passenger_yaw": "회전", + "passenger_pitch": "기울기", + "bunk_sit_x": "좌우", + "bunk_sit_y": "높이", + "bunk_sit_z": "앞뒤", + "bunk_sit_yaw": "회전", + "bunk_sit_pitch": "기울기" + } } } } diff --git a/localization/nl.json b/localization/nl.json index 44f4d1e..35ebdbf 100644 --- a/localization/nl.json +++ b/localization/nl.json @@ -43,6 +43,32 @@ "blindspot_viewer": { "title": "Dode-hoekzicht", "desc": "Leunt vloeiend naar voren om achter de raamstijl te kijken." + }, + "cabin_walk": { + "stand_sit": { + "title": "Opstaan / Gaan zitten", + "desc": "Staat op uit de stoel. Tik om te gaan zitten op de plek waar je naar kijkt, houd vast om terug achter het stuur te gaan." + }, + "forward": { + "title": "Vooruit lopen", + "desc": "Loopt vooruit terwijl je in de cabine staat." + }, + "back": { + "title": "Achteruit lopen", + "desc": "Loopt achteruit terwijl je in de cabine staat." + }, + "left": { + "title": "Stap naar links", + "desc": "Zet een zijstap naar links terwijl je in de cabine staat." + }, + "right": { + "title": "Stap naar rechts", + "desc": "Zet een zijstap naar rechts terwijl je in de cabine staat." + }, + "crouch": { + "title": "Hurken", + "desc": "Hurkt terwijl je staat." + } } }, "settings": { @@ -240,7 +266,7 @@ }, "manual": { "title": "Handmatig", - "desc": "Kijk- en zoomeffecten die je zelf activeert.", + "desc": "Kijk-, zoom- en loopeffecten die je zelf activeert.", "mirror_check": { "title": "Spiegelcontrole", "desc": "Kijkt naar de bijbehorende spiegel zolang een richtingaanwijzer aan staat, en centreert weer als hij uit gaat.", @@ -347,6 +373,18 @@ "title": "Schakelmodus", "desc": "Aan: één keer drukken om naar voren te leunen, nogmaals om terug te gaan. Uit: naar voren leunen zolang de toets ingedrukt is, loslaten om terug te gaan." } + }, + "cabin_walk": { + "title": "Rondlopen in de cabine", + "desc": "Sta op uit de bestuurdersstoel en loop vrij door de cabine als de truck geparkeerd staat.", + "hint": "De standaardindelingen zijn afgesteld op de kleinste slaapcabine van elke truck. Andere cabines moeten mogelijk worden bijgesteld.", + "enabled": { + "title": "Rondlopen in de cabine inschakelen" + }, + "require_parking_brake": { + "title": "Parkeerrem vereist", + "desc": "Aan: parkeerrem vereist, losmaken stuurt je terug. Uit: stilstaan is genoeg." + } } } }, @@ -431,6 +469,61 @@ "speed_system": "metric", "kmh": "km/u", "mph": "mph" + }, + "cabin_walk": { + "needs_parking_brake": "Zet de parkeerrem aan om de stoel te verlaten.", + "needs_stop": "Stop de truck om de stoel te verlaten.", + "back_to_wheel": "Terug achter het stuur: de truck staat niet meer geparkeerd.", + "layout": { + "no_truck": "Stap in een truck om zijn cabine in te stellen.", + "truck": "Truck", + "tip": { + "floor_z_min": "Hoe ver je naar voren kunt lopen, richting de voorruit.", + "floor_z_max": "Hoe ver je naar achteren kunt lopen, richting de achterwand.", + "floor_x_min": "Hoe ver je richting de bestuurdersdeur kunt lopen.", + "floor_x_max": "Hoe ver je richting de bijrijdersdeur kunt lopen.", + "stand_y": "Je ooghoogte als je staat.", + "crouch_depth": "Hoe ver je hoofd zakt als je hurkt.", + "passenger_dx": "Opzij vanaf het spiegelbeeld van je stoel.", + "passenger_dy": "Hoger of lager dan je stoel.", + "passenger_dz": "Verder naar voren of achteren dan je stoel.", + "bunk_sit_x": "Waar je langs de slaapbank zit.", + "bunk_sit_y": "Je ooghoogte als je op de slaapbank zit.", + "bunk_sit_z": "Hoe ver naar achteren de rand van de slaapbank is.", + "yaw": "Waar je kijkt als je zit.", + "pitch": "Hoe ver je omhoog of omlaag kijkt als je zit.", + "has_passenger": "Uitvinken als de bijrijdersstoel inklapt of ontbreekt.", + "has_bunk": "Uitvinken voor een dagcabine zonder slaapbank." + }, + "group": { + "walkway": "Loopruimte", + "heights": "Stahoogte", + "passenger": "Bijrijdersstoel", + "bunk": "Slaapbank" + }, + "go": "Ga erheen", + "usable": "Bruikbaar", + "advanced": "Geavanceerd", + "reset": "Indeling resetten", + "reset_title": "Cabine-indeling resetten", + "reset_message": "Terug naar de standaardindeling voor {truck}?", + "reset_confirm": "Ja, resetten", + "floor_x_min": "Linkergrens", + "floor_x_max": "Rechtergrens", + "floor_z_min": "Voorgrens", + "floor_z_max": "Achtergrens", + "crouch_depth": "Hurkdiepte", + "passenger_dx": "Zijkant", + "passenger_dy": "Hoogte", + "passenger_dz": "Diepte", + "passenger_yaw": "Draaiing", + "passenger_pitch": "Kanteling", + "bunk_sit_x": "Zijkant", + "bunk_sit_y": "Hoogte", + "bunk_sit_z": "Diepte", + "bunk_sit_yaw": "Draaiing", + "bunk_sit_pitch": "Kanteling" + } } } } diff --git a/localization/pl.json b/localization/pl.json index 842c8b5..6bceeaf 100644 --- a/localization/pl.json +++ b/localization/pl.json @@ -43,6 +43,32 @@ "blindspot_viewer": { "title": "Podgląd martwego pola", "desc": "Płynnie pochyla się, aby zajrzeć za słupek szyby." + }, + "cabin_walk": { + "stand_sit": { + "title": "Wstań / Usiądź", + "desc": "Wstaje z fotela. Stuknij, aby usiąść na miejscu, na które patrzysz, przytrzymaj, aby wrócić za kierownicę." + }, + "forward": { + "title": "Idź do przodu", + "desc": "Idzie do przodu, gdy stoisz w kabinie." + }, + "back": { + "title": "Idź do tyłu", + "desc": "Idzie do tyłu, gdy stoisz w kabinie." + }, + "left": { + "title": "Krok w lewo", + "desc": "Robi krok w bok w lewo, gdy stoisz w kabinie." + }, + "right": { + "title": "Krok w prawo", + "desc": "Robi krok w bok w prawo, gdy stoisz w kabinie." + }, + "crouch": { + "title": "Kucnij", + "desc": "Kuca, gdy stoisz." + } } }, "settings": { @@ -240,7 +266,7 @@ }, "manual": { "title": "Ręczne", - "desc": "Efekty spojrzenia i przybliżenia wywoływane przez ciebie.", + "desc": "Efekty patrzenia, przybliżania i chodzenia, które uruchamiasz samodzielnie.", "mirror_check": { "title": "Sprawdzanie lusterek", "desc": "Spogląda w odpowiednie lusterko, gdy włączony jest kierunkowskaz, i wraca na środek po jego wyłączeniu.", @@ -347,6 +373,18 @@ "title": "Tryb przełączania", "desc": "Wł.: naciśnij raz, aby się pochylić, i ponownie, aby wrócić. Wył.: pochylaj się, dopóki trzymasz klawisz, puść, aby wrócić." } + }, + "cabin_walk": { + "title": "Chodzenie po kabinie", + "desc": "Wstań z fotela kierowcy i swobodnie chodź po kabinie, gdy ciężarówka stoi zaparkowana.", + "hint": "Domyślne układy są ustawione dla najmniejszej kabiny sypialnej każdej ciężarówki. Inne kabiny mogą wymagać dostosowania.", + "enabled": { + "title": "Włącz chodzenie po kabinie" + }, + "require_parking_brake": { + "title": "Wymagaj hamulca postojowego", + "desc": "Wł.: wymaga hamulca postojowego, zwolnienie go odsyła cię z powrotem. Wył.: wystarczy postój." + } } } }, @@ -431,6 +469,61 @@ "speed_system": "metric", "kmh": "km/h", "mph": "mph" + }, + "cabin_walk": { + "needs_parking_brake": "Zaciągnij hamulec postojowy, aby opuścić fotel.", + "needs_stop": "Zatrzymaj ciężarówkę, aby opuścić fotel.", + "back_to_wheel": "Z powrotem za kierownicą: ciężarówka nie jest już zaparkowana.", + "layout": { + "no_truck": "Wsiądź do ciężarówki, aby ustawić jej kabinę.", + "truck": "Ciężarówka", + "tip": { + "floor_z_min": "Jak daleko możesz iść do przodu, w stronę szyby.", + "floor_z_max": "Jak daleko możesz iść do tyłu, w stronę tylnej ściany.", + "floor_x_min": "Jak daleko możesz iść w stronę drzwi kierowcy.", + "floor_x_max": "Jak daleko możesz iść w stronę drzwi pasażera.", + "stand_y": "Wysokość twoich oczu na stojąco.", + "crouch_depth": "Jak nisko opada głowa podczas kucania.", + "passenger_dx": "W bok od odbicia twojego fotela.", + "passenger_dy": "Wyżej lub niżej niż twój fotel.", + "passenger_dz": "Bardziej z przodu lub z tyłu niż twój fotel.", + "bunk_sit_x": "Gdzie siadasz wzdłuż koi.", + "bunk_sit_y": "Wysokość twoich oczu, gdy siedzisz na koi.", + "bunk_sit_z": "Jak daleko z tyłu jest krawędź koi.", + "yaw": "Gdzie patrzysz po usadowieniu się.", + "pitch": "Jak bardzo patrzysz w górę lub w dół po usadowieniu się.", + "has_passenger": "Odznacz, jeśli fotel pasażera się składa lub go nie ma.", + "has_bunk": "Odznacz dla krótkiej kabiny bez koi." + }, + "group": { + "walkway": "Obszar chodzenia", + "heights": "Wysokość na stojąco", + "passenger": "Fotel pasażera", + "bunk": "Koja" + }, + "go": "Przejdź tam", + "usable": "Dostępne", + "advanced": "Zaawansowane", + "reset": "Resetuj układ", + "reset_title": "Resetuj układ kabiny", + "reset_message": "Wrócić do domyślnego układu dla {truck}?", + "reset_confirm": "Tak, resetuj", + "floor_x_min": "Granica lewa", + "floor_x_max": "Granica prawa", + "floor_z_min": "Granica przednia", + "floor_z_max": "Granica tylna", + "crouch_depth": "Głębokość kucania", + "passenger_dx": "Bok", + "passenger_dy": "Wysokość", + "passenger_dz": "Głębokość", + "passenger_yaw": "Obrót", + "passenger_pitch": "Pochylenie", + "bunk_sit_x": "Bok", + "bunk_sit_y": "Wysokość", + "bunk_sit_z": "Głębokość", + "bunk_sit_yaw": "Obrót", + "bunk_sit_pitch": "Pochylenie" + } } } } diff --git a/localization/pt_br.json b/localization/pt_br.json index fdf518a..5328e1f 100644 --- a/localization/pt_br.json +++ b/localization/pt_br.json @@ -43,6 +43,32 @@ "blindspot_viewer": { "title": "Visão do ponto cego", "desc": "Inclina-se suavemente para ver atrás da coluna do para-brisa." + }, + "cabin_walk": { + "stand_sit": { + "title": "Levantar / Sentar", + "desc": "Levanta do banco. Toque para sentar no lugar para onde você olha, segure para voltar ao volante." + }, + "forward": { + "title": "Andar para frente", + "desc": "Anda para frente enquanto você está de pé na cabine." + }, + "back": { + "title": "Andar para trás", + "desc": "Anda para trás enquanto você está de pé na cabine." + }, + "left": { + "title": "Passo à esquerda", + "desc": "Dá um passo lateral para a esquerda enquanto você está de pé na cabine." + }, + "right": { + "title": "Passo à direita", + "desc": "Dá um passo lateral para a direita enquanto você está de pé na cabine." + }, + "crouch": { + "title": "Agachar", + "desc": "Agacha quando de pé." + } } }, "settings": { @@ -240,7 +266,7 @@ }, "manual": { "title": "Manual", - "desc": "Efeitos de olhar e zoom que você mesmo aciona.", + "desc": "Efeitos de olhar, zoom e caminhada que você mesmo aciona.", "mirror_check": { "title": "Checagem dos espelhos", "desc": "Olha para o espelho correspondente enquanto uma seta está ligada e volta ao centro quando ela desliga.", @@ -347,6 +373,18 @@ "title": "Modo alternar", "desc": "Ligado: pressione uma vez para se inclinar e de novo para voltar. Desligado: inclina-se enquanto a tecla estiver pressionada, solte para voltar." } + }, + "cabin_walk": { + "title": "Caminhar na cabine", + "desc": "Levante-se do banco do motorista e ande livremente pela cabine com o caminhão estacionado.", + "hint": "Os layouts padrão são ajustados para a menor cabine leito de cada caminhão. Outras cabines podem precisar de ajuste.", + "enabled": { + "title": "Ativar Caminhar na cabine" + }, + "require_parking_brake": { + "title": "Exigir freio de estacionamento", + "desc": "Ativado: exige o freio de estacionamento, soltá-lo leva você de volta. Desativado: basta parar." + } } } }, @@ -431,6 +469,61 @@ "speed_system": "metric", "kmh": "km/h", "mph": "mph" + }, + "cabin_walk": { + "needs_parking_brake": "Acione o freio de estacionamento para sair do banco.", + "needs_stop": "Pare o caminhão para sair do banco.", + "back_to_wheel": "De volta ao volante: o caminhão não está mais estacionado.", + "layout": { + "no_truck": "Entre em um caminhão para configurar a cabine dele.", + "truck": "Caminhão", + "tip": { + "floor_z_min": "Até onde você pode avançar, em direção ao para-brisa.", + "floor_z_max": "Até onde você pode recuar, em direção à parede traseira.", + "floor_x_min": "Até onde você pode ir em direção à porta do motorista.", + "floor_x_max": "Até onde você pode ir em direção à porta do passageiro.", + "stand_y": "A altura dos seus olhos em pé.", + "crouch_depth": "O quanto a cabeça desce ao agachar.", + "passenger_dx": "Para o lado a partir do reflexo do seu banco.", + "passenger_dy": "Mais alto ou mais baixo que o seu banco.", + "passenger_dz": "Mais à frente ou atrás que o seu banco.", + "bunk_sit_x": "Onde você senta ao longo da cama.", + "bunk_sit_y": "A altura dos seus olhos sentado na cama.", + "bunk_sit_z": "Quão para trás fica a borda da cama.", + "yaw": "Para onde você olha depois de sentado.", + "pitch": "Quanto você olha para cima ou para baixo depois de sentado.", + "has_passenger": "Desmarque se o banco do passageiro dobra ou não existe.", + "has_bunk": "Desmarque para uma cabine curta sem cama." + }, + "group": { + "walkway": "Área de circulação", + "heights": "Altura em pé", + "passenger": "Banco do passageiro", + "bunk": "Cama" + }, + "go": "Ir até lá", + "usable": "Utilizável", + "advanced": "Avançado", + "reset": "Redefinir disposição", + "reset_title": "Redefinir disposição da cabine", + "reset_message": "Voltar à disposição padrão para {truck}?", + "reset_confirm": "Sim, redefinir", + "floor_x_min": "Limite esquerdo", + "floor_x_max": "Limite direito", + "floor_z_min": "Limite dianteiro", + "floor_z_max": "Limite traseiro", + "crouch_depth": "Profundidade ao agachar", + "passenger_dx": "Lado", + "passenger_dy": "Altura", + "passenger_dz": "Profundidade", + "passenger_yaw": "Giro", + "passenger_pitch": "Inclinação", + "bunk_sit_x": "Lado", + "bunk_sit_y": "Altura", + "bunk_sit_z": "Profundidade", + "bunk_sit_yaw": "Giro", + "bunk_sit_pitch": "Inclinação" + } } } } diff --git a/localization/ru.json b/localization/ru.json index 468ba59..1b6377e 100644 --- a/localization/ru.json +++ b/localization/ru.json @@ -43,6 +43,32 @@ "blindspot_viewer": { "title": "Обзор слепой зоны", "desc": "Плавно наклоняется, чтобы заглянуть за стойку лобового стекла." + }, + "cabin_walk": { + "stand_sit": { + "title": "Встать / Сесть", + "desc": "Встаёт с сиденья. Нажмите, чтобы сесть на место, куда вы смотрите, удерживайте, чтобы вернуться за руль." + }, + "forward": { + "title": "Идти вперёд", + "desc": "Идёт вперёд, когда вы стоите в кабине." + }, + "back": { + "title": "Идти назад", + "desc": "Идёт назад, когда вы стоите в кабине." + }, + "left": { + "title": "Шаг влево", + "desc": "Делает шаг в сторону влево, когда вы стоите в кабине." + }, + "right": { + "title": "Шаг вправо", + "desc": "Делает шаг в сторону вправо, когда вы стоите в кабине." + }, + "crouch": { + "title": "Присесть", + "desc": "Приседает, когда вы стоите." + } } }, "settings": { @@ -240,7 +266,7 @@ }, "manual": { "title": "Ручное", - "desc": "Эффекты взгляда и приближения, которые вы вызываете сами.", + "desc": "Эффекты обзора, зума и ходьбы, которые вы включаете сами.", "mirror_check": { "title": "Проверка зеркал", "desc": "Смотрит в соответствующее зеркало, пока включён поворотник, и возвращается в центр, когда он выключен.", @@ -347,6 +373,18 @@ "title": "Режим переключения", "desc": "Вкл.: нажмите один раз, чтобы наклониться, и ещё раз, чтобы вернуться. Выкл.: наклоняйтесь, пока клавиша удерживается, отпустите, чтобы вернуться." } + }, + "cabin_walk": { + "title": "Прогулка по кабине", + "desc": "Встаньте с водительского сиденья и свободно ходите по кабине на стоянке.", + "hint": "Стандартные раскладки настроены под самую маленькую спальную кабину каждого грузовика. Другие кабины может понадобиться настроить.", + "enabled": { + "title": "Включить прогулку по кабине" + }, + "require_parking_brake": { + "title": "Требовать стояночный тормоз", + "desc": "Вкл.: нужен стояночный тормоз, его отпускание возвращает вас. Выкл.: достаточно остановиться." + } } } }, @@ -431,6 +469,61 @@ "speed_system": "metric", "kmh": "км/ч", "mph": "миль/ч" + }, + "cabin_walk": { + "needs_parking_brake": "Включите стояночный тормоз, чтобы покинуть сиденье.", + "needs_stop": "Остановите грузовик, чтобы покинуть сиденье.", + "back_to_wheel": "Обратно за руль: грузовик больше не на стоянке.", + "layout": { + "no_truck": "Сядьте в грузовик, чтобы настроить его кабину.", + "truck": "Грузовик", + "tip": { + "floor_z_min": "Как далеко можно пройти вперёд, к лобовому стеклу.", + "floor_z_max": "Как далеко можно пройти назад, к задней стенке.", + "floor_x_min": "Как далеко можно пройти к двери водителя.", + "floor_x_max": "Как далеко можно пройти к двери пассажира.", + "stand_y": "Высота ваших глаз стоя.", + "crouch_depth": "Насколько опускается голова, когда вы приседаете.", + "passenger_dx": "Вбок от зеркального отражения вашего сиденья.", + "passenger_dy": "Выше или ниже вашего сиденья.", + "passenger_dz": "Дальше вперёд или назад, чем ваше сиденье.", + "bunk_sit_x": "Где вы садитесь вдоль спальника.", + "bunk_sit_y": "Высота ваших глаз сидя на спальнике.", + "bunk_sit_z": "Насколько сзади край спальника.", + "yaw": "Куда вы смотрите сидя.", + "pitch": "Насколько вы смотрите вверх или вниз сидя.", + "has_passenger": "Снимите, если пассажирское сиденье складывается или его нет.", + "has_bunk": "Снимите для короткой кабины без спальника." + }, + "group": { + "walkway": "Зона ходьбы", + "heights": "Высота стоя", + "passenger": "Пассажирское сиденье", + "bunk": "Спальник" + }, + "go": "Перейти", + "usable": "Доступно", + "advanced": "Дополнительно", + "reset": "Сбросить планировку", + "reset_title": "Сброс планировки кабины", + "reset_message": "Вернуть планировку по умолчанию для {truck}?", + "reset_confirm": "Да, сбросить", + "floor_x_min": "Левая граница", + "floor_x_max": "Правая граница", + "floor_z_min": "Передняя граница", + "floor_z_max": "Задняя граница", + "crouch_depth": "Глубина приседа", + "passenger_dx": "Вбок", + "passenger_dy": "Высота", + "passenger_dz": "Глубина", + "passenger_yaw": "Поворот", + "passenger_pitch": "Наклон", + "bunk_sit_x": "Вбок", + "bunk_sit_y": "Высота", + "bunk_sit_z": "Глубина", + "bunk_sit_yaw": "Поворот", + "bunk_sit_pitch": "Наклон" + } } } } diff --git a/localization/tr.json b/localization/tr.json index eccc3ec..0ad12dd 100644 --- a/localization/tr.json +++ b/localization/tr.json @@ -43,6 +43,32 @@ "blindspot_viewer": { "title": "Kör nokta görüşü", "desc": "Ön cam direğinin arkasını görmek için yumuşak bir şekilde öne eğilir." + }, + "cabin_walk": { + "stand_sit": { + "title": "Kalk / Otur", + "desc": "Koltuktan kalkar. Baktığın yere oturmak için dokun, direksiyona dönmek için basılı tut." + }, + "forward": { + "title": "İleri Yürü", + "desc": "Kabinde ayaktayken ileri yürür." + }, + "back": { + "title": "Geri Yürü", + "desc": "Kabinde ayaktayken geri yürür." + }, + "left": { + "title": "Sola Adım", + "desc": "Kabinde ayaktayken sola yan adım atar." + }, + "right": { + "title": "Sağa Adım", + "desc": "Kabinde ayaktayken sağa yan adım atar." + }, + "crouch": { + "title": "Çömel", + "desc": "Ayaktayken çömelir." + } } }, "settings": { @@ -240,7 +266,7 @@ }, "manual": { "title": "Manuel", - "desc": "Kendiniz tetiklediğiniz bakış ve yakınlaştırma efektleri.", + "desc": "Kendin tetiklediğin bakış, yakınlaştırma ve yürüme efektleri.", "mirror_check": { "title": "Ayna kontrolü", "desc": "Sinyal açıkken ilgili aynaya bakar ve sinyal kapanınca merkeze döner.", @@ -347,6 +373,18 @@ "title": "Geçiş modu", "desc": "Açık: eğilmek için bir kez, geri dönmek için tekrar basın. Kapalı: basılı tuttuğunuz sürece eğilin, geri dönmek için bırakın." } + }, + "cabin_walk": { + "title": "Kabinde Yürüme", + "desc": "Park halindeyken sürücü koltuğundan kalk ve kabinde serbestçe dolaş.", + "hint": "Varsayılan düzenler her kamyonun en küçük yataklı kabinine göre ayarlanmıştır. Diğer kabinlerin ayarlanması gerekebilir.", + "enabled": { + "title": "Kabinde Yürümeyi Etkinleştir" + }, + "require_parking_brake": { + "title": "El Freni Gereksin", + "desc": "Açık: el freni gerekir, indirmek seni geri gönderir. Kapalı: durmak yeterlidir." + } } } }, @@ -431,6 +469,61 @@ "speed_system": "metric", "kmh": "km/sa", "mph": "mil/sa" + }, + "cabin_walk": { + "needs_parking_brake": "Koltuktan kalkmak için el frenini çek.", + "needs_stop": "Koltuktan kalkmak için tırı durdur.", + "back_to_wheel": "Direksiyona dönüldü: tır artık park halinde değil.", + "layout": { + "no_truck": "Kabinini ayarlamak için bir tıra bin.", + "truck": "Kamyon", + "tip": { + "floor_z_min": "Ne kadar ileri yürüyebileceğin, ön cama doğru.", + "floor_z_max": "Ne kadar geri yürüyebileceğin, arka duvara doğru.", + "floor_x_min": "Sürücü kapısına doğru ne kadar gidebileceğin.", + "floor_x_max": "Yolcu kapısına doğru ne kadar gidebileceğin.", + "stand_y": "Ayaktayken göz yüksekliğin.", + "crouch_depth": "Çömeldiğinde başının ne kadar alçaldığı.", + "passenger_dx": "Koltuğunun yansımasından yana doğru.", + "passenger_dy": "Koltuğundan daha yukarı veya aşağı.", + "passenger_dz": "Koltuğundan daha ileri veya geri.", + "bunk_sit_x": "Yatak boyunca nereye oturduğun.", + "bunk_sit_y": "Yatakta otururken göz yüksekliğin.", + "bunk_sit_z": "Yatak kenarının ne kadar geride olduğu.", + "yaw": "Oturunca nereye baktığın.", + "pitch": "Oturunca ne kadar yukarı veya aşağı baktığın.", + "has_passenger": "Yolcu koltuğu katlanıyorsa veya yoksa işareti kaldır.", + "has_bunk": "Yataksız kısa kabin için işareti kaldır." + }, + "group": { + "walkway": "Yürüme Alanı", + "heights": "Ayakta Yükseklik", + "passenger": "Yolcu Koltuğu", + "bunk": "Yatak" + }, + "go": "Oraya git", + "usable": "Kullanılabilir", + "advanced": "Gelişmiş", + "reset": "Düzeni Sıfırla", + "reset_title": "Kabin Düzenini Sıfırla", + "reset_message": "{truck} için varsayılan düzene dönülsün mü?", + "reset_confirm": "Evet, Sıfırla", + "floor_x_min": "Sol Sınır", + "floor_x_max": "Sağ Sınır", + "floor_z_min": "Ön Sınır", + "floor_z_max": "Arka Sınır", + "crouch_depth": "Çömelme Derinliği", + "passenger_dx": "Yan", + "passenger_dy": "Yükseklik", + "passenger_dz": "Derinlik", + "passenger_yaw": "Dönüş", + "passenger_pitch": "Eğim", + "bunk_sit_x": "Yan", + "bunk_sit_y": "Yükseklik", + "bunk_sit_z": "Derinlik", + "bunk_sit_yaw": "Dönüş", + "bunk_sit_pitch": "Eğim" + } } } } diff --git a/localization/uk.json b/localization/uk.json index 71c1df3..84997b5 100644 --- a/localization/uk.json +++ b/localization/uk.json @@ -43,6 +43,32 @@ "blindspot_viewer": { "title": "Огляд сліпої зони", "desc": "Плавно нахиляється, щоб зазирнути за стійку лобового скла." + }, + "cabin_walk": { + "stand_sit": { + "title": "Встати / Сісти", + "desc": "Встає з сидіння. Натисніть, щоб сісти на місце, куди ви дивитеся, утримуйте, щоб повернутися за кермо." + }, + "forward": { + "title": "Іти вперед", + "desc": "Іде вперед, коли ви стоїте в кабіні." + }, + "back": { + "title": "Іти назад", + "desc": "Іде назад, коли ви стоїте в кабіні." + }, + "left": { + "title": "Крок ліворуч", + "desc": "Робить крок убік ліворуч, коли ви стоїте в кабіні." + }, + "right": { + "title": "Крок праворуч", + "desc": "Робить крок убік праворуч, коли ви стоїте в кабіні." + }, + "crouch": { + "title": "Присісти", + "desc": "Присідає, коли ви стоїте." + } } }, "settings": { @@ -240,7 +266,7 @@ }, "manual": { "title": "Ручне", - "desc": "Ефекти погляду та наближення, які ви викликаєте самі.", + "desc": "Ефекти огляду, зуму та ходьби, які ви вмикаєте самі.", "mirror_check": { "title": "Перевірка дзеркал", "desc": "Дивиться у відповідне дзеркало, поки увімкнено покажчик повороту, і повертається в центр, коли його вимкнено.", @@ -347,6 +373,18 @@ "title": "Режим перемикання", "desc": "Увімк.: натисніть один раз, щоб нахилитися, і ще раз, щоб повернутися. Вимк.: нахиляйтеся, поки клавішу утримано, відпустіть, щоб повернутися." } + }, + "cabin_walk": { + "title": "Прогулянка кабіною", + "desc": "Підведіться з водійського сидіння та вільно ходіть кабіною на стоянці.", + "hint": "Стандартні розкладки налаштовано під найменшу спальну кабіну кожної вантажівки. Інші кабіни може знадобитися налаштувати.", + "enabled": { + "title": "Увімкнути прогулянку кабіною" + }, + "require_parking_brake": { + "title": "Вимагати стоянкове гальмо", + "desc": "Увімк.: потрібне стоянкове гальмо, його відпускання повертає вас. Вимк.: достатньо зупинитися." + } } } }, @@ -431,6 +469,61 @@ "speed_system": "metric", "kmh": "км/год", "mph": "миль/год" + }, + "cabin_walk": { + "needs_parking_brake": "Увімкніть стоянкове гальмо, щоб покинути сидіння.", + "needs_stop": "Зупиніть вантажівку, щоб покинути сидіння.", + "back_to_wheel": "Назад за кермо: вантажівка більше не на стоянці.", + "layout": { + "no_truck": "Сядьте у вантажівку, щоб налаштувати її кабіну.", + "truck": "Вантажівка", + "tip": { + "floor_z_min": "Як далеко можна пройти вперед, до лобового скла.", + "floor_z_max": "Як далеко можна пройти назад, до задньої стінки.", + "floor_x_min": "Як далеко можна пройти до дверей водія.", + "floor_x_max": "Як далеко можна пройти до дверей пасажира.", + "stand_y": "Висота ваших очей стоячи.", + "crouch_depth": "Наскільки опускається голова, коли ви присідаєте.", + "passenger_dx": "Убік від дзеркального відображення вашого сидіння.", + "passenger_dy": "Вище або нижче за ваше сидіння.", + "passenger_dz": "Далі вперед або назад, ніж ваше сидіння.", + "bunk_sit_x": "Де ви сідаєте вздовж спальника.", + "bunk_sit_y": "Висота ваших очей сидячи на спальнику.", + "bunk_sit_z": "Наскільки позаду край спальника.", + "yaw": "Куди ви дивитеся сидячи.", + "pitch": "Наскільки ви дивитеся вгору чи вниз сидячи.", + "has_passenger": "Зніміть, якщо пасажирське сидіння складається або його немає.", + "has_bunk": "Зніміть для короткої кабіни без спальника." + }, + "group": { + "walkway": "Зона ходьби", + "heights": "Висота стоячи", + "passenger": "Пасажирське сидіння", + "bunk": "Спальник" + }, + "go": "Перейти", + "usable": "Доступно", + "advanced": "Додатково", + "reset": "Скинути планування", + "reset_title": "Скидання планування кабіни", + "reset_message": "Повернути планування за замовчуванням для {truck}?", + "reset_confirm": "Так, скинути", + "floor_x_min": "Ліва межа", + "floor_x_max": "Права межа", + "floor_z_min": "Передня межа", + "floor_z_max": "Задня межа", + "crouch_depth": "Глибина присідання", + "passenger_dx": "Вбік", + "passenger_dy": "Висота", + "passenger_dz": "Глибина", + "passenger_yaw": "Поворот", + "passenger_pitch": "Нахил", + "bunk_sit_x": "Вбік", + "bunk_sit_y": "Висота", + "bunk_sit_z": "Глибина", + "bunk_sit_yaw": "Поворот", + "bunk_sit_pitch": "Нахил" + } } } } diff --git a/localization/zh.json b/localization/zh.json index 4eef28d..bb86cf0 100644 --- a/localization/zh.json +++ b/localization/zh.json @@ -43,6 +43,32 @@ "blindspot_viewer": { "title": "盲区视野", "desc": "平滑地身体前倾,看清挡风玻璃立柱后方。" + }, + "cabin_walk": { + "stand_sit": { + "title": "起身 / 坐下", + "desc": "从座位起身。点按坐到你看着的座位,按住回到方向盘前。" + }, + "forward": { + "title": "向前走", + "desc": "在驾驶室内站立时向前走。" + }, + "back": { + "title": "向后走", + "desc": "在驾驶室内站立时向后走。" + }, + "left": { + "title": "向左迈步", + "desc": "在驾驶室内站立时向左横移一步。" + }, + "right": { + "title": "向右迈步", + "desc": "在驾驶室内站立时向右横移一步。" + }, + "crouch": { + "title": "下蹲", + "desc": "站立时下蹲。" + } } }, "settings": { @@ -240,7 +266,7 @@ }, "manual": { "title": "手动", - "desc": "由你自己触发的观察和缩放效果。", + "desc": "由你亲自触发的观察、缩放和行走效果。", "mirror_check": { "title": "查看后视镜", "desc": "转向灯开启时看向对应的后视镜,关闭后回到中间。", @@ -347,6 +373,18 @@ "title": "切换模式", "desc": "开启:按一次前倾,再按一次回位。关闭:按住时前倾,松开后回位。" } + }, + "cabin_walk": { + "title": "驾驶室漫步", + "desc": "停车时离开驾驶座,在驾驶室内自由走动。", + "hint": "默认布局按每辆卡车最小的卧铺驾驶室设定。其他驾驶室可能需要调整。", + "enabled": { + "title": "启用驾驶室漫步" + }, + "require_parking_brake": { + "title": "需要驻车制动", + "desc": "开启:需要拉上驻车制动,松开会让你回去。关闭:停车即可。" + } } } }, @@ -431,6 +469,61 @@ "speed_system": "metric", "kmh": "公里/小时", "mph": "英里/小时" + }, + "cabin_walk": { + "needs_parking_brake": "拉上驻车制动才能离开座位。", + "needs_stop": "停下卡车才能离开座位。", + "back_to_wheel": "已回到方向盘前:卡车不再处于停车状态。", + "layout": { + "no_truck": "上一辆卡车来设置它的驾驶室。", + "truck": "卡车", + "tip": { + "floor_z_min": "你能向前走多远,朝向挡风玻璃。", + "floor_z_max": "你能向后走多远,朝向后壁。", + "floor_x_min": "你能朝驾驶员车门走多远。", + "floor_x_max": "你能朝乘客车门走多远。", + "stand_y": "站立时的眼睛高度。", + "crouch_depth": "下蹲时头部降低的距离。", + "passenger_dx": "相对于你座椅的镜像位置横向偏移。", + "passenger_dy": "比你的座椅高或低。", + "passenger_dz": "比你的座椅靠前或靠后。", + "bunk_sit_x": "你沿卧铺坐的位置。", + "bunk_sit_y": "坐在卧铺上时的眼睛高度。", + "bunk_sit_z": "卧铺边缘在多靠后的位置。", + "yaw": "坐下后看向的方向。", + "pitch": "坐下后向上或向下看的程度。", + "has_passenger": "如果副驾驶座可折叠或不存在,请取消勾选。", + "has_bunk": "对于没有卧铺的短驾驶室,请取消勾选。" + }, + "group": { + "walkway": "行走区域", + "heights": "站立高度", + "passenger": "副驾驶座", + "bunk": "卧铺" + }, + "go": "前往", + "usable": "可用", + "advanced": "高级", + "reset": "重置布局", + "reset_title": "重置驾驶室布局", + "reset_message": "将 {truck} 恢复为默认布局?", + "reset_confirm": "是,重置", + "floor_x_min": "左边界", + "floor_x_max": "右边界", + "floor_z_min": "前边界", + "floor_z_max": "后边界", + "crouch_depth": "下蹲深度", + "passenger_dx": "左右", + "passenger_dy": "高度", + "passenger_dz": "前后", + "passenger_yaw": "转向", + "passenger_pitch": "倾斜", + "bunk_sit_x": "左右", + "bunk_sit_y": "高度", + "bunk_sit_z": "前后", + "bunk_sit_yaw": "转向", + "bunk_sit_pitch": "倾斜" + } } } } diff --git a/src/Plugin.cpp b/src/Plugin.cpp index 42fc3b3..fadd08f 100644 --- a/src/Plugin.cpp +++ b/src/Plugin.cpp @@ -22,12 +22,15 @@ #include "effects/manual/ManualLookEffect.hpp" #include "effects/manual/ManualZoomEffect.hpp" #include "effects/manual/MirrorCheckEffect.hpp" +#include "effects/manual/cabin_walk/CabinWalkEffect.hpp" #include "effects/road/RoadIrregularityEffect.hpp" #include "effects/road/SpeedShakeEffect.hpp" #include "effects/road/SuspensionEffect.hpp" #include "ui/SettingsWindow.hpp" +#include "ui/Widgets.hpp" #include +#include #include #include #include @@ -53,13 +56,25 @@ void OnControlsUpdate(const SPF_Controls *data, void * /*user_data*/) { void OnTruckConstantsUpdate(const SPF_TruckConstants *data, void * /*user_data*/) { - Context().effects.NotifyTruckConstants(*data); + PluginContext &ctx = Context(); + // Another truck (or none, in between two): the game rebuilds the interior + // camera with the player's own pose, which holds none of our offset. + const std::string truck = std::string(data->brand_id) + "/" + data->id; + if (truck != ctx.truck_identity) { + ctx.truck_identity = truck; + ctx.camera_rig.ForgetAppliedPose(); + ctx.ResetEffects(); + } + ctx.effects.NotifyTruckConstants(*data); + if (ctx.cabin_walk) { + ctx.cabin_walk->OnTruckConstantsChanged(*data); + } - // Don't call Cam_SwitchTo here (even deferred): SPF-Framework's native - // camera hooks already refresh the pointer via OnActivate/OnDeactivate - // during truck reconfiguration, and the interior pos/rot getters are - // null-safe meanwhile. Cam_SwitchTo bypasses that guard and can crash - // during slow Quick Job reloads (750ms+), so no fixed delay is safe. + // Don't call Cam_SwitchTo here (even deferred) to get SPF onto the new + // truck's camera: it can crash during slow Quick Job reloads (750ms+), + // so no fixed delay is safe. Picking up the rebuilt camera is SPF's job: + // older releases only did it on a view change, so a truck switch in the + // interior view left them writing to the freed camera. } void OnCommonDataUpdate(const SPF_CommonData *data, void * /*user_data*/) { @@ -76,6 +91,27 @@ void OnTrailersUpdate(const SPF_Trailer *trailers, uint32_t count, // unregistered action stays stuck at 0.0 even with a valid manifest binding. void OnPolledActionTriggered() {} +// Tells the player why Cabin Walk refused to stand up, or sent them back to +// the wheel. +void ShowCabinWalkNotice(PluginContext &ctx) { + const char *key = nullptr; + switch (ctx.cabin_walk->TakeNotice()) { + case CabinWalkEffect::Notice::kNeedsParkingBrake: + key = "ui.cabin_walk.needs_parking_brake"; + break; + case CabinWalkEffect::Notice::kNeedsStop: + key = "ui.cabin_walk.needs_stop"; + break; + case CabinWalkEffect::Notice::kBackToWheel: + key = "ui.cabin_walk.back_to_wheel"; + break; + case CabinWalkEffect::Notice::kNone: + break; + } + if (key && ctx.core->ui) + ui::ShowToast(ctx.core->ui, SPF_NOTIFICATION_WARNING, loc::Tr(key)); +} + float ComputeDeltaTimeSeconds(PluginContext &ctx) { const auto now = std::chrono::steady_clock::now(); if (!ctx.has_last_update_time) { @@ -108,10 +144,15 @@ void OnUnload() { Context().manual_zoom->RestoreFov(); Context().manual_zoom->RestoreDynamicFov(); } + // Give the camera's own limits and the mouse back if the player is up in + // the cabin. Their position is part of the offset below. + PluginContext &ctx = Context(); + if (ctx.cabin_walk) + ctx.cabin_walk->Shutdown(ctx.core ? ctx.core->camera : nullptr); + ctx.cabin_walk_sounds.Shutdown(ctx.core ? ctx.core->sound : nullptr); // Take our offset back out of the interior pose, which the engine keeps // after we're gone. After ManualZoomEffect's RestoreFov above, which // adds the applied FOV offset back on. - PluginContext &ctx = Context(); if (ctx.core && ctx.core->camera) ctx.camera_rig.Remove(ctx.core->camera); // The About tab's logo texture is the one thing this plugin allocates @@ -146,6 +187,16 @@ void OnToggleSettingsWindow() { ctx.core->config->Cfg_SetBool(ctx.config_handle, kKey, new_state); } +// `file` in the plugin's data directory, next to the profiles; empty (the +// file then isn't used) if the data directory isn't available. +std::string DataFilePath(PluginContext &ctx, const char *file) { + const std::string dir = ctx.PluginDataDir(); + if (dir.empty() || !ctx.core->environment->Env_CreatePath( + ctx.environment_handle, dir.c_str())) + return {}; + return dir + "/" + file; +} + void OnActivated(const SPF_Core_API *core_api) { PluginContext &ctx = Context(); ctx.core = core_api; @@ -198,6 +249,14 @@ void OnActivated(const SPF_Core_API *core_api) { ctx.manual_zoom = std::make_unique( core_api->config, ctx.config_handle, core_api->keybinds, ctx.keybinds_handle, &ctx.camera_rig.applied().fov); + // The player's own layouts (the presets are compiled in). + ctx.cabin_layouts = std::make_unique( + core_api->config, DataFilePath(ctx, "cabin_layouts.json")); + if (const std::string dir = ctx.PluginDataDir(); !dir.empty()) + ctx.cabin_walk_sounds.SetDirectory(dir + "/sounds"); + ctx.cabin_walk = std::make_unique( + core_api->config, ctx.config_handle, core_api->keybinds, + ctx.keybinds_handle, core_api->ui, ctx.cabin_layouts.get()); ctx.ReloadEffectsConfig(); ctx.telemetry_handle = @@ -216,6 +275,7 @@ void OnActivated(const SPF_Core_API *core_api) { core_api->telemetry->Tel_GetTruckConstants( ctx.telemetry_handle, &initial_constants, sizeof(initial_constants)); ctx.effects.NotifyTruckConstants(initial_constants); + ctx.cabin_walk->OnTruckConstantsChanged(initial_constants); core_api->telemetry->Tel_RegisterForCommonData(ctx.telemetry_handle, OnCommonDataUpdate, nullptr); core_api->telemetry->Tel_RegisterForTrailers(ctx.telemetry_handle, @@ -281,6 +341,8 @@ void OnUpdate() { if (!is_interior) { if (ctx.was_interior_last_frame && ctx.manual_zoom) ctx.manual_zoom->RestoreFov(); + if (ctx.cabin_walk) + ctx.cabin_walk->OnLeftInterior(); ctx.was_interior_last_frame = false; ctx.has_last_update_time = false; return; @@ -310,19 +372,45 @@ void OnUpdate() { // Differential write (see CameraRig), skipped entirely while the camera // isn't resolved yet. SPF_Camera_API *camera = ctx.core->camera; - const std::optional pose = ctx.camera_rig.ReadPose(camera); + std::optional pose = ctx.camera_rig.ReadPose(camera); if (!pose) return; if (ctx.camera_rig.DetectNativeRecenter(camera, *pose)) ctx.effects.ResetAll(); ctx.camera_rig.DetectExternalSeatWrite(*pose); - const HeadOffset offset = ctx.effects.UpdateAndAccumulate( - dt, ctx.latest_truck_data, ctx.latest_controls_data); + + // Cabin Walk first: whether the player is at the wheel decides which + // effects play, and sitting down turns the player's own view. + HeadOffset walk{}; + if (ctx.cabin_walk) { + walk = ctx.cabin_walk->Update(dt, ctx.latest_truck_data, camera, + ctx.camera_rig.Base(*pose)); + if (const auto &rotation = ctx.cabin_walk->base_rotation()) + ctx.camera_rig.SetBaseRotation(*pose, rotation->first, rotation->second); + ctx.effects.SetAtWheel(ctx.cabin_walk->AtWheel()); + ShowCabinWalkNotice(ctx); + ctx.cabin_walk_sounds.Update(ctx.core->sound); + for (float volume : ctx.cabin_walk->TakeFootsteps()) + ctx.cabin_walk_sounds.PlayFootstep(ctx.core->sound, volume); + } + + HeadOffset offset = ctx.effects.UpdateAndAccumulate(dt, ctx.latest_truck_data, + ctx.latest_controls_data); + offset.pos_x += walk.pos_x; + offset.pos_y += walk.pos_y; + offset.pos_z += walk.pos_z; + offset.yaw += walk.yaw; + offset.pitch += walk.pitch; + offset.roll += walk.roll; + offset.fov += walk.fov; ctx.camera_rig.Apply(camera, *pose, offset); } void OnGameWorldReady() { PluginContext &ctx = Context(); + // A new world starts in the driver's seat, with a new camera. + if (ctx.cabin_walk) + ctx.cabin_walk->SnapToWheel(); ctx.was_interior_last_frame = false; ctx.has_last_update_time = false; ctx.has_truck_data.store(false, std::memory_order_relaxed); diff --git a/src/core/CameraRig.cpp b/src/core/CameraRig.cpp index 8ab2202..385e63a 100644 --- a/src/core/CameraRig.cpp +++ b/src/core/CameraRig.cpp @@ -78,6 +78,26 @@ bool CameraRig::DetectExternalSeatWrite(const Pose &pose) { return any; } +CameraRig::Pose CameraRig::Base(const Pose &pose) const { + return {pose.seat_x - applied_.pos_x, pose.seat_y - applied_.pos_y, + pose.seat_z - applied_.pos_z, pose.yaw_rad - applied_.yaw * kDegToRad, + pose.pitch_rad - applied_.pitch * kDegToRad}; +} + +void CameraRig::SetBaseRotation(Pose &pose, float yaw_rad, + float pitch_rad) const { + pose.yaw_rad = yaw_rad + applied_.yaw * kDegToRad; + pose.pitch_rad = pitch_rad + applied_.pitch * kDegToRad; +} + +void CameraRig::ForgetAppliedPose() { + const float roll = applied_.roll; + applied_ = {}; + applied_.roll = roll; + has_last_written_rot_ = false; + has_last_written_seat_ = false; +} + void CameraRig::Apply(SPF_Camera_API *camera, const Pose &pose, const HeadOffset &offset) { WriteSeat(camera, pose.seat_x, pose.seat_y, pose.seat_z, offset); diff --git a/src/core/CameraRig.hpp b/src/core/CameraRig.hpp index 9d9128f..92454eb 100644 --- a/src/core/CameraRig.hpp +++ b/src/core/CameraRig.hpp @@ -44,6 +44,15 @@ class CameraRig { // offset that's no longer there subtracted from it. bool DetectExternalSeatWrite(const Pose &pose); + // The player's own pose underneath: `pose` minus the applied offset. + Pose Base(const Pose &pose) const; + + // Makes the player's own head rotation under `pose` `yaw_rad`/ + // `pitch_rad`, so the next Apply writes that plus the new offset. For + // turning the player's view itself rather than adding to it (Cabin Walk + // sitting down), with the mouse blocked meanwhile. + void SetBaseRotation(Pose &pose, float yaw_rad, float pitch_rad) const; + // Replaces last frame's offset with `offset` in the live pose. void Apply(SPF_Camera_API *camera, const Pose &pose, const HeadOffset &offset); @@ -54,6 +63,12 @@ class CameraRig { // the seat leaned forward for good. void Remove(SPF_Camera_API *camera); + // The game rebuilt the interior camera (another truck): the new one holds + // the player's own pose, without our offset, so there's nothing to + // subtract any more. Roll is kept: SPF remembers the roll it was given + // and puts it back on the new camera. + void ForgetAppliedPose(); + // The offset currently in the live pose. const HeadOffset &applied() const { return applied_; } diff --git a/src/core/Keybinds.hpp b/src/core/Keybinds.hpp index 826f6f8..6c07a3f 100644 --- a/src/core/Keybinds.hpp +++ b/src/core/Keybinds.hpp @@ -2,6 +2,8 @@ #include "SPF_KeyBinds_API.h" +#include + // MotionCab's keybind actions, declared once for the manifest (group + // name, default key, title/description), the registration in Plugin.cpp // and the effects/UI (full "Group.name" id). @@ -28,15 +30,49 @@ inline constexpr Action kBlindspotPeek{"BlindspotViewer", "peek", "keybinds.blindspot_viewer", "KEY_F10"}; inline constexpr Action kToggleWindow{"UI", "toggle", "UI.toggle", "keybinds.ui_toggle", "KEY_F9"}; +inline constexpr Action kStandSit{ + "CabinWalk", "stand_sit", "CabinWalk.stand_sit", + "keybinds.cabin_walk.stand_sit", "KEY_PAGEUP"}; +// WASD, shared with driving: CabinWalkEffect takes them away from the game +// (Kbind_SetBlockState) only while the player is out of the driver's seat. +inline constexpr Action kWalkForward{"CabinWalk", "forward", + "CabinWalk.forward", + "keybinds.cabin_walk.forward", "KEY_W"}; +inline constexpr Action kWalkBack{"CabinWalk", "back", "CabinWalk.back", + "keybinds.cabin_walk.back", "KEY_S"}; +inline constexpr Action kWalkLeft{"CabinWalk", "left", "CabinWalk.left", + "keybinds.cabin_walk.left", "KEY_A"}; +inline constexpr Action kWalkRight{"CabinWalk", "right", "CabinWalk.right", + "keybinds.cabin_walk.right", "KEY_D"}; +// C rather than Ctrl/Space: SPF never takes a modifier key away from the +// game, and Space releases the parking brake, which sends the player back to +// the wheel. C is the game's cruise control, so it's taken away like WASD. +inline constexpr Action kCrouch{"CabinWalk", "crouch", "CabinWalk.crouch", + "keybinds.cabin_walk.crouch", "KEY_C"}; // Every action, in the order the manifest declares them. -inline constexpr Action kAllActions[] = {kLookLeft, kLookRight, kZoom, - kBlindspotPeek, kToggleWindow}; +inline constexpr Action kAllActions[] = { + kLookLeft, kLookRight, kZoom, kBlindspotPeek, + kToggleWindow, kStandSit, kWalkForward, kWalkBack, + kWalkLeft, kWalkRight, kCrouch}; // Actions the effects poll every frame via IsHeld() instead of reacting to // a Kbind_Register callback. -inline constexpr Action kPolledActions[] = {kLookLeft, kLookRight, kZoom, - kBlindspotPeek}; +inline constexpr Action kPolledActions[] = { + kLookLeft, kLookRight, kZoom, kBlindspotPeek, kStandSit, + kWalkForward, kWalkBack, kWalkLeft, kWalkRight, kCrouch}; + +// Actions on keys the game drives with: declared with the "manual" consume +// policy, so CabinWalkEffect decides when the game stops seeing them. +inline constexpr Action kWalkActions[] = {kWalkForward, kWalkBack, kWalkLeft, + kWalkRight, kCrouch}; + +inline bool IsWalkAction(const Action &action) { + for (const Action &walk : kWalkActions) + if (std::strcmp(walk.id, action.id) == 0) + return true; + return false; +} // Immediate physical state of the action's key, ignoring any hold/toggle // behavior configured for the binding itself: MotionCab implements its own. @@ -45,6 +81,14 @@ inline bool IsHeld(SPF_KeyBinds_API *api, SPF_KeyBinds_Handle *handle, return api->Kbind_GetActionValue(handle, action.id) > 0.5f; } +// The action's value in [0, 1]: 0/1 for a key, the deflection for an +// analog axis bound to it. +inline float Amount(SPF_KeyBinds_API *api, SPF_KeyBinds_Handle *handle, + const Action &action) { + const float value = api->Kbind_GetActionValue(handle, action.id); + return value < 0.0f ? 0.0f : (value > 1.0f ? 1.0f : value); +} + // Turns a held state into a one-frame "just pressed" edge. class PressEdge { public: diff --git a/src/core/Manifest.cpp b/src/core/Manifest.cpp index 429f268..eef3892 100644 --- a/src/core/Manifest.cpp +++ b/src/core/Manifest.cpp @@ -86,8 +86,9 @@ void BuildManifest(SPF_Manifest_Builder_Handle *h, api->Defaults_SetLogging(h, "info", true); api->Defaults_SetLocalization(h, "en"); for (const keybinds::Action &action : keybinds::kAllActions) - api->Defaults_AddKeybind(h, action.group, action.name, "keyboard", - action.default_key, "always"); + api->Defaults_AddKeybind( + h, action.group, action.name, "keyboard", action.default_key, + keybinds::IsWalkAction(action) ? "manual" : "always"); // isVisible=true here only decides the very first launch ever api->Defaults_AddWindow(h, "MotionCab", true, true, 100, 100, kWindowWidth, kWindowHeight, false, true); diff --git a/src/core/PluginContext.cpp b/src/core/PluginContext.cpp index a3f6df8..27a1723 100644 --- a/src/core/PluginContext.cpp +++ b/src/core/PluginContext.cpp @@ -12,6 +12,8 @@ void PluginContext::ReloadEffectsConfig() { effects.LoadAllConfig(); if (manual_zoom) manual_zoom->LoadConfig(); + if (cabin_walk) + cabin_walk->LoadConfig(); } void PluginContext::ResetEffects() { diff --git a/src/core/PluginContext.hpp b/src/core/PluginContext.hpp index 1168de9..d6f83e6 100644 --- a/src/core/PluginContext.hpp +++ b/src/core/PluginContext.hpp @@ -12,6 +12,9 @@ #include "core/CameraRig.hpp" #include "effects/EffectManager.hpp" #include "effects/manual/ManualZoomEffect.hpp" +#include "effects/manual/cabin_walk/CabinLayouts.hpp" +#include "effects/manual/cabin_walk/CabinWalkEffect.hpp" +#include "effects/manual/cabin_walk/CabinWalkSounds.hpp" #include #include @@ -35,6 +38,10 @@ struct PluginContext { EffectManager effects; std::unique_ptr manual_zoom; + // Held apart like manual_zoom, see its class comment. + std::unique_ptr cabin_layouts; + std::unique_ptr cabin_walk; + CabinWalkSounds cabin_walk_sounds; // Latest truck telemetry snapshot, refreshed via Tel_RegisterForTruckData. SPF_TruckData latest_truck_data{}; @@ -49,16 +56,22 @@ struct PluginContext { // Writes the effects' offset into the interior camera (differential // write) and remembers what it applied. CameraRig camera_rig; + // "/" of the last truck telemetry described, to tell a + // truck switch from a change to the same truck. + std::string truck_identity; bool was_interior_last_frame = false; std::chrono::steady_clock::time_point last_update_time{}; bool has_last_update_time = false; - // Re-reads every effect's settings, ManualZoomEffect's included. + // Re-reads every effect's settings, ManualZoomEffect's and + // CabinWalkEffect's included. void ReloadEffectsConfig(); // Clears every effect's smoothing state (settling their fades, see - // EffectManager::ResetAll), ManualZoomEffect's included. + // EffectManager::ResetAll), ManualZoomEffect's included. Not Cabin + // Walk's: a player up in the cabin stays there across a view switch or a + // recenter. void ResetEffects(); // The plugin's data directory (Env_GetPluginDataDir), or empty if the diff --git a/src/core/SettingsSchema.hpp b/src/core/SettingsSchema.hpp index ac2a607..5387684 100644 --- a/src/core/SettingsSchema.hpp +++ b/src/core/SettingsSchema.hpp @@ -154,6 +154,9 @@ inline constexpr Setting kAll[] = { Float("settings.manual.blindspot_viewer.smoothing_time", 0.6f, 0.2f, 1.5f, "%.2f s"), Bool("settings.manual.blindspot_viewer.toggle_mode", true), + // manual.cabin_walk + Bool("settings.manual.cabin_walk.enabled", true), + Bool("settings.manual.cabin_walk.require_parking_brake", true), }; // "settings..." split into its parts. diff --git a/src/effects/Effect.hpp b/src/effects/Effect.hpp index 88081d2..e0cb53d 100644 --- a/src/effects/Effect.hpp +++ b/src/effects/Effect.hpp @@ -40,6 +40,12 @@ class Effect { virtual HeadOffset Update(float dt, const SPF_TruckData &truck, const SPF_Controls &controls) = 0; + // True for the effects that only make sense in the driver's seat (driving + // motion, looking at the mirrors, road motion, ...): they fade out while + // Cabin Walk takes the player elsewhere in the cabin + // (EffectManager::SetAtWheel). + virtual bool NeedsDriverSeat() const { return false; } + // Called whenever the truck's static configuration changes (bought a new // truck, added/removed axles). Default no-op; only effects that need // per-wheel layout (e.g. front/rear classification) override this. diff --git a/src/effects/EffectManager.cpp b/src/effects/EffectManager.cpp index 76bdd7c..51944e6 100644 --- a/src/effects/EffectManager.cpp +++ b/src/effects/EffectManager.cpp @@ -19,16 +19,31 @@ void EffectManager::ResetAll() { for (auto &slot : effects_) { slot.effect->Reset(); slot.fade = slot.effect->IsEnabled() ? 1.0f : 0.0f; + slot.seat_reset = false; } + seat_fade_ = at_wheel_ ? 1.0f : 0.0f; } HeadOffset EffectManager::UpdateAndAccumulate(float dt, const SPF_TruckData &truck, const SPF_Controls &controls) { const float step = dt / kFadeSeconds; + seat_fade_ = at_wheel_ ? std::min(1.0f, seat_fade_ + step) + : std::max(0.0f, seat_fade_ - step); HeadOffset total; for (auto &slot : effects_) { Effect &effect = *slot.effect; + float seat_weight = 1.0f; + if (effect.NeedsDriverSeat()) { + if (seat_fade_ <= 0.0f) { + if (!slot.seat_reset) + effect.Reset(); // start from rest back in the seat + slot.seat_reset = true; + continue; + } + slot.seat_reset = false; + seat_weight = math::SmoothStep(seat_fade_); + } if (effect.IsEnabled()) { slot.fade = std::min(1.0f, slot.fade + step); } else { @@ -41,7 +56,7 @@ HeadOffset EffectManager::UpdateAndAccumulate(float dt, } } const HeadOffset contribution = effect.Update(dt, truck, controls); - const float weight = math::SmoothStep(slot.fade); + const float weight = math::SmoothStep(slot.fade) * seat_weight; total.pos_x += contribution.pos_x * weight; total.pos_y += contribution.pos_y * weight; total.pos_z += contribution.pos_z * weight; diff --git a/src/effects/EffectManager.hpp b/src/effects/EffectManager.hpp index 7911889..c30fef8 100644 --- a/src/effects/EffectManager.hpp +++ b/src/effects/EffectManager.hpp @@ -24,6 +24,11 @@ class EffectManager { // disabled ones), since the camera is being resynced anyway. void ResetAll(); + // Whether the player is in the driver's seat. Effects that need it + // (Effect::NeedsDriverSeat) fade out over kFadeSeconds when they leave + // it, and are Reset() once faded out, so they come back from rest. + void SetAtWheel(bool at_wheel) { at_wheel_ = at_wheel; } + // Advances every enabled (or still fading out) effect and returns the // summed, fade-weighted offset to apply. HeadOffset UpdateAndAccumulate(float dt, const SPF_TruckData &truck, @@ -45,9 +50,12 @@ class EffectManager { struct Slot { std::unique_ptr effect; - float fade = 1.0f; // 0 = contributes nothing, 1 = full contribution + float fade = 1.0f; // 0 = contributes nothing, 1 = full contribution + bool seat_reset = false; // Reset() since the driver's seat was left }; std::vector effects_; + bool at_wheel_ = true; + float seat_fade_ = 1.0f; // same as Slot::fade, for NeedsDriverSeat effects }; } // namespace motioncab diff --git a/src/effects/driving/BodyDynamicsEffect.hpp b/src/effects/driving/BodyDynamicsEffect.hpp index 7273897..ee0471f 100644 --- a/src/effects/driving/BodyDynamicsEffect.hpp +++ b/src/effects/driving/BodyDynamicsEffect.hpp @@ -22,6 +22,7 @@ class BodyDynamicsEffect final : public ConfigurableEffect { "body_dynamics") {} void Reset() override; + bool NeedsDriverSeat() const override { return true; } HeadOffset Update(float dt, const SPF_TruckData &truck, const SPF_Controls &controls) override; diff --git a/src/effects/driving/HeadMotionEffect.hpp b/src/effects/driving/HeadMotionEffect.hpp index 0ebd444..1b144c2 100644 --- a/src/effects/driving/HeadMotionEffect.hpp +++ b/src/effects/driving/HeadMotionEffect.hpp @@ -20,6 +20,7 @@ class HeadMotionEffect final : public ConfigurableEffect { "head_motion") {} void Reset() override; + bool NeedsDriverSeat() const override { return true; } HeadOffset Update(float dt, const SPF_TruckData &truck, const SPF_Controls &controls) override; diff --git a/src/effects/driving/SteeringCameraEffect.hpp b/src/effects/driving/SteeringCameraEffect.hpp index 6d62a47..8fb012a 100644 --- a/src/effects/driving/SteeringCameraEffect.hpp +++ b/src/effects/driving/SteeringCameraEffect.hpp @@ -39,6 +39,7 @@ class SteeringCameraEffect final : public ConfigurableEffect { "steering_camera") {} void Reset() override; + bool NeedsDriverSeat() const override { return true; } HeadOffset Update(float dt, const SPF_TruckData &truck, const SPF_Controls &controls) override; diff --git a/src/effects/manual/BlindspotViewerEffect.hpp b/src/effects/manual/BlindspotViewerEffect.hpp index 573bf31..9695c4e 100644 --- a/src/effects/manual/BlindspotViewerEffect.hpp +++ b/src/effects/manual/BlindspotViewerEffect.hpp @@ -32,6 +32,7 @@ class BlindspotViewerEffect final : public ConfigurableEffect { keybinds_api_(keybinds_api), keybinds_handle_(keybinds_handle) {} void Reset() override; + bool NeedsDriverSeat() const override { return true; } HeadOffset Update(float dt, const SPF_TruckData &truck, const SPF_Controls &controls) override; diff --git a/src/effects/manual/ManualLookEffect.hpp b/src/effects/manual/ManualLookEffect.hpp index 5f7543c..4c3466e 100644 --- a/src/effects/manual/ManualLookEffect.hpp +++ b/src/effects/manual/ManualLookEffect.hpp @@ -27,6 +27,7 @@ class ManualLookEffect final : public ConfigurableEffect { keybinds_api_(keybinds_api), keybinds_handle_(keybinds_handle) {} void Reset() override; + bool NeedsDriverSeat() const override { return true; } HeadOffset Update(float dt, const SPF_TruckData &truck, const SPF_Controls &controls) override; diff --git a/src/effects/manual/MirrorCheckEffect.hpp b/src/effects/manual/MirrorCheckEffect.hpp index cc21085..61b2cf7 100644 --- a/src/effects/manual/MirrorCheckEffect.hpp +++ b/src/effects/manual/MirrorCheckEffect.hpp @@ -27,6 +27,7 @@ class MirrorCheckEffect final : public ConfigurableEffect { "mirror_check") {} void Reset() override; + bool NeedsDriverSeat() const override { return true; } HeadOffset Update(float dt, const SPF_TruckData &truck, const SPF_Controls &controls) override; diff --git a/src/effects/manual/cabin_walk/CabinLayouts.cpp b/src/effects/manual/cabin_walk/CabinLayouts.cpp new file mode 100644 index 0000000..3814229 --- /dev/null +++ b/src/effects/manual/cabin_walk/CabinLayouts.cpp @@ -0,0 +1,174 @@ +#include "CabinLayouts.hpp" + +#include "CabinPresets.hpp" + +#include +#include +#include +#include +#include +#include + +namespace motioncab { + +namespace { + +using L = CabinLayout; +// The ranges the sliders allow, meters or degrees. +constexpr CabinLayoutField kFields[] = { + {"floor_x_min", &L::floor_x_min, -1.5f, 2.0f, false}, + {"floor_x_max", &L::floor_x_max, -1.5f, 2.0f, false}, + {"floor_z_min", &L::floor_z_min, -1.5f, 2.5f, false}, + {"floor_z_max", &L::floor_z_max, -1.5f, 2.5f, false}, + {"stand_y", &L::stand_y, -0.5f, 1.0f, false}, + {"crouch_depth", &L::crouch_depth, 0.1f, 0.8f, false}, + {"passenger_dx", &L::passenger_dx, -0.4f, 0.4f, false}, + {"passenger_dy", &L::passenger_dy, -0.4f, 0.4f, false}, + {"passenger_dz", &L::passenger_dz, -0.4f, 0.4f, false}, + {"passenger_yaw", &L::passenger_yaw, -180.0f, 180.0f, true}, + {"passenger_pitch", &L::passenger_pitch, -80.0f, 80.0f, true}, + {"bunk_sit_x", &L::bunk_sit_x, -1.5f, 2.0f, false}, + {"bunk_sit_y", &L::bunk_sit_y, -1.0f, 1.0f, false}, + {"bunk_sit_z", &L::bunk_sit_z, -1.5f, 2.5f, false}, + {"bunk_sit_yaw", &L::bunk_sit_yaw, -180.0f, 180.0f, true}, + {"bunk_sit_pitch", &L::bunk_sit_pitch, -80.0f, 80.0f, true}, +}; + +std::string Key(const std::string &truck_key, const char *field) { + return "layouts." + truck_key + "." + field; +} + +} // namespace + +float CabinCenterlineX(const SPF_TruckConstants &constants) { + const float head_x = constants.head_position.x; + // Whichever side the driver sits on: the camera frame of a right-hand + // drive truck is mirrored (see CabinLayout). + return std::fabs(head_x) >= kMinHeadOffsetX ? std::fabs(head_x) + : kTypicalCenterlineX; +} + +CabinLayout DefaultCabinLayout(float centerline_x) { + const float c = centerline_x; + constexpr float kWalkwayHalfWidth = 0.2f; + CabinLayout l{}; + l.floor_x_min = c - kWalkwayHalfWidth; + l.floor_x_max = c + kWalkwayHalfWidth; + l.floor_z_min = -0.5f; + l.floor_z_max = 0.6f; + l.stand_y = 0.2f; + l.crouch_depth = 0.45f; + l.passenger_dx = 0.0f; + l.passenger_dy = 0.0f; + l.passenger_dz = 0.0f; + l.passenger_yaw = 0.0f; + l.passenger_pitch = 0.0f; + l.bunk_sit_x = c; + l.bunk_sit_y = 0.0f; + l.bunk_sit_z = 0.8f; + l.bunk_sit_yaw = 0.0f; + l.bunk_sit_pitch = 0.0f; + return l; +} + +std::span CabinLayoutFields() { return kFields; } + +CabinLayout WithMirroredYaws(const CabinLayout &layout) { + CabinLayout m = layout; + m.passenger_yaw = -layout.passenger_yaw; + m.bunk_sit_yaw = -layout.bunk_sit_yaw; + return m; +} + +void NormalizeCabinLayout(CabinLayout &layout) { + if (layout.floor_x_min > layout.floor_x_max) + std::swap(layout.floor_x_min, layout.floor_x_max); + if (layout.floor_z_min > layout.floor_z_max) + std::swap(layout.floor_z_min, layout.floor_z_max); +} + +std::string TruckLayoutKey(const SPF_TruckConstants &constants) { + auto clean = [](const char *s) { + std::string out; + for (; *s; ++s) + out += std::isalnum(static_cast(*s)) ? *s : '_'; + return out; + }; + const std::string brand = clean(constants.brand_id); + const std::string model = clean(constants.id); + if (brand.empty() && model.empty()) + return {}; + return brand + "_" + model; +} + +SPF_Config_Handle *CabinLayoutStore::Handle(bool create) { + if (handle_ || !config_ || path_.empty()) + return handle_; + // Opening a custom context creates a missing file: only once there's a + // layout to write. + std::error_code ec; + if (create || std::filesystem::exists(path_, ec)) + handle_ = config_->Cfg_CreateCustomContext(path_.c_str()); + return handle_; +} + +std::optional +CabinLayoutStore::Read(SPF_Config_Handle *h, + const std::string &truck_key) const { + // A layout always has its walkway (Save writes every field); + // an emptied one (see Forget) doesn't. + if (!h || !config_->Cfg_HasKey(h, Key(truck_key, "floor_x_min").c_str())) + return std::nullopt; + CabinLayout layout = DefaultCabinLayout(); + for (const CabinLayoutField &f : kFields) + layout.*f.member = static_cast(config_->Cfg_GetFloat( + h, Key(truck_key, f.name).c_str(), layout.*f.member)); + layout.has_passenger = config_->Cfg_GetBool( + h, Key(truck_key, "has_passenger").c_str(), layout.has_passenger); + layout.has_bunk = config_->Cfg_GetBool(h, Key(truck_key, "has_bunk").c_str(), + layout.has_bunk); + NormalizeCabinLayout(layout); + return layout; +} + +std::optional +CabinLayoutStore::LoadCustom(const std::string &truck_key) { + if (truck_key.empty()) + return std::nullopt; + return Read(Handle(false), truck_key); +} + +std::optional +CabinLayoutStore::LoadPreset(const std::string &truck_key) { + const CabinLayout *preset = FindCabinPreset(truck_key); + if (!preset) + return std::nullopt; + CabinLayout layout = *preset; + NormalizeCabinLayout(layout); + return layout; +} + +void CabinLayoutStore::Save(const std::string &truck_key, + const CabinLayout &layout) { + SPF_Config_Handle *h = truck_key.empty() ? nullptr : Handle(true); + if (!h) + return; + for (const CabinLayoutField &f : kFields) + config_->Cfg_SetFloat(h, Key(truck_key, f.name).c_str(), layout.*f.member); + config_->Cfg_SetBool(h, Key(truck_key, "has_passenger").c_str(), + layout.has_passenger); + config_->Cfg_SetBool(h, Key(truck_key, "has_bunk").c_str(), layout.has_bunk); + config_->Cfg_Save(h); +} + +void CabinLayoutStore::Forget(const std::string &truck_key) { + SPF_Config_Handle *h = truck_key.empty() ? nullptr : Handle(false); + if (!h) + return; + // Emptied rather than removed: Cfg_RemoveKey doesn't work on a custom + // context file (see ProfileManager.cpp). An empty layout reads as none. + config_->Cfg_SetJsonString(h, ("layouts." + truck_key).c_str(), "{}"); + config_->Cfg_Save(h); +} + +} // namespace motioncab diff --git a/src/effects/manual/cabin_walk/CabinLayouts.hpp b/src/effects/manual/cabin_walk/CabinLayouts.hpp new file mode 100644 index 0000000..0efab52 --- /dev/null +++ b/src/effects/manual/cabin_walk/CabinLayouts.hpp @@ -0,0 +1,125 @@ +#pragma once + +#include "SPF_Config_API.h" +#include "SPF_TelemetryData.h" + +#include +#include +#include +#include + +namespace motioncab { + +// Where Cabin Walk can take the player in one truck's cabin. The game has +// no cabin geometry to read, so it's calibrated in-game per truck model +// (the Quick Settings' Advanced part) and falls back to a generic layout +// until then. +// +// Every position is in the interior camera's seat-position frame, as +// Cam_GetInteriorSeatPos reads it: meters, x right, y up, z backward. +// Angles are degrees, as the head rotation: yaw positive to the left, +// pitch positive up. A right-hand drive truck's interior camera mirrors +// the seat position left to right (seen in game: moving to -x there walks +// out of the driver's door), but not the yaw. So in that frame the cabin's +// centerline is at +x on every truck, angles keep their meaning, and a +// left-hand drive layout fits a right-hand drive truck of the same model +// as it is. +struct CabinLayout { + // The area the standing head can walk over. + float floor_x_min, floor_x_max, floor_z_min, floor_z_max; + float stand_y; // standing head height + float crouch_depth; // how far below it crouching takes the head + // The passenger seat: its position mirrors the player's own seat across + // the centerline (see CabinWalkEffect::SpotAt), following the seat as it's + // adjusted, moved by these offsets (meters: right, up, back), and its + // look. + float passenger_dx, passenger_dy, passenger_dz; + float passenger_yaw, passenger_pitch; + // Sitting on the bunk's front edge, halfway along the mattress. + float bunk_sit_x, bunk_sit_y, bunk_sit_z, bunk_sit_yaw, bunk_sit_pitch; + // False where the passenger seat can't be sat in (folded up, or none), or + // there's no bunk (a day cab): Cabin Walk never takes the player there. + // Not CabinLayoutFields (those are the sliders' floats); stored under + // their own names. + bool has_passenger = true; + bool has_bunk = true; +}; + +// The cabin's centerline, as an x in CabinLayout's frame: the driver's head +// (the frame's origin) sits this far from it, always at a positive x (see +// CabinLayout). SPF_CabinWalk's passenger seat at x = 0.95 puts it at about +// half that on the trucks it was tuned on. +inline constexpr float kTypicalCenterlineX = 0.475f; + +// The truck's centerline from its default head position, which telemetry +// gives relative to the cabin's pivot, on the centerline. A head closer +// than kMinHeadOffsetX to it is taken as missing data (a driver sits well +// off-center), and the typical centerline is used instead. +inline constexpr float kMinHeadOffsetX = 0.1f; +float CabinCenterlineX(const SPF_TruckConstants &constants); + +// A layout for a truck nobody calibrated, built around its centerline: the +// walkway runs down the middle, and the bunk sits across the back. Heights +// and depths are SPF_CabinWalk's defaults, since telemetry says nothing +// about them. +CabinLayout DefaultCabinLayout(float centerline_x = kTypicalCenterlineX); + +// One CabinLayout field: its name (storage key and "ui.cabin_walk.layout. +// " label), its slider's range and its unit. +struct CabinLayoutField { + const char *name; + float CabinLayout::*member; + float min, max; + bool angle; // degrees, else meters +}; + +std::span CabinLayoutFields(); + +// Swaps any min/max pair entered backwards, so the floor stays a valid box. +void NormalizeCabinLayout(CabinLayout &layout); + +// `layout` with its spots' looks mirrored: a left-hand drive layout's looks, +// for a right-hand drive truck (its positions already fit, see above). +CabinLayout WithMirroredYaws(const CabinLayout &layout); + +// True if the truck's driver sits on the right: the default head position, +// relative to the cabin's pivot on its centerline, is right of it. +inline bool IsRightHandDrive(const SPF_TruckConstants &constants) { + return constants.head_position.x > 0.0f; +} + +// "_" of the truck model, reduced to [A-Za-z0-9_] so it +// holds as one config key segment; empty if the truck isn't known yet. +std::string TruckLayoutKey(const SPF_TruckConstants &constants); + +// The calibrated layouts, one per truck model: the player's own, in their +// own JSON file (SPF only gives a plugin one settings.json), created on the +// first save, and the presets compiled into the plugin (CabinPresets.hpp). +class CabinLayoutStore { +public: + CabinLayoutStore(SPF_Config_API *config, std::string path) + : config_(config), path_(std::move(path)) {} + + // Only the preset shipped with the plugin for `truck_key`, if any. + std::optional LoadPreset(const std::string &truck_key); + // Only the player's own layout for `truck_key`, if any. + std::optional LoadCustom(const std::string &truck_key); + // As the player's layout. + void Save(const std::string &truck_key, const CabinLayout &layout); + // Drops the player's layout, back to the preset if any: its entry is + // emptied (Cfg_RemoveKey doesn't work on a custom context file). + void Forget(const std::string &truck_key); + +private: + // The player's file, opened on first use; null while it doesn't exist, + // unless `create` (opening a custom context creates a missing file). + SPF_Config_Handle *Handle(bool create); + std::optional Read(SPF_Config_Handle *h, + const std::string &truck_key) const; + + SPF_Config_API *config_; + std::string path_; + SPF_Config_Handle *handle_ = nullptr; +}; + +} // namespace motioncab diff --git a/src/effects/manual/cabin_walk/CabinPresets.cpp b/src/effects/manual/cabin_walk/CabinPresets.cpp new file mode 100644 index 0000000..a1e949c --- /dev/null +++ b/src/effects/manual/cabin_walk/CabinPresets.cpp @@ -0,0 +1,884 @@ +#include "CabinPresets.hpp" + +#include + +namespace motioncab { + +namespace { + +// The layouts shipped with the plugin, calibrated by hand in game in each +// model's smallest sleeper cabin (see CabinLayout for the frame), every +// field written out: a player's cabin_layouts.json entry has the same +// fields and values. +constexpr CabinPreset kPresets[] = { + // --- Euro Truck Simulator 2 --- + {"daf_vehicle_daf_2021", + {.floor_x_min = 0.47f, + .floor_x_max = 0.96f, + .floor_z_min = -0.32f, + .floor_z_max = 0.0f, + .stand_y = 0.23f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.07f, + .passenger_dz = -0.03f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.72f, + .bunk_sit_y = 0.145f, + .bunk_sit_z = 0.268f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"daf_vehicle_daf_xd", + {.floor_x_min = 0.37f, + .floor_x_max = 0.99f, + .floor_z_min = -0.255f, + .floor_z_max = -0.02f, + .stand_y = 0.309f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.12f, + .passenger_dz = -0.075f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.72f, + .bunk_sit_y = 0.055f, + .bunk_sit_z = 0.281f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"daf_vehicle_daf_xf", + {.floor_x_min = 0.37f, + .floor_x_max = 1.04f, + .floor_z_min = -0.32f, + .floor_z_max = 0.06f, + .stand_y = 0.295f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = 0.0f, + .passenger_dz = -0.015f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.71f, + .bunk_sit_y = 0.064f, + .bunk_sit_z = 0.218f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"daf_vehicle_daf_xf_electric", + {.floor_x_min = 0.37f, + .floor_x_max = 1.01f, + .floor_z_min = -0.33f, + .floor_z_max = 0.0f, + .stand_y = 0.33f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = 0.0f, + .passenger_dz = 0.0f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.72f, + .bunk_sit_y = 0.12f, + .bunk_sit_z = 0.235f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = false, + .has_bunk = true}}, + {"daf_vehicle_daf_xf_euro6", + {.floor_x_min = 0.4f, + .floor_x_max = 1.17f, + .floor_z_min = -0.19f, + .floor_z_max = 0.17f, + .stand_y = 0.23f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = 0.0f, + .passenger_dz = 0.0f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.78f, + .bunk_sit_y = 0.112f, + .bunk_sit_z = 0.42f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"iveco_vehicle_iveco_hiway", + {.floor_x_min = 0.53f, + .floor_x_max = 0.98f, + .floor_z_min = -0.31f, + .floor_z_max = 0.11f, + .stand_y = 0.33f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = 0.0f, + .passenger_dz = -0.13f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.76f, + .bunk_sit_y = 0.18f, + .bunk_sit_z = 0.445f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"iveco_vehicle_iveco_stralis", + {.floor_x_min = 0.37f, + .floor_x_max = 1.11f, + .floor_z_min = -0.29f, + .floor_z_max = -0.01f, + .stand_y = 0.425f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = 0.0f, + .passenger_dz = -0.11f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.75f, + .bunk_sit_y = 0.16f, + .bunk_sit_z = 0.32f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"iveco_vehicle_iveco_sway", + {.floor_x_min = 0.4f, + .floor_x_max = 1.11f, + .floor_z_min = -0.23f, + .floor_z_max = 0.07f, + .stand_y = 0.23f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.08f, + .passenger_dz = 0.0f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.73f, + .bunk_sit_y = 0.03f, + .bunk_sit_z = 0.36f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"man_vehicle_man_tgx", + {.floor_x_min = 0.38f, + .floor_x_max = 1.21f, + .floor_z_min = -0.41f, + .floor_z_max = 0.06f, + .stand_y = 0.244f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.06f, + .passenger_dz = -0.033f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.81f, + .bunk_sit_y = -0.039f, + .bunk_sit_z = 0.236f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"man_vehicle_man_tgx_2020", + {.floor_x_min = 0.37f, + .floor_x_max = 1.04f, + .floor_z_min = -0.33f, + .floor_z_max = 0.04f, + .stand_y = 0.28f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.08f, + .passenger_dz = -0.11f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.74f, + .bunk_sit_y = 0.07f, + .bunk_sit_z = 0.295f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"man_vehicle_man_tgx_euro6", + {.floor_x_min = 0.5f, + .floor_x_max = 1.05f, + .floor_z_min = -0.28f, + .floor_z_max = -0.01f, + .stand_y = 0.23f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.08f, + .passenger_dz = 0.0f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.74f, + .bunk_sit_y = 0.04f, + .bunk_sit_z = 0.321f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"mercedes_vehicle_mercedes_actros", + {.floor_x_min = 0.44f, + .floor_x_max = 1.05f, + .floor_z_min = -0.28f, + .floor_z_max = 0.09f, + .stand_y = 0.28f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.04f, + .passenger_dz = -0.04f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.78f, + .bunk_sit_y = 0.005f, + .bunk_sit_z = 0.415f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"mercedes_vehicle_mercedes_actros2014", + {.floor_x_min = 0.47f, + .floor_x_max = 1.03f, + .floor_z_min = -0.21f, + .floor_z_max = 0.133f, + .stand_y = 0.23f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.04f, + .passenger_dz = 0.0f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.71f, + .bunk_sit_y = 0.02f, + .bunk_sit_z = 0.5f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"renault_vehicle_renault_etech_t", + {.floor_x_min = 0.37f, + .floor_x_max = 1.0f, + .floor_z_min = -0.237f, + .floor_z_max = 0.09f, + .stand_y = 0.38f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = 0.0f, + .passenger_dz = 0.0f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.729f, + .bunk_sit_y = 0.055f, + .bunk_sit_z = 0.27f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"renault_vehicle_renault_magnum", + {.floor_x_min = 0.51f, + .floor_x_max = 0.74f, + .floor_z_min = -0.25f, + .floor_z_max = 0.204f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = -0.09f, + .passenger_dy = 0.0f, + .passenger_dz = -0.248f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.666f, + .bunk_sit_y = -0.149f, + .bunk_sit_z = 0.717f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = false}}, + {"renault_vehicle_renault_premium", + {.floor_x_min = 0.37f, + .floor_x_max = 0.99f, + .floor_z_min = -0.24f, + .floor_z_max = -0.03f, + .stand_y = 0.405f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = 0.0f, + .passenger_dz = -0.135f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.68f, + .bunk_sit_y = 0.095f, + .bunk_sit_z = 0.19f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"renault_vehicle_renault_t", + {.floor_x_min = 0.43f, + .floor_x_max = 0.97f, + .floor_z_min = -0.26f, + .floor_z_max = -0.02f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.03f, + .passenger_dz = 0.0f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.7f, + .bunk_sit_y = -0.13f, + .bunk_sit_z = 0.255f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"scania_vehicle_scania_r", + {.floor_x_min = 0.37f, + .floor_x_max = 0.99f, + .floor_z_min = -0.26f, + .floor_z_max = 0.18f, + .stand_y = 0.368f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.05f, + .passenger_dz = -0.035f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.65f, + .bunk_sit_y = 0.05f, + .bunk_sit_z = 0.425f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"scania_vehicle_scania_r_2016", + {.floor_x_min = 0.37f, + .floor_x_max = 1.0f, + .floor_z_min = -0.32f, + .floor_z_max = 0.04f, + .stand_y = 0.43f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.07f, + .passenger_dz = 0.0f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.68f, + .bunk_sit_y = 0.03f, + .bunk_sit_z = 0.26f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"scania_vehicle_scania_s_2016", + {.floor_x_min = 0.41f, + .floor_x_max = 0.96f, + .floor_z_min = -0.32f, + .floor_z_max = 0.04f, + .stand_y = 0.28f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.07f, + .passenger_dz = 0.0f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.68f, + .bunk_sit_y = 0.03f, + .bunk_sit_z = 0.296f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"scania_vehicle_scania_s_2024e", + {.floor_x_min = 0.41f, + .floor_x_max = 0.96f, + .floor_z_min = -0.32f, + .floor_z_max = 0.04f, + .stand_y = 0.28f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.07f, + .passenger_dz = 0.0f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.68f, + .bunk_sit_y = 0.07f, + .bunk_sit_z = 0.28f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"scania_vehicle_scania_streamline", + {.floor_x_min = 0.37f, + .floor_x_max = 0.99f, + .floor_z_min = -0.26f, + .floor_z_max = 0.18f, + .stand_y = 0.368f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.05f, + .passenger_dz = -0.035f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.65f, + .bunk_sit_y = 0.05f, + .bunk_sit_z = 0.425f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"volvo_vehicle_volvo_fh16", + {.floor_x_min = 0.37f, + .floor_x_max = 1.07f, + .floor_z_min = -0.15f, + .floor_z_max = 0.12f, + .stand_y = 0.381f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = 0.0f, + .passenger_dz = -0.08f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.69f, + .bunk_sit_y = 0.08f, + .bunk_sit_z = 0.4f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"volvo_vehicle_volvo_fh16_2012", + {.floor_x_min = 0.38f, + .floor_x_max = 1.0f, + .floor_z_min = -0.32f, + .floor_z_max = 0.0f, + .stand_y = 0.28f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.04f, + .passenger_dz = -0.145f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.714f, + .bunk_sit_y = 0.01f, + .bunk_sit_z = 0.241f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"volvo_vehicle_volvo_fh_2021", + {.floor_x_min = 0.38f, + .floor_x_max = 1.0f, + .floor_z_min = -0.28f, + .floor_z_max = 0.0f, + .stand_y = 0.28f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.085f, + .passenger_dz = -0.03f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.698f, + .bunk_sit_y = 0.0f, + .bunk_sit_z = 0.241f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"volvo_vehicle_volvo_fh_2024", + {.floor_x_min = 0.38f, + .floor_x_max = 1.0f, + .floor_z_min = -0.28f, + .floor_z_max = 0.0f, + .stand_y = 0.28f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.08f, + .passenger_dz = -0.03f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.73f, + .bunk_sit_y = 0.069f, + .bunk_sit_z = 0.223f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + // --- American Truck Simulator --- + {"freightliner_vehicle_freightliner_cascadia2019", + {.floor_x_min = 0.401f, + .floor_x_max = 0.801f, + .floor_z_min = -0.284f, + .floor_z_max = 0.208f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.02f, + .passenger_dz = -0.03f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.61f, + .bunk_sit_y = 0.055f, + .bunk_sit_z = 0.47f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"freightliner_vehicle_freightliner_cascadia2024", + {.floor_x_min = 0.401f, + .floor_x_max = 0.801f, + .floor_z_min = -0.284f, + .floor_z_max = 0.208f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.02f, + .passenger_dz = -0.03f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.61f, + .bunk_sit_y = 0.055f, + .bunk_sit_z = 0.47f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"intnational_vehicle_intnational_9900i", + {.floor_x_min = 0.277f, + .floor_x_max = 0.677f, + .floor_z_min = -0.5f, + .floor_z_max = 0.123f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.03f, + .passenger_dz = -0.13f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.475f, + .bunk_sit_y = 0.034f, + .bunk_sit_z = 0.423f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"intnational_vehicle_intnational_lonestar", + {.floor_x_min = 0.367f, + .floor_x_max = 0.767f, + .floor_z_min = -0.15f, + .floor_z_max = 0.4f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.05f, + .passenger_dz = -0.1f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.567f, + .bunk_sit_y = -0.03f, + .bunk_sit_z = 0.62f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"intnational_vehicle_intnational_lt", + {.floor_x_min = 0.367f, + .floor_x_max = 0.767f, + .floor_z_min = -0.15f, + .floor_z_max = 0.4f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.05f, + .passenger_dz = -0.1f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.567f, + .bunk_sit_y = -0.03f, + .bunk_sit_z = 0.62f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"kenworth_vehicle_kenworth_t680", + {.floor_x_min = 0.306f, + .floor_x_max = 0.692f, + .floor_z_min = -0.554f, + .floor_z_max = 0.001f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.04f, + .passenger_dz = -0.13f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.51f, + .bunk_sit_y = 0.03f, + .bunk_sit_z = 0.31f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"kenworth_vehicle_kenworth_t680_2022", + {.floor_x_min = 0.31f, + .floor_x_max = 0.71f, + .floor_z_min = -0.395f, + .floor_z_max = 0.413f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.04f, + .passenger_dz = -0.06f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.51f, + .bunk_sit_y = 0.095f, + .bunk_sit_z = 0.605f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"kenworth_vehicle_kenworth_w900", + {.floor_x_min = 0.223f, + .floor_x_max = 0.623f, + .floor_z_min = -0.5f, + .floor_z_max = 0.681f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = 0.03f, + .passenger_dy = 0.0f, + .passenger_dz = -0.045f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.423f, + .bunk_sit_y = -0.06f, + .bunk_sit_z = 0.87f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"mack_vehicle_mack_anthem", + {.floor_x_min = 0.354f, + .floor_x_max = 0.754f, + .floor_z_min = -0.4f, + .floor_z_max = 0.23f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.03f, + .passenger_dz = -0.1f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.55f, + .bunk_sit_y = 0.04f, + .bunk_sit_z = 0.45f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"mack_vehicle_mack_pinnacle", + {.floor_x_min = 0.354f, + .floor_x_max = 0.754f, + .floor_z_min = -0.4f, + .floor_z_max = 0.23f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.03f, + .passenger_dz = -0.1f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.55f, + .bunk_sit_y = 0.04f, + .bunk_sit_z = 0.45f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"peterbilt_vehicle_peterbilt_389", + {.floor_x_min = 0.297f, + .floor_x_max = 0.579f, + .floor_z_min = -0.314f, + .floor_z_max = 0.926f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = 0.0f, + .passenger_dz = 0.0f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.405f, + .bunk_sit_y = 0.11f, + .bunk_sit_z = 1.195f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"peterbilt_vehicle_peterbilt_579", + {.floor_x_min = 0.337f, + .floor_x_max = 0.737f, + .floor_z_min = -0.5f, + .floor_z_max = 0.838f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.03f, + .passenger_dz = -0.02f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.537f, + .bunk_sit_y = 0.0f, + .bunk_sit_z = 1.067f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"volvo_vehicle_volvo_vnl", + {.floor_x_min = 0.42f, + .floor_x_max = 0.77f, + .floor_z_min = -0.4f, + .floor_z_max = 0.816f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.03f, + .passenger_dz = -0.03f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.62f, + .bunk_sit_y = -0.086f, + .bunk_sit_z = 1.069f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"volvo_vehicle_volvo_vnl2018", + {.floor_x_min = 0.37f, + .floor_x_max = 0.85f, + .floor_z_min = -0.32f, + .floor_z_max = 0.62f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.04f, + .passenger_dz = -0.03f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.61f, + .bunk_sit_y = 0.049f, + .bunk_sit_z = 0.881f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"volvo_vehicle_volvo_vnl2025", + {.floor_x_min = 0.351f, + .floor_x_max = 0.751f, + .floor_z_min = -0.311f, + .floor_z_max = 0.18f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.02f, + .passenger_dz = -0.02f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.551f, + .bunk_sit_y = 0.0f, + .bunk_sit_z = 0.393f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"volvo_vehicle_volvo_vnr_e", + {.floor_x_min = 0.37f, + .floor_x_max = 0.85f, + .floor_z_min = -0.32f, + .floor_z_max = 0.04f, + .stand_y = 0.2f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.04f, + .passenger_dz = -0.03f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.475f, + .bunk_sit_y = 0.0f, + .bunk_sit_z = 0.8f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = false}}, + {"westernstar_vehicle_westernstar_49x", + {.floor_x_min = 0.44f, + .floor_x_max = 0.73f, + .floor_z_min = -0.28f, + .floor_z_max = 0.25f, + .stand_y = 0.23f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.03f, + .passenger_dz = -0.05f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.6f, + .bunk_sit_y = 0.055f, + .bunk_sit_z = 0.509f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"westernstar_vehicle_westernstar_5700xe", + {.floor_x_min = 0.44f, + .floor_x_max = 0.71f, + .floor_z_min = -0.12f, + .floor_z_max = 0.533f, + .stand_y = 0.23f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.02f, + .passenger_dz = -0.02f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 0.573f, + .bunk_sit_y = 0.04f, + .bunk_sit_z = 0.8f, + .bunk_sit_yaw = 0.0f, + .bunk_sit_pitch = -10.0f, + .has_passenger = true, + .has_bunk = true}}, + {"westernstar_vehicle_westernstar_57x", + {.floor_x_min = 0.44f, + .floor_x_max = 0.73f, + .floor_z_min = -0.28f, + .floor_z_max = 0.75f, + .stand_y = 0.23f, + .crouch_depth = 0.45f, + .passenger_dx = 0.0f, + .passenger_dy = -0.04f, + .passenger_dz = -0.05f, + .passenger_yaw = 0.0f, + .passenger_pitch = 0.0f, + .bunk_sit_x = 1.227f, + .bunk_sit_y = -0.03f, + .bunk_sit_z = 1.328f, + .bunk_sit_yaw = 90.0f, + .bunk_sit_pitch = -5.413f, + .has_passenger = true, + .has_bunk = true}}, +}; + +} // namespace + +std::span CabinPresets() { return kPresets; } + +const CabinLayout *FindCabinPreset(std::string_view key) { + const auto it = std::find_if( + std::begin(kPresets), std::end(kPresets), + [&](const CabinPreset &preset) { return preset.key == key; }); + return it == std::end(kPresets) ? nullptr : &it->layout; +} + +} // namespace motioncab diff --git a/src/effects/manual/cabin_walk/CabinPresets.hpp b/src/effects/manual/cabin_walk/CabinPresets.hpp new file mode 100644 index 0000000..8a02bb6 --- /dev/null +++ b/src/effects/manual/cabin_walk/CabinPresets.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "CabinLayouts.hpp" + +#include +#include + +namespace motioncab { + +// A layout shipped with the plugin, under its truck model's key +// (TruckLayoutKey). +struct CabinPreset { + std::string_view key; + CabinLayout layout; +}; + +// Every shipped layout (ETS2's and ATS's: no key is shared between them). +std::span CabinPresets(); + +// The shipped layout for `key`, null if there's none. +const CabinLayout *FindCabinPreset(std::string_view key); + +} // namespace motioncab diff --git a/src/effects/manual/cabin_walk/CabinWalkEffect.cpp b/src/effects/manual/cabin_walk/CabinWalkEffect.cpp new file mode 100644 index 0000000..6b83748 --- /dev/null +++ b/src/effects/manual/cabin_walk/CabinWalkEffect.cpp @@ -0,0 +1,795 @@ +#include "CabinWalkEffect.hpp" + +#include "core/Keybinds.hpp" +#include "effects/TelemetryUtil.hpp" +#include "math/Units.hpp" + +#include +#include +#include +#include + +namespace motioncab { + +using math::kDegToRad; +using math::kPi; + +namespace { + +// Walking speed (m/s), slower backward and sideways, and how long it takes +// to get up to speed and to stop (stopping is quicker). +constexpr float kWalkSpeed = 1.0f; +constexpr float kBackwardShare = 0.6f; +constexpr float kSidewaysShare = 0.75f; +constexpr float kAccelerationSeconds = 0.15f; +constexpr float kDecelerationSeconds = 0.1f; +// How deep the head dips each step (meters). +constexpr float kHeadBob = 0.012f; +// The footsteps' volume (0..1) at walking speed. +constexpr float kFootstepVolume = 0.8f; +// Standing up needs the truck at rest; above kMaxAwayKmh the player is +// sent back to the wheel (a parked truck still creeps a little on a slope). +constexpr float kMaxStandKmh = 1.0f; +constexpr float kMaxAwayKmh = 3.0f; +// Holding stand/sit this long goes back to the wheel instead of the tap's +// "sit at the nearest spot". +constexpr float kLongPressSeconds = 0.6f; +// Standing up or sitting down takes kTransitionSeconds for kTransitionMeters, +// scaled by the actual distance within the bounds below. +constexpr float kTransitionSeconds = 1.2f; +constexpr float kTransitionMeters = 0.7f; +constexpr float kTransitionScaleMin = 0.7f; +constexpr float kTransitionScaleMax = 1.6f; +// Hurried back to the wheel (parking brake released, truck rolling). +constexpr float kHurrySeconds = 0.8f; +// Walking by itself to a spot: close enough to sit down from, and how +// soon it slows down before it (speed = distance * gain). +constexpr float kArriveMeters = 0.04f; +constexpr float kArriveGain = 4.0f; +// Crouching slows walking down by this fraction. +constexpr float kCrouchSlowdown = 0.5f; +// Head height changes (crouching). +constexpr float kHeightTime = 0.3f; +// Step bob: one dip per step of kStepMeters, a sideways sway of this +// fraction of the dip, and a slight roll along with the sway. +constexpr float kStepMeters = 0.45f; +constexpr float kBobSwayRatio = 0.6f; +constexpr float kBobRollDegPerMeter = 40.0f; +// How long the head's rotation is held after the camera's re-activation, +// which recenters it a frame or so later. +constexpr float kHoldRotationSeconds = 0.25f; +// Within this distance of a seat (on the floor plane), a tap sits there +// whatever the look. +constexpr float kOverSpotMeters = 0.08f; +// No footstep sound below this share of walking speed (shuffling in place). +constexpr float kMinStepSoundAmount = 0.3f; +constexpr float kBobTime = 0.2f; +// Standing, the body stays upright while the parked truck leans (half on a +// kerb, on a slope): the head tilts against the truck's tilt, turning +// along with standing up and sitting down, within a sane bound. +constexpr float kMaxUprightDeg = 15.0f; +// The camera's roll turns the head the other way from the truck's roll +// (seen in game: +1 leaned the head further with the truck). +constexpr float kUprightRollSign = -1.0f; +// Turning all the way around while up, and looking down at the floor. The +// yaw is wrapped back into +-180 every frame (see Update), so this is just +// headroom the mouse never reaches: 90 degrees past a half-turn in a frame. +constexpr float kAwayYawLimitDeg = 270.0f; +constexpr float kAwayDownLimitDeg = -80.0f; + +// Standing up leans the head forward (over the feet) and a bit down before +// rising; the rise then ends straight up. +constexpr float kRiseLeanMeters = 0.12f; +constexpr float kRiseDipMeters = 0.04f; +constexpr float kRiseEndLiftMeters = 0.10f; +// Sitting down steps over at standing height first, then lowers from a bit +// in front of and above the seat, settling back against the backrest. +constexpr float kSitStepShare = 0.4f; +constexpr float kSitLeanMeters = 0.12f; +constexpr float kSitAboveMeters = 0.10f; +// Moving between seated spots arcs over, more for a longer move. +constexpr float kArcMeters = 0.05f; +constexpr float kArcFullMeters = 0.6f; +// Looking down during the move, and leaning into it on the way to the +// driver's or the passenger seat (degrees, at mid-move). +constexpr float kRiseLookDownDeg = 8.0f; +constexpr float kSitLookDownDeg = 10.0f; +constexpr float kDirectLookDownDeg = 4.0f; +constexpr float kLeanRollDeg = 3.0f; +// The view turns to a seat's look a bit before the head gets there. +constexpr float kTurnShare = 0.85f; + +float Clamp01(float t) { return std::clamp(t, 0.0f, 1.0f); } + +// 0 to 1 with zero speed and acceleration at both ends (quintic), so a move +// neither starts nor stops with a jolt. +float SmootherStep(float t) { + t = Clamp01(t); + return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); +} + +// 0 at both ends, 1 at mid-move, with zero slope at the ends. +float Bell(float t) { + const float s = std::sin(kPi * Clamp01(t)); + return s * s; +} + +float Lerp(float a, float b, float t) { return a + (b - a) * t; } + +// Into [-pi, pi], so a turn never goes across the back: the yaw limits stop +// at +-180 degrees, and a turn across them would jump. +float WrapYaw(float yaw_rad) { return std::remainder(yaw_rad, 2.0f * kPi); } + +} // namespace + +CabinWalkEffect::CabinWalkEffect(SPF_Config_API *config_api, + SPF_Config_Handle *config_handle, + SPF_KeyBinds_API *keybinds_api, + SPF_KeyBinds_Handle *keybinds_handle, + SPF_UI_API *ui_api, CabinLayoutStore *layouts) + : config_(config_api, config_handle, "manual", "cabin_walk"), + keybinds_api_(keybinds_api), keybinds_handle_(keybinds_handle), + ui_api_(ui_api), layouts_(layouts) { + height_.SetTimeConstant(kHeightTime); + bob_amount_.SetTimeConstant(kBobTime); +} + +void CabinWalkEffect::LoadConfig() { + if (!config_.IsAvailable()) + return; + enabled_ = config_.Bool("enabled", enabled_); + require_parking_brake_ = + config_.Bool("require_parking_brake", require_parking_brake_); +} + +HeadOffset CabinWalkEffect::Update(float dt, const SPF_TruckData &truck, + SPF_Camera_API *camera, + const CameraRig::Pose &base) { + // First frame back from another view (see OnLeftInterior). + const bool back_in_cabin = std::exchange(left_interior_, false); + base_ = {base.seat_x, base.seat_y, base.seat_z}; + base_yaw_ = base.yaw_rad; + base_pitch_ = base.pitch_rad; + base_rotation_.reset(); + if (hold_rotation_time_ > 0.0f) { + hold_rotation_time_ -= dt; + std::tie(base_yaw_, base_pitch_) = held_rotation_; + base_rotation_ = held_rotation_; + if (hold_rotation_time_ <= 0.0f) + camera_override_.EndRefresh(camera); + } + + // Turning around freely: past a half-turn, the same look a full turn + // back, before the mouse reaches the widened limit. (Not in a seat that + // keeps the truck's own limits, which may go past 180 on purpose.) + const bool full_turn = + !AtWheel() && !(state_ == State::kSeated && spot_ == Spot::kPassenger); + if (full_turn && std::fabs(base_yaw_) > kPi) { + base_yaw_ = WrapYaw(base_yaw_); + base_rotation_ = {base_yaw_, base_pitch_}; + } + + // Anything that makes being up unsafe (or unwanted) brings the player + // back, unless already on the way to the wheel. + const bool heading_to_wheel = state_ == State::kTransition && + after_ == State::kSeated && + after_spot_ == Spot::kDriver; + if (!AtWheel() && !heading_to_wheel) { + const bool unsafe = telemetry::SpeedKmh(truck) > kMaxAwayKmh || + (require_parking_brake_ && !truck.parking_brake); + if (unsafe && back_in_cabin) { + // It became unsafe while out of the interior view (driving from + // another camera): no walk back to the wheel in the middle of it. + notice_ = Notice::kBackToWheel; + camera_override_.Restore(camera); + SnapToWheel(); + } else if (unsafe || !enabled_) { + if (enabled_) + notice_ = Notice::kBackToWheel; + GoToWheel(unsafe ? kHurrySeconds : TransitionDuration(base_)); + } + } + + if (enabled_) { + HandleStandSitKey(dt, truck, camera); + if (go_to_request_) + HandleGoTo(*std::exchange(go_to_request_, std::nullopt), truck, camera); + } + + switch (state_) { + case State::kSeated: + if (spot_ == Spot::kDriver) + pos_ = base_; + else + FollowSeatedSpot(camera); + break; + case State::kTransition: + UpdateTransition(dt, camera); + break; + case State::kWalking: + UpdateWalking(dt); + break; + } + + if (camera_override_.refreshes() != seen_refreshes_) { + seen_refreshes_ = camera_override_.refreshes(); + held_rotation_ = base_rotation_.value_or(std::pair{base_yaw_, base_pitch_}); + hold_rotation_time_ = kHoldRotationSeconds; + } + + camera_override_.KeepNearPlane(camera); + SetMouseBlocked(state_ == State::kTransition && turn_); + SetWalkKeysBlocked(!AtWheel()); + + // Step bob, fading in and out with the walking speed. + const float speed = state_ == State::kWalking + ? std::hypot(vel_x_.value(), vel_z_.value()) + : 0.0f; + const float amount = + std::clamp(bob_amount_.Update(speed / kWalkSpeed, dt), 0.0f, 1.0f); + const float phase_before = bob_phase_; + bob_phase_ = math::WrapPhase(bob_phase_ + speed * dt / kStepMeters * kPi); + // A footstep where the head dips lowest, a quarter-turn into each step: + // where the phase's cosine changes sign. + if (amount > kMinStepSoundAmount && + (std::cos(phase_before) > 0.0f) != (std::cos(bob_phase_) > 0.0f)) + footsteps_.push_back(kFootstepVolume * amount); + const float sway = kHeadBob * kBobSwayRatio * amount * std::sin(bob_phase_); + const float dip = + kHeadBob * amount * 0.5f * (1.0f - std::cos(2.0f * bob_phase_)); + + HeadOffset offset; + offset.pos_x = pos_.x - base_.x + sway * CameraX() * std::cos(base_yaw_); + offset.pos_y = pos_.y - base_.y - dip; + offset.pos_z = pos_.z - base_.z - sway * std::sin(base_yaw_); + offset.pitch = motion_pitch_deg_; + offset.roll = kBobRollDegPerMeter * kHeadBob * amount * std::sin(bob_phase_) + + motion_roll_deg_; + + // Upright against the truck's tilt, seen from where the head looks (yaw + // positive to the left): the truck's roll is a roll looking ahead and a + // pitch looking aside, its pitch the other way around. + // Upright on the feet, with the cabin in a seat: it changes along with + // the move between the two (standing up, sitting down), not after it. + const float on_feet = state_ == State::kWalking ? 1.0f : 0.0f; + const float upright = + state_ == State::kTransition + ? Lerp(upright_from_, after_ == State::kWalking ? 1.0f : 0.0f, + SmootherStep(progress_)) + : on_feet; + upright_now_ = upright; + if (upright > 1e-3f) { + const auto tilt_deg = [](float turns) { + return std::clamp(turns * 360.0f, -kMaxUprightDeg, kMaxUprightDeg); + }; + const float pitch = tilt_deg(truck.world_placement.orientation.pitch); + const float roll = tilt_deg(truck.world_placement.orientation.roll); + const float c = std::cos(base_yaw_), s = std::sin(base_yaw_); + offset.pitch += upright * (-pitch * c + roll * s); + offset.roll += upright * kUprightRollSign * (roll * c + pitch * s); + } + return offset; +} + +// --- Spots --- + +CabinWalkEffect::SpotPose CabinWalkEffect::SpotAt(Spot spot) const { + const CabinLayout &l = layout_; + auto pose = [](float x, float y, float z, float yaw_deg, float pitch_deg) { + return SpotPose{{x, y, z}, yaw_deg * kDegToRad, pitch_deg * kDegToRad}; + }; + switch (spot) { + case Spot::kPassenger: + // The player's own seat mirrored across the centerline: sat at the same + // height, depth and distance from the center as at the wheel. + return pose(2.0f * centerline_x_ - base_.x + l.passenger_dx, + base_.y + l.passenger_dy, base_.z + l.passenger_dz, + l.passenger_yaw, l.passenger_pitch); + case Spot::kBunkSit: + return pose(l.bunk_sit_x, l.bunk_sit_y, l.bunk_sit_z, l.bunk_sit_yaw, + l.bunk_sit_pitch); + case Spot::kDriver: + break; + } + // The player's own seat, looking where the truck's recenter does. + float yaw_deg = 0.0f, pitch_deg = 0.0f; + if (camera_override_.engaged()) { + yaw_deg = camera_override_.original_default_yaw(); + pitch_deg = camera_override_.original_default_pitch(); + } + return {base_, yaw_deg * kDegToRad, pitch_deg * kDegToRad}; +} + +CabinWalkEffect::Vec3 CabinWalkEffect::Approach(Spot spot) const { + const Vec3 p = SpotAt(spot).pos; + return {std::clamp(p.x, layout_.floor_x_min, layout_.floor_x_max), + layout_.stand_y, + std::clamp(p.z, layout_.floor_z_min, layout_.floor_z_max)}; +} + +CabinWalkEffect::Spot CabinWalkEffect::LookedAtSpot() const { + // The seat whose direction is closest to the look, on the floor plane: + // the seats are too close together in most cabins to go by distance. + const float look_x = -CameraX() * std::sin(base_yaw_); + const float look_z = -std::cos(base_yaw_); + Spot best = Spot::kDriver; + float best_cos = -2.0f; + for (Spot spot : {Spot::kDriver, Spot::kPassenger, Spot::kBunkSit}) { + if (!CanSitAt(spot)) + continue; + const Vec3 p = SpotAt(spot).pos; + const float dx = p.x - pos_.x, dz = p.z - pos_.z; + const float distance = std::hypot(dx, dz); + // Standing on top of it, where the look points doesn't tell. + const float cos = distance < kOverSpotMeters + ? 1.0f + : (dx * look_x + dz * look_z) / distance; + if (cos > best_cos) { + best_cos = cos; + best = spot; + } + } + return best; +} + +// --- Keys --- + +void CabinWalkEffect::HandleStandSitKey(float dt, const SPF_TruckData &truck, + SPF_Camera_API *camera) { + if (!keybinds_api_ || !keybinds_handle_) + return; + const bool held = + keybinds::IsHeld(keybinds_api_, keybinds_handle_, keybinds::kStandSit); + bool tap = false, long_press = false; + if (held) { + stand_sit_held_time_ = stand_sit_held_ ? stand_sit_held_time_ + dt : 0.0f; + if (!stand_sit_long_done_ && stand_sit_held_time_ >= kLongPressSeconds) { + long_press = true; + stand_sit_long_done_ = true; + } + } else { + tap = stand_sit_held_ && !stand_sit_long_done_; + stand_sit_long_done_ = false; + } + stand_sit_held_ = held; + + switch (state_) { + case State::kSeated: + if (spot_ == Spot::kDriver) { + if (tap || long_press) + TryStandUp(truck, camera); + } else if (long_press) { + StandUp(); + walk_to_ = Spot::kDriver; + } else if (tap) { + StandUp(); + } + break; + case State::kWalking: + if (long_press) + walk_to_ = Spot::kDriver; + else if (tap) + walk_to_ = LookedAtSpot(); + break; + case State::kTransition: + break; + } +} + +void CabinWalkEffect::TryStandUp(const SPF_TruckData &truck, + SPF_Camera_API *camera) { + if (telemetry::SpeedKmh(truck) > kMaxStandKmh) { + notice_ = Notice::kNeedsStop; + return; + } + if (require_parking_brake_ && !truck.parking_brake) { + notice_ = Notice::kNeedsParkingBrake; + return; + } + if (!camera_override_.Engage(camera)) { + // Camera not resolved yet: try again on the next press. + return; + } + // Up from where the head really was: leaning toward a mirror, the truck's + // own head offset for that look, now gone. + const std::array &lean = camera_override_.engage_head_offset(); + pos_ = {base_.x + lean[0], base_.y + lean[1], base_.z + lean[2]}; + StandUp(); +} + +void CabinWalkEffect::StandUp() { + const Vec3 to = Approach(spot_); + StartTransition(to, Path::kRise, TransitionDuration(to), State::kWalking, + Spot::kDriver, false); +} + +void CabinWalkEffect::SitAt(Spot spot) { + const Vec3 to = SpotAt(spot).pos; + StartTransition(to, Path::kSit, TransitionDuration(to), State::kSeated, spot, + true); +} + +void CabinWalkEffect::GoToWheel(float duration) { + walk_to_.reset(); + // From standing, it's sitting down; from another seat, moving over. + const bool standing = state_ == State::kWalking; + StartTransition(base_, standing ? Path::kSit : Path::kDirect, duration, + State::kSeated, Spot::kDriver, true); +} + +// --- Transitions --- + +CabinWalkEffect::Vec3 CabinWalkEffect::Facing(Spot spot) const { + // Yaw 0 looks toward -z, positive yaw turns left. + const float yaw = SpotAt(spot).yaw_rad; + return Vec3{-CameraX() * std::sin(yaw), 0.0f, -std::cos(yaw)}; +} + +void CabinWalkEffect::StartTransition(const Vec3 &to, Path path, float duration, + State after, Spot after_spot, bool turn) { + from_ = pos_; + to_ = to; + upright_from_ = upright_now_; + path_ = path; + progress_ = 0.0f; + duration_ = std::max(duration, 0.05f); + after_ = after; + after_spot_ = after_spot; + turn_ = turn; + yaw_from_ = WrapYaw(base_yaw_); + pitch_from_ = base_pitch_; + + auto facing = [&](Spot spot) { return Facing(spot); }; + const float dx = to.x - from_.x, dy = to.y - from_.y, dz = to.z - from_.z; + const float horizontal = std::hypot(dx, dz); + switch (path) { + case Path::kRise: { + const Vec3 f = facing(spot_); + ctrl1_ = {from_.x + f.x * kRiseLeanMeters, from_.y - kRiseDipMeters, + from_.z + f.z * kRiseLeanMeters}; + ctrl2_offset_ = {0.0f, -kRiseEndLiftMeters, 0.0f}; + break; + } + case Path::kSit: { + const Vec3 f = facing(after_spot); + ctrl1_ = {from_.x + dx * kSitStepShare, from_.y, + from_.z + dz * kSitStepShare}; + ctrl2_offset_ = {f.x * kSitLeanMeters, kSitAboveMeters, + f.z * kSitLeanMeters}; + break; + } + case Path::kDirect: { + // Arcs over a sideways move, barely over a mostly vertical one. + const float arc = kArcMeters * Clamp01(horizontal / kArcFullMeters) * + horizontal / (horizontal + std::fabs(dy) + 1e-3f); + ctrl1_ = {from_.x + dx / 3.0f, from_.y + dy / 3.0f + arc, + from_.z + dz / 3.0f}; + ctrl2_offset_ = {-dx / 3.0f, -dy / 3.0f + arc, -dz / 3.0f}; + break; + } + } + + // Sideways share of the move as seen from where the player looks, for + // leaning into it on the way to a front seat (not the bunk). + const bool front_seat = + after == State::kSeated && after_spot != Spot::kBunkSit; + const float right = + CameraX() * dx * std::cos(base_yaw_) - dz * std::sin(base_yaw_); + lean_side_ = front_seat + ? std::clamp(right / std::max(horizontal, 0.2f), -1.0f, 1.0f) + : 0.0f; + + state_ = State::kTransition; +} + +float CabinWalkEffect::TransitionDuration(const Vec3 &from, + const Vec3 &to) const { + const float dx = to.x - from.x, dy = to.y - from.y, dz = to.z - from.z; + const float distance = std::sqrt(dx * dx + dy * dy + dz * dz); + return kTransitionSeconds * std::clamp(distance / kTransitionMeters, + kTransitionScaleMin, + kTransitionScaleMax); +} + +CabinWalkEffect::Vec3 CabinWalkEffect::Bezier(const Vec3 &p0, const Vec3 &p1, + const Vec3 &p2, const Vec3 &p3, + float u) { + const float a = (1.0f - u) * (1.0f - u) * (1.0f - u); + const float b = 3.0f * (1.0f - u) * (1.0f - u) * u; + const float c = 3.0f * (1.0f - u) * u * u; + const float d = u * u * u; + return {a * p0.x + b * p1.x + c * p2.x + d * p3.x, + a * p0.y + b * p1.y + c * p2.y + d * p3.y, + a * p0.z + b * p1.z + c * p2.z + d * p3.z}; +} + +void CabinWalkEffect::UpdateTransition(float dt, SPF_Camera_API *camera) { + progress_ = std::min(1.0f, progress_ + dt / duration_); + const float t = progress_; + // The seat follows the player's own seat, should it change meanwhile. + if (after_ == State::kSeated) + to_ = SpotAt(after_spot_).pos; + + // Along the Bezier path, eased so the body starts and stops smoothly. + const Vec3 ctrl2{to_.x + ctrl2_offset_.x, to_.y + ctrl2_offset_.y, + to_.z + ctrl2_offset_.z}; + pos_ = Bezier(from_, ctrl1_, ctrl2, to_, SmootherStep(t)); + + // The body's own motion on top: looking down at where it goes, leaning + // into a sideways move. + const float look_down = path_ == Path::kRise ? kRiseLookDownDeg + : path_ == Path::kSit ? kSitLookDownDeg + : kDirectLookDownDeg; + motion_pitch_deg_ = -look_down * Bell(t); + motion_roll_deg_ = kLeanRollDeg * lean_side_ * Bell(t); + + if (turn_) { + const SpotPose target = SpotAt(after_spot_); + const float r = SmootherStep(t / kTurnShare); + base_rotation_ = {Lerp(yaw_from_, WrapYaw(target.yaw_rad), r), + Lerp(pitch_from_, target.pitch_rad, r)}; + } + + if (progress_ >= 1.0f) + OnArrived(camera); +} + +void CabinWalkEffect::OnArrived(SPF_Camera_API *camera) { + state_ = after_; + pos_ = to_; + motion_pitch_deg_ = 0.0f; + motion_roll_deg_ = 0.0f; + vel_x_.Reset(); + vel_z_.Reset(); + height_.Reset(pos_.y); + const InteriorCameraOverride::Limits &own = + camera_override_.original_limits(); + const InteriorCameraOverride::Limits away{kAwayYawLimitDeg, -kAwayYawLimitDeg, + own.up, kAwayDownLimitDeg}; + if (state_ == State::kWalking) { + camera_override_.SetLimits(camera, away); + camera_override_.SetDefaults(camera, + camera_override_.original_default_yaw(), + camera_override_.original_default_pitch()); + return; + } + + spot_ = after_spot_; + if (spot_ == Spot::kDriver) { + camera_override_.Restore(camera); + return; + } + // The passenger seat mirrors the driver's view range; the bunk allows + // looking all around, as standing does. The recenter key looks where the + // spot does. + if (spot_ == Spot::kPassenger) + camera_override_.SetLimits(camera, + {-own.right, -own.left, own.up, own.down}); + else + camera_override_.SetLimits(camera, away); + const SpotPose pose = SpotAt(spot_); + seated_yaw_deg_ = pose.yaw_rad / kDegToRad; + seated_pitch_deg_ = pose.pitch_rad / kDegToRad; + camera_override_.SetDefaults(camera, seated_yaw_deg_, seated_pitch_deg_); +} + +void CabinWalkEffect::FollowSeatedSpot(SPF_Camera_API *camera) { + const SpotPose pose = SpotAt(spot_); + pos_ = pose.pos; + const float yaw_deg = pose.yaw_rad / kDegToRad; + const float pitch_deg = pose.pitch_rad / kDegToRad; + if (yaw_deg == seated_yaw_deg_ && pitch_deg == seated_pitch_deg_) + return; + // The spot's look was edited: look that way, and recenter to it. + seated_yaw_deg_ = yaw_deg; + seated_pitch_deg_ = pitch_deg; + base_rotation_ = {WrapYaw(pose.yaw_rad), pose.pitch_rad}; + camera_override_.SetDefaults(camera, yaw_deg, pitch_deg); +} + +// --- Walking --- + +float CabinWalkEffect::ClampToFloor(float value, float min, float max, + float &velocity, + math::SpringDamper1D &spring) const { + const float clamped = std::clamp(value, min, max); + if (clamped != value) { + // Against a wall: slide along it instead of pushing into it. + spring.Reset(); + velocity = 0.0f; + } + return clamped; +} + +void CabinWalkEffect::UpdateWalking(float dt) { + const auto amount = [&](const keybinds::Action &action) { + return keybinds::Amount(keybinds_api_, keybinds_handle_, action); + }; + float forward = amount(keybinds::kWalkForward) - amount(keybinds::kWalkBack); + float side = amount(keybinds::kWalkRight) - amount(keybinds::kWalkLeft); + const float crouch = amount(keybinds::kCrouch); + const float length = std::hypot(forward, side); + if (length > 1.0f) { + forward /= length; + side /= length; + } + if (length > 0.05f) + walk_to_.reset(); // the player takes over + if (forward < 0.0f) + forward *= kBackwardShare; + side *= kSidewaysShare; + + // Relative to where the player looks: yaw 0 looks toward -z, positive + // yaw turns left (toward -x, flipped in a mirrored camera, see CameraX). + const float speed = kWalkSpeed * (1.0f - kCrouchSlowdown * crouch); + float want_x = CameraX() * + (-std::sin(base_yaw_) * forward + std::cos(base_yaw_) * side) * + speed; + float want_z = + (-std::cos(base_yaw_) * forward - std::sin(base_yaw_) * side) * speed; + + if (walk_to_) { + const Vec3 target = Approach(*walk_to_); + const float dx = target.x - pos_.x, dz = target.z - pos_.z; + const float distance = std::hypot(dx, dz); + if (distance < kArriveMeters) { + const Spot spot = *walk_to_; + walk_to_.reset(); + SitAt(spot); + return; + } + const float approach = std::min(kWalkSpeed, distance * kArriveGain); + want_x = dx / distance * approach; + want_z = dz / distance * approach; + } + + // Getting up to speed, or slowing down and stopping (quicker). + const bool speeding_up = + std::hypot(want_x, want_z) >= std::hypot(vel_x_.value(), vel_z_.value()); + const float response = + speeding_up ? kAccelerationSeconds : kDecelerationSeconds; + vel_x_.SetTimeConstant(response); + vel_z_.SetTimeConstant(response); + float vx = vel_x_.Update(want_x, dt); + float vz = vel_z_.Update(want_z, dt); + pos_.x = ClampToFloor(pos_.x + vx * dt, layout_.floor_x_min, + layout_.floor_x_max, vx, vel_x_); + pos_.z = ClampToFloor(pos_.z + vz * dt, layout_.floor_z_min, + layout_.floor_z_max, vz, vel_z_); + + pos_.y = height_.Update(layout_.stand_y - crouch * layout_.crouch_depth, dt); +} + +// --- Lifecycle --- + +void CabinWalkEffect::OnTruckConstantsChanged( + const SPF_TruckConstants &constants) { + const std::string key = TruckLayoutKey(constants); + const bool right_hand_drive = IsRightHandDrive(constants); + // Another cabin on the same model can move the driver's seat. + const float centerline_x = CabinCenterlineX(constants); + if (key == truck_key_ && right_hand_drive == right_hand_drive_ && + centerline_x == centerline_x_) + return; + truck_key_ = key; + right_hand_drive_ = right_hand_drive; + camera_override_.SetRightHandDrive(right_hand_drive); + centerline_x_ = centerline_x; + truck_name_ = std::string(constants.brand) + " " + constants.name; + LoadLayout(); + if (!AtWheel()) + SnapToWheel(); +} + +void CabinWalkEffect::OnLeftInterior() { + SetWalkKeysBlocked(false); + left_interior_ = true; +} + +void CabinWalkEffect::SnapToWheel() { + state_ = State::kSeated; + spot_ = Spot::kDriver; + walk_to_.reset(); + pos_ = base_; + base_rotation_.reset(); + motion_pitch_deg_ = 0.0f; + motion_roll_deg_ = 0.0f; + camera_override_.Forget(); + SetMouseBlocked(false); + SetWalkKeysBlocked(false); +} + +void CabinWalkEffect::Shutdown(SPF_Camera_API *camera) { + if (camera) + camera_override_.Restore(camera); + SetMouseBlocked(false); + SetWalkKeysBlocked(false); +} + +CabinWalkEffect::Notice CabinWalkEffect::TakeNotice() { + return std::exchange(notice_, Notice::kNone); +} + +void CabinWalkEffect::SetMouseBlocked(bool blocked) { + if (blocked == mouse_blocked_ || !ui_api_) + return; + // Only the look axes: the view is being turned for the player. + ui_api_->UI_SetMouseBlockState(blocked, false, false); + mouse_blocked_ = blocked; +} + +void CabinWalkEffect::SetWalkKeysBlocked(bool blocked) { + if (blocked == walk_keys_blocked_ || !keybinds_api_ || !keybinds_handle_) + return; + // Takes effect from the next press: a key already held when the player + // stands up keeps driving until released. + for (const keybinds::Action &action : keybinds::kWalkActions) + keybinds_api_->Kbind_SetBlockState(keybinds_handle_, action.id, blocked); + walk_keys_blocked_ = blocked; +} + +// --- Layout tuning --- + +void CabinWalkEffect::HandleGoTo(Spot spot, const SPF_TruckData &truck, + SPF_Camera_API *camera) { + if (state_ == State::kTransition || !CanSitAt(spot)) + return; + if (AtWheel()) { + TryStandUp(truck, camera); + if (state_ != State::kTransition) + return; // refused, the notice says why + } else if (state_ == State::kSeated) { + if (spot_ == spot) + return; + StandUp(); + } + walk_to_ = spot; +} + +void CabinWalkEffect::SetLayout(const CabinLayout &layout, bool save) { + layout_ = layout; + // Sat in (or walking to) a spot just marked unusable: back on the feet. + if (state_ == State::kSeated && !CanSitAt(spot_)) + StandUp(); + if (walk_to_ && !CanSitAt(*walk_to_)) + walk_to_.reset(); + if (save) + SaveLayout(); +} + +void CabinWalkEffect::ResetLayout() { + if (layouts_ && custom_layout_) + layouts_->Forget(truck_key_); + LoadLayout(); +} + +void CabinWalkEffect::LoadLayout() { + // One layout per truck model, whichever its cabin or side: the player's + // own, else the preset, else one built around the centerline. Made for + // left-hand drive, it has the right positions for a right-hand drive + // truck's mirrored camera, but its looks turn the wrong way. + auto fit = [&](const CabinLayout &layout) { + return right_hand_drive_ ? WithMirroredYaws(layout) : layout; + }; + std::optional custom, preset; + if (layouts_ && !truck_key_.empty()) { + custom = layouts_->LoadCustom(truck_key_); + preset = layouts_->LoadPreset(truck_key_); + } + shipped_layout_ = fit(preset.value_or(DefaultCabinLayout(centerline_x_))); + layout_ = custom ? fit(*custom) : shipped_layout_; + custom_layout_ = custom.has_value(); +} + +void CabinWalkEffect::SaveLayout() { + NormalizeCabinLayout(layout_); + custom_layout_ = true; + if (truck_key_.empty() || !layouts_) + return; + // Saved as for left-hand drive, so both sides get it: a right-hand drive + // truck's looks turned back (see CabinLayout). + layouts_->Save(truck_key_, + right_hand_drive_ ? WithMirroredYaws(layout_) : layout_); +} + +} // namespace motioncab diff --git a/src/effects/manual/cabin_walk/CabinWalkEffect.hpp b/src/effects/manual/cabin_walk/CabinWalkEffect.hpp new file mode 100644 index 0000000..abe8657 --- /dev/null +++ b/src/effects/manual/cabin_walk/CabinWalkEffect.hpp @@ -0,0 +1,275 @@ +#pragma once + +#include "CabinLayouts.hpp" +#include "InteriorCameraOverride.hpp" +#include "SPF_Camera_API.h" +#include "SPF_KeyBinds_API.h" +#include "SPF_UI_API.h" +#include "core/CameraRig.hpp" +#include "core/SettingsSchema.hpp" +#include "effects/ConfigurableEffect.hpp" +#include "math/SpringDamper.hpp" + +#include +#include +#include +#include + +namespace motioncab { + +// Based on SPF_CabinWalk by Track'n'Truck Devs +// (https://github.com/TrackAndTruckDevs/SPF_CabinWalk), +// +// Gets the player out of the driver's seat to walk around the cabin, parked +// with the parking brake on. The stand/sit key stands up; then the walking +// keys move the head freely over the cabin floor, relative to where the +// player looks, with crouching and a light step bob. Tapping +// stand/sit again sits down at the spot looked toward (driver's seat, +// passenger's, or the bunk's edge), walking there first if needed; holding +// it goes back to the wheel. Releasing the parking brake or rolling brings +// the player back to the wheel. +// +// Not a regular Effect: it needs the player's own pose (to walk from the +// seat they set up and to turn their view when sitting down) and owns +// camera state beyond a HeadOffset (rotation limits, mouse). +// Its offset is added to EffectManager's by OnUpdate, which fades the +// driving effects out meanwhile (EffectManager::SetAtWheel). +// +// The cabin itself (floor, heights, spots) comes from a per-truck +// CabinLayout, tuned from the Quick Settings. +class CabinWalkEffect { +public: + enum class Spot { kDriver, kPassenger, kBunkSit }; + // Why the stand/sit key was refused, or the player was sent back to the + // wheel, for OnUpdate to tell them. + enum class Notice { kNone, kNeedsParkingBrake, kNeedsStop, kBackToWheel }; + + CabinWalkEffect(SPF_Config_API *config_api, SPF_Config_Handle *config_handle, + SPF_KeyBinds_API *keybinds_api, + SPF_KeyBinds_Handle *keybinds_handle, SPF_UI_API *ui_api, + CabinLayoutStore *layouts); + + bool IsEnabled() const { return enabled_; } + void LoadConfig(); + + // Advances by `dt` seconds. `base` is the player's own pose underneath + // every offset (CameraRig::Base). Returns the offset to add to the + // effects' one. + HeadOffset Update(float dt, const SPF_TruckData &truck, + SPF_Camera_API *camera, const CameraRig::Pose &base); + + // The player's own head rotation (radians) this frame's write must use, + // while sitting down turns their view (CameraRig::SetBaseRotation). + const std::optional> &base_rotation() const { + return base_rotation_; + } + + // Settled in the driver's seat: nothing overridden, driving effects on. + bool AtWheel() const { + return state_ == State::kSeated && spot_ == Spot::kDriver; + } + + // Loads the new truck's layout. A player away from the wheel is put back + // in the seat at once, since the cabin they were in is gone. + void OnTruckConstantsChanged(const SPF_TruckConstants &constants); + + // Out of the interior view: the game gets the walking keys back + // meanwhile, to drive from another camera. Update() blocks them again + // once back in the cabin, and puts the player straight back at the wheel + // if the truck was set rolling meanwhile. + void OnLeftInterior(); + + // Back in the driver's seat at once, leaving the camera's values to the + // game: after a world reload they belong to a camera that no longer + // exists. + void SnapToWheel(); + + // Plugin unload: gives the camera's own values and the mouse back. + void Shutdown(SPF_Camera_API *camera); + + // The notice raised since the last call, if any. + Notice TakeNotice(); + + // The footsteps to play since the last call, as their volumes (0..1), + // for OnUpdate to hand to CabinWalkSounds. + std::vector TakeFootsteps() { return std::exchange(footsteps_, {}); } + + // --- Layout tuning (Quick Settings) --- + + // Takes the player to sit at `spot`, to check or tune its values + // from there: from the wheel too, standing up first if allowed (parked). + // Done on the next Update(), which has the truck and camera it needs. + void GoToSpot(Spot spot) { go_to_request_ = spot; } + + bool truck_known() const { return !truck_key_.empty(); } + const std::string &truck_name() const { return truck_name_; } + const CabinLayout &layout() const { return layout_; } + // The layout is the player's own, not the one shipped for the truck. + bool custom_layout() const { return custom_layout_; } + // The layout the plugin ships for this truck (its preset, else the one + // built around its centerline), under the player's own: what a layout + // value resets to. + const CabinLayout &shipped_layout() const { return shipped_layout_; } + // Uses `layout` for this truck, saved unless `save` is false (a slider + // still being dragged). + void SetLayout(const CabinLayout &layout, bool save = true); + // Drops the player's layout for this truck, back to the preset shipped + // with the plugin, or the default one. + void ResetLayout(); + +private: + enum class State { kSeated, kTransition, kWalking }; + // How a transition moves the head (see StartTransition): standing up from + // a seat, sitting down onto one, or moving between two seated spots. + enum class Path { kRise, kSit, kDirect }; + + struct Vec3 { + float x, y, z; + }; + struct SpotPose { + Vec3 pos; + float yaw_rad, pitch_rad; + }; + + SpotPose SpotAt(Spot spot) const; + // Where the player sitting at `spot` faces, on the floor plane. + Vec3 Facing(Spot spot) const; + // Where the player stands to get in or out of `spot`: straight above it, + // pulled into the floor area. + Vec3 Approach(Spot spot) const; + +public: + // False for a spot this truck's layout marks unusable (a folded-up + // passenger seat, no bunk). + bool CanSitAt(Spot spot) const { + switch (spot) { + case Spot::kPassenger: + return layout_.has_passenger; + case Spot::kBunkSit: + return layout_.has_bunk; + case Spot::kDriver: + break; + } + return true; + } + +private: + // The seat the player looks toward, for a tap while standing. + Spot LookedAtSpot() const; + + void HandleStandSitKey(float dt, const SPF_TruckData &truck, + SPF_Camera_API *camera); + void TryStandUp(const SPF_TruckData &truck, SPF_Camera_API *camera); + void HandleGoTo(Spot spot, const SPF_TruckData &truck, + SPF_Camera_API *camera); + void StandUp(); + void SitAt(Spot spot); + void GoToWheel(float duration); + void StartTransition(const Vec3 &to, Path path, float duration, State after, + Spot after_spot, bool turn); + float TransitionDuration(const Vec3 &to) const { + return TransitionDuration(pos_, to); + } + float TransitionDuration(const Vec3 &from, const Vec3 &to) const; + void UpdateTransition(float dt, SPF_Camera_API *camera); + // Cubic Bezier through p0..p3 at `u` (0..1). + static Vec3 Bezier(const Vec3 &p0, const Vec3 &p1, const Vec3 &p2, + const Vec3 &p3, float u); + void OnArrived(SPF_Camera_API *camera); + // Seated at a layout spot: follows its values live, so a layout slider + // moves (and turns) the player sitting there right away. + void FollowSeatedSpot(SPF_Camera_API *camera); + void UpdateWalking(float dt); + float ClampToFloor(float value, float min, float max, float &velocity, + math::SpringDamper1D &spring) const; + + void SetMouseBlocked(bool blocked); + // The walking and crouch keys, which the game uses at the wheel, hidden + // from it. + void SetWalkKeysBlocked(bool blocked); + void SaveLayout(); + // -1 on a right-hand drive truck, whose camera mirrors the seat position + // left to right but not the head's yaw (seen in game): a direction worked + // out from the yaw then has its x flipped to move the seat that way. + float CameraX() const { return right_hand_drive_ ? -1.0f : 1.0f; } + // Picks this truck's layout: the player's, else the preset, else one + // built around its centerline. + void LoadLayout(); + + EffectConfig config_; + SPF_KeyBinds_API *keybinds_api_; + SPF_KeyBinds_Handle *keybinds_handle_; + SPF_UI_API *ui_api_; + CabinLayoutStore *layouts_; + + bool enabled_ = true; + bool require_parking_brake_ = + settings::DefaultBool("settings.manual.cabin_walk.require_parking_brake"); + + State state_ = State::kSeated; + Spot spot_ = Spot::kDriver; // the seat, while kSeated + Vec3 pos_{}; // the head, without the step bob + // The player's own seat position and head rotation, last frame. + Vec3 base_{}; + float base_yaw_ = 0.0f, base_pitch_ = 0.0f; + std::optional> base_rotation_; + + // Current transition. + Vec3 from_{}, to_{}; + Path path_ = Path::kDirect; + float progress_ = 0.0f, duration_ = 1.0f; // seconds + State after_ = State::kWalking; + Spot after_spot_ = Spot::kDriver; + bool turn_ = false; // turns the view to the spot's look + float yaw_from_ = 0.0f, pitch_from_ = 0.0f; + // The head's path: a cubic Bezier from from_ to to_ through ctrl1_ and + // to_ + ctrl2_offset_ (relative, since to_ follows a seat that moves). + Vec3 ctrl1_{}, ctrl2_offset_{}; + // Sideways share of the move relative to the view, -1 (left) to 1 + // (right), for the head's lean on the way to a front seat (0 otherwise). + float lean_side_ = 0.0f; + // The body's own motion on top of the path, in degrees: looking down at + // where it steps or sits, leaning into the move. + float motion_pitch_deg_ = 0.0f, motion_roll_deg_ = 0.0f; + // The seated spot's look (degrees) last applied, to notice an edit. + float seated_yaw_deg_ = 0.0f, seated_pitch_deg_ = 0.0f; + + // Walking: m/s along x and z, head height. + math::SpringDamper1D vel_x_, vel_z_, height_; + std::optional walk_to_; // walking by itself to sit there + std::optional go_to_request_; // from GoToSpot, for Update() + math::SpringDamper1D bob_amount_; // 0 = still, 1 = walking speed + // 0 = with the cabin, 1 = upright: its last value, and at the start of + // the current transition. + float upright_now_ = 0.0f, upright_from_ = 0.0f; + float bob_phase_ = 0.0f; // radians, one half-turn per step + + // The stand/sit key: a tap acts on release, a hold once held long enough. + bool stand_sit_held_ = false; + bool stand_sit_long_done_ = false; + float stand_sit_held_time_ = 0.0f; + + InteriorCameraOverride camera_override_; + // The camera's re-activation (InteriorCameraOverride::refreshes) puts the + // head back to its default rotation shortly after: the rotation from + // before is held for a moment instead. + int seen_refreshes_ = 0; + float hold_rotation_time_ = 0.0f; // seconds left + std::pair held_rotation_{}; + bool mouse_blocked_ = false; + bool walk_keys_blocked_ = false; + bool left_interior_ = false; // since the last Update() + Notice notice_ = Notice::kNone; + std::vector footsteps_; + + std::string truck_key_, truck_name_; + CabinLayout layout_ = DefaultCabinLayout(); + bool custom_layout_ = false; + CabinLayout shipped_layout_ = DefaultCabinLayout(); + bool right_hand_drive_ = false; + // The cabin's centerline: the passenger seat mirrors the player's across + // it, and an uncalibrated truck's layout is built around it. + float centerline_x_ = kTypicalCenterlineX; +}; + +} // namespace motioncab diff --git a/src/effects/manual/cabin_walk/CabinWalkSounds.cpp b/src/effects/manual/cabin_walk/CabinWalkSounds.cpp new file mode 100644 index 0000000..46d94ae --- /dev/null +++ b/src/effects/manual/cabin_walk/CabinWalkSounds.cpp @@ -0,0 +1,81 @@ +#include "CabinWalkSounds.hpp" + +#include "core/PluginContext.hpp" + +namespace motioncab { + +namespace { + +constexpr const char *kBankFile = "MotionCab.bank"; +constexpr const char *kGuidsFile = "MotionCab.bank.guids"; + +constexpr const char *kFootstepEvent = "event:/motioncab/footsteps"; + +// Footsteps vary a little so that no two sound alike: pitch (a ratio) and +// volume ranges. +constexpr float kStepPitchMin = 0.93f, kStepPitchMax = 1.07f; +constexpr float kStepVolumeMin = 0.8f; + +} // namespace + +void CabinWalkSounds::Load(SPF_Sound_API *sound) { + PluginContext &ctx = Context(); + const std::string bank = dir_ + "/" + kBankFile; + const std::string guids = dir_ + "/" + kGuidsFile; + // SPF reads the .guids file itself, which names the bank's events: they + // are then found by path. + bank_ = sound->SND_LoadBankFile(bank.c_str(), guids.c_str()); + if (!bank_) + ctx.LogFmt(SPF_LOG_WARN, "(CabinWalk) %s not loaded", kBankFile); +} + +void CabinWalkSounds::Update(SPF_Sound_API *sound) { + if (!sound) + return; + if (!sound->SND_IsReady()) { + // The world is gone, and the bank's handle with it. + ready_ = false; + bank_ = nullptr; + return; + } + if (!ready_) { + ready_ = true; + if (!dir_.empty()) + Load(sound); + } +} + +void CabinWalkSounds::PlayFootstep(SPF_Sound_API *sound, float volume) { + if (!sound || !ready_ || !bank_) + return; + // Not kept: SPF renumbers the events whenever a bank is loaded or unloaded. + const int event = sound->SND_FindEventIndexByPath(kFootstepEvent); + if (event < 0) { + Context().LogThrottledFmt(SPF_LOG_WARN, "cabin_walk_sound_missing", 10000, + "(CabinWalk) %s not found", kFootstepEvent); + return; + } + void *instance = sound->SND_CreateEventInstance(event); + if (!instance) + return; + volume *= + std::uniform_real_distribution(kStepVolumeMin, 1.0f)(random_); + const float pitch = std::uniform_real_distribution( + kStepPitchMin, kStepPitchMax)(random_); + sound->SND_SetEventVolume(instance, volume); + sound->SND_SetEventPitch(instance, pitch); + sound->SND_StartEvent(instance); + // Freed by FMOD once it has finished playing. + sound->SND_ReleaseEvent(instance); +} + +void CabinWalkSounds::Shutdown(SPF_Sound_API *sound) { + if (sound && ready_ && sound->SND_IsReady()) { + if (bank_) + sound->SND_UnloadBank(bank_); + } + ready_ = false; + bank_ = nullptr; +} + +} // namespace motioncab diff --git a/src/effects/manual/cabin_walk/CabinWalkSounds.hpp b/src/effects/manual/cabin_walk/CabinWalkSounds.hpp new file mode 100644 index 0000000..fa4ad47 --- /dev/null +++ b/src/effects/manual/cabin_walk/CabinWalkSounds.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include "SPF_Sound_API.h" + +#include +#include + +namespace motioncab { + +// Plays Cabin Walk's footsteps through the game's FMOD system (SPF's sound +// API), from MotionCab's own bank in data/sounds/, built with FMOD Studio: +// MotionCab.bank with its event:/motioncab/footsteps event, and +// MotionCab.bank.guids (FMOD Studio's exported GUIDs.txt), which names the +// events' GUIDs. The events must output to a bus of the game's (the game +// refuses a second master bank): a bank routed to its own project's master +// bus loads, but stays silent. The FMOD project's master bus has the GUID +// of the game's bus:/cabin/interior ({7fe623a8-a2f0-4ba8-ad2a-9cad68432851}, +// read from the game's master bank), where the cabin's own sounds go: +// bus:/outside/* would muffle them like the world heard from inside. +// +// The sound system only exists while a game world is loaded, so the bank is +// loaded once it's ready, and again after a world reload. SPF reads the +// .guids file along with the bank, so the events are found by path. +class CabinWalkSounds { +public: + // `dir`: the folder holding the bank files. + void SetDirectory(std::string dir) { dir_ = std::move(dir); } + + // Loads the bank once the sound system is ready. Every frame. + void Update(SPF_Sound_API *sound); + + // `volume`: 0..1. + void PlayFootstep(SPF_Sound_API *sound, float volume); + + // Plugin unload: unloads the bank, so a reloaded plugin can load it + // again. + void Shutdown(SPF_Sound_API *sound); + +private: + void Load(SPF_Sound_API *sound); + + std::string dir_; + bool ready_ = false; // loaded for the current sound system + void *bank_ = nullptr; + std::minstd_rand random_{std::random_device{}()}; +}; + +} // namespace motioncab diff --git a/src/effects/manual/cabin_walk/InteriorCameraOverride.cpp b/src/effects/manual/cabin_walk/InteriorCameraOverride.cpp new file mode 100644 index 0000000..52ef5bc --- /dev/null +++ b/src/effects/manual/cabin_walk/InteriorCameraOverride.cpp @@ -0,0 +1,154 @@ +#include "InteriorCameraOverride.hpp" + +#include "math/Units.hpp" + +#include + +namespace motioncab { + +bool InteriorCameraOverride::Engage(SPF_Camera_API *camera) { + if (engaged_) + return true; + if (!camera->Cam_GetInteriorRotationLimits(&limits_.left, &limits_.right, + &limits_.up, &limits_.down) || + !camera->Cam_GetInteriorRotationDefaults(&default_yaw_deg_, + &default_pitch_deg_)) + return false; + + float look_yaw_rad = 0.0f, look_pitch_rad = 0.0f; + camera->Cam_GetInteriorHeadRot(&look_yaw_rad, &look_pitch_rad); + + const size_t count = camera->Cam_GetInteriorAzimuthOverridesCount(); + azimuths_.assign(count, Azimuth{}); + for (size_t i = 0; i < count; ++i) { + Azimuth &a = azimuths_[i]; + a.valid = camera->Cam_GetInteriorAzimuthOverrideStartAzimuth(i, &a.start) && + camera->Cam_GetInteriorAzimuthOverrideEndAzimuth(i, &a.end) && + camera->Cam_GetInteriorAzimuthOverrideOutside(i, &a.outside) && + camera->Cam_GetInteriorAzimuthOverrideStartHeadOffset( + i, &a.start_head[0], &a.start_head[1], &a.start_head[2]) && + camera->Cam_GetInteriorAzimuthOverrideEndHeadOffset( + i, &a.end_head[0], &a.end_head[1], &a.end_head[2]); + if (!a.valid) + continue; + // An empty angle range with no head offset: the override never kicks + // in (how SPF_CabinWalk disables them, confirmed in-game there). + camera->Cam_SetInteriorAzimuthOverrideStartAzimuth(i, 0.0f); + camera->Cam_SetInteriorAzimuthOverrideEndAzimuth(i, 0.0f); + camera->Cam_SetInteriorAzimuthOverrideOutside(i, false); + camera->Cam_SetInteriorAzimuthOverrideStartHeadOffset(i, 0.0f, 0.0f, 0.0f); + camera->Cam_SetInteriorAzimuthOverrideEndHeadOffset(i, 0.0f, 0.0f, 0.0f); + } + // Nothing close to the eyes clipped away (a headrest when looking back + // from the passenger seat, a wall walked up to). + has_near_plane_ = camera->Cam_GetInteriorNearPlane(&near_plane_); + engaged_ = true; + // The zones are the left-hand drive cabin's: a right-hand drive camera + // evaluates them at the mirrored yaw (as it does the defaults, see + // SetRightHandDrive). Their offsets need no mirroring, being in the seat + // frame, which the camera already mirrors. + const float look_yaw_deg = look_yaw_rad / math::kDegToRad; + engage_head_offset_ = + HeadOffsetAt(right_hand_drive_ ? -look_yaw_deg : look_yaw_deg); + RefreshSoundZones(camera); + return true; +} + +std::array InteriorCameraOverride::HeadOffsetAt(float yaw_deg) const { + std::array offset{}; + for (const Azimuth &a : azimuths_) { + if (!a.valid) + continue; + // Interpolated along the zone (which may run past 180 degrees). + const float lo = std::min(a.start, a.end), hi = std::max(a.start, a.end); + if (hi <= lo) + continue; + for (const float angle : {yaw_deg, yaw_deg + 360.0f, yaw_deg - 360.0f}) { + if (angle < lo || angle > hi) + continue; + const float t = (angle - a.start) / (a.end - a.start); + for (int k = 0; k < 3; ++k) + offset[k] = a.start_head[k] + (a.end_head[k] - a.start_head[k]) * t; + return offset; + } + } + return offset; +} + +void InteriorCameraOverride::SetLimits(SPF_Camera_API *camera, + const Limits &limits) { + if (engaged_) + camera->Cam_SetInteriorRotationLimits(limits.left, limits.right, limits.up, + limits.down); +} + +void InteriorCameraOverride::SetDefaults(SPF_Camera_API *camera, float yaw_deg, + float pitch_deg) { + if (engaged_) + camera->Cam_SetInteriorRotationDefaults(yaw_deg, pitch_deg); +} + +void InteriorCameraOverride::Restore(SPF_Camera_API *camera) { + if (!engaged_) + return; + camera->Cam_SetInteriorRotationLimits(limits_.left, limits_.right, limits_.up, + limits_.down); + camera->Cam_SetInteriorRotationDefaults(default_yaw_deg_, default_pitch_deg_); + const size_t count = camera->Cam_GetInteriorAzimuthOverridesCount(); + for (size_t i = 0; i < count && i < azimuths_.size(); ++i) { + const Azimuth &a = azimuths_[i]; + if (!a.valid) + continue; + camera->Cam_SetInteriorAzimuthOverrideStartAzimuth(i, a.start); + camera->Cam_SetInteriorAzimuthOverrideEndAzimuth(i, a.end); + camera->Cam_SetInteriorAzimuthOverrideOutside(i, a.outside); + camera->Cam_SetInteriorAzimuthOverrideStartHeadOffset( + i, a.start_head[0], a.start_head[1], a.start_head[2]); + camera->Cam_SetInteriorAzimuthOverrideEndHeadOffset( + i, a.end_head[0], a.end_head[1], a.end_head[2]); + } + if (has_near_plane_) + camera->Cam_SetInteriorNearPlane(near_plane_); + Forget(); + RefreshSoundZones(camera); +} + +void InteriorCameraOverride::RefreshSoundZones(SPF_Camera_API *camera) { + SPF_CameraType type; + if (!camera->Cam_GetCurrentCamera(&type) || type != SPF_CAMERA_INTERIOR) + return; + // Re-activating recenters the head onto the rotation defaults: made the + // current look first, so that it doesn't move (EndRefresh puts them back). + float yaw_rad = 0.0f, pitch_rad = 0.0f; + if (camera->Cam_GetInteriorHeadRot(&yaw_rad, &pitch_rad)) + RecenterOnto(camera, yaw_rad, pitch_rad); + camera->Cam_SwitchTo(SPF_CAMERA_INTERIOR); + ++refreshes_; +} + +void InteriorCameraOverride::RecenterOnto(SPF_Camera_API *camera, float yaw_rad, + float pitch_rad) { + const float yaw_deg = yaw_rad / math::kDegToRad; + camera->Cam_SetInteriorRotationDefaults( + right_hand_drive_ ? -yaw_deg : yaw_deg, pitch_rad / math::kDegToRad); +} + +void InteriorCameraOverride::EndRefresh(SPF_Camera_API *camera) { + // Both refreshes happen in the driver's seat, whose defaults are the + // truck's own (still known after Restore). + camera->Cam_SetInteriorRotationDefaults(default_yaw_deg_, default_pitch_deg_); +} + +void InteriorCameraOverride::KeepNearPlane(SPF_Camera_API *camera) { + // Every frame: the camera's re-activation may put its own back. + if (engaged_ && has_near_plane_) + camera->Cam_SetInteriorNearPlane(away_near_plane()); +} + +void InteriorCameraOverride::Forget() { + engaged_ = false; + has_near_plane_ = false; + azimuths_.clear(); +} + +} // namespace motioncab diff --git a/src/effects/manual/cabin_walk/InteriorCameraOverride.hpp b/src/effects/manual/cabin_walk/InteriorCameraOverride.hpp new file mode 100644 index 0000000..9cf515b --- /dev/null +++ b/src/effects/manual/cabin_walk/InteriorCameraOverride.hpp @@ -0,0 +1,110 @@ +#pragma once + +#include "SPF_Camera_API.h" + +#include +#include + +namespace motioncab { + +// Swaps the interior camera's rotation limits and defaults for our own while +// the player is out of the driver's seat (Cabin Walk), and puts the +// truck's own values back afterwards. +// +// Engage() also empties the azimuth overrides, the truck's head offsets +// for given look angles (leaning toward a mirror, or out of the window): +// they only make sense from the driver's seat, and would shove a standing +// head around. The game's sound keeps its own copy of them, taken when the +// interior camera is activated, and mixes the cabin as heard from outside +// (FMOD's cabin_out) in the zones flagged "outside": so both Engage() and +// Restore() re-activate the camera for the copy to follow. +class InteriorCameraOverride { +public: + // Degrees, as Cam_Get/SetInteriorRotationLimits take them: left and up + // positive, right and down negative. + struct Limits { + float left, right, up, down; + }; + + // Saves the truck's values, the first time since the last Restore() or + // Forget(), and empties the azimuth overrides. False if the camera isn't + // resolved; nothing is changed then. + bool Engage(SPF_Camera_API *camera); + // The head offset the azimuth overrides were adding at the look angle of + // the last Engage() (leaning toward a mirror), in the seat's frame: gone + // since, so the caller starts from it to avoid a jump. + const std::array &engage_head_offset() const { + return engage_head_offset_; + } + // The head offset the truck's own azimuth overrides give at `yaw_deg` + // (leaning toward a mirror), in the seat's frame; zero where none + // applies. Only while engaged: the camera's own are emptied meanwhile. + std::array HeadOffsetAt(float yaw_deg) const; + // The interior camera's near clipping distance while engaged: a fraction + // of the truck's own, so a headrest or wall right by the eyes isn't + // clipped away. + float away_near_plane() const { return near_plane_ * kAwayNearPlaneShare; } + // Keeps the engaged near plane on the camera. Every frame while engaged. + void KeepNearPlane(SPF_Camera_API *camera); + + // Only while engaged. + void SetLimits(SPF_Camera_API *camera, const Limits &limits); + // Degrees, the head rotation the native recenter key snaps to. + void SetDefaults(SPF_Camera_API *camera, float yaw_deg, float pitch_deg); + + // Puts the truck's values back, if engaged. + void Restore(SPF_Camera_API *camera); + + // Drops the saved values without writing them: after a truck change or a + // world reload, when they belong to a camera that no longer exists. + void Forget(); + + // A right-hand drive camera turns the head to the opposite of the default + // yaw when re-activated (seen in game), so a refresh writes it mirrored. + void SetRightHandDrive(bool right_hand_drive) { + right_hand_drive_ = right_hand_drive; + } + + bool engaged() const { return engaged_; } + // Counts the camera's re-activations. Each one points the rotation + // defaults at the current look for the recenter it causes a frame or so + // later; the caller holds the look meanwhile, then calls EndRefresh() to + // put the truck's defaults back. + int refreshes() const { return refreshes_; } + void EndRefresh(SPF_Camera_API *camera); + // The truck's own values; valid only while engaged. + const Limits &original_limits() const { return limits_; } + float original_default_yaw() const { return default_yaw_deg_; } + float original_default_pitch() const { return default_pitch_deg_; } + +private: + // Re-activates the interior camera, if it's the current one, so the + // game's sound takes a new copy of the azimuth overrides (seen in game: + // a view switch and back ends the outside mix). + void RefreshSoundZones(SPF_Camera_API *camera); + // Sets the rotation defaults so that the camera's next activation + // recenters the head onto this look (radians, as the head rotation), + // mirrored for a right-hand drive camera (see SetRightHandDrive). + void RecenterOnto(SPF_Camera_API *camera, float yaw_rad, float pitch_rad); + + struct Azimuth { + bool valid; + float start, end; + bool outside; + float start_head[3], end_head[3]; + }; + + static constexpr float kAwayNearPlaneShare = 0.25f; + + bool engaged_ = false; + bool right_hand_drive_ = false; + float near_plane_ = 0.0f; + bool has_near_plane_ = false; + int refreshes_ = 0; + std::array engage_head_offset_{}; + Limits limits_{}; + float default_yaw_deg_ = 0.0f, default_pitch_deg_ = 0.0f; + std::vector azimuths_; +}; + +} // namespace motioncab diff --git a/src/effects/road/RoadIrregularityEffect.hpp b/src/effects/road/RoadIrregularityEffect.hpp index c338c8d..2a041dc 100644 --- a/src/effects/road/RoadIrregularityEffect.hpp +++ b/src/effects/road/RoadIrregularityEffect.hpp @@ -24,6 +24,9 @@ class RoadIrregularityEffect final : public ConfigurableEffect { SPF_Config_Handle *config_handle); void Reset() override; + // Road motion is for driving: standing in the parked truck, it'd be idle + // noise. + bool NeedsDriverSeat() const override { return true; } HeadOffset Update(float dt, const SPF_TruckData &truck, const SPF_Controls &controls) override; void OnTruckConstantsChanged(const SPF_TruckConstants &constants) override; diff --git a/src/effects/road/SpeedShakeEffect.hpp b/src/effects/road/SpeedShakeEffect.hpp index cc97a53..bee9891 100644 --- a/src/effects/road/SpeedShakeEffect.hpp +++ b/src/effects/road/SpeedShakeEffect.hpp @@ -31,6 +31,9 @@ class SpeedShakeEffect final : public ConfigurableEffect { SPF_Config_Handle *config_handle); void Reset() override; + // Road motion is for driving: standing in the parked truck, it'd be idle + // noise. + bool NeedsDriverSeat() const override { return true; } HeadOffset Update(float dt, const SPF_TruckData &truck, const SPF_Controls &controls) override; void OnTruckConstantsChanged(const SPF_TruckConstants &constants) override; @@ -64,7 +67,7 @@ class SpeedShakeEffect final : public ConfigurableEffect { // up a new stretch of noise instead of replaying the same one. // Both wrapped with math::WrapNoisePhase. std::array phase_{}; - double envelope_phase_ = 0.0; // not reset: keeps the roughness varying + double envelope_phase_ = 0.0; // not reset: keeps the roughness varying math::SpringDamper1D entry_fade_; // 0 -> 1 after Reset(), no pop math::SpringDamper1D surface_gain_; // amplitude factor from the ground }; diff --git a/src/effects/road/SuspensionEffect.hpp b/src/effects/road/SuspensionEffect.hpp index 095db87..60230ae 100644 --- a/src/effects/road/SuspensionEffect.hpp +++ b/src/effects/road/SuspensionEffect.hpp @@ -30,6 +30,9 @@ class SuspensionEffect final : public ConfigurableEffect { : ConfigurableEffect(config_api, config_handle, "road", "suspension") {} void Reset() override; + // Road motion is for driving: standing in the parked truck, it'd be idle + // noise. + bool NeedsDriverSeat() const override { return true; } HeadOffset Update(float dt, const SPF_TruckData &truck, const SPF_Controls &controls) override; void OnTruckConstantsChanged(const SPF_TruckConstants &constants) override; diff --git a/src/ui/CabinLayoutPanel.cpp b/src/ui/CabinLayoutPanel.cpp new file mode 100644 index 0000000..8fa7c96 --- /dev/null +++ b/src/ui/CabinLayoutPanel.cpp @@ -0,0 +1,221 @@ +#include "CabinLayoutPanel.hpp" + +#include "SPF_Icons.h" +#include "core/Localization.hpp" +#include "core/PluginContext.hpp" +#include "effects/manual/cabin_walk/CabinLayouts.hpp" +#include "ui/Widgets.hpp" + +#include +#include +#include +#include + +namespace motioncab::ui { + +namespace { + +constexpr const char *kResetPopupId = "###confirm_reset_cabin_layout"; + +using Spot = CabinWalkEffect::Spot; + +const CabinLayoutField &FieldNamed(std::string_view name) { + for (const CabinLayoutField &field : CabinLayoutFields()) + if (name == field.name) + return field; + return CabinLayoutFields().front(); +} + +std::string Label(const std::string &key) { return loc::Tr(key.c_str()); } + +// A value's tooltip: what it moves and which way. Both seats' looks share +// theirs. +std::string Tip(std::string_view name) { + std::string_view tip = name; + if (name.ends_with("_yaw")) + tip = "yaw"; + else if (name.ends_with("_pitch")) + tip = "pitch"; + return Label("ui.cabin_walk.layout.tip." + std::string(tip)); +} + +// A value's row: its name, then [-] value [+] (StepperFloat). The value is +// in centimeters (meters stored) or degrees, stepped by 1 cm or 1 degree, +// dragged, typed with Ctrl+click, or right-clicked back to the shipped +// layout's. Applies live; saved when let go. +void DrawValueRow(SPF_UI_API *ui, CabinWalkEffect &walk, const char *name, + const std::string &label, bool indent = false) { + const CabinLayoutField &field = FieldNamed(name); + CabinLayout layout = walk.layout(); + float &value = layout.*field.member; + const float scale = field.angle ? 1.0f : 100.0f; + const char *format = field.angle ? "%.0f\xc2\xb0" : "%.1f cm"; + + ui->UI_TableNextRow(SPF_TABLE_ROW_FLAG_NONE, 0.0f); + ui->UI_TableNextColumn(); + if (indent) + ui->UI_Indent(12.0f); + ui->UI_AlignTextToFramePadding(); + ui->UI_Text(label.c_str()); + if (indent) + ui->UI_Unindent(12.0f); + ui->UI_TableNextColumn(); + float shown = value * scale; + const float shipped = walk.shipped_layout().*field.member; + const std::string tip = Tip(name); + switch (StepperFloat(ui, name, &shown, field.min * scale, field.max * scale, + 1.0f, format, shipped * scale, tip.c_str(), true)) { + case StepperEdit::kLive: + value = shown / scale; + walk.SetLayout(layout, false); + break; + case StepperEdit::kDone: + value = shown / scale; + walk.SetLayout(layout); + break; + case StepperEdit::kNone: + break; + } +} + +// A table of rows: the names' column as wide as they need. +bool BeginRows(SPF_UI_API *ui, const char *id) { + if (!ui->UI_BeginTable(id, 2, SPF_TABLE_FLAG_NONE, 0.0f, 0.0f, 0.0f)) + return false; + ui->UI_TableSetupColumn("name", SPF_TABLE_COLUMN_FLAG_WIDTH_FIXED, 0.0f, 0); + ui->UI_TableSetupColumn("value", SPF_TABLE_COLUMN_FLAG_WIDTH_STRETCH, 0.0f, + 0); + return true; +} + +// Going to sit at a seat (standing up first if at the wheel), to tune it +// from there: a row of its own, the buttons where the values start. +void DrawGoToRow(SPF_UI_API *ui, CabinWalkEffect &walk) { + ui->UI_TableNextRow(SPF_TABLE_ROW_FLAG_NONE, 0.0f); + ui->UI_TableNextColumn(); + ui->UI_AlignTextToFramePadding(); + ui->UI_Text(loc::Tr("ui.cabin_walk.layout.go")); + ui->UI_TableNextColumn(); + bool first = true; + for (const auto &[spot, group] : {std::pair{Spot::kPassenger, "passenger"}, + std::pair{Spot::kBunkSit, "bunk"}}) { + if (!first) + ui->UI_SameLine(0.0f, 6.0f); + first = false; + const std::string go = + WithIcon(ICON_FA_PERSON_WALKING, + Label(std::string("ui.cabin_walk.layout.group.") + group)) + + "###go_" + group; + ui->UI_BeginDisabled(!walk.CanSitAt(spot)); + if (MutedButton(ui, go.c_str())) + walk.GoToSpot(spot); + ui->UI_EndDisabled(); + } +} + +// Whether a seat can be sat in: a passenger seat folded up, a day cab's +// missing bunk. +void DrawUsableRow(SPF_UI_API *ui, CabinWalkEffect &walk, Spot spot) { + CabinLayout layout = walk.layout(); + bool &usable = + spot == Spot::kPassenger ? layout.has_passenger : layout.has_bunk; + ui->UI_TableNextRow(SPF_TABLE_ROW_FLAG_NONE, 0.0f); + ui->UI_TableNextColumn(); + ui->UI_Indent(12.0f); + ui->UI_AlignTextToFramePadding(); + ui->UI_Text(loc::Tr("ui.cabin_walk.layout.usable")); + ui->UI_Unindent(12.0f); + ui->UI_TableNextColumn(); + const std::string id = std::string("###usable_") + + (spot == Spot::kPassenger ? "passenger" : "bunk"); + if (ui->UI_Checkbox(id.c_str(), &usable)) + walk.SetLayout(layout); + ui->UI_SetItemTooltip( + Tip(spot == Spot::kPassenger ? "has_passenger" : "has_bunk").c_str()); +} + +// The truck, and dropping the player's own layout for it (only then). +void DrawTruckRow(SPF_UI_API *ui, CabinWalkEffect &walk) { + ui->UI_TableNextRow(SPF_TABLE_ROW_FLAG_NONE, 0.0f); + ui->UI_TableNextColumn(); + ui->UI_AlignTextToFramePadding(); + ui->UI_Text(loc::Tr("ui.cabin_walk.layout.truck")); + ui->UI_TableNextColumn(); + ui->UI_AlignTextToFramePadding(); + ui->UI_Text(WithIcon(ICON_FA_TRUCK, walk.truck_name()).c_str()); + ui->UI_SameLine(0.0f, 12.0f); + const std::string reset = WithIcon(ICON_FA_ARROW_ROTATE_LEFT, + loc::Tr("ui.cabin_walk.layout.reset")) + + "###reset_cabin_layout"; + ui->UI_BeginDisabled(!walk.custom_layout()); + if (MutedButton(ui, reset.c_str())) + ui->UI_OpenPopup(kResetPopupId, SPF_POPUP_FLAG_NONE); + ui->UI_EndDisabled(); + const std::string title = + loc::Tr("ui.cabin_walk.layout.reset_title") + std::string(kResetPopupId); + if (ConfirmPopup(ui, title, + loc::Tr("ui.cabin_walk.layout.reset_message", + {{"truck", walk.truck_name()}}) + .c_str(), + loc::Tr("ui.cabin_walk.layout.reset_confirm"))) + walk.ResetLayout(); +} + +// The truck and going to a seat, then every value by part of the cabin: the +// walkway's limits, the standing height and crouch, then each seat (whether +// it can be used, where it is and where it looks). +void DrawValues(SPF_UI_API *ui, CabinWalkEffect &walk) { + if (!BeginRows(ui, "cabin_values")) + return; + auto title = [&](const char *group) { + ui->UI_TableNextRow(SPF_TABLE_ROW_FLAG_NONE, 0.0f); + ui->UI_TableNextColumn(); + ui->UI_AlignTextToFramePadding(); + ui->UI_Text( + Label(std::string("ui.cabin_walk.layout.group.") + group).c_str()); + }; + auto rows = [&](std::initializer_list names) { + for (const char *name : names) + DrawValueRow(ui, walk, name, + Label(std::string("ui.cabin_walk.layout.") + name), true); + }; + DrawTruckRow(ui, walk); + DrawGoToRow(ui, walk); + title("walkway"); + rows({"floor_z_min", "floor_z_max", "floor_x_min", "floor_x_max"}); + title("heights"); + DrawValueRow(ui, walk, "stand_y", Label("ui.cabin_walk.layout.passenger_dy"), + true); + rows({"crouch_depth"}); + title("passenger"); + DrawUsableRow(ui, walk, Spot::kPassenger); + rows({"passenger_dx", "passenger_dy", "passenger_dz", "passenger_yaw", + "passenger_pitch"}); + title("bunk"); + DrawUsableRow(ui, walk, Spot::kBunkSit); + rows({"bunk_sit_x", "bunk_sit_y", "bunk_sit_z", "bunk_sit_yaw", + "bunk_sit_pitch"}); + ui->UI_EndTable(); +} + +} // namespace + +void DrawCabinLayoutPanel(SPF_UI_API *ui) { + const PluginContext &ctx = Context(); + CabinWalkEffect *walk = ctx.cabin_walk.get(); + const std::string advanced = + WithIcon(ICON_FA_SLIDERS, loc::Tr("ui.cabin_walk.layout.advanced")) + + "###cabin_layout_advanced"; + if (!ui->UI_CollapsingHeader(advanced.c_str(), SPF_TREE_NODE_FLAG_NONE)) + return; + // Only from the cabin, the game running: paused (menus) or in another + // view, Cabin Walk doesn't run, so an edit wouldn't show. + if (!walk || !walk->truck_known() || !ctx.was_interior_last_frame) { + ui->UI_TextDisabled(loc::Tr("ui.cabin_walk.layout.no_truck")); + return; + } + + DrawValues(ui, *walk); +} + +} // namespace motioncab::ui diff --git a/src/ui/CabinLayoutPanel.hpp b/src/ui/CabinLayoutPanel.hpp new file mode 100644 index 0000000..ad91384 --- /dev/null +++ b/src/ui/CabinLayoutPanel.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "SPF_UI_API.h" + +namespace motioncab::ui { + +// Cabin Walk's per-truck cabin layout, folded under its settings +// ("Advanced"): the truck with a reset of the player's own layout, Go There +// for each seat, and a row per layout value. +void DrawCabinLayoutPanel(SPF_UI_API *ui); + +} // namespace motioncab::ui diff --git a/src/ui/EffectTabs.cpp b/src/ui/EffectTabs.cpp index fbed70f..d686b6d 100644 --- a/src/ui/EffectTabs.cpp +++ b/src/ui/EffectTabs.cpp @@ -4,6 +4,7 @@ #include "core/Keybinds.hpp" #include "core/Localization.hpp" #include "core/SettingsSchema.hpp" +#include "ui/CabinLayoutPanel.hpp" #include "ui/Widgets.hpp" #include @@ -129,7 +130,8 @@ void DrawBool(SPF_UI_API *ui, SPF_Config_API *cfg, SPF_Config_Handle *h, cfg->Cfg_SetBool(h, setting.key, value); } -// One label|slider row. Right-click the slider to reset it to default. +// One label|[-] slider [+] row (StepperFloat), stepped by the format's last +// decimal. Right-click the slider to reset it to default. // `display_scale` shows the value (and the slider's range) multiplied by // it, e.g. to show a setting stored in km/h in mph; the stored value // doesn't change. @@ -144,16 +146,11 @@ void DrawSlider(SPF_UI_API *ui, SPF_Config_API *cfg, SPF_Config_Handle *h, ui->UI_TableSetColumnIndex(0); ui->UI_Text(SettingTitle(key)); ui->UI_TableSetColumnIndex(1); - char hidden_label[112]; - std::snprintf(hidden_label, sizeof(hidden_label), "##%s", key); - ui->UI_SetNextItemWidth(-1.0f); - if (ui->UI_SliderFloat(hidden_label, &value, setting.min * display_scale, - setting.max * display_scale, format, - SPF_SLIDER_FLAG_NONE)) + if (StepperFloat(ui, key, &value, setting.min * display_scale, + setting.max * display_scale, FormatStep(format), format, + setting.default_value * display_scale, + SettingDesc(key)) != StepperEdit::kNone) cfg->Cfg_SetFloat(h, key, value / display_scale); - if (ui->UI_IsItemClicked(SPF_MOUSE_BUTTON_RIGHT)) - cfg->Cfg_SetFloat(h, key, setting.default_value); - ui->UI_SetItemTooltip(SettingDesc(key)); } // A slider row for a speed setting, stored in km/h but shown in the @@ -202,10 +199,15 @@ struct EffectUi { const char *prefix; // "settings.." bool has_hint; // shows the ".hint" text under its toggle std::span keybinds; + // Drawn under its settings, for what doesn't fit a settings row. + void (*extra)(SPF_UI_API *ui) = nullptr; }; constexpr keybinds::Action kManualLookKeybinds[] = {keybinds::kLookLeft, keybinds::kLookRight}; +constexpr keybinds::Action kCabinWalkKeybinds[] = { + keybinds::kStandSit, keybinds::kWalkForward, keybinds::kWalkBack, + keybinds::kWalkLeft, keybinds::kWalkRight, keybinds::kCrouch}; // Every effect, in display order; each tab shows its own group's. constexpr EffectUi kEffects[] = { @@ -232,6 +234,8 @@ constexpr EffectUi kEffects[] = { "settings.manual.blindspot_viewer", false, {&keybinds::kBlindspotPeek, 1}}, + {ICON_FA_PERSON_WALKING, "settings.manual.cabin_walk", true, + kCabinWalkKeybinds, &DrawCabinLayoutPanel}, }; // Every effect section needs its "enabled" toggle in settings::kAll. @@ -268,11 +272,14 @@ void DrawEffect(SPF_UI_API *ui, SPF_Config_API *cfg, SPF_Config_Handle *h, for (const keybinds::Action &action : effect.keybinds) DrawKeybindRow(ui, action); for (const settings::Setting &s : settings::kAll) { - if (settings::InEffect(s, effect.prefix) && !IsEnabledToggle(s)) - DrawSettingRow(ui, cfg, h, s); + if (!settings::InEffect(s, effect.prefix) || IsEnabledToggle(s)) + continue; + DrawSettingRow(ui, cfg, h, s); } EndSettingsTable(ui); } + if (effect.extra) + effect.extra(ui); ui->UI_EndDisabled(); } @@ -284,8 +291,7 @@ void UpdateLabelColumnWidth(SPF_UI_API *ui) { void DrawEffectTab(SPF_UI_API *ui, SPF_Config_API *cfg, SPF_Config_Handle *h, std::string_view group) { - const std::string group_prefix = - "settings." + std::string(group) + "."; + const std::string group_prefix = "settings." + std::string(group) + "."; for (const EffectUi &effect : kEffects) { if (std::string_view(effect.prefix).starts_with(group_prefix)) DrawEffect(ui, cfg, h, effect); diff --git a/src/ui/Widgets.cpp b/src/ui/Widgets.cpp index 45c38a2..e867088 100644 --- a/src/ui/Widgets.cpp +++ b/src/ui/Widgets.cpp @@ -4,7 +4,11 @@ #include "core/Localization.hpp" #include "core/PluginContext.hpp" +#include +#include +#include #include +#include namespace motioncab::ui { @@ -69,6 +73,54 @@ int PushBrandRounding(SPF_UI_API *ui) { return 7; } +StepperEdit StepperFloat(SPF_UI_API *ui, const char *id, float *value, + float min, float max, float step, const char *format, + float reset_value, const char *tooltip, bool drag) { + StepperEdit edit = StepperEdit::kNone; + // Square buttons at both ends, the value filling the width between. + const float button = ui->UI_GetFrameHeight(); + constexpr float kGap = 4.0f; + float avail_w = 0.0f, avail_h = 0.0f; + ui->UI_GetContentRegionAvail(&avail_w, &avail_h); + ui->UI_PushID_Str(id); + auto step_button = [&](const char *icon, float sign) { + if (MutedButton(ui, icon, button)) { + *value = std::clamp(*value + sign * step, min, max); + edit = StepperEdit::kDone; + } + }; + step_button(ICON_FA_MINUS "###minus", -1.0f); + ui->UI_SameLine(0.0f, kGap); + ui->UI_SetNextItemWidth(std::max(avail_w - 2.0f * (button + kGap), 40.0f)); + const bool moved = + drag ? ui->UI_DragFloat("###value", value, (max - min) / 500.0f, min, max, + format, SPF_SLIDER_FLAG_ALWAYS_CLAMP) + : ui->UI_SliderFloat("###value", value, min, max, format, + SPF_SLIDER_FLAG_ALWAYS_CLAMP); + if (moved && edit == StepperEdit::kNone) + edit = StepperEdit::kLive; + if (ui->UI_IsItemDeactivatedAfterEdit()) + edit = StepperEdit::kDone; + if (ui->UI_IsItemClicked(SPF_MOUSE_BUTTON_RIGHT)) { + *value = reset_value; + edit = StepperEdit::kDone; + } + if (tooltip) + ui->UI_SetItemTooltip(tooltip); + ui->UI_SameLine(0.0f, kGap); + step_button(ICON_FA_PLUS "###plus", 1.0f); + ui->UI_PopID(); + return edit; +} + +float FormatStep(const char *format) { + // "%.f": n decimals. + const char *dot = std::strstr(format, "%."); + if (!dot || !std::isdigit(static_cast(dot[2]))) + return 1.0f; + return std::pow(10.0f, -static_cast(dot[2] - '0')); +} + bool MutedButton(SPF_UI_API *ui, const char *label, float width) { PushMutedButtonColors(ui); const bool clicked = ui->UI_Button(label, width, 0.0f); diff --git a/src/ui/Widgets.hpp b/src/ui/Widgets.hpp index cae5ffc..76c4cca 100644 --- a/src/ui/Widgets.hpp +++ b/src/ui/Widgets.hpp @@ -29,6 +29,25 @@ int PushBrandRounding(SPF_UI_API *ui); // keybind buttons, instead of the brighter brand red of regular buttons. bool MutedButton(SPF_UI_API *ui, const char *label, float width = 0.0f); +// How a value was edited this frame (see StepperFloat). +enum class StepperEdit { + kNone, + kLive, // being dragged: apply it, save once let go + kDone, // stepped, reset, typed or let go: save it +}; + +// [-] value [+] filling the width: the value slid (or, with `drag`, +// dragged without a range bar), stepped by `step` with the buttons, +// Ctrl+click to type it, right-click back to `reset_value`. `tooltip`, if +// any, shows over the value. +StepperEdit StepperFloat(SPF_UI_API *ui, const char *id, float *value, + float min, float max, float step, const char *format, + float reset_value, const char *tooltip = nullptr, + bool drag = false); + +// The step a printf-style format shows: 1 for "%.0f", 0.01 for "%.2f". +float FormatStep(const char *format); + // " ", for the icon-prefixed labels used all over the window. std::string WithIcon(const char *icon, std::string_view text);