Created
July 31, 2025 10:16
-
-
Save draobrehtom/067121749a27c9acf5ad359cbcb9ea49 to your computer and use it in GitHub Desktop.
Hack minigame for FiveM in Lua
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- HackingGame Library | |
| -- A clean, reusable hacking mini-game for FiveM | |
| local HackingGame = {} | |
| -- Default configuration | |
| local defaultConfig = { | |
| maxLives = 5, | |
| columnSpeeds = { | |
| min = 150, | |
| max = 255, | |
| increment = 10 | |
| }, | |
| rouletteWords = { | |
| "AKYPUWXL", | |
| "UWPJOIMA", | |
| "WQPWSCHJ", | |
| "UGUENOBF" | |
| }, | |
| sounds = { | |
| click = "HACKING_CLICK", | |
| clickBad = "HACKING_CLICK_BAD", | |
| success = "HACKING_SUCCESS", | |
| failure = "HACKING_FAILURE" | |
| }, | |
| labels = { | |
| success = 0x18EBB648, | |
| winBrute = "WINBRUTE", | |
| loseBrute = "LOSEBRUTE" | |
| }, | |
| background = 1,-- 0-fib 1-pacificstandard 2-humanlabs 3-cityoflossantoslogo 4-blue 5 -merryweather 6- blue2, 7...-black | |
| } | |
| -- Game state | |
| local gameState = { | |
| scaleform = nil, | |
| lives = 0, | |
| program = 0, | |
| clickReturn = nil, | |
| isHacking = false, | |
| isUsingComputer = false, | |
| ipFinished = false, | |
| waitingForResult = false, | |
| config = {}, | |
| callbacks = {} | |
| } | |
| -- Initialize the hacking game | |
| function HackingGame.init(config) | |
| gameState.config = setmetatable(config or {}, {__index = defaultConfig}) | |
| gameState.lives = gameState.config.maxLives | |
| end | |
| -- Reset game state | |
| local function resetGameState() | |
| gameState.program = 0 | |
| gameState.lives = gameState.config.maxLives | |
| gameState.clickReturn = nil | |
| gameState.isHacking = false | |
| gameState.isUsingComputer = false | |
| gameState.ipFinished = false | |
| gameState.waitingForResult = false | |
| end | |
| -- Initialize scaleform | |
| local function initializeScaleform() | |
| local scaleform = RequestScaleformMovieInteractive("HACKING_PC") | |
| while not HasScaleformMovieLoaded(scaleform) do | |
| Citizen.Wait(0) | |
| end | |
| -- Load additional text | |
| local cat = 'hack' | |
| local currentSlot = 0 | |
| while HasAdditionalTextLoaded(currentSlot) and not HasThisAdditionalTextLoaded(cat, currentSlot) do | |
| Citizen.Wait(0) | |
| currentSlot = currentSlot + 1 | |
| end | |
| if not HasThisAdditionalTextLoaded(cat, currentSlot) then | |
| ClearAdditionalText(currentSlot, true) | |
| RequestAdditionalText(cat, currentSlot) | |
| while not HasThisAdditionalTextLoaded(cat, currentSlot) do | |
| Citizen.Wait(0) | |
| end | |
| end | |
| -- Set labels | |
| PushScaleformMovieFunction(scaleform, "SET_LABELS") | |
| PushScaleformMovieFunctionParameterString("Local Disk (C:)") | |
| PushScaleformMovieFunctionParameterString("Wi-Fi") | |
| PushScaleformMovieFunctionParameterString("External Device (D:)") | |
| PushScaleformMovieFunctionParameterString("Step1.exe") | |
| PushScaleformMovieFunctionParameterString("Step2.exe") | |
| PopScaleformMovieFunctionVoid() | |
| -- Set background | |
| PushScaleformMovieFunction(scaleform, "SET_BACKGROUND") | |
| PushScaleformMovieFunctionParameterInt(gameState.config.background) | |
| PopScaleformMovieFunctionVoid() | |
| -- Add programs | |
| PushScaleformMovieFunction(scaleform, "ADD_PROGRAM") | |
| PushScaleformMovieFunctionParameterFloat(1.0) | |
| PushScaleformMovieFunctionParameterFloat(4.0) | |
| PushScaleformMovieFunctionParameterString("This PC") | |
| PopScaleformMovieFunctionVoid() | |
| PushScaleformMovieFunction(scaleform, "ADD_PROGRAM") | |
| PushScaleformMovieFunctionParameterFloat(6.0) | |
| PushScaleformMovieFunctionParameterFloat(6.0) | |
| PushScaleformMovieFunctionParameterString("Shut Down") | |
| PopScaleformMovieFunctionVoid() | |
| -- Set initial lives | |
| PushScaleformMovieFunction(scaleform, "SET_LIVES") | |
| PushScaleformMovieFunctionParameterInt(gameState.lives) | |
| PushScaleformMovieFunctionParameterInt(gameState.config.maxLives) | |
| PopScaleformMovieFunctionVoid() | |
| -- Set column speeds | |
| local speedConfig = gameState.config.columnSpeeds | |
| debugPrint(json.encode(speedConfig)) | |
| for i = 0, 7 do | |
| local a, b = speedConfig.min + (i * speedConfig.increment), speedConfig.min + ((i+1) * speedConfig.increment) | |
| if a > b then a = b end | |
| debugPrint(a, b) | |
| local speed = math.random(a, b) | |
| PushScaleformMovieFunction(scaleform, "SET_COLUMN_SPEED") | |
| PushScaleformMovieFunctionParameterInt(i) | |
| PushScaleformMovieFunctionParameterInt(speed) | |
| PopScaleformMovieFunctionVoid() | |
| end | |
| return scaleform | |
| end | |
| -- Play sound helper | |
| local function playSound(soundName) | |
| local sound = gameState.config.sounds[soundName] | |
| if sound then | |
| PlaySoundFrontend(-1, sound, "", soundName == "success") | |
| end | |
| end | |
| -- Scaleform label helper | |
| local function scaleformLabel(label) | |
| BeginTextCommandScaleformString(label) | |
| EndTextCommandScaleformString() | |
| end | |
| -- Handle game result | |
| local function handleGameResult(success, resultType) | |
| gameState.waitingForResult = false | |
| if gameState.callbacks.onResult then | |
| gameState.callbacks.onResult(success, resultType, { | |
| lives = gameState.lives, | |
| ipFinished = gameState.ipFinished | |
| }) | |
| end | |
| end | |
| -- End hacking session | |
| local function endHacking(cleanup) | |
| if cleanup == nil then cleanup = true end | |
| gameState.isHacking = false | |
| gameState.isUsingComputer = false | |
| if cleanup and gameState.scaleform then | |
| SetScaleformMovieAsNoLongerNeeded(gameState.scaleform) | |
| gameState.scaleform = nil | |
| end | |
| FreezeEntityPosition(PlayerPedId(), false) | |
| if gameState.callbacks.onEnd then | |
| gameState.callbacks.onEnd() | |
| end | |
| end | |
| -- Start hacking mini-game | |
| function HackingGame.start(callbacks) | |
| if gameState.isUsingComputer then | |
| return false, "Already in use" | |
| end | |
| gameState.callbacks = callbacks or {} | |
| resetGameState() | |
| gameState.scaleform = initializeScaleform() | |
| gameState.isUsingComputer = true | |
| FreezeEntityPosition(PlayerPedId(), true) | |
| if gameState.callbacks.onStart then | |
| gameState.callbacks.onStart() | |
| end | |
| return true, "Started successfully" | |
| end | |
| -- Stop hacking mini-game | |
| function HackingGame.stop() | |
| endHacking(true) | |
| resetGameState() | |
| end | |
| -- Get current game state | |
| function HackingGame.getState() | |
| return { | |
| isActive = gameState.isUsingComputer, | |
| isHacking = gameState.isHacking, | |
| lives = gameState.lives, | |
| maxLives = gameState.config.maxLives, | |
| ipFinished = gameState.ipFinished | |
| } | |
| end | |
| -- Main game loop (call this in a thread) | |
| function HackingGame.update() | |
| if not gameState.isUsingComputer or not HasScaleformMovieLoaded(gameState.scaleform) then | |
| return | |
| end | |
| -- Draw scaleform | |
| DrawScaleformMovieFullscreen(gameState.scaleform, 255, 255, 255, 255, 0) | |
| -- Update cursor | |
| PushScaleformMovieFunction(gameState.scaleform, "SET_CURSOR") | |
| PushScaleformMovieFunctionParameterFloat(GetControlNormal(0, 239)) | |
| PushScaleformMovieFunctionParameterFloat(GetControlNormal(0, 240)) | |
| PopScaleformMovieFunctionVoid() | |
| -- Handle input | |
| if IsDisabledControlJustPressed(0, 24) and not gameState.waitingForResult then | |
| PushScaleformMovieFunction(gameState.scaleform, "SET_INPUT_EVENT_SELECT") | |
| gameState.clickReturn = PopScaleformMovieFunction() | |
| playSound("click") | |
| elseif IsDisabledControlJustPressed(0, 176) and gameState.isHacking then | |
| PushScaleformMovieFunction(gameState.scaleform, "SET_INPUT_EVENT_SELECT") | |
| gameState.clickReturn = PopScaleformMovieFunction() | |
| playSound("click") | |
| elseif IsDisabledControlJustPressed(0, 25) and not gameState.isHacking and not gameState.waitingForResult then | |
| PushScaleformMovieFunction(gameState.scaleform, "SET_INPUT_EVENT_BACK") | |
| PopScaleformMovieFunctionVoid() | |
| playSound("click") | |
| elseif gameState.isHacking then | |
| -- Arrow key inputs during hacking | |
| local arrowKeys = { | |
| [172] = 8, -- Up | |
| [173] = 9, -- Down | |
| [174] = 10, -- Left | |
| [175] = 11 -- Right | |
| } | |
| for control, event in pairs(arrowKeys) do | |
| if IsDisabledControlJustPressed(0, control) then | |
| PushScaleformMovieFunction(gameState.scaleform, "SET_INPUT_EVENT") | |
| PushScaleformMovieFunctionParameterInt(event) | |
| playSound("click") | |
| break | |
| end | |
| end | |
| end | |
| -- Process click results | |
| if gameState.clickReturn and GetScaleformMovieFunctionReturnBool(gameState.clickReturn) then | |
| gameState.program = GetScaleformMovieFunctionReturnInt(gameState.clickReturn) | |
| if gameState.program == 82 and not gameState.isHacking then | |
| -- Start IP hacking | |
| gameState.lives = gameState.config.maxLives | |
| PushScaleformMovieFunction(gameState.scaleform, "SET_LIVES") | |
| PushScaleformMovieFunctionParameterInt(gameState.lives) | |
| PushScaleformMovieFunctionParameterInt(gameState.config.maxLives) | |
| PopScaleformMovieFunctionVoid() | |
| PushScaleformMovieFunction(gameState.scaleform, "OPEN_APP") | |
| PushScaleformMovieFunctionParameterFloat(0.0) | |
| PopScaleformMovieFunctionVoid() | |
| gameState.isHacking = true | |
| elseif gameState.program == 83 and not gameState.isHacking and gameState.ipFinished then | |
| -- Start roulette hacking | |
| PushScaleformMovieFunction(gameState.scaleform, "SET_LIVES") | |
| PushScaleformMovieFunctionParameterInt(gameState.lives) | |
| PushScaleformMovieFunctionParameterInt(gameState.config.maxLives) | |
| PopScaleformMovieFunctionVoid() | |
| PushScaleformMovieFunction(gameState.scaleform, "OPEN_APP") | |
| PushScaleformMovieFunctionParameterFloat(1.0) | |
| PopScaleformMovieFunctionVoid() | |
| local randomWord = gameState.config.rouletteWords[math.random(#gameState.config.rouletteWords)] | |
| PushScaleformMovieFunction(gameState.scaleform, "SET_ROULETTE_WORD") | |
| PushScaleformMovieFunctionParameterString(randomWord) | |
| PopScaleformMovieFunctionVoid() | |
| gameState.isHacking = true | |
| elseif gameState.isHacking and gameState.program == 87 then | |
| -- Wrong input | |
| gameState.lives = gameState.lives - 1 | |
| PushScaleformMovieFunction(gameState.scaleform, "SET_LIVES") | |
| PushScaleformMovieFunctionParameterInt(gameState.lives) | |
| PushScaleformMovieFunctionParameterInt(gameState.config.maxLives) | |
| PopScaleformMovieFunctionVoid() | |
| playSound("clickBad") | |
| elseif gameState.isHacking and gameState.program == 84 then | |
| -- IP hack success | |
| playSound("success") | |
| PushScaleformMovieFunction(gameState.scaleform, "SET_IP_OUTCOME") | |
| PushScaleformMovieFunctionParameterBool(true) | |
| scaleformLabel(gameState.config.labels.success) | |
| PopScaleformMovieFunctionVoid() | |
| PushScaleformMovieFunction(gameState.scaleform, "CLOSE_APP") | |
| PopScaleformMovieFunctionVoid() | |
| gameState.isHacking = false | |
| gameState.ipFinished = true | |
| handleGameResult(true, "ip_success") | |
| elseif gameState.isHacking and gameState.program == 85 then | |
| -- IP hack failure | |
| playSound("failure") | |
| PushScaleformMovieFunction(gameState.scaleform, "CLOSE_APP") | |
| PopScaleformMovieFunctionVoid() | |
| endHacking() | |
| resetGameState() | |
| handleGameResult(false, "ip_failure") | |
| elseif gameState.isHacking and gameState.program == 86 then | |
| -- Roulette success | |
| gameState.waitingForResult = true | |
| playSound("success") | |
| PushScaleformMovieFunction(gameState.scaleform, "SET_ROULETTE_OUTCOME") | |
| PushScaleformMovieFunctionParameterBool(true) | |
| scaleformLabel(gameState.config.labels.winBrute) | |
| PopScaleformMovieFunctionVoid() | |
| Wait(3000) | |
| PushScaleformMovieFunction(gameState.scaleform, "CLOSE_APP") | |
| PopScaleformMovieFunctionVoid() | |
| endHacking() | |
| resetGameState() | |
| handleGameResult(true, "roulette_success") | |
| elseif gameState.program == 6 then | |
| -- Power off | |
| endHacking() | |
| resetGameState() | |
| handleGameResult(false, "power_off") | |
| end | |
| -- Check for game over | |
| if gameState.isHacking and gameState.lives <= 0 then | |
| gameState.waitingForResult = true | |
| playSound("failure") | |
| PushScaleformMovieFunction(gameState.scaleform, "SET_ROULETTE_OUTCOME") | |
| PushScaleformMovieFunctionParameterBool(false) | |
| scaleformLabel(gameState.config.labels.loseBrute) | |
| PopScaleformMovieFunctionVoid() | |
| -- Wait for any key pressed | |
| CreateThread(function() | |
| Wait(0) | |
| local pressed = false | |
| while not pressed do | |
| if isAnyButtonJustPressed() then | |
| pressed = true | |
| end | |
| Wait(0) | |
| end | |
| PushScaleformMovieFunction(gameState.scaleform, "CLOSE_APP") | |
| PopScaleformMovieFunctionVoid() | |
| endHacking() | |
| resetGameState() | |
| handleGameResult(false, "lives_depleted") | |
| end) | |
| end | |
| -- Update lives display during hacking | |
| if gameState.isHacking then | |
| PushScaleformMovieFunction(gameState.scaleform, "SHOW_LIVES") | |
| PushScaleformMovieFunctionParameterBool(true) | |
| PopScaleformMovieFunctionVoid() | |
| end | |
| end | |
| end | |
| --- | |
| -- Example usage of the HackingGame library | |
| -- Example: Start hacking with callbacks | |
| local function startHackingMinigameExample() | |
| -- Override default config for this instance | |
| local customConfig = { | |
| maxLives = 3, -- Harder difficulty | |
| rouletteWords = { | |
| "?*%!^@&*", | |
| -- "HARDMODE", | |
| -- "EXTREME1", | |
| -- "DIFFICULT" | |
| }, | |
| columnSpeeds = { | |
| min = 10, -- Faster speeds | |
| max = 355, | |
| increment = 15 | |
| } | |
| } | |
| HackingGame.init(customConfig) | |
| local success, message = HackingGame.start({ | |
| onStart = function() | |
| debugPrint("Hacking started!") | |
| -- Add any setup logic here | |
| end, | |
| onResult = function(success, resultType, gameData) | |
| debugPrint("Hacking result:", success, resultType) | |
| debugPrint("Lives remaining:", gameData.lives) | |
| debugPrint("IP finished:", gameData.ipFinished) | |
| if success then | |
| if resultType == "ip_success" then | |
| debugPrint("IP hack completed successfully!") | |
| -- Player can now proceed to roulette stage | |
| elseif resultType == "roulette_success" then | |
| debugPrint("Roulette hack completed successfully!") | |
| -- Hacking mini-game fully completed | |
| TriggerEvent('myScript:hackingComplete') | |
| end | |
| else | |
| if resultType == "ip_failure" then | |
| debugPrint("IP hack failed!") | |
| TriggerEvent('myScript:hackingFailed') | |
| elseif resultType == "lives_depleted" then | |
| debugPrint("All lives lost!") | |
| TriggerEvent('myScript:hackingFailed') | |
| elseif resultType == "power_off" then | |
| debugPrint("Player powered off the computer") | |
| TriggerEvent('myScript:hackingCancelled') | |
| end | |
| end | |
| end, | |
| onEnd = function() | |
| debugPrint("Hacking session ended") | |
| -- Cleanup any additional resources | |
| end | |
| }) | |
| if not success then | |
| debugPrint("Failed to start hacking:", message) | |
| end | |
| end | |
| ---- | |
| local movementInputs = { | |
| -- Mouse/stick/axis movement inputs (analog, non-buttons) | |
| 1, 2, 3, 4, 5, 6, 8, 9, 12, 13, 30, 31, 32, 33, 34, 35, | |
| 59, 60, 61, 62, 63, 64, 66, 67, 95, 98, 107, 108, 109, 110, 111, | |
| 112, 123, 124, 125, 126, 127, 128, 146, 147, 148, 149, | |
| 150, 151, 218, 219, 220, 221, 239, 240, 266, 267, 268, 269, 270, | |
| 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, | |
| 282, 283, 284, 285, 286, 287, 290, 291, 292, 293, 294, | |
| 295, 332, 333, | |
| } | |
| -- Utility: Check if any button is pressed (excluding movement inputs) | |
| function isAnyButtonJustPressed() | |
| for i = 0, 359 do -- 359 is safe upper bound, extend if needed | |
| if not table.contains(movementInputs, i) and IsControlJustPressed(0, i) then | |
| debugPrint(i) | |
| return true | |
| end | |
| end | |
| return false | |
| end | |
| -- Helper to check if a table contains a value | |
| function table.contains(table, val) | |
| for _, value in ipairs(table) do | |
| if value == val then | |
| return true | |
| end | |
| end | |
| return false | |
| end | |
| --[[ | |
| -- Start scaleform mini-game | |
| local shouldStartMinigame = false | |
| if shouldStartMinigame then | |
| HackingGame.init({ | |
| maxLives = 3, -- Harder difficulty | |
| rouletteWords = { | |
| '?*%!^@&*', | |
| -- 'HARDMODE', | |
| -- 'EXTREME1', | |
| -- 'DIFFICULT' | |
| }, | |
| columnSpeeds = { | |
| min = 10, -- Faster speeds | |
| max = 355, | |
| increment = 15 | |
| } | |
| }) | |
| HackingGame.start({ | |
| onResult = function(success, resultType, gameData) | |
| if success and resultType == 'roulette_success' then | |
| cb(true) | |
| elseif not success then | |
| cb(false) | |
| end | |
| end, | |
| }) | |
| -- Wait for minigame end | |
| while gameState.isUsingComputer and isHackingAllowed() do | |
| HackingGame.update() | |
| Wait(0) | |
| end | |
| HackingGame.stop() | |
| -- Prevent from exident shooting after exiting minigame | |
| local st = GetGameTimer() + 1000 | |
| while GetGameTimer() < st do | |
| DisableControlAction(0, 24, true) | |
| Wait(0) | |
| end | |
| end | |
| ]] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment