This commit is contained in:
Hawk 2024-12-29 21:28:24 +01:00
parent 5d95c0182e
commit 7359d3963b
No known key found for this signature in database
GPG Key ID: 2890D5366F8BAC14
352 changed files with 75699 additions and 0 deletions

View File

@ -0,0 +1,22 @@
import { writable } from "svelte/store";
export const ALL_ACTIONS = writable<boolean>(true);
export const ACTION = writable<any[]>(null);
interface DROPDOWN_DATA {
label: string;
type: string;
option?: string;
data?: any[];
event?: string;
}
interface ACITONS_DATA {
id: string;
label: string;
type?: string;
event?: string;
perms: string[];
dropdown?: DROPDOWN_DATA[];
}

View File

@ -0,0 +1,8 @@
import { writable } from "svelte/store";
export const VEHICLE_DATA = writable<any[]>(null);
export const ITEM_DATA = writable<any[]>(null);
export const JOB_DATA = writable<any[]>(null);
export const GANG_DATA = writable<any[]>(null);
export const LOCATION_DATA = writable<any[]>(null);
export const PED_LIST = writable<any[]>(null);

View File

@ -0,0 +1,9 @@
import { writable } from "svelte/store";
export const ENTITY_INFO = writable<ENTITY_INFO[]>(null);
interface ENTITY_INFO {
show: boolean;
name: string;
hash: string;
}

View File

@ -0,0 +1,32 @@
import { writable } from "svelte/store";
export const PLAYER_DATA = writable(null);
export const SELECTED_PLAYER = writable(null);
export const PLAYER = writable<any[]>(null);
export const PLAYER_VEHICLES = writable<PLAYER_VEHICLES[]>(null);
interface PLAYER_DATA {
id?: string;
name?: string;
cid?: string;
license?: string;
discord?: string;
steam?: string;
fivem?: string;
vehicles?: PLAYER_VEHICLES[];
}
interface PLAYER_VEHICLES {
id?: string;
cid?: string;
label?: string;
brand?: string;
model?: string;
plate?: string;
fuel?: string;
engine?: string;
body?: string;
}

View File

@ -0,0 +1,11 @@
import { writable } from "svelte/store";
export const REPORT = writable<REPORT_DATA[]>(null);
export const REPORTS = writable<REPORT_DATA>(null);
interface REPORT_DATA {
message: string,
citizenid: string,
fullname: string,
time: number,
}

View File

@ -0,0 +1,29 @@
import { writable } from "svelte/store";
export const RESOURCE = writable<RESOURCE_DATA[]>(null);
export const RESOURCES = writable<RESOURCE_DATA>(null);
export const COMMANDS = writable<COMMANDS_DATA[]>(null);
export const SERVER = writable<SERVER_DATA[]>(null);
interface RESOURCE_DATA {
name?: string;
author?: string;
version?: string;
description?: string;
resourceState?: string;
}
interface SERVER_DATA {
TotalCash?: string;
TotalBank?: string;
TotalItems?: string;
CharacterCount?: string;
VehicleCount?: string;
BansCount?: string;
UniquePlayers?: string;
}
interface COMMANDS_DATA {
name?: string;
}

View File

@ -0,0 +1,11 @@
import { writable } from 'svelte/store'
interface ChatData {
message: string
citizenid: string
fullname: string
time: number
}
export const Message = writable<ChatData[]>(null)
export const Messages = writable<ChatData>(null)

View File

@ -0,0 +1,12 @@
import { writable } from "svelte/store";
export const VISIBILITY = writable<boolean>(false);
export const BROWSER_MODE = writable<boolean>(false);
export const RESOURCE_NAME = writable<string>("");
export const DEV_MODE = writable<boolean>(false);
export const MENU_WIDE = writable<boolean>(false);
export const ACTIVE_PAGE = writable<string>("Actions");
export const searchActions = writable('');

View File

@ -0,0 +1,11 @@
import { writable } from "svelte/store";
export const TOGGLE_COORDS = writable<COORDS_DATA[]>(null);
interface COORDS_DATA {
show: boolean;
x: string;
y: string;
z: string;
heading?: string;
}

View File

@ -0,0 +1,15 @@
import { writable } from "svelte/store";
export const VEHICLE_DEV = writable<VEHICLE_DATA[]>(null);
interface VEHICLE_DATA {
show: boolean;
name: string;
model: string;
netID: string;
engine_health?: string;
body_health?: string;
plate?: string;
fuel?: string;
speed?: string;
}

View File

@ -0,0 +1,32 @@
import { onMount, onDestroy } from "svelte";
interface NuiMessage<T = unknown> {
action: string;
data: T;
}
/**
* A function that manage events listeners for receiving data from the client scripts
* @param action The specific `action` that should be listened for.
* @param handler The callback function that will handle data relayed by this function
*
* @example
* useNuiEvent<{VISIBILITY: true, wasVisible: 'something'}>('setVisible', (data) => {
* // whatever logic you want
* })
*
**/
export function ReceiveNUI<T = unknown>(
action: string,
handler: (data: T) => void
) {
const eventListener = (event: MessageEvent<NuiMessage<T>>) => {
const { action: eventAction, data } = event.data;
eventAction === action && handler(data);
};
onMount(() => window.addEventListener("message", eventListener));
onDestroy(() => window.removeEventListener("message", eventListener));
}

View File

@ -0,0 +1,43 @@
import { BROWSER_MODE, RESOURCE_NAME } from '@store/stores'
let isBrowserMode: boolean = false;
BROWSER_MODE.subscribe((value: boolean) => {
isBrowserMode = value;
});
let debugResName: string = "";
RESOURCE_NAME.subscribe((value: string) => {
debugResName = value;
});
/**
* @param eventName - The endpoint eventname to target
* @param data - Data you wish to send in the NUI Callback
*
* @return returnData - A promise for the data sent back by the NuiCallbacks CB argument
*/
export async function SendNUI<T = any>(
eventName: string,
data: unknown = {},
debugReturn?: T
): Promise<T> {
if ((isBrowserMode == true && debugReturn) || (isBrowserMode == true)) {
return Promise.resolve(debugReturn || {} as T)
}
const options = {
method: "post",
headers: {
"Content-Type": "application/json; charset=UTF-8",
},
body: JSON.stringify(data),
};
const resourceName = (window as any).GetParentResourceName
? (window as any).GetParentResourceName()
: debugResName;
const resp: Response = await fetch(`https://${resourceName}/${eventName}`, options);
return await resp.json()
}

View File

@ -0,0 +1,30 @@
import {isEnvBrowser} from "./misc";
interface DebugEvent<T = any> {
action: string;
data: T;
}
/*
* Emulates dispatching an event using SendNuiMessage in the lua scripts.
* This is used when developing in browser
*
* @param events - The event you want to cover
* @param timer - How long until it should trigger (ms)
*/
export const debugData = <P>(events: DebugEvent<P>[], timer = 0): void => {
if (isEnvBrowser()) {
for (const event of events) {
setTimeout(() => {
window.dispatchEvent(
new MessageEvent("message", {
data: {
action: event.action,
data: event.data,
},
})
);
}, timer);
}
}
};

View File

@ -0,0 +1 @@
export const isEnvBrowser = (): boolean => !(window as any).invokeNative;

View File

@ -0,0 +1,79 @@
const MONTH_NAMES = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
]
function getFormattedDate(date, prefomattedDate = false, hideYear = false) {
const day = date.getDate()
const month = MONTH_NAMES[date.getMonth()]
const year = date.getFullYear()
const hours = date.getHours()
let minutes = date.getMinutes()
if (minutes < 10) {
minutes = `0${minutes}`
}
if (prefomattedDate) {
return `${prefomattedDate} at ${hours}:${minutes}`
}
if (hideYear) {
return `${day}. ${month} at ${hours}:${minutes}`
}
return `${day}. ${month} ${year}. at ${hours}:${minutes}`
}
export function timeAgo(dateParam) {
if (!dateParam) {
return 'Unknown'
}
let date
try {
date = typeof dateParam === 'object' ? dateParam : new Date(dateParam)
} catch (e) {
return 'Invalid date'
}
if (isNaN(date)) {
return 'Invalid date'
}
const DAY_IN_MS = 86400000
const today = new Date()
const yesterday = new Date(today - DAY_IN_MS)
const seconds = Math.round((today - date) / 1000)
const minutes = Math.round(seconds / 60)
const isToday = today.toDateString() === date.toDateString()
const isYesterday = yesterday.toDateString() === date.toDateString()
const isThisYear = today.getFullYear() === date.getFullYear()
if (seconds < 5) {
return 'Just Now'
} else if (seconds < 60) {
return `${seconds} Seconds ago`
} else if (seconds < 90) {
return 'A minute ago'
} else if (minutes < 60) {
return `${minutes} Minutes ago`
} else if (isToday) {
return getFormattedDate(date, 'Today')
} else if (isYesterday) {
return getFormattedDate(date, 'Yesterday')
} else if (isThisYear) {
return getFormattedDate(date, false, true)
}
return getFormattedDate(date)
}

View File

@ -0,0 +1,86 @@
# ps-buffs
### Dependencies
***
- PS-Hud: https://github.com/Project-Sloth/ps-hud
### General Information
***
- Buffs are new effects that a player can get. Example: Stamina Buff makes a player run faster.
- They show up as a new icon
- Enhancements are making statuses you already have better. Example: Health Buff heals a player periodically
- They show up as yellow on the status they are affecting. Example: Armor buffs makes the armor icon yellow
- By default adding buffs to a player will just have the buff icon show
- You need to add logic to give the player buff effects (below you can see examples)
- Avaliable buffs you can pick from are in the config file (shared/config.lua)
### Add a buff to a player
```lua
-- Function signature - buffName: string, time: int (1 second = 1000)
exports['ps-buffs']:AddBuff(buffName, time)
-- Example -- Adds a hacking buff for 15 seconds, which the player would see a hacking buff icon on their screen
exports['ps-buffs']:AddBuff("hacking", 15000)
```
### Check if player has a buff
```lua
-- Function signature - buffName: string
exports['ps-buffs']:HasBuff(buffName)
-- Example -- Check if a player has the hacking buff and make it easier to hack something
if exports['ps-buffs']:HasBuff("hacking") then
-- give player more time or less complicated puzzle
end
```
### Buff Effects
***
- We currently have the following buff effects implemented that you can call:
- Stamina Buff - Makes a player run faster and generate a random partial amount of stamina
```lua
-- Function signature - time: int (1 second = 1000), value: double (float)
exports['ps-buffs']:StaminaBuffEffect(time, value)
-- Example -- Adds a stamina buff for 15 seconds and a player runs 1.4 faster.
exports['ps-buffs']:StaminaBuffEffect(15000, 1.4)
```
- Swimming Buff - Makes a player swim faster and generate a random partial amount of stamina
```lua
-- Function signature - time: int (1 second = 1000), value: double (float)
exports['ps-buffs']:SwimmingBuffEffect(time, value)
-- Example -- Adds a swimming buff for 20 seconds and a player swims 1.4 faster.
exports['ps-buffs']:SwimmingBuffEffect(20000, 1.4)
```
- Health Buff - Makes a player's health partially regenerate periodically
```lua
-- Function signature - time: int (1 second = 1000), value: int
exports['ps-buffs']:AddHealthBuff(time, value)
-- Example -- Adds a health buff for 10 seconds and a player periodically gains 10 health.
exports['ps-buffs']:AddHealthBuff(10000, 10)
```
- Armor Buff - Makes a player's armor partially regenerate periodically
```lua
-- Function signature - time: int (1 second = 1000), value: int
exports['ps-buffs']:AddArmorBuff(time, value)
-- Example -- Adds a armor buff for 30 seconds and a player periodically gains 10 armor.
exports['ps-buffs']:AddArmorBuff(30000, 10)
```
- Stress Buff - Makes a player's stress partially decrease periodically
```lua
-- Function signature - time: int (1 second = 1000), value: int
exports['ps-buffs']:AddStressBuff(time, value)
-- Example -- Removes stress for 30 seconds and removes 10 units every 5 seconds
exports['ps-buffs']:AddStressBuff(30000, 10)
```
## Credits
- The majority of the lua code comes from [qb-enhancements](https://github.com/IdrisDose/qb-enhancements) by [IdrisDose](https://github.com/IdrisDose)
- Credits to my boys Silent, Snipe and fjamzoo for help with getting things in place for this to all be possible

View File

@ -0,0 +1,217 @@
local QBCore = exports['qb-core']:GetCoreObject()
local function GetBuffs()
-- If making multiple calls to GetBuffs we dont get a result of the 2nd call
-- The wait seems to fix the issue
Wait(500)
local p = promise.new()
QBCore.Functions.TriggerCallback('buffs:server:fetchBuffs', function(result)
p:resolve(result)
end)
return Citizen.Await(p)
end
--- Method to fetch if player has buff with name and is not nil
--- @param buffName string - Name of the buff
--- @return bool
local function HasBuff(buffName)
local buffs = GetBuffs()
if buffs then
return buffs[buffName] ~= nil
end
return false
end exports('HasBuff', HasBuff)
--- Method to fetch buff details if player has buff active
--- @param buffName string - Name of the buff
--- @return table
local function GetBuff(buffName)
local buffData = Config.Buffs[buffName]
-- Check if we were given a correct buff name
if buffData == nil then
return nil
end
local buffs = GetBuffs()
local time = nil
if buffs then
time = buffs[buffName]
end
if time == nil then
time = 0
end
if buffData.type == 'buff' then
return {
time = time,
buffName = buffName,
iconName = buffData.iconName,
iconColor = buffData.iconColor,
progressColor = buffData.progressColor,
progressValue = (time * 100) / buffData.maxTime,
type = buffData.type,
}
else
return {
time = time,
iconColor = buffData.iconColor,
maxTime = buffData.maxTime,
type = buffData.type,
}
end
end exports('GetBuff', GetBuff)
--- Method to fetch nui details of all buffs, used when a player that had buffs
--- logged out and back in to the server
--- @return table | nil
local function GetBuffNUIData()
local buffs = GetBuffs()
if buffs == nil then
return nil
end
local nuiData = {}
for buffName, buffTime in pairs(buffs) do
local buffData = Config.Buffs[buffName]
if buffData.type == 'buff' then
nuiData[buffName] = {
buffName = buffName,
display = true,
iconName = buffData.iconName,
iconColor = buffData.iconColor,
progressColor = buffData.progressColor,
progressValue = (buffTime * 100) / buffData.maxTime,
}
else
nuiData[buffName] = {
display = true,
enhancementName = buffName,
iconColor = buffData.iconColor
}
end
end
return nuiData
end exports('GetBuffNUIData', GetBuffNUIData)
--- Method to add buff to player
--- @param playerID string - Player identifier
--- @param buffName string - Name of the buff
--- @return bool - Success of removing the player buff
local function AddBuff(buffName, time)
local p = promise.new()
QBCore.Functions.TriggerCallback('buffs:server:addBuff', function(result)
p:resolve(result)
end, buffName, time)
return Citizen.Await(p)
end exports('AddBuff', AddBuff)
--- Custom Buffs, edit to your liking ---
--- Method to add stamina buff to player
--- @param time - Time in ms the health buff will be active
--- @param value - The amount of speed boost the player will recieve
local hasStaminaBuffActive = false
local function StaminaBuffEffect(time, value)
AddBuff("stamina", time)
if not hasStaminaBuffActive then
hasStaminaBuffActive = true
CreateThread(function()
SetRunSprintMultiplierForPlayer(PlayerId(), value)
while exports['ps-buffs']:HasBuff("stamina") do
Wait(500)
SetPlayerStamina(PlayerId(), GetPlayerStamina(PlayerId()) + math.random(1,10))
end
SetRunSprintMultiplierForPlayer(PlayerId(), 1.0)
hasStaminaBuffActive = false
end)
end
end exports('StaminaBuffEffect', StaminaBuffEffect)
--- Method to add swimming buff to player
--- @param time - Time in ms the health buff will be active
--- @param value - The amount of swimming speed boost the player will recieve
local hasSwimmingBuffActive = false
local function SwimmingBuffEffect(time, value)
AddBuff("swimming", time)
if not hasSwimmingBuffActive then
hasSwimmingBuffActive = true
CreateThread(function()
SetSwimMultiplierForPlayer(PlayerId(), value)
while exports['ps-buffs']:HasBuff("swimming") do
Wait(500)
SetPlayerStamina(PlayerId(), GetPlayerStamina(PlayerId()) + math.random(1,10))
end
SetSwimMultiplierForPlayer(PlayerId(), 1.0)
hasSwimmingBuffActive = false
end)
end
end exports('SwimmingBuffEffect', SwimmingBuffEffect)
--- Method to add health buff to player
--- @param time - Time in ms the health buff will be active
--- @param value - The amount of HP the player will gain over time
local hasHealthBuffActive = false
local function AddHealthBuff(time, value)
AddBuff("super-health", time)
if not hasHealthBuffActive then
hasHealthBuffActive = true
CreateThread(function()
while HasBuff("super-health") do
Wait(5000)
if GetEntityHealth(PlayerPedId()) < 200 then
SetEntityHealth(PlayerPedId(), GetEntityHealth(PlayerPedId()) + value)
end
end
hasHealthBuffActive = false
end)
end
end exports('AddHealthBuff', AddHealthBuff)
--- Method to add armor buff to player
--- @param time - Time in ms the health buff will be active
--- @param value - The amount of Armor the player will gain over time
local hasArmorBuffActive = false
local function AddArmorBuff(time, value)
AddBuff("super-armor", time)
if not hasArmorBuffActive then
hasArmorBuffActive = true
CreateThread(function()
while HasBuff("super-armor") do
Wait(5000)
if GetPedArmour(PlayerPedId()) < 100 then
SetPedArmour(PlayerPedId(), GetPedArmour(PlayerPedId()) + value)
end
end
hasArmorBuffActive = false
end)
end
end exports('AddArmorBuff', AddArmorBuff)
--- Method to add stress buff to player
--- @param time - Time in ms the stress buff will be active
--- @param value - The amount of stress the player will lose every 5 seconds
local hasStressBuffActive = false
local function AddStressBuff(time, value)
AddBuff("super-stress", time)
if not hasStressBuffActive then
hasStressBuffActive = true
CreateThread(function()
while HasBuff("super-stress") do
Wait(5000)
TriggerServerEvent("hud:server:RelieveStress", value)
end
hasStressBuffActive = false
end)
end
end exports('AddStressBuff', AddStressBuff)

View File

@ -0,0 +1,26 @@
fx_version 'cerulean'
game 'gta5'
name "ps-buffs"
description "Buff tracker for qbcore"
author "Idris"
version "0.0.1"
lua54 'yes'
use_fxv2_oal 'yes'
shared_scripts {
'shared/config.lua',
}
client_scripts {
'client/*.lua'
}
server_scripts {
'server/*.lua'
}
dependencies {
'qb-core'
}

View File

@ -0,0 +1,194 @@
local QBCore = exports['qb-core']:GetCoreObject()
local playerBuffs = {}
local next = next
--- Adds a buff to player
--- @param citizenID string - Player identifier
--- @param buffName string - Name of the buff
--- @param time number | nil - Optional time to add or how long you want buff to be
--- @return bool
local function AddBuff(sourceID, citizenID, buffName, time)
local buffData = Config.Buffs[buffName]
-- Check if we were given a correct buff name
if buffData == nil then
return false
end
-- If the player had no buffs at all then add them to the table
if not playerBuffs[citizenID] then
playerBuffs[citizenID] = {}
end
local maxTime = buffData.maxTime
-- If the player didnt already have the buff requested set it to time or maxTime
if not playerBuffs[citizenID][buffName] then
local buffTime = maxTime
if time then
buffTime = time
end
playerBuffs[citizenID][buffName] = buffTime
-- Since the player didnt already have this buff tell the front end to show it
if buffData.type == 'buff' then
-- Call client event to send nui to front end to start showing buff
TriggerClientEvent('hud:client:BuffEffect', sourceID, {
buffName = buffName,
display = true,
iconName = buffData.iconName,
iconColor = buffData.iconColor,
progressColor = buffData.progressColor,
progressValue = (buffTime * 100) / buffData.maxTime,
})
else
-- Call client event to send nui to front end to start showing enhancement
TriggerClientEvent('hud:client:EnhancementEffect', sourceID, {
display = true,
enhancementName = buffName,
iconColor = buffData.iconColor
})
end
else
-- Since the player already had a buff increase the buff time, but not higher than max buff time
local newTime = playerBuffs[citizenID][buffName] + time
if newTime > maxTime then
newTime = maxTime
end
playerBuffs[citizenID][buffName] = newTime
end
return true
end exports('AddBuff', AddBuff)
--- Removes a buff from provided player
--- @param citizenID string - Player identifier
--- @param buffName string - Name of the buff
--- @return bool - Success of removing the player buff
local function Removebuff(citizenID, buffName)
local buffData = Config.Buffs[buffName]
if playerBuffs[citizenID] and playerBuffs[citizenID][buffName] then
playerBuffs[citizenID][buffName] = nil
local player = QBCore.Functions.GetPlayerByCitizenId(citizenID)
local sourceID = nil
if player then
sourceID = player.PlayerData.source
end
-- Check if player is online
if sourceID then
-- Send a nui call to front end to stop showing icon
if buffData.type == 'buff' then
-- Call client event to send nui to front end to stop showing buff
TriggerClientEvent('hud:client:BuffEffect', sourceID, {
display = false,
buffName = buffName,
})
else
-- Call client event to send nui to front end to stop showing enhancement
TriggerClientEvent('hud:client:EnhancementEffect', sourceID, {
display = false,
enhancementName = buffName,
})
end
end
-- Check to see if that was the player's last buff
-- If so, then remove the player from the table to ensure we dont loop them
if next(playerBuffs[citizenID]) == nil then
playerBuffs[citizenID] = nil
end
return true
end
return false
end exports('RemoveBuff', RemoveBuff)
--- Method to fetch if player has buff with name and is not nil
--- @param citizenID string - Player identifier
--- @param buffName string - Name of the buff
--- @return bool
local function HasBuff(citizenID, buffName)
if playerBuffs[citizenID] then
return playerBuffs[citizenID][buffName] ~= nil
end
return false
end exports('HasBuff', HasBuff)
QBCore.Functions.CreateCallback('buffs:server:fetchBuffs', function(source, cb)
local player = QBCore.Functions.GetPlayer(source)
local citizenID = player.PlayerData.citizenid
cb(playerBuffs[citizenID])
end)
QBCore.Functions.CreateCallback('buffs:server:addBuff', function(source, cb, buffName, time)
local player = QBCore.Functions.GetPlayer(source)
local citizenID = player.PlayerData.citizenid
cb(AddBuff(source, citizenID, buffName, time))
end)
CreateThread(function()
local function DecrementBuff(sourceID, citizenID, buffName, currentTime)
local buffData = Config.Buffs[buffName]
local updatedTime = currentTime - Config.TickTime
-- Buff ran out of time we need to remove it from the player
if updatedTime <= 0 then
-- Only need to update buffs since they show progress on client
if buffData.type == 'buff' then
-- Check if player is online
if sourceID then
-- Call client event to send nui to front end, progress at 0
TriggerClientEvent('hud:client:BuffEffect', sourceID, {
buffName = buffName,
progressValue = 0,
})
end
end
Removebuff(citizenID, buffName)
else
playerBuffs[citizenID][buffName] = updatedTime
-- Only need to update buffs since they show progress on client
if buffData.type == 'buff' then
-- Check if player is online
if sourceID then
-- Call client event to send nui to front end
TriggerClientEvent('hud:client:BuffEffect', sourceID, {
buffName = buffName,
-- Progress value needs to be from 0 - 100
progressValue = (updatedTime * 100) / buffData.maxTime,
})
end
end
end
end
-- Not proud but need to loop through all timers but decrement it
-- Loop is good as long as we only loop players that have buffs
-- We ensure that when removing any buff, we check to see if it was the player's last buff
-- Then we remove that player from our table to ensure we dont loop them
while true do
for citizenID, buffTable in pairs(playerBuffs) do
local player = QBCore.Functions.GetPlayerByCitizenId(citizenID)
local sourceID = nil
if player then
sourceID = player.PlayerData.source
end
for buffName, currentTime in pairs(buffTable) do
DecrementBuff(sourceID, citizenID, buffName, currentTime)
end
end
Wait(Config.TickTime)
end
end)

View File

@ -0,0 +1,86 @@
Config = Config or {}
-- How fast should the main loop over player buffs run and also how fast to send nui messages to the client
Config.TickTime = 6000
-- "biohazard"
-- "exclamation"
-- "locationarrow"
-- "#FFD700" is gold
-- '#FDE829' is yellow
Config.Buffs = {
-- Buff Section -- Do not change type, even if you know what you are doing
-- You are free to change the index names, but other scripts that relie on this config might need it
['hacking'] = {
iconColor = "#ffffff",
iconName = 'database',
maxTime = 3600000,
progressColor = "#FFD700",
type = 'buff'
},
['intelligence'] = {
iconColor = "#ffffff",
iconName = 'lightbulb',
maxTime = 3600000,
progressColor = "#FFD700",
type = 'buff'
},
['luck'] = {
iconColor = "#ffffff",
iconName = 'dollarsign',
maxTime = 3600000,
progressColor = "#FFD700",
type = 'buff'
},
['stamina'] = {
iconColor = "#ffffff",
iconName = 'wind',
maxTime = 3600000,
progressColor = "#FFD700",
type = 'buff'
},
['strength'] = {
iconColor = "#ffffff",
iconName = 'dumbbell',
maxTime = 3600000,
progressColor = "#FFD700",
type = 'buff'
},
['swimming'] = {
iconColor = "#ffffff",
iconName = 'swimmer',
maxTime = 3600000,
progressColor = "#FFD700",
type = 'buff'
},
-- Enhancement Section -- Do not change type, even if you know what you are doing
-- Do not change the index names (super names) as they are used in the front end to determine which icon to change color
['super-hunger'] = {
maxTime = 3600000,
iconColor = '#FDE829',
type = 'enhancement',
},
['super-thirst'] = {
maxTime = 3600000,
iconColor = '#FDE829',
type = 'enhancement',
},
['super-health'] = {
maxTime = 3600000,
iconColor = '#FDE829',
type = 'enhancement',
},
['super-armor'] = {
maxTime = 3600000,
iconColor = '#FDE829',
type = 'enhancement',
},
['super-stress'] = {
maxTime = 3600000,
iconColor = '#FDE829',
type = 'enhancement',
}
}

View File

@ -0,0 +1,202 @@
CREDITS: SNIPE OP GAMING AND FJAMZOO
--SOME SNIPPETS TO GO INTO PS-BUFFS/CLIENT LUA------------------------------------------------------------------------------------------------------------------------------------------------------------------
HEALTH AND ARMOR----
--- Method to add health buff to player
--- @param time - Time in ms the health buff will be active
--- @param value - The amount of HP the player will gain over time
local hasHealthBuffActive = false
local function AddHealthBuff(time, value)
AddBuff("super-health", time)
if not hasHealthBuffActive then
hasHealthBuffActive = true
CreateThread(function()
while HasBuff("super-health") do
Wait(5000)
if GetEntityHealth(PlayerPedId()) < 200 then
SetEntityHealth(PlayerPedId(), GetEntityHealth(PlayerPedId()) + value)
end
end
hasHealthBuffActive = false
end)
end
end exports('AddHealthBuff', AddHealthBuff)
--- Method to add armor buff to player
--- @param time - Time in ms the health buff will be active
--- @param value - The amount of Armor the player will gain over time
local hasArmorBuffActive = false
local function AddArmorBuff(time, value)
AddBuff("super-armor", time)
if not hasArmorBuffActive then
hasArmorBuffActive = true
CreateThread(function()
while HasBuff("super-armor") do
Wait(5000)
if GetPedArmour(PlayerPedId()) < 100 then
SetPedArmour(PlayerPedId(), GetPedArmour(PlayerPedId()) + value)
end
end
hasArmorBuffActive = false
end)
end
end exports('AddArmorBuff', AddArmorBuff)
RegisterCommand("stambuff", function()
AddHealthBuff(50000, math.random(1,5))
Wait(1000)
AddArmorBuff(50000, math.random(1,5))
end)
exports["ps-buffs"]:AddHealthBuff(time in ms, buff amoount)
exports["ps-buffs"]:AddArmorBuff(time in ms, buff amoount)
STAMINA BUFF--
--- Method to add stamina buff to player
--- @param time - Time in ms the health buff will be active
--- @param value - The amount of speed boost the player will recieve
local hasStaminaBuffActive = false
local function StaminaBuffEffect(time, value)
AddBuff("stamina", time)
if not hasStaminaBuffActive then
hasStaminaBuffActive = true
CreateThread(function()
SetRunSprintMultiplierForPlayer(PlayerId(), value)
while exports['ps-buffs']:HasBuff("stamina") do
Wait(500)
SetPlayerStamina(PlayerId(), GetPlayerStamina(PlayerId()) + math.random(1,10))
end
SetRunSprintMultiplierForPlayer(PlayerId(), 1.0)
hasStaminaBuffActive = false
end)
end
end exports('StaminaBuffEffect', StaminaBuffEffect)
exports["ps-buffs"]:StaminaBuffEffect(time in ms, buff amoount)
Stamina buff with values like time and buff amount
SWIMMING BUFF
--- Method to add swimming buff to player
--- @param time - Time in ms the health buff will be active
--- @param value - The amount of swimming speed boost the player will recieve
local hasSwimmingBuffActive = false
local function SwimmingBuffEffect(time, value)
AddBuff("swimming", time)
if not hasSwimmingBuffActive then
hasSwimmingBuffActive = true
CreateThread(function()
SetSwimMultiplierForPlayer(PlayerId(), value)
while exports['ps-buffs']:HasBuff("swimming") do
Wait(500)
SetPlayerStamina(PlayerId(), GetPlayerStamina(PlayerId()) + math.random(1,10))
end
SetSwimMultiplierForPlayer(PlayerId(), 1.0)
hasSwimmingBuffActive = false
end)
end
end exports('SwimmingBuffEffect', SwimmingBuffEffect)
exports["ps-buffs"]:SwimmingBuffEffect(time in ms, buff ammount)
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Hunger and Thirst Buff!!
Notes. You need to replace the following snippets in core. Do not ADD
If you add this block of code instead of replacing, you will get error In server console with hungerRate is nil or thirstRate is nil!
--------------qb-core/client/loops.lua---------------------------------------------------------------------------
CreateThread(function()
while true do
local sleep = 0
if LocalPlayer.state.isLoggedIn then
sleep = (1000 * 60) * QBCore.Config.UpdateInterval
local hungerRate = 0
local thirstRate = 0
if exports["ps-buffs"]:HasBuff("super-hunger") then hungerRate = QBCore.Config.Player.HungerRate/2 else hungerRate = QBCore.Config.Player.HungerRate end
if exports["ps-buffs"]:HasBuff("super-thirst") then thirstRate = QBCore.Config.Player.ThirstRate/2 else thirstRate = QBCore.Config.Player.ThirstRate end
TriggerServerEvent('QBCore:UpdatePlayer', hungerRate, thirstRate)
end
Wait(sleep)
end
end)
--
--
--
--
--
--
--
qb-core/server/events.lua - Replace the event with this new event--------------------------------------------------------
RegisterNetEvent('QBCore:UpdatePlayer', function(hungerRate, thirstRate)
print('Updating Player', hungerRate, thirstRate)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local newHunger = Player.PlayerData.metadata['hunger'] - hungerRate
local newThirst = Player.PlayerData.metadata['thirst'] - thirstRate
if newHunger <= 0 then
newHunger = 0
end
if newThirst <= 0 then
newThirst = 0
end
Player.Functions.SetMetaData('thirst', newThirst)
Player.Functions.SetMetaData('hunger', newHunger)
TriggerClientEvent('hud:client:UpdateNeeds', src, newHunger, newThirst)
Player.Functions.Save()
end)
--
--
--
--
--
--
--
Replace this in qb-core/client/events.lua---------------------------------------------------------------------------------------------
RegisterNetEvent('QBCore:Player:UpdatePlayerData', function()
local hungerRate = 0
local thirstRate = 0
if exports["ps-buffs"]:HasBuff("super-hunger") then hungerRate = QBCore.Config.Player.HungerRate/2 else hungerRate = QBCore.Config.Player.HungerRate end
if exports["ps-buffs"]:HasBuff("super-thirst") then thirstRate = QBCore.Config.Player.ThirstRate/2 else thirstRate = QBCore.Config.Player.ThirstRate end
TriggerServerEvent('QBCore:UpdatePlayer', hungerRate, thirstRate)
end)
--
--
--
--
--
--
--

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Wild Development
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,13 @@
# ps-dispatch
[Discord Server](https://discord.gg/wilddevelopment)
## Wild Development Presents
Shot Fire
![image1](https://cdn.discordapp.com/attachments/967509707340263515/1047809862115020820/image.png)
İn Vehicle
![image2](https://cdn.discordapp.com/attachments/967509707340263515/1047809934445772820/image.png)
Officer Down Preview İn PD Job
![image3](https://cdn.discordapp.com/attachments/967509707340263515/1047810088989110312/image.png)
Officer Down Preview İn EMS Job
![image4](https://cdn.discordapp.com/attachments/967509707340263515/1047810170786431056/image.png)

View File

@ -0,0 +1,239 @@
local playAnim = false
local phoneProp = 0
local phoneModel = Config.PhoneModel
-- Item checks to return whether or not the client has a phone or not
local function HasPhone()
return QBCore.Functions.HasItem("phone")
end
-- Loads the animdict so we can execute it on the ped
local function loadAnimDict(dict)
RequestAnimDict(dict)
while not HasAnimDictLoaded(dict) do
Wait(0)
end
end
local function DeletePhone()
if phoneProp ~= 0 then
DeleteObject(phoneProp)
phoneProp = 0
end
end
local function NewPropWhoDis()
DeletePhone()
RequestModel(phoneModel)
while not HasModelLoaded(phoneModel) do
Wait(1)
end
phoneProp = CreateObject(phoneModel, 1.0, 1.0, 1.0, 1, 1, 0)
local bone = GetPedBoneIndex(PlayerPedId(), 28422)
if phoneModel == Config.PhoneModel then
AttachEntityToEntity(phoneProp, PlayerPedId(), bone, 0.0, 0.0, 0.0, 15.0, 0.0, 0.0, 1, 1, 0, 0, 2, 1)
else
AttachEntityToEntity(phoneProp, PlayerPedId(), bone, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1, 1, 0, 0, 2, 1)
end
end
-- Does the actual animation of the animation when calling 911
local function PhoneCallAnim()
loadAnimDict("cellphone@")
local ped = PlayerPedId()
CreateThread(function()
NewPropWhoDis()
playAnim = true
while playAnim do
if not IsEntityPlayingAnim(ped, "cellphone@", 'cellphone_text_to_call', 3) then
TaskPlayAnim(ped, "cellphone@", 'cellphone_text_to_call', 3.0, 3.0, -1, 50, 0, false, false, false)
end
Wait(100)
end
end)
end
-- Regular 911 call that goes straight to the Police
RegisterCommand('114', function(source, args, rawCommand)
local msg = rawCommand:sub(5)
if string.len(msg) > 0 then
if not exports['qb-policejob']:IsHandcuffed() then
if HasPhone() then
PhoneCallAnim()
Wait(math.random(3,8) * 1000)
playAnim = false
local plyData = QBCore.Functions.GetPlayerData()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify",{
dispatchcodename = "114call", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "114",
firstStreet = locationInfo,
priority = 2, -- priority
name = plyData.charinfo.firstname:sub(1,1):upper()..plyData.charinfo.firstname:sub(2).. " ".. plyData.charinfo.lastname:sub(1,1):upper()..plyData.charinfo.lastname:sub(2),
number = plyData.charinfo.phone,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = "Ingående opkald", -- message
information = msg,
job = {"police", "ambulance"} -- jobs that will get the alerts
})
Wait(1000)
DeletePhone()
StopEntityAnim(PlayerPedId(), 'cellphone_text_to_call', "cellphone@", 3)
else
QBCore.Functions.Notify("Du kan ikke ringe uden en telefon!", "error", 4500)
end
else
QBCore.Functions.Notify("Du kan ikke ringe når du er i håndjern..", "error", 4500)
end
else
QBCore.Functions.Notify('Angiv en årsag efter 114', "success")
end
end, false)
RegisterCommand('114a', function(source, args, rawCommand)
local msg = rawCommand:sub(5)
if string.len(msg) > 0 then
if not exports['qb-policejob']:IsHandcuffed() then
if HasPhone() then
PhoneCallAnim()
Wait(math.random(3,8) * 1000)
playAnim = false
local plyData = QBCore.Functions.GetPlayerData()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify",{
dispatchcodename = "114call", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "114",
firstStreet = locationInfo,
priority = 2, -- priority
name = "Anonym",
number = "Skjult Nummer",
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = "Ingående anonymt opkald", -- message
information = msg,
job = {"police", "ambulance"} -- jobs that will get the alerts
})
Wait(1000)
DeletePhone()
StopEntityAnim(PlayerPedId(), 'cellphone_text_to_call', "cellphone@", 3)
else
QBCore.Functions.Notify("Du kan ikke ringe uden en telefon!", "error", 4500)
end
else
QBCore.Functions.Notify("Du kan ikke ringe når du er i håndjern..", "error", 4500)
end
else
QBCore.Functions.Notify('Angiv en årsag efter 114', "success")
end
end, false)
-- Regular 311 call that goes straight to the Police
RegisterCommand('112', function(source, args, rawCommand)
local msg = rawCommand:sub(5)
if string.len(msg) > 0 then
if not exports['qb-policejob']:IsHandcuffed() then
if HasPhone() then
PhoneCallAnim()
Wait(math.random(3,8) * 1000)
playAnim = false
local plyData = QBCore.Functions.GetPlayerData()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify",{
dispatchcodename = "112call", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "112",
firstStreet = locationInfo,
priority = 2, -- priority
name = plyData.charinfo.firstname:sub(1,1):upper()..plyData.charinfo.firstname:sub(2).. " ".. plyData.charinfo.lastname:sub(1,1):upper()..plyData.charinfo.lastname:sub(2),
number = plyData.charinfo.phone,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = "Ingående opkald", -- message
information = msg,
job = {"police", "ambulance"} -- jobs that will get the alerts
})
Wait(1000)
DeletePhone()
StopEntityAnim(PlayerPedId(), 'cellphone_text_to_call', "cellphone@", 3)
else
QBCore.Functions.Notify("Du kan ikke ringe uden en telefon!", "error", 4500)
end
else
QBCore.Functions.Notify("Du kan ikke ringe når du er i håndjern..", "error", 4500)
end
else
QBCore.Functions.Notify('Angiv en årsag efter 112', "success")
end
end, false)
-- Regular 311 call that goes straight to the Police
RegisterCommand('112a', function(source, args, rawCommand)
local msg = rawCommand:sub(5)
if string.len(msg) > 0 then
if not exports['qb-policejob']:IsHandcuffed() then
if HasPhone() then
PhoneCallAnim()
Wait(math.random(3,8) * 1000)
playAnim = false
local plyData = QBCore.Functions.GetPlayerData()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify",{
dispatchcodename = "112call", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "112",
firstStreet = locationInfo,
priority = 2, -- priority
name = "Anonym",
number = "Skjult Nummer",
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = "Ingående anonymt opkald", -- message
information = msg,
job = {"police", "ambulance"} -- jobs that will get the alerts
})
Wait(1000)
DeletePhone()
StopEntityAnim(PlayerPedId(), 'cellphone_text_to_call', "cellphone@", 3)
else
QBCore.Functions.Notify("Du kan ikke ringe uden en telefon!", "error", 4500)
end
else
QBCore.Functions.Notify("Du kan ikke ringe når du er i håndjern..", "error", 4500)
end
else
QBCore.Functions.Notify('Angiv en årsag efter 112a', "success")
end
end, false)
Citizen.CreateThread(function()
TriggerEvent('chat:addSuggestion', '/114', 'Send en besked til politiet.', {{ name="besked", help="Besked til politiet."}})
TriggerEvent('chat:addSuggestion', '/114a', 'Send en anonym besked til politiet.', {{ name="besked", help="Anonym besked til politiet."}})
TriggerEvent('chat:addSuggestion', '/112', 'Send en besked til EMS.', {{ name="besked", help="Besked til EMS."}})
TriggerEvent('chat:addSuggestion', '/112a', 'Send en anonym besked til EMS.', {{ name="besked", help="Anonym besked til EMS."}})
end)

View File

@ -0,0 +1,773 @@
local WeaponTable = {
[584646201] = "CLASS 2: AP-Pistol",
[453432689] = "CLASS 1: Pistol",
[3219281620] = "CLASS 1: Pistol MK2",
[1593441988] = "CLASS 1: Combat Pistol",
[-1716589765] = "CLASS 1: Heavy Pistol",
[-1076751822] = "CLASS 1: SNS-Pistol",
[-771403250] = "CLASS 2: Desert Eagle",
[137902532] = "CLASS 2: Vintage Pistol",
[-598887786] = "CLASS 2: Marksman Pistol",
[-1045183535] = "CLASS 2: Revolver",
[911657153] = "Taser",
[324215364] = "CLASS 2: Micro-SMG",
[-619010992] = "CLASS 2: Machine-Pistol",
[736523883] = "CLASS 2: SMG",
[2024373456] = "CLASS 2: SMG MK2",
[-270015777] = "CLASS 2: Assault SMG",
[171789620] = "CLASS 2: Combat PDW",
[-1660422300] = "CLASS 4: Combat MG",
[3686625920] = "CLASS 4: Combat MG MK2",
[1627465347] = "CLASS 4: Gusenberg",
[-1121678507] = "CLASS 2: Mini SMG",
[-1074790547] = "CLASS 3: Assaultrifle",
[961495388] = "CLASS 3: Assaultrifle MK2",
[-2084633992] = "CLASS 3: Carbinerifle",
[4208062921] = "CLASS 3: Carbinerifle MK2",
[3520460075] = "CLASS 3: Carbinerifle MK2",
[-1357824103] = "CLASS 3: Advancedrifle",
[-1063057011] = "CLASS 3: Specialcarbine",
[2132975508] = "CLASS 3: Bulluprifle",
[1649403952] = "CLASS 3: Compactrifle",
[100416529] = "CLASS 4: Sniperrifle",
[205991906] = "CLASS 4: Heavy Sniper",
[177293209] = "CLASS 4: Heavy Sniper MK2",
[-952879014] = "CLASS 4: Marksmanrifle",
[487013001] = "CLASS 2: Pumpshotgun",
[2017895192] = "CLASS 2: Sawnoff Shotgun",
[-1654528753] = "CLASS 3: Bullupshotgun",
[-494615257] = "CLASS 3: Assaultshotgun",
[-1466123874] = "CLASS 3: Musket",
[984333226] = "CLASS 3: Heavyshotgun",
[-275439685] = "CLASS 2: Doublebarrel Shotgun",
[317205821] = "CLASS 2: Autoshotgun",
[-1568386805] = "CLASS 5: GRENADE LAUNCHER",
[-1312131151] = "CLASS 5: RPG",
[125959754] = "CLASS 5: Compactlauncher",
}
local function VehicleTheft(vehicle)
local vehdata = vehicleData(vehicle)
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local heading = getCardinalDirectionFromHeading()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "vehicletheft", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-35",
firstStreet = locationInfo,
model = vehdata.name, -- vehicle name
plate = vehdata.plate, -- vehicle plate
priority = 2,
firstColor = vehdata.colour, -- vehicle color
heading = heading,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('vehicletheft'),
job = { "police" }
})
end
exports('VehicleTheft', VehicleTheft)
local function VehicleShooting(vehdata)
local Player = QBCore.Functions.GetPlayerData()
if Player.job.name == "police" or Player.job.name == "ambulance" then
if Player.job.onduty then
return
end
end
local vehicle = QBCore.Functions.GetClosestVehicle()
local vehdata = vehicleData(vehicle)
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local heading = getCardinalDirectionFromHeading()
local gender = GetPedGender()
local doorCount = 0
local PlayerPed = PlayerPedId()
local CurrentWeapon = GetSelectedPedWeapon(PlayerPed)
local weapon = WeaponTable[CurrentWeapon] or "UKENDT"
if GetEntityBoneIndexByName(vehicle, 'door_pside_f') ~= -1 then doorCount = doorCount + 1 end
if GetEntityBoneIndexByName(vehicle, 'door_pside_r') ~= -1 then doorCount = doorCount + 1 end
if GetEntityBoneIndexByName(vehicle, 'door_dside_f') ~= -1 then doorCount = doorCount + 1 end
if GetEntityBoneIndexByName(vehicle, 'door_dside_r') ~= -1 then doorCount = doorCount + 1 end
if doorCount == 2 then doorCount = "To-dørs" elseif doorCount == 3 then doorCount = "Tre-dørs" elseif doorCount == 4 then doorCount = "Fire-dørs" else doorCount = "UKENDT" end
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "vehicleshots", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-60",
firstStreet = locationInfo,
model = vehdata.name,
plate = vehdata.plate,
gender = gender,
weapon = weapon,
doorCount = doorCount,
priority = 2,
firstColor = vehdata.colour,
heading = heading,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('vehicleshots'),
job = { "police" }
})
end
exports('VehicleShooting', VehicleShooting)
-- Meter Robbery
local function MeterRobbery(camId)
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "meterrobbery", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-90",
firstStreet = locationInfo,
gender = gender,
camId = camId,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('meterrobbery'), -- message
job = { "police" } -- jobs that will get the alerts
})
end
exports('MeterRobbery', MeterRobbery)
-- ATM Robbery
local function ATMRobbery(camId)
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "atmrobbery", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-90",
firstStreet = locationInfo,
gender = gender,
camId = camId,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('atmrobbery'), -- message
job = { "police" } -- jobs that will get the alerts
})
end
exports('ATMRobbery', ATMRobbery)
local function Shooting()
local Player = QBCore.Functions.GetPlayerData()
if Player.job.name == "police" or Player.job.name == "ambulance" then
if Player.job.onduty then
return
end
end
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
local PlayerPed = PlayerPedId()
local CurrentWeapon = GetSelectedPedWeapon(PlayerPed)
local weapon = WeaponTable[CurrentWeapon] or "UNKNOWN"
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "shooting", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-11",
firstStreet = locationInfo,
gender = gender,
weapon = weapon,
model = nil,
plate = nil,
priority = 2,
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('shooting'),
job = { "police" }
})
end
exports('Shooting', Shooting)
local function SpeedingVehicle(vehdata)
local Player = QBCore.Functions.GetPlayerData()
if Player.job.name == "police" or Player.job.name == "ambulance" then
if Player.job.onduty then
return
end
end
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local heading = getCardinalDirectionFromHeading()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "speeding", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-11",
firstStreet = locationInfo,
model = vehdata.name,
plate = vehdata.plate,
priority = 2,
firstColor = vehdata.colour,
heading = heading,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('speeding'),
job = { "police" }
})
end
exports('SpeedingVehicle', SpeedingVehicle)
local function Fight()
local Player = QBCore.Functions.GetPlayerData()
if Player.job.name == "police" or Player.job.name == "ambulance" then
if Player.job.onduty then
return
end
end
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "fight", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-10",
firstStreet = locationInfo,
gender = gender,
model = nil,
plate = nil,
priority = 2,
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('melee'),
job = { "police" }
})
end
exports('Fight', Fight)
local function InjuriedPerson()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "civdown", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-69",
firstStreet = locationInfo,
gender = gender,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('persondown'), -- message
job = { "ambulance" } -- jobs that will get the alerts
})
end
exports('InjuriedPerson', InjuriedPerson)
local function DeceasedPerson()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "civdead", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-69",
firstStreet = locationInfo,
gender = gender,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = "Civil afgik ved døden", -- message
job = { "ambulance" } -- jobs that will get the alerts
})
end
exports('DeceasedPerson', DeceasedPerson)
local function StoreRobbery(camId)
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "storerobbery", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-90",
firstStreet = locationInfo,
gender = gender,
camId = camId,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('storerobbery'), -- message
job = { "police" } -- jobs that will get the alerts
})
end
exports('StoreRobbery', StoreRobbery)
local function FleecaBankRobbery(camId)
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "bankrobbery", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-90",
firstStreet = locationInfo,
gender = gender,
camId = camId,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('fleecabank'), -- message
job = { "police" } -- jobs that will get the alerts
})
end
exports('FleecaBankRobbery', FleecaBankRobbery)
local function PaletoBankRobbery(camId)
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "paletobankrobbery", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-90",
firstStreet = locationInfo,
gender = gender,
camId = camId,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('paletobank'), -- message
job = { "police" } -- jobs that will get the alerts
})
end
exports('PaletoBankRobbery', PaletoBankRobbery)
local function PacificBankRobbery(camId)
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "pacificbankrobbery", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-90",
firstStreet = locationInfo,
gender = gender,
camId = camId,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('pacificbank'), -- message
job = { "police" } -- jobs that will get the alerts
})
end
exports('PacificBankRobbery', PacificBankRobbery)
local function PrisonBreak()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "prisonbreak", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-90",
firstStreet = locationInfo,
gender = gender,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('prisonbreak'), -- message
job = { "police" } -- jobs that will get the alerts
})
end
exports('PrisonBreak', PrisonBreak)
local function VangelicoRobbery(camId)
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
PlaySound(-1, "Lose_1st", "GTAO_FM_Events_Soundset", 0, 0, 1)
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "vangelicorobbery", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-90",
firstStreet = locationInfo,
gender = gender,
camId = camId,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('vangelico'), -- message
job = { "police" } -- jobs that will get the alerts
})
end
exports('VangelicoRobbery', VangelicoRobbery)
local function HouseRobbery()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "houserobbery", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-90",
firstStreet = locationInfo,
gender = gender,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('houserobbery'), -- message
job = { "police" } -- jobs that will get the alerts
})
end
exports('HouseRobbery', HouseRobbery)
local function YachtHeist()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "yachtheist", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-65",
firstStreet = locationInfo,
gender = gender,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('yachtheist'), -- message
job = { "police" } -- jobs that will get the alerts
})
end
exports('YachtHeist', YachtHeist)
local function DrugSale()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "suspicioushandoff", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-13",
firstStreet = locationInfo,
gender = gender,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('drugsell'), -- message
job = { "police" } -- jobs that will get the alerts
})
end
exports('DrugSale', DrugSale)
-- for rcore_gangs, haven't tested server side exports so made this instead. Remove if you do not need :)
RegisterNetEvent('ps-dispatch:client:drugsale', function()
DrugSale()
end)
local function CarJacking(vehicle)
local vehdata = vehicleData(vehicle)
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local heading = getCardinalDirectionFromHeading()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "carjack", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-35",
firstStreet = locationInfo,
model = vehdata.name, -- vehicle name
plate = vehdata.plate, -- vehicle plate
priority = 2,
firstColor = vehdata.colour, -- vehicle color
heading = heading,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('carjacking'),
job = { "police" }
})
end
exports('CarJacking', CarJacking)
local function OfficerDown()
local plyData = QBCore.Functions.GetPlayerData()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local callsign = QBCore.Functions.GetPlayerData().metadata["callsign"]
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "officerdown", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-99",
firstStreet = locationInfo,
name = "BETJENT - " .. plyData.charinfo.firstname:sub(1, 1):upper() .. plyData.charinfo.firstname:sub(2) .. " " .. plyData.charinfo.lastname:sub(1, 1):upper() .. plyData.charinfo.lastname:sub(2),
model = nil,
plate = nil,
callsign = callsign,
priority = 1, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('officerdown'), -- message
job = { "ambulance", "police" } -- jobs that will get the alerts
})
end
exports('OfficerDown', OfficerDown)
RegisterNetEvent("ps-dispatch:client:officerdown", function ()
OfficerDown()
end)
local function EmsDown()
local plyData = QBCore.Functions.GetPlayerData()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local callsign = QBCore.Functions.GetPlayerData().metadata["callsign"]
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "emsdown", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-99",
firstStreet = locationInfo,
name = "EMS - " .. plyData.charinfo.firstname:sub(1, 1):upper() .. plyData.charinfo.firstname:sub(2) .. " " .. plyData.charinfo.lastname:sub(1, 1):upper() .. plyData.charinfo.lastname:sub(2),
model = nil,
plate = nil,
callsign = callsign,
priority = 1, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('emsdown'), -- message
job = { "ambulance", "police" } -- jobs that will get the alerts
})
end
exports('EmsDown', EmsDown)
RegisterNetEvent("ps-dispatch:client:emsdown", function ()
EmsDown()
end)
local function Explosion()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "explosion", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-80",
firstStreet = locationInfo,
gender = nil,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = "Eksplosion meldt", -- message
job = { "police" } -- jobs that will get the alerts
})
end
exports('Explosion', Explosion)
local function SuspiciousActivity()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify", {
dispatchcodename = "susactivity", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-66",
firstStreet = locationInfo,
gender = gender,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('susactivity'), -- message
job = { "police" } -- jobs that will get the alerts
})
end
exports('SuspiciousActivity', SuspiciousActivity)
local function CustomAlert(data)
local coords = data.coords or vec3(0.0, 0.0, 0.0)
local gender = GetPedGender()
if not data.gender then gender = nil end
local job = { "police" }
if data.job then job = data.job end
local locationInfo = getStreetandZone(coords)
TriggerServerEvent("dispatch:server:notify", {
dispatchCode = data.dispatchCode or "INGEN",
firstStreet = locationInfo,
gender = gender,
model = data.model or nil,
plate = data.plate or nil,
priority = data.priority or 2, -- priority
firstColor = data.firstColor or nil,
camId = data.camId or nil,
callsign = data.callsign or nil,
name = data.name or nil,
doorCount = data.doorCount or nil,
heading = data.heading or nil,
automaticGunfire = data.automaticGunfire or false,
origin = {
x = coords.x,
y = coords.y,
z = coords.z
},
dispatchMessage = data.message or "",
job = job,
alert = {
displayCode = data.dispatchCode or "INGEN",
description = data.description or "",
radius = data.radius or 0,
recipientList = job,
blipSprite = data.sprite or 1,
blipColour = data.color or 1,
blipScale = data.scale or 0.5,
blipLength = data.length or 2,
sound = data.sound or "Lose_1st",
sound2 = data.sound2 or "GTAO_FM_Events_Soundset",
offset = data.offset or "false",
blipflash = data.flash or "false"
}
})
end
exports('CustomAlert', CustomAlert)

View File

@ -0,0 +1,224 @@
---------------------------
----rainmad scripts--------
---------------------------
local function ArtGalleryRobbery()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify",{
dispatchcodename = "artgalleryrobbery", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-90",
firstStreet = locationInfo,
gender = gender,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('artgalleryrobbery'), -- message
job = {"police"} -- jobs that will get the alerts
})
end exports('ArtGalleryRobbery', ArtGalleryRobbery)
local function HumaneRobbery()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify",{
dispatchcodename = "humanelabsrobbery", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-90",
firstStreet = locationInfo,
gender = gender,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('humanerobbery'), -- message
job = {"police"} -- jobs that will get the alerts
})
end exports('HumaneRobbery', HumaneRobbery)
local function TrainRobbery()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify",{
dispatchcodename = "trainrobbery", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-90",
firstStreet = locationInfo,
gender = gender,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('trainrobbery'), -- message
job = {"police"} -- jobs that will get the alerts
})
end exports('TrainRobbery', TrainRobbery)
local function VanRobbery()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify",{
dispatchcodename = "vanrobbery", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-90",
firstStreet = locationInfo,
gender = gender,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('vanrobbery'), -- message
job = {"police"} -- jobs that will get the alerts
})
end exports('VanRobbery', VanRobbery)
local function UndergroundRobbery()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify",{
dispatchcodename = "undergroundrobbery", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-90",
firstStreet = locationInfo,
gender = gender,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('underground'), -- message
job = {"police"} -- jobs that will get the alerts
})
end exports('UndergroundRobbery', UndergroundRobbery)
local function DrugBoatRobbery()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify",{
dispatchcodename = "drugboatrobbery", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-90",
firstStreet = locationInfo,
gender = gender,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('drugboatrobbery'), -- message
job = {"police"} -- jobs that will get the alerts
})
end exports('DrugBoatRobbery', DrugBoatRobbery)
local function UnionRobbery()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify",{
dispatchcodename = "unionrobbery", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-90",
firstStreet = locationInfo,
gender = gender,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('unionrobbery'), -- message
job = {"police"} -- jobs that will get the alerts
})
end exports('UnionRobbery', UnionRobbery)
local function CarBoosting(vehicle)
local vehdata = vehicleData(vehicle)
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify",{
dispatchcodename = "carboosting", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-50",
firstStreet = locationInfo,
gender = gender,
model = vehdata.name,
plate = vehdata.plate,
priority = 2,
firstColor = vehdata.colour,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = _U('carboosting'), -- message
job = {"police"} -- jobs that will get the alerts
})
end exports('CarBoosting', CarBoosting)
---------------------------
---- ps-signrobbery -------
---------------------------
local function SignRobbery()
local currentPos = GetEntityCoords(PlayerPedId())
local locationInfo = getStreetandZone(currentPos)
local gender = GetPedGender()
TriggerServerEvent("dispatch:server:notify",{
dispatchcodename = "signrobbery", -- has to match the codes in sv_dispatchcodes.lua so that it generates the right blip
dispatchCode = "10-35",
firstStreet = locationInfo,
gender = gender,
model = nil,
plate = nil,
priority = 2, -- priority
firstColor = nil,
automaticGunfire = false,
origin = {
x = currentPos.x,
y = currentPos.y,
z = currentPos.z
},
dispatchMessage = 'Skiltetyveri begået', -- message
job = {"police"} -- jobs that will get the alerts
})
end exports('SignRobbery', SignRobbery)

View File

@ -0,0 +1,57 @@
CreateThread(function()
local vehicleWhitelist = {[0]=true,[1]=true,[2]=true,[3]=true,[4]=true,[5]=true,[6]=true,[7]=true,[8]=true,[9]=true,[10]=true,[11]=true,[12]=true,[17]=true,[19]=true,[20]=true}
local sleep = 100
while true do
playerPed = PlayerPedId()
if (not isPlayerWhitelisted or Config.Debug) then
for k, v in pairs(Config.Timer) do
if v > 0 then Config.Timer[k] = v - 1 end
end
if GetVehiclePedIsUsing(playerPed) ~= 0 then
local vehicle = GetVehiclePedIsUsing(playerPed, true)
if vehicleWhitelist[GetVehicleClass(vehicle)] then
local driver = GetPedInVehicleSeat(vehicle, -1)
if Config.Timer['Shooting'] == 0 and not BlacklistedWeapon(playerPed) and not IsPedCurrentWeaponSilenced(playerPed) and IsPedArmed(playerPed, 4) then
sleep = 10
if IsPedShooting(playerPed) then
local vehicle = vehicleData(vehicle)
exports['ps-dispatch']:VehicleShooting(vehicle)
Config.Timer['Shooting'] = Config.Shooting.Success
else
Config.Timer['Shooting'] = Config.Shooting.Fail
end
elseif Config.Timer['Speeding'] == 0 and playerPed == driver then
sleep = 100
if (GetEntitySpeed(vehicle) * 3.6) >= (120 + (math.random(30,60))) then
Wait(400)
if IsPedInAnyVehicle(playerPed, true) and ((GetEntitySpeed(vehicle) * 3.6) >= 90) then
local vehicle = vehicleData(vehicle)
exports['ps-dispatch']:SpeedingVehicle(vehicle)
Config.Timer['Speeding'] = Config.Speeding.Success
end
else
Config.Timer['Speeding'] = Config.Speeding.Fail
end
else
sleep = 100
end
end
else
if Config.Timer['Shooting'] == 0 and not IsPedCurrentWeaponSilenced(playerPed) and IsPedArmed(playerPed, 4) then
sleep = 50
if IsPedShooting(playerPed) and not BlacklistedWeapon(playerPed) then
exports['ps-dispatch']:Shooting()
Config.Timer['Shooting'] = Config.Shooting.Success
else
Config.Timer['Shooting'] = Config.Shooting.Fail
end
elseif Config.Timer['Melee'] == 0 and IsPedInMeleeCombat(playerPed) and HasPedBeenDamagedByWeapon(GetMeleeTargetForPed(playerPed), 0, 1) then
sleep = 50
exports['ps-dispatch']:Fight()
Config.Timer['Melee'] = Config.Melee.Success
else sleep = 100 end
end
end
Wait(sleep)
end
end)

View File

@ -0,0 +1,333 @@
PlayerData = {}
PlayerJob = {}
isLoggedIn = true
QBCore = exports['qb-core']:GetCoreObject()
local blips = {}
-- core related
AddEventHandler('onResourceStart', function(resourceName)
if GetCurrentResourceName() == resourceName then
isLoggedIn = true
PlayerData = QBCore.Functions.GetPlayerData()
PlayerJob = QBCore.Functions.GetPlayerData().job
end
end)
RegisterNetEvent("QBCore:Client:OnPlayerLoaded", function()
isLoggedIn = true
PlayerData = QBCore.Functions.GetPlayerData()
PlayerJob = QBCore.Functions.GetPlayerData().job
end)
RegisterNetEvent('QBCore:Client:OnPlayerUnload', function()
PlayerData = {}
isLoggedIn = false
currentCallSign = ""
-- currentVehicle, inVehicle, currentlyArmed, currentWeapon = nil, false, false, `WEAPON_UNARMED`
-- removeHuntingZones()
end)
RegisterNetEvent("QBCore:Client:OnJobUpdate", function(JobInfo)
PlayerData = QBCore.Functions.GetPlayerData()
PlayerJob = JobInfo
end)
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
function _U(entry)
return Locales[Config.Locale][entry]
end
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
function getSpeed() return speedlimit end
function getStreet() return currentStreetName end
function getStreetandZone(coords)
local zone = GetLabelText(GetNameOfZone(coords.x, coords.y, coords.z))
local currentStreetHash = GetStreetNameAtCoord(coords.x, coords.y, coords.z)
currentStreetName = GetStreetNameFromHashKey(currentStreetHash)
playerStreetsLocation = currentStreetName .. ", " .. zone
return playerStreetsLocation
end
function refreshPlayerWhitelisted()
if not PlayerData then return false end
if not PlayerData.job then return false end
if Config.Debug then return true end
for k,v in ipairs({'police'}) do
if v == PlayerData.job.name then
return true
end
end
return false
end
function BlacklistedWeapon(playerPed)
for i = 1, #Config.WeaponBlacklist do
local weaponHash = GetHashKey(Config.WeaponBlacklist[i])
if GetSelectedPedWeapon(playerPed) == weaponHash then
return true -- Is a blacklisted weapon
end
end
return false -- Is not a blacklisted weapon
end
function GetAllPeds()
local getPeds = {}
local findHandle, foundPed = FindFirstPed()
local continueFind = (foundPed and true or false)
local count = 0
while continueFind do
local pedCoords = GetEntityCoords(foundPed)
if GetPedType(foundPed) ~= 28 and not IsEntityDead(foundPed) and not IsPedAPlayer(foundPed) and #(playerCoords - pedCoords) < 80.0 then
getPeds[#getPeds + 1] = foundPed
count = count + 1
end
continueFind, foundPed = FindNextPed(findHandle)
end
EndFindPed(findHandle)
return count
end
function zoneChance(type, zoneMod, street)
if Config.DebugChance then return true end
if not street then street = currentStreetName end
playerCoords = GetEntityCoords(PlayerPedId())
local zone, sendit = GetLabelText(GetNameOfZone(playerCoords.x, playerCoords.y, playerCoords.z)), false
if not nearbyPeds then
nearbyPeds = GetAllPeds()
elseif nearbyPeds < 1 then if Config.Debug then print(('^1[%s] Nobody is nearby to send a report^7'):format(type)) end
return false
end
if zoneMod == nil then zoneMod = 1 end
zoneMod = (math.ceil(zoneMod+0.5))
local hour = GetClockHours()
if hour >= 21 or hour <= 4 then
zoneMod = zoneMod * 1.6
zoneMod = math.ceil(zoneMod+0.5)
end
zoneMod = zoneMod / (nearbyPeds / 3)
zoneMod = (math.ceil(zoneMod+0.5))
local sum = math.random(1, zoneMod)
local chance = string.format('%.2f',(1 / zoneMod) * 100)..'%'
if sum > 1 then
if Config.Debug then print(('^1[%s] %s (%s) - %s nearby peds^7'):format(type, zone, chance, nearbyPeds)) end
sendit = false
else
if Config.Debug then print(('^2[%s] %s (%s) - %s nearby peds^7'):format(type, zone, chance, nearbyPeds)) end
sendit = true
end
print(('^1[%s] %s (%s) - %s nearby peds^7'):format(type, zone, chance, nearbyPeds))
return sendit
end
function vehicleData(vehicle)
local vData = {}
local vehicleClass = GetVehicleClass(vehicle)
local vClass = {[0] = _U('compact'), [1] = _U('sedan'), [2] = _U('suv'), [3] = _U('coupe'), [4] = _U('muscle'), [5] = _U('sports_classic'), [6] = _U('sports'), [7] = _U('super'), [8] = _U('motorcycle'), [9] = _U('offroad'), [10] = _U('industrial'), [11] = _U('utility'), [12] = _U('van'), [17] = _U('service'), [19] = _U('military'), [20] = _U('truck')}
local vehClass = vClass[vehicleClass]
local vehicleName = GetLabelText(GetDisplayNameFromVehicleModel(GetEntityModel(vehicle)))
local vehicleColour1, vehicleColour2 = GetVehicleColours(vehicle)
if vehicleColour1 then
if Config.Colours[tostring(vehicleColour2)] and Config.Colours[tostring(vehicleColour1)] then
vehicleColour = Config.Colours[tostring(vehicleColour2)] .. " on " .. Config.Colours[tostring(vehicleColour1)]
elseif Config.Colours[tostring(vehicleColour1)] then
vehicleColour = Config.Colours[tostring(vehicleColour1)]
elseif Config.Colours[tostring(vehicleColour2)] then
vehicleColour = Config.Colours[tostring(vehicleColour2)]
else
vehicleColour = "Ukendt"
end
end
local plate = GetVehicleNumberPlateText(vehicle)
local doorCount = 0
if GetEntityBoneIndexByName(vehicle, 'door_pside_f') ~= -1 then doorCount = doorCount + 1 end
if GetEntityBoneIndexByName(vehicle, 'door_pside_r') ~= -1 then doorCount = doorCount + 1 end
if GetEntityBoneIndexByName(vehicle, 'door_dside_f') ~= -1 then doorCount = doorCount + 1 end
if GetEntityBoneIndexByName(vehicle, 'door_dside_r') ~= -1 then doorCount = doorCount + 1 end
if doorCount == 2 then doorCount = _U('two_door') elseif doorCount == 3 then doorCount = _U('three_door') elseif doorCount == 4 then doorCount = _U('four_door') else doorCount = '' end
vData.class, vData.name, vData.colour, vData.doors, vData.plate, vData.id = vehClass, vehicleName, vehicleColour, doorCount, plate, NetworkGetNetworkIdFromEntity(vehicle)
return vData
end
function GetPedGender()
local gender = "Mand"
if QBCore.Functions.GetPlayerData().charinfo.gender == 1 then gender = "Kvinde" end
return gender
end
function getCardinalDirectionFromHeading()
local heading = GetEntityHeading(PlayerPedId())
if heading >= 315 or heading < 45 then return "Nordgående"
elseif heading >= 45 and heading < 135 then return "Vestgående"
elseif heading >=135 and heading < 225 then return "Sydgående"
elseif heading >= 225 and heading < 315 then return "Østgående" end
end
function IsPoliceJob(job)
for k, v in pairs(Config.PoliceJob) do
if job == v then
return true
end
end
return false
end
local function IsValidJob(jobList)
for k, v in pairs(jobList) do
if v == PlayerJob.name then
return true
end
end
return false
end
local function CheckOnDuty()
if Config.OnDutyOnly then
return PlayerJob.onduty
end
return true
end
-- Dispatch Itself
local disableNotis, disableNotifSounds = false, false
RegisterNetEvent('dispatch:manageNotifs', function(sentSetting)
local wantedSetting = tostring(sentSetting)
if wantedSetting == "on" then
disableNotis = false
disableNotifSounds = false
QBCore.Functions.Notify("Dispatch slået til", "success")
elseif wantedSetting == "off" then
disableNotis = true
disableNotifSounds = true
QBCore.Functions.Notify("Dispatch slået fra", "success")
elseif wantedSetting == "mute" then
disableNotis = false
disableNotifSounds = true
QBCore.Functions.Notify("Dispatch muted", "success")
else
QBCore.Functions.Notify('Vælg venligt "on", "off" eller "mute", for dispatch notifikationer.', "success")
end
end)
RegisterNetEvent('dispatch:clNotify', function(sNotificationData, sNotificationId, sender)
if sNotificationData ~= nil and isLoggedIn then
if IsValidJob(sNotificationData['job']) and CheckOnDuty() then
if not disableNotis then
if sNotificationData.origin ~= nil then
SendNUIMessage({
update = "newCall",
callID = sNotificationId,
data = sNotificationData,
timer = 5000,
isPolice = IsPoliceJob(PlayerJob.name)
})
end
end
end
end
end)
RegisterNetEvent("ps-dispatch:client:AddCallBlip", function(coords, data, blipId)
if IsValidJob(data.recipientList) and CheckOnDuty() then
PlaySound(-1, data.sound, data.sound2, 0, 0, 1)
TriggerServerEvent("InteractSound_SV:PlayOnSource", data.sound, 0.25) -- For Custom Sounds
CreateThread(function()
local alpha = 255
local blip = nil
local radius = nil
local radiusAlpha = 128
local sprite, colour, scale = 161, 84, 0.5
local randomoffset = math.random(1,100)
if data.blipSprite then sprite = data.blipSprite end
if data.blipColour then colour = data.blipColour end
if data.blipScale then scale = data.blipScale end
if data.radius then radius = data.radius end
if data.offset == "true" then
if randomoffset <= 25 then
radius = AddBlipForRadius(coords.x + math.random(Config.MinOffset, Config.MaxOffset), coords.y + math.random(Config.MinOffset, Config.MaxOffset), coords.z, data.radius)
blip = AddBlipForCoord(coords.x + math.random(Config.MinOffset, Config.MaxOffset), coords.y + math.random(Config.MinOffset, Config.MaxOffset), coords.z)
blips[blipId] = blip
elseif randomoffset >= 26 and randomoffset <= 50 then
radius = AddBlipForRadius(coords.x - math.random(Config.MinOffset, Config.MaxOffset), coords.y + math.random(Config.MinOffset, Config.MaxOffset), coords.z, data.radius)
blip = AddBlipForCoord(coords.x - math.random(Config.MinOffset, Config.MaxOffset), coords.y + math.random(Config.MinOffset, Config.MaxOffset), coords.z)
blips[blipId] = blip
elseif randomoffset >= 51 and randomoffset <= 74 then
radius = AddBlipForRadius(coords.x - math.random(Config.MinOffset, Config.MaxOffset), coords.y - math.random(Config.MinOffset, Config.MaxOffset), coords.z, data.radius)
blip = AddBlipForCoord(coords.x - math.random(Config.MinOffset, Config.MaxOffset), coords.y - math.random(Config.MinOffset, Config.MaxOffset), coords.z)
blips[blipId] = blip
elseif randomoffset >= 75 and randomoffset <= 100 then
radius = AddBlipForRadius(coords.x + math.random(Config.MinOffset, Config.MaxOffset), coords.y - math.random(Config.MinOffset, Config.MaxOffset), coords.z, data.radius)
blip = AddBlipForCoord(coords.x + math.random(Config.MinOffset, Config.MaxOffset), coords.y - math.random(Config.MinOffset, Config.MaxOffset), coords.z)
blips[blipId] = blip
end
elseif data.offset == "false" then
radius = AddBlipForRadius(coords.x, coords.y, coords.z, data.radius)
blip = AddBlipForCoord(coords.x, coords.y, coords.z)
blips[blipId] = blip
end
if data.blipflash == "true" then
SetBlipFlashes(blip, true)
elseif data.blipflash == "false" then
SetBlipFlashes(blip, false)
end
SetBlipSprite(blip, sprite)
SetBlipHighDetail(blip, true)
SetBlipScale(blip, scale)
SetBlipColour(blip, colour)
SetBlipAlpha(blip, alpha)
SetBlipAsShortRange(blip, false)
SetBlipCategory(blip, 2)
SetBlipColour(radius, colour)
SetBlipAlpha(radius, radiusAlpha)
BeginTextCommandSetBlipName('STRING')
AddTextComponentString(data.displayCode..' - '..data.description)
EndTextCommandSetBlipName(blip)
while radiusAlpha ~= 0 do
Wait(data.blipLength * 1000)
radiusAlpha = radiusAlpha - 1
SetBlipAlpha(radius, radiusAlpha)
if radiusAlpha == 0 then
RemoveBlip(radius)
RemoveBlip(blip)
return
end
end
end)
end
end)
RegisterNetEvent('dispatch:getCallResponse', function(message)
SendNUIMessage({
update = "newCall",
callID = math.random(1000, 9999),
data = {
dispatchCode = 'RSP',
priority = 1,
dispatchMessage = "Opkald svar",
information = message
},
timer = 10000,
isPolice = true
})
end)
RegisterNetEvent("ps-dispatch:client:Explosion", function(data)
exports["ps-dispatch"]:Explosion()
end)
RegisterNetEvent("ps-dispatch:client:removeCallBlip", function(blipId)
RemoveBlip(blips[blipId])
end)
RegisterNetEvent("ps-dispatch:client:clearAllBlips", function()
for k, v in pairs(blips) do
RemoveBlip(v)
end
QBCore.Functions.Notify('Alle dispatch-blips er fjernet', "success")
end)

View File

@ -0,0 +1,244 @@
Config = {}
Config.Enable = {}
Config.Timer = {}
Config.PoliceJob = { "police" }
-- Enable if you only want to send alerts to onDuty officers
Config.OnDutyOnly = true
Config.PoliceAndAmbulance = { "police", "ambulance" }
Config.PhoneModel = 'prop_npc_phone_02'
-- sets report chance to 100%
Config.DebugChance = true
-- Explosion Alert Types (Gas Pumps by default)
-- Ex. Config.ExplosionTypes = {1, 2, 3, 4, 5}
Config.ExplosionTypes = {9}
-- enable default alerts
Config.Enable.Speeding = true
Config.Enable.Shooting = true
Config.Enable.Autotheft = true
Config.Enable.Melee = true
Config.Enable.PlayerDowned = true
---------------------------------------------------------------
Config.Locale = 'da'
-- enable alerts when cops break the law and print to console
Config.Debug = false
-- changes the min and max offset for the radius
Config.MinOffset = 1
Config.MaxOffset = 120
---------------------------------------------------------------
Citizen.CreateThread(function()
-- if not GetPlayerPed(-1) then return end
-- while not firstname do
-- Citizen.Wait(10)
-- end
-- if notLoaded then
for k, v in pairs(Config.Enable) do
if Config.Enable[k] ~= false then
Config[k] = {}
Config.Timer[k] = 0 -- Default to 0 seconds
Config[k].Success = 300 -- Default to 30 seconds
Config[k].Fail = 20 -- Default to 2 seconds
end
end
-- If you want to set specific timers, do it here
if Config.Shooting then
Config.Shooting.Success = 100 -- 10 seconds
Config.Shooting.Fail = 0 -- 0 seconds
end
-- notLoaded = nil
-- end
Config.WeaponBlacklist = {
'WEAPON_GRENADE',
'WEAPON_BZGAS',
'WEAPON_MOLOTOV',
'WEAPON_STICKYBOMB',
'WEAPON_PROXMINE',
'WEAPON_SNOWBALL',
'WEAPON_PIPEBOMB',
'WEAPON_BALL',
'WEAPON_SMOKEGRENADE',
'WEAPON_FLARE',
'WEAPON_PETROLCAN',
'WEAPON_FIREEXTINGUISHER',
'WEAPON_HAZARDCAN',
'WEAPON_RAYCARBINE',
'WEAPON_STUNGUN'
}
Config.Colours = {
['0'] = "Metallic Black",
['1'] = "Metallic Graphite Black",
['2'] = "Metallic Black Steel",
['3'] = "Metallic Dark Silver",
['4'] = "Metallic Silver",
['5'] = "Metallic Blue Silver",
['6'] = "Metallic Steel Gray",
['7'] = "Metallic Shadow Silver",
['8'] = "Metallic Stone Silver",
['9'] = "Metallic Midnight Silver",
['10'] = "Metallic Gun Metal",
['11'] = "Metallic Anthracite Grey",
['12'] = "Matte Black",
['13'] = "Matte Gray",
['14'] = "Matte Light Grey",
['15'] = "Util Black",
['16'] = "Util Black Poly",
['17'] = "Util Dark silver",
['18'] = "Util Silver",
['19'] = "Util Gun Metal",
['20'] = "Util Shadow Silver",
['21'] = "Worn Black",
['22'] = "Worn Graphite",
['23'] = "Worn Silver Grey",
['24'] = "Worn Silver",
['25'] = "Worn Blue Silver",
['26'] = "Worn Shadow Silver",
['27'] = "Metallic Red",
['28'] = "Metallic Torino Red",
['29'] = "Metallic Formula Red",
['30'] = "Metallic Blaze Red",
['31'] = "Metallic Graceful Red",
['32'] = "Metallic Garnet Red",
['33'] = "Metallic Desert Red",
['34'] = "Metallic Cabernet Red",
['35'] = "Metallic Candy Red",
['36'] = "Metallic Sunrise Orange",
['37'] = "Metallic Classic Gold",
['38'] = "Metallic Orange",
['39'] = "Matte Red",
['40'] = "Matte Dark Red",
['41'] = "Matte Orange",
['42'] = "Matte Yellow",
['43'] = "Util Red",
['44'] = "Util Bright Red",
['45'] = "Util Garnet Red",
['46'] = "Worn Red",
['47'] = "Worn Golden Red",
['48'] = "Worn Dark Red",
['49'] = "Metallic Dark Green",
['50'] = "Metallic Racing Green",
['51'] = "Metallic Sea Green",
['52'] = "Metallic Olive Green",
['53'] = "Metallic Green",
['54'] = "Metallic Gasoline Blue Green",
['55'] = "Matte Lime Green",
['56'] = "Util Dark Green",
['57'] = "Util Green",
['58'] = "Worn Dark Green",
['59'] = "Worn Green",
['60'] = "Worn Sea Wash",
['61'] = "Metallic Midnight Blue",
['62'] = "Metallic Dark Blue",
['63'] = "Metallic Saxony Blue",
['64'] = "Metallic Blue",
['65'] = "Metallic Mariner Blue",
['66'] = "Metallic Harbor Blue",
['67'] = "Metallic Diamond Blue",
['68'] = "Metallic Surf Blue",
['69'] = "Metallic Nautical Blue",
['70'] = "Metallic Bright Blue",
['71'] = "Metallic Purple Blue",
['72'] = "Metallic Spinnaker Blue",
['73'] = "Metallic Ultra Blue",
['74'] = "Metallic Bright Blue",
['75'] = "Util Dark Blue",
['76'] = "Util Midnight Blue",
['77'] = "Util Blue",
['78'] = "Util Sea Foam Blue",
['79'] = "Uil Lightning blue",
['80'] = "Util Maui Blue Poly",
['81'] = "Util Bright Blue",
['82'] = "Matte Dark Blue",
['83'] = "Matte Blue",
['84'] = "Matte Midnight Blue",
['85'] = "Worn Dark blue",
['86'] = "Worn Blue",
['87'] = "Worn Light blue",
['88'] = "Metallic Taxi Yellow",
['89'] = "Metallic Race Yellow",
['90'] = "Metallic Bronze",
['91'] = "Metallic Yellow Bird",
['92'] = "Metallic Lime",
['93'] = "Metallic Champagne",
['94'] = "Metallic Pueblo Beige",
['95'] = "Metallic Dark Ivory",
['96'] = "Metallic Choco Brown",
['97'] = "Metallic Golden Brown",
['98'] = "Metallic Light Brown",
['99'] = "Metallic Straw Beige",
['100'] = "Metallic Moss Brown",
['101'] = "Metallic Biston Brown",
['102'] = "Metallic Beechwood",
['103'] = "Metallic Dark Beechwood",
['104'] = "Metallic Choco Orange",
['105'] = "Metallic Beach Sand",
['106'] = "Metallic Sun Bleeched Sand",
['107'] = "Metallic Cream",
['108'] = "Util Brown",
['109'] = "Util Medium Brown",
['110'] = "Util Light Brown",
['111'] = "Metallic White",
['112'] = "Metallic Frost White",
['113'] = "Worn Honey Beige",
['114'] = "Worn Brown",
['115'] = "Worn Dark Brown",
['116'] = "Worn straw beige",
['117'] = "Brushed Steel",
['118'] = "Brushed Black Steel",
['119'] = "Brushed Aluminium",
['120'] = "Chrome",
['121'] = "Worn Off White",
['122'] = "Util Off White",
['123'] = "Worn Orange",
['124'] = "Worn Light Orange",
['125'] = "Metallic Securicor Green",
['126'] = "Worn Taxi Yellow",
['127'] = "Police Car Blue",
['128'] = "Matte Green",
['129'] = "Matte Brown",
['130'] = "Worn Orange",
['131'] = "Matte White",
['132'] = "Worn White",
['133'] = "Worn Olive Army Green",
['134'] = "Pure White",
['135'] = "Hot Pink",
['136'] = "Salmon pink",
['137'] = "Metallic Vermillion Pink",
['138'] = "Orange",
['139'] = "Green",
['140'] = "Blue",
['141'] = "Mettalic Black Blue",
['142'] = "Metallic Black Purple",
['143'] = "Metallic Black Red",
['144'] = "hunter green",
['145'] = "Metallic Purple",
['146'] = "Metallic Dark Blue",
['147'] = "Black",
['148'] = "Matte Purple",
['149'] = "Matte Dark Purple",
['150'] = "Metallic Lava Red",
['151'] = "Matte Forest Green",
['152'] = "Matte Olive Drab",
['153'] = "Matte Desert Brown",
['154'] = "Matte Desert Tan",
['155'] = "Matte Foilage Green",
['156'] = "Default Alloy Color",
['157'] = "Epsilon Blue",
['158'] = "Pure Gold",
['159'] = "Brushed Gold",
['160'] = "MP100"
}
end)

View File

@ -0,0 +1,30 @@
fx_version 'cerulean'
game 'gta5'
version '0.0'
description 'https://github.com/Project-Sloth/ps-dispatch'
shared_scripts {
'config.lua',
'locales/locales.lua',
}
client_scripts{
'client/cl_main.lua',
'client/cl_events.lua',
'client/cl_extraalerts.lua',
'client/cl_commands.lua',
'client/cl_loops.lua',
}
server_script {
'server/sv_dispatchcodes.lua',
'server/sv_main.lua'
}
ui_page 'ui/index.html'
files {
'ui/index.html',
'ui/app.js',
'ui/style.css',
}

View File

@ -0,0 +1,63 @@
Locales = {
['da'] = {
['unknown_caller'] = "Opkalder Ukendt",
['caller_unknown'] = "Ukendt",
['caller_local'] = "Lokal Borger",
['call_from'] = "Opkald fra ",
['two_door'] = "To-dørs ",
['three_door'] = "Tre-dørs ",
['four_door'] = "Fire-dørs ",
['compact'] = "Compact",
['sedan'] = "Sedan",
['suv'] = "SUV",
['coupe'] = "Coupe",
['muscle'] = "Muskelbil",
['sports_classic'] = "Klasikker",
['sports'] = "Sportsbil",
['super'] = "Superbil",
['motorcycle'] = "Motorcykel",
['offroad'] = "Off-road",
['industrial'] = "Industriel",
['utility'] = "Utility køretøj",
['van'] = "Van",
['service'] = "Service køretøj",
['military'] = "Militær køretøj",
['truck'] = "Lastbil",
-- Meter Robbery
['meterrobbery'] = "Meter Robbery",
-- ATM Robbery
['atmrobbery'] = "Hæveautomat røveri",
-- DISPATCH MESSAGES
['vehicleshots'] = "Skud affyret fra køretøj",
['melee'] = "Slås kamp i gang",
['shooting'] = "Skyderi i gang",
['driveby'] = "Drive-by skyderi",
['speeding'] = "Uansvarlig kørsel",
['autotheft'] = "Tyveri af et køretøj",
['officerdown'] = "KOLLEGA SKADET",
['persondown'] = "Personen er såret",
['storerobbery'] = "Røveri i butik",
['fleecabank'] = "Fleeca Bank røveri",
['paletobank'] = "Paleto Bank røveri",
['pacificbank'] = "Pacific Bank røveri",
['prisonbreak'] = "Fangeflugt i gang",
['vangelico'] = "Vangelico røveri",
['houserobbery'] = "Hus røveri",
['drugsell'] = "Mistænkelig handel",
['carjacking'] = "Biltyveri",
['vehicletheft'] = "Køretøjstyveri",
['emsdown'] = "EMS nede",
['artgalleryrobbery'] = "Kunstgalleri røveri",
['humanerobbery'] = "Humane Labs røveri",
['trainrobbery'] = "Tog røveri",
['vanrobbery'] = "Sikkerheds Van røveri",
['underground'] = "Bunker røveri",
['drugboatrobbery'] = "Mistænkelig båd",
['unionrobbery'] = "Union Depository røveri",
['carboosting'] = "Bil Boosting i gang",
['yachtheist'] = "Yacht Røveri i gang",
['susactivity'] = "Mistænkelig aktivitet",
}
}

View File

@ -0,0 +1,90 @@
--[[
["vehicleshots"] -> dispatchcodename that you pass with the event of AlertGunShot
displayCode -> Code to be displayed on the blip message
description -> Description of the blip message
radius -> to draw a circle with radius around blip ( the number need to have a .0 behind it, for example 150.0 or 75.0 )
-> if u want to have the radius without the blip just make the blipScale = 0
-> if u want to have the radius centered, disable the offset
recipientList -> list of job names that can see the blip
blipSprite -> blip sprite
blipColour -> blip colour
blipScale -> blip scale
blipLength -> time in seconds at which the blip will fade down, lower the time, faster it will fade. Cannot be 0
offset -> enable or disable the offset for the radius ( radius on 0 and offset on true does nothing )
blipflash -> enable or disable the flashing blip
]]--
dispatchCodes = {
["vehicleshots"] = {displayCode = '10-13', description = "Skud affyret fra køretøj", radius = 0, recipientList = {'police'}, blipSprite = 119, blipColour = 1, blipScale = 1.5, blipLength = 2, sound = "Lose_1st", sound2 = "GTAO_FM_Events_Soundset", offset = "false", blipflash = "false"},
["shooting"] = {displayCode = '10-13', description ="Skud affyret", radius = 0, recipientList = {'police'}, blipSprite = 110, blipColour = 1, blipScale = 1.5, blipLength = 2, sound = "Lose_1st", sound2 = "GTAO_FM_Events_Soundset", offset = "false", blipflash = "false"},
["speeding"] = {displayCode = '10-13', description = "Overtrædelse af fartgrænse", radius = 0, recipientList = {'police'}, blipSprite = 326, blipColour = 84, blipScale = 1.5, blipLength = 2, sound = "Lose_1st", sound2 = "GTAO_FM_Events_Soundset", offset = "false", blipflash = "false"},
["fight"] = {displayCode = '10-10', description = "Igangværende slåskamp", radius = 0, recipientList = {'police'}, blipSprite = 685, blipColour = 69, blipScale = 1.5, blipLength = 2, sound = "Lose_1st", sound2 = "GTAO_FM_Events_Soundset", offset = "false", blipflash = "false"},
["civdown"] = {displayCode = '10-69', description = "Civil nede", radius = 0, recipientList = {'ambulance'}, blipSprite = 126, blipColour = 3, blipScale = 1.5, blipLength = 2, sound = "dispatch", offset = "false", blipflash = "false"},
["civdead"] = {displayCode = '10-69', description = "Civil afgået ved døden", radius = 0, recipientList = {'ambulance'}, blipSprite = 126, blipColour = 3, blipScale = 1.5, blipLength = 2, sound = "dispatch", offset = "false", blipflash = "false"},
["114call"] = {displayCode = '114', description = "114 opkald", radius = 0, recipientList = {'police', 'ambulance'}, blipSprite = 480, blipColour = 1, blipScale = 1.5, blipLength = 2, sound = "Lose_1st", sound2 = "GTAO_FM_Events_Soundset", offset = "false", blipflash = "false"},
["112call"] = {displayCode = '112', description = "112 opkald", radius = 0, recipientList = {'police', 'ambulance'}, blipSprite = 480, blipColour = 3, blipScale = 1.5, blipLength = 2, sound = "Lose_1st", sound2 = "GTAO_FM_Events_Soundset", offset = "false", blipflash = "false"},
["officerdown"] = {displayCode = '10-99', description = "BETJENT NEDE", radius = 15.0, recipientList = {'police', 'ambulance'}, blipSprite = 526, blipColour = 1, blipScale = 1.5, blipLength = 2, sound = "panicbutton", offset = "false", blipflash = "true"},
["emsdown"] = {displayCode = '10-99', description = "EMS NEDE", radius = 15.0, recipientList = {'police', 'ambulance'}, blipSprite = 526, blipColour = 3, blipScale = 1.5, blipLength = 2, sound = "panicbutton", offset = "false", blipflash = "true"},
["storerobbery"] = {displayCode = '10-90', description = "Butiksrøveri igang", radius = 0, recipientList = {'police'}, blipSprite = 52, blipColour = 1, blipScale = 1.5, blipLength = 2, sound = "Lose_1st", sound2 = "GTAO_FM_Events_Soundset", offset = "false", blipflash = "false"},
["bankrobbery"] = {displayCode = '10-90', description = "Fleeca Bank røveri igang", radius = 0, recipientList = {'police'}, blipSprite = 500, blipColour = 2, blipScale = 1.5, blipLength = 2, sound = "robberysound", offset = "false", blipflash = "false"},
["paletobankrobbery"] = {displayCode = '10-90', description = "Paleto Bank røveri igang", radius = 0, recipientList = {'police'}, blipSprite = 500, blipColour = 12, blipScale = 1.5, blipLength = 2, sound = "robberysound", offset = "false", blipflash = "false"},
["pacificbankrobbery"] = {displayCode = '10-90', description = "Pacific Bank røveri igang", radius = 0, recipientList = {'police'}, blipSprite = 500, blipColour = 5, blipScale = 1.5, blipLength = 2, sound = "robberysound", offset = "false", blipflash = "false"},
["prisonbreak"] = {displayCode = '10-90', description = "Fængselsflugt igang", radius = 0, recipientList = {'police'}, blipSprite = 189, blipColour = 59, blipScale = 1.5, blipLength = 2, sound = "robberysound", offset = "false", blipflash = "false"},
["vangelicorobbery"] = {displayCode = '10-90', description = "Vangelico røveri igang", radius = 0, recipientList = {'police'}, blipSprite = 434, blipColour = 5, blipScale = 1.5, blipLength = 2, sound = "robberysound", offset = "false", blipflash = "false"},
["houserobbery"] = {displayCode = '10-90', description = "House røveri igang", radius = 0, recipientList = {'police'}, blipSprite = 40, blipColour = 5, blipScale = 1.5, blipLength = 2, sound = "Lose_1st", sound2 = "GTAO_FM_Events_Soundset", offset = "false", blipflash = "false"},
["suspicioushandoff"] = {displayCode = '10-60', description = "Mistænkelig levering", radius = 120.0, recipientList = {'police'}, blipSprite = 469, blipColour = 52, blipScale = 0, blipLength = 2, sound = "Lose_1st", sound2 = "GTAO_FM_Events_Soundset", offset = "true", blipflash = "false"},
["yachtheist"] = {displayCode = '10-65', description = "Yacht Heist igang", radius = 0, recipientList = {'police'}, blipSprite = 455, blipColour = 60, blipScale = 1.5, blipLength = 2, sound = "robberysound", offset = "false", blipflash = "false"},
["vehicletheft"] = {displayCode = '10-35', description = "Car Theft igang", radius = 0, recipientList = {'police'}, blipSprite = 595, blipColour = 60, blipScale = 1.5, blipLength = 2, sound = "Lose_1st", sound2 = "GTAO_FM_Events_Soundset", offset = "false", blipflash = "false"},
["signrobbery"] = {displayCode = '10-35', description = "Skiltetyveri", radius = 0, recipientList = {'police'}, blipSprite = 358, blipColour = 60, blipScale = 1.5, blipLength = 2, sound = "Lose_1st", sound2 = "GTAO_FM_Events_Soundset", offset = "false", blipflash = "false"},
["susactivity"] = {displayCode = '10-66', description = "Mistænkelig aktivitet", radius = 0, recipientList = {'police'}, blipSprite = 66, blipColour = 37, blipScale = 0.5, blipLength = 2, sound = "Lose_1st", sound2 = "GTAO_FM_Events_Soundset", offset = "false", blipflash = "false"},
-- Meter Robbery
["meterrobbery"] = {displayCode = '10-90', description = "Parkeringsmeter røveri igang", radius = 0, recipientList = {'LEO', 'police'}, blipSprite = 500, clipColour = 1, blipScale = 1.5, blipLength = 2, sound = "Lose_1st", sound2 = "FTAO_FM_Events_Soundset", offset = "false", blipflash = "false"},
-- ATM Robbery
["atmrobbery"] = {displayCode = '10-90', description = "ATM røveri igang", radius = 0, recipientList = {'police'}, blipSprite = 500, blipColour = 1, blipScale = 1.5, blipLength = 2, sound = "Lose_1st", sound2 = "GTAO_FM_Events_Soundset", offset = "false", blipflash = "false"},
-- Rainmad Heists
["artgalleryrobbery"] = {displayCode = '10-90', description = "Kunstgalleri røveri igang", radius = 0, recipientList = {'police'}, blipSprite = 269, blipColour = 59, blipScale = 1.5, blipLength = 2, sound = "robberysound", offset = "false", blipflash = "false"},
["humanelabsrobbery"] = {displayCode = '10-90', description = "Humane Labs røveri igang", radius = 0, recipientList = {'police'}, blipSprite = 499, blipColour = 1, blipScale = 1.5, blipLength = 2, sound = "robberysound", offset = "false", blipflash = "false"},
["trainrobbery"] = {displayCode = '10-90', description = "Togrøveri igang", radius = 0, recipientList = {'police'}, blipSprite = 667, blipColour = 78, blipScale = 1.5, blipLength = 2, sound = "robberysound", offset = "false", blipflash = "false"},
["vanrobbery"] = {displayCode = '10-90', description = "Pengetransport røveri igang", radius = 0, recipientList = {'police'}, blipSprite = 67, blipColour = 59, blipScale = 1.5, blipLength = 2, sound = "robberysound", offset = "false", blipflash = "false"},
["undergroundrobbery"] = {displayCode = '10-90', description = "Undergrundstunnel røveri igang", radius = 0, recipientList = {'police'}, blipSprite = 486, blipColour = 59, blipScale = 1.5, blipLength = 2, sound = "robberysound", offset = "false", blipflash = "false"},
["drugboatrobbery"] = {displayCode = '10-31', description = "Mistænkelig aktivitet på båd", radius = 0, recipientList = {'police'}, blipSprite = 427, blipColour = 26, blipScale = 1.5, blipLength = 2, sound = "robberysound", offset = "false", blipflash = "false"},
["unionrobbery"] = {displayCode = '10-90', description = "Union Depository røveri igang", radius = 0, recipientList = {'police'}, blipSprite = 500, blipColour = 60, blipScale = 1.5, blipLength = 2, sound = "robberysound", offset = "false", blipflash = "false"},
["carboosting"] = {displayCode = '10-50', description = "Carboosting igang", radius = 0, recipientList = {'police'}, blipSprite = 595, blipColour = 60, blipScale = 1.5, blipLength = 2, sound = "Lose_1st", sound2 = "GTAO_FM_Events_Soundset", offset = "false", blipflash = "false"},
["carjack"] = {displayCode = '10-35', description = "Biltyveri igang", radius = 0, recipientList = {'police'}, blipSprite = 595, blipColour = 60, blipScale = 1.5, blipLength = 2, sound = "Lose_1st", sound2 = "GTAO_FM_Events_Soundset", offset = "false", blipflash = "false"},
["explosion"] = {displayCode = '10-80', description = "Eksplosion meldt", radius = 0, recipientList = {'police'}, blipSprite = 436, blipColour = 1, blipScale = 1.5, blipLength = 2, sound = "Lose_1st", sound2 = "GTAO_FM_Events_Soundset", offset = "false", blipflash = "false"},
}

View File

@ -0,0 +1,133 @@
local QBCore = exports['qb-core']:GetCoreObject()
local calls = {}
function _U(entry)
return Locales[Config.Locale][entry]
end
local function IsPoliceJob(job)
for k, v in pairs(Config.PoliceJob) do
if job == v then
return true
end
end
return false
end
local function IsDispatchJob(job)
for k, v in pairs(Config.PoliceAndAmbulance) do
if job == v then
return true
end
end
return false
end
RegisterServerEvent("dispatch:server:notify", function(data)
local newId = #calls + 1
calls[newId] = data
calls[newId]['source'] = source
calls[newId]['callId'] = newId
calls[newId]['units'] = {}
calls[newId]['responses'] = {}
calls[newId]['time'] = os.time() * 1000
TriggerClientEvent('dispatch:clNotify', -1, data, newId, source)
if not data.alert then
TriggerClientEvent("ps-dispatch:client:AddCallBlip", -1, data.origin, dispatchCodes[data.dispatchcodename], newId)
else
TriggerClientEvent("ps-dispatch:client:AddCallBlip", -1, data.origin, data.alert, newId)
end
end)
function GetDispatchCalls() return calls end
exports('GetDispatchCalls', GetDispatchCalls) --
-- this is mdt call
AddEventHandler("dispatch:addUnit", function(callid, player, cb)
if calls[callid] then
if #calls[callid]['units'] > 0 then
for i=1, #calls[callid]['units'] do
if calls[callid]['units'][i]['cid'] == player.cid then
cb(#calls[callid]['units'])
return
end
end
end
if IsPoliceJob(player.job.name) then
calls[callid]['units'][#calls[callid]['units']+1] = { cid = player.cid, fullname = player.fullname, job = 'Police', callsign = player.callsign }
elseif player.job.name == 'ambulance' then
calls[callid]['units'][#calls[callid]['units']+1] = { cid = player.cid, fullname = player.fullname, job = 'EMS', callsign = player.callsign }
end
cb(#calls[callid]['units'])
end
end)
AddEventHandler("dispatch:sendCallResponse", function(player, callid, message, time, cb)
local Player = QBCore.Functions.GetPlayer(player)
local name = Player.PlayerData.charinfo.firstname.. " " ..Player.PlayerData.charinfo.lastname
if calls[callid] then
calls[callid]['responses'][#calls[callid]['responses']+1] = {
name = name,
message = message,
time = time
}
local player = calls[callid]['source']
if GetPlayerPing(player) > 0 then
TriggerClientEvent('dispatch:getCallResponse', player, message)
end
cb(true)
else
cb(false)
end
end)
-- this is mdt call
AddEventHandler("dispatch:removeUnit", function(callid, player, cb)
if calls[callid] then
if #calls[callid]['units'] > 0 then
for i=1, #calls[callid]['units'] do
if calls[callid]['units'][i]['cid'] == player.cid then
calls[callid]['units'][i] = nil
end
end
end
cb(#calls[callid]['units'])
end
end)
RegisterCommand('togglealerts', function(source, args, user)
local source = source
local Player = QBCore.Functions.GetPlayer(source)
local job = Player.PlayerData.job
if IsPoliceJob(job.name) or job.name == 'ambulance' then
TriggerClientEvent('dispatch:manageNotifs', source, args[1])
end
end, false)
-- Explosion Handler
local ExplosionCooldown = false
AddEventHandler('explosionEvent', function(source, info)
if ExplosionCooldown then return end
for i = 1, (#Config.ExplosionTypes) do
if info.explosionType == Config.ExplosionTypes[i] then
TriggerClientEvent("ps-dispatch:client:Explosion", source)
ExplosionCooldown = true
SetTimeout(1500, function()
ExplosionCooldown = false
end)
end
end
end)
QBCore.Commands.Add("cdb", "Fjern alle dispatch-blips", {}, false, function(source, args)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
local job = Player.PlayerData.job.name
if IsDispatchJob(job) then
TriggerClientEvent('ps-dispatch:client:clearAllBlips', src)
end
end)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,165 @@
$(document).ready(() => {
window.addEventListener('message', function (event) {
let data = event.data;
if (data.update == "newCall") {
addNewCall(data.callID, data.timer, data.data, data.isPolice);
};
});
});
const MONTH_NAMES = [
'Januar', 'Februar', 'Marts', 'April', 'Maj', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'December'
];
function getFormattedDate(date, prefomattedDate = false, hideYear = false) {
const day = date.getDate();
const month = MONTH_NAMES[date.getMonth()];
const year = date.getFullYear();
const hours = date.getHours();
let minutes = date.getMinutes();
if (minutes < 10) {
minutes = `0${minutes}`;
}
if (prefomattedDate) {
return `${prefomattedDate} kl. ${hours}:${minutes}`;
}
if (hideYear) {
return `${day}. ${month} kl. ${hours}:${minutes}`;
}
return `${day}. ${month} ${year}. kl. ${hours}:${minutes}`;
}
function timeAgo(dateParam) {
if (!dateParam) {
return null;
}
const date = typeof dateParam === 'object' ? dateParam : new Date(dateParam);
const DAY_IN_MS = 86400000;
const today = new Date();
const yesterday = new Date(today - DAY_IN_MS);
const seconds = Math.round((today - date) / 1000);
const minutes = Math.round(seconds / 60);
const isToday = today.toDateString() === date.toDateString();
const isYesterday = yesterday.toDateString() === date.toDateString();
const isThisYear = today.getFullYear() === date.getFullYear();
if (seconds < 5) {
return 'Lige nu';
} else if (seconds < 60) {
return `${seconds} sekunder siden`;
} else if (seconds < 90) {
return 'Omkring et minut siden';
} else if (minutes < 60) {
return `${minutes} minutter siden`;
} else if (isToday) {
return getFormattedDate(date, 'I dag');
} else if (isYesterday) {
return getFormattedDate(date, 'I går');
} else if (isThisYear) {
return getFormattedDate(date, false, true);
}
return getFormattedDate(date);
}
function addNewCall(callID, timer, info, isPolice) {
const prio = info['priority']
let DispatchItem;
if (info['isDead']){
DispatchItem = `<div class="dispatch-item ${callID} dispatch-item-${info['isDead']} animate__animated"><div class="top-info-holder"><div class="call-id">#${callID}</div><div class="call-code priority-${prio}">${info.dispatchCode}</div><div class="call-name">${info.dispatchMessage}</div></div><div class="bottom-info-holder">`
}
else{
DispatchItem = `<div class="dispatch-item ${callID} dispatch-item-${isPolice} animate__animated"><div class="top-info-holder"><div class="call-id">#${callID}</div><div class="call-code priority-${prio}">${info.dispatchCode}</div><div class="call-name">${info.dispatchMessage}</div></div><div class="bottom-info-holder">`
}
// Above we are defining a default dispatch item and then we will append the data we have been sent.
if (info['time']) {
DispatchItem += `<div class="call-bottom-info"><span class="fas fa-clock" style="width: 20px;"></span>${timeAgo(info['time'])}</div>`
}
if (info['firstStreet']) {
DispatchItem += `<div class="call-bottom-info"><span class="fas fa-map-pin" style="padding-left:3px;width:16px;></span>${info['firstStreet']}</div>`
}
if (info['heading']) {
DispatchItem += `<div class="call-bottom-info"><span class="fas fa-share" style="padding-left:0px;width:19px;></span>${info['heading']}</div>`
}
if (info['callsign']) {
DispatchItem += `<div class="call-bottom-info"><span class="fas fa-solid fa-eye" style="margin-left:-1px;width:20px;></span>${info['callsign']}</div>`
}
if (info['doorCount']) {
DispatchItem += `<div class="call-bottom-info"><span class="fas fa-door-open" style="margin-left:-3px;width:22px;></span>${info['doorCount']}</div>`
}
if (info['weapon']) {
DispatchItem += `<div class="call-bottom-info"><span class="fas fa-bullseye" style="width:19px;></span>${info['weapon']}</div>`
}
if (info["camId"]) {
DispatchItem += `<div class="call-bottom-info"><span class="fas fa-camera" style="width:19px;></span>${info['camId']}</div>`
}
if (info['gender']) {
DispatchItem += `<div class="call-bottom-info"><span class="fas fa-genderless" style="padding-left:3px;width:16px;></span>${info['gender']}</div>`
}
if (info['model'] && info['plate']) {
DispatchItem += `<div class="call-bottom-info"><span class="fas fa-car" style="width:19px;></span>${info['model']}<span class="fas fa-digital-tachograph" style="margin-left: 2vh;"></span>${info['plate']}</div>`
} else if (info['plate']) {
DispatchItem += `<div class="call-bottom-info"><span class="fas fa-digital-tachograph" style="margin-left:-3px;width:22px;></span>${info['plate']}</div>`
} else if (info['model']) {
DispatchItem += `<div class="call-bottom-info"><span class="fas fa-car" style="width:19px;></span>${info['model']}</div>`
}
if (info['firstColor']) {
DispatchItem += `<div class="call-bottom-info"><span class="fas fa-spray-can" style="padding-left:1px;width:19px;></span>${info['firstColor']}</div>`
}
if (info['automaticGunfire'] == true) {
DispatchItem += `<div class="call-bottom-info"><span class="fab fa-blackberry" style="width:19px;></span>Automatic Gunfire</div>`
}
if (info['name'] && info['number']) {
DispatchItem += `<div class="call-bottom-info"><span class="far fa-id-badge" style="padding-left: 1px; width: 18px;></span>${info['name']}<span class="fas fa-mobile-alt" style="margin-left: 2vh;"></span>${info['number']}</div>`
} else if (info['number']) {
DispatchItem += `<div class="call-bottom-info"><span class="fas fa-mobile-alt" style="padding-left:3px;width:16px;></span>${info['number']}</div>`
} else if (info['name']) {
DispatchItem += `<div class="call-bottom-info"><span class="far fa-id-badge" style="padding-left: 1px; width: 18px;></span>${info['name']}</div>`
}
if (info['information']) {
DispatchItem += `<div class="line"></div><div class="call-bottom-info call-bottom-information"><span class="far fa-question-circle" style="width:19px;></span>${info['information']}</div>`
}
DispatchItem += `</div></div>`
$(".dispatch-holder").prepend(DispatchItem)
var timer = 4000
if (prio == 1) {
timer = 12000
} else if (prio == 2) {
timer = 9000
}
$(`.${callID}`).addClass("animate__backInRight");
setTimeout(() => {
$(`.${callID}`).addClass("animate__backOutRight");
setTimeout(() => {
$(`.${callID}`).remove();
}, 1000);
}, timer || 4500);
};

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QB Dispatch</title>
<link rel="stylesheet" href="style.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css">
<script src="app.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" />
</head>
<body>
<div class="dispatch-container">
<div class="dispatch-holder">
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,198 @@
@import url('https://api.hawk.quest/fonts/SF-Pro-Rounded-Regular.woff2');
body {
font-family: 'SF Pro Rounded Regular';
overflow: hidden;
/* display: none; */
text-shadow: 1px 1px 1px rgb(0 0 0 / 32%);
}
::-webkit-scrollbar {
width: 0px;
}
.dispatch-container {
width: 100%;
height: 100%;
position: absolute;
display: flex;
}
.dispatch-holder {
width: 48vh;
height: 50vh;
margin: auto;
margin-top: 21.75vh;
margin-right: 3vh;
display: flex;
flex-direction: column;
overflow-y: scroll;
}
.dispatch-item {
/* -webkit-text-stroke: 0.1mm black; */
/* background-image: url('https://web.archive.org/web/20231211222955/https://cdn.discordapp.com/attachments/1021510724754878484/1043206485620043777/imageedit_6_2893157692.png'); */
background-size: 28px;
background-repeat: no-repeat;
/* background-position-x: right;
background-position-y: top; */
background-position: right 2% bottom 87%;
/* border-radius: 5px; */
margin: auto;
margin-top: 5vh;
margin-right: 1vh;
margin-bottom: .5vh;
background-color: #1401019c;
border-right: .75vh solid rgba(217, 249, 255, 0.671);
min-width: 45.5vh;
width: fit-content;
height: fit-content;
border-radius: 1.1vh;
}
.dispatch-item-true {
background-color: #00030594;
}
.dispatch-item-false {
background-color: rgba(119, 20, 20, 0.911);
border-right: .75vh solid rgb(255, 0, 0);
}
.dispatch-item-officer {
background-color: rgb(97, 3, 11);
}
.top-info-holder {
scale: 0.5px;
width: 95%;
height: fit-content;
margin: unset;
margin-top: .75vh;
margin-left: .75vh;
align-items: center;
display: flex;
flex-direction: row;
color: white;
font-size: 1vh;
white-space: nowrap;
}
.call-id {
background-color: #93f167;
padding: .35vh;
padding-bottom: 0.4vh;
padding-left: 1vh;
padding-right: 1vh;
margin-top: auto;
margin-bottom: auto;
margin-left: 0vh;
margin-right: 0.4vh;
border-radius: 1vh;
text-align: center;
color: black;
}
.call-code {
background-color: #000b0e;
padding: .35vh;
padding-bottom: 0.4vh;
padding-left: 1vh;
padding-right: 1vh;
margin-top: auto;
margin-bottom: auto;
margin-left: 0vh;
margin-right: 0.5vh;
border-radius: 1vh;
text-align: center;
}
.call-name {
margin-top: auto;
margin-bottom: auto;
margin-left: 0vh;
margin-right: auto;
border-radius: 1vh;
text-align: left;
font-size: 1.4vh;
overflow: hidden;
text-overflow: ellipsis;
text-transform: uppercase;
}
.bottom-info-holder {
-webkit-text-stroke: 0.01mm black;
width: 95%;
height: fit-content;
margin: auto;
margin-top: .75vh;
margin-bottom: .6vh;
align-items: center;
display: flex;
flex-direction: column;
color: white;
font-size: 1.2vh;
font-weight: bold;
white-space: nowrap;
text-align: left;
}
.call-bottom-info {
margin: auto;
margin-left: 0vh;
margin-bottom: .5vh;
text-align: left;
overflow: wrap;
white-space: normal;
}
.line {
background-color: rgb(0, 4, 7);
height: 0.05vh;
width: 100%;
margin-top: 0.1vh;
margin-bottom: 0.1vh
}
.call-bottom-information {
margin-top: 0.5vh;
}
.fas {
margin-right: .5vh;
}
.fab {
margin-right: .5vh;
}
.far {
margin-right: .5vh;
}
.fab {
margin-right: .5vh;
}
.priority-1 {
background: linear-gradient(270deg, #b70000, #00020a);
background-size: 400% 400%;
animation: gradient 3s ease infinite;
background-color: #000b0e00;
}
.priority-2 {
background-color: #ff2e2e;
color: black;
}
@keyframes gradient {
0% {
background-position: 0% 50%
}
50% {
background-position: 100% 50%
}
100% {
background-position: 0% 50%
}
}

Binary file not shown.

View File

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

View File

@ -0,0 +1,27 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

2
resources/[ps]/ps-housing/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.DS_Store

View File

@ -0,0 +1,458 @@
Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
https://creativecommons.org/licenses/by-nc-sa/4.0/
This is a human-readable summary of (and not a substitute for) the license. Disclaimer.
You are free to:
Share — copy and redistribute the material in any medium or format
Adapt — remix, transform, and build upon the material
The licensor cannot revoke these freedoms as long as you follow the license terms.
Under the following terms:
Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
NonCommercial — You may not use the material for commercial purposes.
ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
Notices:
You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation.
No warranties are given. The license may not give you all of the permissions necessary for your intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material.
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
Public License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-NonCommercial-ShareAlike 4.0 International Public License
("Public License"). To the extent this Public License may be
interpreted as a contract, You are granted the Licensed Rights in
consideration of Your acceptance of these terms and conditions, and the
Licensor grants You such rights in consideration of benefits the
Licensor receives from making the Licensed Material available under
these terms and conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. BY-NC-SA Compatible License means a license listed at
creativecommons.org/compatiblelicenses, approved by Creative
Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
e. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
g. License Elements means the license attributes listed in the name
of a Creative Commons Public License. The License Elements of this
Public License are Attribution, NonCommercial, and ShareAlike.
h. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
i. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
j. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
k. NonCommercial means not primarily intended for or directed towards
commercial advantage or monetary compensation. For purposes of
this Public License, the exchange of the Licensed Material for
other material subject to Copyright and Similar Rights by digital
file-sharing or similar means is NonCommercial provided there is
no payment of monetary compensation in connection with the
exchange.
l. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
m. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
n. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part, for NonCommercial purposes only; and
b. produce, reproduce, and Share Adapted Material for
NonCommercial purposes only.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. Additional offer from the Licensor -- Adapted Material.
Every recipient of Adapted Material from You
automatically receives an offer from the Licensor to
exercise the Licensed Rights in the Adapted Material
under the conditions of the Adapter's License You apply.
c. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties, including when
the Licensed Material is used other than for NonCommercial
purposes.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
b. ShareAlike.
In addition to the conditions in Section 3(a), if You Share
Adapted Material You produce, the following conditions also apply.
1. The Adapter's License You apply must be a Creative Commons
license with the same License Elements, this version or
later, or a BY-NC-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the
Adapter's License You apply. You may satisfy this condition
in any reasonable manner based on the medium, means, and
context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms
or conditions on, or apply any Effective Technological
Measures to, Adapted Material that restrict exercise of the
rights granted under the Adapter's License You apply.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database for NonCommercial purposes
only;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material,
including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public
licenses. Notwithstanding, Creative Commons may elect to apply one of
its public licenses to material it publishes and in those instances
will be considered the “Licensor.” The text of the Creative Commons
public licenses is dedicated to the public domain under the CC0 Public
Domain Dedication. Except for the limited purpose of indicating that
material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the
public licenses.
Creative Commons may be contacted at creativecommons.org.

View File

@ -0,0 +1,344 @@
# ps-housing
ps-housing is a resource that opens up a world of creative possibilities for housing. Its user-friendly interface lets you decorate any location to your heart's content. The best part? Not only is it completely free, but it's also reliable and functional, unlike many other housing systems available. Dive in and start transforming spaces with ps-housing today!
ps-housing owes its existence to the exceptional coding expertise of [Xirvin#0985](https://github.com/ImXirvin). His application of top-tier coding practices has been instrumental in creating this script. We at Project Sloth are thrilled that he has joined our team and utilized our platform to deliver this incredible, much-anticipated resource. Our sincere appreciation goes out to [Xirvin#0985](https://github.com/ImXirvin) for his outstanding contribution!
# Preview [ps-housing](https://github.com/Project-Sloth/ps-housing)
![image](https://github.com/Project-Sloth/ps-housing/assets/82112471/07b7f8c6-38ea-4f8c-95b6-9bd6bafbbd09)
![image](https://github.com/Project-Sloth/ps-housing/assets/82112471/163ae847-5a44-48cb-89f5-e0c1e7b59383)
![image](https://github.com/Project-Sloth/ps-housing/assets/82112471/655d9bb6-6c6d-4676-b4e0-f4368f3325a9)
![image](https://github.com/Project-Sloth/ps-housing/assets/82112471/fc632975-c2f6-41fb-89cd-a984679f1a41)
# Preview [ps-realtor](https://github.com/Project-Sloth/ps-realtor)
![image](https://github.com/Project-Sloth/ps-realtor/assets/82112471/24e4018a-cb97-42b0-81df-3b0236c7e2dc)
![image](https://github.com/Project-Sloth/ps-realtor/assets/82112471/4d8ece54-ace1-4ffc-b8fb-90274bc94e72)
![image](https://github.com/Project-Sloth/ps-realtor/assets/82112471/188d259c-4c0f-4c91-905c-bf9b826cc518)
![image](https://github.com/Project-Sloth/ps-realtor/assets/82112471/9e033984-45f2-449d-ba6c-bb8742ac08bd)
![image](https://github.com/Project-Sloth/ps-realtor/assets/82112471/0dd078b8-a941-4316-b9e1-26c696023139)
# Usage
- Players can decorate their houses and apartments with a full selection of furniture and decorations (included a wide variety of custom housing props)
- Provides support for housing and apartments and is a full replacement for qb-apartments and qb-housing
- When a player first spawns after enabling ps-housing, they will have to choose an apartment. Once they spawn in the stashitems from their previous qb-apartment will be migrated to their new apartment stash.
- Allows players to purchase and list houses for sale through `ps-realtor` and the realtor job
- Houses come with personal garages
- Houses and apartments come with personal wardrobes and stashes
- Players can share keys to their houses and apartments with other players
## Creating a new property for sale
Players must have the realtor job to create new properties. Additionally if the realtor has a high enough grade level, they can also help players move to new apartments.
All properties must be manually configured for sale by the realtor job, giving you full control over all aspects of properties, and bringing another avenue of roleplay to your server.
- Pick the location where you want to create a new property
- Use `/housing` to open the housing menu
- Click on create new property
- Fill out the details of the property (name, price, description, which shell to use, etc)
- Choose the door location (this is where the person will enter the house)
- Ensure that you place it up against a wall, since players will use target to enter the house
- Choose the garage location
- This point is used both for storing vehicles, as well as the location where the vehicle will spawn when taken out of the garage
- Realtors can edit the details of the property by clicking on the property in the housing menu
- Players can see the properties for sale through the /housing menu as well
## Furnish and decorate a property
Once inside the property, the player can furnish and decorate the property to their liking. They can also invite other players to their property, and give them access to the property. Open the furniture store by pressing `Z`.
This will open a furniture store complete with all of the props. Select an item from the catalog and place it into the property. You can use the placement gizmo to position the item to your liking as well as use the UI tools for fine tune control over the placement. Once you are happy with the positioning, make sure you press `Add to Cart` before moving on. Continue to add as many items as you want to your cart. Once you are done, go to the `Checkout` and purchase the items.
> Note: The place on ground button sometimes does not work properly depending on where the native detects the ground to be.
## Dynamic Doors
Dynamic Doors will turn placed doors into actual working doors, Instead of them being static. (See videos below)
### Preview
https://github.com/complexza/ps-housing/assets/74205343/72cfc135-2f78-42b3-a540-45f02567b6d7
https://github.com/complexza/ps-housing/assets/74205343/0ff26e7f-1341-45fc-8fc6-d65421dec0b2
### Setup
- You will need to set the `Config.DynamicDoors = true`
- You will have to add this convar into your server.cfg `setr game_enableDynamicDoorCreation "true"`
> Note: The convar has to be in your server.cfg in order for the doors to be dynamic!
### Shell Support
* [K4MB1](https://github.com/Project-Sloth/ps-housing/wiki/K4MB1-Shells-Support-&-Offsets)
# Important
* Players need to place their [stash](https://github.com/Project-Sloth/ps-housing/blob/7efd2009050b9a20969877cf69b284352a9309bf/shared/config.lua#LL426C96-L426C96) and [wardrobe](https://github.com/Project-Sloth/ps-housing/blob/7efd2009050b9a20969877cf69b284352a9309bf/shared/config.lua#L427) or else they wont have one. Check [Config](https://github.com/Project-Sloth/ps-housing/blob/7efd2009050b9a20969877cf69b284352a9309bf/shared/config.lua#L422) for more information.
* This entire README is meant for compatibility with default QBCore scripts. If you have different scripts, you'll need to adjust them for compatibility yourself. Refrain from asking us how to circumvent paid scripts that can't be adjusted for ps-housing support. Instead, request their support for ps-housing - this script is fully open source for that reason. Any inquiries related to this be ignored.
# Installation
## PAY ATTENTION TO EACH STEP. DO NOT SKIP ANY.
1. Find the following events in `qb-multicharacter` and change in server/main.lua event to:
`qb-multicharacter > server > main.lua`
```lua
RegisterNetEvent('qb-multicharacter:server:loadUserData', function(cData)
local src = source
if QBCore.Player.Login(src, cData.citizenid) then
repeat
Wait(10)
until hasDonePreloading[src]
print('^2[qb-core]^7 '..GetPlayerName(src)..' (Citizen ID: '..cData.citizenid..') has succesfully loaded!')
QBCore.Commands.Refresh(src)
TriggerClientEvent('ps-housing:client:setupSpawnUI', src, cData)
TriggerEvent("qb-log:server:CreateLog", "joinleave", "Loaded", "green", "**".. GetPlayerName(src) .. "** (<@"..(QBCore.Functions.GetIdentifier(src, 'discord'):gsub("discord:", "") or "unknown").."> | ||" ..(QBCore.Functions.GetIdentifier(src, 'ip') or 'undefined') .. "|| | " ..(QBCore.Functions.GetIdentifier(src, 'license') or 'undefined') .." | " ..cData.citizenid.." | "..src..") loaded..")
end
end)
```
`qb-multicharacter > server > main.lua`
```lua
RegisterNetEvent('qb-multicharacter:server:createCharacter', function(data)
local src = source
local newData = {}
newData.cid = data.cid
newData.charinfo = data
if QBCore.Player.Login(src, false, newData) then
repeat
Wait(10)
until hasDonePreloading[src]
print('^2[qb-core]^7 '..GetPlayerName(src)..' has succesfully loaded!')
QBCore.Commands.Refresh(src)
TriggerClientEvent("qb-multicharacter:client:closeNUI", src)
newData.citizenid = QBCore.Functions.GetPlayer(src).PlayerData.citizenid
TriggerClientEvent('ps-housing:client:setupSpawnUI', src, newData)
GiveStarterItems(src)
end
end)
```
2. Find the following events in `qb-spawn` and change in client/client.lua event to:
`qb-spawn > client.lua > line 51 > 'qb-spawn:client:setupSpawns' event`
```lua
RegisterNetEvent('qb-spawn:client:setupSpawns', function(cData, new, apps)
if not new then
QBCore.Functions.TriggerCallback('qb-spawn:server:getOwnedHouses', function(houses)
local myHouses = {}
if houses ~= nil then
for i = 1, (#houses), 1 do
local house = houses[i]
myHouses[#myHouses+1] = {
house = house,
label = (house.apartment or house.street) .. " " .. house.property_id,
}
end
end
Wait(500)
SendNUIMessage({
action = "setupLocations",
locations = QB.Spawns,
houses = myHouses,
isNew = new
})
end, cData.citizenid)
elseif new then
SendNUIMessage({
action = "setupAppartements",
locations = apps,
isNew = new
})
end
end)
```
`qb-spawn > client.lua > line 134 > 'chooseAppa' NUI Callback`
```lua
RegisterNUICallback('chooseAppa', function(data, cb)
local ped = PlayerPedId()
local appaYeet = data.appType
SetDisplay(false)
DoScreenFadeOut(500)
Wait(100)
FreezeEntityPosition(ped, false)
RenderScriptCams(false, true, 0, true, true)
SetCamActive(cam, false)
DestroyCam(cam, true)
SetCamActive(cam2, false)
DestroyCam(cam2, true)
SetEntityVisible(ped, true)
Wait(500)
TriggerServerEvent('QBCore:Server:OnPlayerLoaded')
TriggerEvent('QBCore:Client:OnPlayerLoaded')
Wait(100)
TriggerServerEvent("ps-housing:server:createNewApartment", appaYeet)
cb('ok')
end)
```
`qb-spawn > client > client.lua > line 169 'spawnplayer' NUI Callback`
```lua
RegisterNUICallback('spawnplayer', function(data, cb)
local location = tostring(data.spawnloc)
local type = tostring(data.typeLoc)
local ped = PlayerPedId()
local PlayerData = QBCore.Functions.GetPlayerData()
local insideMeta = PlayerData.metadata["inside"]
if type == "current" then
PreSpawnPlayer()
QBCore.Functions.GetPlayerData(function(pd)
ped = PlayerPedId()
SetEntityCoords(ped, pd.position.x, pd.position.y, pd.position.z)
SetEntityHeading(ped, pd.position.a)
FreezeEntityPosition(ped, false)
end)
TriggerServerEvent('QBCore:Server:OnPlayerLoaded')
TriggerEvent('QBCore:Client:OnPlayerLoaded')
if insideMeta.property_id ~= nil then
local property_id = insideMeta.property_id
TriggerServerEvent('ps-housing:server:enterProperty', tostring(property_id))
end
PostSpawnPlayer()
elseif type == "house" then
PreSpawnPlayer()
TriggerServerEvent('QBCore:Server:OnPlayerLoaded')
TriggerEvent('QBCore:Client:OnPlayerLoaded')
local property_id = data.spawnloc.property_id
TriggerServerEvent('ps-housing:server:enterProperty', tostring(property_id))
PostSpawnPlayer()
elseif type == "normal" then
local pos = QB.Spawns[location].coords
PreSpawnPlayer()
SetEntityCoords(ped, pos.x, pos.y, pos.z)
TriggerServerEvent('QBCore:Server:OnPlayerLoaded')
TriggerEvent('QBCore:Client:OnPlayerLoaded')
TriggerServerEvent('ps-housing:server:resetMetaData')
SetEntityCoords(ped, pos.x, pos.y, pos.z)
SetEntityHeading(ped, pos.w)
PostSpawnPlayer()
end
cb('ok')
end)
```
`qb-spawn > server.lua > line 3`
```lua
QBCore.Functions.CreateCallback('qb-spawn:server:getOwnedHouses', function(_, cb, cid)
if cid ~= nil then
local houses = MySQL.query.await('SELECT * FROM properties WHERE owner_citizenid = ?', {cid})
if houses[1] ~= nil then
cb(houses)
else
cb({})
end
else
cb({})
end
end)
```
3. Find the following events in `qb-garages` and change:
`qb-garages > server > main.lua > around line 120` on event `qb-garage:server:checkOwnership`
Replace
```lua
local hasHouseKey = exports['qb-houses']:hasKey(result[1].license, result[1].citizenid, house)
```
With
```lua
local hasHouseKey = exports['ps-housing']:IsOwner(src, house)
```
`qb-garages > client > main.lua > around line 451` under event `qb-garages:client:addHouseGarage`
```lua
RegisterNetEvent('qb-garages:client:removeHouseGarage', function(house)
Config.HouseGarages[house] = nil
end)
```
4. Run the `properties.sql` file, but be cautious. If a table named `properties` already exists in your database, this operation will drop it, resulting in the loss of all its data.
5. Delete default [qb-apartments](https://github.com/qbcore-framework/qb-apartments)
6. Delete default [qb-houses](https://github.com/qbcore-framework/qb-houses)
7. Delete `qb-apartments/config.lua` references in `qb-spawn`, `qb-multicharacter` and `qb-phone` fxmanifest.lua (and any other scripts that may reference it).
8. Ensure ps-realtor above ps-housing.
9. In your server.cfg, add `ensure ox_lib` above all other resources
10. Install the dependencies below.
## Migrating houses and apartments from qb-houses and qb-apartments
1. From a client run the `migratehouses` command to automatically convert all houses from qb-houses. It will print a message to the console once complete.
**The `migratehouses` command MUST be run from a client in order to retrieve street and region data for each house**
2. From a client or server console run the `migrateapartments` command to automatically convert all apartments from qb-apartments. It will print a message to the console once complete.
## Resolving the "Foreign key constraint is incorrectly formed" Issue
If you come across an error such as `Foreign key constraint is incorrectly formed` while importing the `properties.sql` into your database, follow these steps to fix it.
1. Open your database in HeidiSQL.
2. Right-click on your database name and select "Edit."
3. Locate the database collation setting take a note of it.
4. You will need to format the `properties.sql` file to match your database collation.
If your database collation is set to `utf8mb4_general_ci`, modify the last line of the `properties.sql` file using VSCode or in HeidiSQL's query tab to the following:
```sql
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
```
This adjustment ensures that `properties.sql` file's character set and collation match that of your database, effectively resolving the issue.
## Item Limits System
1. Choose an item you want to limit under `Config.Furniture` in under `shared/config.lua`
2. Add `["max"] = 3` or the number of your choice to the item (see example below)
```lua
{ ["object"] = "v_res_r_figcat", ["price"] = 300, ["max"] = 2, ["label"] = "Fig Cat" },
```
## Logs System Setup
1. Go to `qb-smallresources/server/logs.lua` and add this:
```lua
['pshousing'] = 'yourdiscordwebhookhere',
```
2. Create a webhook for the channel you want the logs to show up in.
3. Replace the placeholder with your webhook link.
> This system only supports qb-core for now.
## Add new Shells
1. Copy the files from the shell into the `ps-housing/stream` folder.
2. Add the new shell into the Config.Shells in `ps-housing/shared/config.lua`.
# Dependency
1. [ps-realtor](https://github.com/Project-Sloth/ps-realtor)
2. [five-freecam](https://github.com/Deltanic/fivem-freecam)
3. [ox_lib](https://github.com/overextended/ox_lib/releases) - Use the latest release. If you do not use the latest release, MAKE SURE TO BUILD THE UI. Find their docs [here](https://overextended.dev/ox_lib#building-the-ui) on how to build the UI.
4. [ox_target](https://github.com/overextended/ox_target) or [qb-target](https://github.com/qbcore-framework/qb-target) - Change in [Config](https://github.com/Project-Sloth/ps-housing/blob/3c0f197b6d639f13235598393c84aac8d23d5f7a/shared/config.lua#L8), default is qb-target.
## For reference your server.cfg should be ensured like below:
* We highly recommend making a folder named [ps-housing] and add `ps-realtor`, `fivem-freecam`, `ox_lib`, `ps-core`, `ps-housing` inside the folder.
```
ensure ox_lib
ensure ps-housing
ensure ps-realtor
ensure fivem-freecam
```
# Notes
- If a player is in their apartment/house and an admin does a "Bring to me" function, they will not see the player nor will the player see anyone else. This is because the player is still in their own unique routing bucket. **Workaround**: To fix this, the player must go back into their apartment and leave on their own.
- Likewise, if an admin tries to "Go to" or "Spectate" a player that is in their apartment/house, the admin will not be able to see the apartment or player because it is in a different routing bucket.
# Credits
* [Xirvin](https://github.com/ImXirvin)
* [BackSH00TER](https://github.com/backsh00ter)
* [Byte Labs Project](https://github.com/Byte-Labs-Project)
* [Project Sloth Team](https://discord.gg/projectsloth)
* [K4MB1](https://www.k4mb1maps.com/)
* [Candrex](https://github.com/CandrexDev)

View File

@ -0,0 +1,171 @@
-- TBH I should have learnt how lua inheritance worked instead of making a new class but oh well. Maybe next time
Apartment = {
apartmentData = {},
apartments = {},
entranceTarget = nil,
blip = nil,
}
Apartment.__index = Apartment
function Apartment:new(apartmentData)
local self = setmetatable({}, Apartment)
self.apartmentData = apartmentData
self.apartments = {}
self:RegisterPropertyEntrance()
self:CreateBlip()
return self
end
function Apartment:RegisterPropertyEntrance()
local door = self.apartmentData.door
local targetName = string.format("%s_apartment",self.apartmentData.label)
-- not sure why but referencing self directy runs it when registering the zones
local function enterApartment()
self:EnterApartment()
end
local function seeAll()
self:GetMenuForAll()
end
local function seeAllToRaid()
self:GetMenuForAllToRaid()
end
local size = vector3(door.length, door.width, 3.0)
local heading = door.h
Framework[Config.Target].AddApartmentEntrance(door, size, heading, self.apartmentData.label, enterApartment, seeAll, seeAllToRaid, targetName)
end
function Apartment:EnterApartment()
for propertyId, _ in pairs(self.apartments) do
local property = PropertiesTable[propertyId]
if property.owner then
TriggerServerEvent('ps-housing:server:enterProperty', propertyId)
return
end
end
Framework[Config.Notify].Notify("Du har ingen lejlighed her.", "error")
end
function Apartment:GetMenuForAll()
if next(self.apartments) == nil then
Framework[Config.Notify].Notify("Der er ingen lejligheder her.", "error")
return
end
local id = "apartments-" .. self.apartmentData.label
local menu = {
id = id,
title = "Lejligheder",
options = {}
}
for propertyId, _ in pairs(self.apartments) do
table.insert(menu.options,{
title = self.apartmentData.label .. " " .. propertyId,
onSelect = function()
TriggerServerEvent('ps-housing:server:enterProperty', propertyId)
end,
})
end
lib.registerContext(menu)
lib.showContext(id)
end
function Apartment:GetMenuForAllToRaid()
if next(self.apartments) == nil then
Framework[Config.Notify].Notify("Der er ingen lejligheder her.", "error")
return
end
local id = "apartments-" .. self.apartmentData.label
local menu = {
id = id,
title = "Lejligheder til ransagning",
options = {}
}
for propertyId, _ in pairs(self.apartments) do
table.insert(menu.options,{
title = "Ransag " .. self.apartmentData.label .. " " .. propertyId,
onSelect = function()
TriggerServerEvent("ps-housing:server:raidProperty", propertyId)
end,
})
end
lib.registerContext(menu)
lib.showContext(id)
end
function Apartment:CreateBlip(hasProperty)
self:DeleteBlip()
local door = self.apartmentData.door
local blip = AddBlipForCoord(door.x, door.y, door.z)
SetBlipSprite(blip, 475)
SetBlipScale(blip, 0.8)
if hasProperty then
SetBlipColour(blip, 2)
else
SetBlipColour(blip, 4)
end
SetBlipAsShortRange(blip, true)
BeginTextCommandSetBlipName("STRING")
AddTextComponentString(self.apartmentData.label)
EndTextCommandSetBlipName(blip)
self.blip = blip
end
function Apartment:DeleteBlip()
if not self.blip then return end
RemoveBlip(self.blip)
self.blip = nil
end
function Apartment:AddProperty(propertyId)
self.apartments[propertyId] = true
local property = Property.Get(propertyId)
if property ~= nil and property.owner then
self:CreateBlip(true)
end
end
function Apartment:RemoveProperty(propertyId)
if not self:HasProperty(propertyId) then return end
self.apartments[propertyId] = nil
local property = Property.Get(propertyId)
if property.owner then
self:CreateBlip(false)
end
end
function Apartment:HasProperty(propertyId)
return self.apartments[propertyId] ~= nil
end
function Apartment:RemoveApartment()
local targetName = string.format("%s_apartment",self.apartmentData.label)
Framework[Config.Target].RemoveTargetZone(targetName)
self:DeleteBlip()
self = {}
end
function Apartment.Get(apartmentName)
return ApartmentsTable[apartmentName]
end

View File

@ -0,0 +1,741 @@
Property = {
property_id = nil,
propertyData = nil,
shell = nil,
shellData = nil,
inProperty = false,
shellObj = nil,
has_access = false,
owner = false,
storageTarget = nil,
clothingTarget = nil,
furnitureObjs = {},
garageZone = nil,
doorbellPool = {},
entranceTarget = nil, -- needed for ox target
exitTarget = nil, -- needed for ox target
blip = nil,
}
Property.__index = Property
function Property:new(propertyData)
local self = setmetatable({}, Property)
self.property_id = tostring(propertyData.property_id)
-- Remove furnitures from property data for memory purposes
propertyData.furnitures = {}
self.propertyData = propertyData
local citizenid = PlayerData.citizenid
self.owner = propertyData.owner == citizenid
self.has_access = lib.table.contains(self.propertyData.has_access, citizenid)
if propertyData.apartment then
local aptName = propertyData.apartment
local apartment = ApartmentsTable[aptName]
if not apartment and Config.Apartments[aptName] then
ApartmentsTable[aptName] = Apartment:new(Config.Apartments[aptName])
apartment = ApartmentsTable[aptName]
elseif not apartment then
Debug(aptName .. " not found in Config")
return
end
apartment:AddProperty(self.property_id)
else
self:RegisterPropertyEntrance()
self:RegisterGarageZone()
end
return self
end
function Property:GetDoorCoords()
local coords = nil
local dataApartment = self.propertyData.apartment
if dataApartment then
local apartment = dataApartment
coords = Config.Apartments[apartment].door
else
coords = self.propertyData.door_data
end
return coords
end
function Property:CreateShell()
local coords = self:GetDoorCoords()
coords = vec3(coords.x, coords.y, coords.z - 25.0)
self.shell = Shell:CreatePropertyShell(self.propertyData.shell, coords)
self.shellObj = self.shell.entity
local doorOffset = self.shellData.doorOffset
local offset = GetOffsetFromEntityInWorldCoords(self.shellObj, doorOffset.x, doorOffset.y, doorOffset.z)
self:RegisterDoorZone(offset)
SetEntityCoordsNoOffset(cache.ped, offset.x, offset.y, offset.z, false, false, true)
SetEntityHeading(cache.ped, self.shellData.doorOffset.h)
end
function Property:RegisterDoorZone(offset)
local function leave()
self:LeaveShell()
end
local function checkDoor()
self:OpenDoorbellMenu()
end
local coords = offset
local size = vector3(1.0, self.shellData.doorOffset.width, 3.0)
local heading = self.shellData.doorOffset.h
self.exitTarget = Framework[Config.Target].AddDoorZoneInside(coords, size, heading, leave, checkDoor)
end
function Property:RegisterPropertyEntrance()
local door = self.propertyData.door_data
local size = vector3(door.length, door.width, 2.5)
local heading = door.h
--Can be anon functions but I like to keep them named its more readable
local function enter()
TriggerServerEvent("ps-housing:server:enterProperty", self.property_id)
end
local function raid()
TriggerServerEvent("ps-housing:server:raidProperty", self.property_id)
end
local function showcase()
TriggerServerEvent("ps-housing:server:showcaseProperty", self.property_id)
end
local function showData()
local data = lib.callback.await("ps-housing:cb:getPropertyInfo", source, self.property_id)
if not data then return end
local content = "**Ejer:** " .. data.owner .. " \n" .. "**Beskrivelse:** " .. data.description .. " \n" .. "**Gade:** " .. data.street .. " \n" .. "**Region:** " .. data.region .. " \n" .. "**Interiør:** " .. data.shell .. " \n" .. "**Til salg:** " .. (data.for_sale and "Ja" or "Nej")
if data.for_sale then
content = content .. " \n" .. "**Pris:** " .. data.price.. ",-"
end
lib.alertDialog({
header = data.street .. " " .. data.property_id,
content = content,
centered = true,
})
end
local targetName = string.format("%s_%s", self.propertyData.street, self.property_id)
self.entranceTarget = Framework[Config.Target].AddEntrance(door, size, heading, self.property_id, enter, raid, showcase, showData, targetName)
if self.owner or self.has_access then
self:CreateBlip()
end
end
function Property:UnregisterPropertyEntrance()
if not self.entranceTarget then return end
Framework[Config.Target].RemoveTargetZone(self.entranceTarget)
self.entranceTarget = nil
end
function Property:RegisterGarageZone()
if not next(self.propertyData.garage_data) then return end
if not (self.has_access or self.owner) or not self.owner then
return
end
local garageData = self.propertyData.garage_data
local garageName = string.format("property-%s-garage", self.property_id)
local data = {
takeVehicle = {
x = garageData.x,
y = garageData.y,
z = garageData.z,
w = garageData.h
},
type = "house",
label = self.propertyData.street .. self.property_id .. " Garage",
}
TriggerEvent("qb-garages:client:addHouseGarage", self.property_id, data)
self.garageZone = lib.zones.box({
coords = vec3(garageData.x, garageData.y, garageData.z),
size = vector3(garageData.length + 5.0, garageData.width + 5.0, 3.5),
rotation = garageData.h,
debug = Config.DebugMode,
onEnter = function()
TriggerEvent('qb-garages:client:setHouseGarage', self.property_id, true)
end,
})
end
function Property:UnregisterGarageZone()
if not self.garageZone then return end
TriggerEvent("qb-garages:client:removeHouseGarage", self.property_id)
self.garageZone:remove()
self.garageZone = nil
end
function Property:EnterShell()
DoScreenFadeOut(250)
TriggerServerEvent("InteractSound_SV:PlayOnSource", "houses_door_open", 0.25)
Wait(250)
self.inProperty = true
self.shellData = Config.Shells[self.propertyData.shell]
self:CreateShell()
self:LoadFurnitures()
self:GiveMenus()
Wait(250)
DoScreenFadeIn(250)
end
function Property:LeaveShell()
if not self.inProperty then return end
DoScreenFadeOut(250)
TriggerServerEvent("InteractSound_SV:PlayOnSource", "houses_door_open", 0.25)
Wait(250)
local coords = self:GetDoorCoords()
SetEntityCoordsNoOffset(cache.ped, coords.x, coords.y, coords.z, false, false, true)
TriggerServerEvent("ps-housing:server:leaveProperty", self.property_id)
self:UnloadFurnitures()
self.propertyData.furnitures = {}
self.shell:DespawnShell()
self.shell = nil
if self.exitTarget then
Framework[Config.Target].RemoveTargetZone(self.exitTarget)
self.exitTarget = nil
end
self:RemoveBlip()
self:RemoveMenus()
self.doorbellPool = {}
self.inProperty = false
Wait(250)
DoScreenFadeIn(250)
end
function Property:GiveMenus()
if not self.inProperty then return end
local accessAndConfig = self.has_access and Config.AccessCanEditFurniture
if self.owner or accessAndConfig then
Framework[Config.Radial].AddRadialOption(
"furniture_menu",
"Indretning",
"house-user",
function()
Modeler:OpenMenu(self.property_id)
end,
"ps-housing:client:openFurnitureMenu",
{ propertyId = self.property_id }
)
end
if self.owner then
Framework[Config.Radial].AddRadialOption(
"access_menu",
"Ejendomsadgang",
"key",
function()
self:ManageAccessMenu()
end,
"ps-housing:client:openManagePropertyAccessMenu",
{ propertyId = self.property_id }
)
end
end
function Property:RemoveMenus()
if not self.inProperty then return end
Framework[Config.Radial].RemoveRadialOption("furniture_menu")
if self.owner then
Framework[Config.Radial].RemoveRadialOption("access_menu")
end
end
function Property:ManageAccessMenu()
if not self.inProperty then return end
if not self.owner then
Framework[Config.Notify].Notify("Kun ejeren kan gøre dette.", "error")
return
end
--Fuck qb-menu
local id = "property-" .. self.property_id .. "-access"
local menu = {
id = id,
title = "Administrer adgang",
options = {},
}
menu.options[#menu.options + 1] = {
title = "Giv Adgang",
onSelect = function()
self:GiveAccessMenu()
end,
}
menu.options[#menu.options + 1] = {
title = "Fjern Adgang",
onSelect = function()
self:RevokeAccessMenu()
end,
}
lib.registerContext(menu)
lib.showContext(id)
end
function Property:GiveAccessMenu()
if not self.inProperty then return end
if not self.owner then
return
end
local id = "property-" .. self.property_id .. "-access-give"
local menu = {
id = id,
title = "Giv Adgang",
options = {},
}
local players = lib.callback.await("ps-housing:cb:getPlayersInProperty", source, self.property_id) or {}
if #players > 0 then
for i = 1, #players do
local v = players[i]
menu.options[#menu.options + 1] = {
title = v.name,
description = "Giv Adgang",
onSelect = function()
TriggerServerEvent("ps-housing:server:addAccess", self.property_id, v.src)
end,
}
end
lib.registerContext(menu)
lib.showContext(id)
else
Framework[Config.Notify].Notify("Der er ingen i boligen", "error")
end
end
function Property:RevokeAccessMenu()
if not self.owner then
return
end
local id = "property-" .. self.property_id .. "-access-already"
local alreadyAccessMenu = {
id = id,
title = "Revoke Access",
options = {},
}
local playersWithAccess = lib.callback.await("ps-housing:cb:getPlayersWithAccess", source, self.property_id) or {}
-- only stores names and citizenids in a table so if their offline you can still remove them
if #playersWithAccess > 0 then
for i = 1, #playersWithAccess do
local v = playersWithAccess[i]
alreadyAccessMenu.options[#alreadyAccessMenu.options + 1] = {
title = v.name,
description = "Fjern Adgang",
onSelect = function()
TriggerServerEvent("ps-housing:server:removeAccess", self.property_id, v.citizenid)
end,
}
end
lib.registerContext(alreadyAccessMenu)
lib.showContext(id)
else
Framework[Config.Notify].Notify("Ingen har adgang til denne ejendom", "error")
end
end
function Property:OpenDoorbellMenu()
if not self.inProperty then return end
if not next(self.doorbellPool) then
Framework[Config.Notify].Notify("Der er ingen ved døren", "error")
return
end
local id = string.format("property-%s-doorbell", self.property_id)
local menu = {
id = id,
title = "Gæster ved døren",
options = {},
}
for k, v in pairs(self.doorbellPool) do
menu.options[#menu.options + 1] = {
title = v.name,
onSelect = function()
TriggerServerEvent(
"ps-housing:server:doorbellAnswer",
{ targetSrc = v.src, property_id = self.property_id }
)
end,
}
end
lib.registerContext(menu)
lib.showContext(id)
end
function Property:LoadFurniture(furniture)
local coords = GetOffsetFromEntityInWorldCoords(self.shellObj, furniture.position.x, furniture.position.y, furniture.position.z)
local hash = furniture.object
lib.requestModel(hash)
local entity = CreateObjectNoOffset(hash, coords.x, coords.y, coords.z, false, true, false)
SetModelAsNoLongerNeeded(hash)
SetEntityRotation(entity, furniture.rotation.x, furniture.rotation.y, furniture.rotation.z, 2, true)
if furniture.type == 'door' and Config.DynamicDoors then
Debug("Object: "..furniture.label.." wont be frozen")
else
FreezeEntityPosition(entity, true)
end
if furniture.type and Config.FurnitureTypes[furniture.type] then
Config.FurnitureTypes[furniture.type](entity, self.property_id, self.propertyData.shell)
end
self.furnitureObjs[#self.furnitureObjs + 1] = {
entity = entity,
id = furniture.id,
label = furniture.label,
object = furniture.object,
position = {
x = coords.x,
y = coords.y,
z = coords.z,
},
rotation = furniture.rotation,
type = furniture.type,
}
end
function Property:LoadFurnitures()
self.propertyData.furnitures = lib.callback.await('ps-housing:cb:getFurnitures', source, self.property_id) or {}
for i = 1, #self.propertyData.furnitures do
local furniture = self.propertyData.furnitures[i]
self:LoadFurniture(furniture)
end
end
function Property:UnloadFurniture(furniture, index)
local entity = furniture?.entity
if not entity then
for i = 1, #self.furnitureObjs do
if self.furnitureObjs[i]?.id and furniture?.id and self.furnitureObjs[i].id == furniture.id then
entity = self.furnitureObjs[i]?.entity
break
end
end
end
if self.clothingTarget == entity or self.storageTarget == entity then
Framework[Config.Target].RemoveTargetEntity(entity)
if self.clothingTarget == entity then
self.clothingTarget = nil
elseif self.storageTarget == entity then
self.storageTarget = nil
end
end
if index and self.furnitureObjs?[index] then
table.remove(self.furnitureObjs, index)
else
for i = 1, #self.furnitureObjs do
if self.furnitureObjs[i]?.id and furniture?.id and self.furnitureObjs[i].id == furniture.id then
table.remove(self.furnitureObjs, i)
break
end
end
end
DeleteObject(entity)
end
function Property:UnloadFurnitures()
for i = 1, #self.furnitureObjs do
local furniture = self.furnitureObjs[i]
self:UnloadFurniture(furniture, i)
end
self.furnitureObjs = {}
end
function Property:CreateBlip()
local door_data = self.propertyData.door_data
local blip = AddBlipForCoord(door_data.x, door_data.y, door_data.z)
if self.propertyData.garage_data.x ~= nil then
SetBlipSprite(blip, 492)
else
SetBlipSprite(blip, 40)
end
SetBlipScale(blip, 0.8)
SetBlipColour(blip, 2)
SetBlipAsShortRange(blip, true)
BeginTextCommandSetBlipName("STRING")
AddTextComponentString(self.propertyData.street .. " " .. self.property_id)
EndTextCommandSetBlipName(blip)
self.blip = blip
end
function Property:RemoveBlip()
if not self.blip then return end
RemoveBlip(self.blip)
self.blip = nil
end
function Property:RemoveProperty()
local targetName = string.format("%s_%s", self.propertyData.street, self.property_id)
Framework[Config.Target].RemoveTargetZone(targetName)
self:RemoveBlip()
self:LeaveShell()
--@@ comeback to this
-- Think it works now
if self.propertyData.apartment then
ApartmentsTable[self.propertyData.apartment]:RemoveProperty()
end
self = nil
end
local function findFurnitureDifference(new, old)
local added = {}
local removed = {}
for i = 1, #new do
local found = false
for j = 1, #old do
if new[i].id == old[j].id then
found = true
break
end
end
if not found then
added[#added + 1] = new[i]
end
end
for i = 1, #old do
local found = false
for j = 1, #new do
if old[i].id == new[j].id then
found = true
break
end
end
if not found then
removed[#removed + 1] = old[i]
end
end
return added, removed
end
-- I think this whole furniture sync is a bit shit, but I cbf thinking
function Property:UpdateFurnitures(newFurnitures)
if not self.inProperty then return end
local oldFurnitures = self.propertyData.furnitures
local added, removed = findFurnitureDifference(newFurnitures, oldFurnitures)
for i = 1, #added do
local furniture = added[i]
self:LoadFurniture(furniture)
end
for i = 1, #removed do
local furniture = removed[i]
self:UnloadFurniture(furniture)
end
self.propertyData.furnitures = newFurnitures
Modeler:UpdateFurnitures()
end
function Property:UpdateDescription(newDescription)
self.propertyData.description = newDescription
end
function Property:UpdatePrice(newPrice)
self.propertyData.price = newPrice
end
function Property:UpdateForSale(forSale)
self.propertyData.for_sale = forSale
end
function Property:UpdateShell(newShell)
self:LeaveShell()
self.propertyData.shell = newShell
if self.inProperty then
self:EnterShell()
end
end
function Property:UpdateOwner(newOwner)
self.propertyData.owner = newOwner
local citizenid = PlayerData.citizenid
self.owner = newOwner == citizenid
self:UnregisterGarageZone()
self:RegisterGarageZone()
self:CreateBlip()
if not self.inProperty then return end
self:RemoveMenus()
self:GiveMenus()
end
function Property:UpdateImgs(newImgs)
self.propertyData.imgs = newImgs
end
function Property:UpdateDoor(newDoor, newStreet, newRegion)
self.propertyData.door_data = newDoor
self.propertyData.street = newStreet
self.propertyData.region = newRegion
self:UnregisterPropertyEntrance()
self:RegisterPropertyEntrance()
end
function Property:UpdateHas_access(newHas_access)
local citizenid = PlayerData.citizenid
self.propertyData.has_access = newHas_access
self.has_access = lib.table.contains(newHas_access, citizenid)
if not self.inProperty then return end
self:RemoveMenus()
self:GiveMenus()
end
function Property:UpdateGarage(newGarage)
self.propertyData.garage_data = newGarage
self:UnregisterGarageZone()
self:RegisterGarageZone()
end
function Property:UpdateApartment(newApartment)
self:LeaveShell()
local oldAptName = self.propertyData.apartment
local oldApt = ApartmentsTable[oldAptName]
if oldApt then
oldApt:RemoveProperty(self.property_id)
end
self.propertyData.apartment = newApartment
local newApt = ApartmentsTable[newApartment]
if newApt then
newApt:AddProperty(self.property_id)
end
TriggerEvent("ps-housing:client:updateApartment", oldAptName, newApartment)
end
function Property.Get(property_id)
return PropertiesTable[tostring(property_id)]
end
RegisterNetEvent("ps-housing:client:enterProperty", function(property_id)
local property = Property.Get(property_id)
if property ~= nil then
property:EnterShell()
end
end)
RegisterNetEvent("ps-housing:client:updateDoorbellPool", function(property_id, data)
local property = Property.Get(property_id)
property.doorbellPool = data
end)
RegisterNetEvent("ps-housing:client:updateFurniture", function(property_id, furnitures)
local property = Property.Get(property_id)
if not property then return end
property:UpdateFurnitures(furnitures)
end)
RegisterNetEvent("ps-housing:client:updateProperty", function(type, property_id, data)
local property = Property.Get(property_id)
if not property then return end
property[type](property, data)
TriggerEvent("ps-housing:client:updatedProperty", property_id)
end)
RegisterNetEvent("ps-housing:client:openFurnitureMenu", function(data)
Modeler:OpenMenu(data.options.propertyId)
end)
RegisterNetEvent("ps-housing:client:openManagePropertyAccessMenu", function(data)
local property = Property.Get(data.options.propertyId)
if not property then return end
property:ManageAccessMenu()
end)

View File

@ -0,0 +1,157 @@
QBCore = exports['qb-core']:GetCoreObject()
PlayerData = {}
local function createProperty(property)
PropertiesTable[property.property_id] = Property:new(property)
end
RegisterNetEvent('ps-housing:client:addProperty', createProperty)
RegisterNetEvent('ps-housing:client:removeProperty', function (property_id)
local property = Property.Get(property_id)
if property then
property:RemoveProperty()
end
PropertiesTable[property_id] = nil
end)
function InitialiseProperties(properties)
Debug("Initialising properties")
PlayerData = QBCore.Functions.GetPlayerData()
for k, v in pairs(Config.Apartments) do
ApartmentsTable[k] = Apartment:new(v)
end
if not properties then
properties = lib.callback.await('ps-housing:server:requestProperties')
end
for k, v in pairs(properties) do
createProperty(v.propertyData)
end
TriggerEvent("ps-housing:client:initialisedProperties")
Debug("Initialised properties")
end
AddEventHandler("QBCore:Client:OnPlayerLoaded", InitialiseProperties)
RegisterNetEvent('ps-housing:client:initialiseProperties', InitialiseProperties)
AddEventHandler("onResourceStart", function(resourceName) -- Used for when the resource is restarted while in game
if (GetCurrentResourceName() == resourceName) then
InitialiseProperties()
end
end)
RegisterNetEvent('QBCore:Client:OnJobUpdate', function(job)
PlayerData.job = job
end)
RegisterNetEvent('ps-housing:client:setupSpawnUI', function(cData)
DoScreenFadeOut(1000)
local result = lib.callback.await('ps-housing:cb:GetOwnedApartment', source, cData.citizenid)
if result then
TriggerEvent('qb-spawn:client:setupSpawns', cData, false, nil)
TriggerEvent('qb-spawn:client:openUI', true)
-- TriggerEvent("apartments:client:SetHomeBlip", result.type)
else
if Config.StartingApartment then
TriggerEvent('qb-spawn:client:setupSpawns', cData, true, Config.Apartments)
TriggerEvent('qb-spawn:client:openUI', true)
else
TriggerEvent('qb-spawn:client:setupSpawns', cData, false, nil)
TriggerEvent('qb-spawn:client:openUI', true)
end
end
end)
AddEventHandler("onResourceStop", function(resourceName)
if (GetCurrentResourceName() == resourceName) then
if Modeler.IsMenuActive then
Modeler:CloseMenu()
end
for k, v in pairs(PropertiesTable) do
v:RemoveProperty()
end
for k, v in pairs(ApartmentsTable) do
v:RemoveApartment()
end
end
end)
exports('GetProperties', function()
return PropertiesTable
end)
exports('GetProperty', function(property_id)
return Property.Get(property_id)
end)
exports('GetApartments', function()
return ApartmentsTable
end)
exports('GetApartment', function(apartment)
return Apartment.Get(apartment)
end)
exports('GetShells', function()
return Config.Shells
end)
lib.callback.register('ps-housing:cb:confirmPurchase', function(amount, street, id)
return lib.alertDialog({
header = 'Bekræft Køb',
content = 'Er du sikker på at du vil købe '..street..' ' .. id .. ' for ' .. amount .. ' kr?',
centered = true,
cancel = true,
labels = {
confirm = "Køb",
cancel = "Annuller"
}
})
end)
lib.callback.register('ps-housing:cb:confirmRaid', function(street, id)
return lib.alertDialog({
header = 'Ransag',
content = 'Vil du ransage '..street..' ' .. id .. '?',
centered = true,
cancel = true,
labels = {
confirm = "Ransag",
cancel = "Annuller"
}
})
end)
lib.callback.register('ps-housing:cb:ringDoorbell', function()
return lib.alertDialog({
header = 'Ring på dørklokken',
content = 'Du har ingen nøgle her til, vil du ringe på dørklokken??',
centered = true,
cancel = true,
labels = {
confirm = "Ring",
cancel = "Annuller"
}
})
end)
lib.callback.register('ps-housing:cb:showcase', function()
return lib.alertDialog({
header = 'Vis Ejendom',
content = 'Vil du have vist ejendommen?',
centered = true,
cancel = true,
labels = {
confirm = "Ja",
cancel = "Annuller"
}
})
end)

View File

@ -0,0 +1,6 @@
lib.callback.register('ps-housing:client:getclientdata', function(data)
return {
street = GetStreetNameFromHashKey(GetStreetNameAtCoord(data.coords.x, data.coords.y, data.coords.z)),
region = GetLabelText(GetNameOfZone(data.coords.x, data.coords.y, data.coords.z))
}
end)

View File

@ -0,0 +1,646 @@
local Freecam = exports['fivem-freecam']
local function CamThread()
CreateThread(function()
local IsDisabledControlJustPressed = IsDisabledControlJustPressed
local DisableControlAction = DisableControlAction
while Modeler.IsFreecamMode do
if IsDisabledControlJustPressed(0, 26) then -- C
Modeler.IsFreecamMode = false
Modeler:FreecamMode(false)
break
end
DisableControlAction(0, 199, true) -- P
DisableControlAction(0, 200, true) -- ESC
Wait(0)
end
end)
end
local function isInside(coords)
local extent = Modeler.shellMinMax
local isX = coords.x >= extent.min.x and coords.x <= extent.max.x
local isY = coords.y >= extent.min.y and coords.y <= extent.max.y
local isZ = coords.z >= extent.min.z and coords.z <= extent.max.z
if isX and isY and isZ then
return true
end
return false
end
local function getMinMax(shellPos, shellMin, shellMax)
local min = vector3(shellPos.x + shellMin.x, shellPos.y + shellMin.y, shellPos.z + shellMin.z)
local max = vector3(shellPos.x + shellMax.x, shellPos.y + shellMax.y, shellPos.z + shellMax.z)
return {min = min, max = max}
end
AddEventHandler('freecam:onTick', function()
if not Modeler.IsFreecamMode then return end
local update = true
local lookAt = Freecam:GetTarget(5.0)
local camPos = Freecam:GetPosition()
-- see if camPos is the same as the last one
if Modeler.CurrentCameraPosition and Modeler.CurrentCameraLookAt then
local posX = Modeler.CurrentCameraPosition.x == camPos.x
local posY = Modeler.CurrentCameraPosition.y == camPos.y
local posZ = Modeler.CurrentCameraPosition.z == camPos.z
local lookAtX = Modeler.CurrentCameraLookAt.x == lookAt.x
local lookAtY = Modeler.CurrentCameraLookAt.y == lookAt.y
local lookAtZ = Modeler.CurrentCameraLookAt.z == lookAt.z
if posX and posY and posZ and lookAtX and lookAtY and lookAtZ then
return
end
end
if not isInside(camPos) then
Freecam:SetPosition(Modeler.CurrentCameraPosition.x, Modeler.CurrentCameraPosition.y, Modeler.CurrentCameraPosition.z)
update = false
end
if update then
Modeler.CurrentCameraLookAt = lookAt
Modeler.CurrentCameraPosition = camPos
end
SendNUIMessage({
action = "updateCamera",
data = {
cameraPosition = Modeler.CurrentCameraPosition,
cameraLookAt = Modeler.CurrentCameraLookAt,
}
})
end)
-- WHERE THE ACTUAL CLASS STARTS
Modeler = {
IsMenuActive = false,
IsFreecamMode = false,
property_id = nil,
shellPos = nil,
shellMinMax = nil,
CurrentObject = nil,
CurrentCameraPosition = nil,
CurrentCameraLookAt = nil,
CurrentObjectAlpha = 200,
Cart = {},
-- Hover stuff
IsHovering = false,
HoverObject = nil,
HoverDistance = 5.0,
OpenMenu = function(self, property_id)
local property = Property.Get(property_id)
if not property then return end
if not property.owner and not property.has_access then return end
if property.has_access and not Config.AccessCanEditFurniture then return end
self.shellPos = GetEntityCoords(property.shellObj)
local min, max = GetModelDimensions(property.shellData.hash)
self.shellMinMax = getMinMax(self.shellPos, min, max)
self.property_id = property_id
self.IsMenuActive = true
self:UpdateFurnitures()
SendNUIMessage({
action = "setVisible",
data = true
})
SendNUIMessage({
action = "setFurnituresData",
data = Config.Furnitures
})
-- Owned furniture is set by the Property class
SetNuiFocus(true, true)
self:FreecamActive(true)
self:FreecamMode(false)
end,
CloseMenu = function(self)
self.IsMenuActive = false
self:ClearCart()
SendNUIMessage({
action = "setOwnedItems",
data = {},
})
SendNUIMessage({
action = "setVisible",
data = false
})
SetNuiFocus(false, false)
self:HoverOut()
self:StopPlacement()
self:FreecamActive(false)
Wait(500)
self.CurrentCameraPosition = nil
self.CurrentCameraLookAt = nil
self.CurrentObject = nil
self.property_id = nil
end,
FreecamActive = function(self, bool)
if bool then
Freecam:SetActive(true)
Freecam:SetKeyboardSetting('BASE_MOVE_MULTIPLIER', 0.1)
Freecam:SetKeyboardSetting('FAST_MOVE_MULTIPLIER', 2)
Freecam:SetKeyboardSetting('SLOW_MOVE_MULTIPLIER', 2)
Freecam:SetFov(45.0)
self.IsFreecamMode = true
else
Freecam:SetActive(false)
--reset to default
Freecam:SetKeyboardSetting('BASE_MOVE_MULTIPLIER', 5)
Freecam:SetKeyboardSetting('FAST_MOVE_MULTIPLIER', 10)
Freecam:SetKeyboardSetting('SLOW_MOVE_MULTIPLIER', 10)
self.IsFreecamMode = false
end
end,
FreecamMode = function(self, bool)
if bool then --not in UI
self.IsFreecamMode = true
CamThread()
Freecam:SetFrozen(false)
SetNuiFocus(false, false)
else -- in UI
self.IsFreecamMode = false
Freecam:SetFrozen(true)
SetNuiFocus(true, true)
SendNUIMessage({
action = "freecamMode",
data = false
})
end
end,
StartPlacement = function(self, data)
self:HoverOut() -- stops the hover effect on the previous object because sometimes mouseleave doesnt work
local object = data.object
local curObject
local objectRot
local objectPos
Modeler.CurrentCameraLookAt = Freecam:GetTarget(5.0)
Modeler.CurrentCameraPosition = Freecam:GetPosition()
if data.entity then --if the object is already spawned
curObject = data.entity
objectPos = GetEntityCoords(curObject)
objectRot = GetEntityRotation(curObject)
else
self:StopPlacement()
lib.requestModel(object)
curObject = CreateObject(GetHashKey(object), 0.0, 0.0, 0.0, false, true, false)
SetEntityCoords(curObject, self.CurrentCameraLookAt.x, self.CurrentCameraLookAt.y, self.CurrentCameraLookAt.z)
objectRot = GetEntityRotation(curObject)
objectPos = self.CurrentCameraLookAt
end
FreezeEntityPosition(curObject, true)
SetEntityCollision(curObject, false, false)
SetEntityAlpha(curObject, self.CurrentObjectAlpha, false)
SetEntityDrawOutline(curObject, true)
SetEntityDrawOutlineColor(255, 255, 255, 255)
SetEntityDrawOutlineShader(0)
SendNUIMessage({
action = "setObjectAlpha",
data = self.CurrentObjectAlpha
})
SendNUIMessage({
action = "setupModel",
data = {
objectPosition = objectPos,
objectRotation = objectRot,
cameraPosition = self.CurrentCameraPosition,
cameraLookAt = self.CurrentCameraLookAt,
entity = data.entity,
}
})
SetNuiFocus(true, true)
self.CurrentObject = curObject
end,
MoveObject = function (self, data)
local coords = vec3(data.x + 0.0, data.y + 0.0, data.z + 0.0)
if not isInside(coords) then
return
end
SetEntityCoords(self.CurrentObject, coords)
-- get the current offset of this object in relation to the
end,
RotateObject = function (self, data)
SetEntityRotation(self.CurrentObject, data.x + 0.0, data.y + 0.0, data.z + 0.0)
end,
StopPlacement = function (self)
if self.CurrentObject == nil then return end
local canDelete = true
for k, v in pairs(self.Cart) do
if k == self.CurrentObject then
canDelete = false
break
end
end
-- furnitureObjs
-- see if its an owned object
local property = PropertiesTable[self.property_id]
local ownedfurnitures = property.furnitureObjs
for i = 1, #ownedfurnitures do
if ownedfurnitures[i].entity == self.CurrentObject then
self:UpdateFurniture(ownedfurnitures[i])
canDelete = false
break
end
end
if canDelete then
DeleteEntity(self.CurrentObject)
end
SetEntityDrawOutline(self.CurrentObject, false)
SetEntityAlpha(self.CurrentObject, 255, false)
self.CurrentObject = nil
end,
UpdateFurnitures = function(self)
if not self.IsMenuActive then
return
end
if not self.property_id then return end
local property = Property.Get(self.property_id)
if not property then return end
if not property.owner and not property.has_access then return end
if property.has_access and not Config.AccessCanEditFurniture then return end
SendNUIMessage({
action = "setOwnedItems",
data = property.furnitureObjs,
})
end,
-- can be better
-- everytime "Stop Placement" is pressed on an owned object, it will update the furniture
-- maybe should do it all at once when the user leaves the menu????
UpdateFurniture = function (self, item)
local newPos = GetEntityCoords(item.entity)
local newRot = GetEntityRotation(item.entity)
local offsetPos = {
x = math.floor((newPos.x - self.shellPos.x) * 10000) / 10000,
y = math.floor((newPos.y - self.shellPos.y) * 10000) / 10000,
z = math.floor((newPos.z - self.shellPos.z) * 10000) / 10000,
}
local newFurniture = {
id = item.id,
label = item.label,
object = item.object,
position = offsetPos,
rotation = newRot,
type = item.type,
}
TriggerServerEvent("ps-housing:server:updateFurniture", self.property_id, newFurniture)
end,
SetObjectAlpha = function (self, data)
self.CurrentObjectAlpha = data.alpha
SetEntityAlpha(self.CurrentObject, self.CurrentObjectAlpha, false)
end,
PlaceOnGround = function (self)
local x, y, z = table.unpack(GetEntityCoords(self.CurrentObject))
local ground, z = GetGroundZFor_3dCoord(x, y, z, 0)
SetEntityCoords(self.CurrentObject, x, y, z)
return {x = x, y = y, z = z}
end,
SelectCartItem = function (self, data)
self:StopPlacement()
if data ~= nil then
self:StartPlacement(data)
end
end,
AddToCart = function (self, data)
local item = {
label = data.label,
object = data.object,
price = data.price,
entity = self.CurrentObject,
position = GetEntityCoords(self.CurrentObject),
rotation = GetEntityRotation(self.CurrentObject),
type = data.type,
}
self.Cart[self.CurrentObject] = item
SendNUIMessage({
action = "addToCart",
data = item
})
self:StopPlacement()
self.CurrentObject = nil
end,
RemoveFromCart = function (self, data)
local item = data
if item ~= nil then
DeleteEntity(item.entity)
SendNUIMessage({
action = "removeFromCart",
data = item
})
self.Cart[data.entity] = nil
end
end,
UpdateCartItem = function (self, data)
local item = self.Cart[data.entity]
if item ~= nil then
item = data
end
end,
ClearCart = function (self)
for _, v in pairs(self.Cart) do
DeleteEntity(v.entity)
end
self.Cart = {}
SendNUIMessage({
action = "clearCart"
})
end,
BuyCart = function (self)
local items = {}
local totalPrice = 0
-- If the cart is empty, return notify
if not next(self.Cart) then
Framework[Config.Notify].Notify("Din kurv er tom", "error")
return
end
-- seperate loop to get total price so it doesnt have to do all that math for no reason
for _, v in pairs(self.Cart) do
totalPrice = totalPrice + v.price
end
PlayerData = QBCore.Functions.GetPlayerData()
if PlayerData.money.cash < totalPrice and PlayerData.money.bank < totalPrice then
Framework[Config.Notify].Notify("Du har ikke nok penge!", "error")
return
end
for _, v in pairs(self.Cart) do
local offsetPos = {
x = math.floor((v.position.x - self.shellPos.x) * 10000) / 10000,
y = math.floor((v.position.y - self.shellPos.y) * 10000) / 10000,
z = math.floor((v.position.z - self.shellPos.z) * 10000) / 10000,
}
local id = tostring(math.random(100000, 999999)..self.property_id)
items[#items + 1] = {
id = id,
object = v.object,
label = v.label,
position = offsetPos,
rotation = v.rotation,
type = v.type,
}
end
TriggerServerEvent("ps-housing:server:buyFurniture", self.property_id, items, totalPrice)
self:ClearCart()
end,
SetHoverDistance = function (self, data)
self.HoverDistance = data + 0.0
end,
HoverIn = function (self, data)
if self.HoverObject then
local tries = 0
while DoesEntityExist(self.HoverObject) do
SetEntityAsMissionEntity(self.HoverObject, true, true)
DeleteEntity(self.HoverObject)
Wait(50)
tries = tries + 1
if tries > 25 then
break
end
end
self.HoverObject = nil
end
local object = data.object and joaat(data.object) or nil
if object == nil then return end
lib.requestModel(object)
if self.HoverObject then return end
self.HoverObject = CreateObject(object, 0.0, 0.0, 0.0, false, false, false)
Modeler.CurrentCameraLookAt = Freecam:GetTarget(self.HoverDistance)
local camRot = Freecam:GetRotation()
SetEntityCoords(self.HoverObject, self.CurrentCameraLookAt.x, self.CurrentCameraLookAt.y, self.CurrentCameraLookAt.z)
FreezeEntityPosition(self.HoverObject, true)
SetEntityCollision(self.HoverObject, false, false)
SetEntityRotation(self.HoverObject, 0.0, 0.0, camRot.z)
self.IsHovering = true
while self.IsHovering do
local rot = GetEntityRotation(self.HoverObject)
SetEntityRotation(self.HoverObject, rot.x, rot.y, rot.z + 0.1)
Wait(0)
end
end,
HoverOut = function (self)
if self.HoverObject == nil then return end
if self.HoverObject and self.HoverObject ~= 0 then
local tries = 0
while DoesEntityExist(self.HoverObject) do
SetEntityAsMissionEntity(self.HoverObject, true, true)
DeleteEntity(self.HoverObject)
Wait(50)
tries = tries + 1
if tries > 25 then
break
end
end
self.HoverObject = nil
end
self.IsHovering = false
end,
SelectOwnedItem = function (self, data)
self:StopPlacement()
if data ~= nil then
self:StartPlacement(data)
end
end,
RemoveOwnedItem = function (self, data)
local item = data
if item ~= nil then
SendNUIMessage({
action = "removeOwnedItem",
data = item
})
TriggerServerEvent("ps-housing:server:removeFurniture", self.property_id, item.id)
end
end,
}
RegisterNUICallback("previewFurniture", function(data, cb)
Modeler:StartPlacement(data)
cb("ok")
end)
RegisterNUICallback("moveObject", function(data, cb)
Modeler:MoveObject(data)
cb("ok")
end)
RegisterNUICallback("rotateObject", function(data, cb)
Modeler:RotateObject(data)
cb("ok")
end)
RegisterNUICallback("stopPlacement", function(data, cb)
Modeler:StopPlacement()
cb("ok")
end)
RegisterNUICallback("setObjectAlpha", function(data, cb)
Modeler:SetObjectAlpha(data)
cb("ok")
end)
RegisterNUICallback("hideUI", function(data, cb)
Modeler:CloseMenu()
cb("ok")
end)
RegisterNUICallback("freecamMode", function(data, cb)
Modeler:FreecamMode(data)
cb("ok")
end)
RegisterNUICallback("placeOnGround", function(data, cb)
local coords = Modeler:PlaceOnGround()
cb(coords)
end)
RegisterNUICallback("selectCartItem", function(data, cb)
Modeler:SelectCartItem(data)
cb("ok")
end)
RegisterNUICallback("addToCart", function(data, cb)
Modeler:AddToCart(data)
cb("ok")
end)
RegisterNUICallback("removeCartItem", function(data, cb)
Modeler:RemoveFromCart(data)
cb("ok")
end)
RegisterNUICallback("updateCartItem", function(data, cb)
Modeler:UpdateCartItem(data)
cb("ok")
end)
RegisterNUICallback("buyCartItems", function(data, cb)
Modeler:BuyCart()
cb("ok")
end)
RegisterNUICallback("hoverIn", function(data, cb)
Modeler:HoverIn(data)
cb("ok")
end)
RegisterNUICallback("hoverOut", function(data, cb)
Modeler:HoverOut()
cb("ok")
end)
RegisterNUICallback("setHoverDistance", function(data, cb)
Modeler:SetHoverDistance(data)
cb("ok")
end)
RegisterNUICallback("selectOwnedItem", function(data, cb)
Modeler:SelectOwnedItem(data)
cb("ok")
end)
RegisterNUICallback("removeOwnedItem", function(data, cb)
Modeler:RemoveOwnedItem(data)
cb("ok")
end)
RegisterNUICallback("showNotification", function(data, cb)
Framework[Config.Notify].Notify(data.message, data.type)
cb("ok")
end)

View File

@ -0,0 +1,129 @@
Shells = {}
Shell = {
entity = nil,
hash = nil,
position = nil,
rotation = nil,
shellData = nil,
oldCoord = nil,
exitTarget = nil,
}
Shell.__index = Shell
function Shell:SpawnShell(shellHash, position, rotation)
lib.requestModel(shellHash)
local entity = CreateObjectNoOffset(shellHash, position.x, position.y, position.z, false, false, false)
FreezeEntityPosition(entity, true)
SetEntityRotation(entity, rotation, 2, true)
SetModelAsNoLongerNeeded(shellHash)
return entity
end
function Shell:DespawnShell()
if DoesEntityExist(self.entity) then
DeleteEntity(self.entity)
end
if self.exitTarget then
Framework[Config.Target].RemoveTargetZone(self.exitTarget)
end
self = nil
end
function Shell:CreatePropertyShell(shellName, position, rotation)
local self = setmetatable({}, Shell)
self.shellData = Config.Shells[shellName]
self.hash = self.shellData.hash
self.position = position
self.rotation = rotation or vector3(0.0, 0.0, 0.0)
self.entity = self:SpawnShell(self.hash, self.position, self.rotation)
return self
end
-- example of how to use
-- exports["ps-housing"]:CreateTempShell("Modern Hotel", GetEntityCoords(PlayerPedId()), GetEntityRotation(PlayerPedId()), function()
-- Framework[Config.Notify].Notify("You left the shell", "error")
-- local coords = GetEntityCoords(PlayerPedId())
-- SetEntityCoordsNoOffset(PlayerPedId(), coords.x, coords.y, coords.z + 50.0, false, false, true)
-- end)
-- this is used as a constructor for third party scripts
function Shell:CreateTempShell(shellName, position, rotation, leaveCb)
local self = setmetatable({}, Shell)
self.shellData = Config.Shells[shellName]
self.hash = self.shellData.hash
self.position = position
self.rotation = rotation
DoScreenFadeOut(250)
Wait(250)
self.oldCoord = GetEntityCoords(PlayerPedId())
self.entity = self:SpawnShell(self.hash, self.position, self.rotation)
local doorOffset = self.shellData.doorOffset
local offset = GetOffsetFromEntityInWorldCoords(self.entity, doorOffset.x, doorOffset.y, doorOffset.z)
SetEntityCoordsNoOffset(cache.ped, offset.x, offset.y, offset.z, false, false, true)
SetEntityHeading(cache.ped, self.shellData.doorOffset.h)
local coords = offset
local size = vector3(1.0, self.shellData.doorOffset.width, 3.0)
local heading = self.shellData.doorOffset.h
local function leave()
DoScreenFadeOut(250)
Wait(250)
SetEntityCoordsNoOffset(PlayerPedId(), self.oldCoord.x, self.oldCoord.y, self.oldCoord.z, false, false, true)
if leaveCb then
leaveCb()
end
self:DespawnShell()
Wait(250)
DoScreenFadeIn(250)
end
self.exitTarget = Framework[Config.Target].AddDoorZoneInsideTempShell(coords, size, heading, leave)
Wait(250)
DoScreenFadeIn(250)
Shells[self.entity] = self
return self.entity
end
exports('CreateTempShell', function(shellName, position, rotation, leaveCb)
return Shell:CreateTempShell(shellName, position, rotation, leaveCb)
end)
exports("GetShellData", function (shellName)
return Config.Shells[shellName]
end)
exports("DespawnTempShell", function (shellEntity)
if Shells[shellEntity] then
Shells[shellEntity]:DespawnShell()
end
end)

View File

@ -0,0 +1,15 @@
# Overview
*Provide a brief overview of the purpose of this pull request*
# Details
*Provide details on the change you are making, why they are necessary, and any information that will help with understanding.*
# UI Changes / Functionality
*Include a before and after screenshot/recording if applicable. Short recordings showing that your changes work are helpful, but optional.*
# Testing Steps
*Provide a list of repro steps on how to test that your changes are valid.*
- [ ] Did you test the changes you made?
- [ ] Did you test core functionality of the script to ensure your changes do not regress other areas?
- [ ] Did you test your changes in multiplayer to ensure it works correctly on all clients?

View File

@ -0,0 +1,47 @@
fx_version 'cerulean'
game "gta5"
author "Xirvin#0985 and Project Sloth"
version '1.1.3'
repository 'Project-Sloth/ps-housing'
lua54 'yes'
ui_page 'html/index.html'
dependency 'fivem-freecam'
shared_script {
'@ox_lib/init.lua',
"shared/config.lua",
"shared/framework.lua",
}
client_script {
'client/shell.lua',
'client/apartment.lua',
'client/cl_property.lua',
'client/client.lua',
'client/modeler.lua',
'client/migrate.lua'
}
server_script {
'@oxmysql/lib/MySQL.lua',
"server/sv_property.lua",
"server/server.lua",
"server/migrate.lua"
}
files {
'html/**',
'stream/starter_shells_k4mb1.ytyp'
}
this_is_a_map 'yes'
data_file 'DLC_ITYP_REQUEST' 'starter_shells_k4mb1.ytyp'
file 'stream/**.ytyp'
data_file 'DLC_ITYP_REQUEST' 'stream/**.ytyp'

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- FontAwesome Icons Import -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/css/all.min.css">
<!-- Material Icons Import -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<!-- Material Design Icons Import -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font/css/materialdesignicons.min.css">
<!-- Line Awesome Icons Import -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/line-awesome/dist/line-awesome/css/line-awesome.min.css">
<!-- Boostrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.min.css">
<!-- Satoshi Font -->
<link href="https://fonts.cdnfonts.com/css/satoshi" rel="stylesheet">
<title>ps-housing</title>
<script type="module" crossorigin src="./index.js"></script>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,21 @@
DROP table IF EXISTS `properties`;
CREATE TABLE IF NOT EXISTS `properties` (
`property_id` int(11) NOT NULL AUTO_INCREMENT,
`owner_citizenid` varchar(50) NULL,
`street` VARCHAR(100) NULL,
`region` VARCHAR(100) NULL,
`description` LONGTEXT NULL,
`has_access` JSON NULL DEFAULT (JSON_ARRAY()), -- [citizenid1, citizenid2, ...]
`extra_imgs` JSON NULL DEFAULT (JSON_ARRAY()),
`furnitures` JSON NULL DEFAULT (JSON_ARRAY()),
`for_sale` boolean NOT NULL DEFAULT 1,
`price` int(11) NOT NULL DEFAULT 0,
`shell` varchar(50) NOT NULL,
`apartment` varchar(50) NULL DEFAULT NULL, -- if NULL then it's a house
`door_data` JSON NULL DEFAULT NULL, -- {"x": 0.0, "y": 0.0, "z": 0.0, "h": 0.0, "length": 0.0, "width": 0.0}
`garage_data` JSON NULL DEFAULT NULL, -- {"x": 0.0, "y": 0.0, "z": 0.0} -- NULL if no garage
PRIMARY KEY (`property_id`),
CONSTRAINT `FK_owner_citizenid` FOREIGN KEY (`owner_citizenid`) REFERENCES `players` (`citizenid`) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT `UQ_owner_apartment` UNIQUE (`owner_citizenid`, `apartment`) -- A character can only own one apartment
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

View File

@ -0,0 +1,25 @@
local AptNames = {
['apartment1'] = "South Rockford Drive",
["apartment2"] = "Morningwood Blvd",
["apartment3"] = "Integrity Way",
["apartment4"] = "Tinsel Towers",
["apartment5"] = "Fantastic Plaza",
}
local Shells = {
[1] = `shell_v16low`,
[2] = `shell_v16mid`,
[3] = `shell_trevor`,
[4] = `shell_trailer`,
[5] = `shell_lester`,
[6] = `shell_ranch`,
[7] = `container_shell`,
[8] = `furnitured_midapart`,
[9] = `modernhotel_shell`,
[10] = `shell_frankaunt`,
[11] = `shell_garagem`,
[12] = `shell_michael`,
[13] = `shell_office1`,
[14] = `shell_store1`,
[15] = `shell_warehouse1`
}

View File

@ -0,0 +1,268 @@
QBCore = exports['qb-core']:GetCoreObject()
-- PSCore = exports['ps-core']:GetCoreObject()
local dbloaded = false
MySQL.ready(function()
MySQL.query('SELECT * FROM properties', {}, function(result)
if not result then return end
if result.id then -- If only one result
result = {result}
end
for _, v in pairs(result) do
local id = tostring(v.property_id)
local propertyData = {
property_id = tostring(id),
owner = v.owner_citizenid,
street = v.street,
region = v.region,
description = v.description,
has_access = json.decode(v.has_access),
extra_imgs = json.decode(v.extra_imgs),
furnitures = json.decode(v.furnitures),
for_sale = v.for_sale,
price = v.price,
shell = v.shell,
apartment = v.apartment,
door_data = json.decode(v.door_data),
garage_data = json.decode(v.garage_data),
}
PropertiesTable[id] = Property:new(propertyData)
end
dbloaded = true
end)
end)
lib.callback.register("ps-housing:server:requestProperties", function(source)
while not dbloaded do
Wait(100)
end
return PropertiesTable
end)
AddEventHandler("ps-housing:server:registerProperty", function (propertyData) -- triggered by realtor job
local propertyData = propertyData
propertyData.owner = propertyData.owner or nil
propertyData.has_access = propertyData.has_access or {}
propertyData.extra_imgs = propertyData.extra_imgs or {}
propertyData.furnitures = propertyData.furnitures or {}
propertyData.door_data = propertyData.door_data or {}
propertyData.garage_data = propertyData.garage_data or {}
local cols = "(owner_citizenid, street, region, description, has_access, extra_imgs, furnitures, for_sale, price, shell, apartment, door_data, garage_data)"
local vals = "(@owner_citizenid, @street, @region, @description, @has_access, @extra_imgs, @furnitures, @for_sale, @price, @shell, @apartment, @door_data, @garage_data)"
local id = MySQL.insert.await("INSERT INTO properties " .. cols .. " VALUES " .. vals , {
["@owner_citizenid"] = propertyData.owner or nil,
["@street"] = propertyData.street,
["@region"] = propertyData.region,
["@description"] = propertyData.description,
["@has_access"] = json.encode(propertyData.has_access),
["@extra_imgs"] = json.encode(propertyData.extra_imgs),
["@furnitures"] = json.encode(propertyData.furnitures),
["@for_sale"] = propertyData.for_sale ~= nil and propertyData.for_sale or 1,
["@price"] = propertyData.price or 0,
["@shell"] = propertyData.shell,
["@apartment"] = propertyData.apartment,
["@door_data"] = json.encode(propertyData.door_data),
["@garage_data"] = json.encode(propertyData.garage_data),
})
id = tostring(id)
propertyData.property_id = id
PropertiesTable[id] = Property:new(propertyData)
TriggerClientEvent("ps-housing:client:addProperty", -1, propertyData)
if propertyData.apartment then
local player = QBCore.Functions.GetPlayerByCitizenId(propertyData.owner)
local src = player.PlayerData.source
local property = Property.Get(id)
property:PlayerEnter(src)
Wait(1000)
local query = "SELECT skin FROM playerskins WHERE citizenid = ?"
local result = MySQL.Sync.fetchAll(query, {propertyData.owner})
if result and result[1] then
Debug("Player: " .. propertyData.owner .. " skin already exists!")
else
TriggerClientEvent("hp_charcreator:openCreator", src)
-- TriggerClientEvent("qb-clothes:client:CreateFirstCharacter", src)
Debug("Player: " .. propertyData.owner .. " is creating a new character!")
end
Framework[Config.Notify].Notify(src, "Open radial menu for furniture menu and place down your stash and clothing locker.", "info")
-- This will create the stash for the apartment and migrate the items from the old apartment stash if applicable
TriggerEvent("ps-housing:server:createApartmentStash", propertyData.owner, id)
end
end)
lib.callback.register("ps-housing:cb:GetOwnedApartment", function(source, cid)
Debug("ps-housing:cb:GetOwnedApartment", source, cid)
if cid ~= nil then
local result = MySQL.query.await('SELECT * FROM properties WHERE owner_citizenid = ? AND apartment IS NOT NULL AND apartment <> ""', { cid })
if result[1] ~= nil then
return result[1]
end
return nil
else
local src = source
local Player = QBCore.Functions.GetPlayer(src)
local result = MySQL.query.await('SELECT * FROM properties WHERE owner_citizenid = ? AND apartment IS NOT NULL AND apartment <> ""', { Player.PlayerData.citizenid })
if result[1] ~= nil then
return result[1]
end
return nil
end
end)
AddEventHandler("ps-housing:server:updateProperty", function(type, property_id, data)
local property = Property.Get(property_id)
if not property then return end
property[type](property, data)
end)
AddEventHandler("onResourceStart", function(resourceName) -- Used for when the resource is restarted while in game
if (GetCurrentResourceName() == resourceName) then
while not dbloaded do
Wait(100)
end
TriggerClientEvent('ps-housing:client:initialiseProperties', -1, PropertiesTable)
end
end)
RegisterNetEvent("ps-housing:server:createNewApartment", function(aptLabel)
local src = source
if not Config.StartingApartment then return end
local citizenid = GetCitizenid(src)
local PlayerData = GetPlayerData(src)
local apartment = Config.Apartments[aptLabel]
if not apartment then return end
local propertyData = {
owner = citizenid,
description = string.format("Dette er %s's lejlighed ved %s", PlayerData.charinfo.firstname .. " " .. PlayerData.charinfo.lastname, apartment.label),
for_sale = 0,
shell = apartment.shell,
apartment = apartment.label,
}
Debug("Creating new apartment for " .. GetPlayerName(src) .. " in " .. apartment.label)
Framework[Config.Logs].SendLog("Creating new apartment for " .. GetPlayerName(src) .. " in " .. apartment.label)
TriggerEvent("ps-housing:server:registerProperty", propertyData)
end)
-- Creates apartment stash
-- If player has an existing apartment from qb-apartments, it will transfer the items over to the new apartment stash
RegisterNetEvent("ps-housing:server:createApartmentStash", function(citizenId, propertyId)
local stashId = string.format("property_%s", propertyId)
-- Check for existing apartment and corresponding stash
local result = MySQL.query.await('SELECT items, stash FROM stashitems WHERE stash IN (SELECT name FROM apartments WHERE citizenid = ?)', { citizenId })
local items = {}
if result[1] ~= nil then
items = json.decode(result[1].items)
-- Delete the old apartment stash as it is no longer needed
MySQL.Async.execute('DELETE FROM stashitems WHERE stash = ?', { result[1].stash })
end
-- This will create the stash for the apartment (without requiring player to have first opened and placed item in it)
TriggerEvent('ps-inventory:server:SaveStashItems', stashId, items)
end)
RegisterNetEvent('qb-apartments:returnBucket', function()
local src = source
SetPlayerRoutingBucket(src, 0)
end)
AddEventHandler("ps-housing:server:addTenantToApartment", function (data)
local apartment = data.apartment
local targetSrc = tonumber(data.targetSrc)
local realtorSrc = data.realtorSrc
local targetCitizenid = GetCitizenid(targetSrc, realtorSrc)
-- id of current apartment so we can change it
local property_id = nil
for _, v in pairs(PropertiesTable) do
local propertyData = v.propertyData
if propertyData.owner == targetCitizenid then
if propertyData.apartment == apartment then
Framework[Config.Notify].Notify(targetSrc, "Du er allerede i lejligheden", "error")
Framework[Config.Notify].Notify(targetSrc, "Denne person er allerede i lejligheden", "error")
return
elseif #propertyData.apartment > 1 then
property_id = propertyData.property_id
break
end
end
end
local property = Property.Get(property_id)
if not property then return end
property:UpdateApartment(data)
local citizenid = GetCitizenid(targetSrc, realtorSrc)
local targetToAdd = QBCore.Functions.GetPlayerByCitizenId(citizenid)
local targetPlayer = targetToAdd.PlayerData
Framework[Config.Notify].Notify(targetSrc, "Din lejlighed er nu på "..apartment, "success")
Framework[Config.Notify].Notify(realtorSrc, "Du har tilføjet ".. targetPlayer.charinfo.firstname .. " " .. targetPlayer.charinfo.lastname .. " til lejligheden "..apartment, "success")
end)
exports('IsOwner', function(src, property_id)
local property = Property.Get(property_id)
if not property then return false end
local citizenid = GetCitizenid(src, src)
return property:CheckForAccess(citizenid)
end)
function GetCitizenid(targetSrc, callerSrc)
local Player = QBCore.Functions.GetPlayer(tonumber(targetSrc))
if not Player then
Framework[Config.Notify].Notify(callerSrc, "Person ikke fundet.", "error")
return
end
local PlayerData = Player.PlayerData
local citizenid = PlayerData.citizenid
return citizenid
end
function GetCharName(src)
local Player = QBCore.Functions.GetPlayer(tonumber(src))
if not Player then return end
local PlayerData = Player.PlayerData
return PlayerData.charinfo.firstname .. " " .. PlayerData.charinfo.lastname
end
function GetPlayerData(src)
local Player = QBCore.Functions.GetPlayer(tonumber(src))
if not Player then return end
local PlayerData = Player.PlayerData
return PlayerData
end
function GetPlayer(src)
local Player = QBCore.Functions.GetPlayer(tonumber(src))
if not Player then return end
return Player
end
-- if PSCore then
-- PSCore.Functions.CheckForUpdates()
-- PSCore.Functions.CheckResourceName()
-- end

View File

@ -0,0 +1,855 @@
Property = {
property_id = nil,
propertyData = nil,
playersInside = {}, -- src
playersDoorbell = {}, -- src
raiding = false,
}
Property.__index = Property
function Property:new(propertyData)
local self = setmetatable({}, Property)
self.property_id = tostring(propertyData.property_id)
self.propertyData = propertyData
local stashName = string.format("property_%s", propertyData.property_id)
local stashConfig = Config.Shells[propertyData.shell].stash
Framework[Config.Inventory].RegisterInventory(stashName, propertyData.street or propertyData.apartment or stashName, stashConfig)
return self
end
function Property:PlayerEnter(src)
local _src = tostring(src)
self.playersInside[_src] = true
TriggerClientEvent('qb-weathersync:client:DisableSync', src)
TriggerClientEvent('ps-housing:client:enterProperty', src, self.property_id)
if next(self.playersDoorbell) then
TriggerClientEvent("ps-housing:client:updateDoorbellPool", src, self.property_id, self.playersDoorbell)
if self.playersDoorbell[_src] then
self.playersDoorbell[_src] = nil
end
end
local citizenid = GetCitizenid(src)
if self:CheckForAccess(citizenid) then
local Player = QBCore.Functions.GetPlayer(src)
local insideMeta = Player.PlayerData.metadata["inside"]
insideMeta.property_id = self.property_id
Player.Functions.SetMetaData("inside", insideMeta)
end
local bucket = tonumber(self.property_id) -- because the property_id is a string
SetPlayerRoutingBucket(src, bucket)
end
function Property:PlayerLeave(src)
local _src = tostring(src)
self.playersInside[_src] = nil
TriggerClientEvent('qb-weathersync:client:EnableSync', src)
local citizenid = GetCitizenid(src)
if self:CheckForAccess(citizenid) then
local Player = QBCore.Functions.GetPlayer(src)
local insideMeta = Player.PlayerData.metadata["inside"]
insideMeta.property_id = nil
Player.Functions.SetMetaData("inside", insideMeta)
end
SetPlayerRoutingBucket(src, 0)
end
function Property:CheckForAccess(citizenid)
if self.propertyData.owner == citizenid then return true end
return lib.table.contains(self.propertyData.has_access, citizenid)
end
function Property:AddToDoorbellPoolTemp(src)
local _src = tostring(src)
local name = GetCharName(src)
self.playersDoorbell[_src] = {
src = src,
name = name
}
for src, _ in pairs(self.playersInside) do
local targetSrc = tonumber(src)
Framework[Config.Notify].Notify(targetSrc, "Der er gæster ved døren.", "info")
TriggerClientEvent("ps-housing:client:updateDoorbellPool", targetSrc, self.property_id, self.playersDoorbell)
end
Framework[Config.Notify].Notify(src, "Du har ringet på dørklokken...", "info")
SetTimeout(10000, function()
if self.playersDoorbell[_src] then
self.playersDoorbell[_src] = nil
Framework[Config.Notify].Notify(src, "Ingen åbnede døren.", "error")
end
for src, _ in pairs(self.playersInside) do
local targetSrc = tonumber(src)
TriggerClientEvent("ps-housing:client:updateDoorbellPool", targetSrc, self.property_id, self.playersDoorbell)
end
end)
end
function Property:RemoveFromDoorbellPool(src)
local _src = tostring(src)
if self.playersDoorbell[_src] then
self.playersDoorbell[_src] = nil
end
for src, _ in pairs(self.playersInside) do
local targetSrc = tonumber(src)
TriggerClientEvent("ps-housing:client:updateDoorbellPool", targetSrc, self.property_id, self.playersDoorbell)
end
end
function Property:StartRaid()
self.raiding = true
for src, _ in pairs(self.playersInside) do
local targetSrc = tonumber(src)
Framework[Config.Notify].Notify(targetSrc, "Denne ejendom bliver ransaget.", "error")
end
SetTimeout(Config.RaidTimer * 60000, function()
self.raiding = false
end)
end
function Property:UpdateFurnitures(furnitures)
self.propertyData.furnitures = furnitures
MySQL.update("UPDATE properties SET furnitures = @furnitures WHERE property_id = @property_id", {
["@furnitures"] = json.encode(furnitures),
["@property_id"] = self.property_id
})
for src, _ in pairs(self.playersInside) do
local targetSrc = tonumber(src)
TriggerClientEvent("ps-housing:client:updateFurniture", targetSrc, self.property_id, furnitures)
end
end
function Property:UpdateDescription(data)
local description = data.description
local realtorSrc = data.realtorSrc
if self.propertyData.description == description then return end
self.propertyData.description = description
MySQL.update("UPDATE properties SET description = @description WHERE property_id = @property_id", {
["@description"] = description,
["@property_id"] = self.property_id
})
TriggerClientEvent("ps-housing:client:updateProperty", -1, "UpdateDescription", self.property_id, description)
Framework[Config.Logs].SendLog("**Changed Description** of property with id: " .. self.property_id .. " by: " .. GetPlayerName(realtorSrc))
Debug("Changed Description of property with id: " .. self.property_id, "by: " .. GetPlayerName(realtorSrc))
end
function Property:UpdatePrice(data)
local price = data.price
local realtorSrc = data.realtorSrc
if self.propertyData.price == price then return end
self.propertyData.price = price
MySQL.update("UPDATE properties SET price = @price WHERE property_id = @property_id", {
["@price"] = price,
["@property_id"] = self.property_id
})
TriggerClientEvent("ps-housing:client:updateProperty", -1, "UpdatePrice", self.property_id, price)
Framework[Config.Logs].SendLog("**Changed Price** of property with id: " .. self.property_id .. " by: " .. GetPlayerName(realtorSrc))
Debug("Changed Price of property with id: " .. self.property_id, "by: " .. GetPlayerName(realtorSrc))
end
function Property:UpdateForSale(data)
local forsale = data.forsale
local realtorSrc = data.realtorSrc
self.propertyData.for_sale = forsale
MySQL.update("UPDATE properties SET for_sale = @for_sale WHERE property_id = @property_id", {
["@for_sale"] = forsale and 1 or 0,
["@property_id"] = self.property_id
})
TriggerClientEvent("ps-housing:client:updateProperty", -1, "UpdateForSale", self.property_id, forsale)
Framework[Config.Logs].SendLog("**Changed For Sale** of property with id: " .. self.property_id .. " by: " .. GetPlayerName(realtorSrc))
Debug("Changed For Sale of property with id: " .. self.property_id, "by: " .. GetPlayerName(realtorSrc))
end
function Property:UpdateShell(data)
local shell = data.shell
local realtorSrc = data.realtorSrc
if self.propertyData.shell == shell then return end
self.propertyData.shell = shell
MySQL.update("UPDATE properties SET shell = @shell WHERE property_id = @property_id", {
["@shell"] = shell,
["@property_id"] = self.property_id
})
TriggerClientEvent("ps-housing:client:updateProperty", -1, "UpdateShell", self.property_id, shell)
Framework[Config.Logs].SendLog("**Changed Shell** of property with id: " .. self.property_id .. " by: " .. GetPlayerName(realtorSrc))
Debug("Changed Shell of property with id: " .. self.property_id, "by: " .. GetPlayerName(realtorSrc))
end
function Property:UpdateOwner(data)
local targetSrc = data.targetSrc
local realtorSrc = data.realtorSrc
if not realtorSrc then Debug("No Realtor Src found") return end
if not targetSrc then Debug("No Target Src found") return end
local previousOwner = self.propertyData.owner
local targetPlayer = QBCore.Functions.GetPlayer(tonumber(targetSrc))
local PlayerData = targetPlayer.PlayerData
local bank = PlayerData.money.bank
local citizenid = PlayerData.citizenid
if self.propertyData.owner == citizenid then
Framework[Config.Notify].Notify(targetSrc, "Du ejer allerede denne ejendom", "error")
Framework[Config.Notify].Notify(realtorSrc, "Kunden ejer allerede denne ejendom", "error")
return
end
--add callback
local targetAllow = lib.callback.await("ps-housing:cb:confirmPurchase", targetSrc, self.propertyData.price, self.propertyData.street, self.propertyData.property_id)
if targetAllow ~= "confirm" then
Framework[Config.Notify].Notify(targetSrc, "Du bekræftede ikke købet", "info")
Framework[Config.Notify].Notify(realtorSrc, "Kunden bekræftede ikke købet", "error")
return
end
if bank < self.propertyData.price then
Framework[Config.Notify].Notify(targetSrc, "Du har ikke nok penge i banken", "error")
Framework[Config.Notify].Notify(realtorSrc, "Kunden har ikke nok penge i banken", "error")
return
end
targetPlayer.Functions.RemoveMoney('bank', self.propertyData.price, "Købte ejendom: " .. self.propertyData.street .. " " .. self.property_id)
local prevPlayer = QBCore.Functions.GetPlayerByCitizenId(previousOwner)
local realtor = QBCore.Functions.GetPlayer(tonumber(realtorSrc))
local realtorGradeLevel = realtor.PlayerData.job.grade.level
local commission = math.floor(self.propertyData.price * Config.Commissions[realtorGradeLevel])
local totalAfterCommission = self.propertyData.price - commission
if prevPlayer ~= nil then
Framework[Config.Notify].Notify(prevPlayer.PlayerData.source, "Ejendom solgt: " .. self.propertyData.street .. " " .. self.property_id, "success")
prevPlayer.Functions.AddMoney('bank', totalAfterCommission, "Solgte ejendom: " .. self.propertyData.street .. " " .. self.property_id)
elseif previousOwner then
MySQL.Async.execute('UPDATE `players` SET `bank` = `bank` + @price WHERE `citizenid` = @citizenid', {
['@citizenid'] = previousOwner,
['@price'] = totalAfterCommission
})
end
realtor.Functions.AddMoney('bank', commission, "Kommission fra ejendom: " .. self.propertyData.street .. " " .. self.property_id)
self.propertyData.owner = citizenid
MySQL.update("UPDATE properties SET owner_citizenid = @owner_citizenid, for_sale = @for_sale WHERE property_id = @property_id", {
["@owner_citizenid"] = citizenid,
["@for_sale"] = 0,
["@property_id"] = self.property_id
})
self.propertyData.furnitures = {} -- to be fetched on enter
TriggerClientEvent("ps-housing:client:updateProperty", -1, "UpdateOwner", self.property_id, citizenid)
Framework[Config.Logs].SendLog("**House Bought** by: **"..PlayerData.charinfo.firstname.." "..PlayerData.charinfo.lastname.."** for $"..self.propertyData.price.." from **"..realtor.PlayerData.charinfo.firstname.." "..realtor.PlayerData.charinfo.lastname.."** !")
Framework[Config.Notify].Notify(targetSrc, "Du har købt ejendommen for "..self.propertyData.price..",-", "success")
Framework[Config.Notify].Notify(realtorSrc, "Kunden har købt ejendommen for "..self.propertyData.price..",-", "success")
end
function Property:UpdateImgs(data)
local imgs = data.imgs
local realtorSrc = data.realtorSrc
self.propertyData.imgs = imgs
MySQL.update("UPDATE properties SET extra_imgs = @extra_imgs WHERE property_id = @property_id", {
["@extra_imgs"] = json.encode(imgs),
["@property_id"] = self.property_id
})
TriggerClientEvent("ps-housing:client:updateProperty", -1, "UpdateImgs", self.property_id, imgs)
Framework[Config.Logs].SendLog("**Changed Images** of property with id: " .. self.property_id .. " by: " .. GetPlayerName(realtorSrc))
Debug("Changed Imgs of property with id: " .. self.property_id, "by: " .. GetPlayerName(realtorSrc))
end
function Property:UpdateDoor(data)
local door = data.door
if not door then return end
local realtorSrc = data.realtorSrc
local newDoor = {
x = math.floor(door.x * 10000) / 10000,
y = math.floor(door.y * 10000) / 10000,
z = math.floor(door.z * 10000) / 10000,
h = math.floor(door.h * 10000) / 10000,
length = door.length or 1.5,
width = door.width or 2.2,
locked = door.locked or false,
}
self.propertyData.door_data = newDoor
self.propertyData.street = data.street
self.propertyData.region = data.region
MySQL.update("UPDATE properties SET door_data = @door, street = @street, region = @region WHERE property_id = @property_id", {
["@door"] = json.encode(newDoor),
["@property_id"] = self.property_id,
["@street"] = data.street,
["@region"] = data.region
})
TriggerClientEvent("ps-housing:client:updateProperty", -1, "UpdateDoor", self.property_id, newDoor, data.street, data.region)
Framework[Config.Logs].SendLog("**Changed Door** of property with id: " .. self.property_id .. " by: " .. GetPlayerName(realtorSrc))
Debug("Changed Door of property with id: " .. self.property_id, "by: " .. GetPlayerName(realtorSrc))
end
function Property:UpdateHas_access(data)
local has_access = data or {}
self.propertyData.has_access = has_access
MySQL.update("UPDATE properties SET has_access = @has_access WHERE property_id = @property_id", {
["@has_access"] = json.encode(has_access), --Array of cids
["@property_id"] = self.property_id
})
TriggerClientEvent("ps-housing:client:updateProperty", -1, "UpdateHas_access", self.property_id, has_access)
Debug("Changed Has Access of property with id: " .. self.property_id)
end
function Property:UpdateGarage(data)
local garage = data.garage
local realtorSrc = data.realtorSrc
local newData = {}
if data ~= nil then
newData = {
x = math.floor(garage.x * 10000) / 10000,
y = math.floor(garage.y * 10000) / 10000,
z = math.floor(garage.z * 10000) / 10000,
h = math.floor(garage.h * 10000) / 10000,
length = garage.length or 3.0,
width = garage.width or 5.0,
}
end
self.propertyData.garage_data = newData
MySQL.update("UPDATE properties SET garage_data = @garageCoords WHERE property_id = @property_id", {
["@garageCoords"] = json.encode(newData),
["@property_id"] = self.property_id
})
TriggerClientEvent("ps-housing:client:updateProperty", -1, "UpdateGarage", self.property_id, garage)
Framework[Config.Logs].SendLog("**Changed Garage** of property with id: " .. self.property_id .. " by: " .. GetPlayerName(realtorSrc))
Debug("Changed Garage of property with id: " .. self.property_id, "by: " .. GetPlayerName(realtorSrc))
end
function Property:UpdateApartment(data)
local apartment = data.apartment
local realtorSrc = data.realtorSrc
local targetSrc = data.targetSrc
self.propertyData.apartment = apartment
MySQL.update("UPDATE properties SET apartment = @apartment WHERE property_id = @property_id", {
["@apartment"] = apartment,
["@property_id"] = self.property_id
})
Framework[Config.Notify].Notify(realtorSrc, "Ændrede lejligheden for ejendommen med ID: " .. self.property_id .." til ".. apartment, "success")
Framework[Config.Notify].Notify(targetSrc, "Ændrede lejlighed til " .. apartment, "success")
Framework[Config.Logs].SendLog("**Changed Apartment** with id: " .. self.property_id .. " by: **" .. GetPlayerName(realtorSrc) .. "** for **" .. GetPlayerName(targetSrc) .."**")
TriggerClientEvent("ps-housing:client:updateProperty", -1, "UpdateApartment", self.property_id, apartment)
Debug("Changed Apartment of property with id: " .. self.property_id, "by: " .. GetPlayerName(realtorSrc))
end
function Property:DeleteProperty(data)
local realtorSrc = data.realtorSrc
local propertyid = self.property_id
local realtorName = GetPlayerName(realtorSrc)
MySQL.Async.execute("DELETE FROM properties WHERE property_id = @property_id", {
["@property_id"] = propertyid
}, function (rowsChanged)
if rowsChanged > 0 then
Debug("Deleted property with id: " .. propertyid, "by: " .. realtorName)
end
end)
TriggerClientEvent("ps-housing:client:removeProperty", -1, propertyid)
Framework[Config.Notify].Notify(realtorSrc, "Ejendommen med id: " .. propertyid .." blev fjernet.", "info")
Framework[Config.Logs].SendLog("**Property Deleted** with id: " .. propertyid .. " by: " .. realtorName)
PropertiesTable[propertyid] = nil
self = nil
Debug("Deleted property with id: " .. propertyid, "by: " .. realtorName)
end
function Property.Get(property_id)
return PropertiesTable[tostring(property_id)]
end
RegisterNetEvent('ps-housing:server:enterProperty', function (property_id)
local src = source
Debug("Player is trying to enter property", property_id)
local property = Property.Get(property_id)
if not property then
Debug("Properties returned", json.encode(PropertiesTable, {indent = true}))
return
end
local citizenid = GetCitizenid(src)
if property:CheckForAccess(citizenid) then
Debug("Player has access to property")
property:PlayerEnter(src)
Debug("Player entered property")
return
end
local ringDoorbellConfirmation = lib.callback.await('ps-housing:cb:ringDoorbell', src)
if ringDoorbellConfirmation == "confirm" then
property:AddToDoorbellPoolTemp(src)
Debug("Ringing doorbell")
return
end
end)
RegisterNetEvent("ps-housing:server:showcaseProperty", function(property_id)
local src = source
local property = Property.Get(property_id)
if not property then
Debug("Properties returned", json.encode(PropertiesTable, {indent = true}))
return
end
local PlayerData = GetPlayerData(src)
local job = PlayerData.job
local jobName = job.name
local onDuty = job.onduty
if jobName == Config.RealtorJobName and onDuty then
local showcase = lib.callback.await('ps-housing:cb:showcase', src)
if showcase == "confirm" then
property:PlayerEnter(src)
return
end
end
end)
RegisterNetEvent('ps-housing:server:raidProperty', function(property_id)
local src = source
Debug("Player is trying to raid property", property_id)
local property = Property.Get(property_id)
if not property then
Debug("Properties returned", json.encode(PropertiesTable, {indent = true}))
return
end
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local PlayerData = Player.PlayerData
local job = PlayerData.job
local jobName = job.name
local gradeAllowed = tonumber(job.grade.level) >= Config.MinGradeToRaid
local onDuty = job.onduty
local raidItem = Config.RaidItem
-- Check if the police officer has the "stormram" item
local hasStormRam = (Config.Inventory == "ox" and exports.ox_inventory:Search(src, "count", raidItem) > 0) or Player.Functions.GetItemByName(raidItem)
local isAllowedToRaid = PoliceJobs[jobName] and onDuty and gradeAllowed
if isAllowedToRaid then
if hasStormRam then
if not property.raiding then
local confirmRaid = lib.callback.await('ps-housing:cb:confirmRaid', src, (property.propertyData.street or property.propertyData.apartment) .. " " .. property.property_id, property_id)
if confirmRaid == "confirm" then
property:StartRaid(src)
property:PlayerEnter(src)
Framework[Config.Notify].Notify(src, "Ransagning startet", "success")
if Config.ConsumeRaidItem then
-- Remove the "stormram" item from the officer's inventory
if Config.Inventory == 'ox' then
exports.ox_inventory:RemoveItem(src, raidItem, 1)
else
Player.Functions.RemoveItem(raidItem, 1)
TriggerClientEvent("inventory:client:ItemBox", src, QBCore.Shared.Items[raidItem], "remove")
TriggerEvent("inventory:server:RemoveItem", src, raidItem, 1)
end
end
end
else
Framework[Config.Notify].Notify(src, "Ransagning igang", "success")
property:PlayerEnter(src)
end
else
Framework[Config.Notify].Notify(src, "Du skal bruge en rambuk for at starte en ransagning", "error")
end
else
if not PoliceJobs[jobName] then
Framework[Config.Notify].Notify(src, "Kun politiet kan starte ransagninger", "error")
elseif not onDuty then
Framework[Config.Notify].Notify(src, "Du skal være på arbejde for at kunne starte en ransagning", "error")
elseif not gradeAllowed then
Framework[Config.Notify].Notify(src, "Du har ikke høj nok stilling, til at kunne starte en ransagnings", "error")
end
end
end)
lib.callback.register('ps-housing:cb:getFurnitures', function(source, property_id)
local property = Property.Get(property_id)
if not property then return end
return property.propertyData.furnitures or {}
end)
lib.callback.register('ps-housing:cb:getPlayersInProperty', function(source, property_id)
local property = Property.Get(property_id)
if not property then return end
local players = {}
for src, _ in pairs(property.playersInside) do
local targetSrc = tonumber(src)
if targetSrc ~= source then
local name = GetCharName(targetSrc)
players[#players + 1] = {
src = targetSrc,
name = name
}
end
end
return players or {}
end)
RegisterNetEvent('ps-housing:server:leaveProperty', function (property_id)
local src = source
local property = Property.Get(property_id)
if not property then return end
property:PlayerLeave(src)
end)
-- When player presses doorbell, owner can let them in and this is what is triggered
RegisterNetEvent("ps-housing:server:doorbellAnswer", function (data)
local src = source
local targetSrc = data.targetSrc
local property = Property.Get(data.property_id)
if not property then return end
if not property.playersInside[tostring(src)] then return end
property:RemoveFromDoorbellPool(targetSrc)
property:PlayerEnter(targetSrc)
end)
--@@ NEED TO REDO THIS DOG SHIT
-- I think its not bad anymore but if u got a better idea lmk
RegisterNetEvent("ps-housing:server:buyFurniture", function(property_id, items, price)
local src = source
local citizenid = GetCitizenid(src)
local PlayerData = GetPlayerData(src)
local Player = GetPlayer(src)
-- ik ik, i cbf rn. if your reading this and its still shit, tell me
local property = Property.Get(property_id)
if not property then return end
if not property:CheckForAccess(citizenid) then return end
local price = tonumber(price)
if price > PlayerData.money.bank and price > PlayerData.money.cash then
Framework[Config.Notify].Notify(src, "Du har ikke råd!", "error")
return
end
if price <= PlayerData.money.cash then
Player.Functions.RemoveMoney('cash', price, "Købte møbler")
else
Player.Functions.RemoveMoney('bank', price, "Købte møbler")
end
local numFurnitures = #property.propertyData.furnitures
for i = 1, #items do
numFurnitures = numFurnitures + 1
property.propertyData.furnitures[numFurnitures] = items[i]
end
property:UpdateFurnitures(property.propertyData.furnitures)
Framework[Config.Notify].Notify(src, "Du købte møbler for " .. price..",-", "success")
Framework[Config.Logs].SendLog("**Player ".. GetPlayerName(src) .. "** bought furniture for **$" .. price .. "**")
Debug("Player bought furniture for $" .. price, "by: " .. GetPlayerName(src))
end)
RegisterNetEvent("ps-housing:server:removeFurniture", function(property_id, itemid)
local src = source
local property = Property.Get(property_id)
if not property then return end
local citizenid = GetCitizenid(src)
if not property:CheckForAccess(citizenid) then return end
local currentFurnitures = property.propertyData.furnitures
for k, v in pairs(currentFurnitures) do
if v.id == itemid then
table.remove(currentFurnitures, k)
break
end
end
property:UpdateFurnitures(currentFurnitures)
end)
-- @@ VERY BAD
-- I think its not bad anymore but if u got a better idea lmk
RegisterNetEvent("ps-housing:server:updateFurniture", function(property_id, item)
local src = source
local property = Property.Get(property_id)
if not property then return end
local citizenid = GetCitizenid(src)
if not property:CheckForAccess(citizenid) then return end
local currentFurnitures = property.propertyData.furnitures
for k, v in pairs(currentFurnitures) do
if v.id == item.id then
currentFurnitures[k] = item
Debug("Updated furniture", json.encode(item))
break
end
end
property:UpdateFurnitures(currentFurnitures)
end)
RegisterNetEvent("ps-housing:server:addAccess", function(property_id, srcToAdd)
local src = source
local citizenid = GetCitizenid(src)
local property = Property.Get(property_id)
if not property then return end
if not property.propertyData.owner == citizenid then
-- hacker ban or something
Framework[Config.Notify].Notify(src, "Du ejer ikke ejendommen!", "error")
return
end
local has_access = property.propertyData.has_access
local targetCitizenid = GetCitizenid(srcToAdd)
local targetPlayer = GetPlayerData(srcToAdd)
if not property:CheckForAccess(targetCitizenid) then
has_access[#has_access+1] = targetCitizenid
property:UpdateHas_access(has_access)
Framework[Config.Notify].Notify(src, "Du gav adgang til " .. targetPlayer.charinfo.firstname .. " " .. targetPlayer.charinfo.lastname, "success")
Framework[Config.Notify].Notify(srcToAdd, "Du fik adgang til ejendommen!", "success")
else
Framework[Config.Notify].Notify(src, "Personen har allerede adgang til ejendommen!", "error")
end
end)
RegisterNetEvent("ps-housing:server:removeAccess", function(property_id, citizenidToRemove)
local src = source
local citizenid = GetCitizenid(src)
local property = Property.Get(property_id)
if not property then return end
if not property.propertyData.owner == citizenid then
-- hacker ban or something
Framework[Config.Notify].Notify(src, "Du er ikke ejer af ejendommen!", "error")
return
end
local has_access = property.propertyData.has_access
local citizenidToRemove = citizenidToRemove
if property:CheckForAccess(citizenidToRemove) then
for i = 1, #has_access do
if has_access[i] == citizenidToRemove then
table.remove(has_access, i)
break
end
end
property:UpdateHas_access(has_access)
local playerToAdd = QBCore.Functions.GetPlayerByCitizenId(citizenidToRemove) or QBCore.Functions.GetOfflinePlayerByCitizenId(citizenidToRemove)
local removePlayerData = playerToAdd.PlayerData
local srcToRemove = removePlayerData.source
Framework[Config.Notify].Notify(src, "Du fjernede adgangen for " .. removePlayerData.charinfo.firstname .. " " .. removePlayerData.charinfo.lastname, "success")
if srcToRemove then
Framework[Config.Notify].Notify(srcToRemove, "Du mistede adgangen til " .. (property.propertyData.street or property.propertyData.apartment) .. " " .. property.property_id, "error")
end
else
Framework[Config.Notify].Notify(src, "Denne person har ikke adgang til ejendommen!", "error")
end
end)
lib.callback.register("ps-housing:cb:getPlayersWithAccess", function (source, property_id)
local src = source
local citizenidSrc = GetCitizenid(src)
local property = Property.Get(property_id)
if not property then return end
if property.propertyData.owner ~= citizenidSrc then return end
local withAccess = {}
local has_access = property.propertyData.has_access
for i = 1, #has_access do
local citizenid = has_access[i]
local Player = QBCore.Functions.GetPlayerByCitizenId(citizenid) or QBCore.Functions.GetOfflinePlayerByCitizenId(citizenid)
if Player then
withAccess[#withAccess + 1] = {
citizenid = citizenid,
name = Player.PlayerData.charinfo.firstname .. " " .. Player.PlayerData.charinfo.lastname
}
end
end
return withAccess
end)
lib.callback.register('ps-housing:cb:getPropertyInfo', function (source, property_id)
local src = source
local property = Property.Get(property_id)
if not property then return end
local PlayerData = GetPlayerData(src)
local job = PlayerData.job
local jobName = job.name
local onDuty = job.onduty
if not jobName == Config.RealtorJobName and not onDuty then return end
local data = {}
local ownerPlayer, ownerName
local ownerCid = property.propertyData.owner
if ownerCid then
ownerPlayer = QBCore.Functions.GetPlayerByCitizenId(ownerCid) or QBCore.Functions.GetOfflinePlayerByCitizenId(ownerCid)
ownerName = ownerPlayer.PlayerData.charinfo.firstname .. " " .. ownerPlayer.PlayerData.charinfo.lastname
else
ownerName = "Ingen ejer"
end
data.owner = ownerName
data.street = property.propertyData.street
data.region = property.propertyData.region
data.description = property.propertyData.description
data.for_sale = property.propertyData.for_sale
data.price = property.propertyData.price
data.shell = property.propertyData.shell
data.property_id = property.property_id
return data
end)
RegisterNetEvent('ps-housing:server:resetMetaData', function()
local src = source
local Player = QBCore.Functions.GetPlayer(src)
local insideMeta = Player.PlayerData.metadata["inside"]
insideMeta.property_id = nil
Player.Functions.SetMetaData("inside", insideMeta)
end)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,481 @@
Framework = {}
PoliceJobs = {}
-- Convert config table to usable keys
for i = 1, #Config.PoliceJobNames do
PoliceJobs[Config.PoliceJobNames[i]] = true
end
if IsDuplicityVersion() then
Framework.ox = {}
Framework.qb = {}
function Framework.ox.Notify(src, message, type)
type = type == "inform" and "info" or type
TriggerClientEvent("ox_lib:notify", src, {title="Property", description=message, type=type})
end
function Framework.qb.Notify(src, message, type)
type = type == "info" and "primary" or type
TriggerClientEvent('QBCore:Notify', src, message, type)
end
function Framework.ox.RegisterInventory(stash, label, stashConfig)
exports.ox_inventory:RegisterStash(stash, label, stashConfig.slots, stashConfig.maxweight, nil)
end
function Framework.qb.RegisterInventory(stash, label, stashConfig)
-- Used for ox_inventory compat
end
function Framework.qb.SendLog(message)
if Config.EnableLogs then
TriggerEvent('qb-log:server:CreateLog', 'pshousing', 'Housing System', 'blue', message)
end
end
function Framework.ox.SendLog(message)
-- noop
end
return
end
local function hasApartment(apts)
for propertyId, _ in pairs(apts) do
local property = PropertiesTable[propertyId]
if property.owner then
return true
end
end
return false
end
Framework.qb = {
Notify = function(message, type)
type = type == "info" and "primary" or type
TriggerEvent('QBCore:Notify', message, type)
end,
AddEntrance = function(coords, size, heading, propertyId, enter, raid, showcase, showData, targetName)
local property_id = propertyId
exports["qb-target"]:AddBoxZone(
targetName,
vector3(coords.x, coords.y, coords.z),
size.x,
size.y,
{
name = targetName,
heading = heading,
debugPoly = Config.DebugMode,
minZ = coords.z - 1.5,
maxZ = coords.z + 2.0,
},
{
options = {
{
label = "Gå indenfor",
icon = "fas fa-door-open",
action = enter,
canInteract = function()
local property = Property.Get(property_id)
return property.has_access or property.owner
end,
},
{
label = "Fremvis ejendom",
icon = "fas fa-eye",
action = showcase,
canInteract = function()
local job = PlayerData.job
local jobName = job.name
local onDuty = job.onduty
return jobName == Config.RealtorJobName and onDuty
end,
},
{
label = "Ejendoms info",
icon = "fas fa-circle-info",
action = showData,
canInteract = function()
local job = PlayerData.job
local jobName = job.name
local onDuty = job.onduty
return jobName == Config.RealtorJobName and onDuty
end,
},
{
label = "Ring Doorbell",
icon = "fas fa-bell",
action = enter,
canInteract = function()
local property = Property.Get(property_id)
return not property.has_access and not property.owner
end,
},
{
label = "Ring på døren",
icon = "fas fa-building-shield",
action = raid,
canInteract = function()
local job = PlayerData.job
local jobName = job.name
local gradeAllowed = tonumber(job.grade.level) >= Config.MinGradeToRaid
local onDuty = job.onduty
return PoliceJobs[jobName] and gradeAllowed and onDuty
end,
},
},
}
)
return targetName
end,
AddApartmentEntrance = function(coords, size, heading, apartment, enter, seeAll, seeAllToRaid, targetName)
exports['qb-target']:AddBoxZone(targetName, vector3(coords.x, coords.y, coords.z), size.x, size.y, {
name = targetName,
heading = heading,
debugPoly = Config.DebugMode,
minZ = coords.z - 1.0,
maxZ = coords.z + 2.0,
}, {
options = {
{
label = "Gå indenfor",
action = enter,
icon = "fas fa-door-open",
canInteract = function()
local apartments = ApartmentsTable[apartment].apartments
return hasApartment(apartments)
end,
},
{
label = "Se alle lejligheder",
icon = "fas fa-circle-info",
action = seeAll,
},
{
label = "Ransag lejlighed",
action = seeAllToRaid,
icon = "fas fa-building-shield",
canInteract = function()
local job = PlayerData.job
local jobName = job.name
local gradeAllowed = tonumber(job.grade.level) >= Config.MinGradeToRaid
local onDuty = job.onduty
return PoliceJobs[jobName] and gradeAllowed and onDuty
end,
},
}
})
end,
AddDoorZoneInside = function(coords, size, heading, leave, checkDoor)
exports["qb-target"]:AddBoxZone(
"shellExit",
vector3(coords.x, coords.y, coords.z),
size.x,
size.y,
{
name = "shellExit",
heading = heading,
debugPoly = Config.DebugMode,
minZ = coords.z - 2.0,
maxZ = coords.z + 1.0,
},
{
options = {
{
label = "Forlad ejendom",
action = leave,
icon = "fas fa-right-from-bracket",
},
{
label = "Tjek døren",
action = checkDoor,
icon = "fas fa-bell",
},
},
}
)
return "shellExit"
end,
AddDoorZoneInsideTempShell = function(coords, size, heading, leave)
exports["qb-target"]:AddBoxZone(
"shellExit",
vector3(coords.x, coords.y, coords.z),
size.x,
size.y,
{
name = "shellExit",
heading = heading,
debugPoly = Config.DebugMode,
minZ = coords.z - 2.0,
maxZ = coords.z + 1.0,
},
{
options = {
{
label = "Forlad",
action = leave,
icon = "fas fa-right-from-bracket",
},
},
}
)
return "shellExit"
end,
RemoveTargetZone = function(targetName)
exports["qb-target"]:RemoveZone(targetName)
end,
AddRadialOption = function(id, label, icon, _, event, options)
exports['qb-radialmenu']:AddOption({
id = id,
title = label,
icon = icon,
type = 'client',
event = event,
shouldClose = true,
options = options
}, id)
end,
RemoveRadialOption = function(id)
exports['qb-radialmenu']:RemoveOption(id)
end,
AddTargetEntity = function (entity, label, icon, action)
exports["qb-target"]:AddTargetEntity(entity, {
options = {
{
label = label,
icon = icon,
action = action,
},
},
})
end,
RemoveTargetEntity = function (entity)
exports["qb-target"]:RemoveTargetEntity(entity)
end,
OpenInventory = function (stash, stashConfig)
TriggerServerEvent("inventory:server:OpenInventory", "stash", stash, stashConfig)
TriggerEvent("inventory:client:SetCurrentStash", stash)
end,
}
Framework.ox = {
Notify = function(message, type)
type = type == "inform" and "info" or type
lib.notify({
title = 'Ejendom',
description = message,
type = type
})
end,
AddEntrance = function (coords, size, heading, propertyId, enter, raid, showcase, showData, _)
local property_id = propertyId
local handler = exports.ox_target:addBoxZone({
coords = vector3(coords.x, coords.y, coords.z),
size = vector3(size.y, size.x, size.z),
rotation = heading,
debug = Config.DebugMode,
options = {
{
label = "Gå indenfor",
icon = "fas fa-door-open",
onSelect = enter,
canInteract = function()
local property = Property.Get(property_id)
return property.has_access or property.owner
end,
},
{
label = "Fremvis ejendom",
icon = "fas fa-eye",
onSelect = showcase,
canInteract = function()
-- local property = Property.Get(property_id)
-- if property.propertyData.owner ~= nil then return false end -- if its owned, it cannot be showcased
local job = PlayerData.job
local jobName = job.name
return jobName == Config.RealtorJobName
end,
},
{
label = "Ejendoms info",
icon = "fas fa-circle-info",
onSelect = showData,
canInteract = function()
local job = PlayerData.job
local jobName = job.name
local onDuty = job.onduty
return jobName == Config.RealtorJobName and onDuty
end,
},
{
label = "Ring på døren",
icon = "fas fa-bell",
onSelect = enter,
canInteract = function()
local property = Property.Get(property_id)
return not property.has_access and not property.owner
end,
},
{
label = "Ransag lejlighed",
icon = "fas fa-building-shield",
onSelect = raid,
canInteract = function()
local job = PlayerData.job
local jobName = job.name
local gradeAllowed = tonumber(job.grade.level) >= Config.MinGradeToRaid
local onDuty = job.onduty
return PoliceJobs[jobName] and onDuty and gradeAllowed
end,
},
},
})
return handler
end,
AddApartmentEntrance = function (coords, size, heading, apartment, enter, seeAll, seeAllToRaid, _)
local handler = exports.ox_target:addBoxZone({
coords = vector3(coords.x, coords.y, coords.z),
size = vector3(size.y, size.x, size.z),
rotation = heading,
debug = Config.DebugMode,
options = {
{
label = "Gå indenfor",
onSelect = enter,
icon = "fas fa-door-open",
canInteract = function()
local apartments = ApartmentsTable[apartment].apartments
return hasApartment(apartments)
end,
},
{
label = "Se alle lejligheder",
onSelect = seeAll,
icon = "fas fa-circle-info",
},
{
label = "Ransag lejlighed",
onSelect = seeAllToRaid,
icon = "fas fa-building-shield",
canInteract = function()
local job = PlayerData.job
local jobName = job.name
local gradeAllowed = tonumber(job.grade.level) >= Config.MinGradeToRaid
local onDuty = job.onduty
return PoliceJobs[jobName] and onDuty and gradeAllowed
end,
},
},
})
return handler
end,
AddDoorZoneInside = function (coords, size, heading, leave, checkDoor)
local handler = exports.ox_target:addBoxZone({
coords = vector3(coords.x, coords.y, coords.z), --z = 3.0
size = vector3(size.y, size.x, size.z),
rotation = heading,
debug = Config.DebugMode,
options = {
{
name = "leave",
label = "Forlad ejendom",
onSelect = leave,
icon = "fas fa-right-from-bracket",
},
{
name = "doorbell",
label = "Tjek døren",
onSelect = checkDoor,
icon = "fas fa-bell",
},
},
})
return handler
end,
AddDoorZoneInsideTempShell = function (coords, size, heading, leave)
local handler = exports.ox_target:addBoxZone({
coords = vector3(coords.x, coords.y, coords.z), --z = 3.0
size = vector3(size.y, size.x, size.z),
rotation = heading,
debug = Config.DebugMode,
options = {
{
name = "leave",
label = "Forlad",
onSelect = leave,
icon = "fas fa-right-from-bracket",
},
},
})
print("made")
return handler
end,
RemoveTargetZone = function (handler)
exports.ox_target:removeZone(handler)
end,
AddRadialOption = function(id, label, icon, fn)
lib.addRadialItem({
id = id,
icon = icon,
label = label,
onSelect = fn,
})
end,
RemoveRadialOption = function(id)
lib.removeRadialItem(id)
end,
AddTargetEntity = function (entity, label, icon, action)
exports.ox_target:addLocalEntity(entity, {
{
name = label,
label = label,
icon = icon,
onSelect = action,
},
})
end,
RemoveTargetEntity = function (entity)
exports.ox_target:removeLocalEntity(entity)
end,
OpenInventory = function (stash, stashConfig)
exports.ox_inventory:openInventory('stash', stash)
end,
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More