Skip to content

Instantly share code, notes, and snippets.

@draobrehtom
Created July 31, 2025 10:16
Show Gist options
  • Select an option

  • Save draobrehtom/067121749a27c9acf5ad359cbcb9ea49 to your computer and use it in GitHub Desktop.

Select an option

Save draobrehtom/067121749a27c9acf5ad359cbcb9ea49 to your computer and use it in GitHub Desktop.
Hack minigame for FiveM in Lua
-- 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