diff --git a/README.md b/README.md new file mode 100644 index 0000000..9ee7a17 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +
+

More Information & Scripts can be found here!

+ +## Preview + +https://www.youtube.com/watch?v=flCTCk4hW7g + +## What is this? + +This is a one-size fits all solution for jail systems, with high quality features that have yet to be seen in a free prison script. +It also has a lot of innovations, such as multiple prison support, which lets you send people to different prisons on the map (requires setup). + +## Features + + + +## Requirements + +* ESX (1.1+) / QBCore, or code your own bridge to work with your framework. (the bridge is fully accessible!) +* Ox Lib (Required, Works anywhere). + +## Optional XP Support + +Use my free XP system below, or port to another one inside of the bridge! + +https://forum.cfx.re/t/free-pickles-xp-system-standalone-works-with-esx-qb-multicharacter-identifiers/5088064 + +## Optional Metadata Support + +

This is not required to use the script, however enables you to generate items with metadata using the .createItem function in the rewards.

+ + + +## Installation + +

ensure pickle_prisons after ox_lib & pickle_xp (if using it).

+

Go to the "_INSTALL" folder.

+

Add the items.

+

Run the SQL file.

+

Go to the config, and set Config.UseTarget to true/false.

+

Restart the server.

+ +## Documentation + +https://docs.picklemods.com/resources/prisons + +## Need Support? + +Click here! diff --git a/_INSTALL/Items/esx_limit.sql b/_INSTALL/Items/esx_limit.sql new file mode 100644 index 0000000..1a269ea --- /dev/null +++ b/_INSTALL/Items/esx_limit.sql @@ -0,0 +1,6 @@ +INSERT INTO `items` (`name`, `label`, `limit`) VALUES + ('wood', 'Wood', 100), + ('metal', 'Metal', 100), + ('rope', 'Rope', 100), + ('shovel', 'Shovel', 100) +; \ No newline at end of file diff --git a/_INSTALL/Items/esx_weight.sql b/_INSTALL/Items/esx_weight.sql new file mode 100644 index 0000000..5f23a51 --- /dev/null +++ b/_INSTALL/Items/esx_weight.sql @@ -0,0 +1,6 @@ +INSERT INTO `items` (`name`, `label`, `weight`) VALUES + ('wood', 'Wood', 1), + ('metal', 'Metal', 1), + ('rope', 'Rope', 1), + ('shovel', 'Shovel', 1) +; \ No newline at end of file diff --git a/_INSTALL/Items/ox_inventory.lua b/_INSTALL/Items/ox_inventory.lua new file mode 100644 index 0000000..98738bf --- /dev/null +++ b/_INSTALL/Items/ox_inventory.lua @@ -0,0 +1,31 @@ +['wood'] = { + label = 'Wood', + weight = 1, + stack = true, + close = true, + description = nil +}, + +['metal'] = { + label = 'Metal', + weight = 1, + stack = true, + close = true, + description = nil +}, + +['rope'] = { + label = 'Rope', + weight = 1, + stack = true, + close = true, + description = nil +}, + +['shovel'] = { + label = 'Shovel', + weight = 1, + stack = true, + close = true, + description = nil +}, \ No newline at end of file diff --git a/_INSTALL/Items/qbcore.lua b/_INSTALL/Items/qbcore.lua new file mode 100644 index 0000000..c023a7d --- /dev/null +++ b/_INSTALL/Items/qbcore.lua @@ -0,0 +1,4 @@ +["wood"] = {["name"] = "wood", ["label"] = "Wood", ["weight"] = 1, ["type"] = "item", ["image"] = "wood.png", ["unique"] = false, ["useable"] = true, ["shouldClose"] = true, ["combinable"] = nil, ["description"] = ""}, +["metal"] = {["name"] = "metal", ["label"] = "Metal", ["weight"] = 1, ["type"] = "item", ["image"] = "metal.png", ["unique"] = false, ["useable"] = true, ["shouldClose"] = true, ["combinable"] = nil, ["description"] = ""}, +["rope"] = {["name"] = "rope", ["label"] = "Rope", ["weight"] = 1, ["type"] = "item", ["image"] = "rope.png", ["unique"] = false, ["useable"] = true, ["shouldClose"] = true, ["combinable"] = nil, ["description"] = ""}, +["shovel"] = {["name"] = "shovel", ["label"] = "Shovel", ["weight"] = 1, ["type"] = "item", ["image"] = "shovel.png", ["unique"] = false, ["useable"] = true, ["shouldClose"] = true, ["combinable"] = nil, ["description"] = ""}, \ No newline at end of file diff --git a/_INSTALL/SQL/install.sql b/_INSTALL/SQL/install.sql new file mode 100644 index 0000000..8b25097 --- /dev/null +++ b/_INSTALL/SQL/install.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS `pickle_prisons` ( + `identifier` varchar(46) NOT NULL, + `prison` varchar(50) DEFAULT 'default', + `time` int(11) NOT NULL DEFAULT 0, + `inventory` longtext DEFAULT '[]', + `sentence_date` int(11) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; \ No newline at end of file diff --git a/bridge/custom/client.lua b/bridge/custom/client.lua new file mode 100644 index 0000000..f777178 --- /dev/null +++ b/bridge/custom/client.lua @@ -0,0 +1,4 @@ +if GetResourceState('es_extended') == 'started' then return end +if GetResourceState('qb-core') == 'started' then return end + +print("You are not using a supported framework, it will be required to make edits to the bridge files.") \ No newline at end of file diff --git a/bridge/custom/server.lua b/bridge/custom/server.lua new file mode 100644 index 0000000..f777178 --- /dev/null +++ b/bridge/custom/server.lua @@ -0,0 +1,4 @@ +if GetResourceState('es_extended') == 'started' then return end +if GetResourceState('qb-core') == 'started' then return end + +print("You are not using a supported framework, it will be required to make edits to the bridge files.") \ No newline at end of file diff --git a/bridge/esx/client.lua b/bridge/esx/client.lua new file mode 100644 index 0000000..6c12d15 --- /dev/null +++ b/bridge/esx/client.lua @@ -0,0 +1,85 @@ +if GetResourceState('es_extended') ~= 'started' then return end + +ESX = exports.es_extended:getSharedObject() + +function ShowNotification(text) + ESX.ShowNotification(text) +end + +function ServerCallback(name, cb, ...) + ESX.TriggerServerCallback(name, cb, ...) +end + +function GetPlayersInArea(coords, radius) + local coords = coords or GetEntityCoords(PlayerPedId()) + local radius = radius or 3.0 + local list = ESX.Game.GetPlayersInArea(coords, radius) + local players = {} + for _, player in pairs(list) do + if player ~= PlayerId() then + players[#players + 1] = player + end + end + return players +end + +RegisterNetEvent(GetCurrentResourceName()..":showNotification", function(text) + ShowNotification(text) +end) + +RegisterNetEvent('esx:playerLoaded') +AddEventHandler('esx:playerLoaded',function(xPlayer, isNew, skin) + TriggerServerEvent("pickle_prisons:initializePlayer") +end) + +local alreadySpawned = false + +RegisterNetEvent('esx:onPlayerDeath', function() + CheckBreakout = false +end) + +RegisterNetEvent('esx:onPlayerSpawn', function() + if not alreadySpawned then -- Prevents TP to hospital on-load. + alreadySpawned = true + return + end + TeleportHospital() + CheckBreakout = true +end) + +function ToggleOutfit(inPrison) + if inPrison then + local prison = Config.Prisons[Prison.index] + local outfits = prison.outfit or Config.Default.outfit + ESX.TriggerServerCallback('esx_skin:getPlayerSkin', function(skin, jobSkin) + local gender = skin.sex + local outfit = gender == 1 and outfits.female or outfits.male + if not outfit then return end + TriggerEvent('skinchanger:loadClothes', skin, outfit) + end) + else + ESX.TriggerServerCallback('esx_skin:getPlayerSkin', function(skin, jobSkin) + TriggerEvent('skinchanger:loadSkin', skin) + TriggerEvent('esx:restoreLoadout') + end) + end +end + +-- Inventory Fallback + +CreateThread(function() + Wait(100) + + if InitializeInventory then return InitializeInventory() end -- Already loaded through inventory folder. + + Inventory = {} + + Inventory.Items = {} + + Inventory.Ready = false + + RegisterNetEvent("pickle_prisons:setupInventory", function(data) + Inventory.Items = data.items + Inventory.Ready = true + end) +end) \ No newline at end of file diff --git a/bridge/esx/server.lua b/bridge/esx/server.lua new file mode 100644 index 0000000..75b1e2b --- /dev/null +++ b/bridge/esx/server.lua @@ -0,0 +1,138 @@ +if GetResourceState('es_extended') ~= 'started' then return end + +ESX = exports.es_extended:getSharedObject() + +function RegisterCallback(name, cb) + ESX.RegisterServerCallback(name, cb) +end + +function RegisterUsableItem(...) + ESX.RegisterUsableItem(...) +end + +function ShowNotification(target, text) + TriggerClientEvent(GetCurrentResourceName()..":showNotification", target, text) +end + +function GetIdentifier(source) + local xPlayer = ESX.GetPlayerFromId(source) + return xPlayer.identifier +end + +function SetPlayerMetadata(source, key, data) + -- No player metadata in ESX. +end + +function AddMoney(source, count) + local xPlayer = ESX.GetPlayerFromId(source) + xPlayer.addMoney(count) +end + +function RemoveMoney(source, count) + local xPlayer = ESX.GetPlayerFromId(source) + xPlayer.removeMoney(count) +end + +function GetMoney(source) + local xPlayer = ESX.GetPlayerFromId(source) + return xPlayer.getMoney() +end + +function CheckPermission(source, permission) + local xPlayer = ESX.GetPlayerFromId(source) + local name = xPlayer.job.name + local rank = xPlayer.job.grade + local group = xPlayer.getGroup() + if permission.jobs[name] and permission.jobs[name] <= rank then + return true + end + for i=1, #permission.groups do + if group == permission.groups[i] then + return true + end + end +end + +-- Inventory Fallback + +CreateThread(function() + Wait(100) + + if InitializeInventory then return InitializeInventory() end -- Already loaded through inventory folder. + + Inventory = {} + + Inventory.Items = {} + + Inventory.Ready = false + + Inventory.CanCarryItem = function(source, name, count) + local xPlayer = ESX.GetPlayerFromId(source) + if Config.InventoryLimit then + local item = xPlayer.getInventoryItem(name) + return (item.limit >= item.count + count) + else + return xPlayer.canCarryItem(name, count) + end + end + + Inventory.GetInventory = function(source) + local xPlayer = ESX.GetPlayerFromId(source) + local items = {} + local data = xPlayer.getInventory() + for i=1, #data do + local item = data[i] + items[#items + 1] = { + name = item.name, + label = item.label, + count = item.count, + weight = item.weight + } + end + return items + end + + Inventory.AddItem = function(source, name, count, metadata) -- Metadata is not required. + local xPlayer = ESX.GetPlayerFromId(source) + xPlayer.addInventoryItem(name, count) + end + + Inventory.RemoveItem = function(source, name, count) + local xPlayer = ESX.GetPlayerFromId(source) + xPlayer.removeInventoryItem(name, count) + end + + Inventory.AddWeapon = function(source, name, count, metadata) -- Metadata is not required. + local xPlayer = ESX.GetPlayerFromId(source) + xPlayer.addWeapon(name, 0) + end + + Inventory.RemoveWeapon = function(source, name, count) + local xPlayer = ESX.GetPlayerFromId(source) + xPlayer.removeWeapon(name, 0) + end + + Inventory.GetItemCount = function(source, name) + local xPlayer = ESX.GetPlayerFromId(source) + local item = xPlayer.getInventoryItem(name) + return item and item.count or 0 + end + + Inventory.HasWeapon = function(source, name, count) + local xPlayer = ESX.GetPlayerFromId(source) + return xPlayer.hasWeapon(name) + end + + RegisterCallback("pickle_prisons:getInventory", function(source, cb) + cb(Inventory.GetInventory(source)) + end) + + MySQL.ready(function() + MySQL.Async.fetchAll("SELECT * FROM items;", {}, function(results) + for i=1, #results do + Inventory.Items[results[i].name] = {label = results[i].label} + end + Inventory.Ready = true + end) + end) +end) \ No newline at end of file diff --git a/bridge/inventory/ox_inventory/client.lua b/bridge/inventory/ox_inventory/client.lua new file mode 100644 index 0000000..a292502 --- /dev/null +++ b/bridge/inventory/ox_inventory/client.lua @@ -0,0 +1,15 @@ +if GetResourceState('ox_inventory') ~= 'started' then return end + +Inventory = {} + +Inventory.Items = {} + +Inventory.Ready = false + +RegisterNetEvent("pickle_prisons:setupInventory", function(data) + Inventory.Items = data.items + Inventory.Ready = true +end) + +function InitializeInventory() +end \ No newline at end of file diff --git a/bridge/inventory/ox_inventory/server.lua b/bridge/inventory/ox_inventory/server.lua new file mode 100644 index 0000000..1871c53 --- /dev/null +++ b/bridge/inventory/ox_inventory/server.lua @@ -0,0 +1,62 @@ +if GetResourceState('ox_inventory') ~= 'started' then return end + +Inventory = {} + +Inventory.Items = {} + +Inventory.Ready = false + +Inventory.CanCarryItem = function(source, name, count) + return exports.ox_inventory:CanCarryItem(source, name, count) +end + +Inventory.GetInventory = function(source) + local items = {} + local data = exports.ox_inventory:GetInventoryItems(source) + for slot, item in pairs(data) do + items[#items + 1] = { + name = item.name, + label = item.label, + count = item.count, + weight = item.weight, + metadata = item.metadata + } + end + return items +end + +Inventory.AddItem = function(source, name, count, metadata) -- Metadata is not required. + exports.ox_inventory:AddItem(source, name, count, metadata) +end + +Inventory.RemoveItem = function(source, name, count) + exports.ox_inventory:RemoveItem(source, name, count) +end + +Inventory.AddWeapon = function(source, name, count, metadata) -- Metadata is not required. + exports.ox_inventory:AddItem(source, name, count, metadata) +end + +Inventory.RemoveWeapon = function(source, name, count) + exports.ox_inventory:RemoveItem(source, name, count) +end + +Inventory.GetItemCount = function(source, name) + return exports.ox_inventory:Search(source, "count", name) or 0 +end + +Inventory.HasWeapon = function(source, name, count) + return (Inventory.GetItemCount(source, name) > 0) +end + +function InitializeInventory() + RegisterCallback("pickle_prisons:getInventory", function(source, cb) + cb(Inventory.GetInventory(source)) + end) + + for item, data in pairs(exports.ox_inventory:Items()) do + Inventory.Items[item] = {label = data.label} + end + + Inventory.Ready = true +end \ No newline at end of file diff --git a/bridge/qb/client.lua b/bridge/qb/client.lua new file mode 100644 index 0000000..22504be --- /dev/null +++ b/bridge/qb/client.lua @@ -0,0 +1,74 @@ +if GetResourceState('qb-core') ~= 'started' then return end + +QBCore = exports['qb-core']:GetCoreObject() + +function ServerCallback(name, cb, ...) + QBCore.Functions.TriggerCallback(name, cb, ...) +end + +function ShowNotification(text) + QBCore.Functions.Notify(text) +end + +function GetPlayersInArea(coords, radius) + local coords = coords or GetEntityCoords(PlayerPedId()) + local radius = radius or 3.0 + local list = ESX.Game.GetPlayersFromCoords(coords, radius) + local players = {} + for _, player in pairs(list) do + if player ~= PlayerId() then + players[#players + 1] = player + end + end + return players +end + +RegisterNetEvent(GetCurrentResourceName()..":showNotification", function(text) + ShowNotification(text) +end) + +RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() + TriggerServerEvent("pickle_prisons:initializePlayer") +end) + +RegisterNetEvent('pickle_prisons:SetDeathStatus', function(status) + if status then + CheckBreakout = false + else + TeleportHospital() + CheckBreakout = true + end +end) + +function ToggleOutfit(inPrison) + if inPrison then + local prison = Config.Prisons[Prison.index] + local outfits = prison.outfit or Config.Default.outfit + local gender = QBCore.Functions.GetPlayerData().charinfo.gender + local outfit = gender == 1 and outfits.female or outfits.male + if not outfit then return end + TriggerEvent('qb-clothing:client:loadOutfit', {outfitData = outfit}) + else + TriggerServerEvent("qb-clothes:loadPlayerSkin") + end +end + + +-- Inventory Fallback + +CreateThread(function() + Wait(100) + + if InitializeInventory then return InitializeInventory() end -- Already loaded through inventory folder. + + Inventory = {} + + Inventory.Items = {} + + Inventory.Ready = false + + RegisterNetEvent("pickle_prisons:setupInventory", function(data) + Inventory.Items = data.items + Inventory.Ready = true + end) +end) \ No newline at end of file diff --git a/bridge/qb/server.lua b/bridge/qb/server.lua new file mode 100644 index 0000000..b246bf1 --- /dev/null +++ b/bridge/qb/server.lua @@ -0,0 +1,133 @@ +if GetResourceState('qb-core') ~= 'started' then return end + +QBCore = exports['qb-core']:GetCoreObject() + +function RegisterCallback(name, cb) + QBCore.Functions.CreateCallback(name, cb) +end + +function RegisterUsableItem(...) + QBCore.Functions.CreateUseableItem(...) +end + +function ShowNotification(target, text) + TriggerClientEvent(GetCurrentResourceName()..":showNotification", target, text) +end + +function GetIdentifier(source) + local xPlayer = QBCore.Functions.GetPlayer(source).PlayerData + return xPlayer.citizenid +end + +function SetPlayerMetadata(source, key, data) + QBCore.Functions.GetPlayer(source).Functions.SetMetaData(key, data) +end + +RegisterNetEvent("hospital:server:SetDeathStatus", function(status) + local source = source + TriggerClientEvent("pickle_prisons:SetDeathStatus", source, status) +end) + +function AddMoney(source, count) + local xPlayer = QBCore.Functions.GetPlayer(source) + xPlayer.Functions.AddMoney(count) +end + +function RemoveMoney(source, count) + local xPlayer = QBCore.Functions.GetPlayer(source) + xPlayer.Functions.RemoveMoney(count) +end + +function GetMoney(source) + local xPlayer = QBCore.Functions.GetPlayer(source) + return xPlayer.PlayerData.money.cash +end + +function CheckPermission(source, permission) + local xPlayer = QBCore.Functions.GetPlayer(source).PlayerData + local name = xPlayer.job.name + local rank = xPlayer.job.grade.level + if permission.jobs[name] and permission.jobs[name] <= rank then + return true + end + for i=1, #permission.groups do + if QBCore.Functions.HasPermission(source, permission.groups[i]) then + return true + end + end +end + + +-- Inventory Fallback + +CreateThread(function() + Wait(100) + + if InitializeInventory then return InitializeInventory() end -- Already loaded through inventory folder. + + Inventory = {} + + Inventory.Items = {} + + Inventory.Ready = false + + Inventory.CanCarryItem = function(source, name, count) + local xPlayer = QBCore.Functions.GetPlayer(source) + local weight = QBCore.Player.GetTotalWeight(xPlayer.PlayerData.items) + local item = QBCore.Shared.Items[name:lower()] + return ((weight + (item.weight * count)) <= QBCore.Config.Player.MaxWeight) + end + + Inventory.GetInventory = function(source) + local xPlayer = QBCore.Functions.GetPlayer(source) + local items = {} + local data = xPlayer.PlayerData.items + for slot, item in pairs(data) do + items[#items + 1] = { + name = item.name, + label = item.label, + count = item.amount, + weight = item.weight, + metadata = item.info + } + end + return items + end + + Inventory.AddItem = function(source, name, count, metadata) -- Metadata is not required. + local xPlayer = QBCore.Functions.GetPlayer(source) + xPlayer.Functions.AddItem(name, count, nil, metadata) + end + + Inventory.RemoveItem = function(source, name, count) + local xPlayer = QBCore.Functions.GetPlayer(source) + xPlayer.Functions.RemoveItem(name, count) + end + + Inventory.AddWeapon = function(source, name, count, metadata) -- Metadata is not required. + Inventory.AddItem(source, name, count, metadata) + end + + Inventory.RemoveWeapon = function(source, name, count) + Inventory.RemoveItem(source, name, count, metadata) + end + + Inventory.GetItemCount = function(source, name) + local xPlayer = QBCore.Functions.GetPlayer(source) + local item = xPlayer.Functions.GetItemByName(name) + return item and item.amount or 0 + end + + Inventory.HasWeapon = function(source, name, count) + return (Inventory.GetItemCount(source, name) > 0) + end + + RegisterCallback("pickle_prisons:getInventory", function(source, cb) + cb(Inventory.GetInventory(source)) + end) + + for item, data in pairs(QBCore.Shared.Items) do + Inventory.Items[item] = {label = data.label} + end + Inventory.Ready = true +end) \ No newline at end of file diff --git a/bridge/target/ox_target/client.lua b/bridge/target/ox_target/client.lua new file mode 100644 index 0000000..2c57b82 --- /dev/null +++ b/bridge/target/ox_target/client.lua @@ -0,0 +1,26 @@ +if GetResourceState('ox_target') ~= 'started' or not Config.UseTarget then return end + +function AddModel(models, options) + local optionsNames = {} + for i=1, #options do + optionsNames[i] = options[i].name + end + RemoveModel(models, optionsNames) + exports.ox_target:addModel(models, options) +end + +function RemoveModel(models, optionsNames) + exports.ox_target:removeModel(models, optionsNames) +end + +function AddTargetZone(coords, radius, options) + return exports.ox_target:addSphereZone({ + coords = coords, + radius = radius, + options = options + }) +end + +function RemoveTargetZone(index) + exports.ox_target:removeZone(index) +end \ No newline at end of file diff --git a/bridge/target/qb-target/client.lua b/bridge/target/qb-target/client.lua new file mode 100644 index 0000000..4407e3e --- /dev/null +++ b/bridge/target/qb-target/client.lua @@ -0,0 +1,32 @@ +if GetResourceState('ox_target') == 'started' or GetResourceState('qb-target') ~= 'started' or not Config.UseTarget then return end + +local Zones = {} + +function AddModel(models, options) + local optionsNames = {} + for i=1, #options do + optionsNames[i] = options[i].name + end + RemoveModel(models, optionsNames) + exports['qb-target']:AddTargetModel(models, {options = options, distance = 2.5}) +end + +function RemoveModel(models, optionsNames) + exports['qb-target']:RemoveTargetModel(models, optionsNames) +end + +function AddTargetZone(coords, radius, options) + local index + repeat + index = "prison_coord_" .. math.random(1, 999999999) + until not Zones[index] + exports['qb-target']:AddCircleZone(index, coords, radius, {name = index}, { + options = options + }) + return index +end + +function RemoveTargetZone(index) + Zones[index] = nil + exports['qb-target']:RemoveZone(index) +end \ No newline at end of file diff --git a/bridge/target/qtarget/client.lua b/bridge/target/qtarget/client.lua new file mode 100644 index 0000000..31aa88a --- /dev/null +++ b/bridge/target/qtarget/client.lua @@ -0,0 +1,37 @@ +if GetResourceState('ox_target') == 'started' or GetResourceState('qtarget') ~= 'started' or not Config.UseTarget then return end + +local Zones = {} + +function AddModel(models, options) + local optionsNames = {} + for i=1, #options do + optionsNames[i] = options[i].name + end + RemoveModel(models, optionsNames) + exports['qtarget']:AddTargetModel(models, {options = options, distance = 2.5}) +end + +function RemoveModel(models, optionsNames) + exports['qtarget']:RemoveTargetModel(models, optionsNames) +end + +function AddTargetZone(coords, radius, options) + local index + repeat + index = "prison_coord_" .. math.random(1, 999999999) + until not Zones[index] + exports['qtarget']:AddBoxZone(index, coords, radius, radius, { + name = index, + heading = 0.0, + minZ = coords.z, + maxZ = coords.z + radius, + }, { + options = options, + }) + return index +end + +function RemoveTargetZone(index) + Zones[index] = nil + exports['qtarget']:RemoveZone(index) +end \ No newline at end of file diff --git a/bridge/xp/client.lua b/bridge/xp/client.lua new file mode 100644 index 0000000..761c8ad --- /dev/null +++ b/bridge/xp/client.lua @@ -0,0 +1,19 @@ +-- By default, this will use Pickle's XP, but you can change it to your system here. +if not Config.XPEnabled then return end + +function GetLevel(name) + return exports.pickle_xp:GetLevel(name) +end + +function GetXPData() + local data = {} + for k,v in pairs(Config.XPCategories) do + local level = GetLevel(k) + local xp = exports.pickle_xp:GetXP(k) + data[k] = { + label = v.label, + level = level, + } + end + return data +end \ No newline at end of file diff --git a/bridge/xp/server.lua b/bridge/xp/server.lua new file mode 100644 index 0000000..154536c --- /dev/null +++ b/bridge/xp/server.lua @@ -0,0 +1,30 @@ +-- By default, this will use Pickle's XP, but you can change it to your system here. +if not Config.XPEnabled then return end + +function AddPlayerXP(source, name, xp) + exports.pickle_xp:AddPlayerXP(source, name, xp) +end + +function RemovePlayerXP(source, name, xp) + exports.pickle_xp:RemovePlayerXP(source, name, xp) +end + +function GetPlayerLevel(source, name) + return exports.pickle_xp:GetPlayerLevel(source, name) +end + +function GetPlayerXPData(source, name) + local data = {} + for k,v in pairs(Config.XPCategories) do + local level = GetPlayerLevel(source, name) + data[name] = { + label = v.label, + level = level, + } + end + return data +end + +for k,v in pairs(Config.XPCategories) do + exports.pickle_xp:RegisterXPCategory(k, v.label, v.xpStart, v.xpFactor, v.maxLevel) +end \ No newline at end of file diff --git a/config.lua b/config.lua new file mode 100644 index 0000000..6e275e3 --- /dev/null +++ b/config.lua @@ -0,0 +1,626 @@ +Config = {} + +Config.Debug = true + +Config.Language = "en" -- Language to use. + +Config.RenderDistance = 20.0 -- Model Display Radius. + +Config.InteractDistance = 2.0 -- Interact Radius + +Config.UseTarget = false -- When set to true, it'll use targeting instead of key-presses to interact. + +Config.NoModelTargeting = true -- When set to true and using Target, it'll spawn a small invisible prop so you can third-eye when no entity is defined. + +Config.Marker = { -- This will only be used if enabled, not using target, and no model is defined in the interaction. + enabled = true, + id = 2, + scale = 0.25, + color = {255, 255, 255, 127} +} + +Config.NavigationDisplay = true -- This will only be used if enabled, this is used to help the user find the activity point. + +Config.ServeTimeOffline = false -- When set to true, players can serve their time offline, lowering the time by how long they were gone. + +Config.EnableSneakout = false -- When set to true, anytime the player is outside the prison without being part of a breakout, they are freed instead of being brought back. + +Config.XPEnabled = true -- When set to true, this will enable Pickle's XP compatibility, and enable xp rewards. + +Config.XPCategories = { -- Registered XP Types for Pickle's XP. + ["strength"] = { + label = "Strength", + xpStart = 1000, + xpFactor = 0.2, + maxLevel = 100 + }, + ["cooking"] = { + label = "Cooking", + xpStart = 1000, + xpFactor = 0.2, + maxLevel = 100 + }, +} + +Config.Default = { + permissions = { -- Permissions settings for jailing, unjailing, and other things. + jail = { + jobs = {["police"] = 0, ["corrections"] = 0}, -- ["job_name"] = rank_number, ["job_name2"] = rank_number2, + groups = {"admin", "god"} -- "group1", "group2" + }, + unjail = { + jobs = {["police"] = 2, ["corrections"] = 2}, + groups = {"admin", "god"} + }, + alert = { + jobs = {["police"] = 0, ["corrections"] = 0}, + groups = {"admin", "god"} + }, + }, + outfit = { -- Prisoner outfits to set when in jail. Please change this according to your server's clothing numbers. + male = { + ['arms'] = 0, + ['tshirt_1'] = 15, + ['tshirt_2'] = 0, + ['torso_1'] = 86, + ['torso_2'] = 0, + ['bproof_1'] = 0, + ['bproof_2'] = 0, + ['decals_1'] = 0, + ['decals_2'] = 0, + ['chain_1'] = 0, + ['chain_2'] = 0, + ['pants_1'] = 10, + ['pants_2'] = 2, + ['shoes_1'] = 56, + ['shoes_2'] = 0, + ['helmet_1'] = 14, + ['helmet_2'] = 0, + }, + female = { + ['arms'] = 0, + ['tshirt_1'] = 15, + ['tshirt_2'] = 0, + ['torso_1'] = 86, + ['torso_2'] = 0, + ['bproof_1'] = 0, + ['bproof_2'] = 0, + ['decals_1'] = 0, + ['decals_2'] = 0, + ['chain_1'] = 0, + ['chain_2'] = 0, + ['pants_1'] = 10, + ['pants_2'] = 2, + ['shoes_1'] = 56, + ['shoes_2'] = 0, + ['helmet_1'] = 14, + ['helmet_2'] = 0, + } + } +} + +Config.Activities = { + ["workout"] = { + label = "Workout", -- Will have Start / Stop in front of interaction. + sections = { -- Sections for this activity. + ["lift"] = { + label = "Lift Weights", + rewards = { -- Rewards for completing the section. + {type = "xp", name = "strength", amount = 1000}, + }, + process = function(data) -- Section function. + local ped = PlayerPedId() + FreezeEntityPosition(ped, true) + SetEntityCoords(ped, data.coords.x, data.coords.y, data.coords.z - 1.0) + SetEntityHeading(ped, data.heading) + PlayAnim(ped, "amb@world_human_muscle_free_weights@male@barbell@base", "base", -8.0, 8.0, -1, 1, 1.0) + local prop = CreateProp(`prop_curl_bar_01`, data.coords.x, data.coords.y, data.coords.z + 1.0, true, true, false) + local off, rot = vector3(0.0, 0.0, 0.0), vector3(0.0, 0.0, 0.0) + AttachEntityToEntity(prop, ped, GetPedBoneIndex(ped, 28422), off.x, off.y, off.z, rot.x, rot.y, rot.z, false, false, false, true, 2, true) + local result + for i=1, 3 do + result = lib.skillCheck({'easy', 'medium', 'easy'}, {'e'}) + if not result then + break + end + Wait(1000) + end + FreezeEntityPosition(ped, false) + ClearPedTasks(ped) + DeleteEntity(prop) + return result + end + }, + ["situp"] = { + label = "Sit-Ups", + rewards = { -- Rewards for completing the section. + {type = "xp", name = "strength", amount = 1000}, + }, + process = function(data) -- Section function. + local ped = PlayerPedId() + FreezeEntityPosition(ped, true) + SetEntityCoords(ped, data.coords.x, data.coords.y, data.coords.z - 1.0) + SetEntityHeading(ped, data.heading) + PlayAnim(ped, "amb@world_human_sit_ups@male@idle_a", "idle_a", -8.0, 8.0, -1, 1, 1.0) + local result + for i=1, 3 do + result = lib.skillCheck({'easy', 'medium', 'easy'}, {'e'}) + if not result then + break + end + Wait(1000) + end + FreezeEntityPosition(ped, false) + ClearPedTasks(ped) + return result + end + }, + ["pushup"] = { + label = "Pushups", + rewards = { -- Rewards for completing the section. + {type = "xp", name = "strength", amount = 1000}, + }, + process = function(data) -- Section function. + local ped = PlayerPedId() + FreezeEntityPosition(ped, true) + SetEntityCoords(ped, data.coords.x, data.coords.y, data.coords.z - 1.0) + SetEntityHeading(ped, data.heading) + PlayAnim(ped, "amb@world_human_push_ups@male@idle_a", "idle_d", -8.0, 8.0, -1, 1, 1.0) + local result + for i=1, 3 do + result = lib.skillCheck({'easy', 'medium', 'easy'}, {'e'}) + if not result then + break + end + Wait(1000) + end + FreezeEntityPosition(ped, false) + ClearPedTasks(ped) + return result + end + }, + ["pullup"] = { + label = "Pull-ups", + rewards = { -- Rewards for completing the section. + {type = "xp", name = "strength", amount = 1000}, + }, + process = function(data) -- Section function. + local ped = PlayerPedId() + FreezeEntityPosition(ped, true) + SetEntityCoords(ped, data.coords.x, data.coords.y, data.coords.z - 1.0) + SetEntityHeading(ped, data.heading) + TaskStartScenarioInPlace(ped, "prop_human_muscle_chin_ups", 0, -1) + Wait(3000) + local result + for i=1, 3 do + result = lib.skillCheck({'easy', 'medium', 'easy'}, {'e'}) + if not result then + break + end + Wait(1000) + end + FreezeEntityPosition(ped, false) + ClearPedTasks(ped) + return result + end + }, + } + }, + ["clean"] = { + label = "Cleaning Prison", -- Will have Start / Stop in front of interaction. + sections = { -- Sections for this activity. + ["sweep"] = { + label = "Sweep Floor", + rewards = { -- Rewards for completing the section. + {type = "cash", amount = 50}, + }, + process = function(data) -- Section function. + local ped = PlayerPedId() + FreezeEntityPosition(ped, true) + SetEntityCoords(ped, data.coords.x, data.coords.y, data.coords.z) + SetEntityHeading(ped, data.heading) + PlayAnim(ped, "anim@amb@drug_field_workers@rake@male_a@base", "base", -8.0, 8.0, -1, 1, 1.0) + local prop = CreateProp(`prop_tool_broom`, data.coords.x, data.coords.y, data.coords.z + 1.0, true, true, false) + local off, rot = vector3(-0.01, 0.04, -0.03), vector3(0.0, 0.0, 0.0) + AttachEntityToEntity(prop, ped, GetPedBoneIndex(ped, 28422), off.x, off.y, off.z, rot.x, rot.y, rot.z, false, false, false, true, 2, true) + Wait(3000) + FreezeEntityPosition(ped, false) + ClearPedTasks(ped) + DeleteEntity(prop) + return true + end + }, + } + }, + ["kitchen"] = { + label = "Kitchen Job", -- Will have Start / Stop in front of interaction. + sections = { -- Sections for this activity. + ["stock"] = { + label = "Collect Ingredients", + rewards = nil, -- Rewards for completing the section. + process = function(data) -- Section function. + local ped = PlayerPedId() + FreezeEntityPosition(ped, true) + SetEntityHeading(ped, data.heading) + PlayAnim(ped, "amb@world_human_stand_fire@male@idle_a", "idle_a", -8.0, 8.0, -1, 1, 1.0) + Wait(5000) + FreezeEntityPosition(ped, false) + ClearPedTasks(ped) + return true + end + }, + ["cook"] = { + label = "Cook Food", + rewards = nil, -- Rewards for completing the section. + process = function(data) -- Section function. + local ped = PlayerPedId() + FreezeEntityPosition(ped, true) + SetEntityHeading(ped, data.heading) + TaskStartScenarioInPlace(ped, "PROP_HUMAN_BBQ", 0, 1) + local result + for i=1, 3 do + result = lib.skillCheck({'easy', 'medium', 'easy'}, {'e'}) + if not result then + break + end + Wait(1000) + end + FreezeEntityPosition(ped, false) + ClearPedTasks(ped) + return result + end + }, + ["toppings"] = { + label = "Add Toppings", + rewards = nil, -- Rewards for completing the section. + process = function(data) -- Section function. + local ped = PlayerPedId() + FreezeEntityPosition(ped, true) + SetEntityHeading(ped, data.heading) + PlayAnim(ped, "amb@world_human_stand_fire@male@idle_a", "idle_a", -8.0, 8.0, -1, 1, 1.0) + Wait(5000) + FreezeEntityPosition(ped, false) + ClearPedTasks(ped) + return true + end + }, + ["delivery"] = { + label = "Deliver Food", + rewards = {-- Rewards for completing the section. + {type = "cash", amount = 200}, + {type = "xp", name = "cooking", amount = 1000}, + }, + process = function(data) -- Section function. + local ped = PlayerPedId() + local prop = GetActivityEntity("tray") + if not object then + prop = CreateProp(`prop_food_tray_03`, data.coords.x, data.coords.y, data.coords.z + 1.0, true, true, false) + AddActivityEntity("tray", prop) + local off, rot = vector3(0.0, 0.0, 0.0), vector3(0.0, 0.0, 0.0) + AttachEntityToEntity(prop, ped, GetPedBoneIndex(ped, 28422), off.x, off.y, off.z, rot.x, rot.y, rot.z, false, false, false, true, 2, true) + end + FreezeEntityPosition(ped, true) + SetEntityHeading(ped, data.heading) + PlayAnim(ped, "mini@repair", "fixing_a_ped", -8.0, 8.0, -1, 1, 1.0) + Wait(500) + DetachEntity(prop, true, true) + FreezeEntityPosition(prop, true) + PlaceObjectOnGroundProperly(prop) + SetEntityHeading(prop, data.heading) + Wait(1000) + FreezeEntityPosition(ped, false) + ClearPedTasks(ped) + DeleteActivityEntity("tray") + return true + end + }, + } + }, +} + +Config.UnrevokedItems = { -- Items to skip when confiscating the player's inventory. + "burger", + "water", + "cash", + "money", +} + +Config.Breakout = { + alert = true, -- This will start the siren, and notify all law enforcement with permission. + time = 120, -- In seconds, at the end of this time, the tunnel will close for other people to climb into. + model = {modelType = "prop", hash = `prop_rock_1_i`, offset = vector3(0.0, 0.0, -0.2)}, + required = { + {type = "item", name = "shovel", amount = 1}, + }, + process = function(data) + local ped = PlayerPedId() + FreezeEntityPosition(ped, true) + SetEntityCoords(ped, data.coords.x, data.coords.y, data.coords.z - 1.0) + SetEntityHeading(ped, data.heading) + PlayAnim(ped, "random@burial", "a_burial", -8.0, 8.0, -1, 1, 1.0) + local prop = CreateProp(`prop_tool_shovel`, data.coords.x, data.coords.y, data.coords.z + 1.0, true, true, false) + local off, rot = vector3(0.0, 0.0, 0.0), vector3(0.0, 0.0, 0.0) + AttachEntityToEntity(prop, ped, GetPedBoneIndex(ped, 28422), off.x, off.y, off.z, rot.x, rot.y, rot.z, false, false, false, true, 2, true) + local result + for i=1, 3 do + result = lib.skillCheck({'easy', 'medium', 'easy'}, {'e'}) + if not result then + break + end + Wait(1000) + end + FreezeEntityPosition(ped, false) + ClearPedTasks(ped) + DeleteEntity(prop) + return result + end +} + +Config.Alerts = function(index, disabled) + local prison = Config.Prisons[index] + if (not disabled) then + ShowNotification("The prison siren has been activated at " .. prison.label .. "!") + else + ShowNotification("The prison siren has been turned-off at " .. prison.label .. ".") + end +end + +Config.Prisons = { + ["default"] = { -- Default is used as the prison location for players when not defined otherwise. + label = "Boilingbroke Penitentiary", -- Prison label for notifications & texts. + coords = vector3(1691.8187, 2604.5383, 45.5648), -- Location of the prison. + radius = 250.0, -- This is the radius that prisoners will be freed at when exceeding this number. + permissions = nil, -- When nil, defaults to Config.Default.permissions. + outfit = nil, -- When nil, defaults to Config.Default.outfit. + blip = { + label = "Boilingbroke Penitentiary", + coords = vector3(1691.8187, 2604.5383, 45.5648), + id = 188, + color = 44, + scale = 0.85, + }, + hospital = { + coords = vector3(1768.1461, 2570.0391, 45.7299), + heading = 310.6851 + }, + release = { + coords = vector3(1837.1382, 2591.4004, 45.0144), + heading = 175.6774 + }, + breakout = { + start = { + coords = vector3(1759.4132, 2471.3728, 45.7407), + heading = 211.9629 + }, + enter = { + coords = vector3(-472.8028, 2089.3516, 120.0673), + heading = 195.9083 + }, + leave = { + coords = vector3(-595.8946, 2088.1353, 131.3309), + heading = 43.9493 + }, + finish = { + coords = vector3(1947.4604, 2683.6985, 42.7468), + heading = 33.8545 + }, + }, + activities = { + { + name = "workout", + model = {hash = `u_m_y_prisoner_01`}, + coords = vector3(1750.0331, 2479.8518, 45.7407), + heading = 27.7556, + randomSection = true, -- Chooses random section when true, or top-to-bottom when false. + sections = { + { + name = "lift", + coords = vector3(1743.8164, 2483.1914, 45.7407), + heading = 202.0358 + }, + { + name = "situp", + coords = vector3(1743.3867, 2480.9863, 45.7593), + heading = 123.3956 + }, + { + name = "pushup", + coords = vector3(1743.8944, 2479.2173, 45.7593), + heading = 119.1793 + }, + { + name = "pullup", + coords = vector3(1746.5868, 2481.5996, 45.7407), + heading = 118.0926 + }, + } + }, + { + name = "clean", + model = {hash = `u_m_y_prisoner_01`}, + coords = vector3(1773.9653, 2493.1362, 45.7408), + heading = 111.6702, + randomSection = true, -- Chooses random section when true, or top-to-bottom when false. + sections = { + { + name = "sweep", + coords = vector3(1767.6052, 2501.1599, 44.7407), -- Location of the cell. + heading = 207.8018, -- Direction to face the player upon spawn. + }, + { + name = "sweep", + coords = vector3(1765.1724, 2498.3315, 44.7407), -- Location of the cell. + heading = 207.8018, -- Direction to face the player upon spawn. + }, + { + name = "sweep", + coords = vector3(1762.1005, 2496.5417, 44.7407), -- Location of the cell. + heading = 207.8018, -- Direction to face the player upon spawn. + }, + { + name = "sweep", + coords = vector3(1755.3977, 2492.9087, 44.7407), -- Location of the cell. + heading = 207.8018, -- Direction to face the player upon spawn. + }, + { + name = "sweep", + coords = vector3(1752.5946, 2491.2573, 44.7407), -- Location of the cell. + heading = 207.8018, -- Direction to face the player upon spawn. + }, + { + name = "sweep", + coords = vector3(1749.4236, 2489.4070, 44.7407), -- Location of the cell. + heading = 207.8018, -- Direction to face the player upon spawn. + }, + } + }, + { + name = "kitchen", + model = {hash = `s_m_y_chef_01`}, + coords = vector3(1787.3306, 2562.6624, 45.6731), + heading = 111.6702, + randomSection = false, -- Chooses random section when true, or top-to-bottom when false. + sections = { + { + name = "stock", + coords = vector3(1785.7053, 2564.3765, 45.6731), -- Location of the cell. + heading = 4.2023, -- Direction to face the player upon spawn. + }, + { + name = "cook", + coords = vector3(1780.9863, 2564.6482, 45.5927), -- Location of the cell. + heading = 5.9864, -- Direction to face the player upon spawn. + }, + { + name = "toppings", + coords = vector3(1781.5797, 2560.6870, 45.6731), -- Location of the cell. + heading = 179.1711, -- Direction to face the player upon spawn. + }, + { + name = "delivery", + coords = vector3(1785.5145, 2554.4749, 45.6731), -- Location of the cell. + heading = 270.9756, -- Direction to face the player upon spawn. + }, + } + }, + }, + cells = { + { + coords = vector3(1767.6052, 2501.1599, 44.7407), -- Location of the cell. + heading = 207.8018, -- Direction to face the player upon spawn. + size = 1.5, -- Size to check to see if any players are inside the cell. + }, + { + coords = vector3(1765.1724, 2498.3315, 44.7407), -- Location of the cell. + heading = 207.8018, -- Direction to face the player upon spawn. + size = 1.5, -- Size to check to see if any players are inside the cell. + }, + { + coords = vector3(1762.1005, 2496.5417, 44.7407), -- Location of the cell. + heading = 207.8018, -- Direction to face the player upon spawn. + size = 1.5, -- Size to check to see if any players are inside the cell. + }, + { + coords = vector3(1755.3977, 2492.9087, 44.7407), -- Location of the cell. + heading = 207.8018, -- Direction to face the player upon spawn. + size = 1.5, -- Size to check to see if any players are inside the cell. + }, + { + coords = vector3(1752.5946, 2491.2573, 44.7407), -- Location of the cell. + heading = 207.8018, -- Direction to face the player upon spawn. + size = 1.5, -- Size to check to see if any players are inside the cell. + }, + { + coords = vector3(1749.4236, 2489.4070, 44.7407), -- Location of the cell. + heading = 207.8018, -- Direction to face the player upon spawn. + size = 1.5, -- Size to check to see if any players are inside the cell. + }, + }, + stores = { + { + label = "Prison Commissary", + coords = vector3(1779.5208, 2560.6865, 45.6731), + heading = 173.8729, + model = {hash = `s_m_y_chef_01`}, + catalog = { + { + name = "burger", + description = "A great hamburger that is slightly edible.", + amount = 1, + required = { + {type = "cash", amount = 100}, + } + }, + { + name = "water", + description = "Refreshing sink water that'll quench your thirst.", + amount = 1, + required = { + {type = "cash", amount = 100}, + } + }, + } + }, + { + label = "Prison Plug", + coords = vector3(1598.1722, 2550.0127, 45.5649), + heading = 287.9169, + model = {hash = `s_m_y_prisoner_01`}, + catalog = { + { + name = "WEAPON_SWITCHBLADE", + description = "A great tool to take out your enemies.", + amount = 1, + required = { + {type = "item", name = "wood", amount = 1}, + {type = "item", name = "metal", amount = 1}, + } + }, + { + name = "shovel", + description = "Maybe I could use this to escape...", + amount = 1, + required = { + {type = "item", name = "wood", amount = 1}, + {type = "item", name = "metal", amount = 1}, + {type = "item", name = "rope", amount = 1}, + } + }, + } + }, + + }, + lootables = { + { + label = "Wood", -- Lootable Label. + coords = vector3(1627.9252, 2539.87, 45.7227), + heading = 277.6246, + model = {modelType = "prop", hash = `prop_cons_plank`}, + regenTime = 5, -- Time after redemption it can be redeemed again. + rewards = { -- Rewards for redeeming the lootable. + {type = "item", name = "wood", amount = 1}, + }, + }, + { + label = "Metal", + coords = vector3(1776.5386, 2563.7231, 45.57), + heading = 1.5599, + model = {modelType = "prop", hash = `prop_ladel`, offset = vector3(0.0, 0.0, 1.0)}, + regenTime = 5, -- Time after redemption it can be redeemed again. + rewards = { -- Rewards for redeeming the lootable. + {type = "item", name = "metal", amount = 1}, + }, + }, + { + label = "Rope", + coords = vector3(1689.0037, 2548.8884, 45.5604), + heading = 35.3041, + model = {modelType = "prop", hash = `prop_rope_family_3`}, + regenTime = 5, -- Time after redemption it can be redeemed again. + rewards = { -- Rewards for redeeming the lootable. + {type = "item", name = "rope", amount = 1}, + }, + }, + } + } +} \ No newline at end of file diff --git a/core/client.lua b/core/client.lua new file mode 100644 index 0000000..2c46859 --- /dev/null +++ b/core/client.lua @@ -0,0 +1,261 @@ +function CreateBlip(data) + local x,y,z = table.unpack(data.coords) + local blip = AddBlipForCoord(x, y, z) + SetBlipSprite(blip, data.id or 1) + SetBlipDisplay(blip, data.display or 4) + SetBlipScale(blip, data.scale or 1.0) + SetBlipColour(blip, data.color or 1) + if (data.rotation) then + SetBlipRotation(blip, math.ceil(data.rotation)) + end + SetBlipAsShortRange(blip, true) + BeginTextCommandSetBlipName("STRING") + AddTextComponentString(data.label) + EndTextCommandSetBlipName(blip) + return blip +end + +function CreateVeh(modelHash, ...) + RequestModel(modelHash) + while not HasModelLoaded(modelHash) do Wait(0) end + local veh = CreateVehicle(modelHash, ...) + SetModelAsNoLongerNeeded(modelHash) + return veh +end + +function CreateNPC(modelHash, ...) + RequestModel(modelHash) + while not HasModelLoaded(modelHash) do Wait(0) end + local ped = CreatePed(26, modelHash, ...) + SetModelAsNoLongerNeeded(modelHash) + return ped +end + +function CreateProp(modelHash, ...) + RequestModel(modelHash) + while not HasModelLoaded(modelHash) do Wait(0) end + local obj = CreateObject(modelHash, ...) + SetModelAsNoLongerNeeded(modelHash) + return obj +end + +function PlayAnim(ped, dict, ...) + RequestAnimDict(dict) + while not HasAnimDictLoaded(dict) do Wait(0) end + TaskPlayAnim(ped, dict, ...) +end + +function PlayEffect(dict, particleName, entity, off, rot, time, cb) + CreateThread(function() + RequestNamedPtfxAsset(dict) + while not HasNamedPtfxAssetLoaded(dict) do + Wait(0) + end + UseParticleFxAssetNextCall(dict) + Wait(10) + local particleHandle = StartParticleFxLoopedOnEntity(particleName, entity, off.x, off.y, off.z, rot.x, rot.y, rot.z, 1.0) + SetParticleFxLoopedColour(particleHandle, 0, 255, 0 , 0) + Wait(time) + StopParticleFxLooped(particleHandle, false) + cb() + end) +end + +function WarpPlayer(coords, heading, cb) + CreateThread(function() + local ped = PlayerPedId() + DoScreenFadeOut(1000) + Wait(1300) + SetEntityCoords(ped, coords.x, coords.y, coords.z) + SetEntityHeading(ped, heading) + Wait(200) + DoScreenFadeIn(1000) + if cb then cb() end + end) +end + +local interactTick = 0 +local interactCheck = false +local interactText = nil + +function ShowInteractText(text) + local timer = GetGameTimer() + interactTick = timer + if interactText == nil or interactText ~= text then + interactText = text + lib.showTextUI(text) + end + if interactCheck then return end + interactCheck = true + CreateThread(function() + Wait(150) + local timer = GetGameTimer() + interactCheck = false + if timer ~= interactTick then + lib.hideTextUI() + interactText = nil + interactTick = 0 + end + end) +end + +local Interactions = {} +EnableInteraction = true + +function FormatOptions(index, data) + local options = data.options + local list = {} + if not options or #options < 2 then + list[1] = ((options and options[1]) and options[1] or { label = data.label }) + list[1].name = GetCurrentResourceName() .. "_option_" .. math.random(1,999999999) + list[1].onSelect = function() + SelectInteraction(index, 1) + end + return list + end + for i=1, #options do + list[i] = options[i] + list[i].name = GetCurrentResourceName() .. "_option_" .. math.random(1,999999999) + list[i].onSelect = function() + SelectInteraction(index, i) + end + end + return list +end + +function EnsureInteractionModel(index) + local data = Interactions[index] + if not data or data.entity then return end + local entity + if not data.model and Config.UseTarget and Config.NoModelTargeting then + entity = CreateProp(`ng_proc_brick_01a`, data.coords.x, data.coords.y, data.coords.z, false, true, false) + SetEntityAlpha(entity, 0, false) + elseif data.model and (not data.model.modelType or data.model.modelType == "ped") then + local offset = data.model.offset or vector3(0.0, 0.0, 0.0) + entity = CreateNPC(data.model.hash, data.coords.x + offset.x, data.coords.y + offset.y, (data.coords.z - 1.0) + offset.z, data.heading, false, true) + SetEntityInvincible(entity, true) + SetBlockingOfNonTemporaryEvents(entity, true) + elseif data.model and data.model.modelType == "prop" then + local offset = data.model.offset or vector3(0.0, 0.0, 0.0) + entity = CreateProp(data.model.hash, data.coords.x + offset.x, data.coords.y + offset.y, (data.coords.z - 1.0) + offset.z, false, true, false) + else + return + end + FreezeEntityPosition(entity, true) + SetEntityHeading(entity, data.heading) + Interactions[index].entity = entity + return entity +end + +function DeleteInteractionEntity(index) + local data = Interactions[index] + if not data or not data.entity then return end + DeleteEntity(data.entity) + Interactions[index].entity = nil +end + +function SelectInteraction(index, selection) + if not EnableInteraction then return end + local pcoords = GetEntityCoords(PlayerPedId()) + local data = Interactions[index] + if #(data.coords - pcoords) > Config.InteractDistance then + return ShowNotification(_L("interact_far")) + end + Interactions[index].selected(selection) +end + +function CreateInteraction(data, selected) + local index + repeat + index = math.random(1, 999999999) + until not Interactions[index] + local options = FormatOptions(index, data) + Interactions[index] = { + selected = selected, + options = options, + label = data.label, + model = data.model, + coords = data.coords, + radius = data.radius or 1.0, + heading = data.heading, + } + if Config.UseTarget then + Interactions[index].zone = AddTargetZone(Interactions[index].coords, Interactions[index].radius, Interactions[index].options) + end + return index +end + +function UpdateInteraction(index, data, selected) + if not Interactions[index] then return end + Interactions[index].selected = selected + for k,v in pairs(data) do + Interactions[index][k] = v + end + if data.options then + Interactions[index].options = FormatOptions(index, data) + end + if Config.UseTarget then + RemoveTargetZone(Interactions[index].zone) + Interactions[index].zone = AddTargetZone(Interactions[index].coords, Interactions[index].radius, Interactions[index].options) + end +end + +function DeleteInteraction(index) + local data = Interactions[index] + if not data then return end + if (data.entity) then + DeleteInteractionEntity(index) + end + if Config.UseTarget then + RemoveTargetZone(data.zone) + end + Interactions[index] = nil +end + +Citizen.CreateThread(function() + while true do + local ped = PlayerPedId() + local pcoords = GetEntityCoords(ped) + local wait = 1500 + for k,v in pairs(Interactions) do + local coords = v.coords + local dist = #(pcoords-coords) + if (dist < Config.RenderDistance) then + EnsureInteractionModel(k) + if not Config.UseTarget then + if not v.model and Config.Marker and Config.Marker.enabled then + wait = 0 + DrawMarker(Config.Marker.id, coords.x, coords.y, coords.z, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + Config.Marker.scale, Config.Marker.scale, Config.Marker.scale, Config.Marker.color[1], + Config.Marker.color[2], Config.Marker.color[3], Config.Marker.color[4], false, true) + end + if dist < Config.InteractDistance then + wait = 0 + if not ShowInteractText("[E] - " .. v.label) and IsControlJustPressed(1, 51) then + if not v.options or #v.options < 2 then + SelectInteraction(k, 1) + else + lib.registerContext({ + id = 'prison_'..k, + title = v.title or "Options", + options = v.options + }) + lib.showContext('prison_'..k) + end + end + end + end + elseif v.entity then + DeleteInteractionEntity(k) + end + end + Wait(wait) + end +end) + +AddEventHandler('onResourceStop', function(resourceName) + if (GetCurrentResourceName() ~= resourceName) then return end + for k,v in pairs(Interactions) do + DeleteInteraction(k) + end +end) \ No newline at end of file diff --git a/core/shared.lua b/core/shared.lua new file mode 100644 index 0000000..2c571de --- /dev/null +++ b/core/shared.lua @@ -0,0 +1,10 @@ +function v3(coords) return vec3(coords.x, coords.y, coords.z), coords.w end + +function GetRandomInt(min, max, exclude) + for i=1, 1000 do + local int = math.random(min, max) + if exclude == nil or exclude ~= int then + return int + end + end +end \ No newline at end of file diff --git a/fxmanifest.lua b/fxmanifest.lua new file mode 100644 index 0000000..6d52f8d --- /dev/null +++ b/fxmanifest.lua @@ -0,0 +1,33 @@ +fx_version "cerulean" +game "gta5" +author "Pickle Mods" +version "v1.1.0" + +ui_page "nui/index.html" + +files { + "nui/index.html", + "nui/assets/**/*.*", +} + +shared_scripts { + "@ox_lib/init.lua", + "config.lua", + "locales/locale.lua", + "locales/translations/*.lua", + "core/shared.lua" +} + +client_scripts { + "bridge/**/**/client.lua", + "modules/**/client.lua", + "core/client.lua" +} + +server_scripts { + "@mysql-async/lib/MySQL.lua", + "bridge/**/**/server.lua", + "modules/**/server.lua", +} + +lua54 'yes' \ No newline at end of file diff --git a/locales/locale.lua b/locales/locale.lua new file mode 100644 index 0000000..3c04e4d --- /dev/null +++ b/locales/locale.lua @@ -0,0 +1,14 @@ +Language = {} + +function _L(name, ...) + if name then + local str = Language[Config.Language][name] + if str then + return string.format(str, ...) + else + return "ERR_TRANSLATE_"..(name).."_404" + end + else + return "ERR_TRANSLATE_404" + end +end \ No newline at end of file diff --git a/locales/translations/de.lua b/locales/translations/de.lua new file mode 100644 index 0000000..11c10a4 --- /dev/null +++ b/locales/translations/de.lua @@ -0,0 +1,28 @@ +Language["de"] = { + start = "Start", + stop = "Stop", + collect = "Sammeln", + activity_stopped = "Du hast deine aktuelle Tätigkeit beendet.", + activity_started = "Du hast eine Aktivität gestartet,", + section_success = "Du hast diese Aktivität erfolgreich abgeschlossen.", + section_failure = "Du hast diese Aktivität nicht bestanden, versuche es erneut.", + interact_far = "Du bist zu weit von diesem Punkt entfernt, um mit ihm zu interagieren.", + interact_breakout = "Starte Ausbruch", + breakout_success = "Du haben einen Ausbruch begonnen, führe die Gefangenen in die Freiheit!", + breakout_fail = "Du hast es nicht geschafft, auszubrechen, versuche es noch einmal.", + breakout_self = "Du bist aus dem Gefängnis geflohen, aber pass auf, die Polizei sucht nach dir!", + interact_active_breakout = "Betrete Tunnel", + interact_exit_breakout = "Verlasse Tunnel", + confirm_transaction = "Bestätige Transaktion", + confirm_transaction_desc = "Du kannst diese Transaktion nicht mehr rückgängig machen, sobald sie bestätigt wurde.", + missing_item = "Dir Fehlen %s (%s)", + not_prison = "Es gibt keinen Gefangenen mit der ID #%s.", + in_prison = "Gefangener #%s sitzt ab %s minute(s) at %s.", + no_permission = "Du hast keine Erlaubnis, dies zu tun.", + cant_sneakout = "Du kannst dich nicht aus dem Gefängnis schleichen, du musst erst einen Ausbruch starten.", + jail_dialog_title = "Gefängnis-Dialog", + jail_dialog_player = "%s (ID: %s)", + jail_dialog_prisoner = "Häftling", + jail_dialog_prison = "Gefängnis Position", + jail_dialog_sentence = "Satzlänge", +} diff --git a/locales/translations/en.lua b/locales/translations/en.lua new file mode 100644 index 0000000..20ae0a3 --- /dev/null +++ b/locales/translations/en.lua @@ -0,0 +1,28 @@ +Language["en"] = { + start = "Start", + stop = "Stop", + collect = "Collect", + activity_stopped = "You have stopped your current activity.", + activity_started = "You have started an activity.", + section_success = "You have successfuly completed this activity.", + section_failure = "You have failed this activity, try again.", + interact_far = "You are too far from this point to interact with it.", + interact_breakout = "Start Breakout", + breakout_success = "You have started a breakout, lead the prisoners to freedom!", + breakout_fail = "You failed to breakout, try again.", + breakout_self = "You escaped from prison, but beware, the police are looking for you!", + interact_active_breakout = "Enter Tunnel", + interact_exit_breakout = "Exit Tunnel", + confirm_transaction = "Confirm Transaction", + confirm_transaction_desc = "You cannot reverse this transaction once confirmed.", + missing_item = "You are missing %s (%s)", + not_prison = "There is no prisoner with the ID #%s.", + in_prison = "Prisoner #%s is serving %s minute(s) at %s.", + no_permission = "You do not have permission to do this.", + cant_sneakout = "You cannot sneak out of the prison, you must start a breakout.", + jail_dialog_title = "Jail Dialog", + jail_dialog_player = "%s (ID: %s)", + jail_dialog_prisoner = "Prisoner", + jail_dialog_prison = "Prison Location", + jail_dialog_sentence = "Sentence Length", +} \ No newline at end of file diff --git a/modules/activities/client.lua b/modules/activities/client.lua new file mode 100644 index 0000000..255c108 --- /dev/null +++ b/modules/activities/client.lua @@ -0,0 +1,161 @@ +local ActivityStatus +local ActivityInteraction +local ActivityStartInteractions = {} +local ActivityEntities = {} + +function CleanupActivity() + if (ActivityInteraction) then + DeleteInteraction(ActivityInteraction) + end + ActivityInteraction = nil + for k,v in pairs(ActivityEntities) do + DeleteActivityEntity(k) + end +end + +function AddActivityEntity(name, object) + if GetActivityEntity(name) then + DeleteActivityEntity(name) + end + ActivityEntities[name] = object +end + +function GetActivityEntity(name) + return ActivityEntities[name] +end + +function DeleteActivityEntity(name) + if ActivityEntities[name] then + DeleteEntity(ActivityEntities[name]) + end + ActivityEntities[name] = nil +end + +function StartSection(activityIndex, sectionIndex) + CleanupActivity() + Wait(250) + ActivityStatus = { activityIndex = activityIndex, sectionIndex = sectionIndex } + local index = Prison.index + local prison = Config.Prisons[index] + local activity = prison.activities[activityIndex] + local activityCfg = Config.Activities[activity.name] + local section = activity.sections[sectionIndex] + local sectionCfg = activityCfg.sections[section.name] + + local interact = UpdateInteraction(ActivityStartInteractions[activityIndex], { + label = _L("stop") .. " " .. activityCfg.label, + options = {} + }, function(selected) + TriggerServerEvent("pickle_prisons:stopActivity", index, activityIndex) + end) + + local interact = CreateInteraction({ + label = sectionCfg.label, + model = sectionCfg.model, + coords = section.coords, + heading = section.heading + }, function(selected) + EnableInteraction = false + if (sectionCfg.process(section)) then + ShowNotification(_L("section_success")) + ServerCallback("pickle_prisons:startNextSection", function(result) + if (result) then + StartSection(result.activityIndex, result.section) + end + end) + else + ShowNotification(_L("section_failure")) + end + EnableInteraction = true + end) + + ActivityInteraction = interact + + if Config.NavigationDisplay then + CreateThread(function() + while ActivityInteraction == interact do + local pcoords = GetEntityCoords(PlayerPedId()) + local dist = #(section.coords-pcoords) + local meters = math.ceil(dist * 1) + if EnableInteraction then + DrawDestination(section.coords, "Activity", meters) + end + Wait(0) + end + end) + end +end + +function InteractActivity(activityIndex) + local index = Prison.index + local prison = Config.Prisons[index] + local activity = prison.activities[activityIndex] + local activityCfg = Config.Activities[activity.name] + ServerCallback("pickle_prisons:startActivity", function(result, needStop) + if not result then return end + if needStop then + CleanupActivity() + Wait(250) + end + StartSection(activityIndex, result.section) + end, index, activityIndex) +end + +RegisterNetEvent("pickle_prisons:stopActivity", function(status) + ActivityStatus = nil + CleanupActivity() + if status then + local activity = Config.Prisons[status.index].activities[status.activityIndex] + local activityCfg = Config.Activities[activity.name] + if activityCfg then + UpdateInteraction(ActivityStartInteractions[status.activityIndex], { + label = _L("start") .. " " .. activityCfg.label, + options = {} + }, function(selected) + InteractActivity(status.activityIndex) + end) + end + end +end) + +RegisterNetEvent("pickle_prisons:enterPrison", function() + local prison = Config.Prisons[Prison.index] + CleanupActivity() + local prison = Config.Prisons[Prison.index] + for i=1, #prison.activities do + local activity = prison.activities[i] + local activityCfg = Config.Activities[activity.name] + if activityCfg then + ActivityStartInteractions[i] = CreateInteraction({ + label = _L("start") .. " " .. activityCfg.label, + model = activity.model, + coords = activity.coords, + heading = activity.heading + }, function(selected) + InteractActivity(i) + end) + end + end +end) + +RegisterNetEvent("pickle_prisons:leavePrison", function() + for k,v in pairs(ActivityStartInteractions) do + DeleteInteraction(v) + end + CleanupActivity() +end) + +function DrawDestination(coords, label, meters) + local onScreen, screenX, screenY = GetScreenCoordFromWorldCoord(coords.x, coords.y, coords.z) + local icon_scale = 1.0 + local text_scale = 0.25 + -- Icon + RequestStreamedTextureDict("basejumping", false) + DrawSprite("basejumping", "arrow_pointer", screenX, screenY - 0.015, 0.015 * icon_scale, 0.025 * icon_scale, 180.0, 255, 255, 0, 255) + -- Text + SetTextCentre(true) + SetTextScale(0.0, text_scale) + SetTextEntry("STRING") + AddTextComponentString(label .. "\n".. meters .. "m") + DrawText(screenX, screenY) +end \ No newline at end of file diff --git a/modules/activities/server.lua b/modules/activities/server.lua new file mode 100644 index 0000000..521534a --- /dev/null +++ b/modules/activities/server.lua @@ -0,0 +1,64 @@ +local ActivityPlayers = {} + +function GetActivityStatus(source) + return ActivityPlayers[source] +end + +function StopActivity(source, sendEvent) + local status = GetActivityStatus(source) + ActivityPlayers[source] = nil + if sendEvent then + ShowNotification(source, _L("activity_stopped")) + TriggerClientEvent("pickle_prisons:stopActivity", source, status) + end +end + +function StartNextSection(source, index, activityIndex, lastSection) + local prison = Config.Prisons[index] + local activity = prison.activities[activityIndex] + local activityCfg = Config.Activities[activity.name] + if lastSection and activityCfg.sections[activity.sections[lastSection].name].rewards then + GiveRewards(source, activityCfg.sections[activity.sections[lastSection].name].rewards) + end + local section = lastSection + if activity.randomSection then + if #activity.sections > 1 then + repeat + section = math.random(1, #activity.sections) + until section ~= lastSection + else + section = 1 + end + elseif not section or section + 1 > #activity.sections then + section = 1 + else + section = section + 1 + end + return { + index = index, + activityIndex = activityIndex, + section = section + } +end + +RegisterCallback("pickle_prisons:startActivity", function(source, cb, index, activityIndex) + local status = GetActivityStatus(source) + local needStop = (status and true or false) + if status then + StopActivity(source) + end + ActivityPlayers[source] = StartNextSection(source, index, activityIndex) + cb(ActivityPlayers[source], needStop) +end) + +RegisterCallback("pickle_prisons:startNextSection", function(source, cb) + local status = GetActivityStatus(source) + if not status then return end + ActivityPlayers[source] = StartNextSection(source, status.index, status.activityIndex, status.section) + cb(ActivityPlayers[source]) +end) + +RegisterNetEvent("pickle_prisons:stopActivity", function() + local source = source + StopActivity(source, true) +end) \ No newline at end of file diff --git a/modules/lootables/client.lua b/modules/lootables/client.lua new file mode 100644 index 0000000..adbc776 --- /dev/null +++ b/modules/lootables/client.lua @@ -0,0 +1,55 @@ +Lootables = {} + +function DeleteLootable(index, lootID) + local loot = Lootables[index][lootID] + if not loot then return end + DeleteInteraction(loot.interact) + Lootables[index][lootID] = nil +end + +function CleanupLootables() + for k,v in pairs(Lootables) do + for i=1, #v do + DeleteLootable(k, i) + end + end +end + +function CreateLootable(index, lootID) + local loot = Lootables[index][lootID] + if loot.interact then + DeleteInteraction(loot.interact) + end + Lootables[index][lootID].interact = CreateInteraction({ + label = _L("collect") .. " " .. loot.label, + model = loot.model, + coords = loot.coords, + heading = loot.heading + }, function(selected) + local ped = PlayerPedId() + PlayAnim(ped, "random@domestic", "pickup_low", -8.0, 8.0, -1, 1, 1.0) + Wait(1500) + ClearPedTasks(ped) + TriggerServerEvent("pickle_prisons:collectLootable", index, lootID) + end) +end + +RegisterNetEvent("pickle_prisons:lootStatus", function(index, lootID, status) + local loot = Lootables[index][lootID] + if not status and loot.interact then + DeleteInteraction(loot.interact) + Lootables[index][lootID].interact = nil + elseif status then + CreateLootable(index, lootID) + end +end) + +RegisterNetEvent("pickle_prisons:setLootables", function(data) + CleanupLootables() + Lootables = data + for k,v in pairs(Lootables) do + for i=1, #v do + CreateLootable(k, i) + end + end +end) \ No newline at end of file diff --git a/modules/lootables/server.lua b/modules/lootables/server.lua new file mode 100644 index 0000000..2fe42c2 --- /dev/null +++ b/modules/lootables/server.lua @@ -0,0 +1,35 @@ +Lootables = {} + +function UpdateLootables(source) + TriggerClientEvent("pickle_prisons:setLootables", source, Lootables) +end + +RegisterNetEvent("pickle_prisons:collectLootable", function(index, lootID) + local source = source + if Lootables[index] and Lootables[index][lootID] then + local loot = Lootables[index][lootID] + if (os.time() - loot.lastRedeem > loot.regenTime) then + Lootables[index][lootID].lastRedeem = os.time() + GiveRewards(source, loot.rewards) + TriggerClientEvent("pickle_prisons:lootStatus", -1, index, lootID, false) + SetTimeout(1000 * loot.regenTime, function() + TriggerClientEvent("pickle_prisons:lootStatus", -1, index, lootID, true) + end) + end + end +end) + +for k,v in pairs(Config.Prisons) do + Lootables[k] = v + for i=1, #v.lootables do + Lootables[k][i] = { + label = v.lootables[i].label, + coords = v.lootables[i].coords, + heading = v.lootables[i].heading, + model = v.lootables[i].model, + rewards = v.lootables[i].rewards, + regenTime = v.lootables[i].regenTime, + lastRedeem = 0 + } + end +end \ No newline at end of file diff --git a/modules/prison/client.lua b/modules/prison/client.lua new file mode 100644 index 0000000..e437258 --- /dev/null +++ b/modules/prison/client.lua @@ -0,0 +1,234 @@ +Prison = nil + +PrisonInteractions = {} +PrisonSirens = {} + +local CheckBreakout = true + +function InitializeScript() + for k,v in pairs(Config.Prisons) do + PrisonInteractions[k] = {} + if v.blip then + CreateBlip(v.blip) + end + end +end + +function TeleportHospital() + if not Prison then return end + CheckBreakout = false + Wait(2000) + local ped = PlayerPedId() + local coords = Config.Prisons[Prison.index].hospital.coords + local heading = Config.Prisons[Prison.index].hospital.heading + SetEntityCoords(ped, coords.x, coords.y, coords.z) + SetEntityHeading(ped, heading) + Wait(2000) + CheckBreakout = true +end + +function ResetBreakout(index) + local prison = Config.Prisons[index] + if PrisonInteractions[index].breakout then + DeleteInteraction(PrisonInteractions[index].breakout) + end + PrisonInteractions[index].breakout = CreateInteraction({ + label = _L("interact_breakout"), + coords = prison.breakout.start.coords, + heading = prison.breakout.start.heading + }, function(selected) + ServerCallback("pickle_prisons:canBreakout", function(result) + if not result then return end + if Config.Breakout.process(prison.breakout.start) then + ShowNotification(_L("breakout_success")) + TriggerServerEvent("pickle_prisons:startBreakout", index) + else + ShowNotification(_L("breakout_fail")) + end + end, index) + end) +end + +function GetClosestPrison() + local ped = PlayerPedId() + local coords = GetEntityCoords(ped) + local closest + for k,v in pairs(Config.Prisons) do + if (not closest or (#(coords-v.coords) < closest.dist)) then + closest = {index = k, dist = #(coords-v.coords) } + end + end + if closest then + return closest.index + end +end + +function JailDialog() + local players = GetPlayersInArea() + local players_list = {} + local prisons = {} + for i=1, #players do + local id = GetPlayerServerId(players[i]) + players_list[#players_list + 1] = {label = _L("jail_dialog_player", GetPlayerName(players[i]), id), value = id} + end + for k,v in pairs(Config.Prisons) do + prisons[#prisons + 1] = {label = v.label, value = k} + end + if #prisons < 1 or #players_list < 1 then return end + local input = lib.inputDialog(_L("jail_dialog_title"), { + {type = 'select', label = _L("jail_dialog_prisoner"), default = players_list[1].value, required = true, options = players_list}, + {type = 'select', label = _L("jail_dialog_prison"), default = "default", required = true, options = prisons}, + {type = 'number', label = _L("jail_dialog_sentence"), default = 1, required = true, min = 1}, + }) + if not input then return end + TriggerServerEvent("pickle_prisons:jailPlayer", input[1], input[3], input[2]) +end + +RegisterNetEvent("pickle_prisons:jailDialog", JailDialog) + +RegisterNetEvent("pickle_prisons:startBreakout", function(index) + local prison = Config.Prisons[index] + DeleteInteraction(PrisonInteractions[index].breakout) + PrisonInteractions[index].breakout = CreateInteraction({ + label = _L("interact_active_breakout"), + coords = prison.breakout.start.coords, + heading = prison.breakout.start.heading, + model = Config.Breakout.model + }, function(selected) + ServerCallback("pickle_prisons:enterBreakoutPoint", function(result) + if not result then return end + local coords = prison.breakout.enter.coords + local heading = prison.breakout.enter.heading + TriggerServerEvent("pickle_prisons:breakout") + TriggerEvent("pickle_prisons:leavePrison") + WarpPlayer(coords, heading) + local interact = CreateInteraction({ + label = _L("interact_exit_breakout"), + coords = prison.breakout.leave.coords, + heading = prison.breakout.leave.heading, + model = Config.Breakout.model + }, function(selected) + ServerCallback("pickle_prisons:enterBreakoutPoint", function(result) + if not result then return end + local coords = prison.breakout.finish.coords + local heading = prison.breakout.finish.heading + WarpPlayer(coords, heading) + DeleteInteraction(interact) + end, index, "finish") + end) + end, index, "enter") + end) +end) + +RegisterNetEvent("pickle_prisons:stopBreakout", function(index) + ResetBreakout(index) +end) + +RegisterNetEvent("pickle_prisons:jailPlayer", function(data) + Prison = data + local prison = Config.Prisons[data.index] + local cell = prison.cells[math.random(1, #prison.cells)] + local coords = cell.coords + local heading = cell.heading + WarpPlayer(coords, heading, function() + ToggleOutfit(true) + end) + TriggerEvent("pickle_prisons:enterPrison") + Wait(2000) + CreateThread(function() + local coords = prison.coords + while Prison and Prison.index == data.index do + if CheckBreakout then + if Config.EnableSneakout then + local pcoords = GetEntityCoords(PlayerPedId()) + if #(coords - pcoords) > prison.radius then + TriggerServerEvent("pickle_prisons:breakout") + TriggerEvent("pickle_prisons:leavePrison") + break + end + else + local pcoords = GetEntityCoords(PlayerPedId()) + if #(coords - pcoords) > prison.radius then + local cell = prison.cells[math.random(1, #prison.cells)] + local coords = cell.coords + local heading = cell.heading + ShowNotification(_L("cant_sneakout")) + WarpPlayer(coords, heading, function() + ToggleOutfit(true) + end) + end + end + end + Wait(1500) + end + end) +end) + +RegisterNetEvent("pickle_prisons:unjailPlayer", function(data) + TriggerEvent("pickle_prisons:leavePrison") + local prison = Config.Prisons[data.index] + local coords = prison.release.coords + local heading = prison.release.heading + WarpPlayer(coords, heading, function() + ToggleOutfit(false) + end) +end) + +RegisterNetEvent("pickle_prisons:enterPrison", function() + local index = Prison.index + local prison = Config.Prisons[index] + ResetBreakout(index) +end) + +RegisterNetEvent("pickle_prisons:leavePrison", function() + Prison = nil +end) + +RegisterNetEvent("pickle_prisons:startSiren", function(index) + if PrisonSirens[index] then return end + PrisonSirens[index] = true + local prison = Config.Prisons[index] + SendNUIMessage({ + type = "startSiren" + }) + CreateThread(function() + local maxDist = prison.radius * 2 + while PrisonSirens[index] do + if GetClosestPrison() == index then + local pcoords = GetEntityCoords(PlayerPedId()) + local dist = #(prison.coords - pcoords) + local factor = 1.0 - (dist / maxDist) + if factor < 0 then + factor = 0 + end + SendNUIMessage({ + type = "setVolume", + value = factor + }) + end + Wait(1500) + end + SendNUIMessage({ + type = "endSiren" + }) + end) +end) + +RegisterNetEvent("pickle_prisons:stopSiren", function(index) + PrisonSirens[index] = nil +end) + +RegisterNetEvent("pickle_prisons:alert", function(index, disabled) + Config.Alerts(index, disabled) +end) + +AddEventHandler('onResourceStart', function(resourceName) + if (GetCurrentResourceName() ~= resourceName) then + return + end + TriggerServerEvent("pickle_prisons:initializePlayer") +end) + +CreateThread(function() + InitializeScript() +end) \ No newline at end of file diff --git a/modules/prison/server.lua b/modules/prison/server.lua new file mode 100644 index 0000000..8dee252 --- /dev/null +++ b/modules/prison/server.lua @@ -0,0 +1,375 @@ +Prisoners = {} +Breakouts = {} + +function CheckRequired(source, required) + if not required or #required < 1 then return true end + local success = true + local missingItems = {} + for i=1, #required do + local part = required[i] + if (not part.type or part.type == "item") then + local remaining = Inventory.GetItemCount(source, part.name) - (part.amount) + if remaining < 0 then + success = false + missingItems[#missingItems + 1] = {index = i, name = part.name, count = remaining * -1} + end + elseif (part.type == "cash") then + local remaining = GetMoney(source) - part.amount + if remaining < 0 then + success = false + missingItems[#missingItems + 1] = {index = i, name = "cash", count = remaining * -1} + end + elseif (part.type == "weapon" and not Inventory.HasWeapon(source, part.name)) then + success = false + missingItems[#missingItems + 1] = {index = i, name = part.name, count = 1} + end + end + return success, missingItems +end + +function TakeRequired(source, required) + if not required or #required < 1 then return true end + for i=1, #required do + local part = required[i] + if (not part.type or part.type == "item") then + Inventory.RemoveItem(source, part.name, part.amount) + elseif (part.type == "cash") then + RemoveMoney(source, part.amount) + elseif (part.type == "weapon" and not Inventory.HasWeapon(source, part.name)) then + Inventory.RemoveWeapon(source, part.name, 1) + end + end +end + +function GiveRewards(source, rewards) + for i=1, #rewards do + local reward = rewards[i] + if not reward.type or reward.type == "item" then + Inventory.AddItem(source, reward.name, reward.amount, reward.createItem and reward.createItem(craftingData) or nil) + elseif reward.type == "cash" then + AddMoney(source, reward.amount) + elseif reward.type == "weapon" then + Inventory.AddWeapon(source, reward.name, reward.amount, reward.createItem and reward.createItem(craftingData) or nil) + elseif Config.XPEnabled and reward.type == "xp" then + AddPlayerXP(source, reward.name, reward.amount) + end + end +end + +function TakeInventory(source) + local inventory = {} + local data = Inventory.GetInventory(source) + for i=1, #data do + local take = true + for j=1, #Config.UnrevokedItems do + if data[i].name == Config.UnrevokedItems[j] then + take = false + end + end + if take then + inventory[#inventory + 1] = data[i] + Inventory.RemoveItem(source, data[i].name, data[i].count) + end + end + return inventory +end + +function JailPlayer(source, time, index, noSave) + if Prisoners[source] then return end + local index = index or "default" + local prison = Config.Prisons[index] + if not time or not prison then return end + local identifier = GetIdentifier(source) + Prisoners[source] = { + identifier = identifier, + index = index, + time = time, + inventory = TakeInventory(source), + sentence_date = os.time(), + } + SetPlayerMetadata(source, "injail", time) + TriggerClientEvent("pickle_prisons:jailPlayer", source, Prisoners[source]) + if noSave then return end + MySQL.Async.execute("DELETE FROM pickle_prisons WHERE identifier=@identifier;", {["@identifier"] = identifier}) + MySQL.Async.execute("INSERT INTO pickle_prisons (identifier, prison, time, inventory, sentence_date) VALUES (@identifier, @prison, @time, @inventory, @sentence_date);", { + ["@identifier"] = Prisoners[source].identifier, + ["@prison"] = Prisoners[source].index, + ["@time"] = Prisoners[source].time, + ["@inventory"] = json.encode(Prisoners[source].inventory), + ["@sentence_date"] = Prisoners[source].sentence_date, + }) +end + +function UnjailPlayer(source, breakout) + local data = Prisoners[source] + if not data then return end + local inventory = Prisoners[source].inventory + Prisoners[source] = nil + local identifier = GetIdentifier(source) + MySQL.Async.execute("DELETE FROM pickle_prisons WHERE identifier=@identifier;", {["@identifier"] = identifier}) + SetPlayerMetadata(source, "injail", 0) + StopActivity(source) + if breakout then return end + for i=1, #inventory do + Inventory.AddItem(source, inventory[i].name, inventory[i].count, inventory[i].metadata) + end + TriggerClientEvent("pickle_prisons:unjailPlayer", source, data) +end + +function UpdatePrisonTime(source, time) + local identifier = GetIdentifier(source) + MySQL.Async.execute("UPDATE pickle_prisons SET time=@time WHERE identifier=@identifier", { + ["@identifier"] = identifier, + ["@time"] = time, + }) +end + +RegisterCallback("pickle_prisons:canBreakout", function(source, cb, index) + if Breakouts[index] then return cb(false) end + local required = Config.Breakout.required + local success, missingItems = CheckRequired(source, required) + if not success then + for i=1, #missingItems do + ShowNotification(source, _L("missing_item", missingItems[i].name, missingItems[i].count)) + end + return cb(false) + end + cb(true) +end) + +RegisterCallback("pickle_prisons:enterBreakoutPoint", function(source, cb, index, name) + if name == "enter" and not Breakouts[index] then + cb(false) + else + cb(true) + end +end) + +RegisterNetEvent("pickle_prisons:startBreakout", function(index) + local source = source + if Breakouts[index] then return end + local required = Config.Breakout.required + local success, missingItems = CheckRequired(source, required) + if not success then + for i=1, #missingItems do + ShowNotification(source, _L("missing_item", missingItems[i].name, missingItems[i].count)) + end + return + end + TakeRequired(source, required) + Breakouts[index] = {} + TriggerClientEvent("pickle_prisons:startBreakout", -1, index) + if Config.Breakout.alert then + StartSiren(index, Config.Breakout.time) + end + SetTimeout(1000 * Config.Breakout.time, function() + if Config.Breakout.alert then + StopSiren(index, Config.Breakout.time) + end + Breakouts[index] = nil + TriggerClientEvent("pickle_prisons:stopBreakout", -1, index) + end) +end) + +RegisterNetEvent("pickle_prisons:initializePlayer", function() + local source = source + local identifier = GetIdentifier(source) + MySQL.Async.fetchAll("SELECT * FROM pickle_prisons WHERE identifier=@identifier;", {["@identifier"] = identifier}, function(results) + local result = results[1] + if result then + local time = result.time + if Config.ServeTimeOffline then + time = (os.time() - result.sentence_date) + end + Prisoners[source] = { + identifier = result.identifier, + index = result.prison, + time = time, + inventory = json.decode(result.inventory), + sentence_date = result.sentence_date, + } + if time <= 0 then + return UnjailPlayer(source) + end + SetPlayerMetadata(source, "injail", time) + TriggerClientEvent("pickle_prisons:jailPlayer", source, Prisoners[source]) + end + end) + UpdateLootables(source) + TriggerClientEvent("pickle_prisons:setupInventory", source, {items = Inventory.Items}) +end) + +RegisterNetEvent("pickle_prisons:breakout", function() + local source = source + UnjailPlayer(source, true) + ShowNotification(source, _L("breakout_self")) +end) + +RegisterNetEvent("pickle_prisons:jailPlayer", function(target, time, index) + local source = source + JailEvent(source, target, time, index) +end) + +RegisterNetEvent("pickle_prisons:unjailPlayer", function(target) + local source = source + JailEvent(source, target) +end) + +AddEventHandler("playerDropped", function() + local source = source + local identifier = GetIdentifier(source) + if not Prisoners[source] then return end + UpdatePrisonTime(source, Prisoners[source].time) + Prisoners[source] = nil +end) + +AddEventHandler('onResourceStop', function(resourceName) + if (GetCurrentResourceName() ~= resourceName) then return end + for source,v in pairs(Prisoners) do + UpdatePrisonTime(source, v.time) + end +end) + +function JailEvent(source, target, time, index) + if not target or target < 1 then return end + if not GetPlayerName(target) then return end + local prisoner = Prisoners[target] + if prisoner then + local prison = Config.Prisons[prisoner.index] + return ShowNotification(source, _L("in_prison", target, prisoner.time, prison.label)) + else + local index = index or "default" + local prison = Config.Prisons[index] + if not prison then return end + local permissions = prison.permissions or Config.Default.permissions + if not CheckPermission(source, permissions.jail) then + ShowNotification(source, _L("no_permission")) + else + ShowNotification(source, _L("in_prison", target, time, prison.label)) + JailPlayer(target, time, index) + end + end +end + +function UnjailEvent(source, target) + if not target or target < 1 then return end + local prisoner = Prisoners[target] + if not prisoner then + return ShowNotification(source, _L("not_prison", target)) + else + local index = prisoner.index + local prison = Config.Prisons[index] + local permissions = prison.permissions or Config.Default.permissions + if not CheckPermission(source, permissions.unjail) then + ShowNotification(source, _L("no_permission")) + else + UnjailPlayer(target) + end + end +end + +function StartSiren(index, time) + local prison = Config.Prisons[index] + local permissions = prison.permissions or Config.Default.permissions + local players = GetPlayers() + for i=1, #players do + if CheckPermission(players[i], permissions.alert) then + TriggerClientEvent("pickle_prisons:alert", players[i], index) + end + end + TriggerClientEvent("pickle_prisons:startSiren", -1, index) + if time then + SetTimeout(time * 1000, function() + StopSiren(index) + end) + end +end + +function StopSiren(index) + local prison = Config.Prisons[index] + local permissions = prison.permissions or Config.Default.permissions + local players = GetPlayers() + for i=1, #players do + if CheckPermission(players[i], permissions.alert) then + TriggerClientEvent("pickle_prisons:alert", players[i], index, true) + end + end + TriggerClientEvent("pickle_prisons:stopSiren", -1, index) +end + +RegisterCommand("jailstatus", function(source, args, raw) + local target = tonumber(args[1]) or source + local prisoner = Prisoners[target] + if not prisoner then + return ShowNotification(source, _L("not_prison", target)) + else + local prison = Config.Prisons[prisoner.index] + return ShowNotification(source, _L("in_prison", target, prisoner.time, prison.label)) + end +end) + +RegisterCommand("jail", function(source, args, raw) + local target = tonumber(args[1]) + local time = tonumber(args[2]) + JailEvent(source, target, time, args[3]) +end) + +RegisterCommand("unjail", function(source, args, raw) + local target = tonumber(args[1]) + UnjailEvent(source, target) +end) + +RegisterCommand("startsiren", function(source, args, raw) + local index = args[1] + if not index or not Config.Prisons[index] then return end + local prison = Config.Prisons[index] + local permissions = prison.permissions or Config.Default.permissions + if not CheckPermission(source, permissions.alert) then + ShowNotification(source, _L("no_permission")) + else + StartSiren(index) + end +end) + +RegisterCommand("stopsiren", function(source, args, raw) + local index = args[1] + if not index or not Config.Prisons[index] then return end + local prison = Config.Prisons[index] + local permissions = prison.permissions or Config.Default.permissions + if not CheckPermission(source, permissions.alert) then + ShowNotification(source, _L("no_permission")) + else + StopSiren(index) + end +end) + +RegisterCommand("jailmenu", function(source, args, raw) + local allowed = false + for k,v in pairs(Config.Prisons) do + local permissions = v.permissions or Config.Default.permissions + if CheckPermission(source, permissions.alert) then + allowed = true + end + end + if not allowed then + ShowNotification(source, _L("no_permission")) + else + TriggerClientEvent("pickle_prisons:jailDialog", source) + end +end) + +function PrisonTimer() + for source,v in pairs(Prisoners) do + Prisoners[source].time = Prisoners[source].time - 1 + if Prisoners[source].time <= 0 then + UnjailPlayer(source) + else + SetPlayerMetadata(source, "injail", Prisoners[source].time) + end + end + SetTimeout(1000 * 60, PrisonTimer) +end + +CreateThread(function() + PrisonTimer() +end) \ No newline at end of file diff --git a/modules/stores/client.lua b/modules/stores/client.lua new file mode 100644 index 0000000..4572e7b --- /dev/null +++ b/modules/stores/client.lua @@ -0,0 +1,99 @@ +StoreInteractions = {} + +function CleanupStores() + for i=1, #StoreInteractions do + DeleteInteraction(StoreInteractions[i]) + StoreInteractions[i] = nil + end +end + +function StoreSelectItem(index, itemIndex) + local prisonIndex = Prison.index + local prison = Config.Prisons[prisonIndex] + local store = prison.stores[index] + local item = store.catalog[itemIndex] + local options = {} + for i=1, #item.required do + local required = item.required[i] + local title + local description + if required.type ~= "cash" then + title = Inventory.Items[required.name].label + description = "x" .. required.amount + else + title = "$" .. required.amount + end + local option = { + title = title, + description = description + } + options[#options + 1] = option + end + options[#options + 1] = { + title = _L("confirm_transaction"), + description = _L("confirm_transaction_desc"), + onSelect = function() + TriggerServerEvent("pickle_prisons:storeTransaction", prisonIndex, index, itemIndex) + end + } + local id = 'prison_store_' .. prisonIndex .. "_" .. index .. "_" .. itemIndex + lib.registerContext({ + id = id, + title = store.label, + options = options + }) + lib.showContext(id) +end + +function DisplayStore(index) + local options = {} + local prisonIndex = Prison.index + local prison = Config.Prisons[prisonIndex] + local store = prison.stores[index] + for i=1, #store.catalog do + local item = store.catalog[i] + local description + if not item.required or #item.required > 1 or item.required[1].type ~= "cash" then + description = item.description + else + description = "$" .. item.required[1].amount .. " - " .. item.description + end + local option = { + title = Inventory.Items[item.name].label, + description = description, + onSelect = function() + StoreSelectItem(index, i) + end + } + options[#options + 1] = option + end + if #options < 1 then return end + local id = 'prison_store_' .. prisonIndex .. "_" .. index + lib.registerContext({ + id = id, + title = store.label, + options = options + }) + lib.showContext(id) +end + +RegisterNetEvent("pickle_prisons:enterPrison", function() + CleanupStores() + local index = Prison.index + local prison = Config.Prisons[index] + for i=1, #prison.stores do + local store = prison.stores[i] + StoreInteractions[i] = CreateInteraction({ + label = store.label, + model = store.model, + coords = store.coords, + heading = store.heading + }, function(selected) + DisplayStore(i) + end) + end +end) + +RegisterNetEvent("pickle_prisons:leavePrison", function() + CleanupStores() +end) \ No newline at end of file diff --git a/modules/stores/server.lua b/modules/stores/server.lua new file mode 100644 index 0000000..6be7bda --- /dev/null +++ b/modules/stores/server.lua @@ -0,0 +1,20 @@ +RegisterNetEvent("pickle_prisons:storeTransaction", function(index, storeIndex, itemIndex) + local store = Config.Prisons[index].stores[storeIndex] + local item = store.catalog[itemIndex] + local required = item.required + local rewards = item.rewards or {} + local success, missingItems = CheckRequired(source, required) + if not success then + for i=1, #missingItems do + ShowNotification(source, _L("missing_item", missingItems[i].name, missingItems[i].count)) + end + return + end + rewards[#rewards + 1] = { + type = "item", + name = item.name, + amount = item.amount + } + TakeRequired(source, required) + GiveRewards(source, rewards) +end) \ No newline at end of file diff --git a/nui/assets/js/main.js b/nui/assets/js/main.js new file mode 100644 index 0000000..579a6c6 --- /dev/null +++ b/nui/assets/js/main.js @@ -0,0 +1,39 @@ +var audio = document.createElement('audio'); +var volume = 0.5; +var endSiren = false; + +function LoopThread() { + setTimeout(function() { + audio.currentTime = 0 + if (endSiren) { + endSiren = true; + SetAudio("assets/sounds/end_siren.mp3", false); + } + else { + LoopThread(); + } + }, 1000 * 10) +} + +function SetAudio(fileName, loop) { + audio.pause(); + audio.src = fileName; + audio.load(); + audio.volume = volume; + audio.play(); + if (!loop) return; + LoopThread(); +} + +window.addEventListener("message", function(ev) { + var event = ev.data + if (event.type == "setVolume") { + audio.volume = event.value + } + else if (event.type == "endSiren") { + endSiren = true; + } + else if (event.type == "startSiren") { + SetAudio("assets/sounds/siren.mp3", true); + } +}) \ No newline at end of file diff --git a/nui/assets/sounds/end_siren.mp3 b/nui/assets/sounds/end_siren.mp3 new file mode 100644 index 0000000..f736220 Binary files /dev/null and b/nui/assets/sounds/end_siren.mp3 differ diff --git a/nui/assets/sounds/siren.mp3 b/nui/assets/sounds/siren.mp3 new file mode 100644 index 0000000..4c40f81 Binary files /dev/null and b/nui/assets/sounds/siren.mp3 differ diff --git a/nui/index.html b/nui/index.html new file mode 100644 index 0000000..52c2968 --- /dev/null +++ b/nui/index.html @@ -0,0 +1,10 @@ + + + + Pickle's Prisons + + + + + + \ No newline at end of file