local QBCore = exports['qb-core']:GetCoreObject() -- pet system local maxLimit = Config.MaxActivePetsPetPlayer -- ============================ -- Class -- ============================ Pet = { players = {} } --- search for player(source) item's hash inside Pet list function Pet:isSpawned(source, item) if self.players[source] ~= nil then for key, table in pairs(self.players[source]) do if item.info.hash == key then return true end end end return false end --- add pet to Pet table function Pet:setAsSpawned(source, o) self.players[source] = self.players[source] or {} self.players[source][o.item.info.hash] = self.players[source][o.item.info.hash] or {} self.players[source][o.item.info.hash].model = o.model self.players[source][o.item.info.hash].entity = o.entity -- memmory new data saving method self.players[source][o.item.info.hash].name = o.item.name self.players[source][o.item.info.hash].info = o.item.info return true end --- removes pet from Pet table function Pet:setAsDespawned(source, item) self.players[source] = self.players[source] or {} self.players[source][item.info.hash] = nil end --- start spawn chain function Pet:spawnPet(source, model, item) -- sumun pet when it does exist inside our database local isSpawned = Pet:isSpawned(source, item) if isSpawned == true then -- depsawn ped Pet:despawnPet(source, item, nil) return end local limit = Pet:isMaxLimitPedReached(source) if limit == true then TriggerClientEvent('QBCore:Notify', source, string.format(Lang:t('error.reached_max_allowed_pet'), maxLimit), 'error', 2500) return end local Player = QBCore.Functions.GetPlayer(source) -- spawn ped if item.weight == 500 then -- need inital values if Player.PlayerData.items[item.slot] then Player.PlayerData.items[item.slot].weight = math.random(1000, 4500) end Player.Functions.SetInventory(Player.PlayerData.items, true) end if item.info.health <= 100 and item.info.health ~= 0 then -- prevent 100 to stuck in data as health! if Player.PlayerData.items[item.slot] then Player.PlayerData.items[item.slot].info.health = 0 end Player.Functions.SetInventory(Player.PlayerData.items, true) return end local owner = not (item.info.owner.phone == Player.PlayerData.charinfo.phone) TriggerClientEvent('keep-companion:client:callCompanion', source, model, owner, item) end RegisterNetEvent('keep-companion:server:despwan_not_owned_pet', function(hash) Pet:despawnPet(source, { info = { hash = hash } }, true) end) --- check if player reached maximum allowed pet function Pet:isMaxLimitPedReached(source) local count = 0 if self.players[source] == nil then return false else for _ in pairs(self.players[source]) do count = count + 1 end if count == 0 then return false elseif count >= maxLimit then return true end end end --- despawn pet and remove it's data from server function Pet:despawnPet(source, item, revive) -- despawn pet -- save all data after despawning pet TriggerClientEvent('keep-companion:client:despawn', source, item, revive) end function Pet:findbyhash(source, hash) for key, value in pairs(self.players[source]) do if key == hash then return value end end return false end local server_saving_interval = 5000 local server_saving_interval_sec = math.floor(server_saving_interval / 1000) local day = 10 local max_age = 60 * 60 * 24 * day function Pet:save_all_info(source, hash) local Player = QBCore.Functions.GetPlayer(source) if Player == nil then return end local petData = Pet:findbyhash(source, hash) local items = Player.Functions.GetItemsByName(petData.name) local slot = nil -- find item for key, pet_item in pairs(items) do if pet_item.info.hash == hash then slot = pet_item.slot break end end if slot == nil then return end -- skip saving data if pet aleady dead if petData.info.health == 0 then return end if petData.info.health > 100 then if petData.info.age >= max_age then return end -- increase pet age when it didnt reached max age petData.info.age = petData.info.age + (server_saving_interval_sec) Update:food(petData, 'decrease') Update:thirst(petData, 'increase') else petData.info.health = 0 TriggerClientEvent('keep-companion:client:forceKill', source, hash, 'hunger') end if Player.PlayerData.items[slot] then petData.info.health = Round(petData.info.health, 2) -- round values petData.info.thirst = Round(petData.info.thirst, 2) petData.info.food = Round(petData.info.food, 2) Player.PlayerData.items[slot].info = petData.info end Player.Functions.SetInventory(Player.PlayerData.items, true) end RegisterNetEvent('keep-companion:server:setAsDespawned', function(item) if item == nil then return end Pet:setAsDespawned(source, item) end) -- ============================ -- Items -- ============================ local core_items = Config.core_items local function remove_item(src, Player, name, amount) local res = Player.Functions.RemoveItem(name, amount) if res then TriggerClientEvent('ps-inventory:client:ItemBox', src, QBCore.Shared.Items[name], "remove") end return res end -- food QBCore.Functions.CreateUseableItem(core_items.food.item_name, function(source, item) local Player = QBCore.Functions.GetPlayer(source) if Player == nil then return end TriggerClientEvent('keep-companion:client:start_feeding_animation', source) end) RegisterNetEvent('keep-companion:server:increaseFood', function(item) local Player = QBCore.Functions.GetPlayer(source) if Player == nil or item == nil then return end if not remove_item(source, Player, Config.core_items.food.item_name, 1) then TriggerClientEvent('QBCore:Notify', source, 'Kunne ikke fjernes fra dit inventar', 'error', 2500) return end local petData = Pet:findbyhash(source, item.info.hash) petData.info.food = petData.info.food + 50 TriggerClientEvent('QBCore:Notify', source, 'Dit kæledyr blev fodret! Vent lidt for at det har en effekt', 'success', 2500) end) -- change owenership QBCore.Functions.CreateUseableItem(core_items.collar.item_name, function(source, item) TriggerClientEvent('keep-companion:client:collar_process', source) end) -- rename - name tag QBCore.Functions.CreateUseableItem(core_items.nametag.item_name, function(source, item) local Player = QBCore.Functions.GetPlayer(source) if Player == nil or item == nil then return end TriggerClientEvent('keep-companion:client:rename_name_tag', source, item) end) RegisterNetEvent('keep-companion:server:rename_name_tag', function(name) local Player = QBCore.Functions.GetPlayer(source) if Player == nil then return end if not remove_item(source, Player, Config.core_items.nametag.item_name, 1) then TriggerClientEvent('QBCore:Notify', source, Lang:t('error.failed_to_remove_item_from_inventory'), 'error', 2500) return end TriggerClientEvent("keep-companion:client:rename_name_tagAction", source, name) end) -- first aid - revive QBCore.Functions.CreateUseableItem(core_items.firstaid.item_name, function(source, item) local Player = QBCore.Functions.GetPlayer(source) if Player == nil then return end end) QBCore.Functions.CreateUseableItem(core_items.groomingkit.item_name, function(source, item) local Player = QBCore.Functions.GetPlayer(source) if Player == nil then return end TriggerClientEvent('keep-companion:client:start_grooming_process', source) end) RegisterNetEvent('keep-companion:server:grooming_process', function(item) local pet_information = find_pet_model_by_item_name(item.name) local information = { pet_variation_list = PetVariation:getPedVariationsNameList(pet_information.model), pet_information = pet_information, disable = { rename = true }, type = Config.core_items.groomingkit.item_name } TriggerClientEvent('keep-companion:client:initialization_process', source, item, information) end) local function save_info_waterbottle(Player, item, amount) if Player.PlayerData.items[item.slot] then Player.PlayerData.items[item.slot].info.liter = amount end Player.Functions.SetInventory(Player.PlayerData.items, true) end local function initialize_info_waterbottle(Player, item) if Player.PlayerData.items[item.slot] then Player.PlayerData.items[item.slot].info = {} Player.PlayerData.items[item.slot].info.liter = 0 end Player.Functions.SetInventory(Player.PlayerData.items, true) end local function fillwater_bottle(source, item) local Player = QBCore.Functions.GetPlayer(source) if Player == nil then return end local max_c = Config.core_items.waterbottle.settings.max_capacity local water_bottle_refill_value = Config.core_items.waterbottle.settings.water_bottle_refill_value local amount = 0 if type(item.info) ~= "table" or (type(item.info) == "table" and item.info.liter == nil) then initialize_info_waterbottle(Player, item) TriggerClientEvent('QBCore:Notify', source, 'Vasker vandflaske!', 'primary', 2500) return end if item.info.liter == nil then -- backup initialization initialize_info_waterbottle(Player, item) TriggerClientEvent('QBCore:Notify', source, 'Vasker vandflaske!', 'primary', 2500) return end if item.info.liter > max_c then TriggerClientEvent('QBCore:Notify', source, 'Er allerede på maks kapacitet', 'error', 2500) return elseif item.info.liter == max_c then amount = max_c TriggerClientEvent('QBCore:Notify', source, 'Genopfylder allerede. Fyldt flaske har ingen effekt på kapacitet', 'error', 2500) else amount = item.info.liter + water_bottle_refill_value if amount >= max_c then amount = max_c end end if type(amount) ~= "number" then TriggerClientEvent('QBCore:Notify', source, 'Kunne ikke få mængde', 'error', 2500) return end save_info_waterbottle(Player, item, amount) TriggerClientEvent('QBCore:Notify', source, 'Flaske fyldt', 'success', 2500) end QBCore.Functions.CreateUseableItem(core_items.waterbottle.item_name, function(source, item) local Player = QBCore.Functions.GetPlayer(source) local water_bottle_refill_value = Config.core_items.waterbottle.settings.water_bottle_refill_value if Player == nil then return end if not remove_item(source, Player, 'water_bottle', water_bottle_refill_value) then local msg = Lang:t('error.not_enough_water_bottles') msg = string.format(msg, water_bottle_refill_value) TriggerClientEvent('QBCore:Notify', source, msg, 'error', 2500) return end TriggerClientEvent('keep-companion:client:filling_animation', source, item) end) RegisterNetEvent('keep-companion:server:filling_event', function(item) fillwater_bottle(source, item) end) QBCore.Functions.CreateCallback('keep-companion:server:decrease_thirst', function(source, cb, data) local player = QBCore.Functions.GetPlayer(source) local pet_water_bottle = player.Functions.GetItemByName(Config.core_items.waterbottle.item_name) if pet_water_bottle.info == nil then TriggerClientEvent('QBCore:Notify', source, 'Du bør vaske falsken først!', 'error', 2500) print('issue with nill info: https://github.com/swkeep/keep-companion/issues/25') return end if pet_water_bottle.info.liter == nil then TriggerClientEvent('QBCore:Notify', source, 'Du bør vaske falsken først!', 'error', 2500) print('maybe use your water bottle when there is some water_bottle s in your inventory') print('developer: issue with nill info -> liter: https://github.com/swkeep/keep-companion/issues/25') return end pet_water_bottle.info.liter = pet_water_bottle.info.liter - Config.core_items.waterbottle.settings.water_bottle_refill_value if pet_water_bottle.info.liter < 0 then TriggerClientEvent('QBCore:Notify', source, Lang:t('error.not_enough_water_in_your_bottle'), 'error', 2500) return end local petData = Pet:findbyhash(source, data.info.hash) local t_r_p_d = Config.core_items.waterbottle.settings.thirst_reduction_per_drinking if not pet_water_bottle then cb(false) return end if petData.info.thirst < 0 then petData.info.thirst = 0 end if petData.info.thirst <= t_r_p_d then petData.info.thirst = 0 else petData.info.thirst = petData.info.thirst - t_r_p_d end TriggerClientEvent('QBCore:Notify', source, Lang:t('success.successful_drinking'), 'success', 2500) save_info_waterbottle(player, pet_water_bottle, pet_water_bottle.info.liter) end) RegisterNetEvent('keep-companion:server:revivePet', function(item, process_type) local src = source local Player = QBCore.Functions.GetPlayer(src) local petData = Pet:findbyhash(source, item.itemData.info.hash) local heal_amount = Config.core_items.firstaid.settings.heal_amount local revive_bonuses = Config.core_items.firstaid.settings.revive_heal_bonuses local pet_maxHealth = getMaxHealth(item.model) local potential_heal_amount = math.floor(pet_maxHealth * (heal_amount / 100)) local msg = '' if not petData then TriggerClientEvent('QBCore:Notify', src, Lang:t('error.failed_to_start_procces') .. process_type, 'primary', 2500) return end if petData.info.health >= pet_maxHealth then -- pet has more than life more than correct max life rewrite wrong value petData.info.health = pet_maxHealth TriggerClientEvent('QBCore:Notify', src, Lang:t('info.full_life_pet'), 'primary', 2500) return end if not remove_item(source, Player, Config.core_items.firstaid.item_name, 1) then TriggerClientEvent('QBCore:Notify', src, 'Kunne ikke fjernes fra inventar', 'error', 2500) return end if process_type and process_type == 'Heal' then local res = math.floor(petData.info.health + potential_heal_amount) petData.info.health = res if petData.info.health >= pet_maxHealth then petData.info.health = pet_maxHealth end Pet:save_all_info(src, item.itemData.info.hash) msg = Lang:t('success.healing_was_successful') msg = string.format(msg, petData.info.health, pet_maxHealth) TriggerClientEvent('keep-companion:client:update_health_value', src, item, petData.info.health) TriggerClientEvent('QBCore:Notify', src, msg, 'success', 2500) return end petData.info.health = 100 + revive_bonuses Pet:save_all_info(src, item.itemData.info.hash) -- save pet's data Pet:despawnPet(src, petData, true) -- despawn dead pet msg = Lang:t('success.successful_revive') msg = string.format(msg, item.itemData.info.name) TriggerClientEvent('QBCore:Notify', src, msg, 'success', 2500) end) --- get pet max health from confing file by it's model function getMaxHealth(model) for key, value in pairs(Config.pets) do if value.model == model then return value.maxHealth end end end -- all pets for key, value in pairs(Config.pets) do QBCore.Functions.CreateUseableItem(value.name, function(source, item) if item.name ~= value.name then return end local model = value.model -- need inital values if type(item.info) ~= "table" or (type(item.info) == "table" and item.info.hash == nil) then -- init companion initItem(source, item) TriggerClientEvent('QBCore:Notify', source, Lang:t('success.pet_initialization_was_successful'), 'success', 2500) return end local cooldown = PlayersCooldown:isOnCooldown(source) if cooldown > 0 then local msg = Lang:t('info.still_on_cooldown') msg = string.format(msg, (cooldown / 1000)) TriggerClientEvent('QBCore:Notify', source, msg, 'primary', 2500) return end Pet:spawnPet(source, model, item) end) end local function search_inventory(cid) local Player = QBCore.Functions.GetPlayer(cid) if not Player then return false end local count = 0 for k, illegal_item in pairs(Config.k9.illegal_items) do local item = Player.Functions.GetItemByName(illegal_item) if item then count = count + 1 if count > 0 then return true end end end return false end QBCore.Functions.CreateCallback('keep-companion:server:search_inventory', function(source, cb, cid) local res = search_inventory(cid) if not res then TriggerClientEvent('QBCore:Notify', source, 'K9 fandt intet!', 'error', 2500) cb(res) return end cb(res) end) local function search_vehicle(Type, plate) local illegal_items = Config.k9.illegal_items local items_list = nil if Type == 1 then items_list = exports[Config.inventory_name]:getGloveboxes(plate) elseif Type == 2 then items_list = exports[Config.inventory_name]:getTruck(plate) end if items_list then for key, item in pairs(items_list.items) do for k, i_name in pairs(illegal_items) do if item.name == i_name then return true end end end end return false end QBCore.Functions.CreateCallback('keep-companion:server:search_vehicle', function(source, cb, data) local res = search_vehicle(data.key, data.plate) if not res then TriggerClientEvent('QBCore:Notify', source, 'K9 fandt intet!', 'error', 2500) cb(res) return end TriggerClientEvent('QBCore:Notify', source, 'K9 fandt noget!', 'success', 2500) cb(res) end) -- ================================================ -- Item - Updating Information -- ================================================ function FindWhereIsItem(Player, item, source) if Player.PlayerData.items == nil or next(Player.PlayerData.items) == nil then TriggerClientEvent('QBCore:Notify', source, "Ingen ting i inventar!") return false end for k, v in pairs(Player.PlayerData.items) do if Player.PlayerData.items[k] ~= nil then if Player.PlayerData.items[k].info.hash == item.hash then local slot = Player.PlayerData.items[k].slot return Player.PlayerData.items[slot] end end end TriggerClientEvent('QBCore:Notify', source, "Kunne ikke finde kæledyr") return false end RegisterNetEvent('keep-companion:server:updateAllowedInfo', function(item, data) if type(data) ~= "table" or next(data) == nil then return end local Player = QBCore.Functions.GetPlayer(source) local current_pet_data = Pet:findbyhash(source, item.hash) if current_pet_data == nil or current_pet_data == false then return end local requestedItem = Player.Functions.GetItemsByName(current_pet_data.name) if type(requestedItem) == "table" then for key, pet_item in pairs(requestedItem) do if pet_item.info.hash == item.hash then requestedItem = pet_item end end if requestedItem == false then return end end if data.key == 'XP' then Update:xp(source, current_pet_data) return end Update:health(source, data, current_pet_data) end) QBCore.Functions.CreateCallback('keep-companion:server:renamePet', function(source, cb, item) local player = QBCore.Functions.GetPlayer(source) local current_pet_data = Pet:findbyhash(source, item.hash) -- sanity check if player == nil or current_pet_data == nil or current_pet_data == false or type(item.name) ~= "string" then local msg = Lang:t('error.failed_to_rename') msg = string.format(msg, item.name) TriggerClientEvent('QBCore:Notify', source, msg, 'error') cb(false) return end if current_pet_data.info.name == item.content then local msg = Lang:t('error.failed_to_rename_same_name') msg = string.format(msg, item.name) TriggerClientEvent('QBCore:Notify', source, msg, 'error') cb(false) return end current_pet_data.info.name = item.name Pet:save_all_info(source, item.hash) -- save name outside loop -- despawn pet to save name Pet:despawnPet(source, { info = { hash = item.hash } }, true) cb(item.name) end) -- saving thread CreateThread(function() -- #TODO add check to table changes while true do for source, activePets in pairs(Pet.players) do if next(activePets) ~= nil then for hash, petData in pairs(activePets) do Pet:save_all_info(source, hash) end end end Wait(server_saving_interval) end end) QBCore.Functions.CreateCallback('keep-companion:server:updatePedData', function(source, cb, clientRes) local player = QBCore.Functions.GetPlayer(source) if player == nil then cb(false) return end if Pet:setAsSpawned(source, clientRes) then cb(true) return end cb(false) end) RegisterNetEvent('keep-companion:server:onPlayerUnload', function(items) -- save pet's Information when player logout for key, value in pairs(items) do Pet:setAsDespawned(source, value) end end) -- ============================ -- Commands -- ============================ QBCore.Commands.Add('addpet', 'Tilføj kæledyr til inventar (Admin)', {}, false, function(source, args) local PETname = args[1] local src = source local Player = QBCore.Functions.GetPlayer(src) Player.Functions.AddItem(PETname, 1) TriggerClientEvent("inventory:client:ItemBox", src, QBCore.Shared.Items[PETname], "add") end, 'admin') QBCore.Commands.Add('addItem', 'Tilføj ting til inventar (Admin)', {}, false, function(source, item) local Player = QBCore.Functions.GetPlayer(source) Player.Functions.AddItem(item[1], 1) TriggerClientEvent("inventory:client:ItemBox", source, QBCore.Shared.Items[ item[1] ], "add") end, 'admin') QBCore.Commands.Add('renamePet', 'Omdøb kæledyr', { { "navn", "ny navn" } }, false, function(source, args) TriggerClientEvent("keep-companion:client:rename_name_tag", source, args[1]) end, 'admin') -- ============================ -- Cooldown -- ============================ -- start active cooldowns Citizen.CreateThread(function() local timeToClean = 600 -- sec local count = 0 while true do Wait(1000) count = count + 1 local size = PlayersCooldown:onlinePlayers() if size > 0 then for ped, cooldown in pairs(PlayersCooldown.players) do PlayersCooldown:updateCooldown(ped) end end -- remove offline player from cooldown list if count >= timeToClean and not count == 0 then PlayersCooldown:cleanOflinePlayers() count = 0 end end end) RegisterNetEvent('keep-companion:server:ForceRemoveNetEntity', function(netId) local net = NetworkGetEntityFromNetworkId(netId) DeleteEntity(net) end)