├── nui ├── assets │ ├── images │ │ └── wheel.png │ ├── css │ │ └── style.css │ └── js │ │ └── main.js └── index.html ├── fxmanifest.lua ├── README.md └── client.lua /nui/assets/images/wheel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PickleModifications/pickle_wheel/HEAD/nui/assets/images/wheel.png -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'cerulean' 2 | lua54 'yes' 3 | game 'gta5' 4 | 5 | name 'pickle_wheel' 6 | version '1.0.0' 7 | description 'Wheel & Pedal Support for FiveM.' 8 | author 'Pickle Mods' 9 | 10 | ui_page "nui/index.html" 11 | 12 | files { 13 | "nui/index.html", 14 | "nui/assets/**/*.*", 15 | } 16 | 17 | client_scripts { 18 | 'client.lua', 19 | } -------------------------------------------------------------------------------- /nui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
16 |
17 |
18 | 
This is a fully standalone script that allows players to use their wheel & pedals in FiveM without needing to use Scripthook or controller emulation.
11 | 12 | With this resource, you will be able to do the following: 13 | 14 | - Wheel & Pedal Support. 15 | - Wheel, Pedal, and Controls Display. 16 | - Works with any wheel / pedal combo, and allows you to choose your device. 17 | - Change Input Indexes. 18 | - Modify Wheel Deadzone. 19 | - Bind both Controller and Command Actions. 20 | - Locally saved settings. 21 | - Completely standalone, just plug-and-play! 22 | 23 | ## What do I need? 24 | 25 | Nothing at all! 26 | Just add the resource to the server, make sure the resource is started as "pickle_wheel", and have fun! 27 | 28 | ## Basic Usage 29 | 30 | To pull up the wheel panel, use "/wheel" in the chat. 31 | You'll need to select your device from the dropdown, and save the settings after you've tuned it to your liking. 32 | 33 | ## Need Support? 34 | 35 | Click here! 36 | 37 | ## Credits: 38 | 39 | [Spendibus: Wheel & Pedal Gamepad API Snippet](http://spenibus.net/b/p/F/PC-steering-wheel-viewer-prototype-in-html-javascript) 40 | 41 | ## Ready to download? 42 | 43 | Enjoy! 44 | 45 | https://github.com/PickleModifications/pickle_wheel/releases 46 | -------------------------------------------------------------------------------- /client.lua: -------------------------------------------------------------------------------- 1 | local Controls = {} 2 | local DeviceSettings = {} 3 | local PressedBinds = {} 4 | 5 | local lastUpdate = 0 6 | 7 | RegisterNUICallback("updateInput", function(data, cb) 8 | for k,v in pairs(data) do 9 | local value = v 10 | if value > 0.9999 then 11 | value = 0.9999 12 | elseif value < -0.9999 then 13 | value = -0.9999 14 | end 15 | if k == "wheelAxis" then 16 | if value > 0 then 17 | value = value + DeviceSettings.wheelDeadzone 18 | elseif value < 0 then 19 | value = value - DeviceSettings.wheelDeadzone 20 | end 21 | end 22 | Controls[k] = value 23 | end 24 | cb(true) 25 | end) 26 | 27 | RegisterNUICallback("buttonUpdate", function(data, cb) 28 | ButtonInteract(data.index, data.pressed) 29 | cb(true) 30 | end) 31 | 32 | RegisterNUICallback("updateSettings", function(data, cb) 33 | UpdateSettings(data) 34 | cb(true) 35 | end) 36 | 37 | RegisterNUICallback("close", function(data, cb) 38 | SetNuiFocus(false, false) 39 | cb(true) 40 | end) 41 | 42 | function ButtonInteract(index, pressed) 43 | local bind = DeviceSettings.binds[index .. ""] 44 | if bind then 45 | if bind.type == "control" and bind.values[2] ~= nil then 46 | if pressed then 47 | local startTime = GetGameTimer() 48 | PressedBinds[index] = true 49 | CreateThread(function() 50 | local key = tonumber(bind.values[2]) 51 | while lastUpdate < startTime and PressedBinds[index] do 52 | SetControlNormal(0, key, 1.0) 53 | SetControlNormal(1, key, 1.0) 54 | SetControlNormal(2, key, 1.0) 55 | Wait(0) 56 | end 57 | end) 58 | else 59 | PressedBinds[index] = false 60 | end 61 | elseif bind.type == "command" then 62 | if pressed and bind.values[1] ~= nil then 63 | PressedBinds[index] = true 64 | ExecuteCommand(bind.values[1]) 65 | elseif not pressed and bind.values[2] ~= nil then 66 | PressedBinds[index] = false 67 | ExecuteCommand(bind.values[2]) 68 | end 69 | end 70 | end 71 | end 72 | 73 | function UpdateSettings(data) 74 | lastUpdate = GetGameTimer() 75 | DeviceSettings = data 76 | SetResourceKvp("picklewheel", json.encode(DeviceSettings)) 77 | end 78 | 79 | RegisterCommand("wheel", function() 80 | SetNuiFocus(true, true) 81 | SendNUIMessage({ 82 | type = "open" 83 | }) 84 | end) 85 | 86 | CreateThread(function() 87 | Wait(1000) 88 | DeviceSettings = json.decode(GetResourceKvpString("picklewheel")) 89 | SendNUIMessage({ 90 | type = "updateSettings", 91 | data = DeviceSettings 92 | }) 93 | while true do 94 | SetControlNormal(0, 59, Controls.wheelAxis or 0.0) 95 | SetControlNormal(0, 71, Controls.throttleAxis or 0.0) 96 | SetControlNormal(0, 72, Controls.brakeAxis or 0.0) 97 | -- SetControlNormal(0, 59, Controls.axisValueClutch or 0.0) 98 | Wait(0) 99 | end 100 | end) -------------------------------------------------------------------------------- /nui/assets/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color:transparent; 3 | margin:0; 4 | padding:0; 5 | color: white; 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | #container { 10 | display: flex; 11 | opacity: 0; 12 | flex-direction: column; 13 | position: absolute; 14 | top: 50%; 15 | left: 50%; 16 | transform: translate(-50%, -50%); 17 | background-color: rgba(0, 0, 0, 0.5); 18 | width: 50vw; 19 | height: 30vw; 20 | } 21 | 22 | #container > div { 23 | display: flex; 24 | flex-direction: column; 25 | width: 100%; 26 | } 27 | 28 | .wheel-display { 29 | height: 5vw; 30 | } 31 | 32 | #wheel { 33 | display:inline-block; 34 | vertical-align:bottom; 35 | height: 100%; 36 | transform-origin:center center; 37 | margin-right:1vw; 38 | } 39 | 40 | .pedal { 41 | display:inline-block; 42 | vertical-align:bottom; 43 | width:1vw; 44 | height: 100%; 45 | margin-left: 0.25vw; 46 | margin-right:0.25vw; 47 | background-color: rgba(0, 0, 0, 0.5); 48 | } 49 | .pedal > span { 50 | display:inline-block; 51 | width:100%; 52 | height:100%; 53 | background-color:rgba(0, 0, 0, 0.5); 54 | vertical-align:top; 55 | } 56 | 57 | .top { 58 | align-items: center; 59 | padding-bottom: 1vw; 60 | } 61 | 62 | .middle { 63 | overflow-y: scroll; 64 | } 65 | 66 | ::-webkit-scrollbar { 67 | width: 0.5vw; 68 | } 69 | 70 | /* Track */ 71 | ::-webkit-scrollbar-track { 72 | background: transparent; 73 | } 74 | 75 | /* Handle */ 76 | ::-webkit-scrollbar-thumb { 77 | background: #484848; 78 | } 79 | 80 | /* Handle on hover */ 81 | ::-webkit-scrollbar-thumb:hover { 82 | background: #2b2b2b; 83 | } 84 | 85 | .middle > h3, .middle > span { 86 | align-self: center; 87 | width: 50%; 88 | } 89 | 90 | .middle > span { 91 | margin-left: 4vw; 92 | margin-bottom: 1.15vw; 93 | } 94 | 95 | .middle > div { 96 | margin-left: 4vw; 97 | } 98 | 99 | .option { 100 | display: flex; 101 | align-self: center; 102 | width: 50%; 103 | margin-top: 0.1vw; 104 | margin-bottom: 0.1vw; 105 | } 106 | 107 | .option > div:first-child { 108 | display: flex; 109 | } 110 | 111 | .option > div > b { 112 | align-self: center; 113 | } 114 | 115 | .option > div:last-child { 116 | margin-left: auto; 117 | } 118 | 119 | .bottom { 120 | align-items: center; 121 | margin-top: auto; 122 | padding-top: 1vw; 123 | margin-bottom: 1vw; 124 | } 125 | 126 | #throttle { 127 | background-color:#0A0; 128 | } 129 | #brake { 130 | background-color:#C00; 131 | } 132 | 133 | h1 { 134 | font-size: 1.25vw; 135 | } 136 | 137 | h3 { 138 | font-size: 1.0vw; 139 | } 140 | 141 | input { 142 | font-size: 0.5vw; 143 | width: 4.5vw; 144 | } 145 | 146 | select { 147 | width: 5vw; 148 | } 149 | 150 | .pressed > div > b { 151 | color: red; 152 | } 153 | 154 | .button { 155 | display: flex; 156 | justify-content: center; 157 | align-items: center; 158 | width: 25vw; 159 | height: 2vw; 160 | font-size: 1.15vw; 161 | } 162 | 163 | .button.success { 164 | cursor: pointer; 165 | background-color: rgb(0, 100, 0); 166 | transition: 0.25s; 167 | } 168 | 169 | .button.success:hover { 170 | background-color: rgb(0, 200, 0); 171 | } 172 | 173 | #close { 174 | display: flex; 175 | justify-content: center; 176 | align-items: center; 177 | cursor: pointer; 178 | position: absolute; 179 | top: 0; 180 | right: 0; 181 | font-size: 1vw; 182 | width: 1.5vw; 183 | height: 1.5vw; 184 | background-color: rgb(100, 0, 0); 185 | color: white; 186 | transition: 0.25s; 187 | } 188 | 189 | #close:hover { 190 | background-color: red; 191 | } -------------------------------------------------------------------------------- /nui/assets/js/main.js: -------------------------------------------------------------------------------- 1 | let device; 2 | let dataDisplay; 3 | 4 | let elemWheel; 5 | let elemThrottle; 6 | let elemBrake; 7 | 8 | let loopRunning = false; 9 | let deviceFound = false; 10 | let deviceIndex; 11 | let deviceSettings; 12 | 13 | function updateSettings(data, forceUpdate) { 14 | var data = data || { 15 | inputs: {}, 16 | binds: {} 17 | } 18 | if (!forceUpdate) { 19 | deviceSettings = { 20 | deviceIndex: data.inputs[0] || "null", 21 | wheelDeadzone: data.inputs[1] || 0.25, 22 | wheelRadius: data.inputs[2] || 900, 23 | wheelIndex: data.inputs[3] || 0, 24 | throttleIndex: data.inputs[4] || 1, 25 | brakeIndex: data.inputs[5] || 2, 26 | binds: data.binds 27 | } 28 | fetch('https://pickle_wheel/updateSettings', { 29 | method: 'post', 30 | headers: { 31 | 'Accept': 'application/json', 32 | 'Content-Type': 'application/json' 33 | }, 34 | body: JSON.stringify(deviceSettings) 35 | }) 36 | .then(response => { }) 37 | .catch(error => { }); 38 | } 39 | else { 40 | deviceSettings = data; 41 | if (deviceIndex != deviceSettings.deviceIndex) { 42 | deviceIndex = deviceSettings.deviceIndex 43 | if (deviceIndex != "null") { 44 | initApp(deviceIndex) 45 | } 46 | } 47 | } 48 | } 49 | 50 | function safeValue(value) { 51 | if (value) { 52 | return value 53 | } 54 | else { 55 | return null 56 | } 57 | } 58 | 59 | function bindHtml(type, index, val1, val2) { 60 | let bind; 61 | if (type == "control") { 62 | bind = ` 63 |