├── DCS-Input-Command-Injector-Quaggles ├── VERSION.txt ├── README.txt └── DCS-Input-Command-Injector-Quaggles │ └── Scripts │ └── Input │ └── Data.lua ├── InputCommands.zip ├── Inject.lua └── README.md /DCS-Input-Command-Injector-Quaggles/VERSION.txt: -------------------------------------------------------------------------------- 1 | 1.0.13 -------------------------------------------------------------------------------- /InputCommands.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quaggles/dcs-input-command-injector/HEAD/InputCommands.zip -------------------------------------------------------------------------------- /DCS-Input-Command-Injector-Quaggles/README.txt: -------------------------------------------------------------------------------- 1 | Modifies the DCS control scripts to allow merging of user configured input commands from "Saved Games/DCS/" without modifying the original source files: https://github.com/Quaggles/dcs-input-command-injector/ -------------------------------------------------------------------------------- /Inject.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Insert this code into "DCSWorld\Scripts\Input\Data.lua" inside the function declaration for "loadDeviceProfileFromFile" 3 | search for the line 'local function loadDeviceProfileFromFile(filename, deviceName, folder,keep_G_untouched)' and paste this function below it 4 | Then add the line: 5 | QuagglesInputCommandInjector(deviceGenericName, filename, folder, env, result) 6 | into the "loadDeviceProfileFromFile" function below the line: 7 | status, result = pcall(f) 8 | ]]-- 9 | local function QuagglesInputCommandInjector(deviceGenericName, filename, folder, env, result) 10 | local quagglesLogName = 'Quaggles.InputCommandInjector' 11 | local quagglesLoggingEnabled = false 12 | -- Returns true if string starts with supplied string 13 | local function StartsWith(String,Start) 14 | return string.sub(String,1,string.len(Start))==Start 15 | end 16 | 17 | if quagglesLoggingEnabled then log.write(quagglesLogName, log.INFO, 'Detected loading of type: "'..deviceGenericName..'", filename: "'..filename..'"') end 18 | -- Only operate on files that are in this folder 19 | local targetPrefixForAircrafts = "./Mods/aircraft/" 20 | local targetPrefixForDotConfig = "./Config/Input/" 21 | local targetPrefixForConfig = "Config/Input/" 22 | local targetPrefix = nil 23 | if StartsWith(filename, targetPrefixForAircrafts) and StartsWith(folder, targetPrefixForAircrafts) then 24 | targetPrefix = targetPrefixForAircrafts 25 | elseif StartsWith(filename, targetPrefixForDotConfig) and StartsWith(folder, targetPrefixForDotConfig) then 26 | targetPrefix = targetPrefixForDotConfig 27 | elseif StartsWith(filename, targetPrefixForConfig) then 28 | targetPrefix = targetPrefixForConfig 29 | end 30 | if targetPrefix then 31 | -- Transform path to user folder 32 | local newFileName = filename:gsub(targetPrefix, lfs.writedir():gsub('\\','/').."InputCommands/") 33 | if quagglesLoggingEnabled then log.write(quagglesLogName, log.INFO, '--Translated path: "'..newFileName..'"') end 34 | 35 | -- If the user has put a file there continue 36 | if lfs.attributes(newFileName) then 37 | if quagglesLoggingEnabled then log.write(quagglesLogName, log.INFO, '----Found merge at: "'..newFileName..'"') end 38 | --Configure file to run in same environment as the default command entry file 39 | local f, err = loadfile(newFileName) 40 | if err ~= nil then 41 | log.write(quagglesLogName, log.ERROR, '------Failure loading: "'..tostring(newFileName)..'"'..' Error: "'..tostring(err)..'"') 42 | return 43 | else 44 | setfenv(f, env) 45 | local statusInj, resultInj 46 | statusInj, resultInj = pcall(f) 47 | 48 | -- Merge resulting tables 49 | if statusInj then 50 | if result.keyCommands and resultInj.keyCommands then -- If both exist then join 51 | env.join(result.keyCommands, resultInj.keyCommands) 52 | elseif resultInj.keyCommands then -- If just the injected one exists then use it 53 | result.keyCommands = resultInj.keyCommands 54 | end 55 | if deviceGenericName ~= "Keyboard" then -- Don't add axisCommands for keyboard 56 | if result.axisCommands and resultInj.axisCommands then -- If both exist then join 57 | env.join(result.axisCommands, resultInj.axisCommands) 58 | elseif resultInj.axisCommands then -- If just the injected one exists then use it 59 | result.axisCommands = resultInj.axisCommands 60 | end 61 | end 62 | if quagglesLoggingEnabled then log.write(quagglesLogName, log.INFO, '------Merge successful') end 63 | else 64 | if quagglesLoggingEnabled then log.write(quagglesLogName, log.INFO, '------Merge failed: "'..tostring(statusInj)..'"') end 65 | end 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DCS Input Command Injector by Quaggles 2 | 3 | ![image](https://user-images.githubusercontent.com/8382945/113183515-75dbfb00-9297-11eb-965a-492fd9789c26.png) 4 | 5 | ## Summary 6 | 7 | A mod that allows you to configure custom input commands inside your `Saved Games/DCS/` folder instead of inside your game folder, when DCS is run these commands are merged in with the default aircraft commands. This method avoids having to manually merge your command changes into each aircrafts default commands when DCS updates. 8 | 9 | After reading the install guide I'd recommend also looking at the **[DCS Community Keybinds](https://github.com/Munkwolf/dcs-community-keybinds)** project by *Munkwolf*, it uses this mod and contains many community requested input commands without you needing to code them manually. 10 | 11 | ## The goal of this mod 12 | 13 | Commonly in DCS users with unique input systems will need to create custom input commands to allow them to use certain aircraft functions with their HOTAS. Some examples are: 14 | 15 | * Configuring 3 way switches on a Thrustmaster Warthog HOTAS to control switches the module developer never intended to be controlled by a 3 way switch 16 | * Configuring actions that only trigger a cockpit switch while a button is held, for example using the trigger safety on a VKB Gunfighter Pro to turn on Master Arm while it's flipped down and then turn off Master Arm when flipped up 17 | * Adding control entries that the developer forgot, for example the Ka-50 has no individual "Gear Up" and Gear Down" commands, only a gear toggle 18 | 19 | In my case, on my Saitek X-55 Throttle there is an airbrake slider switch that when engaged registers as a single button being held down, when the slider is disengaged the button is released. In DCS by default few aircraft support this type of input so a custom input command is needed, in my case for the F/A-18C: 20 | 21 | ```lua 22 | {down = hotas_commands.THROTTLE_SPEED_BRAKE, up = hotas_commands.THROTTLE_SPEED_BRAKE, cockpit_device_id = devices.HOTAS, value_down = -1.0, value_up = 1.0, name = 'Speed Brake Hold', category = {'Quaggles Custom'}}, 23 | ``` 24 | 25 | Until now the solution was to find the control definition file `DCSWorld\Mods\aircraft\FA-18C\Input\FA-18C\joystick\default.lua` and insert your custom command somewhere inside of it, if you weren't using a mod manager then every time the game was updated your change would be erased and you'd need reinsert your commands into the files for every aircraft you changed. 26 | 27 | If you were using a mod manager such as Open Mod Manager/OVGME if you reapplied your mod after an update and the developers had changed the input commands things could break and conflict. 28 | 29 | With this mod you should just need to re-enable it after every DCS update with Open Mod Manager/OVGME and your custom commands are safe with no need no change anything. 30 | 31 | ## Installation 32 | 33 | 1. Go to the [latest release](https://github.com/Quaggles/dcs-input-command-injector/releases/latest) 34 | 2. Download `DCS-Input-Command-Injector-Quaggles.zip` 35 | 36 | ### [Open Mod Manager (Recommended)](https://github.com/sedenion/OpenModMan/releases/) 37 | 3. Drop the zip file in your mod directory 38 | 4. Enable mod in Open Mod Manager 39 | 5. Reenable with each DCS update 40 | 41 | ### Manual 42 | 3. Extract the zip 43 | 4. Find the `DCS-Input-Command-Injector-Quaggles/Scripts` folder 44 | 5. Move it into your `DCSWorld/` folder 45 | 6. Windows Explorer will ask you if you want to replace `Data.lua`, say yes 46 | 7. Repeat this process every DCS update, if you use Open Mod Manager/OVGME you can just reenable the mod and it handles this for you 47 | 48 | ## Configuration 49 | 50 | New commands are configured in the `Saved Games\DCS\InputCommands` directory, lets go through how to configure a hold command for the speedbrake on the F/A-18C Hornet. 51 | 52 | ### Setting the folder structure 53 | 54 | * ***Recommended*** Grab the premade structure with empty lua files, download and extract the [Input Commands folder](/InputCommands.zip) into your `C://Saved Games/DCS/` directory 55 | 56 | For the F/A-18C the default input files are located in `DCSWorld\Mods\aircraft\FA-18C\Input\FA-18C`, inside this directory are folders with the generic names of your input devices, these can include `joystick`, `keyboard`, `mouse`, `trackir` and `headtracker`. Each generic input folder contains `default.lua` which is the default set of commands the developer has configured, this is an important reference when making your own commands. It also contains many lua files for automatic binding of common hardware like the Thrustmaster Warthog HOTAS but these can be ignored (`*.diff.lua`). 57 | 58 | The DCS input folder structure needs be duplicated so that the folders relative to `DCSWorld\Mods\aircraft` are placed in `Saved Games\DCS\InputCommands`. The folder structure needs to match EXACTLY for each generic input device you want to add commands to. In my F/A-18C Speedbrake Hold example that means I will create the structure `Saved Games\DCS\InputCommands\FA-18C\Input\FA-18C\joystick\`, for an F-14B in the RIO seat I would create `Saved Games\DCS\InputCommands\F14\Input\F-14B-RIO\joystick`. To find the structure for other aircraft browse to `DCSWorld\Mods\aircraft` and follow the folder structure from there until you find the `joystick`,`keyboard`,etc folders for that aircraft. 59 | 60 | IMPORTANT: For some aircraft the 1st and 3rd folders have different names, for example `F14\Input\F-14B-Pilot` make sure this structure is followed correctly or your inputs won't be found. 61 | 62 | An example of the folder structure for some aircraft I have configured: 63 | 64 | ![image](https://user-images.githubusercontent.com/8382945/113282409-37dbe700-932a-11eb-89b2-e311afb75eb1.png) 65 | 66 | ### Creating your custom commands 67 | 68 | ![image](https://user-images.githubusercontent.com/8382945/113173913-37414300-928d-11eb-91ad-6e09b6f64a8b.png) 69 | 70 | Inside the generic input folder `Saved Games\DCS\InputCommands\FA-18C\Input\FA-18C\joystick\` we will create a lua script called `default.lua`, paste in the following text, it contains the Speedbrake Hold example and some commented out templates for the general structure of commands 71 | 72 | ```lua 73 | return { 74 | keyCommands = { 75 | {down = hotas_commands.THROTTLE_SPEED_BRAKE, up = hotas_commands.THROTTLE_SPEED_BRAKE, cockpit_device_id = devices.HOTAS, value_down = -1.0, value_up = 1.0, name = 'Speed Brake Hold', category = {'Quaggles Custom'}}, 76 | {down = hotas_commands.THROTTLE_SPEED_BRAKE, up = hotas_commands.THROTTLE_SPEED_BRAKE, cockpit_device_id = devices.HOTAS, value_down = 1.0, value_up = -1.0, name = 'Speed Brake Inverted', category = {'Quaggles Custom'}}, 77 | -- KeyCommand Template (Remove leading -- to uncomment) 78 | -- {down = CommandNameOnButtonDown, up = CommandNameOnButtonUp, name = 'NameForControlList', category = 'CategoryForControlList'}, 79 | } 80 | } 81 | ``` 82 | 83 | To work out what to put in these templates reference the developer provided default input command file, for the F/A-18C that is in `DCSWorld\Mods\aircraft\FA-18C\Input\FA-18C\joystick\default.lua` 84 | 85 | I'd recommend setting a unique category name for your custom commands so that they are easy to find in the menu. 86 | 87 | ### Hardlinking 88 | If you want to have a set of custom commands for both your HOTAS and your keyboard consider [hardlinking](https://schinagl.priv.at/nt/hardlinkshellext/linkshellextension.html) your `default.lua` from your `joystick` folder to your `keyboard` folder. 89 | 90 | By hardlinking both files look like they are in different directories to Windows and DCS but they actually refer to the same file on the disk meaning if you modify one you automatically modify the other. 91 | 92 | ## Examples 93 | 94 | ### Request AWACS Nearest Bandit 95 | Allows binding request bogey dope to your HOTAS, not every aircraft has this by default in DCS 96 | ```lua 97 | {down = iCommandAWACSBanditBearing, name='Request AWACS Nearest Bandit', category = 'Quaggles Custom'}, 98 | ``` 99 | 100 | ### Enable Su-25T Nightvision 101 | Works with Su-25A and A-10A as well if you add the commands for those aircraft, can be added for nearly any aircraft in the game (Except Su-27, Su-33, J-11, MiG-29, F-15C) if you [follow this guide](https://forums.eagle.ru/topic/134486-night-vision/?tab=comments#comment-2732313) 102 | ```lua 103 | {down = iCommandViewNightVisionGogglesOn, name = _('Night Vision Goggles'), category = _('Quaggles Custom')}, 104 | {pressed = iCommandPlane_Helmet_Brightess_Up, value_pressed = 0.5, name = _('Night Vision Goggles Gain Up'), category = _('Quaggles Custom')}, 105 | {pressed = iCommandPlane_Helmet_Brightess_Down, value_pressed = -0.5, name = _('Night Vision Goggles Gain Down'), category = _('Quaggles Custom')}, 106 | ``` 107 | 108 | ### Ka-50 Gear Up/Down 109 | ```lua 110 | {down = iCommandPlaneGearUp, name = 'Gear Up', category = 'Quaggles Custom'}, 111 | {down = iCommandPlaneGearDown, name = 'Gear Down', category = 'Quaggles Custom'}, 112 | ``` 113 | 114 | ### A-10C Speedbrake Temporary 115 | ```lua 116 | {down = iCommandPlane_HOTAS_SpeedBrakeSwitchAft, up = iCommandPlane_HOTAS_SpeedBrakeSwitchForward, name = 'HOTAS Speed Brake Switch (Hold)', category = 'Quaggles Custom', }, 117 | {down = iCommandPlane_HOTAS_SpeedBrakeSwitchForward, up = iCommandPlane_HOTAS_SpeedBrakeSwitchAft, name = 'HOTAS Speed Brake Switch (Inverted Hold)', category = 'Quaggles Custom', }, 118 | ``` 119 | 120 | ### A-10C VKB Gunfighter Flip Trigger controls master arm 121 | ```lua 122 | {down = iCommandPlaneAHCPMasterArm, up = iCommandPlaneAHCPMasterSafe, name = 'Master Arm Armed [else] Safe', category = 'Quaggles Custom', }, 123 | {down = iCommandPlaneAHCPMasterSafe, up = iCommandPlaneAHCPMasterArm, name = 'Master Arm Safe [else] Armed', category = 'Quaggles Custom', }, 124 | ``` 125 | 126 | ### F/A-18C Speedbrake Temporary 127 | ```lua 128 | {down = hotas_commands.THROTTLE_SPEED_BRAKE, up = hotas_commands.THROTTLE_SPEED_BRAKE, cockpit_device_id = devices.HOTAS, value_down = -1.0, value_up = 1.0, name = _('Speed Brake Hold'), category = {'Quaggles Custom'}}, 129 | {down = hotas_commands.THROTTLE_SPEED_BRAKE, up = hotas_commands.THROTTLE_SPEED_BRAKE, cockpit_device_id = devices.HOTAS, value_down = 1.0, value_up = -1.0, name = _('Speed Brake Hold Inverted'), category = {'Quaggles Custom'}}, 130 | ``` 131 | 132 | ### F/A-18C VKB Gunfighter Flip Trigger controls master arm 133 | ```lua 134 | {down = SMS_commands.MasterArmSw, up = SMS_commands.MasterArmSw, cockpit_device_id = devices.SMS, value_down = 1.0, value_up = 0.0, name = 'Master Arm Armed [else] Safe', category = {'Quaggles Custom'}}, 135 | {down = SMS_commands.MasterArmSw, up = SMS_commands.MasterArmSw, cockpit_device_id = devices.SMS, value_down = 0.0, value_up = 1.0, name = 'Master Arm Safe [else] Armed', category = {'Quaggles Custom'}}, 136 | ``` 137 | 138 | ### F-14 control TID range from front seat 139 | Note: May get broken by Heatblur at any time and could be considered unscrupulous on Multiplayer servers 140 | ```lua 141 | {down = device_commands.TID_range_knob, cockpit_device_id=devices.TID, value_down = -1.0, name = _('TID range: 25'), category = _('Quaggles Custom')}, 142 | {down = device_commands.TID_range_knob, cockpit_device_id=devices.TID, value_down = -0.5, name = _('TID range: 50'), category = _('Quaggles Custom')}, 143 | {down = device_commands.TID_range_knob, cockpit_device_id=devices.TID, value_down = 0.0, name = _('TID range: 100'), category = _('Quaggles Custom')}, 144 | {down = device_commands.TID_range_knob, cockpit_device_id=devices.TID, value_down = 0.5, name = _('TID range: 200'), category = _('Quaggles Custom')}, 145 | {down = device_commands.TID_range_knob, cockpit_device_id=devices.TID, value_down = 1.0, name = _('TID range: 400'), category = _('Quaggles Custom')}, 146 | ``` 147 | 148 | # FAQ 149 | ## My new input commands aren't showing up ingame 150 | First look at `Saved Games\DCS\Logs\dcs.log` at the bottom is likely an error telling you what went wrong in your code, for finding syntax errors in lua I would recommend [Visual Studio Code](https://code.visualstudio.com/) with the [vscode-lua extension](https://marketplace.visualstudio.com/items?itemName=trixnz.vscode-lua), it should highlight them all in red for you making it easy to find that missing comma 😄 151 | 152 | If you have no errors open the mod version of `Scripts\Input\Data.lua` and find the line `local quagglesLoggingEnabled = false` and set it to `true` you will get outputs in the `Saved Games\DCS\Logs\dcs.log` file as the script tries to handle every lua control file, it will tell you the path to the files it is trying to find in your Saved Games folder so you can ensure your folder structure is correct. Remember `../` in a path means get the parent directory. 153 | 154 | ## HELP MY CONTROLS MENU IS BLANK/MISSING 155 | Don't worry, this doesn't mean you've lost all your binds, it means there was an error somewhere in the code loading the commands, usually my injector catches any errors in the `default.lua` and reports them `Saved Games\DCS\Logs\dcs.log`. If you see nothing there it could mean that DCS has been updated and changed the format of the `Scripts/Input/Data.lua` file the mod changes, simple uninstall the mod and the game should work as normal, then wait for an updated version of the mod. 156 | 157 | ## Disclaimer 158 | I am not responsible for any corrupted binds when you use this mod, I've personally never had an issue with this method but I recommend always keeping backups of your binds (`Saved Games\DCS\Config\Input`) if you value them. 159 | -------------------------------------------------------------------------------- /DCS-Input-Command-Injector-Quaggles/DCS-Input-Command-Injector-Quaggles/Scripts/Input/Data.lua: -------------------------------------------------------------------------------- 1 | local InputUtils = require('Input.Utils' ) 2 | local Input = require('Input' ) 3 | local lfs = require('lfs' ) 4 | local U = require('me_utilities') 5 | local Serializer = require('Serializer' ) 6 | local textutil = require('textutil' ) 7 | local i18n = require('i18n' ) 8 | local log = require('log') 9 | 10 | local _ = i18n.ptranslate 11 | 12 | --forward declaration 13 | local unloadProfile 14 | local loadProfile 15 | local createAxisFilter 16 | local applyDiffToDeviceProfile_ 17 | local wizard_assigments 18 | local default_assignments 19 | 20 | local userConfigPath_ 21 | local sysConfigPath_ 22 | local sysPath_ 23 | 24 | local profiles_ = {} 25 | local aliases_ = {} 26 | local controller_ 27 | local uiLayerComboHashes_ 28 | local uiLayerKeyHashes_ 29 | local uiProfileName_ 30 | local disabledDevices_ = {} 31 | local disabledFilename_ = 'disabled.lua' 32 | local printLogEnabled_ = true 33 | local printFileLogEnabled_ = false 34 | 35 | local turnLocalizationHintsOn_ = false 36 | local insideLocalizationHintsFuncCounter_ = 0 37 | local insideExternalProfileFuncCounter_ = 0 38 | 39 | local function printLog(...) 40 | if printLogEnabled_ then 41 | print('Input:', ...) 42 | end 43 | end 44 | 45 | local function printFileLog(...) 46 | if printFileLogEnabled_ then 47 | print('Input:', ...) 48 | end 49 | end 50 | 51 | -- итератор по всем комбинациям устройства для команды 52 | -- использование: 53 | -- for combo in commandCombos(command, deviceName) do 54 | -- end 55 | local function commandCombos(command, deviceName) 56 | local pos = 0 57 | local combos 58 | local deviceCombos 59 | 60 | if command then 61 | combos = command.combos 62 | 63 | if combos then 64 | deviceCombos = combos[deviceName] 65 | end 66 | end 67 | 68 | return function() 69 | if deviceCombos then 70 | 71 | pos = pos + 1 72 | return deviceCombos[pos] 73 | end 74 | end 75 | end 76 | 77 | local function getUiProfileName() 78 | if not uiProfileName_ then 79 | local ProfileDatabase = require('Input.ProfileDatabase') 80 | 81 | uiProfileName_ = ProfileDatabase.getUiProfileName() 82 | end 83 | 84 | return uiProfileName_ 85 | end 86 | 87 | local function setProfileModified_(profile, modified) 88 | profile.modified = modified 89 | 90 | local uiProfileName = getUiProfileName() 91 | 92 | if profile.name == uiProfileName and modified then 93 | -- после изменения слоя UiLayer в командах юнитов могут появиться/исчезнуть конфликты 94 | -- поэтому загруженные юниты нужно загрузить заново 95 | local profilesToUnload = {} 96 | 97 | for i, p in ipairs(profiles_) do 98 | if p.name ~= uiProfileName then 99 | table.insert(profilesToUnload, p.name) 100 | end 101 | end 102 | 103 | uiLayerComboHashes_ = nil 104 | uiLayerKeyHashes_ = nil 105 | 106 | for i, name in ipairs(profilesToUnload) do 107 | unloadProfile(name) 108 | end 109 | end 110 | end 111 | 112 | local function findProfile_(profileName) 113 | for i, profile in ipairs(profiles_) do 114 | if profile.name == profileName then 115 | return profile 116 | end 117 | end 118 | end 119 | 120 | local function getLoadedProfile_(profileName) 121 | local profile = findProfile_(profileName) 122 | if profile and not profile.loaded then 123 | loadProfile(profile) 124 | end 125 | return profile 126 | end 127 | 128 | local function validateDeviceProfileCommand_(profileName, command, deviceName) 129 | local combos = command.combos 130 | 131 | if combos then 132 | local count = #combos 133 | 134 | for i = count, 1, -1 do 135 | local key = combos[i].key 136 | 137 | if key then 138 | if not InputUtils.getKeyBelongToDevice(key, deviceName) then 139 | printLog('Profile [' .. profileName .. '] command [' .. command.name .. '] contains combo key [' .. key .. '] not belong to device [' .. deviceName .. ']!') 140 | table.remove(combos, i) 141 | end 142 | end 143 | end 144 | 145 | if #combos == 0 then 146 | command.combos = nil 147 | end 148 | end 149 | end 150 | 151 | local function validateDeviceProfileCommands_(profileName, commands, deviceName) 152 | if commands then 153 | for i, command in ipairs(commands) do 154 | validateDeviceProfileCommand_(profileName, command, deviceName) 155 | end 156 | end 157 | end 158 | 159 | local function validateDeviceProfile_(profileName, deviceProfile, deviceName) 160 | validateDeviceProfileCommands_(profileName, deviceProfile.keyCommands, deviceName) 161 | validateDeviceProfileCommands_(profileName, deviceProfile.axisCommands, deviceName) 162 | end 163 | 164 | local function getCommandBelongsToCategory(category, command) 165 | local result = true 166 | if category then 167 | result = command.category == category 168 | if not result then 169 | if 'table' == type(command.category) then 170 | for i, categoryName in ipairs(command.category) do 171 | if categoryName == category then 172 | result = true 173 | break 174 | end 175 | end 176 | end 177 | end 178 | end 179 | return result 180 | end 181 | 182 | local function getProfileKeyCommandsCopy(profileName, category) 183 | local result = {} 184 | local profile = getLoadedProfile_(profileName) 185 | 186 | if profile then 187 | for commandHash, command in pairs(profile.keyCommands) do 188 | if getCommandBelongsToCategory(category, command) then 189 | table.insert(result, U.copyTable(nil, command)) 190 | end 191 | end 192 | end 193 | 194 | return result 195 | end 196 | 197 | local function getProfileAxisCommandsCopy(profileName) 198 | local result = {} 199 | local profile = getLoadedProfile_(profileName) 200 | 201 | if profile then 202 | for commandHash, command in pairs(profile.axisCommands) do 203 | table.insert(result, U.copyTable(nil, command)) 204 | end 205 | end 206 | 207 | return result 208 | end 209 | 210 | local function loadDeviceProfileFromFile(filename, deviceName, folder,keep_G_untouched) 211 | --[[ 212 | Insert this code into "DCSWorld\Scripts\Input\Data.lua" inside the function declaration for "loadDeviceProfileFromFile" 213 | search for the line 'local function loadDeviceProfileFromFile(filename, deviceName, folder,keep_G_untouched)' and paste this function below it 214 | Then add the line: 215 | QuagglesInputCommandInjector(deviceGenericName, filename, folder, env, result) 216 | into the "loadDeviceProfileFromFile" function below the line: 217 | status, result = pcall(f) 218 | ]]-- 219 | local function QuagglesInputCommandInjector(deviceGenericName, filename, folder, env, result) 220 | local quagglesLogName = 'Quaggles.InputCommandInjector' 221 | local quagglesLoggingEnabled = false 222 | -- Returns true if string starts with supplied string 223 | local function StartsWith(String,Start) 224 | return string.sub(String,1,string.len(Start))==Start 225 | end 226 | 227 | if quagglesLoggingEnabled then log.write(quagglesLogName, log.INFO, 'Detected loading of type: "'..deviceGenericName..'", filename: "'..filename..'"') end 228 | -- Only operate on files that are in this folder 229 | local targetPrefixForAircrafts = "./Mods/aircraft/" 230 | local targetPrefixForDotConfig = "./Config/Input/" 231 | local targetPrefixForConfig = "Config/Input/" 232 | local targetPrefix = nil 233 | if StartsWith(filename, targetPrefixForAircrafts) and StartsWith(folder, targetPrefixForAircrafts) then 234 | targetPrefix = targetPrefixForAircrafts 235 | elseif StartsWith(filename, targetPrefixForDotConfig) and StartsWith(folder, targetPrefixForDotConfig) then 236 | targetPrefix = targetPrefixForDotConfig 237 | elseif StartsWith(filename, targetPrefixForConfig) then 238 | targetPrefix = targetPrefixForConfig 239 | end 240 | if targetPrefix then 241 | -- Transform path to user folder 242 | local newFileName = filename:gsub(targetPrefix, lfs.writedir():gsub('\\','/').."InputCommands/") 243 | if quagglesLoggingEnabled then log.write(quagglesLogName, log.INFO, '--Translated path: "'..newFileName..'"') end 244 | 245 | -- If the user has put a file there continue 246 | if lfs.attributes(newFileName) then 247 | if quagglesLoggingEnabled then log.write(quagglesLogName, log.INFO, '----Found merge at: "'..newFileName..'"') end 248 | --Configure file to run in same environment as the default command entry file 249 | local f, err = loadfile(newFileName) 250 | if err ~= nil then 251 | log.write(quagglesLogName, log.ERROR, '------Failure loading: "'..tostring(newFileName)..'"'..' Error: "'..tostring(err)..'"') 252 | return 253 | else 254 | setfenv(f, env) 255 | local statusInj, resultInj 256 | statusInj, resultInj = pcall(f) 257 | 258 | -- Merge resulting tables 259 | if statusInj then 260 | if result.keyCommands and resultInj.keyCommands then -- If both exist then join 261 | env.join(result.keyCommands, resultInj.keyCommands) 262 | elseif resultInj.keyCommands then -- If just the injected one exists then use it 263 | result.keyCommands = resultInj.keyCommands 264 | end 265 | if deviceGenericName ~= "Keyboard" then -- Don't add axisCommands for keyboard 266 | if result.axisCommands and resultInj.axisCommands then -- If both exist then join 267 | env.join(result.axisCommands, resultInj.axisCommands) 268 | elseif resultInj.axisCommands then -- If just the injected one exists then use it 269 | result.axisCommands = resultInj.axisCommands 270 | end 271 | end 272 | if quagglesLoggingEnabled then log.write(quagglesLogName, log.INFO, '------Merge successful') end 273 | else 274 | if quagglesLoggingEnabled then log.write(quagglesLogName, log.INFO, '------Merge failed: "'..tostring(statusInj)..'"') end 275 | end 276 | end 277 | end 278 | end 279 | end 280 | 281 | local f, err = loadfile(filename) 282 | local result 283 | local deviceGenericName 284 | if deviceName ~= nil then 285 | deviceGenericName = InputUtils.getDeviceTemplateName(deviceName) 286 | end 287 | 288 | if not f then 289 | -- если пытаются загрузить раскладку для мыши из папки юнита 290 | if deviceGenericName == 'Mouse' and 291 | lfs.realpath(folder) ~= lfs.realpath('Config/Input/Aircrafts/Default/mouse/') and 292 | string.find(filename, 'default.lua') then 293 | 294 | -- то для мыши дефолтную раскладку объединяем с раскладкой для клавиатуры юнита 295 | local mouse = loadDeviceProfileFromFile('Config/Input/Aircrafts/Default/mouse/default.lua', 'Mouse', 'Config/Input/Aircrafts/Default/mouse/') 296 | local keyboard = loadDeviceProfileFromFile(folder .. '../keyboard/default.lua', 'Keyboard', folder) 297 | 298 | if keyboard and keyboard.keyCommands then 299 | for i, command in ipairs(keyboard.keyCommands) do 300 | command.combos = nil 301 | end 302 | 303 | -- join mouse and keyboard keyCommands 304 | for i, value in ipairs(keyboard.keyCommands) do 305 | table.insert(mouse.keyCommands, value) 306 | end 307 | end 308 | 309 | return mouse 310 | end 311 | end 312 | 313 | -- deviceGenericName will be used for automatic combo selection 314 | if f then 315 | 316 | -- cleanup cockpit devices variable [ACS-1111: FC3 kneeboard pages cannot be turned in some cases](https://jira.eagle.ru/browse/ACS-1111) 317 | local old_dev = _G.devices 318 | if not keep_G_untouched then 319 | _G.devices = nil 320 | end 321 | 322 | printFileLog('File[' .. filename .. '] opened successfully!') 323 | 324 | local noLocalize = function(s) 325 | return s 326 | end 327 | 328 | local setupEnv = function(env) 329 | env.devices = nil 330 | env.folder = folder 331 | env.filename = filename 332 | env.deviceName = deviceName 333 | env.external_profile = function (filename, folder_new) 334 | insideExternalProfileFuncCounter_ = insideExternalProfileFuncCounter_ + 1 335 | 336 | local old_filename = env.filename 337 | local old_folder = env.folder 338 | local fnew = folder_new or old_folder 339 | local res = loadDeviceProfileFromFile(filename,deviceName,fnew,true) 340 | 341 | env.filename = old_filename 342 | env.folder = old_folder 343 | 344 | insideExternalProfileFuncCounter_ = insideExternalProfileFuncCounter_ - 1 345 | 346 | return res 347 | end 348 | 349 | env.defaultDeviceAssignmentFor = function (assignment_name) 350 | 351 | if not wizard_assigments and userConfigPath_ ~= nil then 352 | local f, err = loadfile(userConfigPath_ .. 'wizard.lua') 353 | 354 | if f then 355 | wizard_assigments = f() 356 | else 357 | wizard_assigments = {} 358 | end 359 | end 360 | 361 | local assignments = nil 362 | 363 | if deviceName ~= nil then 364 | local wizard_result = wizard_assigments[deviceName] 365 | if wizard_result then 366 | local assignment = wizard_result[assignment_name] 367 | if assignment and assignment.key ~= nil then 368 | assignment.filter = createAxisFilter(assignment.filter) 369 | assignment.fromWizard = true 370 | return {assignment} 371 | end 372 | end 373 | assignments = default_assignments[deviceGenericName] 374 | end 375 | 376 | if assignments == nil then 377 | assignments = default_assignments.default 378 | end 379 | 380 | local assigned = assignments[assignment_name] 381 | 382 | if assigned ~= nil then 383 | if type(assigned) == 'table' then 384 | if assigned.key ~= nil then 385 | return {assigned} 386 | end 387 | else 388 | return {{key = assigned}} 389 | end 390 | end 391 | 392 | return nil 393 | end 394 | 395 | env.MultiEngineDefaultDeviceAssignmentForThrust = function () 396 | local common = env.defaultDeviceAssignmentFor("thrust") 397 | local left = env.defaultDeviceAssignmentFor("thrust_left") 398 | local right = env.defaultDeviceAssignmentFor("thrust_right") 399 | if not common then 400 | return nil,left,right 401 | end 402 | 403 | if left and left [1].key and 404 | right and right[1].key then 405 | return nil,left,right 406 | end 407 | return common,nil,nil 408 | end 409 | 410 | 411 | env.join = function(to, from) 412 | for i, value in ipairs(from) do 413 | table.insert(to, value) 414 | end 415 | 416 | return to 417 | end 418 | 419 | env.ignore_features = function(commands, features) 420 | local featuresHashTable = {} 421 | 422 | for i, feature in ipairs(features) do 423 | featuresHashTable[feature] = true 424 | end 425 | 426 | for i = #commands, 1, -1 do 427 | local command = commands[i] 428 | 429 | if command.features then 430 | for j, commandfeature in ipairs(command.features) do 431 | if featuresHashTable[commandfeature] then 432 | table.remove(commands, i) 433 | 434 | break 435 | end 436 | end 437 | end 438 | end 439 | end 440 | 441 | env.bindKeyboardCommandsToMouse = function(unitInputFolder) 442 | local keyboard = env.external_profile(unitInputFolder .. "keyboard/default.lua") 443 | local mouse = env.external_profile("Config/Input/Aircrafts/Default/mouse/default.lua") 444 | 445 | for i, command in ipairs(keyboard.keyCommands) do 446 | command.combos = nil 447 | end 448 | 449 | env.join(mouse.keyCommands, keyboard.keyCommands) 450 | 451 | return mouse 452 | end 453 | 454 | setmetatable(env, {__index = _G}) 455 | 456 | return env 457 | end 458 | 459 | local env = setupEnv(Input.getEnvTable().Actions) 460 | 461 | local status 462 | local nonLocalized 463 | 464 | -- для локализации у команд и категорий нужно сохранить английские названия 465 | if turnLocalizationHintsOn_ then 466 | local ff, err = loadfile(filename) 467 | 468 | if ff then 469 | local env2 = setupEnv({}) 470 | 471 | env2._ = noLocalize 472 | 473 | setfenv(ff, env2) 474 | 475 | insideLocalizationHintsFuncCounter_ = insideLocalizationHintsFuncCounter_ + 1 476 | 477 | local status, res = pcall(ff) 478 | 479 | if status then 480 | nonLocalized = { 481 | keyCommands = {}, 482 | axisCommands = {}, 483 | } 484 | 485 | for i, keyCommand in ipairs(res.keyCommands or {}) do 486 | table.insert(nonLocalized.keyCommands,{nameHint = keyCommand.name, categoryHint = keyCommand.category}) 487 | end 488 | 489 | for i, axisCommand in ipairs(res.axisCommands or {}) do 490 | table.insert(nonLocalized.axisCommands,{nameHint = axisCommand.name, categoryHint = axisCommand.category}) 491 | end 492 | 493 | else 494 | log.error(res); 495 | end 496 | 497 | insideLocalizationHintsFuncCounter_ = insideLocalizationHintsFuncCounter_ - 1 498 | end 499 | end 500 | 501 | if insideExternalProfileFuncCounter_ > 0 and insideLocalizationHintsFuncCounter_ > 0 then 502 | env._ = noLocalize 503 | else 504 | env._ = InputUtils.localizeInputString 505 | end 506 | 507 | setfenv(f, env) 508 | 509 | local status 510 | 511 | status, result = pcall(f) 512 | QuagglesInputCommandInjector(deviceGenericName, filename, folder, env, result) 513 | 514 | if status then 515 | if nonLocalized then 516 | for i, keyCommand in ipairs(result.keyCommands or {}) do 517 | keyCommand.nameHint = nonLocalized.keyCommands[i].nameHint 518 | keyCommand.categoryHint = nonLocalized.keyCommands[i].categoryHint 519 | end 520 | 521 | for i, axisCommand in ipairs(result.axisCommands or {}) do 522 | axisCommand.nameHint = nonLocalized.axisCommands[i].nameHint 523 | axisCommand.categoryHint = nonLocalized.axisCommands[i].categoryHint 524 | end 525 | end 526 | else -- это ошибка в скрипте! ее быть не должно! 527 | log.error(result); 528 | end 529 | 530 | if not keep_G_untouched then 531 | _G.devices = old_dev 532 | end 533 | else 534 | printFileLog(err) 535 | end 536 | 537 | return result, err 538 | end 539 | 540 | local function getProfileUserConfigPath_(profile) 541 | -- unitName может содержать недопустимые в имени файла символы (например / или * (F/A-18A)) 542 | local unitName = string.gsub(profile.unitName, '([%*/%?<>%|%\\%:"])', '') 543 | 544 | return string.format('%s%s/', userConfigPath_, unitName) 545 | end 546 | 547 | local function loadDeviceProfileDiffFromFile_(filename) 548 | local func, err = loadfile(filename) 549 | 550 | if func then 551 | local env = {} 552 | setfenv(func, env) 553 | local ok, res = pcall(func) 554 | if ok then 555 | printFileLog('File[' .. filename .. '] opened successfully!') 556 | return res 557 | else 558 | log.error('Input Error:' ..res) 559 | end 560 | else 561 | printFileLog(err) 562 | end 563 | end 564 | 565 | local function loadTemplateDeviceProfile(planesPath, profileFolder, deviceName) 566 | local deviceTypeName = InputUtils.getDeviceTypeName(deviceName) 567 | local templateName = InputUtils.getDeviceTemplateName(deviceName) 568 | local folder = planesPath .. profileFolder .. '/' .. deviceTypeName .. '/' 569 | local filename = templateName .. '.lua' 570 | 571 | return loadDeviceProfileFromFile(folder .. filename, deviceName, folder) 572 | end 573 | 574 | local function loadDefaultDeviceProfile(planesPath, profileFolder, deviceName) 575 | local deviceTypeName = InputUtils.getDeviceTypeName(deviceName) 576 | local folder = planesPath .. profileFolder .. '/' .. deviceTypeName .. '/' 577 | local filename = 'default.lua' 578 | 579 | return loadDeviceProfileFromFile(folder .. filename, deviceName, folder) 580 | end 581 | 582 | local function loadPluginDeviceProfile_(profileFolder, deviceName) 583 | local result 584 | local err1 585 | local err2 586 | 587 | result, err1 = loadTemplateDeviceProfile('', profileFolder, deviceName) 588 | 589 | if not result then 590 | result, err2 = loadDefaultDeviceProfile('', profileFolder, deviceName) 591 | end 592 | 593 | return result, err1, err2 594 | end 595 | 596 | local function collectErrors_(errors, result, ...) 597 | for i, err in ipairs({...}) do 598 | table.insert(errors, err) 599 | end 600 | 601 | return result 602 | end 603 | 604 | local function loadDeviceTemplateProfileDiff_(profile, deviceName) 605 | local diff 606 | local deviceTypeName = InputUtils.getDeviceTypeName(deviceName) 607 | local folder = profile.folder .. '/' .. deviceTypeName .. '/' 608 | local attributes = lfs.attributes(folder) 609 | 610 | if attributes and attributes.mode == 'directory' then 611 | local templateName = InputUtils.getDeviceTemplateName(deviceName) 612 | local filename = templateName .. '.diff.lua' 613 | 614 | diff = loadDeviceProfileDiffFromFile_(folder .. filename) 615 | end 616 | 617 | return diff 618 | end 619 | 620 | --!!!TEMPLATE DIFF IS NOT PART OF DEFAULT , USER DIFF WILL BE COPY OF TEMPLATE DIFF , SO TEMPLATE DIFF CAN BE USED ONLY FOR RESET PROCEDURE 621 | local function loadProfileDefaultDeviceProfile_(profile, deviceName , applyTemplateDiff) 622 | local folder = profile.folder 623 | local errors = {} 624 | 625 | local result = collectErrors_(errors, loadTemplateDeviceProfile('', folder, deviceName)) 626 | 627 | if not result then 628 | result = collectErrors_(errors, loadDefaultDeviceProfile('', folder, deviceName)) 629 | end 630 | 631 | if not result and profile.loadDefaultUnitProfile then 632 | result = collectErrors_(errors, loadTemplateDeviceProfile(sysPath_, 'Default', deviceName)) 633 | end 634 | 635 | if not result and profile.loadDefaultUnitProfile then 636 | result = collectErrors_(errors, loadDefaultDeviceProfile(sysPath_, 'Default', deviceName)) 637 | end 638 | 639 | if result and applyTemplateDiff then 640 | local templateDiff = loadDeviceTemplateProfileDiff_(profile, deviceName) 641 | applyDiffToDeviceProfile_(result, templateDiff) 642 | end 643 | 644 | if #errors > 0 then 645 | printFileLog('Profile [' .. profile.name .. '] errors in load process [' .. deviceName .. '] default profile!', table.concat(errors, '\n')) 646 | end 647 | 648 | return result 649 | end 650 | 651 | local function getComboReformersAreEqual_(reformers1, reformers2) 652 | if reformers1 then 653 | if reformers2 then 654 | local count = #reformers1 655 | 656 | if count == #reformers2 then 657 | for i, reformer1 in ipairs(reformers1) do 658 | local found = false 659 | 660 | for j, reformer2 in ipairs(reformers2) do 661 | if reformer1 == reformer2 then 662 | found = true 663 | break 664 | end 665 | end 666 | 667 | if not found then 668 | return false 669 | end 670 | end 671 | 672 | return true 673 | else 674 | return false 675 | end 676 | else 677 | return 0 == #reformers1 678 | end 679 | else 680 | if reformers2 then 681 | return 0 == #reformers2 682 | else 683 | return true 684 | end 685 | end 686 | end 687 | 688 | local function getCombosKeysAreEqual_(combo1, combo2) 689 | if combo1.key == combo2.key then 690 | return getComboReformersAreEqual_(combo1.reformers, combo2.reformers) 691 | end 692 | 693 | return false 694 | end 695 | 696 | local function findCombo_(combos, comboToFind) 697 | if not combos then 698 | return nil 699 | end 700 | for i, combo in ipairs(combos) do 701 | if getCombosKeysAreEqual_(combo, comboToFind) then 702 | return i 703 | end 704 | end 705 | end 706 | 707 | local function loadDeviceProfileDiff_(profile, deviceName) 708 | local diff 709 | local deviceTypeName = InputUtils.getDeviceTypeName(deviceName) 710 | local folder = string.format('%s%s/', getProfileUserConfigPath_(profile), deviceTypeName) 711 | local attributes = lfs.attributes(folder) 712 | 713 | if not attributes or attributes.mode ~= 'directory' then 714 | return nil 715 | end 716 | 717 | local filename = deviceName .. '.diff.lua' 718 | local diff = loadDeviceProfileDiffFromFile_(folder .. filename) 719 | if not diff then 720 | return 721 | end 722 | -- replace Backspace to Back in user .diff file 723 | -- due to renaming Back to Backspace 05.04.2018 :( 724 | for commandHash, info in pairs(diff.keyDiffs or {}) do 725 | for i, addInfo in ipairs(info.added or {}) do 726 | if addInfo.key == 'Backspace' then 727 | addInfo.key = 'Back' 728 | end 729 | end 730 | 731 | for i, removeInfo in ipairs(info.removed or {}) do 732 | if removeInfo.key == 'Backspace' then 733 | removeInfo.key = 'Back' 734 | end 735 | end 736 | end 737 | return diff 738 | end 739 | 740 | local function removeFoundCombos(commandCombos, removed) 741 | if not removed then 742 | return 743 | end 744 | for i, combo in ipairs(removed) do 745 | local index = findCombo_(commandCombos, combo) 746 | 747 | if index then 748 | table.remove(commandCombos, index) 749 | end 750 | end 751 | end 752 | 753 | local function applyAddedCombos_(commandCombos, added) 754 | if not added then 755 | return 756 | end 757 | for i, combo in ipairs(added) do 758 | local index = findCombo_(commandCombos, combo) -- avoid duplicates 759 | if not index then 760 | table.insert(commandCombos, combo) 761 | end 762 | end 763 | end 764 | 765 | local function getDiffComboInfos(diff) 766 | local diffInfos = {} 767 | 768 | for diffCommandHash, commandDiff in pairs(diff) do 769 | if commandDiff.added then 770 | for i, combo in ipairs(commandDiff.added) do 771 | local comboHash = InputUtils.getComboHash(combo.key, combo.reformers) 772 | 773 | diffInfos[comboHash] = diffInfos[comboHash] or {} 774 | diffInfos[comboHash].addedHash = diffCommandHash 775 | end 776 | end 777 | 778 | if commandDiff.removed then 779 | for i, combo in ipairs(commandDiff.removed) do 780 | local comboHash = InputUtils.getComboHash(combo.key, combo.reformers) 781 | 782 | diffInfos[comboHash] = diffInfos[comboHash] or {} 783 | diffInfos[comboHash].removedHash = diffCommandHash 784 | end 785 | end 786 | end 787 | 788 | return diffInfos 789 | end 790 | 791 | local function getDefaultCommandUpdated(commandCombos, commandHash, diffInfos) 792 | for i, combo in ipairs(commandCombos) do 793 | local diffInfo = diffInfos[InputUtils.getComboHash(combo.key, combo.reformers)] 794 | 795 | if diffInfo then 796 | if commandHash ~= diffInfo.addedHash and 797 | commandHash ~= diffInfo.removedHash then 798 | 799 | return true 800 | end 801 | end 802 | end 803 | 804 | return false 805 | end 806 | 807 | local function applyDiffToCommands_(commands, diff, commandHashFunc) 808 | if diff and next(diff) and commands then 809 | local diffInfos = getDiffComboInfos(diff) 810 | 811 | for i, command in ipairs(commands) do 812 | local hash = commandHashFunc(command) 813 | local commandCombos = command.combos 814 | 815 | if commandCombos then 816 | if getDefaultCommandUpdated(commandCombos, hash, diffInfos) then 817 | command.updated = true 818 | else 819 | 820 | local cleanupDefaultCommandCombos = removeFoundCombos 821 | 822 | -- Удаляем из дефолтной раскладки все комбинации, 823 | -- упомянутые в пользовательских данных. 824 | -- Сделано это для того, чтобы при добавлении дефолтного профиля устройства 825 | -- (например, при автоматическом обновлении программы) 826 | -- пользовательские настройки не конфликтовали с дефолтными настройками. 827 | for diffCommandHash, commandDiff in pairs(diff) do 828 | cleanupDefaultCommandCombos(commandCombos, commandDiff.added) 829 | cleanupDefaultCommandCombos(commandCombos, commandDiff.removed) 830 | cleanupDefaultCommandCombos(commandCombos, commandDiff.changed) 831 | end 832 | end 833 | end 834 | 835 | local commandDiff = diff[hash] 836 | local applyRemovedCombos_ = removeFoundCombos 837 | 838 | if commandDiff then 839 | if not commandCombos then 840 | commandCombos = {} 841 | command.combos = commandCombos 842 | end 843 | 844 | applyRemovedCombos_(commandCombos, commandDiff.removed) 845 | applyAddedCombos_(commandCombos, commandDiff.added) 846 | applyAddedCombos_(commandCombos, commandDiff.changed) 847 | end 848 | end 849 | end 850 | end 851 | 852 | local function createForceFeedbackSettings(forceFeedback) 853 | forceFeedback = forceFeedback or {} 854 | 855 | return { 856 | trimmer = forceFeedback.trimmer or 1.0, 857 | shake = forceFeedback.shake or 0.5, 858 | swapAxes = forceFeedback.swapAxes or false, 859 | invertX = forceFeedback.invertX or false, 860 | invertY = forceFeedback.invertY or false, 861 | ignore = forceFeedback.ignore or false, 862 | } 863 | end 864 | 865 | local function applyDiffToForceFeedback_(deviceProfile, diff) 866 | if diff then 867 | local forceFeedback = createForceFeedbackSettings(deviceProfile.forceFeedback) 868 | 869 | for key, value in pairs(diff) do 870 | forceFeedback[key] = value 871 | end 872 | 873 | deviceProfile.forceFeedback = forceFeedback 874 | end 875 | end 876 | 877 | local function loadDeviceProfile_(profile, deviceName) 878 | local errors = {} 879 | 880 | local result = collectErrors_(errors, loadPluginDeviceProfile_(profile.folder, deviceName)) 881 | 882 | if not result then 883 | result = loadProfileDefaultDeviceProfile_(profile, deviceName) 884 | end 885 | 886 | if not result and #errors > 0 then 887 | printFileLog('Profile [' .. profile.name .. '] cannot load device [' .. deviceName .. '] profile!', table.concat(errors, '\n')) 888 | end 889 | 890 | if not result then 891 | return nil 892 | end 893 | 894 | -- Remove combos intersections with wizard 895 | local validateIntersectionWithWizard = function(deviceName, commands) 896 | if commands == nil or type(commands) ~= 'table' then 897 | return 898 | end 899 | 900 | local commandHashToCombos = {} 901 | for i, command in ipairs(commands) do 902 | if command.combos then 903 | for j, combo in ipairs(command.combos) do 904 | local hash = deviceName.."["..InputUtils.createComboString(combo, deviceName).."]" 905 | commandHashToCombos[hash] = commandHashToCombos[hash] or {} 906 | table.insert(commandHashToCombos[hash], {combos = command.combos, name = command.name, index = j}) 907 | end 908 | end 909 | end 910 | 911 | for name, sameAssignments in pairs(commandHashToCombos) do 912 | if #sameAssignments > 1 then 913 | local wizardComboIndex 914 | for j, assignment in ipairs(sameAssignments) do 915 | if assignment.combos[assignment.index].fromWizard then 916 | wizardComboIndex = j 917 | break 918 | end 919 | end 920 | 921 | if wizardComboIndex then 922 | for j, assignment in ipairs(sameAssignments) do 923 | 924 | if j ~= wizardComboIndex then 925 | table.remove(assignment.combos, assignment.index) 926 | end 927 | end 928 | end 929 | end 930 | end 931 | end 932 | 933 | if type(result) == "table" then 934 | for name, commands in pairs(result) do 935 | validateIntersectionWithWizard(deviceName, commands) 936 | end 937 | end 938 | 939 | return result 940 | end 941 | 942 | local function createProfileTable_(name, folder, unitName, default, visible, loadDefaultUnitProfile) 943 | return { 944 | name = name, 945 | folder = folder, 946 | unitName = unitName, 947 | default = default, 948 | visible = visible, 949 | loadDefaultUnitProfile = loadDefaultUnitProfile, 950 | deviceProfiles = nil, 951 | forceFeedback = {}, 952 | loaded = false, 953 | modified = false, 954 | modifiers = {}, 955 | } 956 | end 957 | 958 | local function createProfileCategories(profile) 959 | local profileCategories = {} 960 | local categories = {} 961 | 962 | local addCategory = function(categoryName) 963 | if not categories[categoryName] then 964 | categories[categoryName] = true 965 | table.insert(profileCategories, categoryName) 966 | end 967 | end 968 | 969 | for commandHash, command in pairs(profile.keyCommands) do 970 | local category = command.category 971 | 972 | if category then 973 | if 'table' == type(category) then 974 | for i, categoryName in ipairs(category) do 975 | addCategory(categoryName) 976 | end 977 | else 978 | addCategory(category) 979 | end 980 | else 981 | printLog('Command ' .. command.name .. ' has no category in profile ' .. profile.name) 982 | end 983 | end 984 | 985 | profile.categories = profileCategories 986 | end 987 | 988 | local function getCommandDeviceNamesString(command) 989 | local result 990 | 991 | for deviceName, i in pairs(command.combos) do 992 | if result then 993 | result = result .. ', ' .. deviceName 994 | else 995 | result = deviceName 996 | end 997 | end 998 | 999 | return result 1000 | end 1001 | 1002 | local function sortDeviceCombosReformers_(deviceCombos) 1003 | for i, combo in ipairs(deviceCombos) do 1004 | local reformers = combo.reformers 1005 | 1006 | if reformers then 1007 | table.sort(reformers, textutil.Utf8Compare) 1008 | end 1009 | end 1010 | end 1011 | 1012 | local function copyDeviceCommandToProfileCommand(deviceName, deviceCommand, profileCommand) 1013 | for k, v in pairs(deviceCommand) do 1014 | if 'combos' ~= k then 1015 | profileCommand[k] = v 1016 | end 1017 | end 1018 | 1019 | local deviceCombos = U.copyTable(nil, deviceCommand.combos or {}) 1020 | 1021 | sortDeviceCombosReformers_(deviceCombos) 1022 | 1023 | profileCommand.combos[deviceName] = deviceCombos 1024 | end 1025 | 1026 | local function getReformerValid_(profileName, command, deviceName, reformer, modifiers, warnings) 1027 | local result = true 1028 | local modifier = modifiers[reformer] 1029 | 1030 | if modifier then 1031 | result = (nil ~= modifier.event) 1032 | 1033 | if not result then 1034 | printLog('Profile [' .. profileName.. '] command [' .. command.name .. '] contains unknown reformer key [' .. modifier.key .. '] in device [' .. deviceName .. '] profile!') 1035 | table.insert(warnings, string.format(_('Unknown reformer %s'), modifier.key)) 1036 | end 1037 | else 1038 | printLog('Profile [' .. profileName.. '] command [' .. command.name .. '] contains unknown reformer[' .. reformer .. '] in device [' .. deviceName .. '] profile!') 1039 | table.insert(warnings, string.format(_('Unknown reformer %s'), reformer)) 1040 | 1041 | result = false 1042 | end 1043 | 1044 | return result 1045 | end 1046 | 1047 | local function createKeyHash_(deviceName, key) 1048 | return string.format('%s[%s]', deviceName, key) 1049 | end 1050 | 1051 | local function createModifierHash_(name, modifiers) 1052 | local modifier = modifiers[name] 1053 | 1054 | if modifier then 1055 | return createKeyHash_(modifier.deviceName, modifier.key) 1056 | end 1057 | end 1058 | 1059 | local function createComboHash_(deviceName, combo, modifiers) 1060 | local hash = createKeyHash_(deviceName, combo.key) 1061 | 1062 | if combo.reformers then 1063 | local modifierHashes = {} 1064 | 1065 | for i, name in pairs(combo.reformers) do 1066 | local modifierHash = createModifierHash_(name, modifiers) 1067 | 1068 | if modifierHash then 1069 | table.insert(modifierHashes, modifierHash) 1070 | end 1071 | end 1072 | 1073 | if #modifierHashes > 0 then 1074 | table.sort(modifierHashes) 1075 | 1076 | hash = string.format('%s(%s)', hash, table.concat(modifierHashes, ';')) 1077 | end 1078 | end 1079 | 1080 | return hash 1081 | end 1082 | 1083 | local function createUiLayerComboInfos_() 1084 | local profile = getLoadedProfile_(getUiProfileName()) 1085 | 1086 | -- если симулятор запускается с миссией в командной строке, то слой для UI не заёгружается 1087 | if profile then 1088 | local commands = profile.keyCommands 1089 | local modifiers = profile.modifiers 1090 | 1091 | uiLayerComboHashes_ = {} 1092 | uiLayerKeyHashes_ = {} 1093 | 1094 | for commandHash, command in pairs(commands) do 1095 | for deviceName, combos in pairs(command.combos) do 1096 | 1097 | for i, combo in ipairs(combos) do 1098 | uiLayerComboHashes_ [createComboHash_(deviceName, combo, modifiers) ] = true 1099 | uiLayerKeyHashes_ [createKeyHash_(deviceName, combo.key) ] = true 1100 | end 1101 | end 1102 | end 1103 | end 1104 | end 1105 | 1106 | local function getComboValidUiLayer_(profileName, command, deviceName, combo, modifiers, warnings) 1107 | local result = true 1108 | 1109 | if not uiLayerComboHashes_ then 1110 | createUiLayerComboInfos_() 1111 | end 1112 | 1113 | if uiLayerComboHashes_ then 1114 | -- combo не должны совпадать с комбо для слоя UI 1115 | if uiLayerComboHashes_[createComboHash_(deviceName, combo, modifiers)] then 1116 | printLog('Profile [' .. profileName .. '] command [' .. command.name .. '] contains combo [' .. InputUtils.createComboString(combo, deviceName) .. '] equal to combo in [' .. getUiProfileName() .. ']') 1117 | table.insert(warnings, string.format(_('Is equal to combo in %s'), getUiProfileName())) 1118 | 1119 | result = false 1120 | end 1121 | 1122 | if combo.reformers then 1123 | -- модификаторы combo не должны содержать кнопки из комбо для слоя UI 1124 | for i, name in pairs(combo.reformers) do 1125 | local modifierHash = createModifierHash_(name, modifiers) 1126 | 1127 | if uiLayerKeyHashes_[modifierHash] then 1128 | result = false 1129 | 1130 | printLog('Profile [' .. profileName .. '] command [' .. command.name .. '] combo [' .. InputUtils.createComboString(combo, deviceName) .. ' reformers contain key [' .. modifierHash .. '] presented as key in [' .. getUiProfileName() .. '] combos') 1131 | 1132 | table.insert(warnings, string.format(_('Reformers has key %s presented as key in %s'), modifierHash, getUiProfileName())) 1133 | end 1134 | end 1135 | end 1136 | end 1137 | 1138 | return result 1139 | end 1140 | 1141 | local function getComboValid_(profileName, command, deviceName, combo, modifiers, warnings) 1142 | local result = true 1143 | local key = combo.key 1144 | 1145 | if key then 1146 | result = InputUtils.getKeyNameValid(key) 1147 | 1148 | if result then 1149 | local modifier = modifiers[key] 1150 | 1151 | if modifier and modifier.deviceName == deviceName then 1152 | printLog('Profile [' .. profileName.. '] command [' .. command.name .. '] contains combo key [' .. key .. '] registered as modifier in device [' .. deviceName .. '] profile!') 1153 | table.insert(warnings, string.format(_('Key %s is registered as modifier in device %s'), key, deviceName)) 1154 | 1155 | result = false 1156 | end 1157 | else 1158 | printLog('Profile [' .. profileName.. '] command [' .. command.name .. '] contains unknown combo key [' .. key .. '] in device [' .. deviceName .. '] profile!') 1159 | table.insert(warnings, string.format(_('Unknown кey %s'), key)) 1160 | end 1161 | end 1162 | 1163 | if result then 1164 | if combo.reformers then 1165 | for i, reformer in ipairs(combo.reformers) do 1166 | result = result and getReformerValid_(profileName, command, deviceName, reformer, modifiers, warnings) 1167 | 1168 | if not result then 1169 | break 1170 | end 1171 | end 1172 | end 1173 | end 1174 | 1175 | return result 1176 | end 1177 | 1178 | local function makeComboWarningString_(warnings) 1179 | local result 1180 | 1181 | if #warnings > 0 then 1182 | -- убираем повторяющиеся сообщения 1183 | local t = {} 1184 | local strings = {} 1185 | 1186 | for i, warning in ipairs(warnings) do 1187 | if not t[warning] then 1188 | table.insert(strings, warning) 1189 | t[warning] = true 1190 | end 1191 | end 1192 | 1193 | result = table.concat(strings, '\n') 1194 | end 1195 | 1196 | return result 1197 | end 1198 | 1199 | local function validateProfileCommandCombos(profileName, command) 1200 | local result = not command.updated 1201 | local profile = findProfile_(profileName) 1202 | local modifiers = profile.modifiers 1203 | 1204 | if result then 1205 | for deviceName, combos in pairs(command.combos) do 1206 | for i, combo in ipairs(combos) do 1207 | local warnings = {} 1208 | 1209 | combo.valid = getComboValid_(profileName, command, deviceName, combo, modifiers, warnings) 1210 | 1211 | -- проверим, что кнопки комбо не пересекаются с кнопками из UI Layer 1212 | if profileName ~= getUiProfileName() then 1213 | local uiValid = getComboValidUiLayer_(profileName, command, deviceName, combo, modifiers, warnings) 1214 | 1215 | combo.valid = combo.valid and uiValid 1216 | end 1217 | 1218 | combo.warnings = makeComboWarningString_(warnings) 1219 | result = result and combo.valid 1220 | end 1221 | end 1222 | end 1223 | 1224 | return result 1225 | end 1226 | 1227 | local function checkDeviceCombosIntersectionInProfile(profileName, deviceName, commandsHashTable) 1228 | local profile = findProfile_(profileName) 1229 | local modifiers = profile.modifiers 1230 | 1231 | for commandHash1, command1 in pairs(commandsHashTable) do 1232 | if command1.combos and command1.combos[deviceName] then 1233 | for i, combo1 in ipairs(command1.combos[deviceName]) do 1234 | local combo1Hash = createComboHash_(deviceName, combo1, modifiers) 1235 | for commandHash2, command2 in pairs(commandsHashTable) do 1236 | if command1.name ~= command2.name and command2.combos and command2.combos[deviceName] then 1237 | for i, combo2 in ipairs(command2.combos[deviceName]) do 1238 | if combo1Hash == createComboHash_(deviceName, combo2, modifiers) then 1239 | printLog('Profile [' .. profileName .. '] command [' .. command1.name .. '] contains combo [' .. InputUtils.createComboString(combo1, deviceName) .. '] equal to combo in [' .. command2.name .. ']') 1240 | combo1.valid = false 1241 | command1.valid = false 1242 | combo1.warnings = combo1.warnings and combo1.warnings.."\n" or "" 1243 | combo1.warnings = combo1.warnings .. string.format(_('Is equal to combo in command %s'), command2.name) 1244 | end 1245 | end 1246 | end 1247 | end 1248 | end 1249 | end 1250 | end 1251 | end 1252 | 1253 | local function updateCommandValidation(profileName, deviceName, command, isAxisCommand) 1254 | local profile = getLoadedProfile_(profileName) 1255 | command.valid = validateProfileCommandCombos(profileName, command) 1256 | checkDeviceCombosIntersectionInProfile(profileName, deviceName, isAxisCommand and profile.axisCommands or profile.keyCommands) 1257 | end 1258 | 1259 | local function findCommandByHash_(commands, commandHash) 1260 | if commands then 1261 | return commands[commandHash] 1262 | end 1263 | end 1264 | 1265 | local function findKeyCommand_(profileName, commandHash) 1266 | local profile = getLoadedProfile_(profileName) 1267 | 1268 | return findCommandByHash_(profile.keyCommands, commandHash) 1269 | end 1270 | 1271 | local function findDefaultKeyCommand_(profileName, commandHash) 1272 | local profile = getLoadedProfile_(profileName) 1273 | 1274 | return findCommandByHash_(profile.defaultKeyCommands, commandHash) 1275 | end 1276 | 1277 | local function findAxisCommand_(profileName, commandHash) 1278 | local profile = getLoadedProfile_(profileName) 1279 | 1280 | return findCommandByHash_(profile.axisCommands, commandHash) 1281 | end 1282 | 1283 | local function findDefaultAxisCommand_(profileName, commandHash) 1284 | local profile = getLoadedProfile_(profileName) 1285 | 1286 | return findCommandByHash_(profile.defaultAxisCommands, commandHash) 1287 | end 1288 | 1289 | local function getCommandModifiedCombos_(command, deviceName) 1290 | local modifiedCombos = command.modifiedCombos 1291 | 1292 | if modifiedCombos then 1293 | return modifiedCombos[deviceName] 1294 | end 1295 | 1296 | return false 1297 | end 1298 | 1299 | local function addComboToCommand_(profileName, deviceName, command, combo) 1300 | if command then 1301 | local deviceCombos = command.combos[deviceName] 1302 | 1303 | if not findCombo_(deviceCombos, combo) then 1304 | if not deviceCombos then 1305 | deviceCombos = {} 1306 | command.combos[deviceName] = deviceCombos 1307 | end 1308 | 1309 | table.insert(deviceCombos, U.copyTable(nil, combo)) 1310 | command.valid = validateProfileCommandCombos(profileName, command) 1311 | end 1312 | end 1313 | end 1314 | 1315 | local function removeComboFromCommand_(profileName, deviceName, command, combo) 1316 | if command then 1317 | local deviceCombos = command.combos[deviceName] 1318 | local comboIndex = findCombo_(deviceCombos, combo) 1319 | if comboIndex then 1320 | table.remove(deviceCombos, comboIndex) 1321 | 1322 | command.valid = validateProfileCommandCombos(profileName, command) 1323 | end 1324 | end 1325 | end 1326 | 1327 | local function removeCombosFromCommand_(profileName, command, deviceName) 1328 | if command then 1329 | local deviceCombos = command.combos[deviceName] 1330 | 1331 | if deviceCombos then 1332 | while #deviceCombos > 0 do 1333 | table.remove(deviceCombos) 1334 | end 1335 | end 1336 | 1337 | command.updated = false 1338 | command.valid = validateProfileCommandCombos(profileName, command) 1339 | end 1340 | end 1341 | 1342 | local function removeComboFromCommands_(profileName, deviceName, commands, combo) 1343 | for commandHash, command in pairs(commands) do 1344 | removeComboFromCommand_(profileName, deviceName, command, combo) 1345 | end 1346 | end 1347 | 1348 | local function setDefaultCommandCombos_(profileName, deviceName, defaultCommand, command, commands) 1349 | removeCombosFromCommand_(profileName, command, deviceName) 1350 | 1351 | for combo in commandCombos(defaultCommand, deviceName) do 1352 | removeComboFromCommands_(profileName, deviceName, commands, combo) 1353 | addComboToCommand_(profileName, deviceName, command, combo) 1354 | end 1355 | end 1356 | 1357 | local function setDefaultCommandsCategoryCombos_(profileName, commands, deviceName, category) 1358 | for commandHash, command in pairs(commands) do 1359 | if getCommandBelongsToCategory(category, command) then 1360 | local defaultKeyCommand = findDefaultKeyCommand_(profileName, commandHash) 1361 | 1362 | if defaultKeyCommand then 1363 | setDefaultCommandCombos_(profileName,deviceName, defaultKeyCommand, command, commands) 1364 | end 1365 | end 1366 | end 1367 | end 1368 | 1369 | local function addProfileKeyCommand(profileName, deviceName, keyCommand, commandsHashTable, combosHashTable) 1370 | local commandHash = InputUtils.getKeyCommandHash(keyCommand) 1371 | local command = commandsHashTable[commandHash] 1372 | 1373 | if command then 1374 | if command.name ~= keyCommand.name then 1375 | printLog('Profile[' .. profileName .. '] key command[' .. 1376 | keyCommand.name .. '] for device[' .. 1377 | deviceName.. '] has different name from command[' .. 1378 | command.name .. '] for device[' .. 1379 | getCommandDeviceNamesString(command) .. ']') 1380 | end 1381 | 1382 | command.combos[deviceName] = keyCommand.combos or {} 1383 | else 1384 | command = {combos = {}} 1385 | 1386 | copyDeviceCommandToProfileCommand(deviceName, keyCommand, command) 1387 | 1388 | command.name = keyCommand.name 1389 | command.disabled = keyCommand.disabled 1390 | command.hash = commandHash 1391 | commandsHashTable[commandHash] = command 1392 | end 1393 | 1394 | command.valid = validateProfileCommandCombos(profileName, command) 1395 | end 1396 | 1397 | local function addProfileKeyCommands(profileName, deviceName, deviceProfile, commandsHashTable) 1398 | -- deviceProfile это таблица, загруженная из файла 1399 | local keyCommands = deviceProfile.keyCommands 1400 | 1401 | if keyCommands then 1402 | local combosHashTable = {} 1403 | 1404 | for i, keyCommand in ipairs(keyCommands) do 1405 | addProfileKeyCommand(profileName, deviceName, keyCommand, commandsHashTable, combosHashTable) 1406 | end 1407 | 1408 | checkDeviceCombosIntersectionInProfile(profileName, deviceName, commandsHashTable) 1409 | end 1410 | end 1411 | 1412 | local function addProfileAxisCommand(profileName, deviceName, axisCommand, commandsHashTable, combosHashTable) 1413 | local commandHash = InputUtils.getAxisCommandHash(axisCommand) 1414 | if not commandHash then 1415 | return 1416 | end 1417 | local command = commandsHashTable[commandHash] 1418 | 1419 | if command then 1420 | if command.name ~= axisCommand.name then 1421 | printLog('Profile[' .. profileName .. '] axis command[' .. 1422 | axisCommand.name .. '] for device[' .. 1423 | deviceName.. '] has different name from command[' .. command.name .. '] for device[' .. 1424 | getCommandDeviceNamesString(command) .. ']') 1425 | end 1426 | 1427 | command.combos[deviceName] = axisCommand.combos or {} 1428 | else 1429 | command = {combos = {}} 1430 | 1431 | copyDeviceCommandToProfileCommand(deviceName, axisCommand, command) 1432 | 1433 | command.name = axisCommand.name 1434 | command.hash = commandHash 1435 | commandsHashTable[commandHash] = command 1436 | end 1437 | 1438 | command.valid = validateProfileCommandCombos(profileName, command) 1439 | end 1440 | 1441 | local function addProfileAxisCommands(profileName, deviceName, deviceProfile, commandsHashTable) 1442 | if deviceProfile.axisCommands then 1443 | local combosHashTable = {} 1444 | 1445 | for i, axisCommand in ipairs(deviceProfile.axisCommands) do 1446 | addProfileAxisCommand(profileName, deviceName, axisCommand, commandsHashTable, combosHashTable) 1447 | end 1448 | 1449 | checkDeviceCombosIntersectionInProfile(profileName, deviceName, commandsHashTable) 1450 | end 1451 | end 1452 | 1453 | local function addProfileForceFeedbackSettings(profile, deviceName, deviceProfile) 1454 | if deviceProfile.forceFeedback then 1455 | profile.forceFeedback[deviceName] = U.copyTable(nil, deviceProfile.forceFeedback) 1456 | end 1457 | end 1458 | 1459 | local function getProfileForceFeedbackSettings(profileName, deviceName) 1460 | local profile = getLoadedProfile_(profileName) 1461 | local ffSettings = profile.forceFeedback[deviceName] 1462 | 1463 | if ffSettings then 1464 | return createForceFeedbackSettings(ffSettings) 1465 | end 1466 | end 1467 | 1468 | local function validateCommands_(profileName, commands) 1469 | if commands then--check 1470 | for commandHash, command in pairs(commands) do 1471 | command.valid = validateProfileCommandCombos(profileName, command) 1472 | end 1473 | end 1474 | end 1475 | 1476 | local function setAxisComboFilters(combos, filters) 1477 | if combos then 1478 | for i, combo in ipairs(combos) do 1479 | local axis = combo.key 1480 | 1481 | if axis then 1482 | local filter = filters[axis] 1483 | 1484 | if filter then 1485 | combo.filter = createAxisFilter(filter) 1486 | end 1487 | end 1488 | end 1489 | end 1490 | end 1491 | 1492 | local function setProfileDeviceProfile_(profile, deviceName, deviceProfile) 1493 | validateDeviceProfile_(profile.name, deviceProfile, deviceName) 1494 | profile.deviceProfiles[deviceName] = deviceProfile 1495 | end 1496 | 1497 | local function loadModifiersFromFolder_(folder) 1498 | local result 1499 | local filename = folder .. '/modifiers.lua' 1500 | local f, err = loadfile(filename) 1501 | 1502 | if f then 1503 | printFileLog('File[' .. filename .. '] opened successfully!') 1504 | 1505 | result = f() 1506 | else 1507 | printFileLog(err) 1508 | end 1509 | 1510 | return result, err 1511 | end 1512 | 1513 | -- загружаем измененные пользователем модификаторы 1514 | local function loadProfileUserModifiers_(profile, errors) 1515 | errors = errors or {} 1516 | 1517 | local folder = getProfileUserConfigPath_(profile) 1518 | local modifiers = collectErrors_(errors, loadModifiersFromFolder_(folder)) 1519 | 1520 | if not modifiers and userConfigPath_ ~= nil then 1521 | -- в предыдущей версии инпута измененные модификаторы 1522 | -- располагались в пользовательской папке с профилями 1523 | folder = userConfigPath_ 1524 | modifiers = collectErrors_(errors, loadModifiersFromFolder_(folder)) 1525 | end 1526 | 1527 | return modifiers, folder, errors 1528 | end 1529 | 1530 | -- загружаем дефолтные модификаторы 1531 | local function loadProfileDefaultModifiers_(profile, errors) 1532 | errors = errors or {} 1533 | 1534 | local folder = profile.folder 1535 | local modifiers = collectErrors_(errors, loadModifiersFromFolder_(folder)) 1536 | 1537 | if not modifiers then 1538 | folder = sysPath_ 1539 | modifiers = collectErrors_(errors, loadModifiersFromFolder_(folder)) 1540 | end 1541 | 1542 | return modifiers, folder, errors 1543 | end 1544 | 1545 | local function loadProfileModifiers_(profile) 1546 | local errors = {} 1547 | local modifiers, folder = loadProfileUserModifiers_(profile, errors) 1548 | 1549 | if not modifiers then 1550 | modifiers, folder = loadProfileDefaultModifiers_(profile, errors) 1551 | end 1552 | 1553 | if not modifiers and #errors > 0 then 1554 | printLog('Profile [' .. profile.name .. '] cannot load modifiers!', table.concat(errors, '\n')) 1555 | end 1556 | 1557 | return modifiers, folder 1558 | end 1559 | 1560 | local function getDevicesHash_(folder) 1561 | local result = {} 1562 | local devices = InputUtils.getDevices() 1563 | 1564 | for i, deviceName in ipairs(devices) do 1565 | if folder == sysPath_ then 1566 | local deviceTemplateName = InputUtils.getDeviceTemplateName(deviceName) 1567 | 1568 | result[deviceTemplateName] = deviceName 1569 | else 1570 | result[deviceName] = deviceName 1571 | end 1572 | end 1573 | 1574 | return result 1575 | end 1576 | 1577 | local function createModifier(key, deviceName, switch) 1578 | local event = InputUtils.getInputEvent(key) 1579 | local deviceId = Input.getDeviceId(deviceName) 1580 | return {key = key, event = event, deviceId = deviceId, deviceName = deviceName, switch = switch} 1581 | end 1582 | 1583 | local function createProfileModifiers_(profile) 1584 | local profileModifiers = {} 1585 | local modifiers, folder = loadProfileModifiers_(profile) 1586 | 1587 | if modifiers then 1588 | -- у модификаторов, загружаемых из дефолтной папки sysPath_ 1589 | -- имена устройств не содержат CLSID 1590 | local devicesHash = getDevicesHash_(folder) 1591 | 1592 | for name, modifier in pairs(modifiers) do 1593 | local modifierDeviceName = modifier.device 1594 | local deviceName = devicesHash[modifierDeviceName] 1595 | 1596 | if deviceName then 1597 | local key = modifier.key 1598 | local switch = modifier.switch 1599 | 1600 | profileModifiers[name] = createModifier(key, deviceName, switch) 1601 | end 1602 | end 1603 | end 1604 | 1605 | profile.modifiers = profileModifiers 1606 | end 1607 | 1608 | local function deleteDeviceCombos_(commands, deviceName) 1609 | for commandHash, command in pairs(commands) do 1610 | local combos = command.combos 1611 | 1612 | combos[deviceName] = nil 1613 | 1614 | if not next(combos) then 1615 | -- комбинаций для других устройств в этой команде нет, ее можно удалить 1616 | commands[commandHash] = nil 1617 | end 1618 | end 1619 | end 1620 | 1621 | local function getDeviceProfile(profileName, deviceName) 1622 | local profile = getLoadedProfile_(profileName) 1623 | local deviceProfile = loadDeviceProfile_(profile, deviceName) 1624 | 1625 | return deviceProfile 1626 | end 1627 | 1628 | local function getForceFeedbackSettingsDiff_(forceFeedbackSettings, defaultForceFeedbackSettings) 1629 | local diff = {} 1630 | 1631 | for key, value in pairs(forceFeedbackSettings) do 1632 | if value ~= defaultForceFeedbackSettings[key] then 1633 | diff[key] = value 1634 | end 1635 | end 1636 | 1637 | if next(diff) then 1638 | return diff 1639 | end 1640 | end 1641 | 1642 | local function getForceFeedbackDiff_(profile, deviceName) 1643 | local forceFeedback = profile.forceFeedback[deviceName] 1644 | if not forceFeedback then 1645 | return 1646 | end 1647 | local forceFeedbackSettings = createForceFeedbackSettings(forceFeedback) 1648 | local defaultDeviceProfile = loadProfileDefaultDeviceProfile_(profile, deviceName) 1649 | local defaultForceFeedbackSettings = createForceFeedbackSettings(defaultDeviceProfile.forceFeedback) 1650 | 1651 | return getForceFeedbackSettingsDiff_(forceFeedbackSettings, defaultForceFeedbackSettings) 1652 | end 1653 | 1654 | local function compareFilters_(filter1, filter2) 1655 | if filter1.deadzone == filter2.deadzone and 1656 | filter1.saturationX == filter2.saturationX and 1657 | filter1.saturationY == filter2.saturationY and 1658 | filter1.hardwareDetent == filter2.hardwareDetent and 1659 | filter1.slider == filter2.slider and 1660 | filter1.invert == filter2.invert and 1661 | #filter1.curvature == #filter2.curvature then 1662 | 1663 | for i, value in ipairs(filter1.curvature) do 1664 | if value ~= filter2.curvature[i] then 1665 | return false 1666 | end 1667 | end 1668 | 1669 | if filter1.hardwareDetent and filter2.hardwareDetent then 1670 | if filter1.hardwareDetentMax ~= filter1.hardwareDetentMax then 1671 | return false 1672 | end 1673 | 1674 | if filter1.hardwareDetentAB ~= filter1.hardwareDetentAB then 1675 | return false 1676 | end 1677 | end 1678 | 1679 | return true 1680 | end 1681 | 1682 | return false 1683 | end 1684 | 1685 | local function getFiltersAreEqual_(filter1, filter2) 1686 | if filter1 == filter2 then 1687 | return true 1688 | end 1689 | 1690 | return compareFilters_(createAxisFilter(filter1), createAxisFilter(filter2)) 1691 | end 1692 | 1693 | local function cleanupCombo_(combo, checkDefaultFilter) 1694 | local reformers = combo.reformers 1695 | 1696 | if reformers then 1697 | if not next(reformers) then 1698 | reformers = nil 1699 | end 1700 | end 1701 | 1702 | local filter = combo.filter 1703 | 1704 | if checkDefaultFilter then 1705 | if filter then 1706 | if getFiltersAreEqual_(createAxisFilter(filter), createAxisFilter()) then 1707 | filter = nil 1708 | end 1709 | end 1710 | end 1711 | 1712 | return { 1713 | key = combo.key, 1714 | reformers = reformers, 1715 | filter = filter, 1716 | column = combo.column, 1717 | } 1718 | end 1719 | 1720 | local function getCommandAddedCombos_(command, defaultCommand, deviceName) 1721 | local combos = command.combos[deviceName] 1722 | if not combos then 1723 | return 1724 | end 1725 | local defaultCombos = defaultCommand.combos[deviceName] 1726 | local result 1727 | for i, combo in ipairs(combos) do 1728 | if not findCombo_(defaultCombos, combo) then 1729 | result = result or {} 1730 | table.insert(result, cleanupCombo_(combo, true)) 1731 | end 1732 | end 1733 | return result 1734 | end 1735 | 1736 | local function getCommandRemovedCombos_(command, defaultCommand, deviceName) 1737 | local defaultCombos = defaultCommand.combos[deviceName] 1738 | if not defaultCombos then 1739 | return nil 1740 | end 1741 | local combos = command.combos[deviceName] 1742 | local result 1743 | for i, combo in ipairs(defaultCombos) do 1744 | if not findCombo_(combos, combo) then 1745 | result = result or {} 1746 | table.insert(result, cleanupCombo_(combo)) 1747 | end 1748 | end 1749 | return result 1750 | end 1751 | 1752 | local function getCommandChangedFilterCombos_(command, defaultCommand, deviceName) 1753 | local result 1754 | local combos = command.combos[deviceName] 1755 | local defaultCombos = defaultCommand.combos[deviceName] 1756 | 1757 | if combos then 1758 | for i, combo in ipairs(combos) do 1759 | local index = findCombo_(defaultCombos, combo) 1760 | 1761 | if index then 1762 | local defaultCombo = defaultCombos[index] 1763 | 1764 | if not getFiltersAreEqual_(combo.filter, defaultCombo.filter) then 1765 | result = result or {} 1766 | table.insert(result, cleanupCombo_(combo)) 1767 | end 1768 | end 1769 | end 1770 | end 1771 | return result 1772 | end 1773 | 1774 | local function getCommandDiffCommon_(command, defaultCommand, deviceName) 1775 | local addedCombos = getCommandAddedCombos_ (command, defaultCommand, deviceName) 1776 | local removedCombos = getCommandRemovedCombos_(command, defaultCommand, deviceName) 1777 | 1778 | if addedCombos or removedCombos then 1779 | return { 1780 | name = command.name, 1781 | added = addedCombos, 1782 | removed = removedCombos, 1783 | } 1784 | end 1785 | return nil 1786 | end 1787 | 1788 | local function storeDeviceProfileDiffIntoFile_(filename, diff) 1789 | local file, err = io.open(filename, 'w') 1790 | if not file then 1791 | log.error(string.format('Cannot save profile into file[%s]! Error %s', filename, err)) 1792 | return 1793 | end 1794 | 1795 | local s = Serializer.new(file) 1796 | s:serialize_sorted('local diff', diff) 1797 | file:write('return diff') 1798 | file:close() 1799 | end 1800 | 1801 | local function saveDeviceProfile(profileName, deviceName, filename) 1802 | local profile = getLoadedProfile_(profileName) 1803 | local calcDiff = function(commands,base,calculator) 1804 | local diffs = {} 1805 | for commandHash, command in pairs(commands) do 1806 | local base_command = base[commandHash] 1807 | if base_command then 1808 | local commandDiff = calculator(command, base_command, deviceName) 1809 | if commandDiff then 1810 | diffs[commandHash] = commandDiff 1811 | end 1812 | else 1813 | -- возможно команда сохранена в пользовательских настройках, 1814 | -- но после обновления она исчезла из дефолтных настроек 1815 | print("Cannot find base command for hash", commandHash, command.name, profile.name, deviceName) 1816 | end 1817 | end 1818 | 1819 | if next(diffs) then 1820 | return diffs 1821 | end 1822 | end 1823 | 1824 | local getCommandDiffAxis = function (command, defaultCommand, deviceName) 1825 | local res = getCommandDiffCommon_ (command, defaultCommand, deviceName) 1826 | local changedFilterCombos = getCommandChangedFilterCombos_(command, defaultCommand, deviceName) 1827 | if changedFilterCombos then 1828 | if not res then 1829 | res = { 1830 | name = command.name, 1831 | } 1832 | end 1833 | res.changed = changedFilterCombos 1834 | end 1835 | return res 1836 | end 1837 | 1838 | local diff = { 1839 | ffDiffs = getForceFeedbackDiff_(profile, deviceName), 1840 | keyDiffs = calcDiff(profile.keyCommands ,profile.baseKeyCommands ,getCommandDiffCommon_), 1841 | axisDiffs = calcDiff(profile.axisCommands,profile.baseAxisCommands,getCommandDiffAxis), 1842 | } 1843 | 1844 | if next(diff) then 1845 | storeDeviceProfileDiffIntoFile_(filename, diff) 1846 | else 1847 | os.remove(filename) 1848 | end 1849 | end 1850 | 1851 | local function compareModifiers_(modifier1, modifier2) 1852 | if modifier1 then 1853 | if modifier2 then 1854 | return modifier1.key == modifier2.key and 1855 | modifier1.deviceName == modifier2.deviceName and 1856 | (modifier1.switch or false) == (modifier2.switch or false) 1857 | else 1858 | return false 1859 | end 1860 | elseif modifier2 then 1861 | return false 1862 | else 1863 | return true 1864 | end 1865 | end 1866 | 1867 | local function getModifiersAreEqual_(modifiers1, modifiers2) 1868 | if modifiers1 == modifiers2 then 1869 | return true 1870 | end 1871 | 1872 | local comparedNames = {} 1873 | 1874 | for name, modifier in pairs(modifiers1) do 1875 | if compareModifiers_(modifier, modifiers2[name]) then 1876 | comparedNames[name] = true 1877 | else 1878 | return false 1879 | end 1880 | end 1881 | 1882 | for name, modifier in pairs(modifiers2) do 1883 | if not comparedNames[name] then 1884 | if not compareModifiers_(modifier, modifiers1[name]) then 1885 | return false 1886 | end 1887 | end 1888 | end 1889 | 1890 | return true 1891 | end 1892 | 1893 | local function cleanupModifiers_(modifiers) 1894 | local result = {} 1895 | local cleanupModifier = function(modifier) 1896 | return { 1897 | key = modifier.key, 1898 | device = modifier.deviceName, 1899 | switch = modifier.switch or false, 1900 | } 1901 | end 1902 | 1903 | for name, modifier in pairs(modifiers) do 1904 | result[name] = cleanupModifier(modifier) 1905 | end 1906 | 1907 | return result 1908 | end 1909 | 1910 | local function getProfileDefaultModifiers_(profile) 1911 | local defaultModifiers = {} 1912 | 1913 | for name, modifier in pairs(loadProfileDefaultModifiers_(profile) or {}) do 1914 | defaultModifiers[name] = createModifier(modifier.key, modifier.device, modifier.switch) 1915 | end 1916 | 1917 | return defaultModifiers 1918 | end 1919 | 1920 | local function saveProfileModifiers_(profileName, folder) 1921 | local filename = folder .. 'modifiers.lua' 1922 | local profile = findProfile_(profileName) 1923 | local modifiers = profile.modifiers 1924 | local defaultModifiers = getProfileDefaultModifiers_(profile) 1925 | 1926 | if getModifiersAreEqual_(modifiers, defaultModifiers) then 1927 | os.remove(filename) 1928 | else 1929 | local file, err = io.open(filename, 'w') 1930 | 1931 | if file then 1932 | local s = Serializer.new(file) 1933 | s:serialize_sorted('local modifiers', cleanupModifiers_(modifiers)) 1934 | file:write('return modifiers') 1935 | file:close() 1936 | else 1937 | log.error(string.format('Cannot save modifiers into file[%s]! Error %s', filename, err)) 1938 | end 1939 | end 1940 | end 1941 | 1942 | local function saveDisabledDevices() 1943 | if userConfigPath_ == nil then 1944 | return 1945 | end 1946 | local filename = userConfigPath_ .. disabledFilename_ 1947 | local file, err = io.open(filename, 'w') 1948 | 1949 | if file then 1950 | local s = Serializer.new(file) 1951 | local disabled = { 1952 | devices = disabledDevices_, 1953 | pnp = Input.getPnPDisabled(), 1954 | } 1955 | s:serialize_sorted('local disabled', disabled) 1956 | file:write('return disabled') 1957 | file:close() 1958 | else 1959 | log.error(string.format('Cannot save disabled devices into file[%s]! Error %s', filename, err)) 1960 | end 1961 | end 1962 | 1963 | local function getCommandsInfo(profileCommands, commandActionHashInfos, deviceName) 1964 | local result = {} 1965 | 1966 | for i, profileCommand in ipairs(profileCommands) do 1967 | local commandInfo = {} 1968 | 1969 | commandInfo.category = profileCommand.category 1970 | commandInfo.name = profileCommand.name 1971 | commandInfo.features = profileCommand.features 1972 | commandInfo.actions = {} 1973 | 1974 | for i, actionHashInfo in ipairs(commandActionHashInfos) do 1975 | local action = profileCommand[actionHashInfo.name] 1976 | 1977 | if action then 1978 | local inputName 1979 | 1980 | if actionHashInfo.namedAction then 1981 | inputName = InputUtils.getInputActionName(action) -- некоторые команды могут не иметь имени 1982 | end 1983 | 1984 | if not inputName then 1985 | inputName = tostring(action) 1986 | end 1987 | 1988 | table.insert(commandInfo.actions, {name = actionHashInfo.name, inputName = inputName}) 1989 | end 1990 | end 1991 | 1992 | local combos = profileCommand.combos[deviceName] 1993 | 1994 | if combos and next(combos) then 1995 | commandInfo.combos = {} 1996 | 1997 | for i, combo in ipairs(combos) do 1998 | table.insert(commandInfo.combos, {key = combo.key, reformers = combo.reformers, filter = combo.filter}) 1999 | end 2000 | end 2001 | 2002 | table.insert(result, commandInfo) 2003 | end 2004 | 2005 | return result 2006 | end 2007 | 2008 | local function formatFilter(filter) 2009 | return string.format('curvature = {%s}, deadzone = %g, invert = %s, saturationX = %g, saturationY = %g, slider = %s', 2010 | table.concat(filter.curvature, ', '), 2011 | filter.deadzone, 2012 | tostring(filter.invert), 2013 | filter.saturationX, 2014 | filter.saturationY, 2015 | tostring(filter.slider)) 2016 | end 2017 | 2018 | local function formatCombo(combo) 2019 | local result = string.format('{key = %q', combo.key) -- здесь могут быть кавычки, слеши и прочее 2020 | 2021 | if combo.reformers and #combo.reformers > 0 then 2022 | result = string.format('%s, reformers = {"%s"}', result, table.concat(combo.reformers, '", "')) 2023 | end 2024 | 2025 | if combo.filter then 2026 | result = string.format('%s, filter = {%s},', result, formatFilter(combo.filter)) 2027 | end 2028 | 2029 | result = result .. '}, ' 2030 | 2031 | return result 2032 | end 2033 | 2034 | local function formatCommand(commandInfo) 2035 | local result = '{' 2036 | 2037 | if commandInfo.combos then 2038 | result = result .. 'combos = {' 2039 | 2040 | for i, combo in ipairs(commandInfo.combos) do 2041 | result = result .. formatCombo(combo) 2042 | end 2043 | 2044 | result = result .. '}, ' 2045 | end 2046 | 2047 | for i, action in ipairs(commandInfo.actions) do 2048 | result = string.format('%s%s = %s, ', result, action.name, action.inputName) 2049 | end 2050 | 2051 | result = string.format('%s name = _(%q), ', result, commandInfo.name) 2052 | 2053 | if commandInfo.category then 2054 | if 'table' == type(commandInfo.category) then 2055 | result = string.format('%s category = { ', result) 2056 | 2057 | for i, categoryName in ipairs(commandInfo.category) do 2058 | result = result .. string.format('_(%q), ', categoryName) 2059 | end 2060 | 2061 | result = result .. '}, ' 2062 | else 2063 | result = string.format('%s category = _(%q), ', result, commandInfo.category) 2064 | end 2065 | end 2066 | 2067 | if commandInfo.features then 2068 | result = string.format('%s features = {', result) 2069 | 2070 | for i, feature in ipairs(commandInfo.features) do 2071 | result = result .. string.format('%q, ', feature) 2072 | end 2073 | 2074 | result = result .. '}, ' 2075 | end 2076 | 2077 | result = result .. '},\n' 2078 | 2079 | return result 2080 | end 2081 | 2082 | local function formatForceFeedback(forceFeedback) 2083 | return string.format( 2084 | [[ invertX = %s, 2085 | invertY = %s, 2086 | shake = %g, 2087 | swapAxes = %s, 2088 | trimmer = %g, 2089 | ignore = %s,]], 2090 | tostring(forceFeedback.invertX), 2091 | tostring(forceFeedback.invertY), 2092 | forceFeedback.shake, 2093 | tostring(forceFeedback.swapAxes), 2094 | forceFeedback.trimmer, 2095 | tostring(forceFeedback.ignore)) 2096 | end 2097 | 2098 | local function writeForceFeedbackToFile(file, profileName, deviceName) 2099 | local forceFeedback = getProfileForceFeedbackSettings(profileName, deviceName) 2100 | 2101 | if forceFeedback then 2102 | file:write('forceFeedback = {\n') 2103 | file:write(formatForceFeedback(forceFeedback)) 2104 | file:write('\n},\n') 2105 | end 2106 | end 2107 | 2108 | local function writeKeyCommandsToFile(file, profileName, deviceName) 2109 | local keyCommands = getProfileKeyCommandsCopy(profileName) 2110 | local keyActionHashInfos = InputUtils.getKeyCommandActionHashInfos() 2111 | local commandsInfo = getCommandsInfo(keyCommands, keyActionHashInfos, deviceName) 2112 | 2113 | file:write('keyCommands = {\n') 2114 | 2115 | for i, commandInfo in ipairs(commandsInfo) do 2116 | file:write(formatCommand(commandInfo)) 2117 | end 2118 | 2119 | file:write('},\n') 2120 | end 2121 | 2122 | local function writeAxisCommandsToFile(file, profileName, deviceName) 2123 | local axisCommands = getProfileAxisCommandsCopy(profileName) 2124 | local axisActionHashInfos = InputUtils.getAxisCommandActionHashInfos() 2125 | local commandsInfo = getCommandsInfo(axisCommands, axisActionHashInfos, deviceName) 2126 | 2127 | file:write('axisCommands = {\n') 2128 | 2129 | for i, commandInfo in ipairs(commandsInfo) do 2130 | file:write(formatCommand(commandInfo)) 2131 | end 2132 | 2133 | file:write('},\n') 2134 | end 2135 | 2136 | applyDiffToDeviceProfile_ = function(profile, diff) 2137 | if not diff then 2138 | return 2139 | end 2140 | applyDiffToCommands_ (profile.keyCommands , diff.keyDiffs , InputUtils.getKeyCommandHash) 2141 | applyDiffToCommands_ (profile.axisCommands, diff.axisDiffs, InputUtils.getAxisCommandHash) 2142 | applyDiffToForceFeedback_ (profile, diff.ffDiffs) 2143 | end 2144 | 2145 | loadProfile = function(profile) 2146 | local profileName = profile.name 2147 | local devices = InputUtils.getDevices() 2148 | 2149 | if not profile.deviceProfiles then 2150 | profile.deviceProfiles = {} 2151 | for i, deviceName in ipairs(devices) do 2152 | local deviceProfile = loadDeviceProfile_(profile, deviceName) 2153 | if deviceProfile then 2154 | --!!! NOTE USERS DIFF IS COMPLETELY REPLACE TEMPLATE DIFF !!! 2155 | local diff = loadDeviceProfileDiff_(profile, deviceName) or loadDeviceTemplateProfileDiff_(profile, deviceName) 2156 | applyDiffToDeviceProfile_(deviceProfile, diff) 2157 | setProfileDeviceProfile_(profile, deviceName, deviceProfile) 2158 | end 2159 | end 2160 | end 2161 | 2162 | local keyCommandsHashTable = {} 2163 | local axisCommandsHashTable = {} 2164 | 2165 | createProfileModifiers_(profile) 2166 | 2167 | for deviceName, deviceProfile in pairs(profile.deviceProfiles) do 2168 | addProfileKeyCommands(profileName, deviceName, deviceProfile, keyCommandsHashTable) 2169 | addProfileAxisCommands(profileName, deviceName, deviceProfile, axisCommandsHashTable) 2170 | addProfileForceFeedbackSettings(profile, deviceName, deviceProfile) 2171 | end 2172 | 2173 | profile.keyCommands = keyCommandsHashTable 2174 | profile.axisCommands = axisCommandsHashTable 2175 | 2176 | -- сразу сохраняем дефлтные команды, 2177 | -- поскольку при загрузке новых профилей может поменяться значение в таблице devices["KNEEBOARD"] 2178 | -- и хэши загруженных команд и дефолтных начнут отличаться 2179 | -- bug 0044809 2180 | 2181 | ----------------------------------------------------------------------------------------- 2182 | local loadDefaults = function (with_template_diff) 2183 | local defaultDeviceProfiles = {} 2184 | for i, deviceName in ipairs(devices) do 2185 | defaultDeviceProfiles[deviceName] = loadProfileDefaultDeviceProfile_(profile, deviceName,with_template_diff) 2186 | end 2187 | 2188 | local defaultKeyCommandsHashTable = {} 2189 | local defaultAxisCommandsHashTable = {} 2190 | 2191 | for deviceName, deviceProfile in pairs(defaultDeviceProfiles) do 2192 | validateDeviceProfile_(profileName, deviceProfile, deviceName) 2193 | addProfileKeyCommands(profileName, deviceName, deviceProfile, defaultKeyCommandsHashTable) 2194 | addProfileAxisCommands(profileName, deviceName, deviceProfile, defaultAxisCommandsHashTable) 2195 | end 2196 | 2197 | return defaultKeyCommandsHashTable,defaultAxisCommandsHashTable 2198 | end 2199 | 2200 | local dk,da = loadDefaults(true) 2201 | 2202 | profile.defaultKeyCommands = dk 2203 | profile.defaultAxisCommands = da 2204 | 2205 | --making BASE for DIFFS caclculation 2206 | local bk,ba = loadDefaults(false) 2207 | 2208 | profile.baseKeyCommands = bk 2209 | profile.baseAxisCommands = ba 2210 | 2211 | ----------------------------------------------------------------------------------------- 2212 | createProfileCategories(profile) 2213 | profile.loaded = true 2214 | end 2215 | 2216 | unloadProfile = function(profileName) 2217 | for i, profile in ipairs(profiles_) do 2218 | if profile.name == profileName then 2219 | table.remove(profiles_, i) 2220 | 2221 | local newProfile = createProfileTable_( profile.name, 2222 | profile.folder, 2223 | profile.unitName, 2224 | profile.default, 2225 | profile.visible, 2226 | profile.loadDefaultUnitProfile) 2227 | 2228 | table.insert(profiles_, newProfile) 2229 | 2230 | break 2231 | end 2232 | end 2233 | end 2234 | 2235 | createAxisFilter = function(filter) 2236 | filter = filter or {} 2237 | 2238 | local result = {} 2239 | 2240 | result.deadzone = filter.deadzone or 0 2241 | result.saturationX = filter.saturationX or 1 2242 | result.saturationY = filter.saturationY or 1 2243 | result.hardwareDetentMax = filter.hardwareDetentMax or 0 2244 | result.hardwareDetentAB = filter.hardwareDetentAB or 0 2245 | result.hardwareDetent = not (not filter.hardwareDetent) 2246 | result.slider = not (not filter.slider ) 2247 | result.invert = not (not filter.invert ) 2248 | result.curvature = U.copyTable(nil, filter.curvature or {0}) 2249 | 2250 | return result 2251 | end 2252 | 2253 | ------------------------------------------------------------------------------ 2254 | local fdef, errfdef = loadfile('./Scripts/Input/DefaultAssignments.lua') 2255 | if fdef then 2256 | setfenv(fdef, {}) 2257 | local ok, res = pcall(fdef) 2258 | if ok then 2259 | default_assignments = res 2260 | else 2261 | log.error('Cannot load default assignments '..res) 2262 | end 2263 | else 2264 | log.error('Cannot load default assignments '.. errfdef) 2265 | end 2266 | ------------------------------------------------------------------------------ 2267 | 2268 | local module_interface = { 2269 | commandCombos = commandCombos, 2270 | getProfileKeyCommands = getProfileKeyCommandsCopy, 2271 | getProfileAxisCommands = getProfileAxisCommandsCopy, 2272 | createForceFeedbackSettings = createForceFeedbackSettings, 2273 | getProfileForceFeedbackSettings = getProfileForceFeedbackSettings, 2274 | createAxisFilter = createAxisFilter, 2275 | setAxisComboFilters = setAxisComboFilters, 2276 | createModifier = createModifier, 2277 | getDeviceProfile = getDeviceProfile, 2278 | saveDeviceProfile = saveDeviceProfile, 2279 | loadDeviceProfileFromFile = loadDeviceProfileFromFile, 2280 | getUiProfileName = getUiProfileName, 2281 | unloadProfile = unloadProfile, -- подключено/отключено устройство - сбрасываем загруженный профиль 2282 | --interface functions only 2283 | setController = function(controller) 2284 | controller_ = controller 2285 | end, 2286 | initialize = function(userConfigPath, sysConfigPath) 2287 | userConfigPath_ = userConfigPath 2288 | sysConfigPath_ = sysConfigPath 2289 | sysPath_ = sysConfigPath .. 'Aircrafts/' 2290 | 2291 | if userConfigPath_ then 2292 | local f, err = loadfile(userConfigPath_ .. disabledFilename_) 2293 | if f then 2294 | local ok, res = pcall(f) 2295 | if ok then 2296 | disabledDevices_ = res.devices 2297 | for deviceName, disabled in pairs(disabledDevices_) do 2298 | Input.setDeviceDisabled(deviceName, true) 2299 | end 2300 | Input.setPnPDisabled(res.pnp) 2301 | else 2302 | printLog('Unable to load disabled devices!', res) 2303 | end 2304 | end 2305 | end 2306 | 2307 | local f, err = loadfile(lfs.writedir() .. 'Config/autoexec.cfg') 2308 | 2309 | if f then 2310 | local env = {} 2311 | 2312 | setmetatable(env, {__index = _G}) 2313 | setfenv(f, env) 2314 | 2315 | local ok, res = pcall(f) 2316 | if ok then 2317 | turnLocalizationHintsOn_ = env.input_localization_hints_on 2318 | end 2319 | end 2320 | end, 2321 | enablePrintToLog = function(enable) 2322 | printLogEnabled_ = enable 2323 | end, 2324 | getUnitMarker = function() 2325 | return 'Unit ' 2326 | end, 2327 | getProfileNames = function() 2328 | local result = {} 2329 | for i, profile in ipairs(profiles_) do 2330 | if profile.visible then 2331 | table.insert(result, profile.name) 2332 | end 2333 | end 2334 | return result 2335 | end, 2336 | getProfileNameByUnitName = function(unitName) 2337 | local unitProfile 2338 | 2339 | for i, profile in ipairs(profiles_) do 2340 | if profile.unitName == unitName then 2341 | unitProfile = profile 2342 | 2343 | break 2344 | end 2345 | end 2346 | 2347 | if not unitProfile then 2348 | unitProfile = aliases_[unitName] 2349 | end 2350 | 2351 | if unitProfile then 2352 | return unitProfile.name 2353 | end 2354 | end, 2355 | getProfileUnitName = function(profileName) 2356 | local profile = findProfile_(profileName) 2357 | if profile then 2358 | return profile.unitName 2359 | end 2360 | end, 2361 | getProfileModifiers = function(profileName) 2362 | local modifiers = {} 2363 | local profile = getLoadedProfile_(profileName) 2364 | 2365 | if profile then 2366 | U.copyTable(modifiers, profile.modifiers) 2367 | end 2368 | 2369 | return modifiers 2370 | end, 2371 | getProfileModified = function(profileName) 2372 | local profile = findProfile_(profileName) 2373 | return profile and profile.modified 2374 | end, 2375 | getProfileChanged = function(profileName) 2376 | local profile = findProfile_(profileName) 2377 | return profile and profile.loaded and profile.modified 2378 | end, 2379 | getProfileCategoryNames = function(profileName) 2380 | local result = {} 2381 | local profile = getLoadedProfile_(profileName) 2382 | 2383 | if profile then 2384 | if not profile.loaded then 2385 | loadProfile(profile) 2386 | end 2387 | 2388 | if profile.categories then 2389 | U.copyTable(result, profile.categories) 2390 | end 2391 | end 2392 | 2393 | return result 2394 | end, 2395 | getProfileKeyCommand = function(profileName, commandHash) 2396 | local profile = getLoadedProfile_(profileName) 2397 | 2398 | if profile then 2399 | local command = profile.keyCommands[commandHash] 2400 | 2401 | if command then 2402 | return U.copyTable(nil, command) 2403 | end 2404 | end 2405 | end, 2406 | getProfileAxisCommand = function(profileName, commandHash) 2407 | local profile = getLoadedProfile_(profileName) 2408 | 2409 | if profile then 2410 | local command = profile.axisCommands[commandHash] 2411 | 2412 | if command then 2413 | return U.copyTable(nil, command) 2414 | end 2415 | end 2416 | end, 2417 | createProfile = function(profileInfo) 2418 | local profile = findProfile_(profileInfo.name) 2419 | 2420 | if profile then 2421 | -- некоторые профили используются разными юнитами 2422 | -- например Spitfire 2423 | -- InputProfiles = { 2424 | -- ["SpitfireLFMkIX"] = current_mod_path .. '/Input/SpitfireLFMkIX', 2425 | -- ["SpitfireLFMkIXCW"] = current_mod_path .. '/Input/SpitfireLFMkIX', 2426 | -- }, 2427 | if profile.unitName ~= profileInfo.unitName then 2428 | aliases_[profileInfo.unitName] = profile 2429 | end 2430 | else 2431 | profile = createProfileTable_(profileInfo.name, 2432 | profileInfo.folder, 2433 | profileInfo.unitName, 2434 | profileInfo.default, 2435 | profileInfo.visible, 2436 | profileInfo.loadDefaultUnitProfile) 2437 | 2438 | table.insert(profiles_, profile) 2439 | end 2440 | end, 2441 | getProfileRawKeyCommands = function(profileName) 2442 | local result = {} 2443 | local profile = getLoadedProfile_(profileName) 2444 | 2445 | if profile then 2446 | result = U.copyTable(nil, profile.keyCommands) 2447 | end 2448 | 2449 | return result 2450 | end, 2451 | getProfileRawAxisCommands = function(profileName) 2452 | local result = {} 2453 | local profile = getLoadedProfile_(profileName) 2454 | if profile then 2455 | result = U.copyTable(nil, profile.axisCommands) 2456 | end 2457 | return result 2458 | end, 2459 | getDefaultKeyCommand = function(profileName, commandHash) 2460 | local command = findDefaultKeyCommand_(profileName, commandHash) 2461 | 2462 | if command then 2463 | return U.copyTable(nil, command) 2464 | end 2465 | end, 2466 | setDefaultKeyCommandCombos = function(profileName, commandHash, deviceName) 2467 | local defaultKeyCommand = findDefaultKeyCommand_(profileName, commandHash) 2468 | if not defaultKeyCommand then 2469 | return 2470 | end 2471 | local keyCommand = findKeyCommand_(profileName, commandHash) 2472 | if not keyCommand then 2473 | return 2474 | end 2475 | local profile = getLoadedProfile_(profileName) 2476 | local keyCommands = profile.keyCommands 2477 | setDefaultCommandCombos_(profileName, deviceName, defaultKeyCommand, keyCommand, keyCommands) 2478 | setProfileModified_(profile, true) 2479 | end, 2480 | addComboToKeyCommand = function(profileName, commandHash, deviceName, combo) 2481 | local command = findKeyCommand_(profileName, commandHash) 2482 | 2483 | if command then 2484 | local profile = getLoadedProfile_(profileName) 2485 | local commands = profile.keyCommands 2486 | 2487 | removeComboFromCommands_(profileName, deviceName, commands, combo) 2488 | addComboToCommand_(profileName, deviceName, command, combo) 2489 | setProfileModified_(profile, true) 2490 | end 2491 | end, 2492 | addComboToAxisCommand = function(profileName, commandHash, deviceName, combo) 2493 | local command = findAxisCommand_(profileName, commandHash) 2494 | 2495 | if command then 2496 | local profile = getLoadedProfile_(profileName) 2497 | local commands = profile.axisCommands 2498 | 2499 | removeComboFromCommands_(profileName, deviceName, commands, combo) 2500 | addComboToCommand_(profileName, deviceName, command, combo) 2501 | setProfileModified_(profile, true) 2502 | end 2503 | end, 2504 | removeKeyCommandCombos = function(profileName, commandHash, deviceName) 2505 | local command = findKeyCommand_(profileName, commandHash) 2506 | 2507 | removeCombosFromCommand_(profileName, command, deviceName) 2508 | setProfileModified_(getLoadedProfile_(profileName), true) 2509 | end, 2510 | removeAxisCommandCombos = function(profileName, commandHash, deviceName) 2511 | local command = findAxisCommand_(profileName, commandHash) 2512 | 2513 | removeCombosFromCommand_(profileName, command, deviceName) 2514 | setProfileModified_(getLoadedProfile_(profileName), true) 2515 | end, 2516 | getDefaultAxisCommand = function(profileName, commandHash) 2517 | local command = findDefaultAxisCommand_(profileName, commandHash) 2518 | 2519 | if command then 2520 | return U.copyTable(nil, command) 2521 | end 2522 | end, 2523 | setDefaultAxisCommandCombos = function(profileName, commandHash, deviceName) 2524 | local defaultAxisCommand = findDefaultAxisCommand_(profileName, commandHash) 2525 | if not defaultAxisCommand then 2526 | return 2527 | end 2528 | local axisCommand = findAxisCommand_(profileName, commandHash) 2529 | if not axisCommand then 2530 | return 2531 | end 2532 | local profile = getLoadedProfile_(profileName) 2533 | local axisCommands = profile.axisCommands 2534 | 2535 | setDefaultCommandCombos_(profileName, deviceName, defaultAxisCommand, axisCommand, axisCommands) 2536 | setProfileModified_(profile, true) 2537 | end, 2538 | setAxisCommandComboFilter = function(profileName, commandHash, deviceName, filters) 2539 | local command = findAxisCommand_(profileName, commandHash) 2540 | local combos = command.combos 2541 | 2542 | if combos then 2543 | setAxisComboFilters(combos[deviceName], filters) 2544 | setProfileModified_(getLoadedProfile_(profileName), true) 2545 | end 2546 | end, 2547 | setProfileForceFeedbackSettings = function(profileName, deviceName, settings) 2548 | local profile = getLoadedProfile_(profileName) 2549 | 2550 | profile.forceFeedback[deviceName] = U.copyTable(nil, settings) 2551 | setProfileModified_(profile, true) 2552 | end, 2553 | setProfileModifiers = function(profileName, modifiers) 2554 | local profile = getLoadedProfile_(profileName) 2555 | profile.modifiers = U.copyTable(nil, modifiers) 2556 | for i, profile in ipairs(profiles_) do 2557 | local profileName = profile.name 2558 | 2559 | validateCommands_(profileName, profile.keyCommands) 2560 | validateCommands_(profileName, profile.axisCommands) 2561 | end 2562 | setProfileModified_(profile, true) 2563 | end, 2564 | getDefaultProfileName = function() 2565 | for i, profile in ipairs(profiles_) do 2566 | if profile.default then 2567 | return profile.name 2568 | end 2569 | end 2570 | end, 2571 | loadDeviceProfile = function(profileName, deviceName, filename) 2572 | local deviceProfile = getDeviceProfile(profileName, deviceName) 2573 | if not deviceProfile then 2574 | return 2575 | end 2576 | local diff = loadDeviceProfileDiffFromFile_(filename) 2577 | if not diff then 2578 | return 2579 | end 2580 | 2581 | local profile = getLoadedProfile_(profileName) 2582 | 2583 | applyDiffToDeviceProfile_(deviceProfile, diff) 2584 | setProfileDeviceProfile_(profile, deviceName, deviceProfile) 2585 | 2586 | deleteDeviceCombos_(profile.keyCommands, deviceName) 2587 | deleteDeviceCombos_(profile.axisCommands, deviceName) 2588 | 2589 | addProfileKeyCommands(profileName, deviceName, deviceProfile, profile.keyCommands) 2590 | addProfileAxisCommands(profileName, deviceName, deviceProfile, profile.axisCommands) 2591 | addProfileForceFeedbackSettings(profile, deviceName, deviceProfile) 2592 | 2593 | setProfileModified_(profile, true) 2594 | end, 2595 | saveChanges = function() 2596 | local devices = InputUtils.getDevices() 2597 | 2598 | for i, profile in ipairs(profiles_) do 2599 | if profile.loaded and profile.modified then 2600 | local profileName = profile.name 2601 | local profileUserConfigPath = getProfileUserConfigPath_(profile) 2602 | 2603 | lfs.mkdir(profileUserConfigPath) 2604 | 2605 | saveProfileModifiers_(profileName, profileUserConfigPath) 2606 | 2607 | for j, deviceName in ipairs(devices) do 2608 | local deviceTypeName = InputUtils.getDeviceTypeName(deviceName) 2609 | local folder = string.format('%s%s', profileUserConfigPath, deviceTypeName) 2610 | local filename = string.format('%s/%s.diff.lua', folder, deviceName) 2611 | 2612 | lfs.mkdir(folder) 2613 | saveDeviceProfile(profileName, deviceName, filename) 2614 | end 2615 | 2616 | setProfileModified_(profile, false) 2617 | end 2618 | end 2619 | 2620 | saveDisabledDevices() 2621 | 2622 | if controller_ then 2623 | controller_.inputDataSaved() 2624 | end 2625 | end, 2626 | undoChanges = function() 2627 | for i, profile in ipairs(profiles_) do 2628 | if profile.loaded and profile.modified then 2629 | profiles_[i] = createProfileTable_( profile.name, 2630 | profile.folder, 2631 | profile.unitName, 2632 | profile.default, 2633 | profile.visible, 2634 | profile.loadDefaultUnitProfile) 2635 | end 2636 | end 2637 | 2638 | if controller_ then 2639 | controller_.inputDataRestored() 2640 | end 2641 | end, 2642 | getProfileFolder = function(profileName) 2643 | local profile = findProfile_(profileName) 2644 | if profile then 2645 | return profile.folder 2646 | end 2647 | end, 2648 | unloadProfiles = function() -- подключено/отключено устройство - сбрасываем все загруженные профили 2649 | wizard_assigments = nil 2650 | local newProfiles = {} 2651 | for i, profile in ipairs(profiles_) do 2652 | local newProfile = createProfileTable_( profile.name, 2653 | profile.folder, 2654 | profile.unitName, 2655 | profile.default, 2656 | profile.visible, 2657 | profile.loadDefaultUnitProfile) 2658 | 2659 | table.insert(newProfiles, newProfile) 2660 | end 2661 | profiles_ = newProfiles 2662 | end, 2663 | saveFullDeviceProfile = function(profileName, deviceName, filename)-- используется в Utils/Input/CreateDefaultDeviceLayout.lua 2664 | local file, err = io.open(filename, 'w') 2665 | 2666 | if file then 2667 | file:write('return {\n') 2668 | 2669 | writeForceFeedbackToFile(file, profileName, deviceName) 2670 | writeKeyCommandsToFile (file, profileName, deviceName) 2671 | writeAxisCommandsToFile (file, profileName, deviceName) 2672 | 2673 | file:write('}') 2674 | file:close() 2675 | else 2676 | log.error(string.format('Cannot save profile into file[%s]! Error %s', filename, err)) 2677 | end 2678 | end, 2679 | getKeyIsInUseInUiLayer = function(deviceName, key) -- кнопка назначена в слое для UI 2680 | return uiLayerKeyHashes_[createKeyHash_(deviceName, key)] 2681 | end, 2682 | clearProfile = function(profileName, deviceNames) 2683 | local profile = getLoadedProfile_(profileName) 2684 | if not profile then 2685 | return 2686 | end 2687 | for i, deviceName in ipairs(deviceNames) do 2688 | for commandHash, command in pairs(profile.axisCommands) do 2689 | removeCombosFromCommand_(profileName, command, deviceName) 2690 | end 2691 | 2692 | for commandHash, command in pairs(profile.keyCommands) do 2693 | removeCombosFromCommand_(profileName, command, deviceName) 2694 | end 2695 | end 2696 | setProfileModified_(profile, true) 2697 | end, 2698 | setDeviceDisabled = function(deviceName, disabled) 2699 | if disabled then 2700 | disabledDevices_[deviceName] = true 2701 | else 2702 | disabledDevices_[deviceName] = nil 2703 | end 2704 | Input.setDeviceDisabled(deviceName, disabled) 2705 | end, 2706 | getDeviceDisabled = function(deviceName) 2707 | return disabledDevices_[deviceName] or false 2708 | end, 2709 | getWizardAssignments = function() 2710 | if wizard_assigments then 2711 | return wizard_assigments 2712 | end 2713 | --load from user folder 2714 | local f, err = loadfile(lfs.writedir() .. 'Config/Input/wizard.lua') 2715 | if f then 2716 | wizard_assigments = f() 2717 | end 2718 | return wizard_assigments 2719 | end, 2720 | getDefaultKeyCommands = function(profileName) 2721 | local profile = getLoadedProfile_(profileName) 2722 | 2723 | return U.copyTable(nil, profile.defaultKeyCommands) 2724 | end, 2725 | getDefaultAxisCommands = function(profileName) 2726 | local profile = getLoadedProfile_(profileName) 2727 | return U.copyTable(nil, profile.defaultAxisCommands) 2728 | end, 2729 | updateCommandValidation = updateCommandValidation, 2730 | } 2731 | ------------------------------------------------------------------------------ 2732 | return module_interface --------------------------------------------------------------------------------