├── DCS-Input-Command-Injector-Quaggles ├── DCS-Input-Command-Injector-Quaggles │ └── Scripts │ │ └── Input │ │ └── Data.lua ├── README.txt └── VERSION.txt ├── Inject.lua ├── InputCommands.zip └── README.md /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 | if result then 1202 | local profile = findProfile_(profileName) 1203 | 1204 | local modifiers = profile.modifiers 1205 | 1206 | for deviceName, combos in pairs(command.combos) do 1207 | for i, combo in ipairs(combos) do 1208 | local warnings = {} 1209 | 1210 | combo.valid = getComboValid_(profileName, command, deviceName, combo, modifiers, warnings) 1211 | 1212 | -- проверим, что кнопки комбо не пересекаются с кнопками из UI Layer 1213 | if profileName ~= getUiProfileName() then 1214 | local uiValid = getComboValidUiLayer_(profileName, command, deviceName, combo, modifiers, warnings) 1215 | 1216 | combo.valid = combo.valid and uiValid 1217 | end 1218 | 1219 | combo.warnings = makeComboWarningString_(warnings) 1220 | result = result and combo.valid 1221 | end 1222 | end 1223 | end 1224 | 1225 | return result 1226 | end 1227 | 1228 | local function findCommandByHash_(commands, commandHash) 1229 | if commands then 1230 | return commands[commandHash] 1231 | end 1232 | end 1233 | 1234 | local function findKeyCommand_(profileName, commandHash) 1235 | local profile = getLoadedProfile_(profileName) 1236 | 1237 | return findCommandByHash_(profile.keyCommands, commandHash) 1238 | end 1239 | 1240 | local function findDefaultKeyCommand_(profileName, commandHash) 1241 | local profile = getLoadedProfile_(profileName) 1242 | 1243 | return findCommandByHash_(profile.defaultKeyCommands, commandHash) 1244 | end 1245 | 1246 | local function findAxisCommand_(profileName, commandHash) 1247 | local profile = getLoadedProfile_(profileName) 1248 | 1249 | return findCommandByHash_(profile.axisCommands, commandHash) 1250 | end 1251 | 1252 | local function findDefaultAxisCommand_(profileName, commandHash) 1253 | local profile = getLoadedProfile_(profileName) 1254 | 1255 | return findCommandByHash_(profile.defaultAxisCommands, commandHash) 1256 | end 1257 | 1258 | local function getCommandModifiedCombos_(command, deviceName) 1259 | local modifiedCombos = command.modifiedCombos 1260 | 1261 | if modifiedCombos then 1262 | return modifiedCombos[deviceName] 1263 | end 1264 | 1265 | return false 1266 | end 1267 | 1268 | local function addComboToCommand_(profileName, deviceName, command, combo) 1269 | if command then 1270 | local deviceCombos = command.combos[deviceName] 1271 | 1272 | if not findCombo_(deviceCombos, combo) then 1273 | if not deviceCombos then 1274 | deviceCombos = {} 1275 | command.combos[deviceName] = deviceCombos 1276 | end 1277 | 1278 | table.insert(deviceCombos, U.copyTable(nil, combo)) 1279 | command.valid = validateProfileCommandCombos(profileName, command) 1280 | end 1281 | end 1282 | end 1283 | 1284 | local function removeComboFromCommand_(profileName, deviceName, command, combo) 1285 | if command then 1286 | local deviceCombos = command.combos[deviceName] 1287 | local comboIndex = findCombo_(deviceCombos, combo) 1288 | if comboIndex then 1289 | table.remove(deviceCombos, comboIndex) 1290 | 1291 | command.valid = validateProfileCommandCombos(profileName, command) 1292 | end 1293 | end 1294 | end 1295 | 1296 | local function removeCombosFromCommand_(profileName, command, deviceName) 1297 | if command then 1298 | local deviceCombos = command.combos[deviceName] 1299 | 1300 | if deviceCombos then 1301 | while #deviceCombos > 0 do 1302 | table.remove(deviceCombos) 1303 | end 1304 | end 1305 | 1306 | command.valid = validateProfileCommandCombos(profileName, command) 1307 | end 1308 | end 1309 | 1310 | local function removeComboFromCommands_(profileName, deviceName, commands, combo) 1311 | for commandHash, command in pairs(commands) do 1312 | removeComboFromCommand_(profileName, deviceName, command, combo) 1313 | end 1314 | end 1315 | 1316 | local function setDefaultCommandCombos_(profileName, deviceName, defaultCommand, command, commands) 1317 | removeCombosFromCommand_(profileName, command, deviceName) 1318 | 1319 | for combo in commandCombos(defaultCommand, deviceName) do 1320 | removeComboFromCommands_(profileName, deviceName, commands, combo) 1321 | addComboToCommand_(profileName, deviceName, command, combo) 1322 | end 1323 | end 1324 | 1325 | local function setDefaultCommandsCategoryCombos_(profileName, commands, deviceName, category) 1326 | for commandHash, command in pairs(commands) do 1327 | if getCommandBelongsToCategory(category, command) then 1328 | local defaultKeyCommand = findDefaultKeyCommand_(profileName, commandHash) 1329 | 1330 | if defaultKeyCommand then 1331 | setDefaultCommandCombos_(profileName,deviceName, defaultKeyCommand, command, commands) 1332 | end 1333 | end 1334 | end 1335 | end 1336 | 1337 | local function addProfileKeyCommand(profileName, deviceName, keyCommand, commandsHashTable, combosHashTable) 1338 | local commandHash = InputUtils.getKeyCommandHash(keyCommand) 1339 | local command = commandsHashTable[commandHash] 1340 | 1341 | if command then 1342 | if command.name ~= keyCommand.name then 1343 | printLog('Profile[' .. profileName .. '] key command[' .. 1344 | keyCommand.name .. '] for device[' .. 1345 | deviceName.. '] has different name from command[' .. 1346 | command.name .. '] for device[' .. 1347 | getCommandDeviceNamesString(command) .. ']') 1348 | end 1349 | 1350 | command.combos[deviceName] = keyCommand.combos or {} 1351 | else 1352 | command = {combos = {}} 1353 | 1354 | copyDeviceCommandToProfileCommand(deviceName, keyCommand, command) 1355 | 1356 | command.name = keyCommand.name 1357 | command.disabled = keyCommand.disabled 1358 | command.hash = commandHash 1359 | commandsHashTable[commandHash] = command 1360 | end 1361 | 1362 | command.valid = validateProfileCommandCombos(profileName, command) 1363 | end 1364 | 1365 | local function addProfileKeyCommands(profileName, deviceName, deviceProfile, commandsHashTable) 1366 | -- deviceProfile это таблица, загруженная из файла 1367 | local keyCommands = deviceProfile.keyCommands 1368 | 1369 | if keyCommands then 1370 | local combosHashTable = {} 1371 | 1372 | for i, keyCommand in ipairs(keyCommands) do 1373 | addProfileKeyCommand(profileName, deviceName, keyCommand, commandsHashTable, combosHashTable) 1374 | end 1375 | end 1376 | end 1377 | 1378 | local function addProfileAxisCommand(profileName, deviceName, axisCommand, commandsHashTable, combosHashTable) 1379 | local commandHash = InputUtils.getAxisCommandHash(axisCommand) 1380 | if not commandHash then 1381 | return 1382 | end 1383 | local command = commandsHashTable[commandHash] 1384 | 1385 | if command then 1386 | if command.name ~= axisCommand.name then 1387 | printLog('Profile[' .. profileName .. '] axis command[' .. 1388 | axisCommand.name .. '] for device[' .. 1389 | deviceName.. '] has different name from command[' .. command.name .. '] for device[' .. 1390 | getCommandDeviceNamesString(command) .. ']') 1391 | end 1392 | 1393 | command.combos[deviceName] = axisCommand.combos or {} 1394 | else 1395 | command = {combos = {}} 1396 | 1397 | copyDeviceCommandToProfileCommand(deviceName, axisCommand, command) 1398 | 1399 | command.name = axisCommand.name 1400 | command.hash = commandHash 1401 | commandsHashTable[commandHash] = command 1402 | end 1403 | 1404 | command.valid = validateProfileCommandCombos(profileName, command) 1405 | end 1406 | 1407 | local function addProfileAxisCommands(profileName, deviceName, deviceProfile, commandsHashTable) 1408 | if deviceProfile.axisCommands then 1409 | local combosHashTable = {} 1410 | 1411 | for i, axisCommand in ipairs(deviceProfile.axisCommands) do 1412 | addProfileAxisCommand(profileName, deviceName, axisCommand, commandsHashTable, combosHashTable) 1413 | end 1414 | end 1415 | end 1416 | 1417 | local function addProfileForceFeedbackSettings(profile, deviceName, deviceProfile) 1418 | if deviceProfile.forceFeedback then 1419 | profile.forceFeedback[deviceName] = U.copyTable(nil, deviceProfile.forceFeedback) 1420 | end 1421 | end 1422 | 1423 | local function getProfileForceFeedbackSettings(profileName, deviceName) 1424 | local profile = getLoadedProfile_(profileName) 1425 | local ffSettings = profile.forceFeedback[deviceName] 1426 | 1427 | if ffSettings then 1428 | return createForceFeedbackSettings(ffSettings) 1429 | end 1430 | end 1431 | 1432 | local function validateCommands_(profileName, commands) 1433 | if commands then 1434 | for commandHash, command in pairs(commands) do 1435 | command.valid = validateProfileCommandCombos(profileName, command) 1436 | end 1437 | end 1438 | end 1439 | 1440 | local function setAxisComboFilters(combos, filters) 1441 | if combos then 1442 | for i, combo in ipairs(combos) do 1443 | local axis = combo.key 1444 | 1445 | if axis then 1446 | local filter = filters[axis] 1447 | 1448 | if filter then 1449 | combo.filter = createAxisFilter(filter) 1450 | end 1451 | end 1452 | end 1453 | end 1454 | end 1455 | 1456 | local function setProfileDeviceProfile_(profile, deviceName, deviceProfile) 1457 | validateDeviceProfile_(profile.name, deviceProfile, deviceName) 1458 | profile.deviceProfiles[deviceName] = deviceProfile 1459 | end 1460 | 1461 | local function loadModifiersFromFolder_(folder) 1462 | local result 1463 | local filename = folder .. '/modifiers.lua' 1464 | local f, err = loadfile(filename) 1465 | 1466 | if f then 1467 | printFileLog('File[' .. filename .. '] opened successfully!') 1468 | 1469 | result = f() 1470 | else 1471 | printFileLog(err) 1472 | end 1473 | 1474 | return result, err 1475 | end 1476 | 1477 | -- загружаем измененные пользователем модификаторы 1478 | local function loadProfileUserModifiers_(profile, errors) 1479 | errors = errors or {} 1480 | 1481 | local folder = getProfileUserConfigPath_(profile) 1482 | local modifiers = collectErrors_(errors, loadModifiersFromFolder_(folder)) 1483 | 1484 | if not modifiers and userConfigPath_ ~= nil then 1485 | -- в предыдущей версии инпута измененные модификаторы 1486 | -- располагались в пользовательской папке с профилями 1487 | folder = userConfigPath_ 1488 | modifiers = collectErrors_(errors, loadModifiersFromFolder_(folder)) 1489 | end 1490 | 1491 | return modifiers, folder, errors 1492 | end 1493 | 1494 | -- загружаем дефолтные модификаторы 1495 | local function loadProfileDefaultModifiers_(profile, errors) 1496 | errors = errors or {} 1497 | 1498 | local folder = profile.folder 1499 | local modifiers = collectErrors_(errors, loadModifiersFromFolder_(folder)) 1500 | 1501 | if not modifiers then 1502 | folder = sysPath_ 1503 | modifiers = collectErrors_(errors, loadModifiersFromFolder_(folder)) 1504 | end 1505 | 1506 | return modifiers, folder, errors 1507 | end 1508 | 1509 | local function loadProfileModifiers_(profile) 1510 | local errors = {} 1511 | local modifiers, folder = loadProfileUserModifiers_(profile, errors) 1512 | 1513 | if not modifiers then 1514 | modifiers, folder = loadProfileDefaultModifiers_(profile, errors) 1515 | end 1516 | 1517 | if not modifiers and #errors > 0 then 1518 | printLog('Profile [' .. profile.name .. '] cannot load modifiers!', table.concat(errors, '\n')) 1519 | end 1520 | 1521 | return modifiers, folder 1522 | end 1523 | 1524 | local function getDevicesHash_(folder) 1525 | local result = {} 1526 | local devices = InputUtils.getDevices() 1527 | 1528 | for i, deviceName in ipairs(devices) do 1529 | if folder == sysPath_ then 1530 | local deviceTemplateName = InputUtils.getDeviceTemplateName(deviceName) 1531 | 1532 | result[deviceTemplateName] = deviceName 1533 | else 1534 | result[deviceName] = deviceName 1535 | end 1536 | end 1537 | 1538 | return result 1539 | end 1540 | 1541 | local function createModifier(key, deviceName, switch) 1542 | local event = InputUtils.getInputEvent(key) 1543 | local deviceId = Input.getDeviceId(deviceName) 1544 | return {key = key, event = event, deviceId = deviceId, deviceName = deviceName, switch = switch} 1545 | end 1546 | 1547 | local function createProfileModifiers_(profile) 1548 | local profileModifiers = {} 1549 | local modifiers, folder = loadProfileModifiers_(profile) 1550 | 1551 | if modifiers then 1552 | -- у модификаторов, загружаемых из дефолтной папки sysPath_ 1553 | -- имена устройств не содержат CLSID 1554 | local devicesHash = getDevicesHash_(folder) 1555 | 1556 | for name, modifier in pairs(modifiers) do 1557 | local modifierDeviceName = modifier.device 1558 | local deviceName = devicesHash[modifierDeviceName] 1559 | 1560 | if deviceName then 1561 | local key = modifier.key 1562 | local switch = modifier.switch 1563 | 1564 | profileModifiers[name] = createModifier(key, deviceName, switch) 1565 | end 1566 | end 1567 | end 1568 | 1569 | profile.modifiers = profileModifiers 1570 | end 1571 | 1572 | local function deleteDeviceCombos_(commands, deviceName) 1573 | for commandHash, command in pairs(commands) do 1574 | local combos = command.combos 1575 | 1576 | combos[deviceName] = nil 1577 | 1578 | if not next(combos) then 1579 | -- комбинаций для других устройств в этой команде нет, ее можно удалить 1580 | commands[commandHash] = nil 1581 | end 1582 | end 1583 | end 1584 | 1585 | local function getDeviceProfile(profileName, deviceName) 1586 | local profile = getLoadedProfile_(profileName) 1587 | local deviceProfile = loadDeviceProfile_(profile, deviceName) 1588 | 1589 | return deviceProfile 1590 | end 1591 | 1592 | local function getForceFeedbackSettingsDiff_(forceFeedbackSettings, defaultForceFeedbackSettings) 1593 | local diff = {} 1594 | 1595 | for key, value in pairs(forceFeedbackSettings) do 1596 | if value ~= defaultForceFeedbackSettings[key] then 1597 | diff[key] = value 1598 | end 1599 | end 1600 | 1601 | if next(diff) then 1602 | return diff 1603 | end 1604 | end 1605 | 1606 | local function getForceFeedbackDiff_(profile, deviceName) 1607 | local forceFeedback = profile.forceFeedback[deviceName] 1608 | if not forceFeedback then 1609 | return 1610 | end 1611 | local forceFeedbackSettings = createForceFeedbackSettings(forceFeedback) 1612 | local defaultDeviceProfile = loadProfileDefaultDeviceProfile_(profile, deviceName) 1613 | local defaultForceFeedbackSettings = createForceFeedbackSettings(defaultDeviceProfile.forceFeedback) 1614 | 1615 | return getForceFeedbackSettingsDiff_(forceFeedbackSettings, defaultForceFeedbackSettings) 1616 | end 1617 | 1618 | local function compareFilters_(filter1, filter2) 1619 | if filter1.deadzone == filter2.deadzone and 1620 | filter1.saturationX == filter2.saturationX and 1621 | filter1.saturationY == filter2.saturationY and 1622 | filter1.hardwareDetent == filter2.hardwareDetent and 1623 | filter1.slider == filter2.slider and 1624 | filter1.invert == filter2.invert and 1625 | #filter1.curvature == #filter2.curvature then 1626 | 1627 | for i, value in ipairs(filter1.curvature) do 1628 | if value ~= filter2.curvature[i] then 1629 | return false 1630 | end 1631 | end 1632 | 1633 | if filter1.hardwareDetent and filter2.hardwareDetent then 1634 | if filter1.hardwareDetentMax ~= filter1.hardwareDetentMax then 1635 | return false 1636 | end 1637 | 1638 | if filter1.hardwareDetentAB ~= filter1.hardwareDetentAB then 1639 | return false 1640 | end 1641 | end 1642 | 1643 | return true 1644 | end 1645 | 1646 | return false 1647 | end 1648 | 1649 | local function getFiltersAreEqual_(filter1, filter2) 1650 | if filter1 == filter2 then 1651 | return true 1652 | end 1653 | 1654 | return compareFilters_(createAxisFilter(filter1), createAxisFilter(filter2)) 1655 | end 1656 | 1657 | local function cleanupCombo_(combo, checkDefaultFilter) 1658 | local reformers = combo.reformers 1659 | 1660 | if reformers then 1661 | if not next(reformers) then 1662 | reformers = nil 1663 | end 1664 | end 1665 | 1666 | local filter = combo.filter 1667 | 1668 | if checkDefaultFilter then 1669 | if filter then 1670 | if getFiltersAreEqual_(createAxisFilter(filter), createAxisFilter()) then 1671 | filter = nil 1672 | end 1673 | end 1674 | end 1675 | 1676 | return { 1677 | key = combo.key, 1678 | reformers = reformers, 1679 | filter = filter, 1680 | column = combo.column, 1681 | } 1682 | end 1683 | 1684 | local function getCommandAddedCombos_(command, defaultCommand, deviceName) 1685 | local combos = command.combos[deviceName] 1686 | if not combos then 1687 | return 1688 | end 1689 | local defaultCombos = defaultCommand.combos[deviceName] 1690 | local result 1691 | for i, combo in ipairs(combos) do 1692 | if not findCombo_(defaultCombos, combo) then 1693 | result = result or {} 1694 | table.insert(result, cleanupCombo_(combo, true)) 1695 | end 1696 | end 1697 | return result 1698 | end 1699 | 1700 | local function getCommandRemovedCombos_(command, defaultCommand, deviceName) 1701 | local defaultCombos = defaultCommand.combos[deviceName] 1702 | if not defaultCombos then 1703 | return nil 1704 | end 1705 | local combos = command.combos[deviceName] 1706 | local result 1707 | for i, combo in ipairs(defaultCombos) do 1708 | if not findCombo_(combos, combo) then 1709 | result = result or {} 1710 | table.insert(result, cleanupCombo_(combo)) 1711 | end 1712 | end 1713 | return result 1714 | end 1715 | 1716 | local function getCommandChangedFilterCombos_(command, defaultCommand, deviceName) 1717 | local result 1718 | local combos = command.combos[deviceName] 1719 | local defaultCombos = defaultCommand.combos[deviceName] 1720 | 1721 | if combos then 1722 | for i, combo in ipairs(combos) do 1723 | local index = findCombo_(defaultCombos, combo) 1724 | 1725 | if index then 1726 | local defaultCombo = defaultCombos[index] 1727 | 1728 | if not getFiltersAreEqual_(combo.filter, defaultCombo.filter) then 1729 | result = result or {} 1730 | table.insert(result, cleanupCombo_(combo)) 1731 | end 1732 | end 1733 | end 1734 | end 1735 | return result 1736 | end 1737 | 1738 | local function getCommandDiffCommon_(command, defaultCommand, deviceName) 1739 | local addedCombos = getCommandAddedCombos_ (command, defaultCommand, deviceName) 1740 | local removedCombos = getCommandRemovedCombos_(command, defaultCommand, deviceName) 1741 | 1742 | if addedCombos or removedCombos then 1743 | return { 1744 | name = command.name, 1745 | added = addedCombos, 1746 | removed = removedCombos, 1747 | } 1748 | end 1749 | return nil 1750 | end 1751 | 1752 | local function storeDeviceProfileDiffIntoFile_(filename, diff) 1753 | local file, err = io.open(filename, 'w') 1754 | if not file then 1755 | log.error(string.format('Cannot save profile into file[%s]! Error %s', filename, err)) 1756 | return 1757 | end 1758 | 1759 | local s = Serializer.new(file) 1760 | s:serialize_sorted('local diff', diff) 1761 | file:write('return diff') 1762 | file:close() 1763 | end 1764 | 1765 | local function saveDeviceProfile(profileName, deviceName, filename) 1766 | local profile = getLoadedProfile_(profileName) 1767 | local calcDiff = function(commands,base,calculator) 1768 | local diffs = {} 1769 | for commandHash, command in pairs(commands) do 1770 | local base_command = base[commandHash] 1771 | if base_command then 1772 | local commandDiff = calculator(command, base_command, deviceName) 1773 | if commandDiff then 1774 | diffs[commandHash] = commandDiff 1775 | end 1776 | else 1777 | -- возможно команда сохранена в пользовательских настройках, 1778 | -- но после обновления она исчезла из дефолтных настроек 1779 | print("Cannot find base command for hash", commandHash, command.name, profile.name, deviceName) 1780 | end 1781 | end 1782 | 1783 | if next(diffs) then 1784 | return diffs 1785 | end 1786 | end 1787 | 1788 | local getCommandDiffAxis = function (command, defaultCommand, deviceName) 1789 | local res = getCommandDiffCommon_ (command, defaultCommand, deviceName) 1790 | local changedFilterCombos = getCommandChangedFilterCombos_(command, defaultCommand, deviceName) 1791 | if changedFilterCombos then 1792 | if not res then 1793 | res = { 1794 | name = command.name, 1795 | } 1796 | end 1797 | res.changed = changedFilterCombos 1798 | end 1799 | return res 1800 | end 1801 | 1802 | local diff = { 1803 | ffDiffs = getForceFeedbackDiff_(profile, deviceName), 1804 | keyDiffs = calcDiff(profile.keyCommands ,profile.baseKeyCommands ,getCommandDiffCommon_), 1805 | axisDiffs = calcDiff(profile.axisCommands,profile.baseAxisCommands,getCommandDiffAxis), 1806 | } 1807 | 1808 | if next(diff) then 1809 | storeDeviceProfileDiffIntoFile_(filename, diff) 1810 | else 1811 | os.remove(filename) 1812 | end 1813 | end 1814 | 1815 | local function compareModifiers_(modifier1, modifier2) 1816 | if modifier1 then 1817 | if modifier2 then 1818 | return modifier1.key == modifier2.key and 1819 | modifier1.deviceName == modifier2.deviceName and 1820 | (modifier1.switch or false) == (modifier2.switch or false) 1821 | else 1822 | return false 1823 | end 1824 | elseif modifier2 then 1825 | return false 1826 | else 1827 | return true 1828 | end 1829 | end 1830 | 1831 | local function getModifiersAreEqual_(modifiers1, modifiers2) 1832 | if modifiers1 == modifiers2 then 1833 | return true 1834 | end 1835 | 1836 | local comparedNames = {} 1837 | 1838 | for name, modifier in pairs(modifiers1) do 1839 | if compareModifiers_(modifier, modifiers2[name]) then 1840 | comparedNames[name] = true 1841 | else 1842 | return false 1843 | end 1844 | end 1845 | 1846 | for name, modifier in pairs(modifiers2) do 1847 | if not comparedNames[name] then 1848 | if not compareModifiers_(modifier, modifiers1[name]) then 1849 | return false 1850 | end 1851 | end 1852 | end 1853 | 1854 | return true 1855 | end 1856 | 1857 | local function cleanupModifiers_(modifiers) 1858 | local result = {} 1859 | local cleanupModifier = function(modifier) 1860 | return { 1861 | key = modifier.key, 1862 | device = modifier.deviceName, 1863 | switch = modifier.switch or false, 1864 | } 1865 | end 1866 | 1867 | for name, modifier in pairs(modifiers) do 1868 | result[name] = cleanupModifier(modifier) 1869 | end 1870 | 1871 | return result 1872 | end 1873 | 1874 | local function getProfileDefaultModifiers_(profile) 1875 | local defaultModifiers = {} 1876 | 1877 | for name, modifier in pairs(loadProfileDefaultModifiers_(profile) or {}) do 1878 | defaultModifiers[name] = createModifier(modifier.key, modifier.device, modifier.switch) 1879 | end 1880 | 1881 | return defaultModifiers 1882 | end 1883 | 1884 | local function saveProfileModifiers_(profileName, folder) 1885 | local filename = folder .. 'modifiers.lua' 1886 | local profile = findProfile_(profileName) 1887 | local modifiers = profile.modifiers 1888 | local defaultModifiers = getProfileDefaultModifiers_(profile) 1889 | 1890 | if getModifiersAreEqual_(modifiers, defaultModifiers) then 1891 | os.remove(filename) 1892 | else 1893 | local file, err = io.open(filename, 'w') 1894 | 1895 | if file then 1896 | local s = Serializer.new(file) 1897 | s:serialize_sorted('local modifiers', cleanupModifiers_(modifiers)) 1898 | file:write('return modifiers') 1899 | file:close() 1900 | else 1901 | log.error(string.format('Cannot save modifiers into file[%s]! Error %s', filename, err)) 1902 | end 1903 | end 1904 | end 1905 | 1906 | local function saveDisabledDevices() 1907 | if userConfigPath_ == nil then 1908 | return 1909 | end 1910 | local filename = userConfigPath_ .. disabledFilename_ 1911 | local file, err = io.open(filename, 'w') 1912 | 1913 | if file then 1914 | local s = Serializer.new(file) 1915 | local disabled = { 1916 | devices = disabledDevices_, 1917 | pnp = Input.getPnPDisabled(), 1918 | } 1919 | s:serialize_sorted('local disabled', disabled) 1920 | file:write('return disabled') 1921 | file:close() 1922 | else 1923 | log.error(string.format('Cannot save disabled devices into file[%s]! Error %s', filename, err)) 1924 | end 1925 | end 1926 | 1927 | local function getCommandsInfo(profileCommands, commandActionHashInfos, deviceName) 1928 | local result = {} 1929 | 1930 | for i, profileCommand in ipairs(profileCommands) do 1931 | local commandInfo = {} 1932 | 1933 | commandInfo.category = profileCommand.category 1934 | commandInfo.name = profileCommand.name 1935 | commandInfo.features = profileCommand.features 1936 | commandInfo.actions = {} 1937 | 1938 | for i, actionHashInfo in ipairs(commandActionHashInfos) do 1939 | local action = profileCommand[actionHashInfo.name] 1940 | 1941 | if action then 1942 | local inputName 1943 | 1944 | if actionHashInfo.namedAction then 1945 | inputName = InputUtils.getInputActionName(action) -- некоторые команды могут не иметь имени 1946 | end 1947 | 1948 | if not inputName then 1949 | inputName = tostring(action) 1950 | end 1951 | 1952 | table.insert(commandInfo.actions, {name = actionHashInfo.name, inputName = inputName}) 1953 | end 1954 | end 1955 | 1956 | local combos = profileCommand.combos[deviceName] 1957 | 1958 | if combos and next(combos) then 1959 | commandInfo.combos = {} 1960 | 1961 | for i, combo in ipairs(combos) do 1962 | table.insert(commandInfo.combos, {key = combo.key, reformers = combo.reformers, filter = combo.filter}) 1963 | end 1964 | end 1965 | 1966 | table.insert(result, commandInfo) 1967 | end 1968 | 1969 | return result 1970 | end 1971 | 1972 | local function formatFilter(filter) 1973 | return string.format('curvature = {%s}, deadzone = %g, invert = %s, saturationX = %g, saturationY = %g, slider = %s', 1974 | table.concat(filter.curvature, ', '), 1975 | filter.deadzone, 1976 | tostring(filter.invert), 1977 | filter.saturationX, 1978 | filter.saturationY, 1979 | tostring(filter.slider)) 1980 | end 1981 | 1982 | local function formatCombo(combo) 1983 | local result = string.format('{key = %q', combo.key) -- здесь могут быть кавычки, слеши и прочее 1984 | 1985 | if combo.reformers and #combo.reformers > 0 then 1986 | result = string.format('%s, reformers = {"%s"}', result, table.concat(combo.reformers, '", "')) 1987 | end 1988 | 1989 | if combo.filter then 1990 | result = string.format('%s, filter = {%s},', result, formatFilter(combo.filter)) 1991 | end 1992 | 1993 | result = result .. '}, ' 1994 | 1995 | return result 1996 | end 1997 | 1998 | local function formatCommand(commandInfo) 1999 | local result = '{' 2000 | 2001 | if commandInfo.combos then 2002 | result = result .. 'combos = {' 2003 | 2004 | for i, combo in ipairs(commandInfo.combos) do 2005 | result = result .. formatCombo(combo) 2006 | end 2007 | 2008 | result = result .. '}, ' 2009 | end 2010 | 2011 | for i, action in ipairs(commandInfo.actions) do 2012 | result = string.format('%s%s = %s, ', result, action.name, action.inputName) 2013 | end 2014 | 2015 | result = string.format('%s name = _(%q), ', result, commandInfo.name) 2016 | 2017 | if commandInfo.category then 2018 | if 'table' == type(commandInfo.category) then 2019 | result = string.format('%s category = { ', result) 2020 | 2021 | for i, categoryName in ipairs(commandInfo.category) do 2022 | result = result .. string.format('_(%q), ', categoryName) 2023 | end 2024 | 2025 | result = result .. '}, ' 2026 | else 2027 | result = string.format('%s category = _(%q), ', result, commandInfo.category) 2028 | end 2029 | end 2030 | 2031 | if commandInfo.features then 2032 | result = string.format('%s features = {', result) 2033 | 2034 | for i, feature in ipairs(commandInfo.features) do 2035 | result = result .. string.format('%q, ', feature) 2036 | end 2037 | 2038 | result = result .. '}, ' 2039 | end 2040 | 2041 | result = result .. '},\n' 2042 | 2043 | return result 2044 | end 2045 | 2046 | local function formatForceFeedback(forceFeedback) 2047 | return string.format( 2048 | [[ invertX = %s, 2049 | invertY = %s, 2050 | shake = %g, 2051 | swapAxes = %s, 2052 | trimmer = %g, 2053 | ignore = %s,]], 2054 | tostring(forceFeedback.invertX), 2055 | tostring(forceFeedback.invertY), 2056 | forceFeedback.shake, 2057 | tostring(forceFeedback.swapAxes), 2058 | forceFeedback.trimmer, 2059 | tostring(forceFeedback.ignore)) 2060 | end 2061 | 2062 | local function writeForceFeedbackToFile(file, profileName, deviceName) 2063 | local forceFeedback = getProfileForceFeedbackSettings(profileName, deviceName) 2064 | 2065 | if forceFeedback then 2066 | file:write('forceFeedback = {\n') 2067 | file:write(formatForceFeedback(forceFeedback)) 2068 | file:write('\n},\n') 2069 | end 2070 | end 2071 | 2072 | local function writeKeyCommandsToFile(file, profileName, deviceName) 2073 | local keyCommands = getProfileKeyCommandsCopy(profileName) 2074 | local keyActionHashInfos = InputUtils.getKeyCommandActionHashInfos() 2075 | local commandsInfo = getCommandsInfo(keyCommands, keyActionHashInfos, deviceName) 2076 | 2077 | file:write('keyCommands = {\n') 2078 | 2079 | for i, commandInfo in ipairs(commandsInfo) do 2080 | file:write(formatCommand(commandInfo)) 2081 | end 2082 | 2083 | file:write('},\n') 2084 | end 2085 | 2086 | local function writeAxisCommandsToFile(file, profileName, deviceName) 2087 | local axisCommands = getProfileAxisCommandsCopy(profileName) 2088 | local axisActionHashInfos = InputUtils.getAxisCommandActionHashInfos() 2089 | local commandsInfo = getCommandsInfo(axisCommands, axisActionHashInfos, deviceName) 2090 | 2091 | file:write('axisCommands = {\n') 2092 | 2093 | for i, commandInfo in ipairs(commandsInfo) do 2094 | file:write(formatCommand(commandInfo)) 2095 | end 2096 | 2097 | file:write('},\n') 2098 | end 2099 | 2100 | applyDiffToDeviceProfile_ = function(profile, diff) 2101 | if not diff then 2102 | return 2103 | end 2104 | applyDiffToCommands_ (profile.keyCommands , diff.keyDiffs , InputUtils.getKeyCommandHash) 2105 | applyDiffToCommands_ (profile.axisCommands, diff.axisDiffs, InputUtils.getAxisCommandHash) 2106 | applyDiffToForceFeedback_ (profile, diff.ffDiffs) 2107 | end 2108 | 2109 | loadProfile = function(profile) 2110 | local profileName = profile.name 2111 | local devices = InputUtils.getDevices() 2112 | 2113 | if not profile.deviceProfiles then 2114 | profile.deviceProfiles = {} 2115 | for i, deviceName in ipairs(devices) do 2116 | local deviceProfile = loadDeviceProfile_(profile, deviceName) 2117 | if deviceProfile then 2118 | --!!! NOTE USERS DIFF IS COMPLETELY REPLACE TEMPLATE DIFF !!! 2119 | local diff = loadDeviceProfileDiff_(profile, deviceName) or loadDeviceTemplateProfileDiff_(profile, deviceName) 2120 | applyDiffToDeviceProfile_(deviceProfile, diff) 2121 | setProfileDeviceProfile_(profile, deviceName, deviceProfile) 2122 | end 2123 | end 2124 | end 2125 | 2126 | local keyCommandsHashTable = {} 2127 | local axisCommandsHashTable = {} 2128 | 2129 | createProfileModifiers_(profile) 2130 | 2131 | for deviceName, deviceProfile in pairs(profile.deviceProfiles) do 2132 | addProfileKeyCommands(profileName, deviceName, deviceProfile, keyCommandsHashTable) 2133 | addProfileAxisCommands(profileName, deviceName, deviceProfile, axisCommandsHashTable) 2134 | addProfileForceFeedbackSettings(profile, deviceName, deviceProfile) 2135 | end 2136 | 2137 | profile.keyCommands = keyCommandsHashTable 2138 | profile.axisCommands = axisCommandsHashTable 2139 | 2140 | -- сразу сохраняем дефлтные команды, 2141 | -- поскольку при загрузке новых профилей может поменяться значение в таблице devices["KNEEBOARD"] 2142 | -- и хэши загруженных команд и дефолтных начнут отличаться 2143 | -- bug 0044809 2144 | 2145 | ----------------------------------------------------------------------------------------- 2146 | local loadDefaults = function (with_template_diff) 2147 | local defaultDeviceProfiles = {} 2148 | for i, deviceName in ipairs(devices) do 2149 | defaultDeviceProfiles[deviceName] = loadProfileDefaultDeviceProfile_(profile, deviceName,with_template_diff) 2150 | end 2151 | 2152 | local defaultKeyCommandsHashTable = {} 2153 | local defaultAxisCommandsHashTable = {} 2154 | 2155 | for deviceName, deviceProfile in pairs(defaultDeviceProfiles) do 2156 | validateDeviceProfile_(profileName, deviceProfile, deviceName) 2157 | addProfileKeyCommands(profileName, deviceName, deviceProfile, defaultKeyCommandsHashTable) 2158 | addProfileAxisCommands(profileName, deviceName, deviceProfile, defaultAxisCommandsHashTable) 2159 | end 2160 | 2161 | return defaultKeyCommandsHashTable,defaultAxisCommandsHashTable 2162 | end 2163 | 2164 | local dk,da = loadDefaults(true) 2165 | 2166 | profile.defaultKeyCommands = dk 2167 | profile.defaultAxisCommands = da 2168 | 2169 | --making BASE for DIFFS caclculation 2170 | local bk,ba = loadDefaults(false) 2171 | 2172 | profile.baseKeyCommands = bk 2173 | profile.baseAxisCommands = ba 2174 | 2175 | ----------------------------------------------------------------------------------------- 2176 | createProfileCategories(profile) 2177 | profile.loaded = true 2178 | end 2179 | 2180 | unloadProfile = function(profileName) 2181 | for i, profile in ipairs(profiles_) do 2182 | if profile.name == profileName then 2183 | table.remove(profiles_, i) 2184 | 2185 | local newProfile = createProfileTable_( profile.name, 2186 | profile.folder, 2187 | profile.unitName, 2188 | profile.default, 2189 | profile.visible, 2190 | profile.loadDefaultUnitProfile) 2191 | 2192 | table.insert(profiles_, newProfile) 2193 | 2194 | break 2195 | end 2196 | end 2197 | end 2198 | 2199 | createAxisFilter = function(filter) 2200 | filter = filter or {} 2201 | 2202 | local result = {} 2203 | 2204 | result.deadzone = filter.deadzone or 0 2205 | result.saturationX = filter.saturationX or 1 2206 | result.saturationY = filter.saturationY or 1 2207 | result.hardwareDetentMax = filter.hardwareDetentMax or 0 2208 | result.hardwareDetentAB = filter.hardwareDetentAB or 0 2209 | result.hardwareDetent = not (not filter.hardwareDetent) 2210 | result.slider = not (not filter.slider ) 2211 | result.invert = not (not filter.invert ) 2212 | result.curvature = U.copyTable(nil, filter.curvature or {0}) 2213 | 2214 | return result 2215 | end 2216 | 2217 | ------------------------------------------------------------------------------ 2218 | local fdef, errfdef = loadfile('./Scripts/Input/DefaultAssignments.lua') 2219 | if fdef then 2220 | setfenv(fdef, {}) 2221 | local ok, res = pcall(fdef) 2222 | if ok then 2223 | default_assignments = res 2224 | else 2225 | log.error('Cannot load default assignments '..res) 2226 | end 2227 | else 2228 | log.error('Cannot load default assignments '.. errfdef) 2229 | end 2230 | ------------------------------------------------------------------------------ 2231 | 2232 | local module_interface = { 2233 | commandCombos = commandCombos, 2234 | getProfileKeyCommands = getProfileKeyCommandsCopy, 2235 | getProfileAxisCommands = getProfileAxisCommandsCopy, 2236 | createForceFeedbackSettings = createForceFeedbackSettings, 2237 | getProfileForceFeedbackSettings = getProfileForceFeedbackSettings, 2238 | createAxisFilter = createAxisFilter, 2239 | setAxisComboFilters = setAxisComboFilters, 2240 | createModifier = createModifier, 2241 | getDeviceProfile = getDeviceProfile, 2242 | saveDeviceProfile = saveDeviceProfile, 2243 | loadDeviceProfileFromFile = loadDeviceProfileFromFile, 2244 | getUiProfileName = getUiProfileName, 2245 | unloadProfile = unloadProfile, -- подключено/отключено устройство - сбрасываем загруженный профиль 2246 | --interface functions only 2247 | setController = function(controller) 2248 | controller_ = controller 2249 | end, 2250 | initialize = function(userConfigPath, sysConfigPath) 2251 | userConfigPath_ = userConfigPath 2252 | sysConfigPath_ = sysConfigPath 2253 | sysPath_ = sysConfigPath .. 'Aircrafts/' 2254 | 2255 | if userConfigPath_ then 2256 | local f, err = loadfile(userConfigPath_ .. disabledFilename_) 2257 | if f then 2258 | local ok, res = pcall(f) 2259 | if ok then 2260 | disabledDevices_ = res.devices 2261 | for deviceName, disabled in pairs(disabledDevices_) do 2262 | Input.setDeviceDisabled(deviceName, true) 2263 | end 2264 | Input.setPnPDisabled(res.pnp) 2265 | else 2266 | printLog('Unable to load disabled devices!', res) 2267 | end 2268 | end 2269 | end 2270 | 2271 | local f, err = loadfile(lfs.writedir() .. 'Config/autoexec.cfg') 2272 | 2273 | if f then 2274 | local env = {} 2275 | 2276 | setmetatable(env, {__index = _G}) 2277 | setfenv(f, env) 2278 | 2279 | local ok, res = pcall(f) 2280 | if ok then 2281 | turnLocalizationHintsOn_ = env.input_localization_hints_on 2282 | end 2283 | end 2284 | end, 2285 | enablePrintToLog = function(enable) 2286 | printLogEnabled_ = enable 2287 | end, 2288 | getUnitMarker = function() 2289 | return 'Unit ' 2290 | end, 2291 | getProfileNames = function() 2292 | local result = {} 2293 | for i, profile in ipairs(profiles_) do 2294 | if profile.visible then 2295 | table.insert(result, profile.name) 2296 | end 2297 | end 2298 | return result 2299 | end, 2300 | getProfileNameByUnitName = function(unitName) 2301 | local unitProfile 2302 | 2303 | for i, profile in ipairs(profiles_) do 2304 | if profile.unitName == unitName then 2305 | unitProfile = profile 2306 | 2307 | break 2308 | end 2309 | end 2310 | 2311 | if not unitProfile then 2312 | unitProfile = aliases_[unitName] 2313 | end 2314 | 2315 | if unitProfile then 2316 | return unitProfile.name 2317 | end 2318 | end, 2319 | getProfileUnitName = function(profileName) 2320 | local profile = findProfile_(profileName) 2321 | if profile then 2322 | return profile.unitName 2323 | end 2324 | end, 2325 | getProfileModifiers = function(profileName) 2326 | local modifiers = {} 2327 | local profile = getLoadedProfile_(profileName) 2328 | 2329 | if profile then 2330 | U.copyTable(modifiers, profile.modifiers) 2331 | end 2332 | 2333 | return modifiers 2334 | end, 2335 | getProfileModified = function(profileName) 2336 | local profile = findProfile_(profileName) 2337 | return profile and profile.modified 2338 | end, 2339 | getProfileChanged = function(profileName) 2340 | local profile = findProfile_(profileName) 2341 | return profile and profile.loaded and profile.modified 2342 | end, 2343 | getProfileCategoryNames = function(profileName) 2344 | local result = {} 2345 | local profile = getLoadedProfile_(profileName) 2346 | 2347 | if profile then 2348 | if not profile.loaded then 2349 | loadProfile(profile) 2350 | end 2351 | 2352 | if profile.categories then 2353 | U.copyTable(result, profile.categories) 2354 | end 2355 | end 2356 | 2357 | return result 2358 | end, 2359 | getProfileKeyCommand = function(profileName, commandHash) 2360 | local profile = getLoadedProfile_(profileName) 2361 | 2362 | if profile then 2363 | local command = profile.keyCommands[commandHash] 2364 | 2365 | if command then 2366 | return U.copyTable(nil, command) 2367 | end 2368 | end 2369 | end, 2370 | getProfileAxisCommand = function(profileName, commandHash) 2371 | local profile = getLoadedProfile_(profileName) 2372 | 2373 | if profile then 2374 | local command = profile.axisCommands[commandHash] 2375 | 2376 | if command then 2377 | return U.copyTable(nil, command) 2378 | end 2379 | end 2380 | end, 2381 | createProfile = function(profileInfo) 2382 | local profile = findProfile_(profileInfo.name) 2383 | 2384 | if profile then 2385 | -- некоторые профили используются разными юнитами 2386 | -- например Spitfire 2387 | -- InputProfiles = { 2388 | -- ["SpitfireLFMkIX"] = current_mod_path .. '/Input/SpitfireLFMkIX', 2389 | -- ["SpitfireLFMkIXCW"] = current_mod_path .. '/Input/SpitfireLFMkIX', 2390 | -- }, 2391 | if profile.unitName ~= profileInfo.unitName then 2392 | aliases_[profileInfo.unitName] = profile 2393 | end 2394 | else 2395 | profile = createProfileTable_(profileInfo.name, 2396 | profileInfo.folder, 2397 | profileInfo.unitName, 2398 | profileInfo.default, 2399 | profileInfo.visible, 2400 | profileInfo.loadDefaultUnitProfile) 2401 | 2402 | table.insert(profiles_, profile) 2403 | end 2404 | end, 2405 | getProfileRawKeyCommands = function(profileName) 2406 | local result = {} 2407 | local profile = getLoadedProfile_(profileName) 2408 | 2409 | if profile then 2410 | result = U.copyTable(nil, profile.keyCommands) 2411 | end 2412 | 2413 | return result 2414 | end, 2415 | getProfileRawAxisCommands = function(profileName) 2416 | local result = {} 2417 | local profile = getLoadedProfile_(profileName) 2418 | if profile then 2419 | result = U.copyTable(nil, profile.axisCommands) 2420 | end 2421 | return result 2422 | end, 2423 | getDefaultKeyCommand = function(profileName, commandHash) 2424 | local command = findDefaultKeyCommand_(profileName, commandHash) 2425 | 2426 | if command then 2427 | return U.copyTable(nil, command) 2428 | end 2429 | end, 2430 | setDefaultKeyCommandCombos = function(profileName, commandHash, deviceName) 2431 | local defaultKeyCommand = findDefaultKeyCommand_(profileName, commandHash) 2432 | if not defaultKeyCommand then 2433 | return 2434 | end 2435 | local keyCommand = findKeyCommand_(profileName, commandHash) 2436 | if not keyCommand then 2437 | return 2438 | end 2439 | local profile = getLoadedProfile_(profileName) 2440 | local keyCommands = profile.keyCommands 2441 | setDefaultCommandCombos_(profileName, deviceName, defaultKeyCommand, keyCommand, keyCommands) 2442 | setProfileModified_(profile, true) 2443 | end, 2444 | addComboToKeyCommand = function(profileName, commandHash, deviceName, combo) 2445 | local command = findKeyCommand_(profileName, commandHash) 2446 | 2447 | if command then 2448 | local profile = getLoadedProfile_(profileName) 2449 | local commands = profile.keyCommands 2450 | 2451 | removeComboFromCommands_(profileName, deviceName, commands, combo) 2452 | addComboToCommand_(profileName, deviceName, command, combo) 2453 | setProfileModified_(profile, true) 2454 | end 2455 | end, 2456 | addComboToAxisCommand = function(profileName, commandHash, deviceName, combo) 2457 | local command = findAxisCommand_(profileName, commandHash) 2458 | 2459 | if command then 2460 | local profile = getLoadedProfile_(profileName) 2461 | local commands = profile.axisCommands 2462 | 2463 | removeComboFromCommands_(profileName, deviceName, commands, combo) 2464 | addComboToCommand_(profileName, deviceName, command, combo) 2465 | setProfileModified_(profile, true) 2466 | end 2467 | end, 2468 | removeKeyCommandCombos = function(profileName, commandHash, deviceName) 2469 | local command = findKeyCommand_(profileName, commandHash) 2470 | 2471 | removeCombosFromCommand_(profileName, command, deviceName) 2472 | setProfileModified_(getLoadedProfile_(profileName), true) 2473 | end, 2474 | removeAxisCommandCombos = function(profileName, commandHash, deviceName) 2475 | local command = findAxisCommand_(profileName, commandHash) 2476 | 2477 | removeCombosFromCommand_(profileName, command, deviceName) 2478 | setProfileModified_(getLoadedProfile_(profileName), true) 2479 | end, 2480 | getDefaultAxisCommand = function(profileName, commandHash) 2481 | local command = findDefaultAxisCommand_(profileName, commandHash) 2482 | 2483 | if command then 2484 | return U.copyTable(nil, command) 2485 | end 2486 | end, 2487 | setDefaultAxisCommandCombos = function(profileName, commandHash, deviceName) 2488 | local defaultAxisCommand = findDefaultAxisCommand_(profileName, commandHash) 2489 | if not defaultAxisCommand then 2490 | return 2491 | end 2492 | local axisCommand = findAxisCommand_(profileName, commandHash) 2493 | if not axisCommand then 2494 | return 2495 | end 2496 | local profile = getLoadedProfile_(profileName) 2497 | local axisCommands = profile.axisCommands 2498 | 2499 | setDefaultCommandCombos_(profileName, deviceName, defaultAxisCommand, axisCommand, axisCommands) 2500 | setProfileModified_(profile, true) 2501 | end, 2502 | setAxisCommandComboFilter = function(profileName, commandHash, deviceName, filters) 2503 | local command = findAxisCommand_(profileName, commandHash) 2504 | local combos = command.combos 2505 | 2506 | if combos then 2507 | setAxisComboFilters(combos[deviceName], filters) 2508 | setProfileModified_(getLoadedProfile_(profileName), true) 2509 | end 2510 | end, 2511 | setProfileForceFeedbackSettings = function(profileName, deviceName, settings) 2512 | local profile = getLoadedProfile_(profileName) 2513 | 2514 | profile.forceFeedback[deviceName] = U.copyTable(nil, settings) 2515 | setProfileModified_(profile, true) 2516 | end, 2517 | setProfileModifiers = function(profileName, modifiers) 2518 | local profile = getLoadedProfile_(profileName) 2519 | profile.modifiers = U.copyTable(nil, modifiers) 2520 | for i, profile in ipairs(profiles_) do 2521 | local profileName = profile.name 2522 | 2523 | validateCommands_(profileName, profile.keyCommands) 2524 | validateCommands_(profileName, profile.axisCommands) 2525 | end 2526 | setProfileModified_(profile, true) 2527 | end, 2528 | getDefaultProfileName = function() 2529 | for i, profile in ipairs(profiles_) do 2530 | if profile.default then 2531 | return profile.name 2532 | end 2533 | end 2534 | end, 2535 | loadDeviceProfile = function(profileName, deviceName, filename) 2536 | local deviceProfile = getDeviceProfile(profileName, deviceName) 2537 | if not deviceProfile then 2538 | return 2539 | end 2540 | local diff = loadDeviceProfileDiffFromFile_(filename) 2541 | if not diff then 2542 | return 2543 | end 2544 | 2545 | local profile = getLoadedProfile_(profileName) 2546 | 2547 | applyDiffToDeviceProfile_(deviceProfile, diff) 2548 | setProfileDeviceProfile_(profile, deviceName, deviceProfile) 2549 | 2550 | deleteDeviceCombos_(profile.keyCommands, deviceName) 2551 | deleteDeviceCombos_(profile.axisCommands, deviceName) 2552 | 2553 | addProfileKeyCommands(profileName, deviceName, deviceProfile, profile.keyCommands) 2554 | addProfileAxisCommands(profileName, deviceName, deviceProfile, profile.axisCommands) 2555 | addProfileForceFeedbackSettings(profile, deviceName, deviceProfile) 2556 | 2557 | setProfileModified_(profile, true) 2558 | end, 2559 | saveChanges = function() 2560 | local devices = InputUtils.getDevices() 2561 | 2562 | for i, profile in ipairs(profiles_) do 2563 | if profile.loaded and profile.modified then 2564 | local profileName = profile.name 2565 | local profileUserConfigPath = getProfileUserConfigPath_(profile) 2566 | 2567 | lfs.mkdir(profileUserConfigPath) 2568 | 2569 | saveProfileModifiers_(profileName, profileUserConfigPath) 2570 | 2571 | for j, deviceName in ipairs(devices) do 2572 | local deviceTypeName = InputUtils.getDeviceTypeName(deviceName) 2573 | local folder = string.format('%s%s', profileUserConfigPath, deviceTypeName) 2574 | local filename = string.format('%s/%s.diff.lua', folder, deviceName) 2575 | 2576 | lfs.mkdir(folder) 2577 | saveDeviceProfile(profileName, deviceName, filename) 2578 | end 2579 | 2580 | setProfileModified_(profile, false) 2581 | end 2582 | end 2583 | 2584 | saveDisabledDevices() 2585 | 2586 | if controller_ then 2587 | controller_.inputDataSaved() 2588 | end 2589 | end, 2590 | undoChanges = function() 2591 | for i, profile in ipairs(profiles_) do 2592 | if profile.loaded and profile.modified then 2593 | profiles_[i] = createProfileTable_( profile.name, 2594 | profile.folder, 2595 | profile.unitName, 2596 | profile.default, 2597 | profile.visible, 2598 | profile.loadDefaultUnitProfile) 2599 | end 2600 | end 2601 | 2602 | if controller_ then 2603 | controller_.inputDataRestored() 2604 | end 2605 | end, 2606 | getProfileFolder = function(profileName) 2607 | local profile = findProfile_(profileName) 2608 | if profile then 2609 | return profile.folder 2610 | end 2611 | end, 2612 | unloadProfiles = function() -- подключено/отключено устройство - сбрасываем все загруженные профили 2613 | wizard_assigments = nil 2614 | local newProfiles = {} 2615 | for i, profile in ipairs(profiles_) do 2616 | local newProfile = createProfileTable_( profile.name, 2617 | profile.folder, 2618 | profile.unitName, 2619 | profile.default, 2620 | profile.visible, 2621 | profile.loadDefaultUnitProfile) 2622 | 2623 | table.insert(newProfiles, newProfile) 2624 | end 2625 | profiles_ = newProfiles 2626 | end, 2627 | saveFullDeviceProfile = function(profileName, deviceName, filename)-- используется в Utils/Input/CreateDefaultDeviceLayout.lua 2628 | local file, err = io.open(filename, 'w') 2629 | 2630 | if file then 2631 | file:write('return {\n') 2632 | 2633 | writeForceFeedbackToFile(file, profileName, deviceName) 2634 | writeKeyCommandsToFile (file, profileName, deviceName) 2635 | writeAxisCommandsToFile (file, profileName, deviceName) 2636 | 2637 | file:write('}') 2638 | file:close() 2639 | else 2640 | log.error(string.format('Cannot save profile into file[%s]! Error %s', filename, err)) 2641 | end 2642 | end, 2643 | getKeyIsInUseInUiLayer = function(deviceName, key) -- кнопка назначена в слое для UI 2644 | return uiLayerKeyHashes_[createKeyHash_(deviceName, key)] 2645 | end, 2646 | clearProfile = function(profileName, deviceNames) 2647 | local profile = getLoadedProfile_(profileName) 2648 | if not profile then 2649 | return 2650 | end 2651 | for i, deviceName in ipairs(deviceNames) do 2652 | for commandHash, command in pairs(profile.axisCommands) do 2653 | removeCombosFromCommand_(profileName, command, deviceName) 2654 | end 2655 | 2656 | for commandHash, command in pairs(profile.keyCommands) do 2657 | removeCombosFromCommand_(profileName, command, deviceName) 2658 | end 2659 | end 2660 | setProfileModified_(profile, true) 2661 | end, 2662 | setDeviceDisabled = function(deviceName, disabled) 2663 | if disabled then 2664 | disabledDevices_[deviceName] = true 2665 | else 2666 | disabledDevices_[deviceName] = nil 2667 | end 2668 | Input.setDeviceDisabled(deviceName, disabled) 2669 | end, 2670 | getDeviceDisabled = function(deviceName) 2671 | return disabledDevices_[deviceName] or false 2672 | end, 2673 | getWizardAssignments = function() 2674 | if wizard_assigments then 2675 | return wizard_assigments 2676 | end 2677 | --load from user folder 2678 | local f, err = loadfile(lfs.writedir() .. 'Config/Input/wizard.lua') 2679 | if f then 2680 | wizard_assigments = f() 2681 | end 2682 | return wizard_assigments 2683 | end, 2684 | getDefaultKeyCommands = function(profileName) 2685 | local profile = getLoadedProfile_(profileName) 2686 | 2687 | return U.copyTable(nil, profile.defaultKeyCommands) 2688 | end, 2689 | getDefaultAxisCommands = function(profileName) 2690 | local profile = getLoadedProfile_(profileName) 2691 | return U.copyTable(nil, profile.defaultAxisCommands) 2692 | end, 2693 | } 2694 | ------------------------------------------------------------------------------ 2695 | return module_interface -------------------------------------------------------------------------------- /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/ -------------------------------------------------------------------------------- /DCS-Input-Command-Injector-Quaggles/VERSION.txt: -------------------------------------------------------------------------------- 1 | 1.0.12 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /InputCommands.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quaggles/dcs-input-command-injector/667848f05580d3d4071d562093b8d63fdb418135/InputCommands.zip -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------