diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9c97eb2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = false +indent_style = space +charset = utf-8 +trim_trailing_whitespace = true +indent_size = 4 diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index dfe0770..0000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..7d53132 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,73 @@ +# Contributor Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, +religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team in the discord at https://discord.gg/Z6Whda5hHA. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..7605230 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,63 @@ +name: Bug report +description: Create a report to help us improve or fix something +labels: ['bug', 'need repro'] +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to fill out a bug report! + Please use our Discord Server to ask questions and receive support: https://discord.gg/Z6Whda5hHA + - type: input + id: summary + attributes: + label: Summary + description: Write a short and concise description of your bug. + validations: + required: true + - type: textarea + id: repro + attributes: + label: Reproduction + description: What did you do to make this happen? + placeholder: | + 1. Using ... + 2. Do ... + 3. Then use ... + 4. See error + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What did you expect to happen? + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + description: What actually happened? + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional context + description: If you have any other context about the problem such as screenshots or videos, add them here. + - type: input + id: updated + attributes: + label: Current Version + description: What version of the resource are you currently using? + placeholder: e.g. v1.3.0, v1.4.0 + validations: + required: true + - type: input + id: custom + attributes: + label: Custom Resources + description: Are you using custom resources? Which ones? + placeholder: e.g. zdiscord, qb-target + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..e4ba2fd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Qbox Discord Server + url: https://discord.gg/Z6Whda5hHA + about: Ask questions, receive support, and discuss with the community in our Discord server. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..183d114 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,36 @@ +name: Feature request +description: Suggest an idea for Qbox +labels: enhancement +body: + - type: markdown + attributes: + value: | + Please use our Discord Server to ask questions and receive support: https://discord.gg/Z6Whda5hHA + - type: textarea + id: problem + attributes: + label: The problem + description: A clear and concise description of what the problem is, or what feature you want to be implemented. + placeholder: | + Some examples: + I'm frustrated that ... + It would be nice if ... + validations: + required: true + - type: textarea + id: solution + attributes: + label: Ideal solution + description: A clear and concise description of what you want to happen, with as much detail as possible. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternative solutions + description: A clear and concise description of any alternative solutions or features you've considered. + - type: textarea + id: additional + attributes: + label: Additional context + description: If you have any other context about the problem such as screenshots or videos, add them here. diff --git a/.github/actions/bump-manifest-version.js b/.github/actions/bump-manifest-version.js new file mode 100644 index 0000000..c7895c7 --- /dev/null +++ b/.github/actions/bump-manifest-version.js @@ -0,0 +1,14 @@ +const fs = require('fs') + +const version = process.env.TGT_RELEASE_VERSION +const newVersion = version.replace('v', '') + +const manifestFile = fs.readFileSync('fxmanifest.lua', {encoding: 'utf8'}) + +let newFileContent = manifestFile.replace(/\bversion\s+(.*)$/gm, `version '${newVersion}'`) + +if (newFileContent == manifestFile) { + newFileContent = manifestFile.replace(/\bgame\s+(.*)$/gm, `game 'gta5'\nversion '${newVersion}'`); +} + +fs.writeFileSync('fxmanifest.lua', newFileContent) \ No newline at end of file diff --git a/.github/contributing.md b/.github/contributing.md new file mode 100644 index 0000000..741ad62 --- /dev/null +++ b/.github/contributing.md @@ -0,0 +1,99 @@ +# Contributing to Qbox + +Thank you for taking the time to contribute! + +These guidelines will help you help us in the best way possible regardless of your skill level. We ask that you try to read everything related to the way you'd like to contribute and try and use your best judgement for anything not covered. + +Make sure to also read our [Contributor Code of Conduct](./CODE_OF_CONDUCT.md). + +If you still have further questions after reading be sure to join the [Qbox Discord server][discord link]. + +## Issues + +Open a new issue to report a bug or request a new feature or improvement. + +If you want to ask a question, issues are not the place to do so. Please join our [Discord server][discord link] and ask over there instead. + +Before opening a new issue: + +- [Search](https://github.com/issues?q=is%3Aissue+org%3AQbox-Project) for existing issues; maybe someone else already experienced the same problem that you're having! Duplicate issues will be closed. +- Download the latest release of the resource you are opening the issue for to make sure that it wasn't already fixed. Issues that are already fixed are considered invalid and will be closed. + +When opening a new issue, make sure to pick the right type of form and fill it out. The more information you provide, the easier it will be for us to work on your new issue. Issues that are invalid or do not follow the correct form may be ignored or even closed. + +## Pull Requests + +Open a new pull request to contribute code. + +You can use your own editor of choice, but we recommend using [VSCode](https://code.visualstudio.com/) with these extensions: + +- [GitLens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) +- [Lua Language Server](https://marketplace.visualstudio.com/items?itemName=sumneko.lua) +- [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) +- [CfxLua IntelliSense](https://marketplace.visualstudio.com/items?itemName=overextended.cfxlua-vscode) + +If you are new to contributing code, you can check out and try to fix issues marked with [`good first issue`](https://github.com/issues?q=is%3Aissue+is%3Aopen+org%3AQbox-Project+label%3A%22good+first+issue%22). + +If you want to contribute code but don't know what to do, please check out issues marked with [`help wanted`](https://github.com/issues?q=is%3Aissue+is%3Aopen+org%3AQbox-Project+label%3A%22help+wanted%22) as those are issues that we actually *need* help with. + +If you want to contribute code unrelated to an existing issue, you should open an issue yourself or ask over on the [Discord server][discord link] to discuss it with our team and ask whether your change is wanted, especially if you are planning on adding new features or large designs. + +Before opening a pull request: + +- Make sure that your contribution fits our [code conventions](#code-conventions) described below. After opening a pull request your code will be checked according to them. +- Make sure that your pull request is small and focused. Break it into multiple smaller pull requests if not (see [Small Pull Request Manifesto](https://github.com/PlaytikaOSS/small-pull-request-manifesto)). +- It is recommended to test your changes to make sure they work and don't break existing functionality. + +When opening a pull request, make sure to follow the template and explain your changes. If you are trying to contribute something related to a GitHub issue, make sure to mention it as well. + +## Code Conventions + +Below are conventions that you must follow when contributing code. + +### Commit Message Conventions + +- The first line of a commit message must be 72 characters at most. +- Commit messages and pull request titles must follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). + - Use `fix:` when patching a bug. + - Use `feat:` when introducing a new feature. + - Use `refactor:` when altering code without changing functionality. + - Use `chore:` when your changes don't alter production code. + - Append a `!` after the type/scope of the feature when you change code and introduce a breaking API change. Optionally add a footer to the bottom of your commit message separated by 2 newlines, starting with `BREAKING CHANGE:`, and explaining the breaking change. + +### Lua Conventions + +#### General Style + +- Use 4 space indentation. +- Prefer creating local variables over global ones. +- Don't repeat yourself. If you're using the same operations in multiple different places convert them into a flexible function. +- Exported functions must be properly annotated (see [LuaLS Annotations](https://luals.github.io/wiki/annotations/)). +- Utilize [ox_lib](https://overextended.dev/ox_lib) to make your life easier. Prefer lib calls over native ones. +- Make use of config options where it makes sense to make features optional and/or customizable. Configs should not be modified by other code. + +#### Optimization & Security + +- Consider [this Lua Performance guide](https://springrts.com/wiki/Lua_Performance). +- Don't create unnecessary threads. Always try to find a better method of triggering events. +- Set longer `Wait` calls in distance checking loops when the player is out of range. +- Don't waste cycles; job specific loops should only run for players with that job. +- When possible don't trust the client, *especially* with transactions. +- Balance security and optimizations. +- Use `#(vector3 - vector3)` instead of `GetDistanceBetweenCoords()`. +- Use `myTable[#myTable + 1] = 'value'` instead of `table.insert(myTable, 'value')`. +- Use `myTable['key'] = 'value'` instead of `table.insert(myTable, 'key', 'value')`. + +#### Naming + +- Use `camelCase` for local variables and functions. +- Use `PascalCase` for global variables and functions. +- Avoid acronyms as they can be confusing and context dependant. +- Be descriptive; make it easy for the reader. + - Booleans may be prefixed with `is`, `has`, `are`, etc. + - Arrays should have plural names. + +### JavaScript/TypeScript Conventions + +Consider following the [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html) and the [Google TypeScript Style Guide](https://google.github.io/styleguide/tsguide.html). + +[discord link]: https://discord.gg/Z6Whda5hHA diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..786cbf9 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,10 @@ +## Description + + + +## Checklist + + + +- [ ] I have personally loaded this code into an updated Qbox project and checked all of its functionality. +- [ ] My pull request fits the contribution guidelines & code conventions. diff --git a/.github/workflows/discord-commit.yml b/.github/workflows/discord-commit.yml new file mode 100644 index 0000000..3de7c75 --- /dev/null +++ b/.github/workflows/discord-commit.yml @@ -0,0 +1,16 @@ +name: "Discord Commit" + +on: [push] + +jobs: + report-status: + if: github.event.repository.default_branch == github.ref_name + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Discord Webhook + uses: ChatDisabled/discord-commits@main + with: + id: ${{ secrets.WEBHOOK_ID }} + token: ${{ secrets.WEBHOOK_TOKEN }} diff --git a/.github/workflows/discord-release.yml b/.github/workflows/discord-release.yml new file mode 100644 index 0000000..00b4047 --- /dev/null +++ b/.github/workflows/discord-release.yml @@ -0,0 +1,21 @@ +name: "Discord Releases" + +on: + release: + types: [published] + +jobs: + github-releases-to-discord: + name: Discord Releases Changelog + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Github Releases To Discord + uses: SethCohen/github-releases-to-discord@v1.15.0 + with: + webhook_url: ${{ secrets.WEBHOOK_URL }} + color: "15852866" + username: ${{ github.event.repository.name }} + avatar_url: "https://i.imgur.com/Eh1yiLI.png" + footer_timestamp: true diff --git a/.github/workflows/issues-project.yml b/.github/workflows/issues-project.yml new file mode 100644 index 0000000..4e5ed18 --- /dev/null +++ b/.github/workflows/issues-project.yml @@ -0,0 +1,22 @@ +name: Issues Project Management + +on: + issues: + types: + - opened + +jobs: + add-to-project: + name: Add issue to project + runs-on: ubuntu-latest + steps: + - name: Get App Token + uses: actions/create-github-app-token@v1 + id: generate_token + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.PRIVATE_KEY }} + - uses: actions/add-to-project@v0.5.0 + with: + project-url: https://github.com/orgs/Qbox-project/projects/4 + github-token: ${{ steps.generate_token.outputs.token }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..d19aba5 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,23 @@ +name: Lint +on: [push, pull_request_target] +jobs: + lint: + name: Lint Resource + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Lint + uses: iLLeniumStudios/fivem-lua-lint-action@v2 + with: + capture: "junit.xml" + args: "-t --formatter JUnit" + extra_libs: ox_lib+mysql+qblocales+qbox+qbox_playerdata+qbox_lib + - name: Generate Lint Report + if: always() + uses: mikepenz/action-junit-report@v4 + with: + report_paths: "**/junit.xml" + check_name: Linting Report + fail_on_failure: false diff --git a/.github/workflows/release-action.yml b/.github/workflows/release-action.yml new file mode 100644 index 0000000..e8b7c54 --- /dev/null +++ b/.github/workflows/release-action.yml @@ -0,0 +1,43 @@ +name: "release-action" + +on: + push: + tags: + - "v*" + +jobs: + release-action: + name: "Create Release" + runs-on: "ubuntu-latest" + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.repository.default_branch }} + + - name: Install ZIP + run: sudo apt install zip + + - name: Bundle files + run: | + rm -rf ./.github ./.vscode ./.git + shopt -s extglob + mkdir ./${{ github.event.repository.name }} + cp -r !(${{ github.event.repository.name }}) ${{ github.event.repository.name }} + zip -r ./${{ github.event.repository.name }}.zip ./${{ github.event.repository.name }} + + - name: Get App Token + uses: actions/create-github-app-token@v1 + id: generate_token + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.PRIVATE_KEY }} + + - name: Create Release + uses: marvinpinto/action-automatic-releases@latest + with: + title: ${{ github.ref_name }} + repo_token: '${{ steps.generate_token.outputs.token }}' + prerelease: false + files: ${{ github.event.repository.name }}.zip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0fe126a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,46 @@ +name: Create New Release + +on: + workflow_dispatch: + inputs: + version: + required: true + +jobs: + create-release: + name: Create New Release + runs-on: ubuntu-latest + steps: + - name: Get App Token + uses: actions/create-github-app-token@v1 + id: generate_token + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.PRIVATE_KEY }} + + - name: Checkout Repository + uses: actions/checkout@v4 + with: + token: ${{ steps.generate_token.outputs.token }} + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20.x + + - name: Bump manifest version + run: node .github/actions/bump-manifest-version.js + env: + TGT_RELEASE_VERSION: ${{ inputs.version }} + + - name: Push manifest change + uses: EndBug/add-and-commit@v9 + with: + add: fxmanifest.lua + push: true + message: 'chore: bump manifest version to ${{ inputs.version }}' + + - name: Push Git Tag + run: | + git tag ${{ inputs.version }} + git push origin ${{ inputs.version }} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..5de9729 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "sumneko.lua", + "overextended.cfxlua-vscode", + "ihyajb.qbcore-code-snippets", + "EditorConfig.EditorConfig", + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1f091d0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "Lua.runtime.nonstandardSymbol": ["/**/", "`", "+=", "-=", "*=", "/="], + "Lua.runtime.version": "Lua 5.4", + "Lua.diagnostics.globals": [ + "lib", + "cache", + "QBX", + "locale", + "qbx" + ] +} diff --git a/client/main.lua b/client/main.lua new file mode 100644 index 0000000..136b8e9 --- /dev/null +++ b/client/main.lua @@ -0,0 +1,328 @@ +local config = require 'config.client' +local currentStatusList = {} +local casings = {} +local currentCasing = nil +local bloodDrops = {} +local currentBloodDrop = nil +local fingerprints = {} +local currentFingerprint = 0 +local shotAmount = 0 + +local statusList = { + fight = locale('evidence.red_hands'), + widepupils = locale('evidence.wide_pupils'), + redeyes = locale('evidence.red_eyes'), + weedsmell = locale('evidence.weed_smell'), + gunpowder = locale('evidence.gunpowder'), + chemicals = locale('evidence.chemicals'), + heavybreath = locale('evidence.heavy_breathing'), + sweat = locale('evidence.sweat'), + handbleed = locale('evidence.handbleed'), + confused = locale('evidence.confused'), + alcohol = locale('evidence.alcohol'), + heavyalcohol = locale('evidence.heavy_alcohol'), + agitated = locale('evidence.agitated'), +} + +local ignoredWeapons = { + [`weapon_unarmed`] = true, + [`weapon_snowball`] = true, + [`weapon_stungun`] = true, + [`weapon_petrolcan`] = true, + [`weapon_hazardcan`] = true, + [`weapon_fireextinguisher`] = true, +} + +local function dropBulletCasing(weapon, ped) + local randX = math.random() + math.random(-1, 1) + local randY = math.random() + math.random(-1, 1) + local coords = GetOffsetFromEntityInWorldCoords(ped, randX, randY, 0) + local serial = exports.ox_inventory:getCurrentWeapon().metadata.serial + TriggerServerEvent('evidence:server:CreateCasing', weapon, serial, coords) + Wait(300) +end + +local function dnaHash(s) + local h = string.gsub(s, '.', function(c) + return string.format('%02x', string.byte(c)) + end) + return h +end + +RegisterNetEvent('evidence:client:SetStatus', function(statusId, time) + if time > 0 and statusList[statusId] then + if not currentStatusList?[statusId] or currentStatusList[statusId].time < 20 then + currentStatusList[statusId] = { + text = statusList[statusId], + time = time + } + exports.qbx_core:Notify(currentStatusList[statusId].text, 'error') + end + elseif statusList[statusId] then + currentStatusList[statusId] = nil + end + TriggerServerEvent('evidence:server:UpdateStatus', currentStatusList) +end) + +RegisterNetEvent('evidence:client:AddBlooddrop', function(bloodId, citizenid, bloodtype, coords) + bloodDrops[bloodId] = { + citizenid = citizenid, + bloodtype = bloodtype, + coords = vec3(coords.x, coords.y, coords.z - 0.9) + } +end) + +RegisterNetEvent('evidence:client:RemoveBlooddrop', function(bloodId) + bloodDrops[bloodId] = nil + currentBloodDrop = 0 +end) + +RegisterNetEvent('evidence:client:AddFingerPrint', function(fingerId, fingerprint, coords) + fingerprints[fingerId] = { + fingerprint = fingerprint, + coords = vec3(coords.x, coords.y, coords.z - 0.9) + } +end) + +RegisterNetEvent('evidence:client:RemoveFingerprint', function(fingerId) + fingerprints[fingerId] = nil + currentFingerprint = 0 +end) + +RegisterNetEvent('evidence:client:ClearBlooddropsInArea', function() + local pos = GetEntityCoords(cache.ped) + local bloodDropList = {} + if lib.progressCircle({ + duration = 5000, + position = 'bottom', + label = locale('progressbar.blood_clear'), + useWhileDead = false, + canCancel = true, + disable = { + move = false, + car = false, + combat = true, + mouse = false + } + }) + then + if bloodDrops and next(bloodDrops) then + for bloodId in pairs(bloodDrops) do + if #(pos - bloodDrops[bloodId].coords) < 10.0 then + bloodDropList[#bloodDropList + 1] = bloodId + end + end + TriggerServerEvent('evidence:server:ClearBlooddrops', bloodDropList) + exports.qbx_core:Notify(locale('success.blood_clear'), 'success') + end + else + exports.qbx_core:Notify(locale('error.blood_not_cleared'), 'error') + end +end) + +RegisterNetEvent('evidence:client:AddCasing', function(casingId, weapon, coords, serie) + casings[casingId] = { + type = weapon, + serie = serie and serie or locale('evidence.serial_not_visible'), + coords = vec3(coords.x, coords.y, coords.z - 0.9) + } +end) + +RegisterNetEvent('evidence:client:RemoveCasing', function(casingId) + casings[casingId] = nil + currentCasing = 0 +end) + +RegisterNetEvent('evidence:client:ClearCasingsInArea', function() + local pos = GetEntityCoords(cache.ped) + local casingList = {} + + if lib.progressCircle({ + duration = 5000, + position = 'bottom', + label = locale('progressbar.bullet_casing'), + useWhileDead = false, + canCancel = true, + disable = { + move = false, + car = false, + combat = true, + mouse = false, + } + }) + then + if casings and next(casings) then + for casingId in pairs(casings) do + if #(pos - casings[casingId].coords) < 10.0 then + casingList[#casingList + 1] = casingId + end + end + TriggerServerEvent('evidence:server:ClearCasings', casingList) + exports.qbx_core:Notify(locale('success.bullet_casing_removed'), 'success') + end + else + exports.qbx_core:Notify(locale('error.bullet_casing_not_removed'), 'error') + end +end) + +local function updateStatus() + if not LocalPlayer.state.isLoggedIn then return end + if currentStatusList and next(currentStatusList) then + for k in pairs(currentStatusList) do + if currentStatusList[k].time > 0 then + currentStatusList[k].time -= 10 + else + currentStatusList[k].time = 0 + end + end + TriggerServerEvent('evidence:server:UpdateStatus', currentStatusList) + end + if shotAmount > 0 then + shotAmount = 0 + end +end + +CreateThread(function() + while true do + Wait(10000) + updateStatus() + end +end) + +local function onPlayerShooting() + shotAmount += 1 + if shotAmount > 5 and not currentStatusList?.gunpowder then + if math.random(1, 10) <= 7 then + TriggerEvent('evidence:client:SetStatus', 'gunpowder', 200) + end + end + dropBulletCasing(cache.weapon, cache.ped) +end + +CreateThread(function() -- Gunpowder Status when shooting + while true do + Wait(0) + if IsPedShooting(cache.ped) and not ignoredWeapons[cache.weapon] then + onPlayerShooting() + end + end +end) + +---@param coords vector3 +---@return string +local function getStreetLabel(coords) + local s1, s2 = GetStreetNameAtCoord(coords.x, coords.y, coords.z) + local street1 = GetStreetNameFromHashKey(s1) + local street2 = GetStreetNameFromHashKey(s2) + local streetLabel = street1 + if street2 then + streetLabel = streetLabel .. ' | ' .. street2 + end + local sanitized = streetLabel:gsub("%'", "") + return sanitized +end + +local function getPlayerDistanceFromCoords(coords) + local pos = GetEntityCoords(cache.ped) + return #(pos - coords) +end + +---@class DrawEvidenceIfInRangeArgs +---@field evidenceId integer +---@field coords vector3 +---@field text string +---@field metadata table +---@field serverEventOnPickup string + +---@param args DrawEvidenceIfInRangeArgs +local function drawEvidenceIfInRange(args) + if getPlayerDistanceFromCoords(args.coords) >= 1.5 then return end + qbx.drawText3d({text = args.text, coords = args.coords}) + if IsControlJustReleased(0, 47) then + TriggerServerEvent(args.serverEventOnPickup, args.evidenceId, args.metadata) + end +end + +--- draw 3D text on the ground to show evidence, if they press pickup button, set metadata and add it to their inventory. +CreateThread(function() + while true do + Wait(0) + if currentCasing and currentCasing ~= 0 then + drawEvidenceIfInRange({ + evidenceId = currentCasing, + coords = casings[currentCasing].coords, + text = locale('info.bullet_casing', casings[currentCasing].type), + metadata = { + type = locale('info.casing'), + street = getStreetLabel(casings[currentCasing].coords), + ammolabel = config.ammoLabels[exports.qbx_core:GetWeapons()[casings[currentCasing].type].ammotype], + ammotype = casings[currentCasing].type, + serie = casings[currentCasing].serie + }, + serverEventOnPickup = 'evidence:server:AddCasingToInventory' + }) + end + + if currentBloodDrop and currentBloodDrop ~= 0 then + drawEvidenceIfInRange({ + evidenceId = currentBloodDrop, + coords = bloodDrops[currentBloodDrop].coords, + text = locale('info.blood_text', dnaHash(bloodDrops[currentBloodDrop].citizenid)), + metadata = { + type = locale('info.blood'), + street = getStreetLabel(bloodDrops[currentBloodDrop].coords), + dnalabel = dnaHash(bloodDrops[currentBloodDrop].citizenid), + bloodtype = bloodDrops[currentBloodDrop].bloodtype + }, + serverEventOnPickup = 'evidence:server:AddBlooddropToInventory' + }) + end + + if currentFingerprint and currentFingerprint ~= 0 then + drawEvidenceIfInRange({ + evidenceId = currentFingerprint, + coords = fingerprints[currentFingerprint].coords, + text = locale('info.fingerprint_text'), + metadata = { + type = locale('info.fingerprint'), + street = getStreetLabel(fingerprints[currentFingerprint].coords), + fingerprint = fingerprints[currentFingerprint].fingerprint + }, + serverEventOnPickup = 'evidence:server:AddFingerprintToInventory' + }) + end + end +end) + +local function canDiscoverEvidence() + return LocalPlayer.state.isLoggedIn + and QBX.PlayerData.job.type == 'leo' + and QBX.PlayerData.job.onduty + and IsPlayerFreeAiming(cache.playerId) + and cache.weapon == `WEAPON_FLASHLIGHT` +end + +---@param evidence table +---@return number? evidenceId +local function getCloseEvidence(evidence) + local pos = GetEntityCoords(cache.ped, true) + for evidenceId, v in pairs(evidence) do + local dist = #(pos - v.coords) + if dist < 1.5 then + return evidenceId + end + end +end + +CreateThread(function() + while true do + local closeEvidenceSleep = 1000 + if canDiscoverEvidence() then + closeEvidenceSleep = 10 + currentCasing = getCloseEvidence(casings) or currentCasing + currentBloodDrop = getCloseEvidence(bloodDrops) or currentBloodDrop + currentFingerprint = getCloseEvidence(fingerprints) or currentFingerprint + end + Wait(closeEvidenceSleep) + end +end) \ No newline at end of file diff --git a/config/client.lua b/config/client.lua new file mode 100644 index 0000000..d96ea67 --- /dev/null +++ b/config/client.lua @@ -0,0 +1,10 @@ +return { + ammoLabels = { + AMMO_PISTOL = '9x19mm parabellum bullet', + AMMO_SMG = '9x19mm parabellum bullet', + AMMO_RIFLE = '7.62x39mm bullet', + AMMO_MG = '7.92x57mm mauser bullet', + AMMO_SHOTGUN = '12-gauge bullet', + AMMO_SNIPER = 'Large caliber bullet', + }, +} \ No newline at end of file diff --git a/fxmanifest.lua b/fxmanifest.lua new file mode 100644 index 0000000..a864ebc --- /dev/null +++ b/fxmanifest.lua @@ -0,0 +1,32 @@ +fx_version 'cerulean' +game 'gta5' + +name 'qbx_evidence' +description 'Evidence system for Qbox' +repository 'https://github.com/Qbox-project/qbx_evidence' +version '1.0.0' + +ox_lib 'locale' + +shared_scripts { + '@ox_lib/init.lua', + '@qbx_core/modules/lib.lua' +} + +client_scripts { + '@qbx_core/modules/playerdata.lua', + 'client/main.lua' +} + +server_scripts { + '@oxmysql/lib/MySQL.lua', + 'server/main.lua', +} + +files { + 'config/client.lua', + 'locales/*.json', +} + +lua54 'yes' +use_experimental_fxv2_oal 'yes' \ No newline at end of file diff --git a/server/main.lua b/server/main.lua new file mode 100644 index 0000000..1baa924 --- /dev/null +++ b/server/main.lua @@ -0,0 +1,136 @@ +local playerStatus = {} +local casings = {} +local bloodDrops = {} +local fingerDrops = {} + +local function generateId(table) + local id = lib.string.random('11111') + if not table then return id end + while table[id] do + id = lib.string.random('11111') + end + return id +end + +lib.callback.register('police:GetPlayerStatus', function(_, targetSrc) + local player = exports.qbx_core:GetPlayer(targetSrc) + if not player or not next(playerStatus[targetSrc]) then return {} end + local status = playerStatus[targetSrc] + + local statList = {} + for i = 1, #status do + statList[#statList + 1] = status[i].text + end + + return statList +end) + +RegisterNetEvent('evidence:server:UpdateStatus', function(data) + playerStatus[source] = data +end) + +RegisterNetEvent('evidence:server:CreateBloodDrop', function(citizenid, bloodtype, coords) + local bloodId = generateId(bloodDrops) + bloodDrops[bloodId] = { + dna = citizenid, + bloodtype = bloodtype + } + TriggerClientEvent('evidence:client:AddBlooddrop', -1, bloodId, citizenid, bloodtype, coords) +end) + +RegisterNetEvent('evidence:server:CreateFingerDrop', function(coords) + local player = exports.qbx_core:GetPlayer(source) + local fingerId = generateId(fingerDrops) + fingerDrops[fingerId] = player.PlayerData.metadata.fingerprint + TriggerClientEvent('evidence:client:AddFingerPrint', -1, fingerId, player.PlayerData.metadata.fingerprint, coords) +end) + +RegisterNetEvent('evidence:server:ClearBlooddrops', function(bloodDropList) + if not bloodDropList or not next(bloodDropList) then return end + for _, v in pairs(bloodDropList) do + TriggerClientEvent('evidence:client:RemoveBlooddrop', -1, v) + bloodDrops[v] = nil + end +end) + +RegisterNetEvent('evidence:server:AddBlooddropToInventory', function(bloodId, bloodInfo) + local src = source + local player = exports.qbx_core:GetPlayer(src) + local playerName = player.PlayerData.charinfo.firstname..' '..player.PlayerData.charinfo.lastname + local streetName = bloodInfo.street + local bloodType = bloodInfo.bloodtype + local bloodDNA = bloodInfo.dnalabel + local metadata = {} + metadata.type = 'Blood Evidence' + metadata.description = 'DNA ID: '..bloodDNA + metadata.description = metadata.description..'\n\nBlood Type: '..bloodType + metadata.description = metadata.description..'\n\nCollected By: '..playerName + metadata.description = metadata.description..'\n\nCollected At: '..streetName + if not exports.ox_inventory:RemoveItem(src, 'empty_evidence_bag', 1) then + return exports.qbx_core:Notify(src, locale('error.have_evidence_bag'), 'error') + end + if exports.ox_inventory:AddItem(src, 'filled_evidence_bag', 1, metadata) then + TriggerClientEvent('evidence:client:RemoveBlooddrop', -1, bloodId) + bloodDrops[bloodId] = nil + end +end) + +RegisterNetEvent('evidence:server:AddFingerprintToInventory', function(fingerId, fingerInfo) + local src = source + local player = exports.qbx_core:GetPlayer(src) + local playerName = player.PlayerData.charinfo.firstname..' '..player.PlayerData.charinfo.lastname + local streetName = fingerInfo.street + local fingerprint = fingerInfo.fingerprint + local metadata = {} + metadata.type = 'Fingerprint Evidence' + metadata.description = 'Fingerprint ID: '..fingerprint + metadata.description = metadata.description..'\n\nCollected By: '..playerName + metadata.description = metadata.description..'\n\nCollected At: '..streetName + if not exports.ox_inventory:RemoveItem(src, 'empty_evidence_bag', 1) then + return exports.qbx_core:Notify(src, locale('error.have_evidence_bag'), 'error') + end + if exports.ox_inventory:AddItem(src, 'filled_evidence_bag', 1, metadata) then + TriggerClientEvent('evidence:client:RemoveFingerprint', -1, fingerId) + fingerDrops[fingerId] = nil + end +end) + +RegisterNetEvent('evidence:server:CreateCasing', function(weapon, serial, coords) + local casingId = generateId(casings) + local serieNumber = exports.ox_inventory:GetCurrentWeapon(source).metadata.serial + if not serieNumber then + serieNumber = serial + end + TriggerClientEvent('evidence:client:AddCasing', -1, casingId, weapon, coords, serieNumber) +end) + +RegisterNetEvent('evidence:server:ClearCasings', function(casingList) + if casingList and next(casingList) then + for _, v in pairs(casingList) do + TriggerClientEvent('evidence:client:RemoveCasing', -1, v) + casings[v] = nil + end + end +end) + +RegisterNetEvent('evidence:server:AddCasingToInventory', function(casingId, casingInfo) + local src = source + local player = exports.qbx_core:GetPlayer(src) + local playerName = player.PlayerData.charinfo.firstname..' '..player.PlayerData.charinfo.lastname + local streetName = casingInfo.street + local ammoType = casingInfo.ammolabel + local serialNumber = casingInfo.serie + local metadata = {} + metadata.type = 'Casing Evidence' + metadata.description = 'Ammo Type: '..ammoType + metadata.description = metadata.description..'\n\nSerial #: '..serialNumber + metadata.description = metadata.description..'\n\nCollected By: '..playerName + metadata.description = metadata.description..'\n\nCollected At: '..streetName + if not exports.ox_inventory:RemoveItem(src, 'empty_evidence_bag', 1) then + return exports.qbx_core:Notify(src, locale('error.have_evidence_bag'), 'error') + end + if exports.ox_inventory:AddItem(src, 'filled_evidence_bag', 1, metadata) then + TriggerClientEvent('evidence:client:RemoveCasing', -1, casingId) + casings[casingId] = nil + end +end) \ No newline at end of file