1
0
Fork 0
forked from Simnation/Main
This commit is contained in:
Nordi98 2025-08-06 16:37:06 +02:00
parent 510e3ffcf2
commit f43cf424cf
305 changed files with 34683 additions and 0 deletions

View file

@ -0,0 +1,39 @@
## Pull Request Checklist
- [ ] PR is targeting the `dev` branch
- [ ] I have pulled the latest changes from `dev` and resolved any merge conflicts
- [ ] My code follows the projects style guidelines
- [ ] I have tested my changes and ensured they work as expected
- [ ] Relevant documentation/comments have been added or updated
- [ ] Any dependent changes have been merged
### Resource Relevance and Usage
- [ ] Resource is active on 100+ servers, OR
- [ ] Resource has a verifiable dependency on community_bridge, OR
- [ ] Exception justified (adds value or supports project goals)
### Avoiding Breaking Changes
- [ ] No breaking changes introduced, OR
- [ ] If deprecating: 2-4 month grace period provided
- [ ] Deprecation notices clearly documented in code with dates
### Documentation and Testing
- [ ] At least one usage example included.
- [ ] IntelliSense-style comments added.
- [ ] Unit test coverage provided
## Description
<!-- Please include a summary of the changes and the purpose of this PR -->
## Related Issues
<!-- If this PR addresses any issues, please link them here using "Fixes #issue_number" -->
## Testing Steps
<!-- Describe how the changes were tested and how reviewers can verify them -->
## Additional Notes
<!-- Add any other relevant information or context here -->

View file

@ -0,0 +1,73 @@
name: Update Webhook Notification
on:
release:
types: [published]
jobs:
send-webhook:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq
- name: Send webhook notification
env:
WEBHOOK_URL: ${{ secrets.RELEASE_WEBHOOK_URL }}
run: |
# Extract repository name and release details
REPO_NAME=$(basename "$GITHUB_REPOSITORY")
RELEASE_VERSION=$(jq -r '.release.tag_name // "Unknown Version"' "$GITHUB_EVENT_PATH")
TITLE="$REPO_NAME $RELEASE_VERSION"
DESCRIPTION=$(jq -r '.release.body // "No description provided."' "$GITHUB_EVENT_PATH")
# Construct JSON payload
PAYLOAD=$(jq -n \
--arg title "$TITLE" \
--arg description "$DESCRIPTION" \
'{
"content": "<@&1337224918710095882>",
"allowed_mentions": { "parse": ["roles"] },
"embeds": [
{
"title": $title,
"description": $description,
"color": 15105570,
"footer": { "text": "🔄 Download the latest version of community_bridge to ensure compatibility." },
"image": {
"url": "https://avatars.githubusercontent.com/u/192999457?s=400&u=da632e8f64c85def390cfd1a73c3b664d6882b38&v=4"
}
}
],
"components": [
{
"type": 1,
"components": [
{
"type": 2,
"style": 5,
"label": "Get The Latest Release From Portal",
"url": "https://portal.cfx.re/assets/granted-assets"
},
{
"type": 2,
"style": 5,
"label": "Get The Latest Community Bridge Release",
"url": "https://github.com/The-Order-Of-The-Sacred-Framework/community_bridge/tree/main"
}
]
}
]
}')
# Debugging: Print the payload for verification
echo "$PAYLOAD"
# Send webhook
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
--data-raw "$PAYLOAD" || exit 1

View file

@ -0,0 +1,3 @@
{
"githubPullRequests.ignoredPullRequestBranches": ["main"]
}

View file

@ -0,0 +1,138 @@
# Attributions
> **Community Bridge** This project incorporates code from several outstanding libraries and resources, all licensed under GPLv3. We're grateful to the talented developers who made their work available to the community.
---
## Libraries & Resources
### r\_bridge
**Purpose:** Code for codem-inventory bridging, and initial targeting
**Repository:** [r\_bridge](https://github.com/rumaier/r_bridge)
<details>
<summary>Implementation Details</summary>
**Code we use:**
* Inventory bridging code for codem-inventory integration
* Targeting system foundation
**Modifications made:**
* Adapted inventory bridge for codem compatibility in alternative structure
</details>
---
### dirk\_lib
**Purpose:** Vehicle fuel and vehicle key bridging systems
**Repository:** [dirk\_lib](https://github.com/DirkDigglerz/dirk_lib)
<details>
<summary>Implementation Details</summary>
**Code we use:**
* Client-side vehicle fuel management
* Vehicle key bridging functionality
**Modifications made:**
* None - code used as-is with full credit to original author
</details>
---
### ox\_lib
**Purpose:** External compatibility and architectural inspiration
**Repository:** [ox\_lib](https://github.com/overextended/ox_lib)
<details>
<summary>Implementation Details</summary>
**Compatibility features:**
* External resource integration for raycasting utilities
* Shared conventions
**Implementation approach:**
* No direct code usage - maintains compatibility as external resource
* Follows ox\_lib conventions for seamless integration
* Provides bridge compatibility for servers using ox\_lib ecosystem
</details>
---
### renewed\_lib
**Purpose:** Object placer functionality
**Repository:** [renewed\_lib](https://github.com/Renewed-Scripts/Renewed-Lib)
<details>
<summary>Implementation Details</summary>
**Code we use:**
* Object placement system
**Modifications made:**
* Updated variable naming for consistency
* Added missing parameters for native functions
* Updated deprecated ox\_lib raycast camera export
* Enhanced showtext UI to work with multiple systems
* Moved placement text to locales for internationalization
* Replaced ox\_lib model request exports with our bridge functions
</details>
---
### duff
**Purpose:** Version checker system and update notifications
**Repository:** [duff](https://github.com/DonHulieo/duff/blob/d89ed3b0051194babf5711114a0c437d4e41f433/server/init.lua#L10C1-L28C4)
<details>
<summary>Implementation Details</summary>
**Code we use:**
* Version checker formatting patterns
* Repository information handling
**Modifications made:**
* Removed unnecessary variables
* Enhanced to pull repository information from passed strings
* Adapted print formatting for our use case
</details>
---
## Special Thanks
A heartfelt thank you to all the creators and maintainers of these libraries, bridges, and resources. Your dedication to open-source development and the FiveM community makes projects like Community Bridge possible.
Your willingness to share knowledge and code under GPLv3 licensing enables the entire community to build better, more compatible systems together.
---
## License Compliance
All incorporated code maintains its original GPLv3 licensing. Community Bridge inherits this license to ensure continued open-source availability and community collaboration.
For detailed license information, see the [LICENSE](LICENSE) file in the project root.
---
> *"If I have seen further it is by standing on the shoulders of Giants."* - Isaac Newton

View file

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View file

@ -0,0 +1,107 @@
# Community Bridge
Community Bridge is a modular and extensible compatibility layer for FiveM, designed to unify development across major roleplay frameworks. It provides a consistent API that simplifies integration between popular frameworks such as QBCore, ESX, QBox, and custom solutions.
By bridging core game systems including inventory, dispatch, targeting, door locks, vehicle keys, clothing, fuel, and more, Community Bridge reduces duplicated effort and streamlines script compatibility across servers.
---
![](https://img.shields.io/github/contributors/TheOrderFivem/community_bridge?logo=github)
![](https://img.shields.io/github/v/release/TheOrderFivem/community_bridge?logo=github)
## Features
### Framework Compatibility
* Supports QBCore, ESX, QBox, and custom roleplay frameworks
* Provides a unified API to standardize resource interaction
### Inventory Systems
* Compatible with ox\_inventory, qb-inventory, ps-inventory, codem-inventory, core\_inventory, and others
### Dispatch and MDT
* Integrates with ps-dispatch, cd\_dispatch, lb-tablet, bub\_mdt, and other dispatch systems
* Includes fallback mechanisms to ensure notifications are delivered
### Targeting Systems
* Works with qb-target, ox\_target, sleepless-interact, and similar targeting resources
### Doorlock and Security
* Supports ox\_doorlock, qb-doorlock, rcore\_doorlock, jacksams-doorlock, and other door lock systems
### Vehicle Keys and Locking
* Compatible with all major vehicle key management systems
### Fuel Systems
* Supports all major fuel resources such as legacyfuel, ps-fuel, and more
### Clothing and Appearance
* Integrates with illenium-appearance, fivem-appearance, qb-clothing, esx\_skin, and default fallback clothing systems
### Additional Features
* Progress bars, notifications, weather synchronization, and skill system integration
* Developer tools including 3D interaction points, cutscene management, particle effects, scaleform UI, DUI system, and advanced object placement
---
## Documentation
Complete developer documentation is available at:
[Community Bridge Documentation](https://mrnewbs-scrips.gitbook.io/the-order-of-the-sacred-framework)
---
## Community and Support
Join the Community Bridge Discord server for support, discussion, and contributions:
[Community Discord](https://discord.gg/MukwBuJjP7)
---
## About Community Bridge
community_bridge is developed by The Order of the Sacred Framework, a collaborative team focused on improving interoperability and reducing development friction in the FiveM ecosystem. The project is open source and licensed under GPLv3.
We also have a vscode extension located at https://marketplace.visualstudio.com/items?itemName=TheOrderOfTheSacredFramework.fivem-community-bridge-lua
---
## Why Choose Community Bridge?
* Universal framework compatibility reduces code duplication
* Modular design allows use of only needed components
* Extensive developer utilities for advanced scripting and UI
* Tested in production on hundreds of FiveM servers
* Open source with active community support and regular updates
---
## Frequently Asked Questions
**Q: Will Community Bridge support new frameworks in the future?**
A: Yes, the project actively tracks emerging frameworks and integrates support accordingly.
**Q: Can Community Bridge work with my custom framework?**
A: Community Bridge is designed to be extensible and supports integration with custom frameworks.
**Q: How often is the project updated?**
A: Updates are regularly released to improve compatibility, add features, and fix issues based on community feedback.
**Q: Why is the seo so bad?**
A: Straight up, I have no clue and have tried everything to improve it. If you have tips PLEASE let me know in the discord or pr some changes to the repos dev branch.
---
## Keywords (for SEO)
FiveM framework compatibility, FiveM bridge system, QBCore ESX bridge, FiveM universal inventory, dispatch integration, targeting system, door lock system, vehicle key management, fuel system bridge, clothing system FiveM, FiveM developer tools, roleplay framework integration, Lua scripting FiveM, open source FiveM bridge, cross-framework compatibility, modular FiveM resource
---

View file

@ -0,0 +1,87 @@
fx_version 'cerulean'
game 'gta5'
lua54 'yes'
use_experimental_fxv2_oal 'yes'
author 'The Order of the Sacred Framework'
name 'community_bridge'
description 'A Universal Bridge for Our Community, created by a group of contributors with a shared vision to enhance both user and developer experiences. This bridge connects various frameworks, inventories, target systems, notification systems, and more, fostering compatibility and seamless integration.'
version '0.11.1'
shared_scripts {
'@ox_lib/init.lua',
'settings/sharedConfig.lua',
'lib/init.lua',
'modules/math/*.lua',
'modules/locales/*.lua',
'modules/clothing/**/shared.lua',
}
server_scripts {
'@oxmysql/lib/MySQL.lua',
'settings/serverConfig.lua',
'modules/locales/shared.lua',
'modules/version/server/*.lua',
'modules/framework/**/server.lua',
'modules/inventory/**/server.lua',
'modules/doorlock/**/server.lua',
'modules/phone/**/server.lua',
'modules/managment/**/server.lua',
'modules/dispatch/**/server.lua',
'modules/clothing/**/server.lua',
'modules/shops/**/server.lua',
'modules/helptext/**/server.lua',
'modules/notify/**/server.lua',
'modules/housing/**/server.lua',
'modules/skills/**/server.lua',
'modules/bossmenu/**/server.lua',
"lib/**/server.lua",
'init.lua',
}
client_scripts {
'settings/clientConfig.lua',
'modules/locales/shared.lua',
'modules/framework/**/client.lua',
'modules/inventory/**/client.lua',
'modules/doorlock/**/client.lua',
'modules/phone/**/client.lua',
'modules/weather/**/client.lua',
'modules/vehicleKey/**/client.lua',
'modules/fuel/**/client.lua',
'modules/target/**/client.lua',
'modules/dispatch/**/client.lua',
'modules/progressbar/**/client.lua',
'modules/clothing/**/client.lua',
'modules/input/**/client.lua',
'modules/menu/**/client.lua',
'modules/helptext/**/client.lua',
'modules/notify/**/client.lua',
'modules/dialogue/**/client/*.lua',
'modules/shops/**/client.lua',
'modules/housing/**/client.lua',
'modules/skills/**/client.lua',
'modules/bossmenu/**/client.lua',
'init.lua',
-- 'unit_tests/*.lua',
}
ui_page 'web/dist/index.html'
files {
'web/dist/index.html',
'web/dist/assets/*.css',
'web/dist/assets/*.js',
'locales/*.json',
'lib/**/client/*.lua',
'lib/**/shared/*.lua',
'lib/**/server/*.lua',
'modules/**',
'settings/*.lua',
'lib/init.lua',
}
dependencies {
'/server:6116',
'/onesync',
'ox_lib',
}

View file

@ -0,0 +1,108 @@
Bridge = {}
function Bridge.RegisterModule(moduleName, moduleTable)
if not moduleTable then
if BridgeSharedConfig.DebugLevel ~= 0 then
print("^6 No moduleTable provided for module: ", moduleName, "^0")
end
return
end
local wrappedModule = Bridge[moduleName] or {}
if type(moduleTable) == 'function' then
Bridge[moduleName] = moduleTable
exports(moduleName, moduleTable)
return
end
for functionName, func in pairs(moduleTable) do
wrappedModule[functionName] = func
end
if BridgeSharedConfig.DebugLevel ~= 0 then
print("^2 Registering module:", moduleName, "^0")
end
Bridge[moduleName] = wrappedModule
_ENV[moduleName] = wrappedModule -- Add to the ENV table the modules so is more easy and safe to call from inside
_ENV.Bridge = Bridge -- Add the bridge too to the _ENV
exports(moduleName, function()
return wrappedModule
end)
--trigger update object event
TriggerEvent("Bridge:Refresh", moduleName, wrappedModule)
end
exports("RegisterModule", Bridge.RegisterModule)
--TODO: Create a way to overide functions or create a new functions for module
function Bridge.RegisterModuleFunction(moduleName, functionName, func)
assert(moduleName and functionName and func, string.format("Bridge.RegisterModuleFunction(%s, %s, %s) - Invalid arguments", moduleName, functionName, func))
Bridge[moduleName] = Bridge[moduleName] or {}
Bridge[moduleName][functionName] = func
--trigger update object event
TriggerEvent("Bridge:Refresh", moduleName, Bridge[moduleName])
end
--Bridge
Bridge.RegisterModule("Framework", Framework)
Bridge.RegisterModule("BossMenu", BossMenu)
Bridge.RegisterModule("Inventory", Inventory)
Bridge.RegisterModule("Notify", Notify)
Bridge.RegisterModule("HelpText", HelpText)
Bridge.RegisterModule("Clothing", Clothing)
Bridge.RegisterModule("Language", Language)
Bridge.RegisterModule("Doorlock", Doorlock)
Bridge.RegisterModule("Phone", Phone)
Bridge.RegisterModule("Dispatch", Dispatch)
Bridge.RegisterModule("Shops", Shops)
Bridge.RegisterModule("Housing", Housing)
Bridge.RegisterModule("Version", Version)
--lib
-- Bridge.RegisterModule("Tables", cLib.Tables)
-- Bridge.RegisterModule("Math", cLib.Math)
-- Bridge.RegisterModule("Prints", cLib.Prints)
-- Bridge.RegisterModule("Callback", cLib.Callback)
-- --new
Bridge.RegisterModule("Require", Require)
-- Bridge.RegisterModule("Ids", cLib.Ids)
-- Bridge.RegisterModule("ReboundEntities", cLib.ReboundEntities)
-- Bridge.RegisterModule("LA", cLib.LA)
-- Bridge.RegisterModule("Perlin", cLib.Perlin)
-- Bridge.RegisterModule("Actions", cLib.Actions)
-- Bridge.RegisterModule("Cache", cLib.Cache)
Bridge.RegisterModule("Skills", Skills)
for k, v in pairs(cLib) do
if v then
Bridge.RegisterModule(k, v)
end
end
exports('Bridge', function()
return Bridge
end)
-- ▄▀▀ ██▀ █▀▄ █ █ ██▀ █▀▄
-- ▄█▀ █▄▄ █▀▄ ▀▄▀ █▄▄ █▀▄
if not IsDuplicityVersion() then goto client end
Bridge.RegisterModule('Version', Version)
-- ▄▀▀ █ █ ██▀ █▄ █ ▀█▀
-- ▀▄▄ █▄▄ █ █▄▄ █ ▀█ █
if IsDuplicityVersion() then return end
::client::
Bridge.RegisterModule("Fuel", Fuel)
Bridge.RegisterModule("Input", Input)
Bridge.RegisterModule("ProgressBar", ProgressBar)
Bridge.RegisterModule("VehicleKey", VehicleKey)
Bridge.RegisterModule("Weather", Weather)
Bridge.RegisterModule("Target", Target)
Bridge.RegisterModule("Menu", Menu)
Bridge.RegisterModule("Dialogue", Dialogue)

View file

@ -0,0 +1,155 @@
Anim = Anim or {}
Ids = Ids or Require("lib/utility/shared/ids.lua")
Anim.Active = Anim.Active or {}
Anim.isUpdateLoopRunning = Anim.isUpdateLoopRunning or false
--- This will request the animation dictionary.
--- @param animDict string
--- @return boolean
function Anim.RequestDict(animDict)
if not animDict or type(animDict) ~= "string" then
return false
end
if HasAnimDictLoaded(animDict) then
return true
end
RequestAnimDict(animDict)
local timeout = GetGameTimer() + 2000
while not HasAnimDictLoaded(animDict) and GetGameTimer() < timeout do
Wait(50)
end
return HasAnimDictLoaded(animDict)
end
--- This will start the animation update loop.
function Anim.Start()
if Anim.isUpdateLoopRunning then return end
Anim.isUpdateLoopRunning = true
CreateThread(function()
while Anim.isUpdateLoopRunning do
local idsToProcess = {}
for idKey, _ in pairs(Anim.Active) do
table.insert(idsToProcess, idKey)
end
if #idsToProcess == 0 then
Wait(750)
else
for _, id in ipairs(idsToProcess) do
local animData = Anim.Active[id]
if animData then
local entity = animData.entity
local onComplete = animData.onComplete
if not DoesEntityExist(entity) then
if onComplete then onComplete(false, "despawned") end
Anim.Active[id] = nil
elseif animData.status == "pending_task" then
TaskPlayAnim(entity, animData.animDict, animData.animName, animData.blendIn, animData.blendOut, animData.duration, animData.flag, animData.playbackRate, false, false, false)
animData.startTime = GetGameTimer()
animData.animEndTime = animData.duration > 0 and (animData.startTime + animData.duration) or -1
animData.status = "playing"
elseif animData.status == "playing" then
local animationCompletedNaturally = false
if animData.duration == -1 then
if not IsEntityPlayingAnim(entity, animData.animDict, animData.animName, 3) and GetEntityAnimCurrentTime(entity, animData.animDict, animData.animName) > 0.8 then
animationCompletedNaturally = true
end
elseif animData.animEndTime ~= -1 and GetGameTimer() >= animData.animEndTime then
animationCompletedNaturally = true
end
if animationCompletedNaturally then
if onComplete then onComplete(true, "completed") end
Anim.Active[id] = nil
end
end
end
end
Wait(100)
end
end
end)
end
--- This will play an animation on the specified ped.
--- @param id string | nil
--- @param entity number
--- @param animDict string
--- @param animName string
--- @param blendIn number | nil
--- @param blendOut number | nil
--- @param duration number | nil
--- @param flag number | nil
--- @param playbackRate number | nil
--- @param onComplete function | nil
--- @return string | nil
function Anim.Play(id, entity, animDict, animName, blendIn, blendOut, duration, flag, playbackRate, onComplete)
local newId = id or Ids.CreateUniqueId(Anim.Active)
if Anim.Active[newId] then
if onComplete then
onComplete(false, "id_in_use")
end
return newId
end
if not entity or not DoesEntityExist(entity) or not IsEntityAPed(entity) then
if onComplete then
onComplete(false, "invalid_entity")
end
return nil
end
if not Anim.RequestDict(animDict) then
if onComplete then
onComplete(false, "dict_load_failed")
end
return nil
end
Anim.Active[newId] = {
entity = entity,
animDict = animDict,
animName = animName,
blendIn = blendIn or 8.0,
blendOut = blendOut or -8.0,
duration = duration or -1,
flag = flag or 1,
playbackRate = playbackRate or 0.0,
onComplete = onComplete,
status = "pending_task",
startTime = 0,
animEndTime = 0
}
Anim.Start()
return newId
end
function Anim.Stop(id)
if not id or not Anim.Active or not Anim.Active[id] then
return false
end
local animData = Anim.Active[id]
if animData.entity and DoesEntityExist(animData.entity) and IsEntityAPed(animData.entity) then
if animData.status == "playing" or animData.status == "pending_task" then
StopAnimTask(animData.entity, animData.animDict, animData.animName, 1.0)
end
end
if animData.onComplete then
animData.onComplete(false, "stopped_by_id")
end
Anim.Active[id] = nil
local anyLeft = Anim.Active and next(Anim.Active) ~= nil
if not anyLeft then
Anim.isUpdateLoopRunning = false
end
return true
end
return Anim

View file

@ -0,0 +1,65 @@
Batch = Batch or {}
Batch.Event = Batch.Event or {}
Batch.Event.Queued = Batch.Event.Queued or {}
Batch.Event.IsQueued = Batch.Event.IsQueued or false
local SERVER = IsDuplicityVersion()
-- could do a callback from client to server back to client with a time stamp. Use that timestamp to generate some random string/number and use that fo
-- this event name. Would help by masking from exploits. Thinking about making a module out of it
--- This is used to batch single events together to reduce network strain
---
if SERVER then
function Batch.Event.Queue(src, event, ...)
if src == -1 then
src = GetPlayers()
for k, v in pairs(src) do
local strSrc = tostring(v)
Batch.Event.Queued[strSrc] = Batch.Event.Queued[strSrc] or {}
table.insert(Batch.Event.Queued[strSrc], {
src = v,
event = event,
args = {...}
})
end
else
local strSrc = tostring(src)
Batch.Event.Queued[strSrc] = Batch.Event.Queued[strSrc] or {}
table.insert(Batch.Event.Queued[strSrc], {
src = src,
event = event,
args = {...}
})
end
if Batch.Event.IsQueued then return end
Batch.Event.IsQueued = true
SetTimeout(100, function()
for k, v in pairs(Batch.Event.Queued) do
TriggerClientEvent('community_bridge:client:BatchEvents', v.src, v)
end
Batch.Event.IsQueued = false
Batch.Event.Queued = {}
end)
end
return Batch
else
Batch.Event.Fire = function(array)
local playerSrc = PlayerId()
for k, v in pairs(array) do
if v.src == playerSrc then
local event = v.event
local args = v.args
TriggerEvent(event, table.unpack(args))
end
end
end
RegisterNetEvent('community_bridge:client:BatchEvents', function(array)
Batch.Event.Fire(array)
end)
return Batch
end

View file

@ -0,0 +1,23 @@
-- Test file for FiveM Community Bridge Extension
-- Try the following to test extension features:
-- 1. Type "CommunityBridge." (with the dot) and you should see auto-completion
-- CommunityBridge.
-- 2. Type "AddEventHandler" and see parameter hints
-- AddEventHandler(
-- 3. Hover over these function names to see documentation:
-- GetPlayerData, SetPlayerData, TriggerEvent, RegisterNetEvent
-- 4. Try these snippets (type the word and press Tab):
-- event
-- thread
-- command
-- cbget
-- cbset
-- 5. Test signature help - start typing a function call:
-- CommunityBridge.GetPlayerData(
print("Extension test file loaded")

View file

@ -0,0 +1,63 @@
Cache = Require("lib/cache/shared/cache.lua")
local PlayerPedId = PlayerPedId
local GetVehiclePedIsIn = GetVehiclePedIsIn
local GetVehicleMaxNumberOfPassengers = GetVehicleMaxNumberOfPassengers
local GetPedInVehicleSeat = GetPedInVehicleSeat
local GetPlayerServerId = GetPlayerServerId
local GetSelectedPedWeapon = GetSelectedPedWeapon
local PlayerId = PlayerId
-- Comparison functions for client-side state
--- Get the player's ped ID
---@return integer
function GetPed()
return PlayerPedId()
end
--- Get the vehicle the player is in, if any
---@return integer | nil
function GetVehicle()
local ped = Cache.Get("Ped") -- Use cached ped for efficiency
if not ped then return end -- Or handle error/default case
return GetVehiclePedIsIn(ped, false)
end
--- Get the seat of the player in the vehicle if the vehicle is occupied
---@return integer | nil
function GetVehicleSeat()
local vehicle = Cache.Get("Vehicle")
if not vehicle then return end
local ped = Cache.Get("Ped")
if not ped then return end
for i = -1, GetVehicleMaxNumberOfPassengers(vehicle) - 1 do
if ped == GetPedInVehicleSeat(vehicle, i) then
return i
end
end
end
---Get The Server ID of the player
---@return integer | nil
function GetServerId()
local ped <const> = PlayerId()
if not ped then return end -- Return unarmed hash if no ped
return GetPlayerServerId(ped)
end
--- Get the current weapon of the player
--- @return integer | nil
function GetWeapon()
local ped = Cache.Get("Ped")
if not ped then return end -- Return unarmed hash if no ped
return GetSelectedPedWeapon(ped)
end
Cache.Create("Ped", GetPed, 1000) -- Check ped every second (adjust as needed)
Cache.Create("Vehicle", GetVehicle, 500) -- Check vehicle every 500ms
Cache.Create("Seat", GetVehicleSeat, 500) -- Check if in vehicle every 500ms
Cache.Create("Weapon", GetWeapon , 250) -- Check weapon every 250ms
Cache.Create("ServerId", GetServerId, 1000) -- Check server ID every second
return Cache

View file

@ -0,0 +1,279 @@
---@class CacheEntry
---@field Name string
---@field Compare fun(): any
---@field WaitTime integer | nil
---@field LastChecked integer|nil
---@field OnChange fun(new:any, old:any)[]
---@field Value any
---@class CacheModule
---@field Caches table<string, CacheEntry>
---@field LoopRunning boolean
---@field Create fun(name:string, compare:fun():any, waitTime:integer | nil): CacheEntry
---@field Get fun(name:string): any
---@field OnChange fun(name:string, onChange:fun(new:any, old:any))
---@field Remove fun(name:string)
Cache = Cache or {} ---@type CacheModule -- <-- we use Cache as a global variable so that if we dont required it more than once
Table = Table or Require('lib/utility/shared/tables.lua')
Id = Id or Require('lib/utility/shared/ids.lua')
local Config = Require("settings/sharedConfig.lua")
local max = 5000
local CreateThread = CreateThread
local Wait = Wait
local GetGameTimer = GetGameTimer
local resourceCallbacks = {} -- Add callbacks from resources
local resourceTracker = {
caches = {},
callbacks = {},
initialized = {}
}
Cache.Caches = Cache.Caches or {}
Cache.LoopRunning = Cache.LoopRunning or false
---@param ... any
local function debugPrint(...)
if Config.DebugLevel == 0 then return end
print("^2[Cache]^0", ...)
end
local function HasActiveCaches()
for _, cache in pairs(Cache.Caches) do
if cache.WaitTime ~= nil then
return true
end
end
return false
end
local function processCacheEntry(now, cache)
cache.LastChecked = cache.LastChecked or now
cache.WaitTime = tonumber(cache.WaitTime) or max
local elapsed = now - cache.LastChecked
local remaining = cache.WaitTime - elapsed
if remaining <= 0 then
local oldValue = cache.Value
cache.Value = cache.Compare()
if not Table.Compare(cache.Value, oldValue) and cache.OnChange then
for i, onChange in pairs(cache.OnChange) do
onChange(cache.Value, oldValue)
end
end
cache.LastChecked = now
remaining = cache.WaitTime
end
return remaining
end
local function getNextWait(now)
local minWait = nil
for name, cache in pairs(Cache.Caches) do
if cache.Compare and cache.WaitTime ~= nil then
local remaining = processCacheEntry(now, cache)
if not minWait or remaining < minWait then
minWait = remaining
if minWait <= 0 then break end
end
end
end
return minWait
end
local function StartLoop()
if Cache.LoopRunning then return end
if not HasActiveCaches() then return end
Cache.LoopRunning = true
CreateThread(function()
while Cache.LoopRunning do
local now = GetGameTimer()
local minWait = getNextWait(now)
if minWait then
Wait(math.max(0, minWait))
else
Wait(max)
end
if not HasActiveCaches() then
Cache.LoopRunning = false
end
end
end)
end
local function trackResource(resourceName, type, data)
if not resourceName then return end
-- Initialize resource tracking if needed
if not resourceTracker.initialized[resourceName] then
resourceTracker.initialized[resourceName] = true
resourceTracker.caches[resourceName] = resourceTracker.caches[resourceName] or {}
resourceTracker.callbacks[resourceName] = resourceTracker.callbacks[resourceName] or {}
AddEventHandler('onResourceStop', function(stoppingResource)
if stoppingResource ~= resourceName then return end
-- Clean up caches
for _, cacheName in ipairs(resourceTracker.caches[resourceName] or {}) do
if Cache.Caches[cacheName] then
Cache.Caches[cacheName] = nil
debugPrint(("Removed cache '%s' - resource '%s' stopped"):format(cacheName, resourceName))
end
end
-- Clean up callbacks
for _, cb in ipairs(resourceTracker.callbacks[resourceName] or {}) do
local targetCache = Cache.Caches[cb.cacheName]
if targetCache then
table.remove(targetCache.OnChange, cb.index)
debugPrint(("Removed OnChange callback from cache '%s' - resource '%s' stopped"):format(
cb.cacheName,
resourceName
))
end
end
-- Clear tracking data
resourceTracker.caches[resourceName] = nil
resourceTracker.callbacks[resourceName] = nil
resourceTracker.initialized[resourceName] = nil
end)
end
-- Track the new item
if type == "cache" then
table.insert(resourceTracker.caches[resourceName], data)
elseif type == "callback" then
table.insert(resourceTracker.callbacks[resourceName], data)
end
end
---@param name string
---@param compare fun():any
---@param waitTime integer | nil
---@return CacheEntry | nil
function Cache.Create(name, compare, waitTime)
assert(name, "Cache name is required.")
assert(compare, "Comparison function is required.")
if type(waitTime) ~= "number" then
waitTime = nil
end
local _name = tostring(name)
local cache = Cache.Caches[_name]
if cache and cache.Compare == compare then
debugPrint(_name .. " already exists with the same comparison function.")
return cache
end
local ok, result = pcall(compare)
if not ok then
debugPrint("Error creating cache '" .. _name .. "': " .. tostring(result))
return nil
end
---@type CacheEntry
local newCache = {
Name = _name,
Compare = compare,
WaitTime = waitTime, -- can be nil
LastChecked = nil,
OnChange = {},
Value = result
}
-- Track the cache with its resource
trackResource(GetInvokingResource(), "cache", _name)
Cache.Caches[_name] = newCache
debugPrint(_name .. " created with initial value: " .. tostring(result))
for _, onChange in pairs(newCache.OnChange) do
onChange(newCache.Value, 0)
end
StartLoop()
return newCache
end
---@param name string
---@return any
function Cache.Get(name)
assert(name, "Cache name is required.")
local _name = tostring(name)
local cache = Cache.Caches[_name]
return cache and cache.Value or nil
end
--- This add a callback to the cache entry that will be called when the value changes.
--- The callback will be called with the new value and the old value.
--- you can call the value again to delete the callback.
---@param name string
---@param onChange fun(new:any, old:any)
function Cache.OnChange(name, onChange)
assert(name, "Cache name is required.")
local _name = tostring(name)
local cache = Cache.Caches[_name]
assert(cache, "Cache with name '" .. _name .. "' does not exist.")
local id = Id.CreateUniqueId(Cache.Caches[_name]?.OnChange)
-- Figure out which resource is trying to register this callback
local invokingResource = GetInvokingResource()
if not invokingResource then return end
-- Add the new callback to our list
local callbackIndex = id
cache.OnChange[callbackIndex] = onChange
-- Track the callback with its resource
trackResource(GetInvokingResource(), "callback", {
cacheName = _name,
index = callbackIndex
})
debugPrint(("Added new OnChange callback to cache '%s' from resource '%s'"):format(_name, invokingResource))
return callbackIndex
end
function Cache.RemoveOnChange(name, id)
assert(name, "Cache name is required.")
local _name = tostring(name)
local cache = Cache.Caches[_name]
assert(cache, "Cache with name '" .. _name .. "' does not exist.")
if cache.OnChange[id] then
cache.OnChange[id] = nil
debugPrint("Removed OnChange callback from cache '" .. _name .. "' with ID: " .. id)
else
debugPrint("No OnChange callback found with ID: " .. id .. " in cache '" .. _name .. "'")
end
end
---@param name string
function Cache.Remove(name)
assert(name, "Cache name is required.")
local _name = tostring(name)
local cache = Cache.Caches[_name]
if cache then
Cache.Caches[_name] = nil
debugPrint(_name .. " removed from cache.")
if next(Cache.Caches) == nil then
Cache.LoopRunning = false
end
end
end
---@param name string
---@param newValue any
function Cache.Update(name, newValue)
assert(name, "Cache name is required.")
local _name = tostring(name)
local cache = Cache.Caches[_name]
assert(cache, "Cache with name '" .. _name .. "' does not exist.")
local oldValue = cache.Value
if oldValue ~= newValue then
cache.Value = newValue
for _, onChange in pairs(cache.OnChange) do
onChange(newValue, oldValue)
end
end
end
return Cache

View file

@ -0,0 +1,394 @@
-- Cutscene Manager for FiveM
-- Handles cutscene loading, playback, and character management
---@class Cutscene
Cutscenes = Cutscenes or {}
Cutscene = Cutscene or {}
Cutscene.done = true
-- Constants
local LOAD_TIMEOUT <const> = 10000
local FADE_DURATION <const> = 1000
local CUTSCENE_WAIT <const> = 1000
-- Character model definitions
local characterTags <const> = {
{ male = 'MP_1' },
{ male = 'MP_2' },
{ male = 'MP_3' },
{ male = 'MP_4' },
{ male = 'MP_Male_Character', female = 'MP_Female_Character' },
{ male = 'MP_Male_Character_1', female = 'MP_Female_Character_1' },
{ male = 'MP_Male_Character_2', female = 'MP_Female_Character_2' },
{ male = 'MP_Male_Character_3', female = 'MP_Female_Character_3' },
{ male = 'MP_Male_Character_4', female = 'MP_Female_Character_4' },
{ male = 'MP_Plane_Passenger_1' },
{ male = 'MP_Plane_Passenger_2' },
{ male = 'MP_Plane_Passenger_3' },
{ male = 'MP_Plane_Passenger_4' },
{ male = 'MP_Plane_Passenger_5' },
{ male = 'MP_Plane_Passenger_6' },
{ male = 'MP_Plane_Passenger_7' },
{ male = 'MP_Plane_Passenger_8' },
{ male = 'MP_Plane_Passenger_9' },
}
-- Component definitions for character customization
local componentsToSave <const> = {
-- Components
{ name = "head", id = 0, type = "drawable" },
{ name = "beard", id = 1, type = "drawable" },
{ name = "hair", id = 2, type = "drawable" },
{ name = "arms", id = 3, type = "drawable" },
{ name = "pants", id = 4, type = "drawable" },
{ name = "parachute", id = 5, type = "drawable" },
{ name = "feet", id = 6, type = "drawable" },
{ name = "accessories", id = 7, type = "drawable" },
{ name = "undershirt", id = 8, type = "drawable" },
{ name = "vest", id = 9, type = "drawable" },
{ name = "decals", id = 10, type = "drawable" },
{ name = "jacket", id = 11, type = "drawable" },
-- Props
{ name = "hat", id = 0, type = "prop" },
{ name = "glasses", id = 1, type = "prop" },
{ name = "ears", id = 2, type = "prop" },
{ name = "watch", id = 3, type = "prop" },
{ name = "bracelet", id = 4, type = "prop" },
{ name = "misc", id = 5, type = "prop" },
{ name = "left_wrist", id = 6, type = "prop" },
{ name = "right_wrist", id = 7, type = "prop" },
{ name = "prop8", id = 8, type = "prop" },
{ name = "prop9", id = 9, type = "prop" },
}
-- Utility Functions
function table.shallow_copy(t)
local t2 = {}
for k, v in pairs(t) do
t2[k] = v
end
return t2
end
local function WaitForModelLoad(modelHash)
local timeout = GetGameTimer() + LOAD_TIMEOUT
while not HasModelLoaded(modelHash) and GetGameTimer() < timeout do
Wait(0)
end
return HasModelLoaded(modelHash)
end
local function CreatePedFromModel(modelName, coords)
local model = GetHashKey(modelName)
RequestModel(model)
if not WaitForModelLoad(model) then
print("Failed to load model: " .. modelName)
return nil
end
local ped = CreatePed(0, model, coords.x, coords.y, coords.z, 0.0, true, false)
if not DoesEntityExist(ped) then
print("Failed to create ped from model: " .. modelName)
return nil
end
return ped
end
-- Cutscene Core Functions
local function LoadCutscene(cutscene)
assert(cutscene, "Cutscene.Load called without a cutscene name.")
local playbackList = IsPedMale(PlayerPedId()) and 31 or 103
RequestCutsceneWithPlaybackList(cutscene, playbackList, 8)
local timeout = GetGameTimer() + LOAD_TIMEOUT
while not HasCutsceneLoaded() and GetGameTimer() < timeout do
Wait(0)
end
if not HasCutsceneLoaded() then
print("Cutscene failed to load: ", cutscene)
return false
end
return true
end
local function GetCutsceneTags(cutscene)
if not LoadCutscene(cutscene) then return end
StartCutscene(0)
Wait(CUTSCENE_WAIT)
local tags = {}
for _, tag in pairs(characterTags) do
if DoesCutsceneEntityExist(tag.male, 0) or DoesCutsceneEntityExist(tag.female, 0) then
table.insert(tags, tag)
end
end
StopCutsceneImmediately()
Wait(CUTSCENE_WAIT * 2)
return tags
end
-- Character Outfit Management
local function SavePedOutfit(ped)
local outfitData = {}
for _, component in ipairs(componentsToSave) do
if component.type == "drawable" then
outfitData[component.name] = {
id = component.id,
type = component.type,
drawable = GetPedDrawableVariation(ped, component.id),
texture = GetPedTextureVariation(ped, component.id),
palette = GetPedPaletteVariation(ped, component.id),
}
elseif component.type == "prop" then
outfitData[component.name] = {
id = component.id,
type = component.type,
propIndex = GetPedPropIndex(ped, component.id),
propTexture = GetPedPropTextureIndex(ped, component.id),
}
end
end
return outfitData
end
local function ApplyPedOutfit(ped, outfitData)
if not outfitData or type(outfitData) ~= "table" then
print("ApplyPedOutfit: Invalid outfitData provided.")
return
end
for componentName, data in pairs(outfitData) do
if data.type == "drawable" then
SetPedComponentVariation(ped, data.id, data.drawable or 0, data.texture or 0, data.palette or 0)
elseif data.type == "prop" then
if data.propIndex == -1 or data.propIndex == nil then
ClearPedProp(ped, data.id)
else
SetPedPropIndex(ped, data.id, data.propIndex, data.propTexture or 0, true)
end
end
end
end
-- Public Interface
function Cutscene.GetTags(cutscene)
return GetCutsceneTags(cutscene)
end
function Cutscene.Load(cutscene)
return LoadCutscene(cutscene)
end
function Cutscene.SavePedOutfit(ped)
return SavePedOutfit(ped)
end
function Cutscene.ApplyPedOutfit(ped, outfitData)
return ApplyPedOutfit(ped, outfitData)
end
function Cutscene.Create(cutscene, coords, srcs)
local lastCoords = coords or GetEntityCoords(PlayerPedId())
DoScreenFadeOut(0)
local tagsFromCutscene = GetCutsceneTags(cutscene)
if not LoadCutscene(cutscene) then
print("Cutscene.Create: Failed to load cutscene", cutscene)
DoScreenFadeIn(0)
return false
end
srcs = srcs or {}
local clothes = {}
local localped = PlayerPedId()
-- Process players and create necessary peds
local playersToProcess = { { ped = localped, identifier = "localplayer", coords = lastCoords } }
for _, src_raw in ipairs(srcs) do
if type(src_raw) == 'number' then
if src_raw and not DoesEntityExist(src_raw) then
local playerPed = GetPlayerPed(GetPlayerFromServerId(src_raw))
if DoesEntityExist(playerPed) then
local ped = ClonePed(playerPed, false, false, true)
table.insert(playersToProcess, { ped = ped, identifier = "player" })
end
else
table.insert(playersToProcess, { ped = src_raw, identifier = "user" })
end
elseif type(src_raw) == 'string' then
local ped = CreatePedFromModel(src_raw, GetEntityCoords(localped))
if ped then
table.insert(playersToProcess, { ped = ped, identifier = 'script' })
end
end
end
-- Process available tags and assign to players
local availableTags = table.shallow_copy(tagsFromCutscene or {})
local usedTags = {}
local cleanupTags = {}
for _, playerData in ipairs(playersToProcess) do
local currentPed = playerData.ped
if not currentPed or not DoesEntityExist(currentPed) then goto continue end
local tagTable = table.remove(availableTags, 1)
if not tagTable then
print("Cutscene.Create: No available tags for player", playerData.identifier)
break
end
local isPedMale = IsPedMale(currentPed)
local tag = isPedMale and tagTable.male or tagTable.female
local unusedTag = not isPedMale and tagTable.male or tagTable.female -- needs to be this way as default has to be null for missing female
SetCutsceneEntityStreamingFlags(tag, 0, 1)
RegisterEntityForCutscene(currentPed, tag, 0, GetEntityModel(currentPed), 64)
SetCutscenePedComponentVariationFromPed(tag, currentPed, 0)
clothes[tag] = {
clothing = SavePedOutfit(currentPed),
ped = currentPed
}
table.insert(usedTags, tag)
if unusedTag then table.insert(cleanupTags, unusedTag) end
::continue::
end
-- Clean up unused tags
for _, tag in ipairs(cleanupTags) do
local ped = RegisterEntityForCutscene(0, tag, 3, 0, 64)
if ped then
SetEntityVisible(ped, false, false)
end
end
-- Handle remaining unused tags
for _, tag in ipairs(availableTags) do
for _, tagType in pairs({ tag.male, tag.female }) do
if tagType then
local ped = RegisterEntityForCutscene(0, tagType, 3, 0, 64)
if ped then
SetEntityVisible(ped, false, false)
end
end
end
end
return {
cutscene = cutscene,
coords = coords,
tags = usedTags,
srcs = srcs,
peds = playersToProcess,
clothes = clothes,
}
end
function Cutscene.Start(cutsceneData)
if not cutsceneData then
print("Cutscene.Start: Cutscene data is nil.")
return
end
DoScreenFadeIn(FADE_DURATION)
Cutscene.done = false
local clothes = cutsceneData.clothes
local coords = cutsceneData.coords
-- Start cutscene
if coords then
if type(coords) == 'boolean' then
coords = GetEntityCoords(PlayerPedId())
end
StartCutsceneAtCoords(coords.x, coords.y, coords.z, 0)
else
StartCutscene(0)
end
Wait(100)
-- Apply clothing to cutscene characters
for k, datam in pairs(clothes) do
local ped = datam.ped
if DoesEntityExist(ped) then
SetCutscenePedComponentVariationFromPed(k, ped, 0)
ApplyPedOutfit(ped, datam.clothing)
end
end
-- Scene loading thread
CreateThread(function()
local lastCoords
while not Cutscene.done do
local coords = GetWorldCoordFromScreenCoord(0.5, 0.5)
if not lastCoords or #(lastCoords - coords) > 100 then
NewLoadSceneStartSphere(coords.x, coords.y, coords.z, 2000, 0)
lastCoords = coords
end
Wait(500)
end
end)
-- Control blocking thread
CreateThread(function()
while not Cutscene.done do
DisableAllControlActions(0)
DisableFrontendThisFrame()
Wait(3)
end
end)
-- Main cutscene loop
while not HasCutsceneFinished() and not Cutscene.done do
Wait(0)
if IsDisabledControlJustPressed(0, 200) then
DoScreenFadeOut(FADE_DURATION)
Wait(FADE_DURATION)
StopCutsceneImmediately()
Wait(500)
Cutscene.done = true
break
end
end
-- Cleanup
DoScreenFadeIn(FADE_DURATION)
for _, playerData in ipairs(cutsceneData.peds) do
local ped = playerData.ped
if not ped or not DoesEntityExist(ped) then goto continue end
if playerData.identifier == 'script' then
DeleteEntity(ped)
elseif playerData.identifier == 'localplayer' then
SetEntityCoords(ped, playerData.coords.x, playerData.coords.y, playerData.coords.z, false, false, false, false)
end
::continue::
end
Cutscene.done = true
end
-- RegisterCommand('cutscene', function(source, args, rawCommand)
-- local cutscene = args[1]
-- if cutscene then
-- local cutsceneData = Cutscene.Create(cutscene, false, { 1,1,1 })
-- Cutscene.Start(cutsceneData)
-- end
-- end, false)
return Cutscene

View file

@ -0,0 +1,329 @@
-- -- DUI (Direct User Interface) Manager for FiveM
-- -- Basic table-based implementation with full mouse support
-- ---@class DUI
-- DUI = {}
-- -- Constants
-- local DEFAULT_WIDTH <const> = 1280
-- local DEFAULT_HEIGHT <const> = 720
-- -- Store active DUI instances
-- local activeInstances = {}
-- local instanceCounter = 0
-- -- Mouse tracking state
-- local mouseState = {
-- tracking = false,
-- activeId = nil,
-- lastX = 0,
-- lastY = 0,
-- isPressed = false,
-- currentButton = nil
-- }
-- -- Mouse button constants
-- local MOUSE_BUTTONS <const> = {
-- LEFT = "left",
-- MIDDLE = "middle",
-- RIGHT = "right"
-- }
-- ---@class DUIInstance
-- ---@field id number Unique identifier for this DUI instance
-- ---@field url string URL loaded in the DUI
-- ---@field width number Width of the DUI
-- ---@field height number Height of the DUI
-- ---@field handle number DUI browser handle
-- ---@field txd string Texture dictionary name
-- ---@field txn string Texture name
-- ---@field active boolean Whether the DUI is active
-- ---@field trackMouse boolean Whether mouse tracking is enabled for this instance
-- ---@field mouseScale table<string, number> Scale factors for mouse coordinates
-- -- Create a new DUI instance
-- ---@param url string URL to load
-- ---@param width? number Width of the DUI (default: 1280)
-- ---@param height? number Height of the DUI (default: 720)
-- ---@return number|nil id The DUI instance ID if successful, nil if failed
-- function DUI.Create(url, width, height)
-- if not url or type(url) ~= "string" then
-- return print("DUI.Create: URL is required and must be a string")
-- end
-- width = width or DEFAULT_WIDTH
-- height = height or DEFAULT_HEIGHT
-- instanceCounter = instanceCounter + 1
-- local id = instanceCounter
-- local handle = CreateDui(url, width, height)
-- if not handle then
-- return print("DUI.Create: Failed to create DUI instance")
-- end
-- local instance = {
-- id = id,
-- url = url,
-- width = width,
-- height = height,
-- handle = handle,
-- txd = "dui_" .. id,
-- txn = "texture_" .. id,
-- active = true,
-- trackMouse = false,
-- mouseScale = {
-- x = 1.0,
-- y = 1.0
-- }
-- }
-- -- Create runtime texture
-- local duiHandle = GetDuiHandle(handle)
-- CreateRuntimeTextureFromDuiHandle(CreateRuntimeTxd(instance.txd), instance.txn, duiHandle)
-- activeInstances[id] = instance
-- return id
-- end
-- -- Destroy a DUI instance
-- ---@param id number DUI instance ID
-- ---@return boolean success Whether the operation was successful
-- function DUI.Destroy(id)
-- local instance = activeInstances[id]
-- if not instance then return false end
-- if instance.handle then
-- DestroyDui(instance.handle)
-- end
-- activeInstances[id] = nil
-- return true
-- end
-- -- Set URL for a DUI instance
-- ---@param id number DUI instance ID
-- ---@param url string New URL to load
-- ---@return boolean success Whether the operation was successful
-- function DUI.SetURL(id, url)
-- local instance = activeInstances[id]
-- if not instance then return false end
-- SetDuiUrl(instance.handle, url)
-- instance.url = url
-- return true
-- end
-- -- Send a message to the DUI
-- ---@param id number DUI instance ID
-- ---@param message table Message to send (will be JSON encoded)
-- ---@return boolean success Whether the operation was successful
-- function DUI.SendMessage(id, message)
-- local instance = activeInstances[id]
-- if not instance then return false end
-- SendDuiMessage(instance.handle, json.encode(message))
-- return true
-- end
-- -- Mouse interaction functions
-- -- Move mouse cursor on DUI
-- ---@param id number DUI instance ID
-- ---@param x number X coordinate
-- ---@param y number Y coordinate
-- ---@return boolean success Whether the operation was successful
-- function DUI.MoveMouse(id, x, y)
-- local instance = activeInstances[id]
-- if not instance then return false end
-- SendDuiMouseMove(instance.handle, x, y)
-- return true
-- end
-- -- Simulate mouse button press
-- ---@param id number DUI instance ID
-- ---@param button "left"|"middle"|"right" Mouse button to simulate
-- ---@return boolean success Whether the operation was successful
-- function DUI.MouseDown(id, button)
-- local instance = activeInstances[id]
-- if not instance then return false end
-- if not MOUSE_BUTTONS[button:upper()] then
-- return print("DUI.MouseDown: Invalid button. Must be 'left', 'middle', or 'right'")
-- end
-- -- Update mouse state
-- if instance.trackMouse then
-- mouseState.isPressed = true
-- mouseState.currentButton = button
-- end
-- SendDuiMouseDown(instance.handle, button)
-- return true
-- end
-- -- Simulate mouse button release
-- ---@param id number DUI instance ID
-- ---@param button "left"|"middle"|"right" Mouse button to simulate
-- ---@return boolean success Whether the operation was successful
-- function DUI.MouseUp(id, button)
-- local instance = activeInstances[id]
-- if not instance then return false end
-- if not MOUSE_BUTTONS[button:upper()] then
-- return print("DUI.MouseUp: Invalid button. Must be 'left', 'middle', or 'right'")
-- end
-- -- Update mouse state
-- if instance.trackMouse then
-- mouseState.isPressed = false
-- mouseState.currentButton = nil
-- end
-- SendDuiMouseUp(instance.handle, button)
-- return true
-- end
-- -- Simulate mouse wheel movement
-- ---@param id number DUI instance ID
-- ---@param deltaY number Vertical scroll amount
-- ---@param deltaX number Horizontal scroll amount
-- ---@return boolean success Whether the operation was successful
-- function DUI.MouseWheel(id, deltaY, deltaX)
-- local instance = activeInstances[id]
-- if not instance then return false end
-- SendDuiMouseWheel(instance.handle, deltaY, deltaX)
-- return true
-- end
-- -- Click helper function (combines MoveMouse, MouseDown, and MouseUp)
-- ---@param id number DUI instance ID
-- ---@param x number X coordinate
-- ---@param y number Y coordinate
-- ---@param button? "left"|"middle"|"right" Mouse button to click (default: "left")
-- ---@return boolean success Whether the operation was successful
-- function DUI.Click(id, x, y, button)
-- button = button or "left"
-- if not DUI.MoveMouse(id, x, y) then return false end
-- if not DUI.MouseDown(id, button) then return false end
-- -- Small delay to simulate real click
-- Citizen.Wait(50)
-- return DUI.MouseUp(id, button)
-- end
-- -- Enable or disable mouse tracking for a DUI instance
-- ---@param id number DUI instance ID
-- ---@param enabled boolean Whether to enable mouse tracking
-- ---@param scaleX? number Scale factor for X coordinates (default: 1.0)
-- ---@param scaleY? number Scale factor for Y coordinates (default: 1.0)
-- ---@return boolean success Whether the operation was successful
-- function DUI.TrackMouse(id, enabled, scaleX, scaleY)
-- local instance = activeInstances[id]
-- if not instance then return false end
-- instance.trackMouse = enabled
-- instance.mouseScale.x = scaleX or 1.0
-- instance.mouseScale.y = scaleY or 1.0
-- if enabled then
-- mouseState.tracking = true
-- mouseState.activeId = id
-- elseif mouseState.activeId == id then
-- mouseState.tracking = false
-- mouseState.activeId = nil
-- end
-- return true
-- end
-- -- Update mouse coordinates from screen position
-- ---@param screenX number Screen X coordinate
-- ---@param screenY number Screen Y coordinate
-- ---@return boolean updated Whether coordinates were updated
-- local function UpdateMousePosition(screenX, screenY)
-- if not mouseState.tracking or not mouseState.activeId then return false end
-- local instance = activeInstances[mouseState.activeId]
-- if not instance or not instance.trackMouse then return false end
-- -- Scale coordinates based on DUI dimensions and scale factors
-- local scaledX = screenX * instance.mouseScale.x
-- local scaledY = screenY * instance.mouseScale.y
-- -- Only update if position changed
-- if scaledX ~= mouseState.lastX or scaledY ~= mouseState.lastY then
-- mouseState.lastX = scaledX
-- mouseState.lastY = scaledY
-- DUI.MoveMouse(mouseState.activeId, scaledX, scaledY)
-- return true
-- end
-- return false
-- end
-- -- Mouse tracking thread
-- Citizen.CreateThread(function()
-- while true do
-- if mouseState.tracking and mouseState.activeId then
-- local instance = activeInstances[mouseState.activeId]
-- if instance and instance.trackMouse then
-- -- Get current cursor position
-- local screenX, screenY = GetNuiCursorPosition()
-- UpdateMousePosition(screenX, screenY)
-- -- Handle held mouse buttons
-- if mouseState.isPressed and mouseState.currentButton then
-- -- Keep the button pressed state
-- SendDuiMouseDown(instance.handle, mouseState.currentButton)
-- end
-- end
-- end
-- Citizen.Wait(0)
-- end
-- end)
-- -- Utility functions
-- -- Get textures for a DUI instance
-- ---@param id number DUI instance ID
-- ---@return string|nil txd, string|nil txn Texture dictionary and name, or nil if instance not found
-- function DUI.GetTextures(id)
-- local instance = activeInstances[id]
-- if not instance then return nil, nil end
-- return instance.txd, instance.txn
-- end
-- -- Check if a DUI instance exists
-- ---@param id number DUI instance ID
-- ---@return boolean exists Whether the instance exists and is active
-- function DUI.Exists(id)
-- local instance = activeInstances[id]
-- return instance ~= nil and instance.active
-- end
-- -- Get all active DUI instances
-- ---@return table<number, DUIInstance> instances Table of active DUI instances
-- function DUI.GetActiveInstances()
-- return activeInstances
-- end
-- -- Cleanup all DUI instances
-- function DUI.CleanupAll()
-- for id in pairs(activeInstances) do
-- DUI.Destroy(id)
-- end
-- end
-- -- Automatic cleanup on resource stop
-- AddEventHandler('onResourceStop', function(resourceName)
-- if GetCurrentResourceName() ~= resourceName then return end
-- DUI.CleanupAll()
-- end)
-- return DUI

View file

@ -0,0 +1,248 @@
Utility = Utility or Require("lib/utility/client/utility.lua")
Ids = Ids or Require("lib/utility/shared/ids.lua")
Point = Point or Require("lib/points/client/points.lua")
ClientEntityActions = ClientEntityActions or Require("lib/entities/client/client_entity_actions_ext.lua") -- Added
local Entities = {} -- Stores entity data received from server
ClientEntity = {} -- Renamed from BaseEntity
local function SpawnEntity(entityData)
entityData = entityData and entityData.args
if entityData.spawned and DoesEntityExist(entityData.spawned) then return end -- Already spawned
-- for k, v in pairs(entityData.args) do
-- print(string.format("SpawnEntity %s: %s", k, v))
-- end
local model = entityData.model and type(entityData.model) == 'string' and GetHashKey(entityData.model) or entityData.model
if model and not Utility.LoadModel(model) then
print(string.format("[ClientEntity] Failed to load model %s for entity %s", entityData.model, entityData.id))
return
end
local entity = nil
local coords = entityData.coords
local rotation = entityData.rotation or vector3(0.0, 0.0, 0.0) -- Default rotation if not provided
if entityData.entityType == 'object' then
entity = CreateObject(model, coords.x, coords.y, coords.z, false, false, false)
SetEntityRotation(entity, rotation.x, rotation.y, rotation.z, 2, true)
elseif entityData.entityType == 'ped' then
entity = CreatePed(4, model, coords.x, coords.y, coords.z, type(rotation) == 'number' and rotation or rotation.z, false, false)
elseif entityData.entityType == 'vehicle' then
entity = CreateVehicle(model, coords.x, coords.y, coords.z, type(rotation) == 'number' and rotation or rotation.z, false, false)
else
print(string.format("[ClientEntity] Unknown entity type '%s' for entity %s", entityData.entityType, entityData.id))
end
if entity and model then
entityData.spawned = entity
SetModelAsNoLongerNeeded(model)
SetEntityAsMissionEntity(entity, true, true)
FreezeEntityPosition(entity, true)
else
SetModelAsNoLongerNeeded(model)
end
if entityData.OnSpawn then
entityData.OnSpawn(entityData)
end
end
local function RemoveEntity(entityData)
entityData = entityData and entityData.args or entityData
if not entityData then return end
ClientEntityActions.StopAction(entityData.id)
if entityData.spawned and DoesEntityExist(entityData.spawned) then
local entityHandle = entityData.spawned
entityData.spawned = nil
SetEntityAsMissionEntity(entityHandle, false, false)
DeleteEntity(entityHandle)
end
if entityData.OnRemove then
entityData.OnRemove(entityData)
end
end
--- Registers an entity received from the server and sets up proximity spawning.
-- @param entityData table Data received from the server via 'community_bridge:client:CreateEntity'
function ClientEntity.Create(entityData)
entityData.id = entityData.id or Ids.CreateUniqueId(Entities)
if Entities[entityData.id] then return Entities[entityData.id] end -- Already registered
Entities[entityData.id] = entityData
return Point.Register(entityData.id, entityData.coords, entityData.spawnDistance or 50.0, entityData, SpawnEntity, RemoveEntity, function() end)
end
--Depricated use ClientEntity.Create instead
--- Registers an entity and spawns it in the world if not already spawned.
ClientEntity.Register = ClientEntity.Create
function ClientEntity.CreateBulk(entities)
local registeredEntities = {}
for _, entityData in pairs(entities) do
local entity = ClientEntity.Create(entityData)
registeredEntities[entity.id] = entity
end
return registeredEntities
end
-- Depricated use ClientEntity.CreateBulk instead
ClientEntity.RegisterBulk = ClientEntity.CreateBulk
--- Unregisters an entity and removes it from the world if spawned.
-- @param id string|number The ID of the entity to unregister.
function ClientEntity.Destroy(id)
local entityData = Entities[id]
if not entityData then return end
Point.Remove(id)
RemoveEntity(entityData)
Entities[id] = nil
end
ClientEntity.Unregister = ClientEntity.Destroy
--- Updates the data for a registered entity.
-- @param id string|number The ID of the entity to update.
-- @param data table The data fields to update.
function ClientEntity.Update(id, data)
local entityData = Entities[id]
-- print(string.format("[ClientEntity] Updating entity %s", id))
if not entityData then return end
local needsPointUpdate = false
for key, value in pairs(data) do
if key == 'coords' and #(entityData.coords - value) > 0.1 then
needsPointUpdate = true
end
if key == 'spawnDistance' and entityData.spawnDistance ~= value then
needsPointUpdate = true
end
entityData[key] = value
end
-- If entity is currently spawned, apply updates
if entityData.spawned and DoesEntityExist(entityData.spawned) then
if data.coords then
SetEntityCoords(entityData.spawned, entityData.coords.x, entityData.coords.y, entityData.coords.z, false, false, false, true)
end
if data.rotation then
if entityData.entityType == 'object' then
SetEntityRotation(entityData.spawned, entityData.rotation.x, entityData.rotation.y, entityData.rotation.z, 2, true)
else -- Ped/Vehicle heading
SetEntityHeading(entityData.spawned, type(entityData.rotation) == 'number' and entityData.rotation or entityData.rotation.z)
end
end
if data.freeze ~= nil then
FreezeEntityPosition(entityData.spawned, data.freeze)
end
-- Add other updatable properties as needed
end
-- Update Point registration if coords or distance changed
if needsPointUpdate then
Point.Remove(id)
Point.Register(
entityData.id,
entityData.coords,
entityData.spawnDistance or 50.0,
SpawnEntity,
RemoveEntity,
nil,
entityData
)
end
if entityData.OnUpdate and type(entityData.OnUpdate) == 'function' then
entityData.OnUpdate(entityData, data)
end
end
function ClientEntity.Get(id)
return Entities[id]
end
function ClientEntity.GetAll()
return Entities
end
function ClientEntity.RegisterAction(name, func)
ClientEntityActions.RegisterAction(name, func)
end
function ClientEntity.OnCreate(_type, func)
ClientEntity.OnCreates = ClientEntity.OnCreates or {}
if not ClientEntity.OnCreates[_type] then
ClientEntity.OnCreates[_type] = {}
end
table.insert(ClientEntity.OnCreates[_type], func)
end
-- Network Event Handlers
RegisterNetEvent("community_bridge:client:CreateEntity", function(entityData)
ClientEntity.Create(entityData)
end)
RegisterNetEvent("community_bridge:client:CreateEntities", function(entities)
ClientEntity.CreateBulk(entities)
end)
RegisterNetEvent("community_bridge:client:DeleteEntity", function(id)
ClientEntity.Unregister(id)
end)
RegisterNetEvent("community_bridge:client:UpdateEntity", function(id, data)
ClientEntity.Update(id, data)
end)
-- New handler for entity actions
RegisterNetEvent("community_bridge:client:TriggerEntityAction", function(entityId, actionName, ...)
local entityData = Entities[entityId]
-- Check if entity exists locally (it doesn't need to be spawned to queue actions)
if entityData then
if actionName == "Stop" then
ClientEntityActions.StopAction(entityId)
elseif actionName == "Skip" then
ClientEntityActions.SkipAction(entityId)
else
print(string.format("[ClientEntity] Triggering action '%s' for entity %s", actionName, entityId))
local currentAction = ClientEntityActions.ActionQueue[entityId] and ClientEntityActions.ActionQueue[entityId][1]
ClientEntityActions.QueueAction(entityData, actionName, ...)
end
-- else
-- Optional: Log if action received but entity doesn't exist locally at all
-- print(string.format("[ClientEntity] Received action '%s' for non-existent entity %s.", actionName, entityId))
end
end)
RegisterNetEvent("community_bridge:client:TriggerEntityActions", function(entityId, actions, endPosition)
local entityData = Entities[entityId]
if entityData then
for _, actionData in pairs(actions) do
local actionName = actionData.name
local actionParams = actionData.params
if actionName == "Stop" then
ClientEntityActions.StopAction(entityId)
elseif actionName == "Skip" then
ClientEntityActions.SkipAction(entityId)
else
local currentAction = ClientEntityActions.ActionQueue[entityId] and ClientEntityActions.ActionQueue[entityId][1]
ClientEntityActions.QueueAction(entityData, actionName, table.unpack(actionParams))
end
end
else
print(string.format("[ClientEntity] Received actions for non-existent entity %s.", entityId))
end
end)
-- Resource Stop Cleanup
AddEventHandler('onResourceStop', function(resourceName)
if resourceName == GetCurrentResourceName() then
for id, entityData in pairs(Entities) do
Point.Remove(id) -- Clean up point registration
RemoveEntity(entityData) -- Clean up spawned game entity
end
Entities = {} -- Clear local cache
end
end)
return ClientEntity

View file

@ -0,0 +1,136 @@
ClientEntityActions = {}
ClientEntityActions.ActionThreads = {} -- Store running action threads { [entityId] = thread }
ClientEntityActions.ActionQueue = {} -- Stores pending actions { [entityId] = {{name="ActionName", args={...}}, ...} }
ClientEntityActions.IsActionRunning = {} -- Tracks if an action is currently running { [entityId] = boolean }
ClientEntityActions.RegisteredActions = {} -- New: Registry for action implementations { [actionName] = function(entityData, ...) }
-- Forward declaration
--- Processes the next action in the queue for a given entity.
-- @param entityId string|number The ID of the entity.
function ClientEntityActions.ProcessNextAction(entityId)
if ClientEntityActions.IsActionRunning [entityId] then return end -- Already running something
local queue = ClientEntityActions.ActionQueue[entityId]
if not queue or #queue == 0 then return end -- Queue is empty
local nextAction = table.remove(queue, 1) -- Dequeue (FIFO)
local entityData = ClientEntity.Get(entityId) -- Assumes ClientEntity is accessible
-- Check if entity is still valid and spawned before starting next action
if not entityData or not entityData.spawned or not DoesEntityExist(entityData.spawned) then
-- Entity despawned while idle, clear queue and do nothing
ClientEntityActions.ActionQueue[entityId] = nil
return
end
-- Look up the action in the registry
local actionFunc = ClientEntityActions.RegisteredActions [nextAction.name]
if actionFunc then
-- print(string.format("[ClientEntityActions] Starting action '%s' for entity %s", nextAction.name, entityId))
ClientEntityActions.IsActionRunning [entityId] = true
-- Call the registered function
ClientEntityActions.ActionQueue[entityId] = actionFunc(entityData, table.unpack(nextAction.args))
if not ClientEntityActions.ActionQueue[entityId] then
-- If the action function doesn't return a queue, set it to nil
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
end
else
print(string.format("[ClientEntityActions] Unknown action '%s' dequeued for entity %s", nextAction.name, entityId))
-- Skip unknown action and try the next one immediately
ClientEntityActions.ActionQueue[entityId] = ClientEntityActions.ProcessNextAction(entityId)
end
end
--- Registers a custom action implementation.
-- The action function should handle its own logic, including threading if needed,
-- and MUST call ClientEntityActions.ProcessNextAction(entityData.id) when it completes or fails,
-- after setting ClientEntityActions.IsActionRunning [entityData.id] = false.
-- @param actionName string The name used to trigger this action.
-- @param actionFunc function The function to execute. Signature: function(entityData, ...)
function ClientEntityActions.RegisterAction(actionName, actionFunc)
if ClientEntityActions.RegisteredActions [actionName] then
print(string.format("[ClientEntityActions] WARNING: Overwriting registered action '%s'", actionName))
end
assert(type(actionName) == "string", "actionName must be a string")
ClientEntityActions.RegisteredActions[actionName] = actionFunc
-- print(string.format("[ClientEntityActions] Registered action: %s", actionName))
end
--- Queues an action for an entity. Starts processing if idle.
-- @param entityData table The entity data.
-- @param actionName string The name of the action.
-- @param ... any Arguments for the action.
function ClientEntityActions.QueueAction(entityData, actionName, ...)
local entityId = entityData.id
if not ClientEntityActions.ActionQueue[entityId] then
ClientEntityActions.ActionQueue[entityId] = {}
end
local actionArgs = {...}
table.insert(ClientEntityActions.ActionQueue[entityId], { name = actionName, args = actionArgs })
-- print(string.format("[ClientEntityActions] Queued action '%s' for entity %s. Queue size: %d", actionName, entityId, #ClientEntityActions.ActionQueue[entityId]))
-- If the entity isn't currently doing anything, start processing immediately
if not ClientEntityActions.IsActionRunning [entityId] then
ClientEntityActions.ProcessNextAction(entityId)
end
end
--- Stops the current action and clears the queue for a specific entity.
-- @param entityId string|number The ID of the entity.
function ClientEntityActions.StopAction(entityId)
-- print(string.format("[ClientEntityActions] Stopping all actions for entity %s", entityId))
ClientEntityActions.ActionQueue[entityId] = nil -- Clear the queue
ClientEntityActions.IsActionRunning [entityId] = false -- Mark as not running (this will stop loops in threads)
-- Stop current task/thread if applicable
if ClientEntityActions.ActionThreads[entityId] then
-- Lua threads stop themselves based on ClientEntityActions.IsActionRunning flag
ClientEntityActions.ActionThreads[entityId] = nil
end
-- Specific task clearing for peds
local entityData = ClientEntity.Get(entityId)
if entityData and entityData.spawned and DoesEntityExist(entityData.spawned) then
if IsEntityAPed(entityData.spawned) then
ClearPedTasksImmediately(entityData.spawned) -- Use Immediately for forceful stop
end
-- Other entity types might need different stop logic
end
end
--- Skips the current action and starts the next one in the queue, if any.
-- @param entityId string|number The ID of the entity.
function ClientEntityActions.SkipAction(entityId)
if not ClientEntityActions.IsActionRunning [entityId] then
-- print(string.format("[ClientEntityActions] SkipAction called for %s, but no action running.", entityId))
return -- Nothing to skip
end
-- print(string.format("[ClientEntityActions] Skipping current action for entity %s", entityId))
ClientEntityActions.IsActionRunning [entityId] = false -- Mark as not running (this will stop loops in threads)
-- Stop current task/thread if applicable
if ClientEntityActions.ActionThreads[entityId] then
ClientEntityActions.ActionThreads[entityId] = nil
end
local entityData = ClientEntity.Get(entityId)
if entityData and entityData.spawned and DoesEntityExist(entityData.spawned) then
if IsEntityAPed(entityData.spawned) then
ClearPedTasksImmediately(entityData.spawned)
end
end
-- Immediately try to process the next action
ClientEntityActions.ProcessNextAction(entityId)
end
-- Add server-callable functions for Stop and Skip
function ClientEntityActions.Stop(entityData)
ClientEntityActions.StopAction(entityData.id)
end
function ClientEntityActions.Skip(entityData)
ClientEntityActions.SkipAction(entityData.id)
end
return ClientEntityActions

View file

@ -0,0 +1,359 @@
DefaultActions = {}
ClientEntityActions = ClientEntityActions or Require("lib/entities/client/client_entity_actions.lua")
LA = LA or Require("lib/utility/shared/la.lua")
--- Internal implementation for walking. Registered via RegisterAction.
function DefaultActions.WalkTo(entityData, coords, speed, timeout)
local entity = entityData.spawned
local entityId = entityData.id -- Store ID locally for safety in thread
if not entity or not DoesEntityExist(entity) or not IsEntityAPed(entity) then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
return
end
-- Clear previous tasks just in case
ClearPedTasks(entity)
local thread = CreateThread(function()
TaskGoToCoordAnyMeans(entity, coords.x, coords.y, coords.z, speed or 1.0, 0, false, 786603, timeout or -1)
-- Wait until task is completed/interrupted or entity is despawned/changed
local entityCoords = GetEntityCoords(entity)
while ClientEntityActions.IsActionRunning[entityId] and entityData.spawned == entity and DoesEntityExist(entity) and #(entityCoords - coords) > 2.0 do
entityCoords = GetEntityCoords(entity)
Wait(0) -- Yield to avoid freezing the game
end
ClientEntityActions.ActionThreads[entityId] = nil -- Clear thread reference
-- Only process next action if this thread was the one running the action
if ClientEntityActions.IsActionRunning[entityId] then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
end
end)
ClientEntityActions.ActionThreads[entityId] = thread
end
--- Internal implementation for playing an animation. Registered via RegisterAction.
--- @param entityData table
--- @param animDict string
--- @param animName string
--- @param blendIn number (Optional, default 8.0)
--- @param blendOut number (Optional, default -8.0)
--- @param duration number (Optional, default -1 for loop/until stopped)
--- @param flag number (Optional, default 0)
--- @param playbackRate number (Optional, default 0.0)
function DefaultActions.PlayAnim(entityData, animDict, animName, blendIn, blendOut, duration, flag, playbackRate)
local entity = entityData.spawned
local entityId = entityData.id
if not entity or not DoesEntityExist(entity) or not IsEntityAPed(entity) then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
return
end
blendIn = blendIn or 8.0
blendOut = blendOut or -8.0
duration = duration or -1
flag = flag or 0
playbackRate = playbackRate or 0.0
local thread = CreateThread(function()
if not HasAnimDictLoaded(animDict) then
RequestAnimDict(animDict)
local timeout = 100
while not HasAnimDictLoaded(animDict) and timeout > 0 do
Wait(10)
timeout = timeout - 1
end
end
if HasAnimDictLoaded(animDict) then
TaskPlayAnim(entity, animDict, animName, blendIn, blendOut, duration, flag, playbackRate, false, false, false)
-- Wait for completion or interruption
local startTime = GetGameTimer()
local animTime = duration > 0 and (startTime + duration) or -1
while ClientEntityActions.IsActionRunning[entityId] and entityData.spawned == entity and DoesEntityExist(entity) do
local isPlaying = IsEntityPlayingAnim(entity, animDict, animName, 3)
-- Break conditions:
-- 1. Action was stopped/skipped externally
-- 2. Entity changed/despawned
-- 3. Animation finished naturally (if not looping based on flags/duration)
-- 4. Duration expired (if duration > 0)
if not ClientEntityActions.IsActionRunning[entityId] or entityData.spawned ~= entity or not DoesEntityExist(entity) then break end
if duration == -1 and not isPlaying and GetEntityAnimCurrentTime(entity, animDict, animName) > 0.1 then break end -- Check if non-looping anim finished
if animTime ~= -1 and GetGameTimer() >= animTime then break end
Wait(100) -- Check periodically
end
-- Don't remove dict here if other actions might use it immediately after
-- Consider a separate cleanup mechanism if needed
else
print(string.format("[ClientEntityActions] Failed to load anim dict '%s' for entity %s", animDict, entityId))
end
-- Crucial: Mark as finished and process next
if ClientEntityActions.IsActionRunning[entityId] then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
end
end)
ClientEntityActions.ActionThreads[entityId] = thread
end
--- Internal implementation for lerping. Registered via RegisterAction.
function DefaultActions.LerpTo(entityData, targetCoords, duration, easingType, easingDirection)
local entity = entityData.spawned
local entityId = entityData.id -- Store ID locally
if not entity or not DoesEntityExist(entity) then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
return
end
local startCoords = GetEntityCoords(entity)
local startTime = GetGameTimer()
easingType = easingType or "linear"
easingDirection = easingDirection or "inout"
local thread = CreateThread(function()
while GetGameTimer() < startTime + duration do
-- Check if action should continue
if not ClientEntityActions.IsActionRunning[entityId] or not entityData.spawned or entityData.spawned ~= entity or not DoesEntityExist(entity) then
break -- Stop if entity despawned, changed, or action was stopped/skipped
end
local elapsed = GetGameTimer() - startTime
local t = LA.Clamp(elapsed / duration, 0.0, 1.0)
local easedT = LA.EaseInOut(t, easingType) -- Default
if easingDirection == "in" then
easedT = LA.EaseIn(t, easingType)
elseif easingDirection == "out" then
easedT = LA.EaseOut(t, easingType)
end
local currentPos = LA.LerpVector(startCoords, targetCoords, easedT)
SetEntityCoordsNoOffset(entity, currentPos.x, currentPos.y, currentPos.z, false, false, false)
Wait(0)
end
-- Ensure final position if completed fully and action wasn't stopped/skipped
if ClientEntityActions.IsActionRunning[entityId] and entityData.spawned == entity and DoesEntityExist(entity) then
SetEntityCoordsNoOffset(entity, targetCoords.x, targetCoords.y, targetCoords.z, false, false, false)
end
ClientEntityActions.ActionThreads[entityId] = nil -- Clear thread reference
-- Only process next action if this thread was the one running the action
if ClientEntityActions.IsActionRunning[entityId] then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
end
end)
ClientEntityActions.ActionThreads[entityId] = thread
end
--- Internal implementation for attaching a prop. Registered via RegisterAction.
--- This action completes immediately after attaching. Use DetachProp to remove.
--- @param entityData table
--- @param propModel string|number
--- @param boneIndex number (Optional, default -1 for root)
--- @param offsetPos vector3 (Optional, default vector3(0,0,0))
--- @param offsetRot vector3 (Optional, default vector3(0,0,0))
--- @param useSoftPinning boolean (Optional, default false)
--- @param collision boolean (Optional, default false)
--- @param isPed boolean (Optional, default false) - Seems unused in native?
--- @param vertexIndex number (Optional, default 2) - Seems unused in native?
--- @param fixedRot boolean (Optional, default true)
function DefaultActions.AttachProp(entityData, propModel, boneName, offsetPos, offsetRot, useSoftPinning, collision, isPed, vertexIndex, fixedRot)
local entity = entityData.spawned
local entityId = entityData.id
if not entity or not DoesEntityExist(entity) then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
return
end
local modelHash = Utility.GetEntityHashFromModel(propModel)
if not Utility.LoadModel(modelHash) then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
return
end
local boneIndex = GetEntityBoneIndexByName(entity, boneName)
local coords = GetEntityCoords(entity)
local prop = CreateObject(modelHash, coords.x, coords.y, coords.z, false, false, false)
SetModelAsNoLongerNeeded(modelHash)
boneIndex = boneIndex or GetPedBoneIndex(entity, 60309) -- SKEL_R_Hand if not specified and is ped
if boneIndex == -1 then boneIndex = 0 end -- Default to root if bone not found or not ped
offsetPos = offsetPos or vector3(0.0, 0.0, 0.0)
offsetRot = offsetRot or vector3(0.0, 0.0, 0.0)
AttachEntityToEntity(prop, entity, boneIndex, offsetPos.x, offsetPos.y, offsetPos.z, offsetRot.x, offsetRot.y, offsetRot.z, false, useSoftPinning or false, collision or false, isPed or false, vertexIndex or 2, fixedRot == nil and true or fixedRot)
entityData.props = entityData.props or {} -- Ensure props table exists
table.insert(entityData.props, prop) -- Store the prop handle in the entity data
-- Store the attached prop handle for later removal
if not entityData.attachedProps then entityData.attachedProps = {} end
entityData.attachedProps[propModel] = prop -- Store by model name/hash for easy lookup
-- This action finishes immediately
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
end
--- Internal implementation for detaching a prop. Registered via RegisterAction.
--- @param entityData table
--- @param propModel string|number The model name/hash of the prop to detach.
function DefaultActions.DetachProp(entityData, propModel)
local entityId = entityData.id
if entityData.attachedProps and propModel then
local propHandle = entityData.attachedProps[propModel]
if propHandle and DoesEntityExist(propHandle) then
DetachEntity(propHandle, true, true) -- Detach
DeleteEntity(propHandle) -- Delete
entityData.attachedProps[propModel] = nil -- Remove from tracking
-- print(string.format("[ClientEntityActions] Detached prop '%s' from entity %s", propModel, entityId))
else
-- print(string.format("[ClientEntityActions] Prop '%s' not found attached to entity %s for detachment.", propModel, entityId))
end
else
-- print(string.format("[ClientEntityActions] No props tracked or propModel not specified for detachment on entity %s.", entityId))
end
-- This action finishes immediately
-- ClientEntityActions.IsActionRunning[entityId] = false
-- ClientEntityActions.ProcessNextAction(entityId)
end
function DefaultActions.GetInCar(entityData, vehicleData, seatIndex, timeout)
local entity = entityData.spawned
local vehicle = vehicleData.spawned
local entityId = entityData.id
if not entity or not DoesEntityExist(entity) or not IsEntityAPed(entity) then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
return
end
if not vehicle or not DoesEntityExist(vehicle) or not IsEntityAVehicle(vehicle) then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
return
end
-- Clear previous tasks just in case
ClearPedTasks(entity)
local thread = CreateThread(function()
TaskEnterVehicle(entity, vehicle, timeout or 1000, seatIndex or -1, 1.0, 1, 0) -- Enter vehicle
Wait(timeout or 1000) -- Wait for a bit to ensure the task is completed
ClientEntityActions.ActionThreads[entityId] = nil -- Clear thread reference
-- Only process next action if this thread was the one running the action
if ClientEntityActions.IsActionRunning[entityId] then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
end
end)
ClientEntityActions.ActionThreads[entityId] = thread
end
function DefaultActions.Freeze(entityData, freeze)
local entity = entityData.spawned
local entityId = entityData.id
if not entity or not DoesEntityExist(entity) then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
return
end
FreezeEntityPosition(entity, freeze or true)
-- This action finishes immediately
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
end
function DefaultActions.PlaceOnGround(entityData)
local entity = entityData.spawned
local entityId = entityData.id
if not entity or not DoesEntityExist(entity) then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
return
end
PlaceObjectOnGroundProperly(entity)
-- This action finishes immediately
-- ClientEntityActions.IsActionRunning[entityId] = false
-- ClientEntityActions.ProcessNextAction(entityId)
end
function DefaultActions.BobUpAndDown(entityData, speed, height)
local entity = entityData.spawned
local entityId = entityData.id
if not entity or not DoesEntityExist(entity) then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
return
end
CreateThread(function()
local coords = GetEntityCoords(entity)
local originalZ = coords.z
while DoesEntityExist(entity) do
-- Calculate the new Z coordinate
local newZ = originalZ + math.sin(GetGameTimer() * (speed / 1000)) * height
-- Set the new coordinates
SetEntityCoords(entity, coords.x, coords.y, newZ)
-- Wait for 10 milliseconds
Wait(10)
end
end)
-- ClientEntityActions.IsActionRunning[entityId] = false
-- ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
end
function DefaultActions.Circle(entityData, radius, speed)
local entity = entityData.spawned
local entityId = entityData.id
if not entity or not DoesEntityExist(entity) then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
return
end
local coords = GetEntityCoords(entity)
local angle = 0.0
CreateThread(function()
while DoesEntityExist(entity) do
FreezeEntityPosition(entity, false)
local pos = LA.Circle(angle, radius, coords)
SetEntityCoords(entity, pos.x, pos.y, pos.z, false, false, false, false)
angle = angle + speed * GetFrameTime()
FreezeEntityPosition(entity, true)
Wait(0)
end
end)
-- ClientEntityActions.IsActionRunning[entityId] = false
-- ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
end
function DefaultActions.Collisions(entityData, enable, keepPhysics)
local entity = entityData.spawned
SetEntityCollision(entity, enable, keepPhysics)
end
for name, func in pairs(DefaultActions) do
ClientEntityActions.RegisterAction(name, func)
end
return ClientEntityActions

View file

@ -0,0 +1,135 @@
Ids = Ids or Require("lib/utility/shared/ids.lua")
local Entities = {}
ServerEntity = {} -- Renamed from EntityRelay
--- Creates a server-side representation of an entity and notifies clients.
-- @param entityType string 'object', 'ped', or 'vehicle'
-- @param model string|number
-- @param coords vector3
-- @param rotation vector3|number Heading for peds/vehicles, rotation for objects
-- @param meta table Optional additional data
-- @return table The created entity data
function ServerEntity.New(id, entityType, model, coords, rotation, meta)
local self = meta or {}
self.id = id or Ids.CreateUniqueId(Entities)
self.entityType = entityType
self.model = model
self.coords = coords
self.rotation = rotation or (entityType == 'object' and vector3(0.0, 0.0, 0.0) or 0.0) -- Default rotation or heading
self.resource = GetInvokingResource()
assert(self.id, "ID Failed to generate")
assert(self.entityType, "EntityType is required")
assert(self.model, "Model is required for entity creation")
assert(self.coords, "Coords are required for entity creation")
ServerEntity.Add(self)
return self
end
function ServerEntity.Create(id, entityType, model, coords, rotation, meta)
local self = ServerEntity.New(id, entityType, model, coords, rotation, meta)
if not self then
print("Failed to create entity with ID: " .. tostring(id))
return nil
end
TriggerClientEvent("community_bridge:client:CreateEntity", -1, self)
return self
end
function ServerEntity.CreateBulk(entities)
local createdEntities = {}
for _, entityData in pairs(entities) do
local id = entityData.id or Ids.CreateUniqueId(Entities)
local entity = ServerEntity.New(
id,
entityData.entityType,
entityData.model,
entityData.coords,
entityData.rotation,
entityData.meta
)
createdEntities[id] = entity
end
TriggerClientEvent("community_bridge:client:CreateEntities", -1, createdEntities)
return createdEntities
end
--- Deletes a server-side entity representation and notifies clients.
-- @param id string|number The ID of the entity to delete.
function ServerEntity.Delete(id)
if Entities[id] then
ServerEntity.Remove(id)
TriggerClientEvent("community_bridge:client:DeleteEntity", -1, id)
end
end
--- Updates data for a server-side entity and notifies clients.
-- @param id string|number The ID of the entity to update.
-- @param data table The data fields to update.
function ServerEntity.Update(id, data)
local entity = Entities[id]
print("Updating entity: ", id, entity)
if not entity then return false end
for key, value in pairs(data) do
entity[key] = value
end
TriggerClientEvent("community_bridge:client:UpdateEntity", -1, id, data)
return true
end
--- Triggers a specific action on the client-side entity.
-- Clients will only execute the action if the entity is currently spawned for them.
-- @param entityId string|number The ID of the entity.
-- @param actionName string The name of the action to trigger (must match a function in ClientEntityActions).
-- @param ... any Additional arguments for the action function.
function ServerEntity.TriggerAction(entityId, actionName, endPosition, ...)
print("Triggering action: ", entityId, actionName, ...)
local entity = Entities[entityId]
if not entity then
print(string.format("[ServerEntity] Attempted to trigger action '%s' on non-existent entity %s", actionName, entityId))
return
end
TriggerClientEvent("community_bridge:client:TriggerEntityAction", -1, entityId, actionName, endPosition, ...)
end
function ServerEntity.TriggerActions(entityId, actions, endPosition)
local entity = Entities[entityId]
if not entity then
print(string.format("[ServerEntity] Attempted to trigger actions on non-existent entity %s", entityId))
return
end
TriggerClientEvent("community_bridge:client:TriggerEntityActions", -1, entityId, actions, endPosition)
end
function ServerEntity.GetAll()
return Entities
end
function ServerEntity.Get(id)
return Entities[id]
end
function ServerEntity.Add(self)
Entities[self.id] = self
end
function ServerEntity.Remove(id)
Entities[id] = nil
end
-- Clean up entities associated with a stopped resource
AddEventHandler('onResourceStop', function(resourceName)
local toDelete = {}
for id, entity in pairs(Entities) do
if entity.resource == resourceName then
table.insert(toDelete, id)
end
end
for _, id in pairs(toDelete) do
ServerEntity.Delete(id)
end
end)
return ServerEntity

View file

@ -0,0 +1,49 @@
local Actions = {}
Action = {}
if not IsDuplicityVersion() then goto client end
function Action.Fire(id, players, ...)
local action = Actions[id]
if not action then return end
if type(players) == "table" then
for _, player in ipairs(players) do
TriggerClientEvent(GetCurrentResourceName() .. "client:Action", tonumber(player), id, ...)
end
return
end
TriggerClientEvent(GetCurrentResourceName() .. "client:Action", tonumber(players or -1), id, ...)
end
if IsDuplicityVersion() then return Actions end
::client::
function Action.Create(id, action)
assert(type(id) == "string", "id must be a string")
assert(type(action) == "function", "action must be a function")
Actions[id] = action
end
function Action.Remove(id)
Actions[id] = nil
end
function Action.Get(id)
return Actions[id]
end
function Action.GetAll()
return Actions
end
RegisterNetEvent(GetCurrentResourceName() .. "client:Action", function(id, ...)
local action = Actions[id]
if not action then return end
action(...)
end)
exports("Action", Action)
return Action

View file

@ -0,0 +1,117 @@
ItemsBuilder = ItemsBuilder or {}
ItemsBuilder = {}
---This will generate the items in the formats of qb_core, qb_core_old and ox_inventory. It will then build a lua file in the generateditems folder of the community_bridge.
---@param invoking string
---@param itemsTable table
ItemsBuilder.Generate = function(invoking, outputPrefix, itemsTable, useQB)
if not itemsTable then return end
invoking = invoking or GetInvokingResource() or GetCurrentResourceName() or "community_bridge"
local resourcePath = string.format("%s/%s/", GetResourcePath(invoking), outputPrefix or "generated_items")
-- remove any doubles slashes after initial double slash
-- resourcePath = resourcePath:gsub("//", "/")
-- check if directory exists
local folder = io.open(resourcePath, "r")
if not folder then
local createDirectoryCMD = string.format("if not exist \"%s\" mkdir \"%s\"", resourcePath, resourcePath)
local returned, err = io.popen(createDirectoryCMD)
if not returned then
print("🌮 Failed to create directory: ", err)
return
end
returned:close()
print("🌮 Created Directory: ", resourcePath)
else
folder:close()
end
local qbOld = {}
local qbNew = {}
local oxInventory = {}
if useQB then
for key, item in pairs(itemsTable) do
qbOld[key] = string.format(
"['%s'] = {name = '%s', label = '%s', weight = %s, type = 'item', image = '%s', unique = %s, useable = %s, shouldClose = %s, description = '%s'},",
key, key, item.label, item.weight, item.image or key .. 'png', item.unique, item.useable, item.shouldClose, item.description
)
qbNew[key] = string.format(
"['%s'] = {['name'] = '%s', ['label'] = '%s', ['weight'] = %s, ['type'] = 'item',['image'] = '%s', ['unique'] = %s, ['useable'] = %s, ['shouldClose'] = %s, ['description'] = '%s'},",
key, key, item.label, item.weight, item.image or key .. 'png', item.unique, item.useable, item.shouldClose, item.description
)
imagewithoutpng = item?.image and item.image:gsub(".png", "")
shouldRenderImage = imagewithoutpng and imagewithoutpng ~= key
oxInventory[key] = string.format(
[[
["%s"] = {
label = "%s",
description = "%s",
weight = %s,
stack = %s,
close = %s,
%s
}, ]],
key, item.label, item.description, item.weight, not item.unique, item.shouldClose,
shouldRenderImage and item.image and string.format([[client = {
image = '%s'
}]], item.image) or ""
)
end
else
for key, item in pairs(itemsTable) do
-- ['peanut_butter'] = {['name'] = 'peanut_butter',['label'] = 'Peanut Butter',['weight'] = 1000,['type'] = 'item',['image'] = 'peanut_butter.png',['unique'] = false,['useable'] = false,['shouldClose'] = true,['combinable'] = nil,['description'] = 'A cooking ingredient'},
qbOld[key] = string.format(
"['%s'] = {name = '%s', label = '%s', weight = %s, type = 'item', image = '%s', unique = %s, useable = %s, shouldClose = %s, description = '%s'},",
key, key, item.label, item.weight, item?.client?.image or key .. 'png', not item.stack, true, item.close, item.description
)
qbNew[key] = string.format(
"['%s'] = {['name'] = '%s', ['label'] = '%s', ['weight'] = %s, ['type'] = 'item', ['image'] = '%s', ['unique'] = %s, ['useable'] = %s, ['shouldClose'] = %s, ['description'] = '%s'},",
key, key, item.label, item.weight, item?.client?.image or key .. 'png', not item.stack, true, item.close, item.description
)
imagewithoutpng = item?.client?.image and item.client.image:gsub(".png", "")
shouldRenderImage = imagewithoutpng and imagewithoutpng ~= key
oxInventory[key] = string.format(
[[
["%s"] = {
label = "%s",
description = "%s",
weight = %s,
stack = %s,
close = %s,
%s
}, ]],
key, item.label, item.description, item.weight, item.stack, item.close,
shouldRenderImage and item?.client?.image and string.format( [[client = {
image = '%s'
}]], item.client.image) or ""
)
end
end
local function write(fileName, content)
local filePath = resourcePath .. fileName
local file = io.open(filePath, "w")
if file then
for key, value in pairs(content) do
file:write(string.format("%s\n", value))
end
file:close()
print("🌮 Items File Created: " .. filePath)
else
print("🌮 Something Broke for: " .. filePath)
end
end
write(invoking.."(".."qb_core_old).lua", qbOld)
write(invoking.."(".."qb_core_new).lua", qbNew)
write(invoking.."(".."ox_inventory).lua", oxInventory)
end
exports('ItemsBuilder', ItemsBuilder)
return ItemsBuilder

View file

@ -0,0 +1,74 @@
LootTable = LootTable or {}
local lootTables = {}
LootTable.Register = function(name, items)
local repackedTable = {}
for k, v in pairs(items) do
table.insert(repackedTable, {
name = k,
min = v.min,
max = v.max,
chance = v.chance,
tier = v.tier,
item = v.item,
metadata = v.metadata
})
end
lootTables[name] = repackedTable
return lootTables[name]
end
LootTable.Get = function(name)
assert(name, "No Name Passed For Loot Table")
return lootTables[name] or {}
end
LootTable.GetRandomItem = function(name, tier, randomNumber)
assert(name, "No Name Passed For Loot Table")
if tier == nil then tier = 1 end
local lootTable = lootTables[name] or {}
math.randomseed(GetGameTimer())
local chance = randomNumber or math.random(1, 100)
for _, v in pairs(lootTable) do
if v.chance <= chance and tier == v.tier then
return { item = v.item, metadata = v.metadata, count = math.random(v.min, v.max), tier = v.tier, chance = v.chance}
end
end
end
LootTable.GetRandomItems = function(name, tier, randomNumber)
assert(name, "No Name Passed For Loot Table")
if tier == nil then tier = 1 end
local lootTable = LootTable.Get(name)
math.randomseed(GetGameTimer())
local chance = randomNumber or math.random(1, 100)
local items = {}
for _, v in pairs(lootTable) do
if v.chance <= chance and tier == v.tier then
table.insert(items,{item = v.item, metadata = v.metadata, count = math.random(v.min, v.max), tier = v.tier, chance = v.chance})
end
end
return items
end
LootTable.GetRandomItemsWithLimit = function(name, tier, randomNumber)
assert(name, "No Name Passed For Loot Table")
if tier == nil then tier = 1 end
local lootTable = LootTable.Get(name)
math.randomseed(GetGameTimer())
local chance = randomNumber or math.random(1, 100)
local items = {}
for k = #lootTable, 1, -1 do
local v = lootTable[k]
if chance <= v.chance and tier == v.tier then
table.insert(items, {v.item, v.metadata, math.random(v.min, v.max), v.tier, v.chance})
table.remove(lootTable, k)
end
end
return items
end
--
exports('LootTable', LootTable)
return LootTable

View file

@ -0,0 +1,86 @@
loadedModules = {}
function Require(modulePath, resourceName)
if resourceName and type(resourceName) ~= "string" then
resourceName = GetInvokingResource()
end
if not resourceName then
resourceName = "community_bridge"
end
local id = resourceName .. ":" .. modulePath
if loadedModules[id] then
if BridgeSharedConfig.DebugLevel ~= 0 then
print("^2 Returning cached module [" .. id .. "] ^0")
end
return loadedModules[id]
end
local file = LoadResourceFile(resourceName, modulePath)
if not file then
error("Error loading file [" .. id .. "]")
end
local chunk, loadErr = load(file, id)
if not chunk then
error("Error wrapping module [" .. id .. "] Message: " .. loadErr)
end
local success, result = pcall(chunk)
if not success then
error("Error executing module [" .. id .. "] Message: " .. result)
end
loadedModules[id] = result
return result
end
cLib = {
Require = Require,
Callback = Callback or Require("lib/utility/shared/callbacks.lua"),
Ids = Ids or Require("lib/utility/shared/ids.lua"),
ReboundEntities = ReboundEntities or Require("lib/utility/shared/rebound_entities.lua"),
Tables = Tables or Require("lib/utility/shared/tables.lua"),
Prints = Prints or Require("lib/utility/shared/prints.lua"),
Math = Math or Require("lib/utility/shared/math.lua"),
LA = LA or Require("lib/utility/shared/la.lua"),
Perlin = Perlin or Require("lib/utility/shared/perlin.lua"),
-- Action = Action or Require("lib/entities/shared/actions.lua"),
}
exports('cLib', cLib)
if not IsDuplicityVersion() then goto client end
cLib.SQL = SQL or Require("lib/sql/server/sqlHandler.lua")
cLib.Logs = Logs or Require("lib/logs/server/logs.lua")
cLib.ItemsBuilder = ItemsBuilder or Require("lib/generators/server/ItemsBuilder.lua")
cLib.LootTables = LootTables or Require("lib/generators/server/lootTables.lua")
cLib.Cache = Cache or Require("lib/cache/shared/cache.lua")
cLib.ServerEntity = ServerEntity or Require("lib/entities/server/server_entity.lua")
cLib.Marker = Marker or Require("lib/markers/server/server.lua")
cLib.Particle = Particle or Require("lib/particles/server/particles.lua")
cLib.Shell = Shells or Require("lib/shells/server/shells.lua")
if IsDuplicityVersion() then return cLib end
::client::
cLib.Scaleform = Scaleform or Require("lib/scaleform/client/scaleform.lua")
cLib.Placeable = Placeable or Require("lib/placers/client/object_placer.lua")
cLib.Utility = Utility or Require("lib/utility/client/utility.lua")
cLib.PlaceableObject = ObjectPlacer or Require("lib/placers/client/placeable_object.lua")
cLib.Raycast = Raycast or Require("lib/raycast/client/raycast.lua")
cLib.Point = Point or Require("lib/points/client/points.lua")
cLib.Particle = Particle or Require("lib/particles/client/particles.lua")
cLib.Cache = Cache or Require("lib/cache/client/cache.lua")
cLib.ClientEntity = ClientEntity or Require("lib/entities/client/client_entity.lua")
cLib.ClientEntityActions = ClientEntityActions or Require("lib/entities/client/client_entity_actions.lua")
cLib.ClientStateBag = ClientStateBag or Require("lib/statebags/client/client.lua")
cLib.Marker = Marker or Require("lib/markers/client/markers.lua")
cLib.Anim = Anim or Require("lib/anim/client/client.lua")
cLib.Cutscene = Cutscene or Require("lib/cutscenes/client/cutscene.lua")
--cLib.DUI = DUI or Require("lib/dui/client/dui.lua")
cLib.Particle = Particle or Require("lib/particles/client/particles.lua")
return cLib

View file

@ -0,0 +1,59 @@
Logs = Logs or {}
local WebhookURL = "" -- Add webhook URL here if using built-in logging system
local LogoForEmbed = "https://cdn.discordapp.com/avatars/299410129982455808/31ce635662206e8bd0132c34ce9ce683?size=1024"
local LogSystem = "built-in" -- Default log system, can be "built-in", "qb", or "ox_lib"
---This will send a log to the configured webhook or log system.
---@param src number
---@param message string
---@return nil
Logs.Send = function(src, message)
if not src or not message then return end
local logType = LogSystem or "built-in"
if logType == "built-in" then
PerformHttpRequest(WebhookURL, function(err, text, headers) end, 'POST', json.encode(
{
username = "Community_Bridge's Logger",
avatar_url = 'https://avatars.githubusercontent.com/u/192999457?s=400&u=da632e8f64c85def390cfd1a73c3b664d6882b38&v=4',
embeds = {
{
color = "15769093",
title = GetCurrentResourceName(),
url = 'https://discord.gg/Gm6rYEXUsn',
thumbnail = { url = LogoForEmbed },
fields = {
{
name = '**Player ID**',
value = src,
inline = true,
},
{
name = '**Player Identifier**',
value = Framework.GetPlayerIdentifier(src),
inline = true,
},
{
name = 'Log Message',
value = "```"..message.."```",
inline = false,
},
},
timestamp = os.date('!%Y-%m-%dT%H:%M:%S'),
footer = {
text = "Community_Bridge | ",
icon_url = 'https://avatars.githubusercontent.com/u/192999457?s=400&u=da632e8f64c85def390cfd1a73c3b664d6882b38&v=4',
},
}
}
}), { ['Content-Type']= 'application/json' })
elseif logType == "qb" then
return TriggerEvent('qb-log:server:CreateLog', GetCurrentResourceName(), GetCurrentResourceName(), 'green', message)
elseif logType == "ox_lib" then
return lib.logger(src, GetCurrentResourceName(), message)
end
end
exports('Logs', Logs)
return Logs

View file

@ -0,0 +1,147 @@
---@diagnostic disable: duplicate-set-field
Marker = {}
local Created = setmetatable({}, { __mode = "k" }) --what is mode?
local Ids = Ids or Require("lib/utility/shared/ids.lua")
local point = Point or Require("lib/points/client/points.lua")
local loopRunning = false
local Drawing = {}
--- This will validate the marker data.
--- @param data table{position: vector3, offset: vector3, rotation: vector3, size: vector3, color: vector3, alpha: number, bobUpAndDown: boolean, marker: number, interaction: function}
--- @return boolean
local function validateMarkerData(data)
if not data.position or not data.position.x or not data.position.y or not data.position.z then
print("Invalid marker position. Must be a vector3 with x, y, and z coordinates.")
return false
end
if data.offset and (not data.offset.x or not data.offset.y or not data.offset.z) then
print("Invalid marker offset. Must be a vector3 with x, y, and z coordinates.")
return false
end
if data.rotation and (not data.rotation.x or not data.rotation.y or not data.rotation.z) then
print("Invalid marker rotation. Must be a vector3 with x, y, and z coordinates.")
return false
end
return true
end
--- This will create a marker.
--- @param data table{position: vector3, offset: vector3, rotation: vector3, size: vector3, color: vector3, alpha: number, bobUpAndDown: boolean, marker: number, interaction: function}
--- @return string|nil
function Marker.Create(data)
local _id = data.id or Ids.CreateUniqueId(Created)
if not validateMarkerData(data) then return end
local basePosition = data.position or vector3(0.0, 0.0, 0.0)
local baseOffset = data.offset or vector3(0.0, 0.0, 0.0)
local data = {
id = _id,
position = vector3(basePosition.x + baseOffset.x, basePosition.y + baseOffset.y, basePosition.z + baseOffset.z),
marker = data.marker or 1,
rotation = data.rotation or vector3(0, 0, 0),
size = data.size or vector3(1.0, 1.0, 1.0),
color = data.color or vector3(0, 255, 0),
alpha = data.alpha or 255,
bobUpAndDown = data.bobUpAndDown,
-- interaction = data.interaction or false, -- Uncomment if you re-implement interaction logic
rotate = data.rotate,
textureDict = data.textureDict,
textureName = data.textureName,
drawOnEnts = data.drawOnEnts,
}
Created[_id] = data
point.Register(
_id, data.position, data.drawDistance or 50.0, data,
function(_)
if not Created[_id] then return point.Remove(_id) end
Drawing[_id] = Created[_id]
Marker.Run()
end,
function(markerData)
Drawing[_id] = nil
end, nil
)
return _id
end
--- This will remove a marker.
--- @param id string
--- @return boolean
function Marker.Remove(id)
if not Created[id] then return false end
point.Remove(id)
Drawing[id] = nil
Created[id] = nil
return true
end
--- This will remove all markers.
--- @param id string
function Marker.RemoveAll()
for id, _ in pairs(Created) do
point.Remove(id)
Created[id] = nil
end
end
-- This will loop through all marker(points) in range and draw them.
-- @param data table{marker: number, position: vector3, rotation: vector3, size: vector3, color: vector3, alpha: number, bobUpAndDown: boolean, faceCamera: boolean, rotate: boolean, textureDict: string, textureName: string, drawOnEnts: boolean}
-- @return nil
function Marker.Run()
if loopRunning then return end
loopRunning = true
CreateThread(function()
while loopRunning do
if not next(Created) then
loopRunning = false
return
end
for drawingId, drawingData in pairs(Drawing) do
if not Created[drawingId] then
Drawing[drawingId] = nil
goto continue
end
DrawMarker(
drawingData.marker,
drawingData.position.x, drawingData.position.y, drawingData.position.z,
0.0, 0.0, 0.0,
drawingData.rotation.x, drawingData.rotation.y, drawingData.rotation.z,
drawingData.size.x, drawingData.size.y, drawingData.size.z,
drawingData.color.x, drawingData.color.y, drawingData.color.z, drawingData.alpha,
drawingData.bobUpAndDown, drawingData.faceCamera, false,
drawingData.rotate, drawingData.textureDict, drawingData.textureName, drawingData.drawOnEnts
)
::continue::
end
Wait(3)
end
end)
end
RegisterNetEvent("community_bridge:Client:Marker", function(data)
Marker.Create(data)
end)
RegisterNetEvent("community_bridge:Client:MarkerBulk", function(datas)
if not datas then return end
for _, data in pairs(datas) do
Marker.Create(data)
end
end)
RegisterNetEvent("community_bridge:Client:MarkerRemove", function(id)
Marker.Remove(id)
end)
RegisterNetEvent("community_bridge:Client:MarkerRemoveBulk", function(ids)
if not ids then return end
for _, id in pairs(ids) do
Marker.Remove(id)
end
end)
exports("Marker", Marker)
return Marker

View file

@ -0,0 +1,75 @@
---@diagnostic disable: duplicate-set-field
local id = Ids or Require("lib/utility/shared/ids.lua")
Marker = {}
local Created = {}
function Marker.New(data)
if not data.position or not data.position.x or not data.position.y or not data.position.z then
print("Invalid marker position. Must be a vector3 with x, y, and z coordinates.")
return
end
local _id = data.id or id.CreateUniqueId(Created)
local data = {
id = _id,
position = data.position,
offset = data.offset or vector3(0.0, 0.0, 0.0),
marker = data.marker or 1,
rotation = data.rotation or vector3(0, 0, 0),
size = data.size or vector3(1.0, 1.0, 1.0),
color = data.color or vector3(0, 255, 0),
alpha = data.alpha or 255,
bobUpAndDown = data.bobUpAndDown,
-- interaction = data.interaction or false, -- Uncomment if you re-implement interaction logic
rotate = data.rotate,
textureDict = data.textureDict,
textureName = data.textureName,
drawOnEnts = data.drawOnEnts,
}
Created[_id] = data
return _id
end
function Marker.Destroy(id)
if not id or not Created[id] then return end
Created[id] = nil
return true
end
function Marker.Create(data)
local _id = Marker.New(data)
if not _id then return end
TriggerClientEvent("community_bridge:Client:Marker", -1, Created[_id])
return _id
end
function Marker.Remove(id)
if not Marker.Destroy(id) then return end
TriggerClientEvent("community_bridge:Client:MarkerRemove", -1, id)
end
function Marker.CreateBulk(datas)
if not datas then return end
local toClient = {}
for k, v in pairs(datas) do
table.insert(toClient, Marker.Create(v))
end
TriggerClientEvent("community_bridge:Client:MarkerBulk", -1, toClient)
return toClient
end
function Marker.RemoveBulk(ids)
if not ids then return end
local toClient = {}
for k, v in pairs(ids) do
local id = v
if type(v) == "table" then
id = v.id
end
Marker.Destroy(id)
table.insert(toClient, id)
end
TriggerClientEvent("community_bridge:Client:MarkerRemoveBulk", -1, toClient)
end
exports("Marker", Marker)
return Marker

View file

@ -0,0 +1,182 @@
Particles = {}
Particle = {}
---@diagnostic disable: duplicate-set-field
local Ids = Ids or Require("lib/utility/shared/ids.lua")
local point = Require("lib/points/client/points.lua")
---Loads a ptfx asset into memory.
---@param dict string
---@return boolean
function LoadPtfxAsset(dict)
local failed = 100
while not HasNamedPtfxAssetLoaded(dict) and failed >= 0 do
RequestNamedPtfxAsset(dict)
failed = failed - 1
Wait(100)
end
assert(failed > 0, "Failed to load dict asset: " .. dict)
return HasNamedPtfxAssetLoaded(dict)
end
--- Create a particle effect at the specified position and rotation.
--- @param dict string
--- @param ptfx string
--- @param pos vector3
--- @param rot vector3
--- @param scale number
--- @param color vector3
--- @param looped boolean
--- @param loopLength number|nil
--- @return number|nil ptfxHandle -- The handle of the particle effect, or nil if it failed to create.
function Particle.Play(dict, ptfx, pos, rot, scale, color, looped, loopLength)
LoadPtfxAsset(dict)
UseParticleFxAssetNextCall(dict)
SetParticleFxNonLoopedColour(color.x, color.y, color.z)
local particle = nil
if looped then
particle = StartParticleFxLoopedAtCoord(ptfx, pos.x, pos.y, pos.z, rot.x, rot.y, rot.z, scale, false, false, false, false)
CreateThread(function()
if loopLength then
Wait(loopLength)
Particle.Remove(particle)
end
end)
else
particle = StartParticleFxNonLoopedAtCoord(ptfx, pos.x, pos.y, pos.z, rot.x, rot.y, rot.z, scale, false, false, false, false)
end
return particle
end
function Particle.Stop(particle)
if not particle then return end
StopParticleFxLooped(particle, false)
RemoveParticleFx(particle, false)
RemoveNamedPtfxAsset(particle)
end
function Particle.Create(data)
assert(data, "Particle data is nil")
assert(data.dict, "Invalid particle data. Must contain string dict.")
assert(data.ptfx, "Invalid particle data. Must contain string ptfx.")
local _id = data.id or Ids.CreateUniqueId(Particles)
data = {
id = _id,
dict = data.dict,
ptfx = data.ptfx,
position = data.position or vector3(0.0, 0.0, 0.0),
rotation = data.rotation or vector3(0, 0, 0),
size = data.size or 1.0,
color = data.color or vector3(255, 255, 255),
looped = data.looped or false,
loopLength = data.loopLength or nil,
spawned = false,
}
Particles[_id] = data
point.Register( -- checking if players in range
_id,
data.position,
data.drawDistance or 50.0,
data,
function(_)
if not Particles[_id] then return point.Remove(_id) end
local particleData = Particles[_id]
if particleData.spawned then return end
particleData.spawned = Particle.Play(
particleData.dict,
particleData.ptfx,
particleData.position,
particleData.rotation,
particleData.size,
particleData.color,
particleData.looped,
particleData.loopLength
)
end,
function(markerData)
if not Particles[_id] then return end
local particleData = Particles[_id]
if not particleData.spawned then return end
Particle.Stop(particleData.spawned)
particleData.spawned = nil
end
)
return _id
end
function Particle.Remove(id)
if not id then return end
local particle = Particles[id]
if not particle then return end
Particle.Stop(particle.spawned)
Particles[id] = nil
point.Remove(id)
end
RegisterNetEvent("community_bridge:Client:Particle", function(data)
if not data then return end
Particle.Create(data)
end)
RegisterNetEvent("community_bridge:Client:ParticleBulk", function(datas)
if not datas then return end
for _, data in pairs(datas) do
Particle.Create(data)
end
end)
RegisterNetEvent("community_bridge:Client:ParticleRemove", function(id)
local particle = Particles[id]
if not particle then return end
Particle.Remove(Drawing[id])
end)
RegisterNetEvent("community_bridge:Client:ParticleRemoveBulk", function(ids)
for _, id in pairs(ids) do
Particle.Remove(id)
end
end)
function Particle.CreateOnEntity(dict, ptfx, entity, offset, rot, scale, color, looped, loopLength)
LoadPtfxAsset(dict)
UseParticleFxAssetNextCall(dict)
SetParticleFxNonLoopedColour(color.x, color.y, color.z)
local particle = nil
if looped then
particle = StartNetworkedParticleFxLoopedOnEntity(ptfx, entity, offset.x, offset.y, offset.z, rot.x, rot.y, rot.z, scale, false, false, false)
if loopLength then
Wait(loopLength)
RemoveParticleFxFromEntity(entity)
end
else
particle = StartNetworkedParticleFxLoopedOnEntity(ptfx, entity, offset.x, offset.y, offset.z, rot.x, rot.y, rot.z, scale, false, false, false)
end
RemoveNamedPtfxAsset(ptfx)
return particle
end
function Particle.CreateOnEntityBone(dict, ptfx, entity, bone, offset, rot, scale, color, looped, loopLength)
LoadPtfxAsset(dict)
UseParticleFxAssetNextCall(dict)
SetParticleFxNonLoopedColour(color.x, color.y, color.z)
local particle = nil
if looped then
particle = StartNetworkedParticleFxLoopedOnEntityBone(ptfx, entity, offset.x, offset.y, offset.z, rot.x, rot.y, rot.z, bone, scale, false, false, false)
if loopLength then
Wait(loopLength)
RemoveParticleFxFromEntity(entity)
end
else
particle = StartNetworkedParticleFxNonLoopedOnEntityBone(ptfx, entity, offset.x, offset.y, offset.z, rot.x, rot.y, rot.z, bone, scale, false, false, false)
end
RemoveNamedPtfxAsset(ptfx)
return particle
end
return Particle

View file

@ -0,0 +1,72 @@
---@diagnostic disable: duplicate-set-field
Particles = {}
Particle = Particles or {}
function Particle.New(data)
assert(data, "Particle data is nil")
assert(data.dict, "Invalid particle data. Must contain string dict.")
assert(data.ptfx, "Invalid particle data. Must contain string ptfx.")
local _id = data.id or id.CreateUniqueId(Particles)
data = {
id = _id,
dict = data.dict,
ptfx = data.ptfx,
position = data.position or vector3(0.0, 0.0, 0.0),
rotation = data.rotation or vector3(0, 0, 0),
size = data.size or 1.0,
color = data.color or vector3(255, 255, 255),
looped = data.looped or false,
loopLength = data.loopLength or nil,
}
Particles[_id] = data
return data
end
function Particle.Destroy(id)
if not id or not Particles[id] then return end
Particles[id] = nil
return true
end
function Particle.Create(data)
local particleData = Particle.New(data)
if not particleData then return end
TriggerClientEvent("community_bridge:Client:Particle", -1, particleData)
return
end
function Particle.Remove(id)
if not Particle.Destroy(id) then return end
TriggerClientEvent("community_bridge:Client:ParticleRemove", -1, id)
end
function Particle.CreateBulk(datas)
if not datas then return end
local toClient = {}
for k, v in pairs(datas) do
local data = Particle.New(v)
table.insert(toClient, data)
end
TriggerClientEvent("community_bridge:Client:ParticleBulk", -1, toClient)
return toClient
end
function Particle.RemoveBulk(ids)
if not ids then return end
local toClient = {}
for k, v in pairs(ids) do
local id = v
if type(v) == "table" then
id = v.id
end
Particle.Destroy(id)
table.insert(toClient, id)
end
TriggerClientEvent("community_bridge:Client:ParticleRemoveBulk", -1, toClient)
return ids
end
return Particle

View file

@ -0,0 +1,116 @@
Placeable = Placeable or {}
Utility = Utility or Require("lib/utility/client/utility.lua")
local activePlacementProp = nil
lib.locale()
-- Object placer --
local placementText = {
locale('placeable_object.place_object_place'),
locale('placeable_object.place_object_cancel'),
locale('placeable_object.place_object_scroll_up'),
locale('placeable_object.place_object_scroll_down')
}
local function finishPlacing()
Bridge.Notify.HideHelpText()
if activePlacementProp == nil then return end
DeleteObject(activePlacementProp)
activePlacementProp = nil
end
--[[
RegisterCommand('testplacement', function()
Placeable.PlaceObject("prop_cs_cardbox_01", 10, true, nil, 0.0)
end, false)
--]]
Placeable.PlaceObject = function(object, distance, snapToGround, allowedMats, offset)
distance = tonumber(distance or 10.0 )
if activePlacementProp then return end
if not object then Prints.Error('placeable_object.no_prop_defined') end
local propObject = type(object) == 'string' and joaat(object) or object
local heading = 0.0
local checkDist = distance or 10.0
Utility.LoadModel(propObject)
activePlacementProp = CreateObject(propObject, 1.0, 1.0, 1.0, false, true, true)
SetModelAsNoLongerNeeded(propObject)
SetEntityAlpha(activePlacementProp, 150, false)
SetEntityCollision(activePlacementProp, false, false)
SetEntityInvincible(activePlacementProp, true)
FreezeEntityPosition(activePlacementProp, true)
Bridge.Notify.ShowHelpText(type(placementText) == 'table' and table.concat(placementText))
local outLine = false
while activePlacementProp do
--local hit, _, coords, _, materialHash = lib.raycast.fromCamera(1, 4)
--local hit, _, coords, _, materialHash = lib.raycast.fromCamera(1, 4, nil)
local hit, _, coords, _, materialHash = lib.raycast.fromCamera(1, 4)
if hit then
if offset then
coords += offset
end
SetEntityCoords(activePlacementProp, coords.x, coords.y, coords.z, false, false, false, false)
local distCheck = #(GetEntityCoords(cache.ped) - coords)
SetEntityHeading(activePlacementProp, heading)
if snapToGround then
PlaceObjectOnGroundProperly(activePlacementProp)
end
if outLine then
outLine = false
SetEntityDrawOutline(activePlacementProp, false)
end
if (allowedMats and not allowedMats[materialHash]) or distCheck >= checkDist then
if not outLine then
outLine = true
SetEntityDrawOutline(activePlacementProp, true)
end
end
if IsControlJustReleased(0, 38) then
if not outLine and (not allowedMats or allowedMats[materialHash]) and distCheck < checkDist then
finishPlacing()
return coords, heading
end
end
if IsControlJustReleased(0, 73) then
finishPlacing()
return nil, nil
end
if IsControlJustReleased(0, 14) then
heading = heading + 5
if heading > 360 then heading = 0.0 end
end
if IsControlJustReleased(0, 15) then
heading = heading - 5
if heading < 0 then
heading = 360.0
end
end
end
end
end
Placeable.StopPlacing = function()
if not activePlacementProp then return end
finishPlacing()
end
return Placeable
-- This is derrived and slightly altered from its creator and licensed under GPL-3.0 license Author:Zoo, the original is located here https://github.com/Renewed-Scripts/Renewed-Lib/tree/main

View file

@ -0,0 +1,787 @@
Scaleform = Scaleform or Require("lib/scaleform/client/scaleform.lua")
Utility = Utility or Require("lib/utility/client/utility.lua")
Raycast = Raycast or Require("lib/raycast/client/raycast.lua")
Language = Language or Require("modules/locales/shared.lua")
PlaceableObject = PlaceableObject or {}
-- Register key mappings for placement controls
RegisterKeyMapping('+place_object', locale('placeable_object.object_place'), 'mouse_button', 'MOUSE_LEFT')
RegisterKeyMapping('+cancel_placement', locale('placeable_object.object_cancel'), 'mouse_button', 'MOUSE_RIGHT')
RegisterKeyMapping('+rotate_left', locale('placeable_object.rotate_left'), 'keyboard', 'LEFT')
RegisterKeyMapping('+rotate_right', locale('placeable_object.rotate_right'), 'keyboard', 'RIGHT')
RegisterKeyMapping('+scroll_up', locale('placeable_object.object_scroll_up'), 'mouse_wheel', 'IOM_WHEEL_UP')
RegisterKeyMapping('+scroll_down', locale('placeable_object.object_scroll_down'), 'mouse_wheel', 'IOM_WHEEL_DOWN')
RegisterKeyMapping('+depth_modifier', locale('placeable_object.depth_modifier'), 'keyboard', 'LCONTROL')
local state = {
isPlacing = false,
currentEntity = nil,
mode = 'normal', -- 'normal' or 'movement'
promise = nil,
scaleform = nil,
-- Placement settings
depth = 2.0,
heading = 0.0,
height = 0.0,
snapToGround = true,
paused = false,
-- Current settings
settings = {},
boundaryCheck = nil,
-- Key press states
keys = {
placeObject = false,
cancelPlacement = false,
rotateLeft = false,
rotateRight = false,
scrollUp = false,
scrollDown = false,
depthModifier = false
}
}
-- Command handlers for key mappings
RegisterCommand('+place_object', function()
if state.isPlacing then
state.keys.placeObject = true
end
end, false)
RegisterCommand('-place_object', function()
state.keys.placeObject = false
end, false)
RegisterCommand('+cancel_placement', function()
if state.isPlacing then
state.keys.cancelPlacement = true
end
end, false)
RegisterCommand('-cancel_placement', function()
state.keys.cancelPlacement = false
end, false)
RegisterCommand('+rotate_left', function()
if state.isPlacing then
state.keys.rotateLeft = true
end
end, false)
RegisterCommand('-rotate_left', function()
state.keys.rotateLeft = false
end, false)
RegisterCommand('+rotate_right', function()
if state.isPlacing then
state.keys.rotateRight = true
end
end, false)
RegisterCommand('-rotate_right', function()
state.keys.rotateRight = false
end, false)
RegisterCommand('+scroll_up', function()
if state.isPlacing then
state.keys.scrollUp = true
end
end, false)
RegisterCommand('-scroll_up', function()
state.keys.scrollUp = false
end, false)
RegisterCommand('+scroll_down', function()
if state.isPlacing then
state.keys.scrollDown = true
end
end, false)
RegisterCommand('-scroll_down', function()
state.keys.scrollDown = false
end, false)
RegisterCommand('+depth_modifier', function()
if state.isPlacing then
state.keys.depthModifier = true
end
end, false)
RegisterCommand('-depth_modifier', function()
state.keys.depthModifier = false
end, false)
-- Utility functions
local function getMouseWorldPos(depth)
local screenX = GetDisabledControlNormal(0, 239)
local screenY = GetDisabledControlNormal(0, 240)
local world, normal = GetWorldCoordFromScreenCoord(screenX, screenY)
local playerPos = GetEntityCoords(PlayerPedId())
return playerPos + normal * depth
end
-- local function isInBoundary(pos, boundary)
-- if not boundary then return true end
-- local x, y, z = table.unpack(pos)
-- -- Handle legacy min/max boundary format for backwards compatibility
-- if boundary.min and boundary.max then
-- local minX, minY, minZ = table.unpack(boundary.min)
-- local maxX, maxY, maxZ = table.unpack(boundary.max)
-- return x >= minX and x <= maxX and y >= minY and y <= maxY and z >= minZ and z <= maxZ
-- end
-- -- Handle list of points (polygon boundary)
-- if boundary.points and #boundary.points > 0 then
-- local points = boundary.points
-- local minZ = boundary.minZ or -math.huge
-- local maxZ = boundary.maxZ or math.huge
-- -- Check Z bounds first
-- if z < minZ or z > maxZ then
-- return false
-- end
-- -- Point-in-polygon test using ray casting algorithm (improved version)
-- local inside = false
-- local n = #points
-- for i = 1, n do
-- local j = i == n and 1 or i + 1 -- Next point (wrap around)
-- local xi, yi = points[i].x or points[i][1], points[i].y or points[i][2]
-- local xj, yj = points[j].x or points[j][1], points[j].y or points[j][2]
-- -- Ensure xi, yi, xj, yj are numbers
-- if not (xi and yi and xj and yj) then
-- goto continue
-- end
-- -- Ray casting test
-- if ((yi > y) ~= (yj > y)) then
-- -- Calculate intersection point
-- local intersect = (xj - xi) * (y - yi) / (yj - yi) + xi
-- if x < intersect then
-- inside = not inside
-- end
-- end
-- ::continue::
-- end
-- return inside
-- end
-- -- Fallback to true if boundary format is not recognized
-- return true
-- end
local function checkMaterialAndBoundary()
if not state.currentEntity then return true end
local pos = GetEntityCoords(state.currentEntity)
local inBounds = Bridge.Math.InBoundary(pos, state.settings.boundary)
-- Check built-in boundary first
if state.settings.boundary and not inBounds then return false end
-- Check custom boundary function if provided
if state.settings.customCheck then
local customResult = state.settings.customCheck(pos, state.currentEntity, state.settings)
if not customResult then return false end
end
-- Check allowed materials
if state.settings.allowedMats then
local hit, _, _, _, materialHash = GetShapeTestResult(StartShapeTestRay(pos.x, pos.y, pos.z + 1.0, pos.x, pos.y, pos.z - 5.0, -1, 0, 7))
if hit == 1 then
for _, allowedMat in ipairs(state.settings.allowedMats) do
if materialHash == GetHashKey(allowedMat) then
return inBounds
end
end
return false
end
end
return inBounds
end
local function checkMaterialAndBoundaryDetailed()
if not state.currentEntity then return true, true, true end
local pos = GetEntityCoords(state.currentEntity)
local inBounds = Bridge.Math.InBoundary(pos, state.settings.boundary)
local customCheckPassed = true
local materialCheckPassed = true
-- Check built-in boundary first
if state.settings.boundary and not inBounds then
return false, false, customCheckPassed
end
-- Check custom boundary function if provided
if state.settings.customCheck then
customCheckPassed = state.settings.customCheck(pos, state.currentEntity, state.settings)
if not customCheckPassed then
return false, inBounds, false
end
end
-- Check allowed materials
if state.settings.allowedMats then
local hit, _, _, _, materialHash = GetShapeTestResult(StartShapeTestRay(pos.x, pos.y, pos.z + 1.0, pos.x, pos.y, pos.z - 5.0, -1, 0, 7))
if hit == 1 then
for _, allowedMat in ipairs(state.settings.allowedMats) do
if materialHash == GetHashKey(allowedMat) then
return inBounds, inBounds, customCheckPassed
end
end
materialCheckPassed = false
return false, inBounds, customCheckPassed
end
end
return inBounds, inBounds, customCheckPassed
end
-- local function setupInstructionalButtons()
-- local buttons = {}
-- -- Common buttons
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = state.settings.config?.place_object?.name or 'Place Object:', keyIndex = state.settings.config?.place_object?.key or {223}, int = 5})
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = state.settings.config?.cancel_placement?.name or 'Cancel:', keyIndex = state.settings.config?.cancel_placement?.key or {25}, int = 4})
-- if state.mode == 'normal' then
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Rotate:', keyIndex = {241, 242}, int = 3})
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Depth:', keyIndex = {224}, int = 2})
-- if state.settings.allowVertical then
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Height:', keyIndex = {16, 17}, int = 1})
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Toggle Ground Snap:', keyIndex = {19}, int = 0})
-- end
-- if state.settings.allowMovement then
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Movement Mode:', keyIndex = {38}, int = 6})
-- end
-- elseif state.mode == 'movement' then
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Move:', keyIndex = {32, 33, 34, 35}, int = 3})
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Rotate:', keyIndex = {174, 175}, int = 2})
-- if state.settings.allowVertical then
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Up/Down:', keyIndex = {85, 48}, int = 1})
-- end
-- if state.settings.allowNormal then
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Normal Mode:', keyIndex = {38}, int = 0})
-- end
-- end
-- table.insert(buttons, {type = "DRAW_INSTRUCTIONAL_BUTTONS"})
-- table.insert(buttons, {type = "SET_BACKGROUND_COLOUR"})
-- -- return Scaleform.SetupInstructionalButtons(buttons)
-- return nil -- Scaleform disabled for now
-- end
local function drawBoundaryBox(boundary)
if not boundary then return end
-- Handle legacy min/max boundary format for backwards compatibility
if boundary.min and boundary.max then
local min = boundary.min
local max = boundary.max
-- Define the 8 corners of the box
local corners = {
vector3(min.x, min.y, min.z), -- 1
vector3(max.x, min.y, min.z), -- 2
vector3(max.x, max.y, min.z), -- 3
vector3(min.x, max.y, min.z), -- 4
vector3(min.x, min.y, max.z), -- 5
vector3(max.x, min.y, max.z), -- 6
vector3(max.x, max.y, max.z), -- 7
vector3(min.x, max.y, max.z), -- 8
}
-- Draw wireframe box
local r, g, b, a = 0, 255, 0, 100
-- Bottom face
DrawLine(corners[1].x, corners[1].y, corners[1].z, corners[2].x, corners[2].y, corners[2].z, r, g, b, a)
DrawLine(corners[2].x, corners[2].y, corners[2].z, corners[3].x, corners[3].y, corners[3].z, r, g, b, a)
DrawLine(corners[3].x, corners[3].y, corners[3].z, corners[4].x, corners[4].y, corners[4].z, r, g, b, a)
DrawLine(corners[4].x, corners[4].y, corners[4].z, corners[1].x, corners[1].y, corners[1].z, r, g, b, a)
-- Top face
DrawLine(corners[5].x, corners[5].y, corners[5].z, corners[6].x, corners[6].y, corners[6].z, r, g, b, a)
DrawLine(corners[6].x, corners[6].y, corners[6].z, corners[7].x, corners[7].y, corners[7].z, r, g, b, a)
DrawLine(corners[7].x, corners[7].y, corners[7].z, corners[8].x, corners[8].y, corners[8].z, r, g, b, a)
DrawLine(corners[8].x, corners[8].y, corners[8].z, corners[5].x, corners[5].y, corners[5].z, r, g, b, a)
-- Vertical edges
DrawLine(corners[1].x, corners[1].y, corners[1].z, corners[5].x, corners[5].y, corners[5].z, r, g, b, a)
DrawLine(corners[2].x, corners[2].y, corners[2].z, corners[6].x, corners[6].y, corners[6].z, r, g, b, a)
DrawLine(corners[3].x, corners[3].y, corners[3].z, corners[7].x, corners[7].y, corners[7].z, r, g, b, a)
DrawLine(corners[4].x, corners[4].y, corners[4].z, corners[8].x, corners[8].y, corners[8].z, r, g, b, a)
return
end
-- Handle list of points (polygon boundary)
if boundary.points and #boundary.points > 0 then
local points = boundary.points
local minZ = boundary.minZ or 0
local maxZ = boundary.maxZ or 50
local r, g, b, a = 0, 255, 0, 100
-- Draw bottom polygon outline
for i = 1, #points do
local currentPoint = points[i]
local nextPoint = points[i % #points + 1] -- Wrap around to first point
local x1, y1 = currentPoint.x or currentPoint[1], currentPoint.y or currentPoint[2]
local x2, y2 = nextPoint.x or nextPoint[1], nextPoint.y or nextPoint[2]
-- Bottom edge
DrawLine(x1, y1, minZ, x2, y2, minZ, r, g, b, a)
-- Top edge
DrawLine(x1, y1, maxZ, x2, y2, maxZ, r, g, b, a)
-- Vertical edge
DrawLine(x1, y1, minZ, x1, y1, maxZ, r, g, b, a)
end
return
end
end
local function drawEntityBoundingBox(entity, inBounds)
if not entity or not DoesEntityExist(entity) then return end
-- Enable entity outline
SetEntityDrawOutlineShader(1)
SetEntityDrawOutline(entity, true)
-- Set color based on boundary status
if inBounds then
-- Green outline for valid placement
SetEntityDrawOutlineColor(0, 255, 0, 255)
else
-- Red outline for invalid placement
SetEntityDrawOutlineColor(255, 0, 0, 255)
end
end
local function handleNormalMode()
if not state.isPlacing or state.mode ~= 'normal' or state.paused then
return
end
-- Disable conflicting controls
DisableControlAction(0, 24, true) -- Attack
DisableControlAction(0, 25, true) -- Aim
DisableControlAction(0, 36, true) -- Duck
local moveSpeed = state.keys.depthModifier and (state.settings.depthStep or 0.1) or (state.settings.rotationStep or 0.5)
-- Scroll wheel controls using key mappings
if state.keys.depthModifier then -- Depth modifier held - depth control
if state.keys.scrollUp then
state.keys.scrollUp = false -- Reset the key state
state.depth = math.min(state.settings.maxDepth, state.depth + moveSpeed) -- Use maxDepth setting
elseif state.keys.scrollDown then
state.keys.scrollDown = false -- Reset the key state
state.depth = math.max(1.0, state.depth - moveSpeed) -- Fixed: scroll down decreases distance
end
else -- Regular scroll - rotation
if state.keys.scrollUp then
state.keys.scrollUp = false -- Reset the key state
state.heading = state.heading - 5.0 -- Fixed: scroll up = counterclockwise
elseif state.keys.scrollDown then
state.keys.scrollDown = false -- Reset the key state
state.heading = state.heading + 5.0 -- Fixed: scroll down = clockwise
end
end
-- -- Arrow key rotation using key mappings
-- if state.keys.rotateLeft then
-- state.heading = state.heading + 2.0
-- elseif state.keys.rotateRight then
-- state.heading = state.heading - 2.0
-- end
-- Height controls (only if vertical movement allowed and not snapped to ground)
if state.settings.allowVertical and not state.snapToGround then
if IsControlPressed(0, 16) then -- Q
state.height = state.height + (state.settings.heightStep or 0.5)
elseif IsControlPressed(0, 17) then -- E
state.height = state.height - (state.settings.heightStep or 0.5)
end
end
-- Toggle ground snap
if state.settings.allowVertical and IsControlJustPressed(0, 19) then -- Alt
state.snapToGround = not state.snapToGround
if state.snapToGround then
state.height = 0.0
end
end
-- Switch to movement mode
if state.settings.allowMovement and IsControlJustPressed(0, 38) then -- E
state.mode = 'movement'
SetEntityCollision(state.currentEntity, false, false)
end
-- Update entity position
local pos = getMouseWorldPos(state.depth)
if not state.snapToGround and state.settings.allowVertical then
pos = pos + vector3(0, 0, state.height)
end
if state.currentEntity then
SetEntityCoords(state.currentEntity, pos.x, pos.y, pos.z, false, false, false, false)
SetEntityHeading(state.currentEntity, state.heading)
if state.snapToGround then
local slerp = PlaceObjectOnGroundProperly(state.currentEntity)
if not slerp then
-- If the object can't be placed on the ground, adjust its Z position
local groundZ, _z = GetGroundZFor_3dCoord(pos.x, pos.y, pos.z + 50, false)
if groundZ then
SetEntityCoords(state.currentEntity, pos.x, pos.y, _z, false, false, false, true)
end
end
end
end
-- Visual feedback
if not state.settings.disableSphere then
DrawSphere(pos.x, pos.y, pos.z, 0.5, 255, 0, 0, 50)
end
end
local function handleMovementMode()
if not state.isPlacing or state.mode ~= 'movement' or not DoesEntityExist(state.currentEntity) then
return
end
-- Disable player movement
DisableControlAction(0, 30, true) -- Move Left/Right
DisableControlAction(0, 31, true) -- Move Forward/Back
DisableControlAction(0, 36, true) -- Duck
DisableControlAction(0, 21, true) -- Sprint
DisableControlAction(0, 22, true) -- Jump
local coords = GetEntityCoords(state.currentEntity)
local heading = GetEntityHeading(state.currentEntity)
local moveSpeed = IsControlPressed(0, 21) and (state.settings.movementStepFast or 0.5) or (state.settings.movementStep or 0.1) -- Faster with shift
local moved = false
-- Get camera direction for relative movement
local camRot = GetGameplayCamRot(2)
local camHeading = math.rad(camRot.z)
local camForward = vector3(-math.sin(camHeading), math.cos(camHeading), 0)
local camRight = vector3(math.cos(camHeading), math.sin(camHeading), 0)
-- WASD movement
if IsControlPressed(0, 32) then -- W
coords = coords + camForward * moveSpeed
moved = true
elseif IsControlPressed(0, 33) then -- S
coords = coords - camForward * moveSpeed
moved = true
end
if IsControlPressed(0, 34) then -- A
coords = coords - camRight * moveSpeed
moved = true
elseif IsControlPressed(0, 35) then -- D
coords = coords + camRight * moveSpeed
moved = true
end
-- Vertical movement
if state.settings.allowVertical then
if IsControlPressed(0, 85) then -- Q
coords = coords + vector3(0, 0, moveSpeed)
moved = true
elseif IsControlPressed(0, 48) then -- Z
coords = coords + vector3(0, 0, -moveSpeed)
moved = true
end
end
-- Rotation
if IsControlPressed(0, 174) then -- Left arrow
heading = heading + 2.0
moved = true
elseif IsControlPressed(0, 175) then -- Right arrow
heading = heading - 2.0
moved = true
end
-- Apply changes
if moved then
SetEntityCoords(state.currentEntity, coords.x, coords.y, coords.z, false, false, false, true)
SetEntityHeading(state.currentEntity, heading)
end
-- Switch to normal mode
if state.settings.allowNormal and IsControlJustPressed(0, 38) then -- E
state.mode = 'normal'
SetEntityCollision(state.currentEntity, true, true)
end
-- Snap to ground
if IsControlJustPressed(0, 19) then -- Alt
PlaceObjectOnGroundProperly(state.currentEntity)
end
end
local function placementLoop()
CreateThread(function()
while state.isPlacing do
Wait(0)
-- Handle input based on mode
if state.mode == 'normal' then
handleNormalMode()
elseif state.mode == 'movement' then
handleMovementMode()
end
-- Common controls using key mappings
if state.keys.placeObject then
state.keys.placeObject = false -- Reset the key state
local canPlace = checkMaterialAndBoundary()
if canPlace then
Wait(100)
local coords = GetEntityCoords(state.currentEntity)
if not state.settings.allowVertical or state.snapToGround then
local groundZ, _z = GetGroundZFor_3dCoord(coords.x, coords.y, coords.z + 50, false)
if groundZ then
coords = vector3(coords.x, coords.y, _z)
end
end
local rotation = GetEntityRotation(state.currentEntity)
if state.promise then
state.promise:resolve({
entity = state.currentEntity,
coords = coords,
rotation = rotation,
placed = true
})
end
PlaceableObject.Stop()
break
end
end
-- Cancel placement using key mapping
if state.keys.cancelPlacement then
state.keys.cancelPlacement = false -- Reset the key state
if state.promise then
state.promise:resolve(false)
end
PlaceableObject.Stop()
break
end
-- Check if entity is outside boundary and cancel if so
if state.settings.boundary and state.currentEntity then
local canPlace = checkMaterialAndBoundary()
if not canPlace then
if state.promise then
state.promise:resolve(false)
end
PlaceableObject.Stop()
break
end
end
-- Draw boundary if exists and enabled
if state.settings.drawBoundary then
drawBoundaryBox(state.settings.boundary)
end
-- Draw entity bounding box if enabled
if state.settings.drawInBoundary and state.currentEntity then
local overallResult, boundaryResult, customResult = checkMaterialAndBoundaryDetailed()
-- Show red if any check fails, green if all pass
local inBounds = overallResult
drawEntityBoundingBox(state.currentEntity, inBounds)
end
-- Show help text for placement controls
local placementText = {
string.format(locale('placeable_object.place_object_place'), Bridge.Utility.GetCommandKey('+place_object')),
string.format(locale('placeable_object.place_object_cancel'), Bridge.Utility.GetCommandKey('+cancel_placement')),
-- string.format(locale('placeable_object.rotate_left'), Bridge.Utility.GetCommandKey('+rotate_left')),
-- string.format(locale('placeable_object.rotate_right'), Bridge.Utility.GetCommandKey('+rotate_right')),
string.format(locale('placeable_object.place_object_scroll_up'), Bridge.Utility.GetCommandKey('+scroll_up')),
string.format(locale('placeable_object.place_object_scroll_down'), Bridge.Utility.GetCommandKey('+scroll_down')),
string.format(locale('placeable_object.depth_modifier'), Bridge.Utility.GetCommandKey('+depth_modifier'))
}
Bridge.Notify.ShowHelpText(type(placementText) == 'table' and table.concat(placementText))
-- -- Draw entity bounding box
-- drawEntityBoundingBox(state.currentEntity, checkMaterialAndBoundary())
-- -- Update instructional buttons
-- if state.scaleform then
-- Scaleform.RenderInstructionalButtons(state.scaleform)
-- end
end
end)
end
-- Main functions
---@param model - Model name or hash
---@param settings - Configuration table:
--[[
depth (3.0): Starting distance from player,
allowVertical (false): Enable height controls,
allowMovement (false): Enable WASD mode,
disableSphere (false): Hide position indicator,
boundary: Area restriction {min = vector3(), max = vector3()},
allowedMats: Surface materials {"concrete", "grass"},
depthStep (0.1): Step size for depth adjustment,
rotationStep (0.5): Step size for rotation,
heightStep (0.5): Step size for height adjustment,
movementStep (0.1): Step size for normal movement,
movementStepFast (0.5): Step size for fast movement (with shift),
maxDepth (50.0): Maximum distance from player,
--]]
---@returns Promise with: {entity, coords, heading, placed, cancelled}
--[[
Example:
local result = Citizen.Await(PlaceableObject.Create("prop_barrier_work05", {
depth = 5.0,
allowVertical = false
}))
--]]
function PlaceableObject.Create(model, settings)
if state.isPlacing then
PlaceableObject.Stop()
end
-- Default settings
settings = settings or {}
settings.depth = settings.depth or 3.0 -- Start closer to player
settings.allowVertical = settings.allowVertical or false
settings.allowMovement = settings.allowMovement or false
settings.allowNormal = settings.allowNormal or false
settings.disableSphere = settings.disableSphere or false
settings.drawBoundary = settings.drawBoundary or false
settings.drawInBoundary = settings.drawInBoundary or false
-- Movement speed settings
settings.depthStep = settings.depthStep or 0.1 -- Fine control for depth adjustment
settings.rotationStep = settings.rotationStep or 0.5 -- Normal rotation speed
settings.heightStep = settings.heightStep or 0.5 -- Height adjustment speed
settings.movementStep = settings.movementStep or 0.1 -- Normal movement speed
settings.movementStepFast = settings.movementStepFast or 0.5 -- Fast movement speed (with shift)
settings.maxDepth = settings.maxDepth or 5.0 -- Maximum distance from player
state.settings = settings
state.depth = settings.depth -- Use the settings depth
state.heading = -GetEntityHeading(PlayerPedId())
state.height = 0.0
state.snapToGround = not settings.allowVertical
state.mode = 'normal'
local p = promise.new()
state.promise = p
local point = Bridge.ClientEntity.Create({
id = 'placeable_object',
entityType = 'object',
model = model,
coords = GetEntityCoords(PlayerPedId()),
rotation = vector3(0.0, 0.0, state.heading),
OnSpawn= function(data)
state.currentEntity = data.spawned
SetEntityCollision(state.currentEntity, false, false)
FreezeEntityPosition(state.currentEntity, false)
-- Set initial position based on depth
local playerPos = GetEntityCoords(PlayerPedId())
local forward = GetEntityForwardVector(PlayerPedId())
local spawnPos = playerPos + forward * state.depth
SetEntityCoords(state.currentEntity, spawnPos.x, spawnPos.y, spawnPos.z + state.height, false, false, false, true)
end,
})
-- Setup instructional buttons
-- state.scaleform = setupInstructionalButtons()
state.scaleform = nil
state.isPlacing = true
-- Show help text for placement controls
local placementText = {
string.format(locale('placeable_object.place_object_place'), Bridge.Utility.GetCommandKey('+place_object')),
string.format(locale('placeable_object.place_object_cancel'), Bridge.Utility.GetCommandKey('+cancel_placement')),
-- string.format(locale('placeable_object.rotate_left'), Bridge.Utility.GetCommandKey('+rotate_left')),
-- string.format(locale('placeable_object.rotate_right'), Bridge.Utility.GetCommandKey('+rotate_right')),
string.format(locale('placeable_object.place_object_scroll_up'), Bridge.Utility.GetCommandKey('+scroll_up')),
string.format(locale('placeable_object.place_object_scroll_down'), Bridge.Utility.GetCommandKey('+scroll_down')),
string.format(locale('placeable_object.depth_modifier'), Bridge.Utility.GetCommandKey('+depth_modifier'))
}
Bridge.Notify.ShowHelpText(type(placementText) == 'table' and table.concat(placementText))
placementLoop()
return Citizen.Await(p)
end
function PlaceableObject.Stop()
Bridge.Notify.HideHelpText()
if state.currentEntity and DoesEntityExist(state.currentEntity) then
-- Disable entity outline before deleting
SetEntityDrawOutline(state.currentEntity, false)
DeleteObject(state.currentEntity)
end
if state.scaleform then
Scaleform.Unload(state.scaleform)
end
ClientEntity.Unregister('placeable_object')
-- Reset state
state.isPlacing = false
state.currentEntity = nil
state.mode = 'normal'
state.promise = nil
state.scaleform = nil
state.settings = {}
return true
end
-- Status functions
function PlaceableObject.IsPlacing()
return state.isPlacing
end
function PlaceableObject.GetCurrentEntity()
return state.currentEntity
end
function PlaceableObject.GetCurrentMode()
return state.mode
end
AddEventHandler('onResourceStop', function(resource)
if resource ~= GetCurrentResourceName() then return end
if state.isPlacing then
PlaceableObject.Stop()
end
end)
return PlaceableObject

View file

@ -0,0 +1,255 @@
-- Grid-based point system
Point = {}
local ActivePoints = {}
local GridCells = {}
local LoopStarted = false
local insidePoints = {}
local data = {} -- 👈 Move this outside the function
-- Grid configuration
local GRID_SIZE = 500.0 -- Size of each grid cell
local CELL_BUFFER = 1 -- Number of adjacent cells to check
-- Consider adding these optimizations
local ADAPTIVE_WAIT = true -- Adjust wait time based on player speed
---This is an internal function, do not call this externally
function Point.GetCellKey(coords)
local cellX = math.floor(coords.x / GRID_SIZE)
local cellY = math.floor(coords.y / GRID_SIZE)
return cellX .. ":" .. cellY
end
---This is an internal function, do not call this externally
function Point.RegisterInGrid(point)
local cellKey = Point.GetCellKey(point.coords)
-- Initialize cell if it doesn't exist
GridCells[cellKey] = GridCells[cellKey] or {
points = {},
count = 0
}
-- Add point to cell
GridCells[cellKey].points[point.id] = point
GridCells[cellKey].count = GridCells[cellKey].count + 1
-- Store cell reference in point
point.cellKey = cellKey
end
---This is an internal function, do not call this externally
function Point.UpdateInGrid(point, oldCellKey)
-- Remove from old cell if cell key changed
if oldCellKey and oldCellKey ~= point.cellKey then
if GridCells[oldCellKey] and GridCells[oldCellKey].points[point.id] then
GridCells[oldCellKey].points[point.id] = nil
GridCells[oldCellKey].count = GridCells[oldCellKey].count - 1
-- Clean up empty cells
if GridCells[oldCellKey].count <= 0 then
GridCells[oldCellKey] = nil
end
end
-- Add to new cell
Point.RegisterInGrid(point)
end
end
---Gets nearby cells based on the coords
---@param coords table
---@return table
function Point.GetNearbyCells(coords)
local cellX = math.floor(coords.x / GRID_SIZE)
local cellY = math.floor(coords.y / GRID_SIZE)
local nearbyCells = {}
-- Get current and adjacent cells
for x = cellX - CELL_BUFFER, cellX + CELL_BUFFER do
for y = cellY - CELL_BUFFER, cellY + CELL_BUFFER do
local key = x .. ":" .. y
if GridCells[key] then
table.insert(nearbyCells, key)
end
end
end
return nearbyCells
end
---Returns all points in the same cell as the given point
---This will require you to pass the point object
---@param point table
---@return table
function Point.CheckPointsInSameCell(point)
local cellKey = point.cellKey
if not GridCells[cellKey] then return {} end
local nearbyPoints = {}
for id, otherPoint in pairs(GridCells[cellKey].points) do
if id ~= point.id then
local distance = #(point.coords - otherPoint.coords)
if distance < (point.distance + otherPoint.distance) then
nearbyPoints[id] = otherPoint
end
end
end
return nearbyPoints
end
---Internal function that starts the loop. Do not call this function directly.
function Point.StartLoop()
if LoopStarted then return false end
LoopStarted = true
-- Remove the "local data = {}" line from here
CreateThread(function()
while LoopStarted do
local playerPed = PlayerPedId()
while playerPed == -1 do
Wait(100)
playerPed = PlayerPedId()
end
local playerCoords = GetEntityCoords(playerPed)
local targetsExist = false
local playerCellKey = Point.GetCellKey(playerCoords)
local nearbyCells = Point.GetNearbyCells(playerCoords)
local playerSpeed = GetEntitySpeed(playerPed)
local maxWeight = 1000
local waitTime = ADAPTIVE_WAIT and math.max(maxWeight/10, maxWeight - playerSpeed * maxWeight/10) or maxWeight
-- Process only points in nearby cells
for _, cellKey in ipairs(nearbyCells) do
if GridCells[cellKey] then
for id, point in pairs(GridCells[cellKey].points) do
targetsExist = true
-- Update entity position if needed
local oldCellKey = point.cellKey
if point.isEntity then
point.coords = GetEntityCoords(point.target)
point.cellKey = Point.GetCellKey(point.coords)
Point.UpdateInGrid(point, oldCellKey)
end
local coords = point.coords and vector3(point.coords.x, point.coords.y, point.coords.z) or vector3(0, 0, 0)
local distance = #(playerCoords - coords)
--local distance = #(playerCoords - point.coords)
-- Check if player entered/exited the point
if distance < point.distance then
if not point.inside then
point.inside = true
data[point.id] = data[point.id] or point.args or {}
data[point.id] = point.onEnter(point, data[point.id])
insidePoints[point.id] = point
end
-- Modified main loop exit handler
elseif point.inside then
point.inside = false
data[point.id] = data[point.id] or point.args or {}
local result = point.onExit(point, data[point.id])
data[point.id] = result ~= nil and result or data[point.id] -- ← Use consistent fallback
point.args = data[point.id] -- ← Update point.args to match data[point.id]
insidePoints[point.id] = nil
end
if point.onNearby then
point.onNearby(GridCells[cellKey]?.points, waitTime)
end
end
end
end
for id, insidepoint in pairs(insidePoints) do
local pos = insidepoint.coords and vector3(insidepoint.coords.x, insidepoint.coords.y, insidepoint.coords.z) or vector3(0, 0, 0)
local dist = #(playerCoords - pos)
if dist > insidepoint.distance then
insidepoint.inside = false
data[insidepoint.id] = data[insidepoint.id] or insidepoint.args or {}
local result = insidepoint.onExit(insidepoint, data[insidepoint.id])
data[insidepoint.id] = result ~= nil and result or data[insidepoint.id]
insidepoint.args = data[insidepoint.id] -- ← Keep data in sync
insidePoints[insidepoint.id] = nil
end
end
Wait(waitTime) -- Faster updates when moving quickly
end
end)
return true
end
---Create a point based on a vector or entityID
---@param id string
---@param target number || vector3
---@param distance number
---@param _onEnter function
---@param _onExit function
---@param _onNearby function
---@param data self
---@return table
function Point.Register(id, target, distance, args, _onEnter, _onExit, _onNearby)
local isEntity = type(target) == "number"
local coords = isEntity and GetEntityCoords(target) or target
local self = {}
self.id = id
self.target = target -- Store entity ID or Vector3
self.isEntity = isEntity
self.coords = coords
self.distance = distance
self.onEnter = _onEnter or function() end
self.onExit = _onExit or function() end
self.onNearby = _onNearby or function() end
self.inside = false -- Track if player is inside
self.args = args or {}
ActivePoints[id] = self
Point.RegisterInGrid(self)
Point.StartLoop()
return self
end
---Remove an exsisting point by its id
---@param id string
function Point.Remove(id)
local point = ActivePoints[id]
if point then
local cellKey = point.cellKey
if GridCells[cellKey] and GridCells[cellKey].points[id] then
GridCells[cellKey].points[id] = nil
GridCells[cellKey].count = GridCells[cellKey].count - 1
if GridCells[cellKey].count <= 0 then
GridCells[cellKey] = nil
end
end
ActivePoints[id] = nil
end
end
---Returnes a point by its id
---@param id string
---@return table
function Point.Get(id)
return ActivePoints[id]
end
function Point.UpdateCoords(id, coords)
local point = ActivePoints[id]
if point then
point.coords = coords
local oldCellKey = point.cellKey
point.cellKey = Point.GetCellKey(coords)
Point.UpdateInGrid(point, oldCellKey)
end
end
---Returns all points
---@return table
function Point.GetAll()
return ActivePoints
end
return Point

View file

@ -0,0 +1,53 @@
Raycast = {}
--Rework of oxs Raycast system
function Raycast.GetForwardVector(rotation)
local camRot = rotation or GetFinalRenderedCamRot(2)
-- Convert each component to radians
local rx = math.rad(camRot.x)
local ry = math.rad(camRot.y)
local rz = math.rad(camRot.z)
-- Calculate sin and cos for each axis
local sx = math.sin(rx)
local cx = math.cos(rx)
local sy = math.sin(ry)
local cy = math.cos(ry)
local sz = math.sin(rz)
local cz = math.cos(rz)
-- Create forward vector components
local x = -sz * math.abs(cx)
local y = cz * math.abs(cx)
local z = sx
return vector3(x, y, z)
end
function Raycast.ToCoords(startCoords, endCoords, flag, ignore)
local probe = StartShapeTestLosProbe(startCoords.x, startCoords.y, startCoords.z, endCoords.x, endCoords.y, endCoords.z, flag or 511, PlayerPedId(), ignore or 4)
local retval, entity, finalCoords, normals, material = 1, nil, nil, nil, nil
local timeout = 500
while retval == 1 and timeout > 0 do
retval, entity, finalCoords, normals, material = GetShapeTestResultIncludingMaterial(probe)
timeout = timeout - 1
Wait(0)
end
return retval, entity, finalCoords, normals, material
end
function Raycast.FromCamera(flags, ignore, distance)
local coords = GetFinalRenderedCamCoord()
distance = distance or 10
local destination = coords + Raycast.GetForwardVector() * distance
local retval, entity, finalCoords, normals, material = Raycast.ToCoords(coords, destination, flags, ignore)
if retval ~= 1 then
local newDest = destination - vector3(0, 0, 10)
return Raycast.ToCoords(destination, newDest, flags, ignore)
end
return retval, entity, finalCoords, normals, material
end
return Raycast

View file

@ -0,0 +1,130 @@
---@class Scaleform
local Scaleform = {}
-- Constants
local SCALEFORM_TIMEOUT = 5000
local BACKGROUND_COLOR_VALUE = 80
local CONTROL_TYPE = 2
local RENDER_WAIT_TIME = 2
-- Local utility functions
local function setupButton(scaleform, button)
PushScaleformMovieFunction(scaleform, button.type)
if button.int then
PushScaleformMovieFunctionParameterInt(button.int)
end
-- Handle key index setup
if button.keyIndex then
if type(button.keyIndex) == "table" then
for _, keyCode in pairs(button.keyIndex) do
N_0xe83a3e3557a56640(GetControlInstructionalButton(CONTROL_TYPE, keyCode, true))
end
else
ScaleformMovieMethodAddParamPlayerNameString(GetControlInstructionalButton(CONTROL_TYPE, button.keyIndex[1],
true))
end
end
-- Handle button name setup
if button.name then
BeginTextCommandScaleformString("STRING")
AddTextComponentScaleform(button.name)
EndTextCommandScaleformString()
end
-- Handle background color
if button.type == 'SET_BACKGROUND_COLOUR' then
for _ = 1, 4 do
PushScaleformMovieFunctionParameterInt(BACKGROUND_COLOR_VALUE)
end
end
PopScaleformMovieFunctionVoid()
end
---Creates and sets up a scaleform movie
---@param scaleformName string The name of the scaleform to load
---@param buttons table Array of button configurations
---@return number scaleform The loaded scaleform handle
local function setupScaleform(scaleformName, buttons)
local scaleform = RequestScaleformMovie(scaleformName)
local timeout = SCALEFORM_TIMEOUT
-- Wait for scaleform to load
while not HasScaleformMovieLoaded(scaleform) and timeout > 0 do
timeout = timeout - 1
Wait(0)
end
if timeout <= 0 then
error('Scaleform failed to load: ' .. scaleformName)
end
DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 0, 0)
-- Setup each button
for _, button in ipairs(buttons) do
setupButton(scaleform, button)
end
return scaleform
end
---Sets up instructional buttons with default configuration
---@param buttons table Optional custom button configuration
---@return number scaleform The configured instructional buttons scaleform
function Scaleform.SetupInstructionalButtons(buttons)
buttons = buttons or {
-- Default button configuration commented out
-- Uncomment and modify as needed
-- {type = "CLEAR_ALL"},
-- {type = "SET_CLEAR_SPACE", int = 200},
-- {type = "SET_DATA_SLOT", name = config?.place_object?.name or 'Place Object:', keyIndex = config?.place_object?.key or {223}, int = 5},
-- {type = "SET_DATA_SLOT", name = config?.cancel_placement?.name or 'Cancel Placement:', keyIndex = config?.cancel_placement?.key or {222}, int = 4},
-- {type = "SET_DATA_SLOT", name = config?.snap_to_ground?.name or 'Snap to Ground:', keyIndex = config?.snap_to_ground?.key or {19}, int = 1},
-- {type = "SET_DATA_SLOT", name = config?.rotate?.name or 'Rotate:', keyIndex = config?.rotate?.key or {14, 15}, int = 2},
-- {type = "SET_DATA_SLOT", name = config?.distance?.name or 'Distance:', keyIndex = config?.distance?.key or {14,15,36}, int = 3},
-- {type = "SET_DATA_SLOT", name = config?.toggle_placement?.name or 'Toggle Placement:', keyIndex = config?.toggle_placement?.key or {199}, int = 0},
-- {type = "DRAW_INSTRUCTIONAL_BUTTONS"},
-- {type = "SET_BACKGROUND_COLOUR"},
}
return setupScaleform("instructional_buttons", buttons)
end
-- Active scaleform tracking
local activeScaleform = nil
---Runs a scaleform with optional update callback
---@param scaleform number The scaleform handle to run
---@param onUpdate function Optional callback for updates during runtime
function Scaleform.Run(scaleform, onUpdate)
if activeScaleform then return end
activeScaleform = scaleform
CreateThread(function()
while activeScaleform do
DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0)
if onUpdate then
local shouldStop = onUpdate()
if shouldStop then
Scaleform.Stop()
break
end
end
Wait(RENDER_WAIT_TIME)
end
end)
end
---Stops the currently running scaleform
function Scaleform.Stop()
activeScaleform = nil
end
exports("Scaleform", Scaleform)
return Scaleform

View file

@ -0,0 +1,53 @@
Shells = Shells or {}
--Data table
Shells.Targets = Shells.Targets or {
['entrance'] = {
['enter'] = {
label = 'Enter',
icon = 'fa-solid fa-door-open',
onSelect = function(entity, shellId, objectId)
TriggerServerEvent('community_bridge:server:EnterShell', shellId, objectId)
end
},
},
['exit'] = {
['leave'] = {
label = 'Exit',
icon = 'fa-solid fa-door-closed',
onSelect = function(entity, shellId, objectId)
TriggerServerEvent('community_bridge:server:ExitShell', shellId, objectId)
end
}
}
}
--Functions to set data table
Shells.Target = {
Set = function(shellType, options)
assert(shellType, "Shells.Target.Set: 'shellType' is required")
options = options or {}
for key, value in pairs(options) do
if Shells.Targets[shellType] and Shells.Targets[shellType][key] then
value.onSelect = Shells.Targets[shellType][key].onSelect
end
Shells.Targets[shellType] = Shells.Targets[shellType] or {}
Shells.Targets[shellType][key] = value
end
return true
end,
Get = function(shellType, shellId, objectId)
local aOptions = {}
for key, value in pairs(Shells.Targets[shellType] or {}) do
local onSelect = value.onSelect
value.onSelect = function(entity)
onSelect(entity, shellId, objectId)
end
table.insert(aOptions, value)
end
return aOptions
end
}
return Shells

View file

@ -0,0 +1,269 @@
local Target = Require('modules/target/_default/init.lua')
local ClientEntity = Require("lib/entities/client/client_entity.lua")
Shells = Shells or Require("lib/shells/client/config.lua")
Shells.All = Shells.All or {}
local insideShell = false
Shells.Events = {
OnSpawn = {},
OnRemove = {},
}
--Set Target lang or additionals
-- Shells.Target.Set('entrance', {
-- enter = {
-- label = 'yerp',
-- icon = 'fa-solid fa-door-open',
-- },
-- })
-- Shells.Target.Set('exit', {
-- leave = {
-- label = 'leerp',
-- icon = 'fa-solid fa-door-closed',
-- },
-- })
Shells.Event = {
Add = function(eventName, callback)
if Shells.Events[eventName] then
table.insert(Shells.Events[eventName], callback)
else
print(string.format("Shells.Event.Add: Invalid event name '%s'", eventName))
end
end,
Trigger = function(eventName, ...)
if Shells.Events[eventName] then
for _, callback in ipairs(Shells.Events[eventName]) do
callback(...)
end
else
print(string.format("Shells.Event.Trigger: Invalid event name '%s'", eventName))
end
end,
}
-- Shells.Event.Add('OnSpawn', function(pointData, entity)
-- print(string.format("Exterior point created with ID: %s, Type: %s, Entity: %s", pointData.id, pointData.type, entity))
-- end)
-- Shells.Event.Add('OnRemove', function(pointData)
-- print(string.format("Interior point created with ID: %s, Type: %s", pointData.id, pointData.type))
-- end)
function Shells.AddInteriorObject(shell, objectData)
objectData.OnSpawn = function(pointData)
Shells.Event.Trigger('OnSpawn', objectData, pointData.spawned)
local targetOptions = Shells.Target.Get(objectData.type, shell.id, objectData.id)
if targetOptions then
local size = vector3(objectData.distance / 2, objectData.distance / 2, objectData.distance / 2) -- this might need to be size
Target.AddBoxZone(objectData.id, objectData.coords, size, objectData.rotation.z, targetOptions, true)
end
end
objectData.OnRemove = function(pointData)
Shells.Event.Trigger('OnRemove', objectData, pointData.spawned)
Target.RemoveZone(objectData.id)
end
return ClientEntity.Register(objectData)
end
function Shells.SetupInterior(shell)
if not shell or not shell.interior then return end
for _, v in pairs(shell.interior) do
local pointData = Shells.AddInteriorObject(shell, v)
shell.interiorSpawned[pointData.id] = pointData
end
end
function Shells.SetupExterior(shell)
if not shell or not shell.exterior then return end
for k, v in pairs(shell.exterior) do
local pointData = Shells.AddInteriorObject(shell, v)
shell.exteriorSpawned[pointData.id] = pointData
end
end
function Shells.ClearInterior(shell)
if not shell or not shell.interiorSpawned then return end
for _, v in pairs(shell.interiorSpawned) do
ClientEntity.Unregister(v.id)
Target.RemoveZone(v.id)
end
shell.interiorSpawned = {}
end
function Shells.ClearExterior(shell)
if not shell or not shell.exteriorSpawned then return end
for _, v in pairs(shell.exteriorSpawned) do
ClientEntity.Unregister(v.id)
Target.RemoveZone(v.id)
end
shell.exteriorSpawned = {}
end
function Shells.New(data)
assert(data.id, "Shells.Create: 'id' is required")
assert(data.model, "Shells.Create: 'shellModel' is required")
assert(data.coords, "Shells.Create: 'coords' is required")
local exterior = data.exterior or {}
local exteriorSpawned = {}
for k, v in pairs(exterior or {}) do
v.OnSpawn = function(pointData)
Shells.Event.Trigger('OnSpawn', v, pointData.spawned)
local targetOptions = Shells.Target.Get(v.type, data.id, v.id)
if targetOptions then
local size = vector3(v.distance / 2, v.distance / 2, v.distance / 2)
Target.AddBoxZone(v.id, v.coords, size, v.rotation.z, targetOptions, true)
end
end
v.OnRemove = function(pointData)
Shells.Event.Trigger('OnRemove', v, pointData.spawned)
Target.RemoveZone(v.id)
end
local pointData = ClientEntity.Register(v)
exteriorSpawned[pointData.id] = pointData
end
data.interiorSpawned = {}
data.exteriorSpawned = exteriorSpawned
Shells.All[data.id] = data
return data
end
local returnPoint = nil
function Shells.Enter(id, entranceId)
local shell = Shells.All[id]
if not shell then
print(string.format("Shells.Spawn: Shell with ID '%s' not found", id))
return
end
local entrance = shell.interior[entranceId]
if not entrance then
print(string.format("Shells.Enter: Entrance with ID '%s' not found in shell '%s'", entranceId, id))
return
end
local ped = PlayerPedId()
returnPoint = GetEntityCoords(ped)
DoScreenFadeOut(1000)
Wait(1000)
local entranceCoords = entrance.coords
SetEntityCoords(ped, entranceCoords.x, entranceCoords.y, entranceCoords.z, false, false, false, true)
FreezeEntityPosition(ped, true)
local oldShell = insideShell and Shells.All[insideShell]
if oldShell?.id ~= id then
Shells.ClearExterior(oldShell)
Shells.ClearInterior(oldShell)
end
Shells.ClearExterior(shell)
Shells.SetupInterior(shell)
ClientEntity.Register(shell)
Wait(1000) -- Wait for the fade out to complete
FreezeEntityPosition(ped, false)
DoScreenFadeIn(1000)
insideShell = shell.id
end
function Shells.Exit(id, exitId)
local shell = Shells.All[id]
if not shell then
print(string.format("Shells.Exit: Shell with ID '%s' not found", id))
return
end
local oldCoords = GetEntityCoords(PlayerPedId())
local oldPoint = shell.exterior[exitId]
if not oldPoint then
print(string.format("Shells.Exit: Old point with ID '%s' not found in shell '%s'", exitId, id))
return
end
DoScreenFadeOut(1000)
Wait(1000)
SetEntityCoords(PlayerPedId(), oldPoint.coords.x, oldPoint.coords.y, oldPoint.coords.z, false, false, false, true)
FreezeEntityPosition(PlayerPedId(), true)
Shells.ClearInterior(shell)
ClientEntity.Unregister(shell.id)
Shells.SetupExterior(shell)
shell.interiorSpawned = {}
FreezeEntityPosition(PlayerPedId(), false)
DoScreenFadeIn(1000)
insideShell = false
end
function Shells.Inside()
return insideShell
end
RegisterNetEvent('community_bridge:client:CreateShell', function(shell)
Shells.New(shell)
end)
RegisterNetEvent('community_bridge:client:EnterShell', function(shellId, entranceId, oldId)
local shell = Shells.All[shellId]
if not shell then
print(string.format("Shells.EnterShell: Shell with ID '%s' not found", shellId))
return
end
Shells.Enter(shellId, entranceId, oldId)
end)
RegisterNetEvent('community_bridge:client:ExitShell', function(shellId, oldId)
local shell = Shells.All[shellId]
print(string.format("Shells.ExitShell: Exiting shell '%s'", shellId))
if not shell then
print(string.format("Shells.ExitShell: Shell with ID '%s' not found", shellId))
return
end
Shells.Exit(shellId, oldId)
end)
RegisterNetEvent('community_bridge:client:AddObjectsToShell', function (shellId, interiorObjects, exteriorObjects)
local shell = Shells.All[shellId]
print(string.format("Shells.AddObjectsToShell: Adding objects to shell '%s'", shellId),
json.encode({interiorObjects = interiorObjects, exteriorObjects = exteriorObjects}, { indent = true }))
if not shell then
print(string.format("Shells.AddObjectsToShell: Shell with ID '%s' not found", shellId))
return
end
local insideShell = Shells.Inside()
if interiorObjects then
for _, obj in pairs(interiorObjects) do
if not shell.interior[obj.id] then
shell.interior[obj.id] = obj
if insideShell and insideShell == shellId then
local pointData = Shells.AddInteriorObject(shell, obj)
shell.interiorSpawned[pointData.id] = pointData
end
end
end
end
if exteriorObjects then
for _, obj in pairs(exteriorObjects) do
if not shell.exterior[obj.id] then
shell.exterior[obj.id] = obj
if not insideShell then
local pointData = Shells.AddInteriorObject(shell, obj)
shell.exteriorSpawned[pointData.id] = pointData
end
end
end
end
end)
RegisterNetEvent('community_bridge:client:CreateShells', function(shells)
print("Shells.CreateShells: Creating shells")
for _, shell in pairs(shells) do
Shells.New(shell)
end
end)
-- TriggerClientEvent('community_bridge:client:CreateShells', -1, toClient)
-- TriggerClientEvent('community_bridge:client:ExitShell', src, oldId)
AddEventHandler('onResourceStart', function(resource)
if resource == GetCurrentResourceName() then
DoScreenFadeIn(1000) -- Fade in when resource stops
if not returnPoint then return end
SetEntityCoords(PlayerPedId(), returnPoint.x, returnPoint.y, returnPoint.z, false, false, false, true)
end
end)

View file

@ -0,0 +1,379 @@
Ids = Ids or Require("lib/utility/shared/ids.lua")
Shells = Shells or {}
Shells.All = Shells.All or {}
Shells.ActivePlayers = Shells.ActivePlayers or {} -- Track players in shells
Shells.Interactable = Shells.Interactable or {}
Shells.BucketsInUse = Shells.BucketsInUse or {} -- Track buckets in use
function Shells.Interactable.New(_type, id, model, coords, rotation, entityType, distance, meta)
assert(_type, "Shells.Interactable.Create: 'type' is required")
assert(id, "Shells.Interactable.Create: 'name' is required")
assert(coords, "Shells.Interactable.Create: 'coords' is required")
return {
type = _type,
id = id,
model = model,
coords = coords,
rotation = rotation or vector3(0.0, 0.0, 0.0),
distance = distance or 2.0,
entityType = entityType or "object", -- Default entity type
meta = meta or {},
}
end
function Shells.New(data)
local id = data.id or Ids.CreateUniqueId(Shells.All)
local _type = data.type or "none"
local model = data.model
local size = data.size or vector3(10.0, 10.0, 10.0) -- Default size if not provided
local coords = data.coords or vector3(0.0, 0.0, 0.0) -- Default coordinates if not provided
local rotation = data.rotation or vector3(0.0, 0.0, 0.0) -- Default rotation if not provided
local interior = data.interior or {}
local exterior = data.exterior or {}
local bucket = data.bucket or Ids.RandomNumber(Shells.BucketsInUse, 4)
assert(id, "Shells.Create: 'id' is required")
assert(model, "Shells.Create: 'shellModel' is required")
assert(coords, "Shells.Create: 'coords' is required")
local interiorInteractions = {}
for k, v in pairs(interior or {}) do
local interiorCoords = coords + (v.offset or vector3(0.0, 0.0, 0.0))
local interaction = Shells.Interactable.New(
v.type or "none",
v.id or Ids.CreateUniqueId(interiorInteractions),
v.model,
interiorCoords,
v.rotation or vector3(0.0, 0.0, 0.0),
v.entityType or "object", -- Default entity type for interior interactions
v.distance or 2.0,
v.meta
)
interiorInteractions[interaction.id] = interaction
end
local exteriorInteractions = {}
for k, v in pairs(exterior or {}) do
local interaction = Shells.Interactable.New(
v.type or "none",
v.id or Ids.CreateUniqueId(exteriorInteractions),
v.model,
v.coords,
v.rotation or vector3(0.0, 0.0, 0.0),
v.entityType or "object", -- Default entity type for exterior interactions
v.distance or 2.0,
v.meta
)
exteriorInteractions[interaction.id] = interaction
end
local shellData = {
id = id,
type = _type,
entityType = "object", -- Default entity type for shells
model = model,
coords = coords,
size = size or vector3(10.0, 10.0, 10.0), -- Default size if not provided
rotation = rotation or vector3(0.0, 0.0, 0.0),
interior = interiorInteractions,
exterior = exteriorInteractions,
}
Shells.All[id] = shellData
return shellData
end
function Shells.Create(data)
local shell = Shells.New(data)
assert(shell, "Shells.Create: 'shell' is required")
TriggerClientEvent('community_bridge:client:CreateShell', -1, shell)
return shell
end
function Shells.CreateBulk(shells)
assert(shells, "Shells.CreateBulk: 'shells' is required")
assert(type(shells) == "table", "Shells.CreateBulk: 'shells' must be a table")
local toClient = {}
for _, shellData in pairs(shells) do
local shell = Shells.New(shellData)
toClient[shell.id] = shell
end
TriggerClientEvent('community_bridge:client:CreateShells', -1, toClient)
end
function Shells.Enter(src, shellId, entranceId)
src = tonumber(src)
assert(src, "Shells.EnterShell: 'src' is required")
assert(shellId, "Shells.EnterShell: 'shellId' is required")
print(shellId)
local shell = Shells.All[shellId]
assert(shell, "Shell not found: " .. tostring(shellId))
if shell.onEnter then
local canEnter = shell.onEnter(src, shellId)
if not canEnter then return false end
end
if not shell.bucket then
local randNum = Ids.RandomNumber(Shells.BucketsInUse, 4)
shell.bucket = tonumber(randNum)
Shells.BucketsInUse[tostring(randNum)] = true
end
print(shell.bucket)
SetPlayerRoutingBucket(src, shell.bucket)
local exit = shell.exterior[entranceId]?.meta?.link
TriggerClientEvent('community_bridge:client:EnterShell', src, shellId, exit, Shells.ActivePlayers[tostring(src)])
Shells.ActivePlayers[tostring(src)] = shellId
return true
end
function Shells.Exit(src, shellId, oldId)
print(oldId)
src = tonumber(src)
assert(src, "Shells.ExitShell: 'src' is required")
assert(shellId, "Player is not in a shell")
local shell = Shells.All[shellId]
assert(shell, "Shell not found: " .. tostring(shellId))
-- Restore original routing bucket
SetPlayerRoutingBucket(src, 0)
local exit = shell.interior[oldId]?.meta?.link
-- Clear player's shell data
Shells.ActivePlayers[tostring(src)] = nil
if shell.onExit then
shell.onExit(src, shellId)
end
TriggerClientEvent('community_bridge:client:ExitShell', src, shellId, exit)
return true
end
function Shells.Get(shellId)
assert(shellId, "Shells.GetShellById: 'shellId' is required")
return Shells.All[shellId]
end
function Shells.Inside(src)
src = tonumber(src)
assert(src, "Shells.IsInside: 'src' is required")
local shellId = Shells.ActivePlayers[tostring(src)]
if not shellId then
return false
end
local shell = Shells.All[shellId]
if not shell then
return false
end
return shell
end
function Shells.AddObjects(shellId, objects)
assert(shellId, "Shells.AddObjects: 'shellId' is required")
assert(objects, "Shells.AddObjects: 'objects' is required")
assert(type(objects) == "table", "Shells.AddObjects: 'objects' must be a table")
local shell = Shells.All[shellId]
assert(shell, "Shell not found: " .. tostring(shellId))
local interiors = objects.interior or {}
local exteriors = objects.exterior or {}
for _, objData in pairs(interiors) do
local obj = Shells.Interactable.New(
objData.type,
objData.id,
objData.model,
objData.coords,
objData.rotation,
objData.entityType,
objData.distance,
objData.meta
)
shell.interior[obj.id] = obj -- Update the shell's interior with the new object
end
for _, objData in pairs(exteriors) do
local obj = Shells.Interactable.New(
objData.type,
objData.id,
objData.model,
objData.coords,
objData.rotation,
objData.entityType,
objData.distance,
objData.meta
)
shell.exterior[obj.id] = obj -- Update the shell's exterior with the new object
end
TriggerClientEvent('community_bridge:client:AddObjectsToShell', -1, shellId, shell.interior, shell.exterior)
return shell
end
function Shells.RemoveObjects(shellId, objectIds)
assert(shellId, "Shells.RemoveObjects: 'shellId' is required")
assert(objectIds, "Shells.RemoveObjects: 'objects' is required")
if type(objectIds) ~= "table" then
objectIds = {objectIds} -- Ensure it's a table
end
local shell = Shells.All[shellId]
assert(shell, "Shell not found: " .. tostring(shellId))
for _, objId in pairs(objectIds) do
shell.interior[objId] = nil
shell.exterior[objId] = nil
end
TriggerClientEvent('community_bridge:client:RemoveObjectsFromShell', -1, shellId, objectIds)
return shell
end
RegisterNetEvent('community_bridge:server:EnterShell', function(shellId, entranceId)
local src = source
Shells.Enter(src, shellId, entranceId)
end)
RegisterNetEvent('community_bridge:server:ExitShell', function(shellId, oldId)
local src = source
Shells.Exit(src, shellId, oldId)
end)
AddEventHandler('onPlayerJoining', function(playerId)
local src = source
TriggerClientEvent('community_bridge:client:CreateShells', src, Shells.All)
end)
function Shells.GetInteractable(shellId, id)
assert(shellId, "Shells.GetInteractable: 'shellId' is required")
assert(id, "Shells.GetInteractable: 'id' is required")
local shell = Shells.All[shellId]
if not shell then
return nil, "Shell not found: " .. tostring(shellId)
end
local interactable = shell.interior[id] or shell.exterior[id]
if not interactable then
return nil, "Interactable not found: " .. tostring(id)
end
return interactable
end
function Shells.GetClosestInteractable(shellId, coords, exterior)
assert(shellId, "Shells.GetClosestInteriorInteractable: 'shellId' is required")
assert(coords, "Shells.GetClosestInteriorInteractable: 'coords' is required")
local shell = Shells.All[shellId]
if not shell then
return nil, "Shell not found: " .. tostring(shellId)
end
local closest = nil
local closestDistance = math.huge
if exterior then
for _, interactable in pairs(shell.exterior) do
local distance = #(coords - interactable.coords)
if distance < closestDistance then
closestDistance = distance
closest = interactable
end
end
return closest, closestDistance
end
for _, interactable in pairs(shell.interior) do
local distance = #(coords - interactable.coords)
if distance < closestDistance then
closestDistance = distance
closest = interactable
end
end
return closest, closestDistance
end
local testShell = nil
RegisterCommand('shells:create', function(source, args, rawCommand)
local coords = GetEntityCoords(GetPlayerPed(source))
if testShell then
Shells.Exit(source, testShell.id, 'exit1') -- Exit previous shell if it exists
end
testShell = Shells.Create({
id = Ids.CreateUniqueId(Shells.All),
type = "shell",
model = "shell_garagem",
coords = coords + vector3(0.0, 0.0, 100.0), -- Adjusted to place shell slightly below player
rotation = vector3(0.0, 0.0, 0.0),
size = vector3(10.0, 10.0, 10.0),
interior = {
{
id = 'exit1',
type = 'exit',
coords = coords + vector3(0.0, 0.0, 1.0),
rotation = vector3(0.0, 0.0, 0.0),
distance = 2.0,
meta = {
link = 'entrance1',
}
}
},
exterior = {
{
id = 'entrance1',
type = 'entrance',
-- entityType = "object",
-- model = "xm_int_lev_sub_chair_02",
coords = coords - vector3(0.0, 0.0, 0.5),
rotation = vector3(0.0, 0.0, 0.0),
distance = 2.0,
meta = {
link = 'exit1',
}
}
},
})
end, true)
RegisterCommand('shells:addobject', function(source, args, rawCommand)
if not testShell then
print("No shell created. Use /shells:create first.")
return
end
local model = args[1]
if not model then
print("Usage: /shells:addobject <shellId> <model>")
return
end
local shellId = testShell.id
local coords = GetEntityCoords(GetPlayerPed(source))
local objectData = {
type = "none",
entityType = "ped",
id = Ids.CreateUniqueId(Shells.All[shellId].interior),
model = model,
coords = coords - vector3(0.0, 0.0, 0.5), -- Adjusted to place object slightly above player
rotation = vector3(0.0, 0.0, 0.0),
distance = 2.0,
meta = {}
}
Shells.AddObjects(shellId, {interior = {objectData}})
end, true)
AddEventHandler('onResourceStop', function(resource)
if resource == GetCurrentResourceName() then
for src, shellId in pairs(Shells.ActivePlayers) do
SetPlayerRoutingBucket(tonumber(src), 0) -- Reset routing bucket for all players
end
end
end)
AddEventHandler('community_bridge:Server:OnPlayerUnload', function(src)
if not Shells.ActivePlayers[tostring(src)] then return end
print(string.format("Player %d is exiting shell %s", src, Shells.ActivePlayers[tostring(src)]))
Shells.Exit(src, Shells.ActivePlayers[tostring(src)])
Shells.ActivePlayers[tostring(src)] = nil
end)

View file

@ -0,0 +1,73 @@
SQL = {}
Require("lib/MySQL.lua", "oxmysql")
--- Creates a table in the database if it does not exist.
-- @param tableName The name of the table to create. Example: {{ name = "identifier", type = "VARCHAR(50)", primary = true }}
-- @param columns A table containing column definitions, where each column is a table with 'name' and 'type'.
---@return nil
function SQL.Create(tableName, columns)
assert(MySQL, "Tried using module SQL without MySQL being loaded")
local columnsList = {}
for i, column in pairs(columns) do
table.insert(columnsList, string.format("%s %s", column.name, column.type))
end
local query = string.format("CREATE TABLE IF NOT EXISTS %s (%s);",
tableName,
table.concat(columnsList, ", ")
)
MySQL.query.await(query)
end
-- insert if not exist otherwise update
function SQL.InsertOrUpdate(tableName, data)
assert(MySQL, "Tried using module SQL without MySQL being loaded")
local columns = {}
local values = {}
local updates = {}
for column, value in pairs(data) do
table.insert(columns, column)
table.insert(values, "'" .. value .. "'") -- Ensure values are properly quoted
table.insert(updates, column .. " = VALUES(" .. column .. ")") -- Use VALUES() for update
end
local query = string.format(
"INSERT INTO %s (%s) VALUES (%s) ON DUPLICATE KEY UPDATE %s;",
tableName,
table.concat(columns, ", "),
table.concat(values, ", "),
table.concat(updates, ", ")
)
MySQL.query.await(query)
end
function SQL.Get(tableName, where)
assert(MySQL, "Tried using module SQL without MySQL being loaded")
local query = string.format("SELECT * FROM %s WHERE %s;", tableName, where)
local result = MySQL.query.await(query)
return result
end
function SQL.GetAll(tableName)
assert(MySQL, "Tried using module SQL without MySQL being loaded")
local query = string.format("SELECT * FROM %s;", tableName)
local result = MySQL.query.await(query)
return result
end
function SQL.Delete(tableName, where)
assert(MySQL, "Tried using module SQL without MySQL being loaded")
local query = string.format("DELETE FROM %s WHERE %s;", tableName, where)
MySQL.query.await(query)
end
exports('SQL', function()
return SQL
end)
return SQL

View file

@ -0,0 +1,48 @@
ClientStateBag = {}
---Gets an entity from a statebag name
---@param entityId string The entity ID or statebag name
---@return number|nil entity The entity handle or nil if not found
local function getStateBagEntity(entityId)
local _entity = GetEntityFromStateBagName(entityId)
return _entity
end
local function getPlayerFromStateBagName(stateBagName)
return getPlayerFromStateBagName(stateBagName)
end
---Adds a handler for entity statebag changes
---@param keyName string The statebag key to watch for changes
---@param entityId string|nil The specific entity ID to watch, or nil for all entities
---@param callback function The callback function to handle changes (function(entityId, key, value, lastValue, replicated))
---@return number handler The handler ID
function ClientStateBag.AddEntityChangeHandler(keyName, entityId, callback)
return AddStateBagChangeHandler(keyName, entityId or nil, function(bagName, key, value, lastValue, replicated)
local entity = getStateBagEntity(bagName)
if not DoesEntityExist(entity) then return end
if entity then
return callback(entity, key, value, lastValue, replicated)
end
end)
end
---Adds a handler for player statebag changes
---@param keyName string The statebag key to watch for changes
---@param filter boolean|nil If true, only watch for changes from the current player
---@param callback function The callback function to handle changes (function(playerId, key, value, lastValue, replicated))
---@return number handler The handler ID
function ClientStateBag.AddPlayerChangeHandler(keyName, filter, callback)
return AddStateBagChangeHandler(keyName, filter and ("player:%s"):format(GetPlayerServerId(PlayerId())) or nil,
function(bagName, key, value, lastValue, replicated)
local actualPlayerId = getPlayerFromStateBagName(bagName)
if DoesEntityExist(actualPlayerId) and (actualPlayerId ~= PlayerPedId()) then -- you cant have a statebag value if you are not the player
return false
end
if actualPlayerId and actualPlayerId ~= 0 then
return callback(tonumber(actualPlayerId), key, value, lastValue, replicated)
end
end)
end
return ClientStateBag

View file

@ -0,0 +1,640 @@
---@class Utility
Utility = Utility or {}
local blipIDs = {}
local spawnedPeds = {}
Locales = Locales or Require('modules/locales/shared.lua')
-- === Local Helpers ===
---Get the hash of a model (string or number)
---@param model string|number
---@return number
local function getModelHash(model)
if type(model) ~= 'number' then
return joaat(model)
end
return model
end
---Ensure a model is loaded into memory
---@param model string|number
---@return boolean, number
local function ensureModelLoaded(model)
local hash = getModelHash(model)
if not IsModelValid(hash) and not IsModelInCdimage(hash) then return false, hash end
RequestModel(hash)
local count = 0
while not HasModelLoaded(hash) and count < 30000 do
Wait(0)
count = count + 1
end
return HasModelLoaded(hash), hash
end
---Add a text entry if possible
---@param key string
---@param text string
local function addTextEntryOnce(key, text)
if not AddTextEntry then return end
AddTextEntry(key, text)
end
---Create a blip safely and store its reference
---@param coords vector3
---@param sprite number
---@param color number
---@param scale number
---@param label string
---@param shortRange boolean
---@param displayType number
---@return number
local function safeAddBlip(coords, sprite, color, scale, label, shortRange, displayType)
local blip = AddBlipForCoord(coords.x, coords.y, coords.z)
SetBlipSprite(blip, sprite or 8)
SetBlipColour(blip, color or 3)
SetBlipScale(blip, scale or 0.8)
SetBlipDisplay(blip, displayType or 2)
SetBlipAsShortRange(blip, shortRange)
addTextEntryOnce(label, label)
BeginTextCommandSetBlipName(label)
EndTextCommandSetBlipName(blip)
table.insert(blipIDs, blip)
return blip
end
---Create a entiyty blip safely and store its reference
---@param entity number
---@param sprite number
---@param color number
---@param scale number
---@param label string
---@param shortRange boolean
---@param displayType number
---@return number
local function safeAddEntityBlip(entity, sprite, color, scale, label, shortRange, displayType)
local blip = AddBlipForEntity(entity)
SetBlipSprite(blip, sprite or 8)
SetBlipColour(blip, color or 3)
SetBlipScale(blip, scale or 0.8)
SetBlipDisplay(blip, displayType or 2)
SetBlipAsShortRange(blip, shortRange)
ShowHeadingIndicatorOnBlip(blip, true)
addTextEntryOnce(label, label)
BeginTextCommandSetBlipName(label)
EndTextCommandSetBlipName(blip)
table.insert(blipIDs, blip)
return blip
end
---Remove a blip safely from the stored list
---@param blip number
---@return boolean
local function safeRemoveBlip(blip)
for i, storedBlip in ipairs(blipIDs) do
if storedBlip == blip then
RemoveBlip(storedBlip)
table.remove(blipIDs, i)
return true
end
end
return false
end
---Add a text entry if possible (shortcut)
---@param text string
local function safeAddTextEntry(text)
if not AddTextEntry then return end
AddTextEntry(text, text)
end
-- === Public Utility Functions ===
---Create a prop with the given model and coordinates
---@param model string|number
---@param coords vector3
---@param heading number
---@param networked boolean
---@return number|nil
function Utility.CreateProp(model, coords, heading, networked)
local loaded, hash = ensureModelLoaded(model)
if not loaded then return nil, Prints and Prints.Error and Prints.Error("Model Has Not Loaded") end
local propEntity = CreateObject(hash, coords.x, coords.y, coords.z, networked, false, false)
SetEntityHeading(propEntity, heading)
SetModelAsNoLongerNeeded(hash)
return propEntity
end
---Get street and crossing names at given coordinates
---@param coords vector3
---@return string, string
function Utility.GetStreetNameAtCoords(coords)
local streetHash, crossingHash = GetStreetNameAtCoord(coords.x, coords.y, coords.z)
return GetStreetNameFromHashKey(streetHash), GetStreetNameFromHashKey(crossingHash)
end
---Create a vehicle with the given model and coordinates
---@param model string|number
---@param coords vector3
---@param heading number
---@param networked boolean
---@return number|nil, table
function Utility.CreateVehicle(model, coords, heading, networked)
local loaded, hash = ensureModelLoaded(model)
if not loaded then return nil, {}, Prints and Prints.Error and Prints.Error("Model Has Not Loaded") end
local vehicle = CreateVehicle(hash, coords.x, coords.y, coords.z, heading, networked, false)
SetVehicleHasBeenOwnedByPlayer(vehicle, true)
SetVehicleNeedsToBeHotwired(vehicle, false)
SetVehRadioStation(vehicle, "OFF")
SetModelAsNoLongerNeeded(hash)
return vehicle, {
networkid = NetworkGetNetworkIdFromEntity(vehicle) or 0,
coords = GetEntityCoords(vehicle),
heading = GetEntityHeading(vehicle),
}
end
---Create a ped with the given model and coordinates
---@param model string|number
---@param coords vector3
---@param heading number
---@param networked boolean
---@param settings table|nil
---@return number|nil
function Utility.CreatePed(model, coords, heading, networked, settings)
local loaded, hash = ensureModelLoaded(model)
if not loaded then return nil, Prints and Prints.Error and Prints.Error("Model Has Not Loaded") end
local spawnedEntity = CreatePed(0, hash, coords.x, coords.y, coords.z, heading, networked, false)
SetModelAsNoLongerNeeded(hash)
table.insert(spawnedPeds, spawnedEntity)
return spawnedEntity
end
---Show a busy spinner with the given text
---@param text string
---@return boolean
function Utility.StartBusySpinner(text)
safeAddTextEntry(text)
BeginTextCommandBusyString(text)
AddTextComponentSubstringPlayerName(text)
EndTextCommandBusyString(0)
return true
end
---Stop the busy spinner if active
---@return boolean
function Utility.StopBusySpinner()
if BusyspinnerIsOn() then
BusyspinnerOff()
return true
end
return false
end
---Create a blip at the given coordinates
---@param coords vector3
---@param sprite number
---@param color number
---@param scale number
---@param label string
---@param shortRange boolean
---@param displayType number
---@return number
function Utility.CreateBlip(coords, sprite, color, scale, label, shortRange, displayType)
return safeAddBlip(coords, sprite, color, scale, label, shortRange, displayType)
end
---Create a blip on the provided entity
---@param entity number
---@param sprite number
---@param color number
---@param scale number
---@param label string
---@param shortRange boolean
---@param displayType number
---@return number
function Utility.CreateEntityBlip(entity, sprite, color, scale, label, shortRange, displayType)
return safeAddEntityBlip(entity, sprite, color, scale, label, shortRange, displayType)
end
---Remove a blip if it exists
---@param blip number
---@return boolean
function Utility.RemoveBlip(blip)
return safeRemoveBlip(blip)
end
---Load a model into memory
---@param model string|number
---@return boolean
function Utility.LoadModel(model)
local loaded = ensureModelLoaded(model)
return loaded
end
---Request an animation dictionary
---@param dict string
---@return boolean
function Utility.RequestAnimDict(dict)
RequestAnimDict(dict)
local count = 0
while not HasAnimDictLoaded(dict) and count < 30000 do
Wait(0)
count = count + 1
end
return HasAnimDictLoaded(dict)
end
---Remove a ped if it exists
---@param entity number
---@return boolean
function Utility.RemovePed(entity)
local success = false
if DoesEntityExist(entity) then
DeleteEntity(entity)
end
for i, storedEntity in ipairs(spawnedPeds) do
if storedEntity == entity then
table.remove(spawnedPeds, i)
success = true
break
end
end
return success
end
---Show a native input menu and return the result
---@param text string
---@param length number
---@return string|boolean
function Utility.NativeInputMenu(text, length)
local maxLength = Math and Math.Clamp and Math.Clamp(length, 1, 50) or math.min(math.max(length or 10, 1), 50)
local menuText = text or 'enter text'
safeAddTextEntry(menuText)
DisplayOnscreenKeyboard(1, menuText, "", "", "", "", "", maxLength)
while (UpdateOnscreenKeyboard() == 0) do
DisableAllControlActions(0)
Wait(0)
end
if (GetOnscreenKeyboardResult()) then
return GetOnscreenKeyboardResult()
end
return false
end
---Get the skin data of a ped
---@param entity number
---@return table
function Utility.GetEntitySkinData(entity)
local skinData = { clothing = {}, props = {} }
for i = 0, 11 do
skinData.clothing[i] = { GetPedDrawableVariation(entity, i), GetPedTextureVariation(entity, i) }
end
for i = 0, 13 do
skinData.props[i] = { GetPedPropIndex(entity, i), GetPedPropTextureIndex(entity, i) }
end
return skinData
end
---Apply skin data to a ped
---@param entity number
---@param skinData table
---@return boolean
function Utility.SetEntitySkinData(entity, skinData)
for i = 0, 11 do
SetPedComponentVariation(entity, i, skinData.clothing[i][1], skinData.clothing[i][2], 0)
end
for i = 0, 13 do
SetPedPropIndex(entity, i, skinData.props[i][1], skinData.props[i][2], 0)
end
return true
end
---Reload the player's skin and remove attached objects
---@return boolean
function Utility.ReloadSkin()
local skinData = Utility.GetEntitySkinData(cache.ped)
Utility.SetEntitySkinData(cache.ped, skinData)
for _, props in pairs(GetGamePool("CObject")) do
if IsEntityAttachedToEntity(cache.ped, props) then
SetEntityAsMissionEntity(props, true, true)
DeleteObject(props)
DeleteEntity(props)
end
end
return true
end
---Show a native help text
---@param text string
---@param duration number
function Utility.HelpText(text, duration)
safeAddTextEntry(text)
BeginTextCommandDisplayHelp(text)
EndTextCommandDisplayHelp(0, false, true, duration or 5000)
end
---Draw 3D help text in the world
---@param coords vector3
---@param text string
---@param scale number
function Utility.Draw3DHelpText(coords, text, scale)
local onScreen, x, y = GetScreenCoordFromWorldCoord(coords.x, coords.y, coords.z)
if onScreen then
SetTextScale(scale or 0.35, scale or 0.35)
SetTextFont(4)
SetTextProportional(1)
SetTextColour(255, 255, 255, 215)
SetTextEntry("STRING")
SetTextCentre(1)
AddTextComponentString(text)
DrawText(x, y)
local factor = (string.len(text)) / 370
DrawRect(x, y + 0.0125, 0.015 + factor, 0.03, 41, 11, 41, 100)
end
end
---Show a native notification
---@param text string
function Utility.NotifyText(text)
safeAddTextEntry(text)
SetNotificationTextEntry(text)
DrawNotification(false, true)
end
---Teleport the player to given coordinates
---@param coords vector3
---@param conditionFunction function|nil
---@param afterTeleportFunction function|nil
function Utility.TeleportPlayer(coords, conditionFunction, afterTeleportFunction)
if conditionFunction ~= nil then
if not conditionFunction() then
return
end
end
DoScreenFadeOut(2500)
Wait(2500)
SetEntityCoords(cache.ped, coords.x, coords.y, coords.z, false, false, false, false)
if coords.w then
SetEntityHeading(cache.ped, coords.w)
end
FreezeEntityPosition(cache.ped, true)
local count = 0
while not HasCollisionLoadedAroundEntity(cache.ped) and count <= 30000 do
RequestCollisionAtCoord(coords.x, coords.y, coords.z)
Wait(0)
count = count + 1
end
FreezeEntityPosition(cache.ped, false)
DoScreenFadeIn(1000)
if afterTeleportFunction ~= nil then
afterTeleportFunction()
end
end
---Get the hash from a model
---@param model string|number
---@return number
function Utility.GetEntityHashFromModel(model)
return getModelHash(model)
end
---Get the closest player to given coordinates
---@param coords vector3|nil
---@param distanceScope number|nil
---@param includeMe boolean|nil
---@return number, number, number
function Utility.GetClosestPlayer(coords, distanceScope, includeMe)
local players = GetActivePlayers()
local closestPlayer = 0
local selfPed = cache.ped
local selfCoords = coords or GetEntityCoords(cache.ped)
local closestDistance = distanceScope or 5
for _, player in ipairs(players) do
local playerPed = GetPlayerPed(player)
if includeMe or playerPed ~= selfPed then
local playerCoords = GetEntityCoords(playerPed)
local distance = #(selfCoords - playerCoords)
if closestDistance == -1 or distance < closestDistance then
closestPlayer = player
closestDistance = distance
end
end
end
return closestPlayer, closestDistance, GetPlayerServerId(closestPlayer)
end
---Get the closest vehicle to given coordinates
---@param coords vector3|nil
---@param distanceScope number|nil
---@param includePlayerVeh boolean|nil
---@return number|nil, vector3|nil, number|nil
function Utility.GetClosestVehicle(coords, distanceScope, includePlayerVeh)
local vehicleEntity = nil
local vehicleNetID = nil
local vehicleCoords = nil
local selfCoords = coords or GetEntityCoords(cache.ped)
local closestDistance = distanceScope or 5
local includeMyVeh = includePlayerVeh or false
local gamePoolVehicles = GetGamePool("CVehicle")
local playerVehicle = IsPedInAnyVehicle(cache.ped, false) and GetVehiclePedIsIn(cache.ped, false) or 0
for i = 1, #gamePoolVehicles do
local thisVehicle = gamePoolVehicles[i]
if DoesEntityExist(thisVehicle) and (includeMyVeh or thisVehicle ~= playerVehicle) then
local thisVehicleCoords = GetEntityCoords(thisVehicle)
local distance = #(selfCoords - thisVehicleCoords)
if closestDistance == -1 or distance < closestDistance then
vehicleEntity = thisVehicle
vehicleNetID = NetworkGetNetworkIdFromEntity(thisVehicle) or nil
vehicleCoords = thisVehicleCoords
closestDistance = distance
end
end
end
return vehicleEntity, vehicleCoords, vehicleNetID
end
-- Deprecated point functions (no changes)
function Utility.RegisterPoint(pointID, pointCoords, pointDistance, _onEnter, _onExit, _nearby)
return Point.Register(pointID, pointCoords, pointDistance, nil, _onEnter, _onExit, _nearby)
end
function Utility.GetPointById(pointID)
return Point.Get(pointID)
end
function Utility.GetActivePoints()
return Point.GetAll()
end
function Utility.RemovePoint(pointID)
return Point.Remove(pointID)
end
---Simple switch-case function
---@generic T
---@param value T The value to match against the cases
---@param cases table<T|false, fun(): any> Table with case functions and an optional default (false key)
---@return any|false result The return value of the matched case function, or false if none matched
function Utility.Switch(value, cases)
local caseFunc = cases[value] or cases[false]
if caseFunc and type(caseFunc) == "function" then
local ok, result = pcall(caseFunc)
return ok and result or false
end
return false
end
function Utility.CopyToClipboard(text)
if not text then return false end
if type(text) ~= "string" then
text = json.encode(text, { indent = true })
end
SendNUIMessage({
type = "copytoclipboard",
text = text
})
local message = Locales and Locales.Locale("clipboard.copy")
--TriggerEvent('community_bridge:Client:Notify', message, 'success')
return true
end
--- Pattern match-like function
---@generic T
---@param value T The value to match
---@param patterns table<T|fun(T):boolean|false, fun(): any> A list of matchers and their handlers
---@return any|false result The result of the first matched case, or false if none
function Utility.Match(value, patterns)
for pattern, handler in pairs(patterns) do
if type(pattern) == "function" then
local ok, matched = pcall(pattern, value)
if ok and matched then
local success, result = pcall(handler)
return success and result or false
end
elseif pattern == value then
local success, result = pcall(handler)
return success and result or false
end
end
if patterns[false] then
local ok, result = pcall(patterns[false])
return ok and result or false
end
return false
end
---Get zone name at coordinates
---@param coords vector3
---@return string
function Utility.GetZoneName(coords)
local zoneHash = GetNameOfZone(coords.x, coords.y, coords.z)
return GetLabelText(zoneHash)
end
local SpecialKeyCodes = {
['b_116'] = 'Scroll Up',
['b_115'] = 'Scroll Down',
['b_100'] = 'LMB',
['b_101'] = 'RMB',
['b_102'] = 'MMB',
['b_103'] = 'Extra 1',
['b_104'] = 'Extra 2',
['b_105'] = 'Extra 3',
['b_106'] = 'Extra 4',
['b_107'] = 'Extra 5',
['b_108'] = 'Extra 6',
['b_109'] = 'Extra 7',
['b_110'] = 'Extra 8',
['b_1015'] = 'AltLeft',
['b_1000'] = 'ShiftLeft',
['b_2000'] = 'Space',
['b_1013'] = 'ControlLeft',
['b_1002'] = 'Tab',
['b_1014'] = 'ControlRight',
['b_140'] = 'Numpad4',
['b_142'] = 'Numpad6',
['b_144'] = 'Numpad8',
['b_141'] = 'Numpad5',
['b_143'] = 'Numpad7',
['b_145'] = 'Numpad9',
['b_200'] = 'Insert',
['b_1012'] = 'CapsLock',
['b_170'] = 'F1',
['b_171'] = 'F2',
['b_172'] = 'F3',
['b_173'] = 'F4',
['b_174'] = 'F5',
['b_175'] = 'F6',
['b_176'] = 'F7',
['b_177'] = 'F8',
['b_178'] = 'F9',
['b_179'] = 'F10',
['b_180'] = 'F11',
['b_181'] = 'F12',
['b_194'] = 'ArrowUp',
['b_195'] = 'ArrowDown',
['b_196'] = 'ArrowLeft',
['b_197'] = 'ArrowRight',
['b_1003'] = 'Enter',
['b_1004'] = 'Backspace',
['b_198'] = 'Delete',
['b_199'] = 'Escape',
['b_1009'] = 'PageUp',
['b_1010'] = 'PageDown',
['b_1008'] = 'Home',
['b_131'] = 'NumpadAdd',
['b_130'] = 'NumpadSubstract',
['b_211'] = 'Insert',
['b_210'] = 'Delete',
['b_212'] = 'End',
['b_1055'] = 'Home',
['b_1056'] = 'PageUp',
}
local function translateKey(key)
if string.find(key, "t_") then
return string.gsub(key, "t_", "")
elseif SpecialKeyCodes[key] then
return SpecialKeyCodes[key]
else
return key
end
end
function Utility.GetCommandKey(commandName)
local hash = GetHashKey(commandName) | 0x80000000
local button = GetControlInstructionalButton(2, hash, true)
if not button or button == "" or button == "NULL" then
hash = GetHashKey(commandName)
button = GetControlInstructionalButton(2, hash, true)
end
return translateKey(button)
end
AddEventHandler('onResourceStop', function(resource)
if resource ~= GetCurrentResourceName() then return end
for _, blip in pairs(blipIDs) do
if blip and DoesBlipExist(blip) then
RemoveBlip(blip)
end
end
for _, ped in pairs(spawnedPeds) do
if ped and DoesEntityExist(ped) then
DeleteEntity(ped)
end
end
end)
exports('Utility', Utility)
return Utility

View file

@ -0,0 +1,143 @@
local Callback = {}
local CallbackRegistry = {}
-- Constants
local RESOURCE = GetCurrentResourceName() or 'unknown'
local EVENT_NAMES = {
CLIENT_TO_SERVER = RESOURCE .. ':CS:Callback',
SERVER_TO_CLIENT = RESOURCE .. ':SC:Callback',
CLIENT_RESPONSE = RESOURCE .. ':CSR:Callback',
SERVER_RESPONSE = RESOURCE .. ':SCR:Callback'
}
-- Utility functions
local function generateCallbackId(name)
return string.format('%s_%d', name, math.random(1000000, 9999999))
end
local function handleResponse(registry, name, callbackId, ...)
local data = registry[callbackId]
if not data then return end
if data.callback then
data.callback(...)
end
if data.promise then
data.promise:resolve({ ... })
end
registry[callbackId] = nil
end
local function triggerCallback(eventName, target, name, args, callback)
local callbackId = generateCallbackId(name)
local promise = promise.new()
CallbackRegistry[callbackId] = {
callback = callback,
promise = promise
}
if type(target) == 'table' then
for _, id in ipairs(target) do
TriggerClientEvent(eventName, tonumber(id), name, callbackId, table.unpack(args))
end
else
TriggerClientEvent(eventName, tonumber(target), name, callbackId, table.unpack(args))
end
if not callback then
local result = Citizen.Await(promise)
local returnResults = (result and type(result) == 'table' ) and result or {result}
return table.unpack(returnResults)
end
end
-- Server-side implementation
if IsDuplicityVersion() then
function Callback.Register(name, handler)
Callback[name] = handler
end
function Callback.Trigger(name, target, ...)
local args = { ... }
local callback = type(args[1]) == 'function' and table.remove(args, 1) or nil
return triggerCallback(EVENT_NAMES.SERVER_TO_CLIENT, target or -1, name, args, callback)
end
RegisterNetEvent(EVENT_NAMES.CLIENT_TO_SERVER, function(name, callbackId, ...)
if not name or not callbackId then return print(string.format("[%s] Warning: Invalid callback parameters - name: %s, callbackId: %s", RESOURCE, tostring(name), tostring(callbackId))) end
local handler = Callback[name]
if not handler then return end
local playerId = source
if not playerId or playerId == 0 then return print(string.format("[%s] Warning: Invalid source for callback '%s'", RESOURCE, name)) end
local result = table.pack(handler(playerId, ...))
TriggerClientEvent(EVENT_NAMES.CLIENT_RESPONSE, playerId, name, callbackId, table.unpack(result))
end)
RegisterNetEvent(EVENT_NAMES.SERVER_RESPONSE, function(name, callbackId, ...)
handleResponse(CallbackRegistry, name, callbackId, ...)
end)
-- Client-side implementation
else
local ClientCallbacks = {}
local ReboundCallbacks = {}
function Callback.Register(name, handler)
ClientCallbacks[name] = handler
end
function Callback.RegisterRebound(name, handler)
ReboundCallbacks[name] = handler
end
function Callback.Trigger(name, ...)
local args = { ... }
local callback = type(args[1]) == 'function' and table.remove(args, 1) or nil
local callbackId = generateCallbackId(name)
local promise = promise.new()
CallbackRegistry[callbackId] = {
callback = callback,
promise = promise
}
TriggerServerEvent(EVENT_NAMES.CLIENT_TO_SERVER, name, callbackId, table.unpack(args))
if not callback then
local result = Citizen.Await(promise)
return table.unpack(result)
end
end
RegisterNetEvent(EVENT_NAMES.CLIENT_RESPONSE, function(name, callbackId, ...)
if ReboundCallbacks[name] then
ReboundCallbacks[name](...)
end
handleResponse(CallbackRegistry, name, callbackId, ...)
end)
RegisterNetEvent(EVENT_NAMES.SERVER_TO_CLIENT, function(name, callbackId, ...)
local handler = ClientCallbacks[name]
if not handler then return end
local result = table.pack(handler(...))
TriggerServerEvent(EVENT_NAMES.SERVER_RESPONSE, name, callbackId, table.unpack(result))
end)
end
-- Exports
exports('Callback', Callback)
exports('RegisterCallback', Callback.Register)
exports('TriggerCallback', Callback.Trigger)
if not IsDuplicityVersion() then
exports('RegisterRebound', Callback.RegisterRebound)
end
return Callback

View file

@ -0,0 +1,72 @@
Ids = Ids or {}
---This will generate a unique id.
---@param tbl table | nil
---@param len number | nil
---@param pattern string | nil
---@return string
Ids.CreateUniqueId = function(tbl, len, pattern) -- both optional
tbl = tbl or {} -- table to check uniqueness. Ids to check against must be the key to the tables value
len = len or 8
local id = ""
for i = 1, len do
local char = ""
if pattern then
local charIndex = math.random(1, #pattern)
char = pattern:sub(charIndex, charIndex)
else
char = math.random(1, 2) == 1 and string.char(math.random(65, 90)) or math.random(0, 9) -- CAP letter and number
end
id = id .. char
end
if tbl[id] then
return Ids.CreateUniqueId(tbl, len, pattern)
end
return id
end
---This will generate a unique id.
---@param tbl table
---@param len number
---@return string
Ids.RandomUpper = function(tbl, len)
return Ids.CreateUniqueId(tbl, len, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
end
---This will generate a unique id.
---@param tbl table
---@param len number
---@return string
Ids.RandomLower = function(tbl, len)
return Ids.CreateUniqueId(tbl, len, "abcdefghijklmnopqrstuvwxyz")
end
---This will generate a unique id.
---@param tbl table
---@param len number
---@return string
Ids.RandomString = function(tbl, len)
return Ids.CreateUniqueId(tbl, len, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
end
---This will generate a unique id.
---@param tbl table
---@param len number
---@return string
Ids.RandomNumber = function(tbl, len)
return Ids.CreateUniqueId(tbl, len, "0123456789")
end
---This will generate a unique id.
---@param tbl table
---@param len number
---@return string
Ids.Random = function(tbl, len)
return Ids.CreateUniqueId(tbl, len)
end
exports("Ids", Ids)
return Ids

View file

@ -0,0 +1,532 @@
LA = LA or {}
--https://easings.net
LA.Lerp = function(a, b, t)
return a + (b - a) * t
end
LA.LerpVector = function(a, b, t)
return vector3(LA.Lerp(a.x, b.x, t), LA.Lerp(a.y, b.y, t), LA.Lerp(a.z, b.z, t))
end
LA.EaseInSine = function(t)
return 1 - math.cos((t * math.pi) / 2)
end
LA.EaseOutSine = function(t)
return math.sin((t * math.pi) / 2)
end
LA.EaseInOutSine = function(t)
return -(math.cos(math.pi * t) - 1) / 2
end
LA.EaseInCubic = function(t)
return t ^ 3
end
LA.EaseOutCubic = function(t)
return 1 - (1 - t) ^ 3
end
LA.EaseInOutCubic = function(t)
return t < 0.5 and 4 * t ^ 3 or 1 - ((-2 * t + 2) ^ 3) / 2
end
LA.EaseInQuint = function(t)
return t ^ 5
end
LA.EaseOutQuint = function(t)
return 1 - (1 - t) ^ 5
end
LA.EaseInOutQuint = function(t)
return t < 0.5 and 16 * t ^ 5 or 1 - ((-2 * t + 2) ^ 5) / 2
end
LA.EaseInCirc = function(t)
return 1 - math.sqrt(1 - t ^ 2)
end
LA.EaseOutCirc = function(t)
return math.sqrt(1 - (t - 1) ^ 2)
end
LA.EaseInOutCirc = function(t)
return t < 0.5 and (1 - math.sqrt(1 - (2 * t) ^ 2)) / 2 or (math.sqrt(1 - (-2 * t + 2) ^ 2) + 1) / 2
end
LA.EaseInElastic = function(t)
return t == 0 and 0 or t == 1 and 1 or -2 ^ (10 * t - 10) * math.sin((t * 10 - 10.75) * (2 * math.pi) / 3)
end
LA.EaseOutElastic = function(t)
return t == 0 and 0 or t == 1 and 1 or 2 ^ (-10 * t) * math.sin((t * 10 - 0.75) * (2 * math.pi) / 3) + 1
end
LA.EaseInOutElastic = function(t)
return t == 0 and 0 or t == 1 and 1 or t < 0.5 and -(2 ^ (20 * t - 10) * math.sin((20 * t - 11.125) * (2 * math.pi) / 4.5)) / 2 or (2 ^ (-20 * t + 10) * math.sin((20 * t - 11.125) * (2 * math.pi) / 4.5)) / 2 + 1
end
LA.EaseInQuad = function(t)
return t ^ 2
end
LA.EaseOutQuad = function(t)
return 1 - (1 - t) ^ 2
end
LA.EaseInOutQuad = function(t)
return t < 0.5 and 2 * t ^ 2 or 1 - (-2 * t + 2) ^ 2 / 2
end
LA.EaseInQuart = function(t)
return t ^ 4
end
LA.EaseOutQuart = function(t)
return 1 - (1 - t) ^ 4
end
LA.EaseInOutQuart = function(t)
return t < 0.5 and 8 * t ^ 4 or 1 - ((-2 * t + 2) ^ 4) / 2
end
LA.EaseInExpo = function(t)
return t == 0 and 0 or 2 ^ (10 * t - 10)
end
LA.EaseOutExpo = function(t)
return t == 1 and 1 or 1 - 2 ^ (-10 * t)
end
LA.EaseInOutExpo = function(t)
return t == 0 and 0 or t == 1 and 1 or t < 0.5 and 2 ^ (20 * t - 10) / 2 or (2 - 2 ^ (-20 * t + 10)) / 2
end
LA.EaseInBack = function(t)
return 2.70158 * t ^ 3 - 1.70158 * t ^ 2
end
LA.EaseOutBack = function(t)
return 1 + 2.70158 * (t - 1) ^ 3 + 1.70158 * (t - 1) ^ 2
end
LA.EaseInOutBack = function(t)
return t < 0.5 and (2 * t) ^ 2 * ((1.70158 + 1) * 2 * t - 1.70158) / 2 or ((2 * t - 2) ^ 2 * ((1.70158 + 1) * (t * 2 - 2) + 1.70158) + 2) / 2
end
LA.EaseInBounce = function(t)
print(1 - LA.EaseOutBounce(1 - t))
return 1 - LA.EaseOutBounce(1 - t)
end
LA.EaseOutBounce = function(t)
if t < 1 / 2.75 then
return 7.5625 * t ^ 2
elseif t < 2 / 2.75 then
return 7.5625 * (t - 1.5 / 2.75) ^ 2 + 0.75
elseif t < 2.5 / 2.75 then
return 7.5625 * (t - 2.25 / 2.75) ^ 2 + 0.9375
else
return 7.5625 * (t - 2.625 / 2.75) ^ 2 + 0.984375
end
end
LA.EaseInOutBounce = function(t)
return t < 0.5 and (1 - LA.EaseOutBounce(1 - 2 * t)) / 2 or (1 + LA.EaseOutBounce(2 * t - 1)) / 2
end
LA.EaseIn = function(t, easingType)
easingType = string.lower(easingType)
if easingType == "linear" then
return t
elseif easingType == "sine" then
return LA.EaseInSine(t)
elseif easingType == "cubic" then
return LA.EaseInCubic(t)
elseif easingType == "quint" then
return LA.EaseInQuint(t)
elseif easingType == "circ" then
return LA.EaseInCirc(t)
elseif easingType == "elastic" then
return LA.EaseInElastic(t)
elseif easingType == "quad" then
return LA.EaseInQuad(t)
elseif easingType == "quart" then
return LA.EaseInQuart(t)
elseif easingType == "expo" then
return LA.EaseInExpo(t)
elseif easingType == "back" then
return LA.EaseInBack(t)
elseif easingType == "bounce" then
return LA.EaseInBounce(t)
end
end
LA.EaseOut = function(t, easingType)
easingType = string.lower(easingType)
if easingType == "linear" then
return t
elseif easingType == "sine" then
return LA.EaseOutSine(t)
elseif easingType == "cubic" then
return LA.EaseOutCubic(t)
elseif easingType == "quint" then
return LA.EaseOutQuint(t)
elseif easingType == "circ" then
return LA.EaseOutCirc(t)
elseif easingType == "elastic" then
return LA.EaseOutElastic(t)
elseif easingType == "quad" then
return LA.EaseOutQuad(t)
elseif easingType == "quart" then
return LA.EaseOutQuart(t)
elseif easingType == "expo" then
return LA.EaseOutExpo(t)
elseif easingType == "back" then
return LA.EaseOutBack(t)
elseif easingType == "bounce" then
return LA.EaseOutBounce(t)
end
end
LA.EaseInOut = function(t, easingType)
easingType = string.lower(easingType)
if easingType == "linear" then
return t
elseif easingType == "sine" then
return LA.EaseInOutSine(t)
elseif easingType == "cubic" then
return LA.EaseInOutCubic(t)
elseif easingType == "quint" then
return LA.EaseInOutQuint(t)
elseif easingType == "circ" then
return LA.EaseInOutCirc(t)
elseif easingType == "elastic" then
return LA.EaseInOutElastic(t)
elseif easingType == "quad" then
return LA.EaseInOutQuad(t)
elseif easingType == "quart" then
return LA.EaseInOutQuart(t)
elseif easingType == "expo" then
return LA.EaseInOutExpo(t)
elseif easingType == "back" then
return LA.EaseInOutBack(t)
elseif easingType == "bounce" then
return LA.EaseInOutBounce(t)
end
end
LA.EaseInVector = function(a, b, t, easingType)
local tEase = LA.EaseIn(t, easingType)
local x, y, z = a.x, a.y, a.z
local x2, y2, z2 = b.x, b.y, b.z
local x3, y3, z3 = x2 - x, y2 - y, z2 - z
local x4, y4, z4 = x3 * tEase, y3 * tEase, z3 * tEase
local x5, y5, z5 = x + x4, y + y4, z + z4
return vector3(x5, y5, z5)
end
LA.EaseOutVector = function(a, b, t, easingType)
local tEase = LA.EaseOut(t, easingType)
local x, y, z = a.x, a.y, a.z
local x2, y2, z2 = b.x, b.y, b.z
local x3, y3, z3 = x2 - x, y2 - y, z2 - z
local x4, y4, z4 = x3 * tEase, y3 * tEase, z3 * tEase
local x5, y5, z5 = x + x4, y + y4, z + z4
return vector3(x5, y5, z5)
end
LA.EaseInOutVector = function(a, b, t, easingType)
local tEase = LA.EaseInOut(t, easingType)
local x, y, z = a.x, a.y, a.z
local x2, y2, z2 = b.x, b.y, b.z
local x3, y3, z3 = x2 - x, y2 - y, z2 - z
local x4, y4, z4 = x3 * tEase, y3 * tEase, z3 * tEase
local x5, y5, z5 = x + x4, y + y4, z + z4
return vector3(x5, y5, z5)
end
LA.EaseVector = function(inout, a, b, t, easingType)
inout = string.lower(inout)
if inout == "in" then
return LA.EaseInVector(a, b, t, easingType)
elseif inout == "out" then
return LA.EaseOutVector(a, b, t, easingType)
elseif inout == "inout" then
return LA.EaseInOutVector(a, b, t, easingType)
end
assert(false, "Invalid type")
end
LA.EaseOnAxis = function(inout, a, b, t, easingType, axis)
axis = axis or vector3(0, 0, 0)
local x, y, z = a.x, a.y, a.z
local increment = 0
if inout == "in" then
increment = LA.EaseIn(t, easingType)
elseif inout == "out" then
increment = LA.EaseOut(t, easingType)
elseif inout == "inout" then
increment = LA.EaseInOut(t, easingType)
end
return axis * increment
end
LA.TestCheck = function(point1, point2)
local dx = point1.x - point2.x
local dy = point1.y - point2.y
local dz = point1.z - point2.z
return math.sqrt(dx*dx + dy*dy + dz*dz)
end
LA.BoxZoneCheck = function(point, lower, upper)
local x1, y1, z1 = lower.x, lower.y, lower.z
local x2, y2, z2 = upper.x, upper.y, upper.z
return point.x > x1 and point.x < x2 and point.y > y1 and point.y < y2 and point.z > z1 and point.z < z2
end
LA.DistanceCheck = function(pointA, pointB)
return #(pointA - pointB) < 0.5
end
LA.Chance = function(chance)
assert(chance, "Chance must be passed")
assert(type(chance) == "number", "Chance must be a number")
assert(chance >= 0 and chance <= 100, "Chance must be between 0 and 100")
return math.random(1, 100) <= chance
end
LA.Vector4To3 = function(vector4)
assert(vector4, "Vector4 must be passed")
assert(type(vector4) == "vector4", "Vector4 must be a vector4")
return vector3(vector4.x, vector4.y, vector4.z), vector4.w
end
LA.Dot = function(vectorA, vectorB)
return vectorA.x * vectorB.x + vectorA.y * vectorB.y + vectorA.z * vectorB.z
end
LA.Length = function(vector)
return math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z)
end
LA.Normalize = function(vector)
local length = LA.Length(vector)
return vector3(vector.x / length, vector.y / length, vector.z / length)
end
LA.Create2DRotationMatrix = function(angle) -- angle in radians
local c = math.cos(angle)
local s = math.sin(angle)
return {
c, -s, 0, 0,
s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
}
end
LA.Create3DAxisRotationMatrix = function(vector)
local yaw = vector.z
local pitch = vector.x
local roll = vector.y
local cy = math.cos(yaw)
local sy = math.sin(yaw)
local cp = math.cos(pitch)
local sp = math.sin(pitch)
local cr = math.cos(roll)
local sr = math.sin(roll)
local matrix = {
vector3(cp * cy, cp * sy, -sp),
vector3(cy * sp * sr - cr * sy, sy * sp * sr + cr * cy, cp * sr),
vector3(cy * sp * cr + sr * sy, cr * cy * sp - sr * sy, cp * cr)
}
return matrix
end
LA.Circle = function(t, radius, center)
local x = radius * math.cos(t) + center.x
local y = radius * math.sin(t) + center.y
return vector3(x, y, center.z)
end
LA.Clamp = function(value, min, max)
return math.min(math.max(value, min), max)
end
LA.CrossProduct = function(a, b)
local x = a.y * b.z - a.z * b.y
local y = a.z * b.x - a.x * b.z
local z = a.x * b.y - a.y * b.x
return vector3(x, y, z)
end
LA.CreateTranslateMatrix = function(x, y, z)
return {
1, 0, 0, x,
0, 1, 0, y,
0, 0, 1, z,
0, 0, 0, 1,
}
end
LA.MultiplyMatrix = function(m1, m2)
local result = {}
for i = 1, 4 do
for j = 1, 4 do
local sum = 0
for k = 1, 4 do
sum = sum + m1[(i - 1) * 4 + k] * m2[(k - 1) * 4 + j]
end
result[(i - 1) * 4 + j] = sum
end
end
return result
end
LA.MultiplyMatrixByVector = function(m, v)
local result = {}
for i = 1, 4 do
local sum = 0
for j = 1, 4 do
sum = sum + m[(i - 1) * 4 + j] * v[j]
end
result[i] = sum
end
return result
end
--Slines
LA.SplineLerp = function(nodes, start, endPos, t)
-- Ensure t is clamped between 0 and 1
t = LA.Clamp(t, 0, 1)
-- Find the two closest nodes around t
local prevNode, nextNode = nil, nil
for i = 1, #nodes - 1 do
if nodes[i].time <= t and nodes[i + 1].time >= t then
prevNode, nextNode = nodes[i], nodes[i + 1]
break
end
end
-- Edge cases: If t is before or after the defined range
if not prevNode then return start end
if not nextNode then return endPos end
-- Normalize t within the segment
local segmentT = (t - prevNode.time) / (nextNode.time - prevNode.time)
-- Interpolate the value (y-axis)
local smoothedT = LA.Lerp(prevNode.value, nextNode.value, segmentT)
-- Perform final Lerp using the interpolated smoothing factor
return LA.Lerp(start, endPos, smoothedT)
end
LA.SplineSmooth = function(nodes, points, t)
-- Ensure valid input
if #points < 2 then return points end
t = LA.Clamp(t, 0, 1)
local smoothedPoints = {}
for i = 1, #points - 1 do
local smoothedPos = LA.SplineLerp(nodes, points[i], points[i + 1], t)
table.insert(smoothedPoints, smoothedPos)
end
-- Ensure last point remains unchanged
table.insert(smoothedPoints, points[#points])
return smoothedPoints
end
LA.Spline = function(points, resolution)
-- Ensure valid input
if #points < 2 then return points end
-- Create a list of nodes
local nodes = {}
for i = 1, #points do
table.insert(nodes, { time = i / (#points - 1), value = i })
end
-- Smooth the points
local smoothedPoints = {}
for i = 0, 1, resolution do
local smoothedPos = LA.SplineSmooth(nodes, points, i)
table.insert(smoothedPoints, smoothedPos)
end
return smoothedPoints
end
LA.SplineCatmullRom = function(points, resolution)
-- Ensure valid input
if #points < 4 then return points end
-- Create a list of nodes
local nodes = {}
for i = 1, #points do
table.insert(nodes, { time = i / (#points - 1), value = i })
end
-- Smooth the points
local smoothedPoints = {}
for i = 0, 1, resolution do
local smoothedPos = LA.SplineSmooth(nodes, points, i)
table.insert(smoothedPoints, smoothedPos)
end
return smoothedPoints
end
exports('LA', LA)
return LA
-- local easingTypes = {
-- "linear",
-- "sine",
-- "cubic",
-- "quint",
-- "circ",
-- "elastic",
-- "quad",
-- "quart",
-- "expo",
-- "back",
-- "bounce"
-- }
--local inout = {
-- "in",
-- "out",
-- "inout"
--}
-- LA.LerpAngle = function(a, b, t)
-- local num = math.abs(b - a) % 360
-- local num2 = 360 - num
-- if num < num2 then
-- return a + num * t
-- else
-- return a - num2 * t
-- end
-- end
-- LA.LerpVectorAngle = function(a, b, t)
-- local x, y, z = a.x, a.y, a.z
-- local x2, y2, z2 = b.x, b.y, b.z
-- local x3, y3, z3 = LA.LerpAngle(x, x2, t), LA.LerpAngle(y, y2, t), LA.LerpAngle(z, z2, t)
-- return vector3(x3, y3, z3)
-- end
-- return LA

View file

@ -0,0 +1,146 @@
Math = Math or {}
function Math.Clamp(value, min, max)
return math.min(math.max(value, min), max)
end
function Math.Remap(value, min, max, newMin, newMax)
return newMin + (value - min) / (max - min) * (newMax - newMin)
end
function Math.PointInRadius(radius)
local angle = math.rad(math.random(0, 360))
return vector2(radius * math.cos(angle), radius * math.sin(angle))
end
function Math.Normalize(value, min, max)
if max == min then return 0 end -- Avoid division by zero
return (value - min) / (max - min)
end
function Math.Normalize2D(x, y)
if type(x) == "vector2" then
x, y = x.x, x.y
end
local length = math.sqrt(x*x + y*y)
return length ~= 0 and vector2(x / length, y / length) or vector2(0, 0)
end
function Math.Normalize3D(x, y, z)
if type(x) == "vector3" then
x, y, z = x.x, x.y, x.z
end
local length = math.sqrt(x*x + y*y + z*z)
return length ~= 0 and vector3(x / length, y / length, z / length) or vector3(0, 0, 0)
end
function Math.Normalize4D(x, y, z, w)
if type(x) == "vector4" then
x, y, z, w = x.x, x.y, x.z, x.w
end
local length = math.sqrt(x*x + y*y + z*z + w*w)
return length ~= 0 and vector4(x / length, y / length, z / length, w / length) or vector4(0, 0, 0, 0)
end
function Math.DirectionToTarget(fromV3, toV3)
return Math.Normalize3D(toV3.x - fromV3.x, toV3.y - fromV3.y, toV3.z - fromV3.z)
end
function Deg2Rad(deg)
return deg * math.pi / 180.0
end
function RotVector(pos, rot)
local pitch = Deg2Rad(rot.x)
local roll = Deg2Rad(rot.y)
local yaw = Deg2Rad(rot.z)
local cosY = math.cos(yaw)
local sinY = math.sin(yaw)
local cosP = math.cos(pitch)
local sinP = math.sin(pitch)
local cosR = math.cos(roll)
local sinR = math.sin(roll)
local m11 = cosY * cosR + sinY * sinP * sinR
local m12 = sinR * cosP
local m13 = -sinY * cosR + cosY * sinP * sinR
local m21 = -cosY * sinR + sinY * sinP * cosR
local m22 = cosR * cosP
local m23 = sinR * sinY + cosY * sinP * cosR
local m31 = sinY * cosP
local m32 = -sinP
local m33 = cosY * cosP
return vector3(pos.x * m11 + pos.y * m21 + pos.z * m31, pos.x * m12 + pos.y * m22 + pos.z * m32, pos.x * m13 + pos.y * m23 + pos.z * m33)
end
function Math.GetOffsetFromMatrix(position, rotation, offset)
local rotated = RotVector(offset, rotation)
print("Rotated: " .. tostring(rotated))
return position + rotated
end
function Math.InBoundary(pos, boundary)
if not boundary then return true end
local x, y, z = table.unpack(pos)
-- Handle legacy min/max boundary format for backwards compatibility
if boundary.min and boundary.max then
local minX, minY, minZ = table.unpack(boundary.min)
local maxX, maxY, maxZ = table.unpack(boundary.max)
return x >= minX and x <= maxX and y >= minY and y <= maxY and z >= minZ and z <= maxZ
end
-- Handle list of points (polygon boundary)
if boundary.points and #boundary.points > 0 then
local points = boundary.points
local minZ = boundary.minZ or -math.huge
local maxZ = boundary.maxZ or math.huge
-- Check Z bounds first
if z < minZ or z > maxZ then
return false
end
-- Point-in-polygon test using ray casting algorithm (improved version)
local inside = false
local n = #points
for i = 1, n do
local j = i == n and 1 or i + 1 -- Next point (wrap around)
local xi, yi = points[i].x or points[i][1], points[i].y or points[i][2]
local xj, yj = points[j].x or points[j][1], points[j].y or points[j][2]
-- Ensure xi, yi, xj, yj are numbers
if not (xi and yi and xj and yj) then
goto continue
end
-- Ray casting test
if ((yi > y) ~= (yj > y)) then
-- Calculate intersection point
local intersect = (xj - xi) * (y - yi) / (yj - yi) + xi
if x < intersect then
inside = not inside
end
end
::continue::
end
return inside
end
-- Fallback to true if boundary format is not recognized
return true
end
exports('Math', Math)
return Math

View file

@ -0,0 +1,110 @@
local Perlin = {}
-- Permutation table for random gradients
local perm = {}
local grad3 = {
{1,1,0}, {-1,1,0}, {1,-1,0}, {-1,-1,0},
{1,0,1}, {-1,0,1}, {1,0,-1}, {-1,0,-1},
{0,1,1}, {0,-1,1}, {0,1,-1}, {0,-1,-1}
}
-- Initialize permutation table
local function ShufflePermutation()
local p = {}
for i = 0, 255 do
p[i] = i
end
for i = 255, 1, -1 do
local j = math.random(i + 1) - 1
p[i], p[j] = p[j], p[i]
end
for i = 0, 255 do
perm[i] = p[i]
perm[i + 256] = p[i] -- Repeat for wrapping
end
end
ShufflePermutation() -- Randomize on load
-- Dot product helper
local function Dot(g, x, y, z)
return g[1] * x + g[2] * y + (z and g[3] * z or 0)
end
-- Fade function (smootherstep)
local function Fade(t)
return t * t * t * (t * (t * 6 - 15) + 10)
end
-- Linear interpolation
local function Lerp(a, b, t)
return a + (b - a) * t
end
-- **Perlin Noise 1D**
function Perlin.Noise1D(x)
local X = math.floor(x) & 255
x = x - math.floor(x)
local u = Fade(x)
local a = perm[X]
local b = perm[X + 1]
return Lerp(a, b, u) * (2 / 255) - 1
end
-- **Perlin Noise 2D**
function Perlin.Noise2D(x, y)
local X = math.floor(x) & 255
local Y = math.floor(y) & 255
x, y = x - math.floor(x), y - math.floor(y)
local u, v = Fade(x), Fade(y)
local aa = perm[X] + Y
local ab = perm[X] + Y + 1
local ba = perm[X + 1] + Y
local bb = perm[X + 1] + Y + 1
return Lerp(
Lerp(Dot(grad3[perm[aa] % 12 + 1], x, y), Dot(grad3[perm[ba] % 12 + 1], x - 1, y), u),
Lerp(Dot(grad3[perm[ab] % 12 + 1], x, y - 1), Dot(grad3[perm[bb] % 12 + 1], x - 1, y - 1), u),
v
)
end
-- **Perlin Noise 3D**
function Perlin.Noise3D(x, y, z)
local X = math.floor(x) & 255
local Y = math.floor(y) & 255
local Z = math.floor(z) & 255
x, y, z = x - math.floor(x), y - math.floor(y), z - math.floor(z)
local u, v, w = Fade(x), Fade(y), Fade(z)
local aaa = perm[X] + Y + Z
local aba = perm[X] + Y + Z + 1
local aab = perm[X] + Y + 1 + Z
local abb = perm[X] + Y + 1 + Z + 1
local baa = perm[X + 1] + Y + Z
local bba = perm[X + 1] + Y + Z + 1
local bab = perm[X + 1] + Y + 1 + Z
local bbb = perm[X + 1] + Y + 1 + Z + 1
return Lerp(
Lerp(
Lerp(Dot(grad3[perm[aaa] % 12 + 1], x, y, z), Dot(grad3[perm[baa] % 12 + 1], x - 1, y, z), u),
Lerp(Dot(grad3[perm[aab] % 12 + 1], x, y - 1, z), Dot(grad3[perm[bab] % 12 + 1], x - 1, y - 1, z), u),
v
),
Lerp(
Lerp(Dot(grad3[perm[aba] % 12 + 1], x, y, z - 1), Dot(grad3[perm[bba] % 12 + 1], x - 1, y, z - 1), u),
Lerp(Dot(grad3[perm[abb] % 12 + 1], x, y - 1, z - 1), Dot(grad3[perm[bbb] % 12 + 1], x - 1, y - 1, z - 1), u),
v
),
w
)
end
exports('Perlin', Perlin)
return Perlin

View file

@ -0,0 +1,34 @@
Prints = Prints or {}
local function printMessage(level, color, message)
if type(message) == 'table' then
message = json.encode(message)
end
print(color .. '[' .. level .. ']:', message)
end
---This will print a colored message to the console with the designated prefix.
---@param message string
Prints.Info = function(message)
printMessage('INFO', '^5', message)
end
---This will print a colored message to the console with the designated prefix.
---@param message string
Prints.Warn = function(message)
printMessage('WARN', '^3', message)
end
---This will print a colored message to the console with the designated prefix.
---@param message string
Prints.Error = function(message)
printMessage('ERROR', '^1', message)
end
---This will print a colored message to the console with the designated prefix.
---@param message string
Prints.Debug = function(message)
printMessage('DEBUG', '^2', message)
end
return Prints

View file

@ -0,0 +1,324 @@
local Entities = {}
local isServer = IsDuplicityVersion()
local registeredFunctions = {}
ReboundEntities = {}
function ReboundEntities.AddFunction(func)
assert(func, "Func is nil")
table.insert(registeredFunctions, func)
return #registeredFunctions
end
function ReboundEntities.RemoveFunction(id)
assert(id and registeredFunctions[id], "Invalid ID")
registeredFunctions[id] = false
end
function FireFunctions(...)
for _, func in pairs(registeredFunctions) do
func(...)
end
end
function ReboundEntities.Register(entityData)
assert(entityData and entityData.position, "Invalid entity data")
entityData.id = entityData.id or Ids.CreateUniqueId(Entities)
entityData.isServer = isServer
setmetatable(entityData, { __tostring = function() return entityData.id end })
Entities[entityData.id] = entityData
FireFunctions(entityData)
return entityData
end
local function Unregister(id)
Entities[id] = nil
end
function ReboundEntities.GetSyncData(entityData, key)
return entityData[key]
end
function ReboundEntities.GetAll()
return Entities
end
function ReboundEntities.GetById(id)
return Entities[tostring(id)]
end
function ReboundEntities.GetByModel(model)
local entities = {}
for _, entity in pairs(Entities) do
if entity.model == model then
table.insert(entities, entity)
end
end
return entities
end
function ReboundEntities.GetClosest(pos)
local closest, closestDist = nil, 9999
for _, entity in pairs(Entities) do
local dist = #(pos - entity.position)
if dist < closestDist then
closest, closestDist = entity, dist
end
end
return closest
end
function ReboundEntities.GetWithinRadius(pos, radius)
local entities = {}
for _, entity in pairs(Entities) do
if #(pos - entity.position) < radius then
table.insert(entities, entity)
end
end
return entities
end
function ReboundEntities.SetOnSyncKeyChange(entityData, cb)
local data = ReboundEntities.GetById(entityData.id or entityData)
assert(data, "Entity not found")
data.onSyncKeyChange = cb
end
exports('ReboundEntities', ReboundEntities)
if not isServer then goto client end
function ReboundEntities.Create(entityData, src)
local entity = ReboundEntities.Register(entityData)
assert(entity and entity.position, "Invalid entity data")
entity.rotation = entity.rotation or vector3(0, 0, entityData.heading or 0)
TriggerClientEvent(GetCurrentResourceName() .. ":client:CreateReboundEntity", src or -1, entity)
return entity
end
function ReboundEntities.SetCheckRestricted(entityData, cb)
assert(type(cb) == "function", "Check restricted is not a function")
entityData.restricted = cb
end
function ReboundEntities.CheckRestricted(src, entityData)
return entityData.restricted and entityData.restricted(tonumber(src), entityData) or false
end
function ReboundEntities.Refresh(src)
TriggerClientEvent(GetCurrentResourceName() .. ":client:CreateReboundEntities", tonumber(src), ReboundEntities.GetAccessibleList(src))
end
function ReboundEntities.Delete(id)
Unregister(id)
TriggerClientEvent(GetCurrentResourceName() .. ":client:DeleteReboundEntity", -1, id)
end
function ReboundEntities.DeleteMultiple(entityDatas)
local ids = {}
for _, data in pairs(entityDatas) do
Unregister(data.id)
table.insert(ids, data.id)
end
TriggerClientEvent(GetCurrentResourceName() .. ":client:DeleteReboundEntities", -1, ids)
end
function ReboundEntities.CreateMultiple(entityDatas, src, restricted)
local _entityDatas = {}
for _, data in pairs(entityDatas) do
local entity = ReboundEntities.Register(data)
assert(entity and entity.position, "Invalid entity data")
entity.rotation = entity.rotation or vector3(0, 0, 0)
if restricted then ReboundEntities.SetCheckRestricted(entity, restricted) end
if not ReboundEntities.CheckRestricted(src, entity) then
_entityDatas[entity.id] = entity
end
end
TriggerClientEvent(GetCurrentResourceName() .. ":client:CreateReboundEntities", src or -1, _entityDatas)
return _entityDatas
end
function ReboundEntities.SetSyncData(entityData, key, value)
entityData[key] = value
if entityData.onSyncKeyChange then
entityData.onSyncKeyChange(entityData, key, value)
end
TriggerClientEvent(GetCurrentResourceName() .. ":client:SetReboundSyncData", -1, entityData.id, key, value)
end
function ReboundEntities.GetAccessibleList(src)
local list = {}
for _, data in pairs(ReboundEntities.GetAll()) do
if not ReboundEntities.CheckRestricted(src, data) then
list[data.id] = data
end
end
return list
end
RegisterNetEvent("playerJoining", function()
ReboundEntities.Refresh(source)
end)
AddEventHandler('onResourceStart', function(resource)
if resource ~= GetCurrentResourceName() then return end
Wait(1000)
for _, src in pairs(GetPlayers()) do
ReboundEntities.Refresh(src)
end
end)
::client::
if isServer then return ReboundEntities end
function ReboundEntities.LoadModel(model)
assert(model, "Model is nil")
model = type(model) == "number" and model or GetHashKey(model) -- Corrected to GetHashKey
RequestModel(model)
for i = 1, 100 do
if HasModelLoaded(model) then return model end
Wait(100)
end
error(string.format("Failed to load model %s", model))
end
function ReboundEntities.Spawn(entityData)
local model = entityData.model
local position = entityData.position
assert(position, "Position is nil")
local rotation = entityData.rotation or vector3(0, 0, 0)
local entity = model and CreateObject(ReboundEntities.LoadModel(model), position.x, position.y, position.z, false, false, false)
if entity then SetEntityRotation(entity, rotation.x, rotation.y, rotation.z) end
if entityData.onSpawn then entity = entityData.onSpawn(entityData, entity) or entity end
return entity, true
end
function ReboundEntities.SetOnSpawn(id, cb)
local entity = ReboundEntities.GetById(tostring(id))
assert(entity, "Entity not found")
entity.onSpawn = cb
return true
end
function ReboundEntities.Despawn(entityData)
local entity = entityData.entity
if entityData.onDespawn then entityData.onDespawn(entityData, entity) end
if entity and DoesEntityExist(entity) then DeleteEntity(entity) end
return nil, nil
end
function ReboundEntities.SetOnDespawn(id, cb)
local entity = ReboundEntities.GetById(tostring(id))
assert(entity, "Entity not found")
entity.onDespawn = cb
return true
end
local spawnLoopRunning = false
function ReboundEntities.SpawnLoop(distanceToCheck, waitTime)
if spawnLoopRunning then return end
spawnLoopRunning = true
distanceToCheck = distanceToCheck or 50
waitTime = waitTime or 2500
CreateThread(function()
while spawnLoopRunning do
for _, entity in pairs(Entities) do
local pos = GetEntityCoords(PlayerPedId())
local position = vector3(entity.position.x, entity.position.y, entity.position.z)
local dist = #(pos - position)
if dist <= distanceToCheck and not entity.entity then
entity.entity, entity.inRange = ReboundEntities.Spawn(entity)
elseif dist > distanceToCheck and entity.entity then
entity.entity, entity.inRange = ReboundEntities.Despawn(entity)
end
end
Wait(waitTime)
end
end)
end
function ReboundEntities.GetByEntity(entity)
for _, data in pairs(Entities) do
if data.entity == entity then return data end
end
end
function ReboundEntities.CreateClient(entityData)
local entity = ReboundEntities.Register(entityData)
if entity then ReboundEntities.SpawnLoop() end
return entity
end
function ReboundEntities.DeleteClient(id)
local entityData = ReboundEntities.GetById(tostring(id))
assert(not entityData.isServer, "Cannot delete server entity from client")
if entityData.entity then ReboundEntities.Despawn(entityData) end
Unregister(id)
return true
end
function ReboundEntities.CreateMultipleClient(entityDatas)
local _entityDatas = {}
for _, data in pairs(entityDatas) do
local entityData = ReboundEntities.CreateClient(data)
_entityDatas[entityData.id] = entityData
end
return _entityDatas
end
function ReboundEntities.DeleteMultipleClient(ids)
for _, id in pairs(ids) do
ReboundEntities.DeleteClient(id)
end
end
local function DeleteFromServer(id)
local entityData = ReboundEntities.GetById(tostring(id))
if entityData then
entityData.isServer = nil
ReboundEntities.DeleteClient(id)
end
end
local function DeleteMultipleFromServer(ids)
for _, id in pairs(ids) do
DeleteFromServer(id)
end
end
RegisterNetEvent(GetCurrentResourceName() .. ":client:CreateReboundEntity", function(entityData)
ReboundEntities.CreateClient(entityData)
end)
RegisterNetEvent(GetCurrentResourceName() .. ":client:DeleteReboundEntity", function(id)
DeleteFromServer(id)
end)
RegisterNetEvent(GetCurrentResourceName() .. ":client:CreateReboundEntities", function(entityDatas)
ReboundEntities.CreateMultipleClient(entityDatas)
end)
RegisterNetEvent(GetCurrentResourceName() .. ":client:DeleteReboundEntities", function(ids)
ReboundEntities.DeleteMultipleClient(ids)
end)
RegisterNetEvent(GetCurrentResourceName() .. ":client:SetReboundSyncData", function(id, key, value)
local entityData = ReboundEntities.GetById(id)
if not entityData then return end
entityData[key] = value
if entityData.onSyncKeyChange then
entityData.onSyncKeyChange(entityData, key, value)
end
end)
AddEventHandler('onResourceStop', function(resource)
if resource ~= GetCurrentResourceName() then return end
spawnLoopRunning = false
for _, entity in pairs(Entities) do
if entity.entity then DeleteEntity(tonumber(entity.entity)) end
end
end)
return ReboundEntities

View file

@ -0,0 +1,45 @@
local ReboundEntities = ReboundEntities or Require("lib/utility/shared/rebound_entities.lua") -- Fixed path
local Action = Action or Require("lib/entities/shared/actions.lua") -- Fixed path and name
local LA = LA or Require("lib/utility/shared/la.lua") -- Fixed path
REA = {}
if not IsDuplicityVersion() then goto client end
if IsDuplicityVersion() then return end
::client::
local ActionsInProgress = {}
Action.Create("LerpToPosition", function(reboundId, start, position, duration, shouldRepeat, overrideStartTime)
local re = ReboundEntities.GetById(reboundId) -- Use GetById
assert(re, "Rebound entity not found")
local entity = re and re.entity
if not entity or not DoesEntityExist(entity) then return end -- Check existence
local startTime = GetGameTimer()
local endTime = overrideStartTime or startTime + duration
ActionsInProgress[reboundId] = {reboundId, start, position, duration, shouldRepeat, startTime}
local t = 0
CreateThread(function()
while shouldRepeat or t < 1 do
-- Check if entity still exists
if not re.entity or not DoesEntityExist(re.entity) then
ActionsInProgress[reboundId] = nil
break
end
entity = re.entity -- Update entity handle in case it changed (unlikely but safe)
t = (GetGameTimer() - startTime) / duration
local lerpPosition = LA.LerpVector(start, position, t)
SetEntityCoordsNoOffset(entity, lerpPosition.x, lerpPosition.y, lerpPosition.z, false, false, false) -- Use NoOffset
Wait(0)
end
-- Set final position if not repeating
if not shouldRepeat and re.entity and DoesEntityExist(re.entity) then
SetEntityCoordsNoOffset(re.entity, position.x, position.y, position.z, false, false, false)
end
ActionsInProgress[reboundId] = nil -- Clear when done
end)
end)

View file

@ -0,0 +1,138 @@
Table = {}
Table.CheckPopulated = function(tbl)
if #tbl == 0 then
for _, _ in pairs(tbl) do
return true
end
return false
end
return true
end
Table.DeepClone = function(tbl, out, omit)
if type(tbl) ~= "table" then return tbl end
local new = out or {}
omit = omit or {}
for key, data in pairs(tbl) do
if not omit[key] then
if type(data) == "table" then
new[key] = Table.DeepClone(data)
else
new[key] = data
end
end
end
return new
end
Table.TableContains = function(tbl, search, nested)
for _, v in pairs(tbl) do
if nested and type(v) == "table" then
return Table.TableContains(v, search)
elseif v == search then
return true, v
end
end
return false
end
Table.TableContainsKey = function(tbl, search)
for k, _ in pairs(tbl) do
if k == search then
return true, k
end
end
return false
end
Table.TableGetKeys = function(tbl)
local keys = {}
for k ,_ in pairs(tbl) do
table.insert(keys,k)
end
return keys
end
Table.GetClosest = function(coords, tbl)
local closestPoint = nil
local dist = math.huge
for k, v in pairs(tbl) do
local c = v.coords
local d = c and #(coords - c)
if d < dist then
dist = d
closestPoint = v
end
end
return closestPoint
end
Table.FindFirstUnoccupiedSlot = function(tbl)
local occupiedSlots = {}
for _, v in pairs(tbl) do
if v.slot then
occupiedSlots[v.slot] = true
end
end
for i = 1, BridgeServerConfig.MaxInventorySlots do
if not occupiedSlots[i] then
return i
end
end
return nil
end
Table.Append = function(tbl1, tbl2)
for _, v in pairs(tbl2) do
table.insert(tbl1, v)
end
return tbl1
end
Table.Split = function(tbl, size)
local new1 = {}
local new2 = {}
size = size or math.floor(#tbl / 2)
if size > #tbl then
assert(false, "Size is greater than the length of the table.")
end
for i = 1, size do
table.insert(new1, tbl[i])
end
for i = size + 1, #tbl do
table.insert(new2, tbl[i])
end
return new1, new2
end
Table.Shuffle = function(tbl)
for i = #tbl, 2, -1 do
local j = math.random(i)
tbl[i], tbl[j] = tbl[j], tbl[i]
end
return tbl
end
Table.Compare = function(a, b)
if type(a) == "table" then
for k, v in pairs(a) do
if not Table.Compare(v, b[k]) then return false end
end
return true
else
return a == b
end
end
Table.Count = function(tbl)
local count = 0
for _ in pairs(tbl) do
count = count + 1
end
return count
end
exports("Table", Table)
return Table

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "Jy het nie genoeg geld om hierdie item te koop nie",
"Input": "Hoeveelheid Invoer",
"PurchaseAmount": "Aankoop Hoeveelheid",
"PurchasedItem": "Jy het gekoop ",
"CurrencySymbol": "$ %s",
"Confirm": "Bevestig Aankoop",
"AreYouSure": "Is jy seker jy wil 'n %s koop",
"PayByCash": "Betaal %s met kontant",
"PayByCard": "Betaal %s met kaart",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Winkel Naam"
},
"UNITTEST": {
"UNITTESTA": "Ek is 'n %s %s",
"locale-unit-test": "Dit is 'n toets string"
},
"clipboard": {
"copy": "Gekopieer na klipbord"
},
"placeable_object": {
"no_prop_defined": "Jy het nie enige voorwerp gedefinieer om te plaas nie",
"cant_place_here": "Jy kan nie hierdie voorwerp hier plaas nie",
"place_object_place": "[E] Plaas \n",
"place_object_cancel": "[X] Kanselleer \n",
"place_object_scroll_up": "[Scroll Up] Draai regs \n",
"place_object_scroll_down": "[Scroll Down] Draai links \n",
"object_place": "[%s] Plaas \n",
"object_cancel": "[%s] Kanselleer \n",
"object_scroll_up": "[%s] Draai regs \n",
"object_scroll_down": "[%s] Draai links \n",
"depth_modifier": "[%s] Diepte Beheer Wysiging \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "ليس لديك ما يكفي من المال لشراء هذا العنصر",
"Input": "إدخال الكمية",
"PurchaseAmount": "كمية الشراء",
"PurchasedItem": "لقد قمت بشراء ",
"CurrencySymbol": "$ %s",
"Confirm": "تأكيد الشراء",
"AreYouSure": "هل أنت متأكد أنك تريد شراء %s",
"PayByCash": "ادفع %s نقداً",
"PayByCard": "ادفع %s بالبطاقة",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "اسم المتجر"
},
"UNITTEST": {
"UNITTESTA": "أنا %s %s",
"locale-unit-test": "هذا نص تجريبي"
},
"clipboard": {
"copy": "تم النسخ إلى الحافظة"
},
"placeable_object": {
"no_prop_defined": "لم تحدد أي كائن للوضع",
"cant_place_here": "لا يمكنك وضع هذا الكائن هنا",
"place_object_place": "[E] وضع \n",
"place_object_cancel": "[X] إلغاء \n",
"place_object_scroll_up": "[Scroll Up] استدارة يمين \n",
"place_object_scroll_down": "[Scroll Down] استدارة يسار \n",
"object_place": "[%s] وضع \n",
"object_cancel": "[%s] إلغاء \n",
"object_scroll_up": "[%s] استدارة يمين \n",
"object_scroll_down": "[%s] استدارة يسار \n",
"depth_modifier": "[%s] تعديل التحكم بالعمق \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "Nemáte dostatek peněz na koupi této položky",
"Input": "Zadání množství",
"PurchaseAmount": "Množství nákupu",
"PurchasedItem": "Zakoupili jste ",
"CurrencySymbol": "$ %s",
"Confirm": "Potvrdit nákup",
"AreYouSure": "Jste si jisti, že chcete koupit %s",
"PayByCash": "Zaplatit %s hotovostí",
"PayByCard": "Zaplatit %s kartou",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Název obchodu"
},
"UNITTEST": {
"UNITTESTA": "Jsem %s %s",
"locale-unit-test": "Toto je testovací řetězec"
},
"clipboard": {
"copy": "Zkopírováno do schránky"
},
"placeable_object": {
"no_prop_defined": "Nedefinovali jste žádný objekt k umístění",
"cant_place_here": "Tento objekt nemůžete umístit zde",
"place_object_place": "[E] Umístit \n",
"place_object_cancel": "[X] Zrušit \n",
"place_object_scroll_up": "[Scroll Up] Otočit doprava \n",
"place_object_scroll_down": "[Scroll Down] Otočit doleva \n",
"object_place": "[%s] Umístit \n",
"object_cancel": "[%s] Zrušit \n",
"object_scroll_up": "[%s] Otočit doprava \n",
"object_scroll_down": "[%s] Otočit doleva \n",
"depth_modifier": "[%s] Modifikátor kontroly hloubky \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "Du har ikke nok penge til at købe denne vare",
"Input": "Mængde input",
"PurchaseAmount": "Købs mængde",
"PurchasedItem": "Du har købt ",
"CurrencySymbol": "$ %s",
"Confirm": "Bekræft køb",
"AreYouSure": "Er du sikker på, at du vil købe en %s",
"PayByCash": "Betal %s med kontanter",
"PayByCard": "Betal %s med kort",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Butiksnavn"
},
"UNITTEST": {
"UNITTESTA": "Jeg er en %s %s",
"locale-unit-test": "Dette er en test streng"
},
"clipboard": {
"copy": "Kopieret til udklipsholder"
},
"placeable_object": {
"no_prop_defined": "Du har ikke defineret noget objekt at placere",
"cant_place_here": "Du kan ikke placere dette objekt her",
"place_object_place": "[E] Placer \n",
"place_object_cancel": "[X] Annuller \n",
"place_object_scroll_up": "[Scroll Up] Drej til højre \n",
"place_object_scroll_down": "[Scroll Down] Drej til venstre \n",
"object_place": "[%s] Placer \n",
"object_cancel": "[%s] Annuller \n",
"object_scroll_up": "[%s] Drej til højre \n",
"object_scroll_down": "[%s] Drej til venstre \n",
"depth_modifier": "[%s] Dybde kontrol modifier \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "Sie haben nicht genug Geld, um diesen Artikel zu kaufen",
"Input": "Mengen-Eingabe",
"PurchaseAmount": "Kaufmenge",
"PurchasedItem": "Sie haben gekauft ",
"CurrencySymbol": "$ %s",
"Confirm": "Kauf bestätigen",
"AreYouSure": "Sind Sie sicher, dass Sie einen %s kaufen möchten",
"PayByCash": "%s mit Bargeld bezahlen",
"PayByCard": "%s mit Karte bezahlen",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Geschäftsname"
},
"UNITTEST": {
"UNITTESTA": "Ich bin ein %s %s",
"locale-unit-test": "Dies ist ein Test-String"
},
"clipboard": {
"copy": "In die Zwischenablage kopiert"
},
"placeable_object": {
"no_prop_defined": "Sie haben kein Objekt zum Platzieren definiert",
"cant_place_here": "Sie können dieses Objekt hier nicht platzieren",
"place_object_place": "[E] Platzieren \n",
"place_object_cancel": "[X] Abbrechen \n",
"place_object_scroll_up": "[Scroll Up] Nach rechts drehen \n",
"place_object_scroll_down": "[Scroll Down] Nach links drehen \n",
"object_place": "[%s] Platzieren \n",
"object_cancel": "[%s] Abbrechen \n",
"object_scroll_up": "[%s] Nach rechts drehen \n",
"object_scroll_down": "[%s] Nach links drehen \n",
"depth_modifier": "[%s] Tiefenkontrolle Modifikator \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "Δεν έχετε αρκετά χρήματα για να αγοράσετε αυτό το αντικείμενο",
"Input": "Εισαγωγή ποσότητας",
"PurchaseAmount": "Ποσότητα αγοράς",
"PurchasedItem": "Αγοράσατε ",
"CurrencySymbol": "$ %s",
"Confirm": "Επιβεβαίωση αγοράς",
"AreYouSure": "Είστε βέβαιοι ότι θέλετε να αγοράσετε ένα %s",
"PayByCash": "Πληρωμή %s με μετρητά",
"PayByCard": "Πληρωμή %s με κάρτα",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Όνομα καταστήματος"
},
"UNITTEST": {
"UNITTESTA": "Είμαι ένας %s %s",
"locale-unit-test": "Αυτό είναι ένα δοκιμαστικό κείμενο"
},
"clipboard": {
"copy": "Αντιγράφηκε στο clipboard"
},
"placeable_object": {
"no_prop_defined": "Δεν ορίσατε κανένα αντικείμενο για τοποθέτηση",
"cant_place_here": "Δεν μπορείτε να τοποθετήσετε αυτό το αντικείμενο εδώ",
"place_object_place": "[E] Τοποθέτηση \n",
"place_object_cancel": "[X] Ακύρωση \n",
"place_object_scroll_up": "[Scroll Up] Περιστροφή δεξιά \n",
"place_object_scroll_down": "[Scroll Down] Περιστροφή αριστερά \n",
"object_place": "[%s] Τοποθέτηση \n",
"object_cancel": "[%s] Ακύρωση \n",
"object_scroll_up": "[%s] Περιστροφή δεξιά \n",
"object_scroll_down": "[%s] Περιστροφή αριστερά \n",
"depth_modifier": "[%s] Τροποποιητής ελέγχου βάθους \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "Ye don't have enough booty to plunder this here item",
"Input": "Amount o' Booty",
"PurchaseAmount": "Plunder Quantity",
"PurchasedItem": "Ye Have Plundered ",
"CurrencySymbol": "$ %s",
"Confirm": "Aye, Plunder!",
"AreYouSure": "Be ye sure ye want to plunder a %s?",
"PayByCash": "Pay %s with doubloons",
"PayByCard": "Pay %s with the magic card",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Pirate's Emporium"
},
"UNITTEST": {
"UNITTESTA": "I be a %s %s",
"locale-unit-test": "This be a test string, arrr!"
},
"clipboard": {
"copy": "Copied to ye clipboard, matey!"
},
"placeable_object": {
"no_prop_defined": "Ye didn't pick any booty to place, ye scallywag!",
"cant_place_here": "Ye can't be plunderin' here!",
"place_object_place": "[E] Place, arrr! \n",
"place_object_cancel": "[X] Abandon Ship! \n",
"place_object_scroll_up": "[Scroll Up] Turn starboard \n",
"place_object_scroll_down": "[Scroll Down] Turn port \n",
"object_place": "[%s] Place, arrr! \n",
"object_cancel": "[%s] Abandon Ship! \n",
"object_scroll_up": "[%s] Turn starboard \n",
"object_scroll_down": "[%s] Turn port \n",
"depth_modifier": "[%s] Depth Control, savvy? \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "You don't have enough money to buy this item",
"Input": "Amount Input",
"PurchaseAmount": "Purchase Quantity",
"PurchasedItem": "You Have Purchased ",
"CurrencySymbol": "$ %s",
"Confirm": "Confirm Purchase",
"AreYouSure": "Are you sure you want to purchase a %s",
"PayByCash": "Pay %s with cash",
"PayByCard": "Pay %s with card",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Shop Name"
},
"UNITTEST": {
"UNITTESTA": "I'm a %s %s",
"locale-unit-test": "This is a test string"
},
"clipboard": {
"copy": "Copied to clipboard"
},
"placeable_object": {
"no_prop_defined": "You didn't define any object to place",
"cant_place_here": "You can't place this object here",
"place_object_place": "[E] Place \n",
"place_object_cancel": "[X] Cancel \n",
"place_object_scroll_up": "[Scroll Up] Turn right \n",
"place_object_scroll_down": "[Scroll Down] Turn left \n",
"object_place": "[%s] Place \n",
"object_cancel": "[%s] Cancel \n",
"object_scroll_up": "[%s] Turn right \n",
"object_scroll_down": "[%s] Turn left \n",
"depth_modifier": "[%s] Depth Control Modifier \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "No tienes suficiente dinero para comprar este artículo",
"Input": "Entrada de cantidad",
"PurchaseAmount": "Cantidad de compra",
"PurchasedItem": "Has comprado ",
"CurrencySymbol": "$ %s",
"Confirm": "Confirmar compra",
"AreYouSure": "¿Estás seguro de que quieres comprar un %s?",
"PayByCash": "Pagar %s en efectivo",
"PayByCard": "Pagar %s con tarjeta",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Nombre de la tienda"
},
"UNITTEST": {
"UNITTESTA": "Soy un %s %s",
"locale-unit-test": "Esta es una cadena de prueba"
},
"clipboard": {
"copy": "Copiado al portapapeles"
},
"placeable_object": {
"no_prop_defined": "No has definido ningún objeto para colocar",
"cant_place_here": "No puedes colocar este objeto aquí",
"place_object_place": "[E] Colocar \n",
"place_object_cancel": "[X] Cancelar \n",
"place_object_scroll_up": "[Scroll Up] Girar a la derecha \n",
"place_object_scroll_down": "[Scroll Down] Girar a la izquierda \n",
"object_place": "[%s] Colocar \n",
"object_cancel": "[%s] Cancelar \n",
"object_scroll_up": "[%s] Girar a la derecha \n",
"object_scroll_down": "[%s] Girar a la izquierda \n",
"depth_modifier": "[%s] Modificador de control de profundidad \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "Sinulla ei ole tarpeeksi rahaa ostaaksesi tätä tuotetta",
"Input": "Määrän syöttö",
"PurchaseAmount": "Ostomäärä",
"PurchasedItem": "Olet ostanut ",
"CurrencySymbol": "$ %s",
"Confirm": "Vahvista osto",
"AreYouSure": "Oletko varma, että haluat ostaa %s",
"PayByCash": "Maksa %s käteisellä",
"PayByCard": "Maksa %s kortilla",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Kaupan nimi"
},
"UNITTEST": {
"UNITTESTA": "Olen %s %s",
"locale-unit-test": "Tämä on testimerkki"
},
"clipboard": {
"copy": "Kopioitu leikepöydälle"
},
"placeable_object": {
"no_prop_defined": "Et ole määrittänyt mitään objektia sijoitettavaksi",
"cant_place_here": "Et voi sijoittaa tätä objektia tähän",
"place_object_place": "[E] Sijoita \n",
"place_object_cancel": "[X] Peruuta \n",
"place_object_scroll_up": "[Scroll Up] Käännä oikealle \n",
"place_object_scroll_down": "[Scroll Down] Käännä vasemmalle \n",
"object_place": "[%s] Sijoita \n",
"object_cancel": "[%s] Peruuta \n",
"object_scroll_up": "[%s] Käännä oikealle \n",
"object_scroll_down": "[%s] Käännä vasemmalle \n",
"depth_modifier": "[%s] Syvyyshallinnan muokkaaja \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "Vous n'avez pas assez d'argent pour acheter cet article",
"Input": "Saisie de quantité",
"PurchaseAmount": "Quantité d'achat",
"PurchasedItem": "Vous avez acheté ",
"CurrencySymbol": "$ %s",
"Confirm": "Confirmer l'achat",
"AreYouSure": "Êtes-vous sûr de vouloir acheter un %s",
"PayByCash": "Payer %s en espèces",
"PayByCard": "Payer %s par carte",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Nom du magasin"
},
"UNITTEST": {
"UNITTESTA": "Je suis un %s %s",
"locale-unit-test": "Ceci est une chaîne de test"
},
"clipboard": {
"copy": "Copié dans le presse-papiers"
},
"placeable_object": {
"no_prop_defined": "Vous n'avez défini aucun objet à placer",
"cant_place_here": "Vous ne pouvez pas placer cet objet ici",
"place_object_place": "[E] Placer \n",
"place_object_cancel": "[X] Annuler \n",
"place_object_scroll_up": "[Scroll Up] Tourner à droite \n",
"place_object_scroll_down": "[Scroll Down] Tourner à gauche \n",
"object_place": "[%s] Placer \n",
"object_cancel": "[%s] Annuler \n",
"object_scroll_up": "[%s] Tourner à droite \n",
"object_scroll_down": "[%s] Tourner à gauche \n",
"depth_modifier": "[%s] Modificateur de contrôle de profondeur \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "इस वस्तु को खरीदने के लिए आपके पास पर्याप्त पैसे नहीं हैं",
"Input": "मात्रा इनपुट",
"PurchaseAmount": "खरीदारी की मात्रा",
"PurchasedItem": "आपने खरीदा है ",
"CurrencySymbol": "$ %s",
"Confirm": "खरीदारी की पुष्टि करें",
"AreYouSure": "क्या आप वाकई %s खरीदना चाहते हैं",
"PayByCash": "%s नकदी से भुगतान करें",
"PayByCard": "%s कार्ड से भुगतान करें",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "दुकान का नाम"
},
"UNITTEST": {
"UNITTESTA": "मैं एक %s %s हूं",
"locale-unit-test": "यह एक परीक्षण स्ट्रिंग है"
},
"clipboard": {
"copy": "क्लिपबोर्ड पर कॉपी किया गया"
},
"placeable_object": {
"no_prop_defined": "आपने रखने के लिए कोई वस्तु परिभाषित नहीं की है",
"cant_place_here": "आप इस वस्तु को यहां नहीं रख सकते",
"place_object_place": "[E] रखें \n",
"place_object_cancel": "[X] रद्द करें \n",
"place_object_scroll_up": "[Scroll Up] दाएं मुड़ें \n",
"place_object_scroll_down": "[Scroll Down] बाएं मुड़ें \n",
"object_place": "[%s] रखें \n",
"object_cancel": "[%s] रद्द करें \n",
"object_scroll_up": "[%s] दाएं मुड़ें \n",
"object_scroll_down": "[%s] बाएं मुड़ें \n",
"depth_modifier": "[%s] गहराई नियंत्रण संशोधक \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "Nincs elég pénzed ennek a tételnek a megvásárlásához",
"Input": "Mennyiség bevitel",
"PurchaseAmount": "Vásárlási mennyiség",
"PurchasedItem": "Megvásároltad ",
"CurrencySymbol": "$ %s",
"Confirm": "Vásárlás megerősítése",
"AreYouSure": "Biztosan meg akarsz vásárolni egy %s-t",
"PayByCash": "%s fizetése készpénzzel",
"PayByCard": "%s fizetése kártyával",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Bolt neve"
},
"UNITTEST": {
"UNITTESTA": "Én egy %s %s vagyok",
"locale-unit-test": "Ez egy teszt string"
},
"clipboard": {
"copy": "Vágólapra másolva"
},
"placeable_object": {
"no_prop_defined": "Nem definiáltál semmilyen objektumot elhelyezésre",
"cant_place_here": "Nem helyezheted el ezt az objektumot itt",
"place_object_place": "[E] Elhelyezés \n",
"place_object_cancel": "[X] Mégse \n",
"place_object_scroll_up": "[Scroll Up] Jobbra fordítás \n",
"place_object_scroll_down": "[Scroll Down] Balra fordítás \n",
"object_place": "[%s] Elhelyezés \n",
"object_cancel": "[%s] Mégse \n",
"object_scroll_up": "[%s] Jobbra fordítás \n",
"object_scroll_down": "[%s] Balra fordítás \n",
"depth_modifier": "[%s] Mélység vezérlő módosító \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "Non hai abbastanza denaro per comprare questo articolo",
"Input": "Inserimento quantità",
"PurchaseAmount": "Quantità di acquisto",
"PurchasedItem": "Hai acquistato ",
"CurrencySymbol": "$ %s",
"Confirm": "Conferma acquisto",
"AreYouSure": "Sei sicuro di voler acquistare un %s",
"PayByCash": "Paga %s in contanti",
"PayByCard": "Paga %s con carta",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Nome negozio"
},
"UNITTEST": {
"UNITTESTA": "Sono un %s %s",
"locale-unit-test": "Questa è una stringa di test"
},
"clipboard": {
"copy": "Copiato negli appunti"
},
"placeable_object": {
"no_prop_defined": "Non hai definito alcun oggetto da posizionare",
"cant_place_here": "Non puoi posizionare questo oggetto qui",
"place_object_place": "[E] Posiziona \n",
"place_object_cancel": "[X] Annulla \n",
"place_object_scroll_up": "[Scroll Up] Ruota a destra \n",
"place_object_scroll_down": "[Scroll Down] Ruota a sinistra \n",
"object_place": "[%s] Posiziona \n",
"object_cancel": "[%s] Annulla \n",
"object_scroll_up": "[%s] Ruota a destra \n",
"object_scroll_down": "[%s] Ruota a sinistra \n",
"depth_modifier": "[%s] Modificatore controllo profondità \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "このアイテムを購入するのに十分なお金がありません",
"Input": "数量入力",
"PurchaseAmount": "購入数量",
"PurchasedItem": "購入しました ",
"CurrencySymbol": "$ %s",
"Confirm": "購入を確認",
"AreYouSure": "%sを購入してもよろしいですか",
"PayByCash": "%sを現金で支払う",
"PayByCard": "%sをカードで支払う",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "店舗名"
},
"UNITTEST": {
"UNITTESTA": "私は%s %sです",
"locale-unit-test": "これはテスト文字列です"
},
"clipboard": {
"copy": "クリップボードにコピーされました"
},
"placeable_object": {
"no_prop_defined": "配置するオブジェクトが定義されていません",
"cant_place_here": "このオブジェクトをここに配置することはできません",
"place_object_place": "[E] 配置 \n",
"place_object_cancel": "[X] キャンセル \n",
"place_object_scroll_up": "[Scroll Up] 右回転 \n",
"place_object_scroll_down": "[Scroll Down] 左回転 \n",
"object_place": "[%s] 配置 \n",
"object_cancel": "[%s] キャンセル \n",
"object_scroll_up": "[%s] 右回転 \n",
"object_scroll_down": "[%s] 左回転 \n",
"depth_modifier": "[%s] 深度制御修飾子 \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "이 아이템을 구매할 돈이 부족합니다",
"Input": "수량 입력",
"PurchaseAmount": "구매 수량",
"PurchasedItem": "구매했습니다 ",
"CurrencySymbol": "$ %s",
"Confirm": "구매 확인",
"AreYouSure": "%s를 구매하시겠습니까",
"PayByCash": "%s를 현금으로 지불",
"PayByCard": "%s를 카드로 지불",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "상점 이름"
},
"UNITTEST": {
"UNITTESTA": "나는 %s %s입니다",
"locale-unit-test": "이것은 테스트 문자열입니다"
},
"clipboard": {
"copy": "클립보드에 복사됨"
},
"placeable_object": {
"no_prop_defined": "배치할 객체를 정의하지 않았습니다",
"cant_place_here": "이 객체를 여기에 배치할 수 없습니다",
"place_object_place": "[E] 배치 \n",
"place_object_cancel": "[X] 취소 \n",
"place_object_scroll_up": "[Scroll Up] 오른쪽으로 회전 \n",
"place_object_scroll_down": "[Scroll Down] 왼쪽으로 회전 \n",
"object_place": "[%s] 배치 \n",
"object_cancel": "[%s] 취소 \n",
"object_scroll_up": "[%s] 오른쪽으로 회전 \n",
"object_scroll_down": "[%s] 왼쪽으로 회전 \n",
"depth_modifier": "[%s] 깊이 제어 수정자 \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "Je hebt niet genoeg geld om dit item te kopen",
"Input": "Hoeveelheid invoer",
"PurchaseAmount": "Aankoop hoeveelheid",
"PurchasedItem": "Je hebt gekocht ",
"CurrencySymbol": "$ %s",
"Confirm": "Aankoop bevestigen",
"AreYouSure": "Weet je zeker dat je een %s wilt kopen",
"PayByCash": "Betaal %s contant",
"PayByCard": "Betaal %s met kaart",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Winkelnaam"
},
"UNITTEST": {
"UNITTESTA": "Ik ben een %s %s",
"locale-unit-test": "Dit is een test string"
},
"clipboard": {
"copy": "Gekopieerd naar klembord"
},
"placeable_object": {
"no_prop_defined": "Je hebt geen object gedefinieerd om te plaatsen",
"cant_place_here": "Je kunt dit object hier niet plaatsen",
"place_object_place": "[E] Plaatsen \n",
"place_object_cancel": "[X] Annuleren \n",
"place_object_scroll_up": "[Scroll Up] Rechtsom draaien \n",
"place_object_scroll_down": "[Scroll Down] Linksom draaien \n",
"object_place": "[%s] Plaatsen \n",
"object_cancel": "[%s] Annuleren \n",
"object_scroll_up": "[%s] Rechtsom draaien \n",
"object_scroll_down": "[%s] Linksom draaien \n",
"depth_modifier": "[%s] Diepte controle modifier \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "Du har ikke nok penger til å kjøpe denne varen",
"Input": "Mengde inndata",
"PurchaseAmount": "Kjøpsmengde",
"PurchasedItem": "Du har kjøpt ",
"CurrencySymbol": "$ %s",
"Confirm": "Bekreft kjøp",
"AreYouSure": "Er du sikker på at du vil kjøpe en %s",
"PayByCash": "Betal %s med kontanter",
"PayByCard": "Betal %s med kort",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Butikknavn"
},
"UNITTEST": {
"UNITTESTA": "Jeg er en %s %s",
"locale-unit-test": "Dette er en teststreng"
},
"clipboard": {
"copy": "Kopiert til utklippstavle"
},
"placeable_object": {
"no_prop_defined": "Du har ikke definert noe objekt å plassere",
"cant_place_here": "Du kan ikke plassere dette objektet her",
"place_object_place": "[E] Plasser \n",
"place_object_cancel": "[X] Avbryt \n",
"place_object_scroll_up": "[Scroll Up] Snu til høyre \n",
"place_object_scroll_down": "[Scroll Down] Snu til venstre \n",
"object_place": "[%s] Plasser \n",
"object_cancel": "[%s] Avbryt \n",
"object_scroll_up": "[%s] Snu til høyre \n",
"object_scroll_down": "[%s] Snu til venstre \n",
"depth_modifier": "[%s] Dybdekontroll modifikator \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "Nie masz wystarczająco pieniędzy, aby kupić ten przedmiot",
"Input": "Wprowadzanie ilości",
"PurchaseAmount": "Ilość zakupu",
"PurchasedItem": "Kupiłeś ",
"CurrencySymbol": "$ %s",
"Confirm": "Potwierdź zakup",
"AreYouSure": "Czy na pewno chcesz kupić %s",
"PayByCash": "Zapłać %s gotówką",
"PayByCard": "Zapłać %s kartą",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Nazwa sklepu"
},
"UNITTEST": {
"UNITTESTA": "Jestem %s %s",
"locale-unit-test": "To jest ciąg testowy"
},
"clipboard": {
"copy": "Skopiowano do schowka"
},
"placeable_object": {
"no_prop_defined": "Nie zdefiniowałeś żadnego obiektu do umieszczenia",
"cant_place_here": "Nie możesz umieścić tego obiektu tutaj",
"place_object_place": "[E] Umieść \n",
"place_object_cancel": "[X] Anuluj \n",
"place_object_scroll_up": "[Scroll Up] Obróć w prawo \n",
"place_object_scroll_down": "[Scroll Down] Obróć w lewo \n",
"object_place": "[%s] Umieść \n",
"object_cancel": "[%s] Anuluj \n",
"object_scroll_up": "[%s] Obróć w prawo \n",
"object_scroll_down": "[%s] Obróć w lewo \n",
"depth_modifier": "[%s] Modyfikator kontroli głębokości \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "Você não tem dinheiro suficiente para comprar este item",
"Input": "Entrada de quantidade",
"PurchaseAmount": "Quantidade de compra",
"PurchasedItem": "Você comprou ",
"CurrencySymbol": "$ %s",
"Confirm": "Confirmar compra",
"AreYouSure": "Você tem certeza que quer comprar um %s",
"PayByCash": "Pagar %s em dinheiro",
"PayByCard": "Pagar %s com cartão",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Nome da loja"
},
"UNITTEST": {
"UNITTESTA": "Eu sou um %s %s",
"locale-unit-test": "Esta é uma string de teste"
},
"clipboard": {
"copy": "Copiado para a área de transferência"
},
"placeable_object": {
"no_prop_defined": "Você não definiu nenhum objeto para colocar",
"cant_place_here": "Você não pode colocar este objeto aqui",
"place_object_place": "[E] Colocar \n",
"place_object_cancel": "[X] Cancelar \n",
"place_object_scroll_up": "[Scroll Up] Girar para a direita \n",
"place_object_scroll_down": "[Scroll Down] Girar para a esquerda \n",
"object_place": "[%s] Colocar \n",
"object_cancel": "[%s] Cancelar \n",
"object_scroll_up": "[%s] Girar para a direita \n",
"object_scroll_down": "[%s] Girar para a esquerda \n",
"depth_modifier": "[%s] Modificador de controle de profundidade \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "Nu ai destui bani să cumperi acest articol",
"Input": "Introducere cantitate",
"PurchaseAmount": "Cantitate de cumpărare",
"PurchasedItem": "Ai cumpărat ",
"CurrencySymbol": "$ %s",
"Confirm": "Confirmă cumpărarea",
"AreYouSure": "Ești sigur că vrei să cumperi un %s",
"PayByCash": "Plătește %s cu numerar",
"PayByCard": "Plătește %s cu cardul",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Numele magazinului"
},
"UNITTEST": {
"UNITTESTA": "Sunt un %s %s",
"locale-unit-test": "Acesta este un șir de test"
},
"clipboard": {
"copy": "Copiat în clipboard"
},
"placeable_object": {
"no_prop_defined": "Nu ai definit niciun obiect de plasat",
"cant_place_here": "Nu poți plasa acest obiect aici",
"place_object_place": "[E] Plasează \n",
"place_object_cancel": "[X] Anulează \n",
"place_object_scroll_up": "[Scroll Up] Rotește la dreapta \n",
"place_object_scroll_down": "[Scroll Down] Rotește la stânga \n",
"object_place": "[%s] Plasează \n",
"object_cancel": "[%s] Anulează \n",
"object_scroll_up": "[%s] Rotește la dreapta \n",
"object_scroll_down": "[%s] Rotește la stânga \n",
"depth_modifier": "[%s] Modificator control adâncime \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "У вас недостаточно денег для покупки этого предмета",
"Input": "Ввод количества",
"PurchaseAmount": "Количество покупки",
"PurchasedItem": "Вы купили ",
"CurrencySymbol": "$ %s",
"Confirm": "Подтвердить покупку",
"AreYouSure": "Вы уверены, что хотите купить %s",
"PayByCash": "Оплатить %s наличными",
"PayByCard": "Оплатить %s картой",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Название магазина"
},
"UNITTEST": {
"UNITTESTA": "Я %s %s",
"locale-unit-test": "Это тестовая строка"
},
"clipboard": {
"copy": "Скопировано в буфер обмена"
},
"placeable_object": {
"no_prop_defined": "Вы не определили никакого объекта для размещения",
"cant_place_here": "Вы не можете разместить этот объект здесь",
"place_object_place": "[E] Разместить \n",
"place_object_cancel": "[X] Отменить \n",
"place_object_scroll_up": "[Scroll Up] Повернуть вправо \n",
"place_object_scroll_down": "[Scroll Down] Повернуть влево \n",
"object_place": "[%s] Разместить \n",
"object_cancel": "[%s] Отменить \n",
"object_scroll_up": "[%s] Повернуть вправо \n",
"object_scroll_down": "[%s] Повернуть влево \n",
"depth_modifier": "[%s] Модификатор контроля глубины \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "Du har inte tillräckligt med pengar för att köpa denna vara",
"Input": "Mängd inmatning",
"PurchaseAmount": "Inköpsmängd",
"PurchasedItem": "Du har köpt ",
"CurrencySymbol": "$ %s",
"Confirm": "Bekräfta köp",
"AreYouSure": "Är du säker på att du vill köpa en %s",
"PayByCash": "Betala %s kontant",
"PayByCard": "Betala %s med kort",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Butiksnamn"
},
"UNITTEST": {
"UNITTESTA": "Jag är en %s %s",
"locale-unit-test": "Detta är en teststräng"
},
"clipboard": {
"copy": "Kopierat till urklipp"
},
"placeable_object": {
"no_prop_defined": "Du har inte definierat något objekt att placera",
"cant_place_here": "Du kan inte placera detta objekt här",
"place_object_place": "[E] Placera \n",
"place_object_cancel": "[X] Avbryt \n",
"place_object_scroll_up": "[Scroll Up] Vrid höger \n",
"place_object_scroll_down": "[Scroll Down] Vrid vänster \n",
"object_place": "[%s] Placera \n",
"object_cancel": "[%s] Avbryt \n",
"object_scroll_up": "[%s] Vrid höger \n",
"object_scroll_down": "[%s] Vrid vänster \n",
"depth_modifier": "[%s] Djupkontroll modifierare \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "คุณไม่มีเงินเพียงพอที่จะซื้อสิ่งนี้",
"Input": "ป้อนจำนวน",
"PurchaseAmount": "จำนวนที่ซื้อ",
"PurchasedItem": "คุณได้ซื้อ ",
"CurrencySymbol": "$ %s",
"Confirm": "ยืนยันการซื้อ",
"AreYouSure": "คุณแน่ใจหรือว่าต้องการซื้อ %s",
"PayByCash": "จ่าย %s ด้วยเงินสด",
"PayByCard": "จ่าย %s ด้วยบัตร",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "ชื่อร้าน"
},
"UNITTEST": {
"UNITTESTA": "ฉันเป็น %s %s",
"locale-unit-test": "นี่คือสตริงทดสอบ"
},
"clipboard": {
"copy": "คัดลอกไปยังคลิปบอร์ด"
},
"placeable_object": {
"no_prop_defined": "คุณไม่ได้กำหนดวัตถุใดๆ เพื่อวาง",
"cant_place_here": "คุณไม่สามารถวางวัตถุนี้ที่นี่",
"place_object_place": "[E] วาง \n",
"place_object_cancel": "[X] ยกเลิก \n",
"place_object_scroll_up": "[Scroll Up] หมุนไปทางขวา \n",
"place_object_scroll_down": "[Scroll Down] หมุนไปทางซ้าย \n",
"object_place": "[%s] วาง \n",
"object_cancel": "[%s] ยกเลิก \n",
"object_scroll_up": "[%s] หมุนไปทางขวา \n",
"object_scroll_down": "[%s] หมุนไปทางซ้าย \n",
"depth_modifier": "[%s] ตัวปรับเปลี่ยนการควบคุมความลึก \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "Bu öğeyi satın almak için yeterli paranız yok",
"Input": "Miktar girişi",
"PurchaseAmount": "Satın alma miktarı",
"PurchasedItem": "Satın aldınız ",
"CurrencySymbol": "$ %s",
"Confirm": "Satın almayı onayla",
"AreYouSure": "Bir %s satın almak istediğinizden emin misiniz",
"PayByCash": "%s nakit ile öde",
"PayByCard": "%s kart ile öde",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "Mağaza adı"
},
"UNITTEST": {
"UNITTESTA": "Ben bir %s %s'yim",
"locale-unit-test": "Bu bir test dizesidir"
},
"clipboard": {
"copy": "Panoya kopyalandı"
},
"placeable_object": {
"no_prop_defined": "Yerleştirmek için herhangi bir nesne tanımlamadınız",
"cant_place_here": "Bu nesneyi buraya yerleştiremezsiniz",
"place_object_place": "[E] Yerleştir \n",
"place_object_cancel": "[X] İptal \n",
"place_object_scroll_up": "[Scroll Up] Sağa döndür \n",
"place_object_scroll_down": "[Scroll Down] Sola döndür \n",
"object_place": "[%s] Yerleştir \n",
"object_cancel": "[%s] İptal \n",
"object_scroll_up": "[%s] Sağa döndür \n",
"object_scroll_down": "[%s] Sola döndür \n",
"depth_modifier": "[%s] Derinlik kontrol düzenleyicisi \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "您没有足够的钱购买此物品",
"Input": "数量输入",
"PurchaseAmount": "购买数量",
"PurchasedItem": "您已购买 ",
"CurrencySymbol": "$ %s",
"Confirm": "确认购买",
"AreYouSure": "您确定要购买 %s 吗",
"PayByCash": "用现金支付 %s",
"PayByCard": "用卡支付 %s",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "商店名称"
},
"UNITTEST": {
"UNITTESTA": "我是一个 %s %s",
"locale-unit-test": "这是一个测试字符串"
},
"clipboard": {
"copy": "已复制到剪贴板"
},
"placeable_object": {
"no_prop_defined": "您没有定义任何要放置的对象",
"cant_place_here": "您不能在此处放置此对象",
"place_object_place": "[E] 放置 \n",
"place_object_cancel": "[X] 取消 \n",
"place_object_scroll_up": "[Scroll Up] 向右转 \n",
"place_object_scroll_down": "[Scroll Down] 向左转 \n",
"object_place": "[%s] 放置 \n",
"object_cancel": "[%s] 取消 \n",
"object_scroll_up": "[%s] 向右转 \n",
"object_scroll_down": "[%s] 向左转 \n",
"depth_modifier": "[%s] 深度控制修改器 \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "您沒有足夠的錢購買此物品",
"Input": "數量輸入",
"PurchaseAmount": "購買數量",
"PurchasedItem": "您已購買 ",
"CurrencySymbol": "$ %s",
"Confirm": "確認購買",
"AreYouSure": "您確定要購買 %s 嗎",
"PayByCash": "用現金支付 %s",
"PayByCard": "用卡支付 %s",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "商店名稱"
},
"UNITTEST": {
"UNITTESTA": "我是一個 %s %s",
"locale-unit-test": "這是一個測試字符串"
},
"clipboard": {
"copy": "已復制到剪貼板"
},
"placeable_object": {
"no_prop_defined": "您沒有定義任何要放置的對象",
"cant_place_here": "您不能在此處放置此對象",
"place_object_place": "[E] 放置 \n",
"place_object_cancel": "[X] 取消 \n",
"place_object_scroll_up": "[Scroll Up] 向右轉 \n",
"place_object_scroll_down": "[Scroll Down] 向左轉 \n",
"object_place": "[%s] 放置 \n",
"object_cancel": "[%s] 取消 \n",
"object_scroll_up": "[%s] 向右轉 \n",
"object_scroll_down": "[%s] 向左轉 \n",
"depth_modifier": "[%s] 深度控制修改器 \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "您沒有足夠的錢購買此物品",
"Input": "數量輸入",
"PurchaseAmount": "購買數量",
"PurchasedItem": "您已購買 ",
"CurrencySymbol": "$ %s",
"Confirm": "確認購買",
"AreYouSure": "您確定要購買 %s 嗎",
"PayByCash": "用現金支付 %s",
"PayByCard": "用卡支付 %s",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "商店名稱"
},
"UNITTEST": {
"UNITTESTA": "我是一個 %s %s",
"locale-unit-test": "這是一個測試字符串"
},
"clipboard": {
"copy": "已複製到剪貼簿"
},
"placeable_object": {
"no_prop_defined": "您沒有定義任何要放置的物件",
"cant_place_here": "您不能在此處放置此物件",
"place_object_place": "[E] 放置 \n",
"place_object_cancel": "[X] 取消 \n",
"place_object_scroll_up": "[Scroll Up] 向右轉 \n",
"place_object_scroll_down": "[Scroll Down] 向左轉 \n",
"object_place": "[%s] 放置 \n",
"object_cancel": "[%s] 取消 \n",
"object_scroll_up": "[%s] 向右轉 \n",
"object_scroll_down": "[%s] 向左轉 \n",
"depth_modifier": "[%s] 深度控制修改器 \n"
}
}

View file

@ -0,0 +1,37 @@
{
"Shops": {
"NotEnoughMoney": "您没有足够的钱购买此物品",
"Input": "数量输入",
"PurchaseAmount": "购买数量",
"PurchasedItem": "您已购买 ",
"CurrencySymbol": "$ %s",
"Confirm": "确认购买",
"AreYouSure": "您确定要购买 %s 吗",
"PayByCash": "用现金支付 %s",
"PayByCard": "用卡支付 %s",
"CardIcon": "fa-solid fa-building-columns",
"CashIcon": "fa-solid fa-money-bill-wave",
"ShopIcon": "fa-solid fa-basket-shopping",
"ShopName": "商店名称"
},
"UNITTEST": {
"UNITTESTA": "我是一个 %s %s",
"locale-unit-test": "这是一个测试字符串"
},
"clipboard": {
"copy": "已复制到剪贴板"
},
"placeable_object": {
"no_prop_defined": "您没有定义任何要放置的对象",
"cant_place_here": "您不能在此处放置此对象",
"place_object_place": "[E] 放置 \n",
"place_object_cancel": "[X] 取消 \n",
"place_object_scroll_up": "[Scroll Up] 向右转 \n",
"place_object_scroll_down": "[Scroll Down] 向左转 \n",
"object_place": "[%s] 放置 \n",
"object_cancel": "[%s] 取消 \n",
"object_scroll_up": "[%s] 向右转 \n",
"object_scroll_down": "[%s] 向左转 \n",
"depth_modifier": "[%s] 深度控制修改器 \n"
}
}

View file

@ -0,0 +1,24 @@
---@diagnostic disable: duplicate-set-field
BossMenu = BossMenu or {}
---This will get the name of the module being used.
---@return string
BossMenu.GetResourceName = function()
return "default"
end
RegisterNetEvent('community_bridge:client:OpenBossMenu', function(jobName, jobType)
-- these systems seem to do the verification for isboss themselves, so we don't need to check if the player is a boss.
-- also this source check is to ensure that the event is only triggered by the server.
if source ~= 65535 then return end
if BossMenu.GetResourceName() == "esx_society" then
local ESX = exports["es_extended"]:getSharedObject() -- better solution needed but fuck it for now.
TriggerEvent('esx_society:openBossMenu', jobName, function(menu)
ESX.CloseContext()
end, {wash = false})
elseif BossMenu.GetResourceName() == "qbx_management" then
exports.qbx_management:OpenBossMenu(jobType)
end
end)
return BossMenu

View file

@ -0,0 +1,14 @@
---@diagnostic disable: duplicate-set-field
BossMenu = BossMenu or {}
---This will get the name of the module being used.
---@return string
BossMenu.GetResourceName = function()
return "default"
end
BossMenu.OpenBossMenu = function(src, jobName, jobType)
print("You are using the community_bridge module for boss menus. Please ensure you have the correct dependencies installed.")
end
return BossMenu

View file

@ -0,0 +1,12 @@
---@diagnostic disable: duplicate-set-field
if GetResourceState('esx_society') ~= 'started' then return end
BossMenu = BossMenu or {}
---This will get the name of the module being used.
---@return string
BossMenu.GetResourceName = function()
return "esx_society"
end
return BossMenu

View file

@ -0,0 +1,21 @@
---@diagnostic disable: duplicate-set-field
if GetResourceState('esx_society') ~= 'started' then return end
BossMenu = BossMenu or {}
---This will get the name of the module being used.
---@return string
BossMenu.GetResourceName = function()
return "esx_society"
end
local registeredSocieties = {}
BossMenu.OpenBossMenu = function(src, jobName, jobType)
if not registeredSocieties[jobName] then
exports["esx_society"]:registerSociety(jobName, jobName, 'society_' .. jobName, 'society_' .. jobName, 'society_' .. jobName, {type = 'private'})
registeredSocieties[jobName] = true
end
TriggerClientEvent("community_bridge:client:OpenBossMenu", src, jobName, jobType)
end
return BossMenu

View file

@ -0,0 +1,13 @@
---@diagnostic disable: duplicate-set-field
if GetResourceState('qb-management') ~= 'started' then return end
if GetResourceState('qbx_management') == 'started' then return end
BossMenu = BossMenu or {}
---This will get the name of the module being used.
---@return string
BossMenu.GetResourceName = function()
return "qb-management"
end
return BossMenu

View file

@ -0,0 +1,17 @@
---@diagnostic disable: duplicate-set-field
if GetResourceState('qb-management') ~= 'started' then return end
if GetResourceState('qbx_management') == 'started' then return end
BossMenu = BossMenu or {}
---This will get the name of the module being used.
---@return string
BossMenu.GetResourceName = function()
return "qb-management"
end
BossMenu.OpenBossMenu = function(src, jobName, jobType)
TriggerClientEvent("qb-bossmenu:client:OpenMenu", src)
end
return BossMenu

View file

@ -0,0 +1,12 @@
---@diagnostic disable: duplicate-set-field
if GetResourceState('qbx_management') ~= 'started' then return end
BossMenu = BossMenu or {}
---This will get the name of the module being used.
---@return string
BossMenu.GetResourceName = function()
return "qbx_management"
end
return BossMenu

View file

@ -0,0 +1,16 @@
---@diagnostic disable: duplicate-set-field
if GetResourceState('qbx_management') ~= 'started' then return end
BossMenu = BossMenu or {}
---This will get the name of the module being used.
---@return string
BossMenu.GetResourceName = function()
return "qbx_management"
end
BossMenu.OpenBossMenu = function(src, jobName, jobType)
TriggerClientEvent("community_bridge:client:OpenBossMenu", src, jobName, jobType)
end
return BossMenu

View file

@ -0,0 +1,150 @@
---@diagnostic disable: duplicate-set-field
Clothing = Clothing or {}
ClothingBackup = {}
Callback = Callback or Require("lib/utility/shared/callbacks.lua")
Ultility = Utility or Require('lib/utility/client/utility.lua')
Cache = Cache or Require('lib/cache/shared/cache.lua')
function Clothing.IsMale()
local ped = PlayerPedId()
if not ped then return end
if GetEntityModel(ped) == `mp_m_freemode_01` then
return true
end
return false
end
---Get the skin data of a ped
---@param entity number
---@return table
function Clothing.GetAppearance(entity)
if not entity and not DoesEntityExist(entity) then return end
local model = GetEntityModel(entity)
local skinData = { model = model, components = {}, props = {} }
for i = 0, 11 do
table.insert(skinData.components,
{ component_id = i, drawable = GetPedDrawableVariation(entity, i), texture = GetPedTextureVariation(entity, i) })
end
for i = 0, 13 do
table.insert(skinData.props,
{ prop_id = i, drawable = GetPedPropIndex(entity, i), texture = GetPedPropTextureIndex(entity, i) })
end
return skinData
end
function Clothing.CopyAppearanceToClipboard()
local ped = PlayerPedId()
if not ped or not DoesEntityExist(ped) then return end
local skinData = Clothing.GetAppearance(ped)
if not skinData then return end
Ultility.CopyToClipboard(skinData)
end
Callback.Register('community_bridge:cb:GetAppearance', function()
local ped = PlayerPedId()
if not ped or not DoesEntityExist(ped) then return end
local skinData = Clothing.GetAppearance(ped)
return skinData
end)
---Apply skin data to a ped
---@param entity number
---@param skinData table
---@return boolean
function Clothing.SetAppearance(entity, skinData)
for k, v in pairs(skinData.components or {}) do
if v.component_id then
SetPedComponentVariation(entity, v.component_id, v.drawable, v.texture, 0)
end
end
for k, v in pairs(skinData.props or {}) do
if v.prop_id then
SetPedPropIndex(entity, v.prop_id, v.drawable, v.texture, 0)
end
end
return true
end
---This will return the peds components to the previously stored components
---@return boolean
Clothing.RestoreAppearance = function(entity)
Clothing.SetAppearance(entity, ClothingBackup)
return true
end
Clothing.UpdateAppearanceBackup = function(data)
ClothingBackup = data
end
Clothing.RunningDebug = false
Clothing.Cache = nil -- maybe change this to the actual cache system???!? For future not lazy me
Cache.Create('Clothing', function()
local ped = Cache.Get('Ped')
local appearance = Clothing.GetAppearance(ped)
if Table.Compare(Clothing.Cache, appearance) then
return false
end
return appearance
end, 1000)
local onChange = nil
Clothing.ToggleDebugging = function()
if Clothing.RunningDebug then
Clothing.RunningDebug = false
print("Clothing Debugging Disabled")
return Cache.RemoveOnChange('Clothing', onChange)
end
Clothing.RunningDebug = true
print("Clothing Debugging Enabled")
if Clothing.OpenMenu then
Clothing.OpenMenu()
end
onChange = Cache.OnChange('Clothing', function(new, old)
print("Clothing Debugging", new)
for k, v in pairs(old.components) do
if v.component_id then
if new.components[k].drawable ~= v.drawable or new.components[k].texture ~= v.texture then
print("Component ID: " ..
v.component_id ..
" Drawable: " .. new.components[k].drawable .. " Texture: " .. new.components[k].texture)
end
end
end
for k, v in pairs(old.props) do
if v.prop_id then
if new.props[k].drawable ~= v.drawable or new.props[k].texture ~= v.texture then
print("Prop ID: " ..
v.prop_id .. " Drawable: " .. new.props[k].drawable .. " Texture: " .. new.props[k].texture)
end
end
end
end)
end
RegisterNetEvent('community_bridge:client:SetAppearance', function(data)
Clothing.SetAppearance(PlayerPedId(), data)
end)
RegisterNetEvent('community_bridge:client:GetAppearance', function()
Clothing.GetAppearance(PlayerPedId())
end)
RegisterNetEvent('community_bridge:client:RestoreAppearance', function()
Clothing.RestoreAppearance(PlayerPedId())
end)
-- RegisterCommand("clothing:enabledebug", function(source, args, rawCommand)
-- Clothing.ToggleDebugging()
-- end)
-- RegisterCommand("clothing:copy", function(source, args, rawCommand)
-- Clothing.CopyAppearanceToClipboard()
-- end)
return Clothing

View file

@ -0,0 +1,56 @@
---@diagnostic disable: duplicate-set-field
Clothing = Clothing or {}
Clothing.LastAppearance = Clothing.LastAppearance or {}
Callback = Callback or Require("lib/utility/shared/callbacks.lua")
function Clothing.IsMale(src)
local ped = GetPlayerPed(src)
if not ped or not DoesEntityExist(ped) then return end
return GetEntityModel(ped) == `mp_m_freemode_01`
end
function Clothing.GetAppearance(src)
return Callback.Trigger('community_bridge:cb:GetAppearance', src)
end
Clothing.SetAppearance = function(src, data)
local strSrc = tostring(src)
Clothing.LastAppearance[strSrc] = Clothing.GetAppearance(src)
TriggerClientEvent('community_bridge:client:SetAppearance', src, data)
end
--- Sets a player's appearance based on gender-specific data
---@param src number The server ID of the player
---@param data table Table containing separate appearance data for male and female characters
---@return table|nil Appearance updated player appearance data or nil if failed
function Clothing.SetAppearanceExt(src, data)
local tbl = Clothing.IsMale(src) and data.male or data.female
Clothing.SetAppearance(src, tbl)
end
Clothing.RestoreAppearance = function(src)
TriggerClientEvent('community_bridge:client:RestoreAppearance', src)
end
-- RegisterNetEvent('community_bridge:client:SetAppearance', function(data)
-- local src = source
-- Clothing.SetAppearance(src, data)
-- end)
-- RegisterNetEvent('community_bridge:client:GetAppearance', function()
-- local src = source
-- Clothing.GetAppearance(src)
-- end)
-- RegisterNetEvent('community_bridge:client:RestoreAppearance', function()
-- local src = source
-- Clothing.RestoreAppearance(src)
-- end)
-- RegisterNetEvent('community_bridge:client:ReloadSkin', function()
-- local src = source
-- Clothing.ReloadSkin(src)
-- end)
return Clothing

View file

@ -0,0 +1,8 @@
---@diagnostic disable: duplicate-set-field
if GetResourceState('esx_skin') == 'missing' then return end
if GetResourceState('rcore_clothing') ~= 'missing' then return end
Clothing = Clothing or {}
function Clothing.OpenMenu()
TriggerEvent('esx_skin:openMenu', function() end, function() end, true)
end

View file

@ -0,0 +1,177 @@
---@diagnostic disable: duplicate-set-field
if GetResourceState('esx_skin') == 'missing' then return end
if GetResourceState('rcore_clothing') ~= 'missing' then return end
Clothing = Clothing or {}
Clothing.Players = {}
Callback = Callback or Require("lib/utility/shared/callbacks.lua")
Table = Table or Require('lib/utility/shared/tables.lua')
---Internal function to get the full appearance data including skin, model, and converted format
---@param src number The server ID of the player
---@return table|nil The player's full appearance data or nil if not found
function Clothing.GetFullAppearanceData(src)
src = src and tonumber(src)
assert(src, "src is nil")
local citId = Bridge.Framework.GetPlayerIdentifier(src)
if not citId then return end
if Clothing.Players[citId] then return Clothing.Players[citId] end
local result = MySQL.query.await('SELECT skin FROM users WHERE identifier = ?', { citId })
if result[1] == nil then return end
local model = GetEntityModel(GetPlayerPed(src))
local skinData = json.decode(result[1].skin)
local converted = Clothing.ConvertToDefault(skinData)
-- Store complete data in the cache
Clothing.Players[citId] = {
model = model,
skin = skinData,
converted = converted
}
return Clothing.Players[citId]
end
---Retrieves a player's converted appearance data for easy use across modules
---@param src number The server ID of the player
---@param fullData boolean Optional - If true, returns the full data object including skin and model
---@return table|nil The player's converted appearance data or full appearance data if fullData=true
function Clothing.GetAppearance(src, fullData)
if fullData then
return Clothing.GetFullAppearanceData(src)
end
local completeData = Clothing.GetFullAppearanceData(src)
if not completeData then return nil end
return completeData.converted
end
---This will save the player's current appearance to the database
---@param src number|string
function Clothing.Save(src)
src = src and tonumber(src)
assert(src, "src is nil")
local citId = Bridge.Framework.GetPlayerIdentifier(src)
if not citId then return end
local currentClothing = Clothing.GetFullAppearanceData(src)
if not currentClothing then return end
local currentSkin = currentClothing.skin
local encodedSkin = json.encode(currentSkin)
MySQL.update.await('UPDATE users SET skin = ? WHERE identifier = ?', {
encodedSkin,
citId
})
end
---Sets a player's appearance based on the provided data
---@param src number The server ID of the player
---@param data table The appearance data to apply
---@param updateBackup boolean Whether to update the backup appearance data
---@return table|nil The updated player appearance data or nil if failed
function Clothing.SetAppearance(src, data, updateBackup, save)
src = src and tonumber(src)
assert(src, "src is nil")
local citId = Bridge.Framework.GetPlayerIdentifier(src)
if not citId then return end
local model = GetEntityModel(GetPlayerPed(src))
if not model then return end
local converted = Clothing.ConvertFromDefault(data)
-- Get full appearance data
local currentClothing = Clothing.GetFullAppearanceData(src)
if not currentClothing then return end
local currentSkin = currentClothing.skin
for k, v in pairs(converted) do
currentSkin[k] = v
end
if not Clothing.Players[citId].backup or updateBackup then
Clothing.Players[citId].backup = currentClothing.converted
end
Clothing.Players[citId] = Clothing.Players[citId] or {}
Clothing.Players[citId].skin = currentSkin
Clothing.Players[citId].model = model
Clothing.Players[citId].converted = Clothing.ConvertToDefault(currentSkin)
if save then
local encodedSkin = json.encode(currentSkin)
-- saving 1 sql call by adding the query here instead of calling save
MySQL.update.await('UPDATE users SET skin = ? WHERE identifier = ?', {
encodedSkin,
citId
})
end
TriggerClientEvent('community_bridge:client:SetAppearance', src, Clothing.Players[citId].converted)
return Clothing.Players[citId]
end
--- Sets a player's appearance based on gender-specific data
---@param src number The server ID of the player
---@param data table Table containing separate appearance data for male and female characters
---@return table|nil Appearance updated player appearance data or nil if failed
function Clothing.SetAppearanceExt(src, data)
local tbl = Clothing.IsMale(src) and data.male or data.female
Clothing.SetAppearance(src, tbl)
end
---Reverts a player's appearance to their backup appearance
---@param src number The server ID of the player
---@return boolean|nil Returns true if successful or nil if failed
function Clothing.Revert(src)
src = src and tonumber(src)
assert(src, "src is nil")
local currentClothing = Clothing.GetFullAppearanceData(src)
if not currentClothing then return end
local backup = currentClothing.backup
if not backup then return end
return Clothing.SetAppearance(src, backup)
end
---This will open the menu for the passed src
---@param src number|string
function Clothing.OpenMenu(src)
src = src and tonumber(src)
assert(src, "src is nil")
TriggerClientEvent('esx_skin:openMenu', src)
end
---Event handler for when a player loads into the server
---Caches the player's appearance data
AddEventHandler('community_bridge:Server:OnPlayerLoaded', function(src)
src = src and tonumber(src)
assert(src, "src is nil")
-- Use GetFullAppearanceData to cache the complete appearance
Clothing.GetFullAppearanceData(src)
end)
---Event handler for when a player disconnects from the server
---Removes the player's appearance data from the cache
AddEventHandler('community_bridge:Server:OnPlayerUnload', function(src)
src = src and tonumber(src)
assert(src, "src is nil")
local strSrc = tostring(src)
Clothing.Players[strSrc] = nil
end)
---Event handler for when the resource starts
---Caches appearance data for all currently connected players
AddEventHandler('onResourceStart', function(resource)
if resource ~= GetCurrentResourceName() then return end
for _, playerId in ipairs(GetPlayers()) do
local src = tonumber(playerId)
local strSrc = tostring(src)
if not Clothing.Players[strSrc] then
Clothing.Players[strSrc] = Clothing.GetAppearance(src)
end
end
end)
---Callback handler for retrieving a player's appearance data
Callback.Register('community_bridge:cb:GetAppearance', function(source)
local src = source
return Clothing.GetAppearance(src)
end)

View file

@ -0,0 +1,204 @@
---@diagnostic disable: duplicate-set-field
if GetResourceState('esx_skin') == 'missing' then return end
if GetResourceState('rcore_clothing') ~= 'missing' then return end
Clothing = Clothing or {}
-- {"glasses_2":0,"shoes_1":70,"dad":0,"shoes_2":2,"pants_1":28,"eye_squint":0,"ears_1":-1,"hair_color_1":61,"eyebrows_6":0,"bodyb_3":-1,"beard_1":11,"complexion_2":0,"arms_2":0,"hair_1":76,"nose_1":0,"blush_2":0,"bracelets_2":0,"blush_1":0,"jaw_2":0,"helmet_1":-1,"eyebrows_3":0,"watches_1":-1,"eyebrows_4":0,"jaw_1":0,"lipstick_1":0,"eyebrows_1":0,"nose_4":0,"age_2":0,"torso_1":23,"hair_2":0,"chin_2":0,"arms":1,"chain_1":22,"nose_2":0,"cheeks_1":2,"tshirt_1":4,"glasses_1":0,"pants_2":3,"lipstick_4":0,"chin_13":0,"beard_4":0,"beard_3":0,"chain_2":2,"cheeks_3":6,"sex":0,"lipstick_3":0,"makeup_1":0,"hair_color_2":29,"mask_2":0,"chin_1":0,"eyebrows_5":0,"bodyb_2":0,"sun_2":0,"watches_2":0,"sun_1":0,"chin_4":0,"nose_3":0,"helmet_2":0,"bags_2":0,"moles_2":0,"mask_1":0,"blemishes_2":0,"chest_1":0,"cheeks_2":-10,"age_1":0,"chest_2":0,"beard_2":10,"torso_2":2,"blush_3":0,"bproof_1":0,"moles_1":0,"chin_3":0,"lip_thickness":-2,"lipstick_2":0,"chest_3":0,"complexion_1":0,"bodyb_4":0,"neck_thickness":0,"bproof_2":0,"makeup_3":0,"tshirt_2":2,"makeup_2":0,"makeup_4":0,"bracelets_1":-1,"decals_2":0,"nose_6":0,"bodyb_1":-1,"bags_1":0,"blemishes_1":0,"decals_1":0,"mom":21,"eyebrows_2":0,"eye_color":0,"skin_md_weight":50,"face_md_weight":50,"nose_5":10,"ears_2":0}
Components = {}
Components.Map = {
[1] = 'mask', -- componentId
[2] = 'ears',
[3] = 'arms',
[4] = 'pants',
[5] = 'bags',
[6] = 'shoes',
[7] = 'chain',
[8] = 'tshirt',
[9] = 'bproof',
[10] = 'decals',
[11] = 'torso'
}
Components.InverseMap = {
mask = 1, -- componentId
arms = 3,
pants = 4,
bags = 5,
shoes = 6,
chain = 7,
tshirt = 8,
bproof = 9,
decals = 10,
torso = 11,
}
Props = {}
Props.Map = {
[0] = 'helmet', -- propId
[1] = 'glasses',
[2] = 'ears',
[6] = 'watches',
[7] = 'bracelets'
}
Props.InverseMap = {
helmet_1 = 0,
helmet_2 = 0,
glasses_1 = 1,
glasses_2 = 1,
ears_1 = 2,
ears_2 = 2,
watches_1 = 6,
watches_2 = 6,
bracelets_1 = 7,
bracelets_2 = 7,
}
-- Converts from illinium format to esx format
function Components.ConvertFromDefault(defaultComponents)
local returnComponents = {}
for index, componentData in pairs(defaultComponents or {}) do
local componentId = Components.Map[componentData.component_id]
if componentId then
returnComponents[componentId .. '_1'] = componentData.drawable
returnComponents[componentId .. '_2'] = componentData.texture
end
end
return returnComponents
end
function Components.ConvertToDefault(esxComponents)
local returnComponents = {}
for componentIndex, componentData in pairs(esxComponents or {}) do
local isTexture = componentIndex:find('_2')
componentIndex = componentIndex:gsub('_1', ''):gsub('_2', '')
local componentId = Components.InverseMap[componentIndex]
if componentId then
if isTexture then
returnComponents[componentId] = returnComponents[componentId] or {}
returnComponents[componentId].texture = componentData
else
returnComponents[componentId] = returnComponents[componentId] or {}
returnComponents[componentId].component_id = componentId
returnComponents[componentId].drawable = componentData
returnComponents[componentId].texture = returnComponents[componentId].texture or 0
end
end
end
return returnComponents
end
function Props.ConvertFromDefault(defaultProps)
local returnProps = {}
for index, propData in pairs(defaultProps or {}) do
local propId = Props.Map[propData.prop_id]
if propId then
returnProps[propId .. '_1'] = propData.drawable
returnProps[propId .. '_2'] = propData.texture
end
end
return returnProps
end
function Props.ConvertToDefault(esxProps)
local returnProps = {}
for propIndex, propData in pairs(esxProps or {}) do
local isTexture = propIndex:find('_2')
propIndex = propIndex:gsub('_1', ''):gsub('_2', '')
local propId = Props.InverseMap[propIndex]
if propId then
if isTexture then
returnProps[propId] = returnProps[propId] or {}
returnProps[propId].texture = propData
else
returnProps[propId] = returnProps[propId] or {}
returnProps[propId].prop_id = propId
returnProps[propId].drawable = propData
returnProps[propId].texture = returnProps[propId].texture or 0
end
end
table.sort(returnProps, function(a, b)
return a.prop_id < b.prop_id
end)
end
return returnProps
end
function Clothing.ConvertFromDefault(defaultClothing)
local components = Components.ConvertFromDefault(defaultClothing.components)
local props = Props.ConvertFromDefault(defaultClothing.props)
for propsIndex, propData in pairs(props) do
components[propsIndex] = propData
end
return components --skin
end
function Clothing.ConvertToDefault(esxClothing)
local components = Components.ConvertToDefault(esxClothing)
local props = Props.ConvertToDefault(esxClothing)
return { components = components, props = props }
end
-- Clothing = {}
-- StoredOldClothing = {}
-- Clothing.SetAppearance = function(clothingData)
-- if GetEntityModel(cache.ped) == `mp_m_freemode_01` then
-- clothingData = clothingData.male
-- else
-- clothingData = clothingData.female
-- end
-- local repackedTable = {}
-- local componentMap = {
-- [1] = "mask",
-- [3] = "arms",
-- [4] = "pants",
-- [5] = "bag",
-- [6] = "shoes",
-- [7] = "accessory",
-- [8] = "t-shirt",
-- [9] = "vest",
-- [10] = "decals",
-- [11] = "torso2"
-- }
-- local propMap = {
-- [0] = "hat",
-- [1] = "glass",
-- [2] = "ear",
-- [6] = "watch",
-- [7] = "bracelet"
-- }
-- local specialMap = {
-- eye_color_id = "eye_color",
-- moles_id = "moles",
-- ageing_id = "ageing",
-- hair_id = "hair",
-- face_id = "face"
-- }
-- for _, data in pairs(clothingData) do
-- if componentMap[data.component_id] then
-- repackedTable[componentMap[data.component_id]] = {drawable = data.drawable, texture = data.texture}
-- elseif propMap[data.prop_id] then
-- repackedTable[propMap[data.prop_id]] = {drawable = data.drawable, texture = data.texture}
-- elseif specialMap[data.eye_color_id] then
-- repackedTable[specialMap[data.eye_color_id]] = {drawable = data.drawable, texture = data.texture}
-- elseif specialMap[data.moles_id] then
-- repackedTable[specialMap[data.moles_id]] = {drawable = data.drawable, texture = data.texture}
-- elseif specialMap[data.ageing_id] then
-- repackedTable[specialMap[data.ageing_id]] = {drawable = data.drawable, texture = data.texture}
-- elseif specialMap[data.hair_id] then
-- repackedTable[specialMap[data.hair_id]] = {drawable = data.drawable, texture = data.texture}
-- elseif specialMap[data.face_id] then
-- repackedTable[specialMap[data.face_id]] = {drawable = data.drawable, texture = data.texture}
-- end
-- end
-- TriggerEvent('esx-clothing:client:loadOutfit', repackedTable)
-- end

View file

@ -0,0 +1,8 @@
---@diagnostic disable: duplicate-set-field
if GetResourceState('fivem-appearance') == 'missing' then return end
if GetResourceState('rcore_clothing') ~= 'missing' then return end
Clothing = Clothing or {}
function Clothing.OpenMenu()
TriggerEvent('qb-clothing:client:openMenu')
end

View file

@ -0,0 +1,341 @@
---@diagnostic disable: duplicate-set-field
if GetResourceState('fivem-appearance') == 'missing' then return end
if GetResourceState('rcore_clothing') ~= 'missing' then return end
Clothing = Clothing or {}
Clothing.Players = {}
Callback = Callback or Require("lib/utility/shared/callbacks.lua")
Table = Table or Require('lib/utility/shared/tables.lua')
--- Internal function to get the full appearance data including skin, model, and converted format
---@param src number The server ID of the player
---@return table|nil The player's full appearance data or nil if not found
function Clothing.GetFullAppearanceData(src)
src = src and tonumber(src)
assert(src, "src is nil")
local citId = Bridge.Framework.GetPlayerIdentifier(src)
if not citId then return end
if Clothing.Players[citId] then return Clothing.Players[citId] end
local result = MySQL.query.await('SELECT * FROM playerskins WHERE citizenid = ? AND active = ?', { citId, 1 })
if result[1] == nil then return end
local model = result[1].model
local skinData = json.decode(result[1].skin)
local converted = skinData
-- Store complete data in the cache
Clothing.Players[citId] = {
model = model,
skin = skinData,
converted = converted
}
return Clothing.Players[citId]
end
--- Retrieves a player's converted appearance data for easy use across modules
---@param src number The server ID of the player
---@param fullData boolean Optional - If true, returns the full data object including skin and model
---@return table|nil The player's converted appearance data or full appearance data if fullData=true
function Clothing.GetAppearance(src, fullData)
if fullData then
return Clothing.GetFullAppearanceData(src)
end
local completeData = Clothing.GetFullAppearanceData(src)
if not completeData then return nil end
return completeData.converted
end
--- Sets a player's appearance based on the provided data
---@param src number The server ID of the player
---@param data table The appearance data to apply
---@param updateBackup boolean Whether to update the backup appearance data
---@return table|nil The updated player appearance data or nil if failed
function Clothing.SetAppearance(src, data, updateBackup, save)
src = src and tonumber(src)
assert(src, "src is nil")
local citId = Bridge.Framework.GetPlayerIdentifier(src)
if not citId then return end
local model = GetEntityModel(GetPlayerPed(src))
if not model then return end
local converted = data
-- Get full appearance data
local currentClothing = Clothing.GetFullAppearanceData(src)
if not currentClothing then return end
local currentSkin = currentClothing.skin
for k, v in pairs(converted) do
currentSkin[k] = v
end
if not Clothing.Players[citId].backup or updateBackup then
Clothing.Players[citId].backup = currentClothing.converted
end
Clothing.Players[citId] = Clothing.Players[citId] or {}
Clothing.Players[citId].skin = currentSkin
Clothing.Players[citId].model = model
Clothing.Players[citId].converted = converted
if save then
MySQL.update.await('UPDATE playerskins SET skin = ? WHERE citizenid = ? AND active = ?', {
json.encode(currentSkin),
citId,
1
})
end
TriggerClientEvent('community_bridge:client:SetAppearance', src, Clothing.Players[citId].converted)
return Clothing.Players[citId]
end
--- Sets a player's appearance based on gender-specific data
---@param src number The server ID of the player
---@param data table Table containing separate appearance data for male and female characters
---@return table|nil Appearance updated player appearance data or nil if failed
function Clothing.SetAppearanceExt(src, data)
local tbl = Clothing.IsMale(src) and data.male or data.female
Clothing.SetAppearance(src, tbl)
end
--- Reverts a player's appearance to their backup appearance
---@param src number The server ID of the player
---@return boolean|nil Returns true if successful or nil if failed
function Clothing.Revert(src)
src = src and tonumber(src)
local currentClothing = Clothing.GetFullAppearanceData(src)
if not currentClothing then return end
local backup = currentClothing.backup
if not backup then return end
return Clothing.SetAppearance(src, backup)
end
function Clothing.OpenMenu(src)
src = src and tonumber(src)
assert(src, "src is nil")
TriggerClientEvent('qb-clothing:client:openMenu', src) -- does this event work while on esx?
end
--- Event handler for when a player loads into the server
--- Caches the player's appearance data
AddEventHandler('community_bridge:Server:OnPlayerLoaded', function(src)
src = src and tonumber(src)
assert(src, "src is nil")
-- Use GetFullAppearanceData to cache the complete appearance
Clothing.GetFullAppearanceData(src)
end)
--- Event handler for when a player disconnects from the server
--- Removes the player's appearance data from the cache
AddEventHandler('community_bridge:Server:OnPlayerUnload', function(src)
src = src and tonumber(src)
assert(src, "src is nil")
local strSrc = tostring(src)
Clothing.Players[strSrc] = nil
end)
--- Event handler for when the resource starts
--- Caches appearance data for all currently connected players
AddEventHandler('onResourceStart', function(resource)
if resource ~= GetCurrentResourceName() then return end
for _, playerId in ipairs(GetPlayers()) do
local src = tonumber(playerId)
local strSrc = tostring(src)
if not Clothing.Players[strSrc] then
Clothing.Players[strSrc] = Clothing.GetAppearance(src)
end
end
end)
--- Callback handler for retrieving a player's appearance data
Callback.Register('community_bridge:cb:GetAppearance', function(source)
local src = source
return Clothing.GetAppearance(src)
end)
RegisterCommand('clothing:debug', function(source, args, rawCommand)
local src = source
Clothing.SetAppearance(src, {
components = {
{ component_id = 1, drawable = math.random(0, 50), texture = 0 },
{ component_id = 2, drawable = math.random(0, 50), texture = 0 },
{ component_id = 3, drawable = math.random(0, 50), texture = 0 },
{ component_id = 4, drawable = math.random(0, 50), texture = 0 },
{ component_id = 5, drawable = math.random(0, 50), texture = 0 },
{ component_id = 6, drawable = math.random(0, 50), texture = 0 },
{ component_id = 7, drawable = math.random(0, 50), texture = 0 },
{ component_id = 8, drawable = math.random(0, 50), texture = 0 },
},
props = {
{ prop_id = 3, drawable = 0, texture = 0 },
{ prop_id = 1, drawable = 0, texture = 0 },
{ prop_id = 2, drawable = 0, texture = 0 },
}
}, false, true)
end, false)
RegisterCommand('clothing:revert', function(source, args, rawCommand)
local src = source
Clothing.Revert(src)
end, false)
RegisterCommand('clothing:current', function(source, args, rawCommand)
local src = source
local currentClothing = Clothing.GetAppearance(src)
if not currentClothing then return end
print(json.encode(currentClothing, { indent = true }))
end, false)
RegisterCommand('clothing:openmenu', function(source, args, rawCommand)
local src = source
Clothing.OpenMenu(src)
end, false)
-- This should be moved into unit tests
-- RegisterCommand('clothing:crowley', function(source, args, rawCommand)
-- local src = source
-- Clothing.SetAppearance(src, {
-- components= {
-- {
-- drawable= 0,
-- texture= 0,
-- component_id= 0
-- },
-- {
-- drawable= 0,
-- texture= 0,
-- component_id= 1
-- },
-- {
-- drawable= 19,
-- texture= 0,
-- component_id= 2
-- },
-- {
-- drawable= 6,
-- texture= 0,
-- component_id= 3
-- },
-- {
-- drawable= 0,
-- texture= 0,
-- component_id= 4
-- },
-- {
-- drawable= 0,
-- texture= 0,
-- component_id= 5
-- },
-- {
-- drawable= 0,
-- texture= 0,
-- component_id= 6
-- },
-- {
-- drawable= 0,
-- texture= 0,
-- component_id= 7
-- },
-- {
-- drawable= 23,
-- texture= 0,
-- component_id= 8
-- },
-- {
-- drawable= 0,
-- texture= 0,
-- component_id= 9
-- },
-- {
-- drawable= 0,
-- texture= 0,
-- component_id= 10
-- },
-- {
-- drawable= 4,
-- texture= 2,
-- component_id= 11
-- }
-- },
-- props= {
-- {
-- drawable= 27,
-- prop_id= 0,
-- texture= 0
-- },
-- {
-- drawable= -1,
-- prop_id= 1,
-- texture= -1
-- },
-- {
-- drawable= -1,
-- prop_id= 2,
-- texture= -1
-- },
-- {
-- drawable= -1,
-- prop_id= 3,
-- texture= -1
-- },
-- {
-- drawable= -1,
-- prop_id= 4,
-- texture= -1
-- },
-- {
-- drawable= -1,
-- prop_id= 5,
-- texture= -1
-- },
-- {
-- drawable= -1,
-- prop_id= 6,
-- texture= -1
-- },
-- {
-- drawable= -1,
-- prop_id= 7,
-- texture= -1
-- },
-- {
-- drawable= -1,
-- prop_id= 8,
-- texture= -1
-- },
-- {
-- drawable= -1,
-- prop_id= 9,
-- texture= -1
-- },
-- {
-- drawable= -1,
-- prop_id= 10,
-- texture= -1
-- },
-- {
-- drawable= -1,
-- prop_id= 11,
-- texture= -1
-- },
-- {
-- drawable= -1,
-- prop_id= 12,
-- texture= -1
-- },
-- {
-- drawable= -1,
-- prop_id= 13,
-- texture= -1
-- }
-- },
-- model= 1885233650
-- }, false, true)
-- end, false)

View file

@ -0,0 +1,43 @@
-- {
-- male = {
-- name = "Short Sleeve",
-- outfitData = {
-- ["pants"] = {item = 133, texture = 0}, -- Pants
-- ["arms"] = {item = 31, texture = 0}, -- Arms
-- ["t-shirt"] = {item = 35, texture = 0}, -- T Shirt
-- ["vest"] = {item = 34, texture = 0}, -- Body Vest
-- ["torso2"] = {item = 48, texture = 0}, -- Jacket
-- ["shoes"] = {item = 52, texture = 0}, -- Shoes
-- ["accessory"] = {item = 0, texture = 0}, -- Neck Accessory
-- ["bag"] = {item = 0, texture = 0}, -- Bag
-- ["hat"] = {item = 0, texture = 0}, -- Hat
-- ["glass"] = {item = 0, texture = 0}, -- Glasses
-- ["mask"] = {item = 0, texture = 0} -- Mask
-- },
-- grades = {0, 1, 2, 3, 4},
-- },
-- female = {
-- name = "Short Sleeve",
-- outfitData = {
-- ["pants"] = {item = 133, texture = 0}, -- Pants
-- ["arms"] = {item = 31, texture = 0}, -- Arms
-- ["t-shirt"] = {item = 35, texture = 0}, -- T Shirt
-- ["vest"] = {item = 34, texture = 0}, -- Body Vest
-- ["torso2"] = {item = 48, texture = 0}, -- Jacket
-- ["shoes"] = {item = 52, texture = 0}, -- Shoes
-- ["accessory"] = {item = 0, texture = 0}, -- Neck Accessory
-- ["bag"] = {item = 0, texture = 0}, -- Bag
-- ["hat"] = {item = 0, texture = 0}, -- Hat
-- ["glass"] = {item = 0, texture = 0}, -- Glasses
-- ["mask"] = {item = 0, texture = 0} -- Mask
-- },
-- }
-- }
-- function Clothing.ConvertData(data)
-- return data
-- end
-- This still needed?

View file

@ -0,0 +1,8 @@
---@diagnostic disable: duplicate-set-field
if GetResourceState('illenium-appearance') == 'missing' then return end
if GetResourceState('rcore_clothing') ~= 'missing' then return end
Clothing = Clothing or {}
function Clothing.OpenMenu()
TriggerEvent('qb-clothing:client:openMenu')
end

View file

@ -0,0 +1,253 @@
---@diagnostic disable: duplicate-set-field
if GetResourceState('illenium-appearance') == 'missing' then return end
if GetResourceState('rcore_clothing') ~= 'missing' then return end
Clothing = Clothing or {}
Clothing.Players = {}
Callback = Callback or Require("lib/utility/shared/callbacks.lua")
Table = Table or Require('lib/utility/shared/tables.lua')
--- Internal function to get the full appearance data including skin, model, and converted format
---@param src number The server ID of the player
---@return table|nil The player's full appearance data or nil if not found
function Clothing.GetFullAppearanceData(src)
src = src and tonumber(src)
assert(src, "src is nil")
local citId = Bridge.Framework.GetPlayerIdentifier(src)
if not citId then return end
if Clothing.Players[citId] then return Clothing.Players[citId] end
local result = MySQL.query.await('SELECT * FROM playerskins WHERE citizenid = ? AND active = ?', { citId, 1 })
if result[1] == nil then return end
local model = result[1].model
local skinData = json.decode(result[1].skin)
local converted = skinData
-- Store complete data in the cache
Clothing.Players[citId] = {
model = model,
skin = skinData,
converted = converted
}
return Clothing.Players[citId]
end
--- Retrieves a player's converted appearance data for easy use across modules
---@param src number The server ID of the player
---@param fullData boolean Optional - If true, returns the full data object including skin and model
---@return table|nil The player's converted appearance data or full appearance data if fullData=true
function Clothing.GetAppearance(src, fullData)
if fullData then
return Clothing.GetFullAppearanceData(src)
end
local completeData = Clothing.GetFullAppearanceData(src)
if not completeData then return nil end
return completeData.converted
end
--- Sets a player's appearance based on the provided data
---@param src number The server ID of the player
---@param data table The appearance data to apply
---@param updateBackup boolean Whether to update the backup appearance data
---@return table|nil The updated player appearance data or nil if failed
function Clothing.SetAppearance(src, data, updateBackup, save)
src = src and tonumber(src)
assert(src, "src is nil")
local citId = Bridge.Framework.GetPlayerIdentifier(src)
if not citId then return end
local model = GetEntityModel(GetPlayerPed(src))
if not model then return end
local converted = data
-- Get full appearance data
local currentClothing = Clothing.GetFullAppearanceData(src)
if not currentClothing then return end
local currentSkin = currentClothing.skin
for k, v in pairs(converted) do
currentSkin[k] = v
end
if not Clothing.Players[citId].backup or updateBackup then
Clothing.Players[citId].backup = currentClothing.converted
end
Clothing.Players[citId] = Clothing.Players[citId] or {}
Clothing.Players[citId].skin = currentSkin
Clothing.Players[citId].model = model
Clothing.Players[citId].converted = converted
if save then
MySQL.update.await('UPDATE playerskins SET skin = ? WHERE citizenid = ? AND active = ?', {
json.encode(currentSkin),
citId,
1
})
end
TriggerClientEvent('community_bridge:client:SetAppearance', src, Clothing.Players[citId].converted)
return Clothing.Players[citId]
end
--- Sets a player's appearance based on gender-specific data
---@param src number The server ID of the player
---@param data table Table containing separate appearance data for male and female characters
---@return table|nil Appearance updated player appearance data or nil if failed
function Clothing.SetAppearanceExt(src, data)
local tbl = Clothing.IsMale(src) and data.male or data.female
Clothing.SetAppearance(src, tbl)
end
--- Reverts a player's appearance to their backup appearance
---@param src number The server ID of the player
---@return boolean|nil Returns true if successful or nil if failed
function Clothing.Revert(src)
src = src and tonumber(src)
assert(src, "src is nil")
local currentClothing = Clothing.GetFullAppearanceData(src)
if not currentClothing then return end
local backup = currentClothing.backup
print(src, backup)
if not backup then return end
return Clothing.SetAppearance(src, backup)
end
function Clothing.OpenMenu(src)
src = src and tonumber(src)
assert(src, "src is nil")
TriggerClientEvent('qb-clothing:client:openMenu', src)
end
--- Event handler for when a player loads into the server
--- Caches the player's appearance data
AddEventHandler('community_bridge:Server:OnPlayerLoaded', function(src)
src = src and tonumber(src)
assert(src, "src is nil")
-- Use GetFullAppearanceData to cache the complete appearance
Clothing.GetFullAppearanceData(src)
end)
--- Event handler for when a player disconnects from the server
--- Removes the player's appearance data from the cache
AddEventHandler('community_bridge:Server:OnPlayerUnload', function(src)
src = src and tonumber(src)
assert(src, "src is nil")
local strSrc = tostring(src)
Clothing.Players[strSrc] = nil
end)
--- Event handler for when the resource starts
--- Caches appearance data for all currently connected players
AddEventHandler('onResourceStart', function(resource)
if resource ~= GetCurrentResourceName() then return end
for _, playerId in ipairs(GetPlayers()) do
local src = tonumber(playerId)
local strSrc = tostring(src)
if not Clothing.Players[strSrc] then
Clothing.Players[strSrc] = Clothing.GetAppearance(src)
end
end
end)
--- Callback handler for retrieving a player's appearance data
Callback.Register('community_bridge:cb:GetAppearance', function(source)
local src = source
return Clothing.GetAppearance(src)
end)
-- The below Should go to unit test
RegisterCommand('clothing:debug', function(source, args, rawCommand)
local src = source
Clothing.SetAppearance(src, {
components = {
{ component_id = 0, drawable = math.random(0, 50), texture = 0 },
{ component_id = 1, drawable = math.random(0, 50), texture = 0 },
{ component_id = 2, drawable = math.random(0, 50), texture = 0 },
{ component_id = 3, drawable = math.random(0, 50), texture = 0 },
{ component_id = 4, drawable = math.random(0, 50), texture = 0 },
{ component_id = 5, drawable = math.random(0, 50), texture = 0 },
{ component_id = 6, drawable = math.random(0, 50), texture = 0 },
{ component_id = 7, drawable = math.random(0, 50), texture = 0 },
{ component_id = 8, drawable = math.random(0, 50), texture = 0 },
{ component_id = 9, drawable = math.random(0, 50), texture = 0 },
{ component_id = 10, drawable = math.random(0, 50), texture = 0 },
{ component_id = 11, drawable = math.random(0, 50), texture = 0 },
},
props = {
{ prop_id = 0, drawable = math.random(0, 50), texture = 0 },
{ prop_id = 1, drawable = math.random(0, 50), texture = 0 },
{ prop_id = 2, drawable = math.random(0, 50), texture = 0 },
{ prop_id = 3, drawable = math.random(0, 50), texture = 0 },
{ prop_id = 4, drawable = math.random(0, 50), texture = 0 },
{ prop_id = 5, drawable = math.random(0, 50), texture = 0 },
{ prop_id = 6, drawable = math.random(0, 50), texture = 0 },
{ prop_id = 7, drawable = math.random(0, 50), texture = 0 },
{ prop_id = 8, drawable = math.random(0, 50), texture = 0 },
{ prop_id = 9, drawable = math.random(0, 50), texture = 0 },
{ prop_id = 10, drawable = math.random(0, 50), texture = 0 },
{ prop_id = 11, drawable = math.random(0, 50), texture = 0 },
{ prop_id = 12, drawable = math.random(0, 50), texture = 0 },
{ prop_id = 13, drawable = math.random(0, 50), texture = 0 },
}
}, false, true)
end, false)
RegisterCommand('clothing:revert', function(source, args, rawCommand)
local src = source
Clothing.Revert(src)
end, false)
RegisterCommand('clothing:current', function(source, args, rawCommand)
local src = source
local currentClothing = Clothing.GetAppearance(src)
if not currentClothing then return end
print(json.encode(currentClothing, { indent = true }))
end, false)
RegisterCommand('clothing:openmenu', function(source, args, rawCommand)
local src = source
Clothing.OpenMenu(src)
end, false)
RegisterCommand('clothing:crowley', function(source, args, rawCommand)
local src = source
Clothing.SetAppearance(src, {
components = {
{ drawable = 0, texture = 0, component_id = 0 },
{ drawable = 0, texture = 0, component_id = 1 },
{ drawable = 19, texture = 0, component_id = 2 },
{ drawable = 6, texture = 0, component_id = 3 },
{ drawable = 0, texture = 0, component_id = 4 },
{ drawable = 0, texture = 0, component_id = 5 },
{ drawable = 0, texture = 0, component_id = 6 },
{ drawable = 0, texture = 0, component_id = 7 },
{ drawable = 23, texture = 0, component_id = 8 },
{ drawable = 0, texture = 0, component_id = 9 },
{ drawable = 0, texture = 0, component_id = 10 },
{ drawable = 4, texture = 2, component_id = 11 }
},
props = {
{ drawable = 27, prop_id = 0, texture = 0 },
{ drawable = 0, prop_id = 1, texture = 0 },
{ drawable = 0, prop_id = 2, texture = 0 },
{ drawable = 0, prop_id = 3, texture = 0 },
{ drawable = 0, prop_id = 4, texture = 0 },
{ drawable = 0, prop_id = 5, texture = 0 },
{ drawable = 0, prop_id = 6, texture = 0 },
{ drawable = 0, prop_id = 7, texture = 0 },
{ drawable = 0, prop_id = 8, texture = 0 },
{ drawable = 0, prop_id = 9, texture = 0 },
{ drawable = 0, prop_id = 10, texture = 0 },
{ drawable = 0, prop_id = 11, texture = 0 },
{ drawable = 0, prop_id = 12, texture = 0 },
{ drawable = 0, prop_id = 13, texture = 0 }
}
}, false, true)
end, false)

View file

@ -0,0 +1,222 @@
-- Get appearence data
-- {
-- "tattoos": {
-- "ZONE_HAIR": [
-- {
-- "label": "hair-0-188",
-- "collection": "multiplayer_overlays",
-- "name": "hair-0-188",
-- "hashFemale": "FM_F_Hair_003_c",
-- "hashMale": "FM_M_Hair_003_c",
-- "zone": "ZONE_HAIR"
-- }
-- ]
-- },
-- "components": [
-- {
-- "texture": -1,
-- "drawable": -1,
-- "component_id": 0
-- },
-- {
-- "texture": -1,
-- "drawable": -1,
-- "component_id": 1
-- },
-- {
-- "texture": -1,
-- "drawable": -1,
-- "component_id": 2
-- },
-- {
-- "texture": -1,
-- "drawable": -1,
-- "component_id": 3
-- },
-- {
-- "texture": -1,
-- "drawable": -1,
-- "component_id": 4
-- },
-- {
-- "texture": -1,
-- "drawable": -1,
-- "component_id": 5
-- },
-- {
-- "texture": -1,
-- "drawable": -1,
-- "component_id": 6
-- },
-- {
-- "texture": -1,
-- "drawable": -1,
-- "component_id": 7
-- },
-- {
-- "texture": -1,
-- "drawable": -1,
-- "component_id": 8
-- },
-- {
-- "texture": -1,
-- "drawable": -1,
-- "component_id": 9
-- },
-- {
-- "texture": -1,
-- "drawable": -1,
-- "component_id": 10
-- },
-- {
-- "texture": -1,
-- "drawable": -1,
-- "component_id": 11
-- }
-- ],
-- "headBlend": {
-- "skinSecond": 0,
-- "skinMix": 0.0,
-- "shapeFirst": 0,
-- "shapeThird": 0,
-- "skinFirst": 0,
-- "shapeMix": 0.0,
-- "shapeSecond": 0,
-- "skinThird": 0,
-- "thirdMix": 0.0
-- },
-- "hair": {
-- "texture": -1,
-- "style": -1,
-- "color": -1,
-- "highlight": -1
-- },
-- "eyeColor": -1,
-- "headOverlays": {
-- "lipstick": {
-- "color": 0,
-- "style": 0,
-- "opacity": 0.0,
-- "secondColor": 0
-- },
-- "moleAndFreckles": {
-- "color": 0,
-- "style": 0,
-- "opacity": 0.0,
-- "secondColor": 0
-- },
-- "ageing": {
-- "color": 0,
-- "style": 0,
-- "opacity": 0.0,
-- "secondColor": 0
-- },
-- "makeUp": {
-- "color": 0,
-- "style": 0,
-- "opacity": 0.0,
-- "secondColor": 0
-- },
-- "beard": {
-- "color": 0,
-- "style": 0,
-- "opacity": 0.0,
-- "secondColor": 0
-- },
-- "blush": {
-- "color": 0,
-- "style": 0,
-- "opacity": 0.0,
-- "secondColor": 0
-- },
-- "complexion": {
-- "color": 0,
-- "style": 0,
-- "opacity": 0.0,
-- "secondColor": 0
-- },
-- "bodyBlemishes": {
-- "color": 0,
-- "style": 0,
-- "opacity": 0.0,
-- "secondColor": 0
-- },
-- "eyebrows": {
-- "color": 0,
-- "style": 0,
-- "opacity": 0.0,
-- "secondColor": 0
-- },
-- "chestHair": {
-- "color": 0,
-- "style": 0,
-- "opacity": 0.0,
-- "secondColor": 0
-- },
-- "sunDamage": {
-- "color": 0,
-- "style": 0,
-- "opacity": 0.0,
-- "secondColor": 0
-- },
-- "blemishes": {
-- "color": 0,
-- "style": 0,
-- "opacity": 0.0,
-- "secondColor": 0
-- }
-- },
-- "faceFeatures": {
-- "jawBoneBackSize": 0.0,
-- "noseWidth": 0.0,
-- "eyeBrownHigh": 0.0,
-- "chinBoneSize": 0.0,
-- "chinHole": 0.0,
-- "neckThickness": 0.0,
-- "noseBoneTwist": 0.0,
-- "noseBoneHigh": 0.0,
-- "nosePeakHigh": 0.0,
-- "nosePeakSize": 0.0,
-- "eyesOpening": 0.0,
-- "cheeksBoneHigh": 0.0,
-- "cheeksBoneWidth": 0.0,
-- "cheeksWidth": 0.0,
-- "lipsThickness": 0.0,
-- "nosePeakLowering": 0.0,
-- "jawBoneWidth": 0.0,
-- "eyeBrownForward": 0.0,
-- "chinBoneLenght": 0.0,
-- "chinBoneLowering": 0.0
-- },
-- "model": "mp_m_freemode_01",
-- "props": [
-- {
-- "drawable": -1,
-- "prop_id": 0,
-- "texture": -1
-- },
-- {
-- "drawable": -1,
-- "prop_id": 1,
-- "texture": -1
-- },
-- {
-- "drawable": -1,
-- "prop_id": 2,
-- "texture": -1
-- },
-- {
-- "drawable": -1,
-- "prop_id": 6,
-- "texture": -1
-- },
-- {
-- "drawable": -1,
-- "prop_id": 7,
-- "texture": -1
-- }
-- ]
-- }
-- this still needed?

View file

@ -0,0 +1,8 @@
---@diagnostic disable: duplicate-set-field
if GetResourceState('qb-clothing') == 'missing' then return end
if GetResourceState('rcore_clothing') ~= 'missing' then return end
Clothing = Clothing or {}
function Clothing.OpenMenu()
TriggerEvent('qb-clothing:client:openMenu')
end

View file

@ -0,0 +1,239 @@
---@diagnostic disable: duplicate-set-field
if GetResourceState('qb-clothing') == 'missing' then return end
if GetResourceState('rcore_clothing') ~= 'missing' then return end
Clothing = Clothing or {}
Clothing.Players = {}
Callback = Callback or Require("lib/utility/shared/callbacks.lua")
Table = Table or Require('lib/utility/shared/tables.lua')
QBCore = QBCore or exports['qb-core']:GetCoreObject() -- probably easiest for now to call the Framework module
--- Internal function to get the full appearance data including skin, model, and converted format
---@param src number The server ID of the player
---@return table|nil The player's full appearance data or nil if not found
function Clothing.GetFullAppearanceData(src)
src = src and tonumber(src)
assert(src, "src is nil")
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local citId = Player.PlayerData.citizenid
if not citId then return end
if Clothing.Players[citId] then return Clothing.Players[citId] end
local result = MySQL.query.await('SELECT * FROM playerskins WHERE citizenid = ? AND active = ?', { citId, 1 })
if result[1] == nil then return end
local model = Player.PlayerData.model
local skinData = json.decode(result[1].skin)
local converted = Clothing.ConvertToDefault(skinData)
-- Store complete data in the cache
Clothing.Players[citId] = {
model = model,
skin = skinData,
converted = converted
}
return Clothing.Players[citId]
end
--- Retrieves a player's converted appearance data for easy use across modules
---@param src number The server ID of the player
---@param fullData boolean Optional - If true, returns the full data object including skin and model
---@return table|nil The player's converted appearance data or full appearance data if fullData=true
function Clothing.GetAppearance(src, fullData)
if fullData then
return Clothing.GetFullAppearanceData(src)
end
local completeData = Clothing.GetFullAppearanceData(src)
if not completeData then return nil end
return completeData.converted
end
--- Sets a player's appearance based on the provided data
---@param src number The server ID of the player
---@param data table The appearance data to apply
---@param updateBackup boolean Whether to update the backup appearance data
---@return table|nil The updated player appearance data or nil if failed
function Clothing.SetAppearance(src, data, updateBackup, save)
src = src and tonumber(src)
assert(src, "src is nil")
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local citId = Player.PlayerData.citizenid
if not citId then return end
local model = GetEntityModel(GetPlayerPed(src))
if not model then return end
local converted = Clothing.ConvertFromDefault(data)
-- Get full appearance data
local currentClothing = Clothing.GetFullAppearanceData(src)
if not currentClothing then return end
local currentSkin = currentClothing.skin
for k, v in pairs(converted) do
currentSkin[k] = v
end
if not Clothing.Players[citId].backup or updateBackup then
Clothing.Players[citId].backup = currentClothing.converted
end
Clothing.Players[citId] = Clothing.Players[citId] or {}
Clothing.Players[citId].skin = currentSkin
Clothing.Players[citId].model = model
Clothing.Players[citId].converted = Clothing.ConvertToDefault(currentSkin)
if save then
MySQL.update.await('UPDATE playerskins SET skin = ? WHERE citizenid = ? AND active = ?', {
json.encode(currentSkin),
citId,
1
})
end
TriggerClientEvent('community_bridge:client:SetAppearance', src, Clothing.Players[citId].converted)
return Clothing.Players[citId]
end
--- Sets a player's appearance based on gender-specific data
---@param src number The server ID of the player
---@param data table Table containing separate appearance data for male and female characters
---@return table|nil Appearance updated player appearance data or nil if failed
function Clothing.SetAppearanceExt(src, data)
local tbl = Clothing.IsMale(src) and data.male or data.female
Clothing.SetAppearance(src, tbl)
end
--- Reverts a player's appearance to their backup appearance
---@param src number The server ID of the player
---@return boolean|nil Returns true if successful or nil if failed
function Clothing.Revert(src)
src = src and tonumber(src)
assert(src, "src is nil")
local currentClothing = Clothing.GetFullAppearanceData(src)
if not currentClothing then return end
local backup = currentClothing.backup
print(src, backup)
if not backup then return end
return Clothing.SetAppearance(src, backup)
end
function Clothing.OpenMenu(src)
src = src and tonumber(src)
assert(src, "src is nil")
TriggerClientEvent('qb-clothing:client:openMenu', src)
end
--- Event handler for when a player loads into the server
--- Caches the player's appearance data
AddEventHandler('community_bridge:Server:OnPlayerLoaded', function(src)
src = src and tonumber(src)
assert(src, "src is nil")
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local citId = Player.PlayerData.citizenid
if not citId then return end
-- Use GetFullAppearanceData to cache the complete appearance
Clothing.GetFullAppearanceData(src)
end)
--- Event handler for when a player disconnects from the server
--- Removes the player's appearance data from the cache
AddEventHandler('community_bridge:Server:OnPlayerUnload', function(src)
src = src and tonumber(src)
assert(src, "src is nil")
local strSrc = tostring(src)
Clothing.Players[strSrc] = nil
end)
--- Event handler for when the resource starts
--- Caches appearance data for all currently connected players
AddEventHandler('onResourceStart', function(resource)
if resource ~= GetCurrentResourceName() then return end
for _, playerId in ipairs(GetPlayers()) do
local src = tonumber(playerId)
local strSrc = tostring(src)
if not Clothing.Players[strSrc] then
Clothing.Players[strSrc] = Clothing.GetAppearance(src)
end
end
end)
--- Callback handler for retrieving a player's appearance data
Callback.Register('community_bridge:cb:GetAppearance', function(source)
local src = source
return Clothing.GetAppearance(src)
end)
--[[
RegisterCommand('clothing:debug', function(source, args, rawCommand)
local src = source
Clothing.SetAppearance(src, {
components ={
{component_id = 1, drawable = math.random(0, 50), texture = 0},
{component_id = 2, drawable = math.random(0, 50), texture = 0},
{component_id = 3, drawable = math.random(0, 50), texture = 0},
{component_id = 4, drawable = math.random(0, 50), texture = 0},
{component_id = 5, drawable = math.random(0, 50), texture = 0},
{component_id = 6, drawable = math.random(0, 50), texture = 0},
{component_id = 7, drawable = math.random(0, 50), texture = 0},
{component_id = 8, drawable = math.random(0, 50), texture = 0},
},
props = {
{prop_id = 3, drawable = 0, texture = 0},
{prop_id = 1, drawable = 0, texture = 0},
{prop_id = 2, drawable = 0, texture = 0},
}
}, false, true)
end, false)
RegisterCommand('clothing:revert', function(source, args, rawCommand)
local src = source
Clothing.Revert(src)
end, false)
RegisterCommand('clothing:current', function(source, args, rawCommand)
local src = source
local currentClothing = Clothing.GetAppearance(src)
if not currentClothing then return end
print(json.encode(currentClothing, { indent = true }))
end, false)
RegisterCommand('clothing:openmenu', function(source, args, rawCommand)
local src = source
Clothing.OpenMenu(src)
end, false)
--]]
-- RegisterCommand('clothing:crowley', function(source, args, rawCommand)
-- local src = source
-- Clothing.SetAppearance(src, {
-- components = {
-- {drawable = 0, texture = 0, component_id = 0},
-- {drawable = 0, texture = 0, component_id = 1},
-- {drawable = 19, texture = 0, component_id = 2},
-- {drawable = 6, texture = 0, component_id = 3},
-- {drawable = 0, texture = 0, component_id = 4},
-- {drawable = 0, texture = 0, component_id = 5},
-- {drawable = 0, texture = 0, component_id = 6},
-- {drawable = 0, texture = 0, component_id = 7},
-- {drawable = 23, texture = 0, component_id = 8},
-- {drawable = 0, texture = 0, component_id = 9},
-- {drawable = 0, texture = 0, component_id = 10},
-- {drawable = 4, texture = 2, component_id = 11}
-- },
-- props = {
-- {drawable = 27, prop_id = 0, texture = 0},
-- {drawable = 0, prop_id = 1, texture = 0},
-- {drawable = 0, prop_id = 2, texture = 0},
-- {drawable = 0, prop_id = 3, texture = 0},
-- {drawable = 0, prop_id = 4, texture = 0},
-- {drawable = 0, prop_id = 5, texture = 0},
-- -- {drawable = 0, prop_id = 6, texture = 0},
-- -- {drawable = 0, prop_id = 7, texture = 0},
-- }
-- }, false, true)
-- end, false)

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