-- Separate part running in render thread (can’t exchange data with main module -- directly) require 'common' ac.store('$SmallTweaks.DSEmulatorAvailable', 1) ac.broadcastSharedEvent('$SmallTweaks.ReloadScript') DualSenseEmulator.available = true DualSenseEmulator.carIndex = __carIndex ---@type {snackInstallURL: string, snackShortURL: string, snackFullURL: string} local config = JSON.parse(io.load('%s/config.json' % __dirname)) local settings = ac.storage{ LastWorkingIP = '' } local localIPs = {} local selectedIP = 1 web.get('https://acstuff.ru/j/config.json', function (err, response) local update = JSON.parse(response and response.body or '') if update.snackInstallURL and update.snackShortURL and update.snackFullURL and not table.same(update, config) then ac.log('Config updated') config = update io.save('%s/config.json' % __dirname, JSON.stringify(update)) end end) local function launchApp() local serverPort = 46557 ac.debug('Port', serverPort) table.clear(localIPs) os.runConsoleProcess({ filename = ac.getFolder(ac.FolderID.ExtRoot)..'/internal/plugins/AcTools.GamepadServer.exe', workingDirectory = __dirname, environment = { GAMEPAD_SERVER_PORT = serverPort, }, dataCallback = function (err, data) if not err and data:sub(1, 5) == 'ws://' then localIPs[#localIPs + 1] = data:trim() if localIPs[#localIPs] == settings.LastWorkingIP then selectedIP = #localIPs end end (err and ac.warn or ac.log)(data) end, inheritEnvironment = true, terminateWithScript = true, timeout = 0 }, function (err, data) ac.warn('Gamepad server shut down: err='..tostring(err)..', stderr='..(data and data.stderr or '?').. ', stdout='..(data and data.stdout or '?')) setTimeout(launchApp, 3) end) end launchApp() local qr = require('shared/utils/qr') local icons = ui.atlasIcons('res/icons.png', 2, 2, { Android = {1, 1}, Apple = {1, 2}, Expo = {2, 1} }) local function step(v) return (math.floor(v / 2) + math.smoothstep(math.min(v % 2, 1))) end local function loadingAnimation(center) local t = ui.time() ui.pathArcTo(center, 20, step(t * 1.3 + 1) * 4.5 + t * 3, step(t * 1.3) * 4.5 + 5 + t * 3, 40) ui.pathStroke(rgbm.colors.black, false, 4) end local installationGuideCur = {} local installationGuideDone = false local function installationGuideStep(title, qrFilename, nextStep) local c = ui.getCursor() ui.offsetCursorX(135) ui.text(title) ui.offsetCursorX(135) ui.drawRectFilled(ui.getCursor(), ui.getCursor() + 200, rgbm.colors.white, 6) loadingAnimation(ui.getCursor() + 100) ui.drawImageRounded(qrFilename, ui.getCursor(), ui.getCursor() + 200, 6) ui.setCursor(c + vec2(0, 92)) if ui.modernButton('Back', vec2(60, 60), ui.ButtonFlags.VerticalLayout, ui.Icons.ArrowLeft) then table.remove(installationGuideCur) end ui.setCursor(c + vec2(140 * 3 + 16 - 40, 92)) if ui.modernButton(nextStep and 'Next' or 'OK', vec2(60, 60), (nextStep and 0 or ui.ButtonFlags.Confirm) + ui.ButtonFlags.VerticalLayout, nextStep and ui.Icons.ArrowRight or ui.Icons.Confirm) then if nextStep then installationGuideCur[#installationGuideCur + 1] = nextStep else installationGuideDone = true end end ui.setCursor(c + vec2(0, 230)) end local function installationGuide() if #installationGuideCur > 0 then return installationGuideCur[#installationGuideCur]() end ui.text('What is your mobile OS?') local w = 140 if ui.modernButton('Android', vec2(w, 40), ui.ButtonFlags.None, icons.Android) then installationGuideCur[#installationGuideCur + 1] = function () installationGuideStep('Install Expo Go:', qr.encode('https://play.google.com/store/apps/details?id=host.exp.exponent'), function () installationGuideStep('Scan QR code with Expo Go:', qr.encode(config.snackInstallURL), function () installationGuideStep('Start Gamepad FX app and scan:', qr.encode(localIPs[selectedIP])) end) end) end end ui.sameLine(0, 8) if ui.modernButton('iOS', vec2(w, 40), ui.ButtonFlags.None, icons.Apple) then installationGuideCur[#installationGuideCur + 1] = function () installationGuideStep('Install Expo Go:', qr.encode('https://itunes.apple.com/app/apple-store/id982107779'), function () installationGuideStep('Scan QR code with camera:', qr.encode(config.snackInstallURL), function () installationGuideStep('Start Gamepad FX app and scan:', qr.encode(localIPs[selectedIP])) end) end) end end ui.sameLine(0, 8) if ui.modernButton('Custom', vec2(w, 40), ui.ButtonFlags.None, icons.Expo) then installationGuideCur[#installationGuideCur + 1] = function () ui.textWrapped('Get this Expo Snack written with React Native to run on your device somehow:') if ui.textHyperlink(config.snackShortURL) then os.openURL(config.snackFullURL) end if ui.itemHovered() then ui.setTooltip('Click to open URL in browser') end ui.offsetCursorY(12) end end end function script.drawUI() ui.setCursor(vec2(ui.windowWidth() - 252, 90)) ui.childWindow('qrCodePopup', vec2(224, 272), function () ui.drawRectFilled(vec2(0, 0), vec2(224, 272), rgbm(0, 0, 0, 0.4), 10) ui.drawRectFilled(vec2(12, 60), vec2(212, 260), rgbm.colors.white, 8) loadingAnimation(vec2(112, 160)) ui.drawImageRounded(qr.encode(localIPs[selectedIP]), vec2(12, 60), vec2(212, 260), 8) ui.backupCursor() ui.setCursor(vec2(12, 60)) ui.invisibleButton('next', 200) if ui.itemHovered() then if #localIPs > 1 then ui.setMouseCursor(ui.MouseCursor.Hand) ui.setTooltip('%s (%d/%d)\nClick to try the next IP address' % {localIPs[selectedIP], selectedIP, #localIPs}) if ui.itemClicked(ui.MouseButton.Left, true) then selectedIP = selectedIP + 1 if selectedIP > #localIPs then selectedIP = 1 end end else ui.setTooltip('%s (no other IPs detected)' % localIPs[selectedIP]) end end ui.restoreCursor() ui.pushFont(ui.Font.Title) ui.textAligned('Gamepad FX Mobile', 0.5, vec2(224, 38)) ui.popFont() ui.setCursor(vec2(20, 39)) ui.pushFont(ui.Font.Small) ui.text('Scan QR code with') ui.sameLine() if ui.textHyperlink('Gamepad FX app') then installationGuideCur = {} installationGuideDone = false ui.modalDialog('Gamepad FX Installation', function () installationGuide() ui.offsetCursorY(5) return ui.modernButton('Close', vec2(ui.availableSpaceX(), 40), ui.ButtonFlags.Cancel, ui.Icons.Cancel) or installationGuideDone end) end ui.popFont() end) end function script.frameBeginPreview(dt) DualSenseEmulator.touch1Pos.x = SM.touch1X DualSenseEmulator.touch1Pos.y = SM.touch1Y DualSenseEmulator.touch2Pos.x = SM.touch2X DualSenseEmulator.touch2Pos.y = SM.touch2Y DualSenseEmulator.batteryCharge = SM.batteryCharge / 100 DualSenseEmulator.batteryCharging = SM.batteryCharging end local sim = ac.getSim() local car = ac.getCar(__carIndex) local aiIni = ac.INIConfig.carData(__carIndex, 'ai.ini') local rpmUp = aiIni:get('GEARS', 'UP', 9000) local rpmDown = math.max(aiIni:get('GEARS', 'DOWN', 6000), aiIni:get('GEARS', 'UP', 9000) / 2) local prevPressed = {} local wasOffline = true function script.update(dt) local offlineNow = IsOffline() ac.setDrawUIActive(offlineNow and #localIPs > 0) if #installationGuideCur > 0 and not offlineNow then installationGuideDone = true end if wasOffline and not offlineNow then settings.LastWorkingIP = localIPs[selectedIP] or '' end wasOffline = offlineNow SM.lightBarColor[2] = math.saturateN(DualSenseEmulator.lightBarColor.r) * 255 SM.lightBarColor[1] = math.saturateN(DualSenseEmulator.lightBarColor.g) * 255 SM.lightBarColor[0] = math.saturateN(DualSenseEmulator.lightBarColor.b) * 255 SM.lightBarColor[3] = math.saturateN(DualSenseEmulator.lightBarColor.mult) * 255 SM.relativeRpm = 65535 * math.lerpInvSat(car.rpm, rpmDown, rpmUp) if sim.isPaused or sim.isInMainMenu or sim.isReplayActive or not sim.isWindowForeground then SM.vibrationLeft = 0 SM.vibrationRight = 0 end SM.headlightsActive = car.headlightsActive SM.lowBeamsActive = car.lowBeams SM.absOff = car.absModes > 0 and car.absMode == 0 SM.tcOff = car.tractionControlModes > 0 and car.tractionControlMode == 0 SM.absPresent = car.absModes > 0 SM.tcPresent = car.tractionControlModes > 0 SM.turboPresent = car.adjustableTurbo SM.clutchPresent = not car.autoClutch SM.wipersPresent = car.wiperModes > 1 SM.headlightsPresent = car.headlightsAreHeadlights SM.needsDPad = sim.needsDPad SM.gearsCount = car.gearCount SM.gear = car.gear + 1 SM.paused = sim.isPaused or sim.isInMainMenu or sim.isReplayActive or sim.isLookingAtSessionResults -- ac.debug('sim.needsDPad', sim.needsDPad) if SM.wiperDown ~= (prevPressed.wiperDown == true) then prevPressed.wiperDown = not prevPressed.wiperDown if prevPressed.wiperDown then ac.setWiperMode((car.wiperMode + car.wiperModes - 1) % car.wiperModes) end end if SM.wiperUp ~= (prevPressed.wiperUp == true) then prevPressed.wiperUp = not prevPressed.wiperUp if prevPressed.wiperUp then ac.setWiperMode((car.wiperMode + 1) % car.wiperModes) end end if SM.neutralGear ~= (prevPressed.neutralGear == true) then prevPressed.neutralGear = not prevPressed.neutralGear if prevPressed.neutralGear then ac.switchToNeutralGear() end end if SM.lowBeams ~= (prevPressed.lowBeams == true) then prevPressed.lowBeams = not prevPressed.lowBeams if prevPressed.lowBeams then ac.setHighBeams(car.lowBeams) end end local r = 0 if SM.povDir == 1 then r = r + ac.GamepadButton.DPadUp elseif SM.povDir == 2 then r = r + ac.GamepadButton.DPadRight elseif SM.povDir == 3 then r = r + ac.GamepadButton.DPadDown elseif SM.povDir == 4 then r = r + ac.GamepadButton.DPadLeft end if SM.povClick then r = r + ac.GamepadButton.A end if SM.pause then r = r + ac.GamepadButton.Start end -- ac.debug('SM.povDir', SM.povDir) -- ac.debug('SM.povClick', SM.povClick) -- ac.debug('SM.pause', SM.pause) ac.setButtonPressed(r) end