forked from Simnation/Main
ed
This commit is contained in:
parent
510e3ffcf2
commit
f43cf424cf
305 changed files with 34683 additions and 0 deletions
39
resources/[carscripts]/community_bridge/.github/pull_request_template.md
vendored
Normal file
39
resources/[carscripts]/community_bridge/.github/pull_request_template.md
vendored
Normal 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 project’s 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 -->
|
73
resources/[carscripts]/community_bridge/.github/workflows/main.yml
vendored
Normal file
73
resources/[carscripts]/community_bridge/.github/workflows/main.yml
vendored
Normal 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
|
3
resources/[carscripts]/community_bridge/.vscode/settings.json
vendored
Normal file
3
resources/[carscripts]/community_bridge/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"githubPullRequests.ignoredPullRequestBranches": ["main"]
|
||||
}
|
138
resources/[carscripts]/community_bridge/Attributions.md
Normal file
138
resources/[carscripts]/community_bridge/Attributions.md
Normal 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
|
674
resources/[carscripts]/community_bridge/LICENSE
Normal file
674
resources/[carscripts]/community_bridge/LICENSE
Normal 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>.
|
107
resources/[carscripts]/community_bridge/README.md
Normal file
107
resources/[carscripts]/community_bridge/README.md
Normal 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.
|
||||
|
||||
---
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## 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
|
||||
|
||||
---
|
87
resources/[carscripts]/community_bridge/fxmanifest.lua
Normal file
87
resources/[carscripts]/community_bridge/fxmanifest.lua
Normal 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',
|
||||
}
|
108
resources/[carscripts]/community_bridge/init.lua
Normal file
108
resources/[carscripts]/community_bridge/init.lua
Normal 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)
|
|
@ -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
|
|
@ -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
|
|
@ -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")
|
63
resources/[carscripts]/community_bridge/lib/cache/client/cache.lua
vendored
Normal file
63
resources/[carscripts]/community_bridge/lib/cache/client/cache.lua
vendored
Normal 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
|
0
resources/[carscripts]/community_bridge/lib/cache/server/cache.lua
vendored
Normal file
0
resources/[carscripts]/community_bridge/lib/cache/server/cache.lua
vendored
Normal file
279
resources/[carscripts]/community_bridge/lib/cache/shared/cache.lua
vendored
Normal file
279
resources/[carscripts]/community_bridge/lib/cache/shared/cache.lua
vendored
Normal 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
|
|
@ -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
|
329
resources/[carscripts]/community_bridge/lib/dui/client/dui.lua
Normal file
329
resources/[carscripts]/community_bridge/lib/dui/client/dui.lua
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
|
@ -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
|
86
resources/[carscripts]/community_bridge/lib/init.lua
Normal file
86
resources/[carscripts]/community_bridge/lib/init.lua
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
37
resources/[carscripts]/community_bridge/locales/af.json
Normal file
37
resources/[carscripts]/community_bridge/locales/af.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/ar.json
Normal file
37
resources/[carscripts]/community_bridge/locales/ar.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/cs.json
Normal file
37
resources/[carscripts]/community_bridge/locales/cs.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/da.json
Normal file
37
resources/[carscripts]/community_bridge/locales/da.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/de.json
Normal file
37
resources/[carscripts]/community_bridge/locales/de.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/el.json
Normal file
37
resources/[carscripts]/community_bridge/locales/el.json
Normal 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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/en.json
Normal file
37
resources/[carscripts]/community_bridge/locales/en.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/es.json
Normal file
37
resources/[carscripts]/community_bridge/locales/es.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/fi.json
Normal file
37
resources/[carscripts]/community_bridge/locales/fi.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/fr.json
Normal file
37
resources/[carscripts]/community_bridge/locales/fr.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/hi.json
Normal file
37
resources/[carscripts]/community_bridge/locales/hi.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/hu.json
Normal file
37
resources/[carscripts]/community_bridge/locales/hu.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/it.json
Normal file
37
resources/[carscripts]/community_bridge/locales/it.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/ja.json
Normal file
37
resources/[carscripts]/community_bridge/locales/ja.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/ko.json
Normal file
37
resources/[carscripts]/community_bridge/locales/ko.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/nl.json
Normal file
37
resources/[carscripts]/community_bridge/locales/nl.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/no.json
Normal file
37
resources/[carscripts]/community_bridge/locales/no.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/pl.json
Normal file
37
resources/[carscripts]/community_bridge/locales/pl.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/pt.json
Normal file
37
resources/[carscripts]/community_bridge/locales/pt.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/ro.json
Normal file
37
resources/[carscripts]/community_bridge/locales/ro.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/ru.json
Normal file
37
resources/[carscripts]/community_bridge/locales/ru.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/sv.json
Normal file
37
resources/[carscripts]/community_bridge/locales/sv.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/th.json
Normal file
37
resources/[carscripts]/community_bridge/locales/th.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/tr.json
Normal file
37
resources/[carscripts]/community_bridge/locales/tr.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/zh-cn.json
Normal file
37
resources/[carscripts]/community_bridge/locales/zh-cn.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/zh-hk.json
Normal file
37
resources/[carscripts]/community_bridge/locales/zh-hk.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/zh-tw.json
Normal file
37
resources/[carscripts]/community_bridge/locales/zh-tw.json
Normal 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"
|
||||
}
|
||||
}
|
37
resources/[carscripts]/community_bridge/locales/zh.json
Normal file
37
resources/[carscripts]/community_bridge/locales/zh.json
Normal 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"
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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?
|
|
@ -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
|
|
@ -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)
|
|
@ -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?
|
|
@ -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
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue