chore: baseline v0.1.0
This commit is contained in:
commit
cb69941884
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = crlf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
13
.gitattributes
vendored
Normal file
13
.gitattributes
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
# Sources
|
||||
*.cpp text diff=cpp linguist-language=cpp
|
||||
*.hpp text diff=cpp linguist-language=cpp
|
||||
*.rhai text diff=rust linguist-language=rust
|
||||
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.paa binary
|
||||
|
||||
# Linguistics
|
||||
# Exclude included files and examples from stats
|
||||
include/* linguist-vendored
|
||||
extra/* linguist-vendored
|
||||
17
.gitea/CONTRIBUTING.md
Normal file
17
.gitea/CONTRIBUTING.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Contributing Setup & Guidelines
|
||||
|
||||
## Setting up the Development Environment
|
||||
|
||||
### 1. Clone the repository from GitHub
|
||||
|
||||
### 2. Install HEMTT
|
||||
|
||||
The latest version of HEMTT can be installed by running:
|
||||
|
||||
```cmd
|
||||
winget install hemtt
|
||||
```
|
||||
|
||||
## Coding Guidelines
|
||||
|
||||
This mod follows the same coding guidelines as the ACE3 mod, which can be found [here](https://ace3.acemod.org/wiki/development/coding-guidelines).
|
||||
31
.gitea/ISSUE_TEMPLATE/bug-report.md
Normal file
31
.gitea/ISSUE_TEMPLATE/bug-report.md
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report to help us improve
|
||||
title: ""
|
||||
labels: kind/bug
|
||||
---
|
||||
|
||||
## Describe the bug
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
## To reproduce
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
## Expected behavior
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
## Attachments
|
||||
|
||||
If applicable, add screenshots or RPT logs to help explain your problem.
|
||||
|
||||
## Additional context
|
||||
|
||||
Add any other context about the problem here.
|
||||
18
.gitea/ISSUE_TEMPLATE/feature-request.md
Normal file
18
.gitea/ISSUE_TEMPLATE/feature-request.md
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest a feature to be added
|
||||
title: ""
|
||||
labels: kind/feature-request
|
||||
---
|
||||
|
||||
## Describe the feature that you would like
|
||||
|
||||
A clear and concise description of the feature you'd want.
|
||||
|
||||
## Possible alternatives
|
||||
|
||||
Possible alternatives to your suggestion.
|
||||
|
||||
## Additional context
|
||||
|
||||
Add any other context about the feature here.
|
||||
16
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
16
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
@ -0,0 +1,16 @@
|
||||
**When merged this pull request will:**
|
||||
|
||||
- Describe what this pull request will do
|
||||
- Each change in a separate line
|
||||
|
||||
### Important
|
||||
|
||||
- [ ] If the contribution affects [the documentation](../docs), please include your changes in this pull request.
|
||||
- [ ] [Development Guidelines](https://github.com/IDSolutions/MOD_REPO/blob/main/.github/CONTRIBUTING.md) are read, understood and applied.
|
||||
- [ ] Title of this PR uses our standard template `Component - Add|Fix|Improve|Change|Make|Remove {changes}`.
|
||||
|
||||
<!-- Known issues that need to be addressed -->
|
||||
|
||||
### Known Issues
|
||||
|
||||
- [ ] Issue
|
||||
1
.gitea/assets/placeholder.txt
Normal file
1
.gitea/assets/placeholder.txt
Normal file
@ -0,0 +1 @@
|
||||
|
||||
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
# Rust
|
||||
/target/
|
||||
**/*.rs.bk
|
||||
*.pdb
|
||||
|
||||
# Cargo
|
||||
Cargo.lock
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Build artifacts
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Misc
|
||||
node_modules/
|
||||
docus/.nuxt/
|
||||
docus/.output/
|
||||
docus/.data/
|
||||
docus/.nitro/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Arma
|
||||
arma/ui/map-viewer/
|
||||
arma/server/surrealdb/forge.db/
|
||||
promo/
|
||||
49
Architecture_Diagram.md
Normal file
49
Architecture_Diagram.md
Normal file
@ -0,0 +1,49 @@
|
||||
# Forge Architecture
|
||||
|
||||
## Runtime Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Client[Arma Client Addons] --> Server[Arma Server Addons]
|
||||
Server --> Bridge[Extension Bridge]
|
||||
Bridge --> Extension[Rust arma-rs Extension]
|
||||
Extension --> Services[Service Layer]
|
||||
Services --> Repositories[Repository Traits]
|
||||
Repositories --> Surreal[(SurrealDB)]
|
||||
```
|
||||
|
||||
## Persistence Startup
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Arma as Arma Server
|
||||
participant Ext as Forge Extension
|
||||
participant Db as SurrealDB
|
||||
|
||||
Arma->>Ext: init
|
||||
Ext->>Db: connect
|
||||
Ext->>Db: apply schema modules
|
||||
Db-->>Ext: ready
|
||||
Arma->>Ext: status
|
||||
Ext-->>Arma: connected
|
||||
```
|
||||
|
||||
## Data Access
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant SQF as SQF Addon
|
||||
participant Ext as Extension Command
|
||||
participant Service as Service
|
||||
participant Repo as Repository
|
||||
participant Db as SurrealDB
|
||||
|
||||
SQF->>Ext: domain command
|
||||
Ext->>Service: validate and execute
|
||||
Service->>Repo: repository call
|
||||
Repo->>Db: query/upsert/delete
|
||||
Db-->>Repo: result
|
||||
Repo-->>Service: domain model
|
||||
Service-->>Ext: response
|
||||
Ext-->>SQF: serialized result
|
||||
```
|
||||
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"arma/server/extension",
|
||||
"bin/icom",
|
||||
"lib/models",
|
||||
"lib/repositories",
|
||||
"lib/services",
|
||||
"lib/shared",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.dependencies]
|
||||
arma-rs = { version = "1.11.15", features = ["chrono", "serde_json", "uuid"] }
|
||||
chrono = "0.4.42"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
tokio = { version = "1.47.1", features = ["full"] }
|
||||
uuid = { version = "1.18.1", features = ["v4"] }
|
||||
119
LICENSE.md
Normal file
119
LICENSE.md
Normal file
@ -0,0 +1,119 @@
|
||||

|
||||
|
||||
## Brief summary of this Licence
|
||||
|
||||
PLEASE, NOTE THAT THIS SUMMARY HAS NO LEGAL EFFECT AND IS ONLY OF AN INFORMATORY NATURE DESIGNED FOR YOU TO GET THE BASIC INFORMATION ABOUT THE CONTENT OF THIS LICENCE. THE ONLY LEGALLY BINDING PROVISIONS ARE THOSE IN THE ORIGINAL AND FULL TEXT OF THIS LICENCE.
|
||||
|
||||
With this licence you are free to adapt (i.e. modify, rework or update) and share (i.e. copy, distribute or transmit) the material under the following conditions:
|
||||
|
||||
- **Attribution** - You must attribute the material in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the material).
|
||||
- **Noncommercial** - You may not use this material for any commercial purposes.
|
||||
- **Arma Only** - You may not convert or adapt this material to be used in other games than Arma.
|
||||
- **Share Alike** - If you adapt, or build upon this material, you may distribute the resulting material only under the same license.
|
||||
|
||||
---
|
||||
|
||||
# Full version of licence
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Arma Public License - Share Alike ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
|
||||
|
||||
### Section 1 – Definitions
|
||||
|
||||
1. **Adapted Material** means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
|
||||
2. **Adapter's License** means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
|
||||
3. **ArmaOnly** means primarily intended for or directed towards the use in any of existing and future Arma games, including but not limited to Arma: Cold War Assault, Arma, Arma 2 and Arma 3 and its official sequels and expansion packs.
|
||||
4. **Arma Public Share Alike Compatible License** means a license listed at [https://www.bohemia.net/community/licenses](https://www.bohemia.net/community/licenses) as essentially the equivalent of this Public License.
|
||||
5. **Copyright and Similar Rights** means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
|
||||
6. **Effective Technological Measures** means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
|
||||
7. **Exceptions and Limitations** means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
|
||||
8. **Licensed Material** means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
|
||||
9. **Licensed Rights** means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
|
||||
10. **Licensor** means the individual(s) or entity(ies) granting rights under this Public License.
|
||||
11. **NonCommercial** means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange.
|
||||
12. **Share** means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
|
||||
13. **Sui Generis Database Rights** means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
|
||||
14. **You** means the individual or entity exercising the Licensed Rights under this Public License. **Your** has a corresponding meaning.
|
||||
|
||||
### Section 2 – Scope
|
||||
|
||||
1. **License grant**
|
||||
1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
|
||||
1. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial and ArmaOnly purposes only; and
|
||||
2. produce, reproduce, and Share Adapted Material for NonCommercial and ArmaOnly purposes only.
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
|
||||
3. Term. The term of this Public License is specified in Section 6(a).
|
||||
4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
|
||||
5. Downstream recipients.
|
||||
1. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
|
||||
2. Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply.
|
||||
3. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
|
||||
6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(a)(i).
|
||||
2. **Other rights**
|
||||
1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
|
||||
2. Patent and trademark rights are not licensed under this Public License.
|
||||
3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial and ArmaOnly purposes.
|
||||
|
||||
### Section 3 – License Conditions
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
|
||||
|
||||
1. **Attribution**
|
||||
1. If You Share the Licensed Material (including in modified form), You must:
|
||||
1. retain the following if it is supplied by the Licensor with the Licensed Material:
|
||||
1. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
|
||||
2. a copyright notice;
|
||||
3. a notice that refers to this Public License;
|
||||
4. a notice that refers to the disclaimer of warranties;
|
||||
5. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
|
||||
2. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
|
||||
3. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
|
||||
3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(a) to the extent reasonably practicable.
|
||||
2. **ShareAlike**
|
||||
In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply.
|
||||
1. The Adapter’s License You apply must be this Public License, or an Arma Public Share Alike Compatible License.
|
||||
2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material.
|
||||
3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply.
|
||||
|
||||
### Section 4 – Sui Generis Database Rights
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
|
||||
|
||||
1. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial and ArmaOnly purposes only;
|
||||
2. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and
|
||||
3. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
|
||||
|
||||
### Section 5 – Disclaimer of Warranties and Limitation of Liability
|
||||
|
||||
1. **Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.**
|
||||
2. **To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.**
|
||||
3. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
|
||||
|
||||
### Section 6 – Term and Termination
|
||||
|
||||
1. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
|
||||
2. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
|
||||
1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
|
||||
2. upon express reinstatement by the Licensor.
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
|
||||
3. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
|
||||
4. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
|
||||
|
||||
### Section 7 – Other Terms and Conditions
|
||||
|
||||
1. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
|
||||
2. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
|
||||
|
||||
### Section 8 – Interpretation
|
||||
|
||||
1. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
|
||||
2. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
|
||||
3. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
|
||||
4. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
|
||||
|
||||
### Bohemia Interactive Notices
|
||||
|
||||
1. Bohemia Interactive a.s. is not a party to this License, and makes no warranty whatsoever in connection with the Licensed Material. Bohemia Interactive a.s. will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, Bohemia Interactive a.s. may elect to apply the Public License to material it publishes and in those instances it becomes the "Licensor".
|
||||
2. Except for the limited purpose of indicating to the public that the Licensed Material is shared under this Public License, Bohemia Interactive a.s. does not authorize the use by either party of the trademarks "Arma", "Bohemia Interactive" or any related trademark or logo of Arma or Bohemia Interactive without the prior written consent of Bohemia Interactive a.s.
|
||||
62
README.md
Normal file
62
README.md
Normal file
@ -0,0 +1,62 @@
|
||||
# Forge
|
||||
|
||||
Forge is a framework for Arma 3 persistent game servers. It combines SQF
|
||||
addons, a Rust `arma-rs` extension, shared service crates, and web-based client
|
||||
interfaces for player data, organizations, banking, garages, lockers, phones,
|
||||
CAD, stores, and task workflows.
|
||||
|
||||
## Storage
|
||||
|
||||
Durable persistence is backed by SurrealDB. The server extension loads schema
|
||||
modules at startup and routes domain repositories through the SurrealDB client.
|
||||
|
||||
```toml
|
||||
[surreal]
|
||||
endpoint = "127.0.0.1:8000"
|
||||
namespace = "forge"
|
||||
database = "main"
|
||||
username = "root"
|
||||
password = "root"
|
||||
connect_timeout_ms = 5000
|
||||
```
|
||||
|
||||
## Workspace
|
||||
|
||||
```text
|
||||
arma/
|
||||
client/ Client-side addons and browser UIs
|
||||
server/ Server-side addons and extension crate
|
||||
bin/
|
||||
icom/ Interprocess communication helper
|
||||
lib/
|
||||
models/ Shared domain models
|
||||
repositories/ Repository traits and in-memory test stores
|
||||
services/ Domain business logic
|
||||
shared/ Cross-crate helpers
|
||||
tools/ Web UI build tooling
|
||||
```
|
||||
|
||||
## Common Commands
|
||||
|
||||
```powershell
|
||||
cargo test
|
||||
npm run build:webui
|
||||
.\build-arma.ps1
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Framework Documentation](./docs/README.md)
|
||||
- [Framework Architecture](./docs/FRAMEWORK_ARCHITECTURE.md)
|
||||
- [Module Reference](./docs/MODULE_REFERENCE.md)
|
||||
- [Development Guide](./docs/DEVELOPMENT_GUIDE.md)
|
||||
- [Git Workflow](./docs/GIT_WORKFLOW.md)
|
||||
|
||||
## Extension Status
|
||||
|
||||
```sqf
|
||||
"forge_server" callExtension ["status", []];
|
||||
"forge_server" callExtension ["surreal:status", []];
|
||||
```
|
||||
|
||||
Both commands report the persistence connection state.
|
||||
12
arma/client/.editorconfig
Normal file
12
arma/client/.editorconfig
Normal file
@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = crlf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
13
arma/client/.gitattributes
vendored
Normal file
13
arma/client/.gitattributes
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
# Sources
|
||||
*.cpp text diff=cpp linguist-language=cpp
|
||||
*.hpp text diff=cpp linguist-language=cpp
|
||||
*.rhai text diff=rust linguist-language=rust
|
||||
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.paa binary
|
||||
|
||||
# Linguistics
|
||||
# Exclude included files and examples from stats
|
||||
include/* linguist-vendored
|
||||
extra/* linguist-vendored
|
||||
17
arma/client/.github/CONTRIBUTING.md
vendored
Normal file
17
arma/client/.github/CONTRIBUTING.md
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
# Contributing Setup & Guidelines
|
||||
|
||||
## Setting up the Development Environment
|
||||
|
||||
### 1. Clone the repository from GitHub
|
||||
|
||||
### 2. Install HEMTT
|
||||
|
||||
The latest version of HEMTT can be installed by running:
|
||||
|
||||
```cmd
|
||||
winget install hemtt
|
||||
```
|
||||
|
||||
## Coding Guidelines
|
||||
|
||||
This mod follows the same coding guidelines as the ACE3 mod, which can be found [here](https://ace3.acemod.org/wiki/development/coding-guidelines).
|
||||
31
arma/client/.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
31
arma/client/.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report to help us improve
|
||||
title: ""
|
||||
labels: kind/bug
|
||||
---
|
||||
|
||||
## Describe the bug
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
## To reproduce
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
## Expected behavior
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
## Attachments
|
||||
|
||||
If applicable, add screenshots or RPT logs to help explain your problem.
|
||||
|
||||
## Additional context
|
||||
|
||||
Add any other context about the problem here.
|
||||
18
arma/client/.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
18
arma/client/.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest a feature to be added
|
||||
title: ""
|
||||
labels: kind/feature-request
|
||||
---
|
||||
|
||||
## Describe the feature that you would like
|
||||
|
||||
A clear and concise description of the feature you'd want.
|
||||
|
||||
## Possible alternatives
|
||||
|
||||
Possible alternatives to your suggestion.
|
||||
|
||||
## Additional context
|
||||
|
||||
Add any other context about the feature here.
|
||||
16
arma/client/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
16
arma/client/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
**When merged this pull request will:**
|
||||
|
||||
- Describe what this pull request will do
|
||||
- Each change in a separate line
|
||||
|
||||
### Important
|
||||
|
||||
- [ ] If the contribution affects [the documentation](../docs), please include your changes in this pull request.
|
||||
- [ ] [Development Guidelines](https://github.com/IDSolutions/MOD_REPO/blob/main/.github/CONTRIBUTING.md) are read, understood and applied.
|
||||
- [ ] Title of this PR uses our standard template `Component - Add|Fix|Improve|Change|Make|Remove {changes}`.
|
||||
|
||||
<!-- Known issues that need to be addressed -->
|
||||
|
||||
### Known Issues
|
||||
|
||||
- [ ] Issue
|
||||
1
arma/client/.github/assets/placeholder.txt
vendored
Normal file
1
arma/client/.github/assets/placeholder.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
|
||||
28
arma/client/.github/workflows/check.yml
vendored
Normal file
28
arma/client/.github/workflows/check.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
name: HEMTT
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate Config
|
||||
run: python tools/config_style_checker.py
|
||||
- name: Check for BOM
|
||||
uses: arma-actions/bom-check@master
|
||||
with:
|
||||
path: "addons"
|
||||
|
||||
- name: Setup HEMTT
|
||||
uses: arma-actions/hemtt@v1
|
||||
- name: Run HEMTT check
|
||||
run: hemtt check --pedantic
|
||||
18
arma/client/.gitignore
vendored
Normal file
18
arma/client/.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
# HEMTT
|
||||
hemtt.exe
|
||||
.hemtt/missions/~*
|
||||
.hemttout/
|
||||
releases/
|
||||
.hemttprivatekey
|
||||
|
||||
# Textures
|
||||
Exports/
|
||||
*.spp
|
||||
*.spp.painter_lock
|
||||
*.psd
|
||||
|
||||
# Other
|
||||
*.biprivatekey
|
||||
*.zip
|
||||
*.pbo
|
||||
*.sqfc
|
||||
28
arma/client/.hemtt/commands/ctrlWebBrowserAction.yml
Normal file
28
arma/client/.hemtt/commands/ctrlWebBrowserAction.yml
Normal file
@ -0,0 +1,28 @@
|
||||
name: ctrlWebBrowserAction
|
||||
description: Executes an action on a web browser control
|
||||
groups:
|
||||
- GUI Control
|
||||
syntax:
|
||||
- call: !Binary [control, actionArray]
|
||||
ret:
|
||||
- Nothing
|
||||
- Nothing
|
||||
params:
|
||||
- name: control
|
||||
type: Control
|
||||
description: Web browser control to execute action on
|
||||
- name: actionArray
|
||||
type: ArrayUnknown
|
||||
description: |
|
||||
Array in format [actionType, actionData] where:
|
||||
- actionType (String): Type of action ("ExecJS", "LoadURL", "Reload", "Stop", etc.)
|
||||
- actionData (String): Data for the action (JavaScript code for ExecJS, URL for LoadURL, empty string for others)
|
||||
argument_loc: Local
|
||||
effect_loc: Local
|
||||
since:
|
||||
arma_3:
|
||||
major: 2
|
||||
minor: 2
|
||||
examples:
|
||||
- <sqf>_control ctrlWebBrowserAction ["ExecJS", "document.getElementById('test').innerHTML = 'Hello World!'"];</sqf>
|
||||
- <sqf>_control ctrlWebBrowserAction ["LoadURL", "https://community.bistudio.com"];</sqf>
|
||||
13
arma/client/.hemtt/hooks/post_release/01_move_readme.rhai
Normal file
13
arma/client/.hemtt/hooks/post_release/01_move_readme.rhai
Normal file
@ -0,0 +1,13 @@
|
||||
let readme = HEMTT_RFS.join("docs")
|
||||
.join("README.md")
|
||||
.open_file()
|
||||
.read();
|
||||
readme.replace("0.0.0",
|
||||
HEMTT.project()
|
||||
.version()
|
||||
.to_string_short()
|
||||
);
|
||||
HEMTT_RFS.join("README.md")
|
||||
.create_file()
|
||||
.write(readme);
|
||||
print("README.md version set to " + HEMTT.project().version());
|
||||
26
arma/client/.hemtt/hooks/pre_build/01_set_version.rhai
Normal file
26
arma/client/.hemtt/hooks/pre_build/01_set_version.rhai
Normal file
@ -0,0 +1,26 @@
|
||||
let modcpp = HEMTT_VFS.join("mod.cpp")
|
||||
.open_file()
|
||||
.read();
|
||||
modcpp.replace("0.0.0",
|
||||
HEMTT.project()
|
||||
.version()
|
||||
.to_string_short()
|
||||
);
|
||||
HEMTT_VFS.join("mod.cpp")
|
||||
.create_file()
|
||||
.write(modcpp);
|
||||
print("mod.cpp version set to " + HEMTT.project().version());
|
||||
|
||||
// Currently unused, but included anyway
|
||||
let readme = HEMTT_VFS.join("README.md")
|
||||
.open_file()
|
||||
.read();
|
||||
readme.replace("0.0.0",
|
||||
HEMTT.project()
|
||||
.version()
|
||||
.to_string_short()
|
||||
);
|
||||
HEMTT_VFS.join("README.md")
|
||||
.create_file()
|
||||
.write(readme);
|
||||
print("README.md version set to " + HEMTT.project().version());
|
||||
24
arma/client/.hemtt/launch.toml
Normal file
24
arma/client/.hemtt/launch.toml
Normal file
@ -0,0 +1,24 @@
|
||||
[default]
|
||||
workshop = [
|
||||
"450814997", # CBA_A3
|
||||
"3499977893", # Advanced Dev Tools
|
||||
"623475643", # 3DEN Enhanced
|
||||
"3023395342", # 3DEN Attributes Fast Load
|
||||
]
|
||||
presets = []
|
||||
dlc = []
|
||||
optionals = []
|
||||
parameters = [
|
||||
"-skipIntro",
|
||||
"-noSplash",
|
||||
"-showScriptErrors",
|
||||
"-debug",
|
||||
"-filePatching",
|
||||
"-world=empty",
|
||||
]
|
||||
|
||||
[ace]
|
||||
extends = "default"
|
||||
workshop = [
|
||||
"463939057", # ACE
|
||||
]
|
||||
40
arma/client/.hemtt/lints.toml
Normal file
40
arma/client/.hemtt/lints.toml
Normal file
@ -0,0 +1,40 @@
|
||||
[sqf.banned_commands]
|
||||
options.banned = [
|
||||
# "spawn", # Scheduled should be avoided whenever possible
|
||||
"execVM", # Script files should never be run directly, they should be functions
|
||||
"remoteExec", # CBA events should be used for networking
|
||||
]
|
||||
|
||||
[sqf.banned_macros]
|
||||
options.release = [
|
||||
"DEBUG_MODE_FULL",
|
||||
"DISABLE_COMPILE_CACHE",
|
||||
]
|
||||
|
||||
[sqf.event_unknown]
|
||||
options.ignore = [
|
||||
"JSDialog",
|
||||
]
|
||||
|
||||
[sqf.this_call]
|
||||
enabled = true
|
||||
|
||||
[sqf.undefined]
|
||||
enabled = true
|
||||
options.check_orphan_code = true
|
||||
|
||||
[sqf.unused]
|
||||
enabled = true # many false positives without DEBUG_MODE_FULL
|
||||
options.check_params = false
|
||||
|
||||
[sqf.shadowed]
|
||||
enabled = false
|
||||
|
||||
[sqf.not_private]
|
||||
enabled = true
|
||||
|
||||
[config.file_type]
|
||||
options.allow_no_extension = false
|
||||
|
||||
[stringtables.usage]
|
||||
options.ignore_unused = true
|
||||
24
arma/client/.hemtt/project.toml
Normal file
24
arma/client/.hemtt/project.toml
Normal file
@ -0,0 +1,24 @@
|
||||
name = "forge-client"
|
||||
author = "J.Schmidt"
|
||||
prefix = "forge_client"
|
||||
mainprefix = "forge"
|
||||
|
||||
[version]
|
||||
path = "addons/main/script_version.hpp"
|
||||
git_hash = 0
|
||||
|
||||
[files]
|
||||
include = [
|
||||
"mod.cpp",
|
||||
"meta.cpp",
|
||||
"icon_64_ca.paa",
|
||||
"icon_128_ca.paa",
|
||||
"icon_128_highlight_ca.paa",
|
||||
"title_ca.paa",
|
||||
"LICENSE.md",
|
||||
"README.md",
|
||||
]
|
||||
exclude = []
|
||||
|
||||
[properties]
|
||||
author = "J.Schmidt"
|
||||
19
arma/client/.hemtt/scripts/update_build.rhai
Normal file
19
arma/client/.hemtt/scripts/update_build.rhai
Normal file
@ -0,0 +1,19 @@
|
||||
// Read the current contents of script_version.hpp
|
||||
let script_version = HEMTT_RFS.join("addons")
|
||||
.join("main")
|
||||
.join("script_version.hpp")
|
||||
.open_file()
|
||||
.read();
|
||||
|
||||
// Replace the current version with the new version
|
||||
let prefix = "#define BUILD ";
|
||||
let current = HEMTT.project().version().build();
|
||||
let next = current + 1;
|
||||
script_version.replace(prefix + current.to_string(), prefix + next.to_string());
|
||||
|
||||
// Write the modified contents to script_version.hpp
|
||||
HEMTT_RFS.join("addons")
|
||||
.join("main")
|
||||
.join("script_version.hpp")
|
||||
.create_file()
|
||||
.write(script_version);
|
||||
23
arma/client/.hemtt/scripts/update_minor.rhai
Normal file
23
arma/client/.hemtt/scripts/update_minor.rhai
Normal file
@ -0,0 +1,23 @@
|
||||
// Read the current contents of script_version.hpp
|
||||
let script_version = HEMTT_RFS.join("addons")
|
||||
.join("main")
|
||||
.join("script_version.hpp")
|
||||
.open_file()
|
||||
.read();
|
||||
|
||||
// Replace the current version with the new version
|
||||
let prefix = "#define MINOR ";
|
||||
let current = HEMTT.project().version().minor();
|
||||
let next = current + 1;
|
||||
|
||||
// Updating minor version should reset patch number
|
||||
script_version.replace(prefix + current.to_string(), prefix + next.to_string());
|
||||
current = HEMTT.project().version().patch();
|
||||
script_version.replace("#define PATCH " + current.to_string(), "#define PATCH 0");
|
||||
|
||||
// Write the modified contents to script_version.hpp
|
||||
HEMTT_RFS.join("addons")
|
||||
.join("main")
|
||||
.join("script_version.hpp")
|
||||
.create_file()
|
||||
.write(script_version);
|
||||
20
arma/client/.hemtt/scripts/update_patch.rhai
Normal file
20
arma/client/.hemtt/scripts/update_patch.rhai
Normal file
@ -0,0 +1,20 @@
|
||||
// Read the current contents of script_version.hpp
|
||||
let script_version = HEMTT_RFS.join("addons")
|
||||
.join("main")
|
||||
.join("script_version.hpp")
|
||||
.open_file()
|
||||
.read();
|
||||
|
||||
// Replace the current version with the new version
|
||||
let prefix = "#define PATCH ";
|
||||
let current = HEMTT.project().version().patch();
|
||||
let next = current + 1;
|
||||
|
||||
script_version.replace(prefix + current.to_string(), prefix + next.to_string());
|
||||
|
||||
// Write the modified contents to script_version.hpp
|
||||
HEMTT_RFS.join("addons")
|
||||
.join("main")
|
||||
.join("script_version.hpp")
|
||||
.create_file()
|
||||
.write(script_version);
|
||||
119
arma/client/LICENSE.md
Normal file
119
arma/client/LICENSE.md
Normal file
@ -0,0 +1,119 @@
|
||||

|
||||
|
||||
## Brief summary of this Licence
|
||||
|
||||
PLEASE, NOTE THAT THIS SUMMARY HAS NO LEGAL EFFECT AND IS ONLY OF AN INFORMATORY NATURE DESIGNED FOR YOU TO GET THE BASIC INFORMATION ABOUT THE CONTENT OF THIS LICENCE. THE ONLY LEGALLY BINDING PROVISIONS ARE THOSE IN THE ORIGINAL AND FULL TEXT OF THIS LICENCE.
|
||||
|
||||
With this licence you are free to adapt (i.e. modify, rework or update) and share (i.e. copy, distribute or transmit) the material under the following conditions:
|
||||
|
||||
- **Attribution** - You must attribute the material in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the material).
|
||||
- **Noncommercial** - You may not use this material for any commercial purposes.
|
||||
- **Arma Only** - You may not convert or adapt this material to be used in other games than Arma.
|
||||
- **Share Alike** - If you adapt, or build upon this material, you may distribute the resulting material only under the same license.
|
||||
|
||||
---
|
||||
|
||||
# Full version of licence
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Arma Public License - Share Alike ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
|
||||
|
||||
### Section 1 – Definitions
|
||||
|
||||
1. **Adapted Material** means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
|
||||
2. **Adapter's License** means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
|
||||
3. **ArmaOnly** means primarily intended for or directed towards the use in any of existing and future Arma games, including but not limited to Arma: Cold War Assault, Arma, Arma 2 and Arma 3 and its official sequels and expansion packs.
|
||||
4. **Arma Public Share Alike Compatible License** means a license listed at [https://www.bohemia.net/community/licenses](https://www.bohemia.net/community/licenses) as essentially the equivalent of this Public License.
|
||||
5. **Copyright and Similar Rights** means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
|
||||
6. **Effective Technological Measures** means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
|
||||
7. **Exceptions and Limitations** means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
|
||||
8. **Licensed Material** means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
|
||||
9. **Licensed Rights** means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
|
||||
10. **Licensor** means the individual(s) or entity(ies) granting rights under this Public License.
|
||||
11. **NonCommercial** means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange.
|
||||
12. **Share** means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
|
||||
13. **Sui Generis Database Rights** means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
|
||||
14. **You** means the individual or entity exercising the Licensed Rights under this Public License. **Your** has a corresponding meaning.
|
||||
|
||||
### Section 2 – Scope
|
||||
|
||||
1. **License grant**
|
||||
1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
|
||||
1. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial and ArmaOnly purposes only; and
|
||||
2. produce, reproduce, and Share Adapted Material for NonCommercial and ArmaOnly purposes only.
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
|
||||
3. Term. The term of this Public License is specified in Section 6(a).
|
||||
4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
|
||||
5. Downstream recipients.
|
||||
1. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
|
||||
2. Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply.
|
||||
3. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
|
||||
6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(a)(i).
|
||||
2. **Other rights**
|
||||
1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
|
||||
2. Patent and trademark rights are not licensed under this Public License.
|
||||
3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial and ArmaOnly purposes.
|
||||
|
||||
### Section 3 – License Conditions
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
|
||||
|
||||
1. **Attribution**
|
||||
1. If You Share the Licensed Material (including in modified form), You must:
|
||||
1. retain the following if it is supplied by the Licensor with the Licensed Material:
|
||||
1. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
|
||||
2. a copyright notice;
|
||||
3. a notice that refers to this Public License;
|
||||
4. a notice that refers to the disclaimer of warranties;
|
||||
5. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
|
||||
2. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
|
||||
3. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
|
||||
3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(a) to the extent reasonably practicable.
|
||||
2. **ShareAlike**
|
||||
In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply.
|
||||
1. The Adapter’s License You apply must be this Public License, or an Arma Public Share Alike Compatible License.
|
||||
2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material.
|
||||
3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply.
|
||||
|
||||
### Section 4 – Sui Generis Database Rights
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
|
||||
|
||||
1. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial and ArmaOnly purposes only;
|
||||
2. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and
|
||||
3. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
|
||||
|
||||
### Section 5 – Disclaimer of Warranties and Limitation of Liability
|
||||
|
||||
1. **Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.**
|
||||
2. **To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.**
|
||||
3. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
|
||||
|
||||
### Section 6 – Term and Termination
|
||||
|
||||
1. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
|
||||
2. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
|
||||
1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
|
||||
2. upon express reinstatement by the Licensor.
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
|
||||
3. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
|
||||
4. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
|
||||
|
||||
### Section 7 – Other Terms and Conditions
|
||||
|
||||
1. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
|
||||
2. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
|
||||
|
||||
### Section 8 – Interpretation
|
||||
|
||||
1. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
|
||||
2. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
|
||||
3. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
|
||||
4. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
|
||||
|
||||
### Bohemia Interactive Notices
|
||||
|
||||
1. Bohemia Interactive a.s. is not a party to this License, and makes no warranty whatsoever in connection with the Licensed Material. Bohemia Interactive a.s. will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, Bohemia Interactive a.s. may elect to apply the Public License to material it publishes and in those instances it becomes the "Licensor".
|
||||
2. Except for the limited purpose of indicating to the public that the Licensed Material is shared under this Public License, Bohemia Interactive a.s. does not authorize the use by either party of the trademarks "Arma", "Bohemia Interactive" or any related trademark or logo of Arma or Bohemia Interactive without the prior written consent of Bohemia Interactive a.s.
|
||||
46
arma/client/README.md
Normal file
46
arma/client/README.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Forge Client
|
||||
|
||||
Forge Client contains the Arma client-side addons for Forge. It owns player UI,
|
||||
browser bridges, client repositories, local event handling, and client-to-server
|
||||
CBA RPC requests.
|
||||
|
||||
The client mod pairs with `arma/server`: client addons collect player input and
|
||||
render state, while server addons and the Rust extension own authoritative
|
||||
state and persistence.
|
||||
|
||||
## Requirements
|
||||
- CBA A3
|
||||
- ACE3 for features that use ACE interactions, arsenal, spectator, or medical
|
||||
integrations
|
||||
- Forge Server running the matching server-side addons
|
||||
|
||||
## Addons
|
||||
- `main`: shared client mod config and macros
|
||||
- `common`: shared browser UI bridge helpers
|
||||
- `actor`: player interaction menu and actor repository
|
||||
- `bank`: banking UI and account request bridge
|
||||
- `cad`: map/CAD UI for dispatch, groups, tasks, and support requests
|
||||
- `garage`: vehicle storage and virtual garage UI
|
||||
- `locker`: locker and virtual arsenal repositories
|
||||
- `notifications`: notification HUD and sounds
|
||||
- `org`: organization portal UI
|
||||
- `phone`: phone, contacts, messages, and email UI
|
||||
- `store`: storefront catalog and checkout UI
|
||||
|
||||
## UI Pattern
|
||||
Most feature UIs use an Arma display with a `CT_WEBBROWSER` control. JavaScript
|
||||
sends JSON events through A3API, SQF handles them in `fnc_handleUIEvents.sqf`,
|
||||
and response events are sent back into the browser with `ctrlWebBrowserAction
|
||||
["ExecJS", ...]`.
|
||||
|
||||
Client repositories cache the most recent state for display only. Server addons
|
||||
and the extension remain authoritative.
|
||||
|
||||
## Documentation
|
||||
- [Root client usage guide](../../docs/CLIENT_USAGE_GUIDE.md)
|
||||
- [Client docs](./docs/README.md)
|
||||
- [Common web UI framework notes](./addons/common/WEB_UI_FRAMEWORK.md)
|
||||
- [CAD map integration notes](./addons/cad/MAP_README.md)
|
||||
|
||||
## License
|
||||
Forge Client is licensed under [APL-SA](./LICENSE.md).
|
||||
1
arma/client/addons/actor/$PBOPREFIX$
Normal file
1
arma/client/addons/actor/$PBOPREFIX$
Normal file
@ -0,0 +1 @@
|
||||
forge\forge_client\addons\actor
|
||||
19
arma/client/addons/actor/CfgEventHandlers.hpp
Normal file
19
arma/client/addons/actor/CfgEventHandlers.hpp
Normal file
@ -0,0 +1,19 @@
|
||||
class Extended_PreStart_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preStart));
|
||||
};
|
||||
};
|
||||
|
||||
class Extended_PreInit_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
|
||||
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_preInitClient));
|
||||
};
|
||||
};
|
||||
|
||||
class Extended_PostInit_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_postInit));
|
||||
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_postInitClient));
|
||||
};
|
||||
};
|
||||
28
arma/client/addons/actor/README.md
Normal file
28
arma/client/addons/actor/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Forge Client Actor
|
||||
|
||||
## Overview
|
||||
The actor addon owns the player interaction menu and client-side actor
|
||||
repository. It initializes actor state from the server, tracks client-visible
|
||||
actor fields, and routes menu actions to other Forge UIs.
|
||||
|
||||
## Dependencies
|
||||
- `forge_client_main`
|
||||
- server actor events from `forge_server_actor`
|
||||
- runtime integrations with bank, CAD, garage, org, phone, store, locker, and
|
||||
notifications addons
|
||||
|
||||
## Main Components
|
||||
- `fnc_initRepository.sqf` manages client actor state and server init/save
|
||||
requests.
|
||||
- `fnc_openUI.sqf` opens `RscActorMenu`.
|
||||
- `fnc_handleUIEvents.sqf` handles browser menu actions.
|
||||
|
||||
## Event Surface
|
||||
The actor menu can open bank, ATM mode, CAD, garage, virtual garage, org, phone,
|
||||
store, and ACE arsenal interactions. Client post-init also wires player killed
|
||||
and respawn handlers into the server economy flow.
|
||||
|
||||
## Runtime Notes
|
||||
Actor state is loaded before dependent systems initialize. When the server sends
|
||||
actor sync data, the repository updates local view state and clears the loading
|
||||
screen.
|
||||
3
arma/client/addons/actor/XEH_PREP.hpp
Normal file
3
arma/client/addons/actor/XEH_PREP.hpp
Normal file
@ -0,0 +1,3 @@
|
||||
PREP(handleUIEvents);
|
||||
PREP(initRepository);
|
||||
PREP(openUI);
|
||||
1
arma/client/addons/actor/XEH_postInit.sqf
Normal file
1
arma/client/addons/actor/XEH_postInit.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "script_component.hpp"
|
||||
92
arma/client/addons/actor/XEH_postInitClient.sqf
Normal file
92
arma/client/addons/actor/XEH_postInitClient.sqf
Normal file
@ -0,0 +1,92 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
removeAllWeapons player;
|
||||
removeAllAssignedItems player;
|
||||
removeUniform player;
|
||||
removeVest player;
|
||||
removeBackpack player;
|
||||
removeGoggles player;
|
||||
removeHeadgear player;
|
||||
|
||||
SETPVAR(player,FORGE_isLoaded,false);
|
||||
cutText ["Loading In...", "BLACK", 1];
|
||||
|
||||
player addEventHandler ["Killed", {
|
||||
params ["_unit", "_killer", "_instigator", "_useEffects"];
|
||||
[SRPC(economy,onKilled), [_unit]] call CFUNC(serverEvent);
|
||||
}];
|
||||
|
||||
player addEventHandler ["Respawn", {
|
||||
params ["_unit", "_corpse"];
|
||||
|
||||
private _uid = getPlayerUID player;
|
||||
[SRPC(economy,onRespawn), [_unit, _corpse, _uid]] call CFUNC(serverEvent);
|
||||
}];
|
||||
|
||||
if (isNil QGVAR(ActorRepository)) then { call FUNC(initRepository); };
|
||||
|
||||
GVAR(resetMedicalSpectator) = {
|
||||
player switchMove "";
|
||||
player playMoveNow "";
|
||||
|
||||
["Terminate"] call BFUNC(EGSpectator);
|
||||
|
||||
private _spectatorDisplay = findDisplay 60492;
|
||||
if !(isNull _spectatorDisplay) then { _spectatorDisplay closeDisplay 1; };
|
||||
if !(isNull player) then {
|
||||
player switchCamera "INTERNAL";
|
||||
player enableSimulation true;
|
||||
};
|
||||
|
||||
cameraEffectEnableHUD true;
|
||||
showCinemaBorder false;
|
||||
disableUserInput false;
|
||||
};
|
||||
|
||||
[QGVAR(initActor), {
|
||||
GVAR(ActorRepository) call ["init", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(onActorRespawn), {
|
||||
params [["_loadout", [], [[]]], ["_medSpawnPos", [0,0,0], [[]]], ["_medSpawnDir", 0, [0]]];
|
||||
|
||||
private _message = ["warning", "Medical Alert", "You have been revived at a medical facility.", 5000];
|
||||
EGVAR(notifications,NotificationService) call ["create", _message];
|
||||
|
||||
player setUnitLoadout _loadout;
|
||||
player setPosATL _medSpawnPos;
|
||||
player setDir _medSpawnDir;
|
||||
player switchMove "Acts_LyingWounded_loop";
|
||||
|
||||
[] spawn {
|
||||
["Initialize", [player, [], false, true, true, true, true, true, false, false]] call BFUNC(EGSpectator);
|
||||
uiSleep 5;
|
||||
[SRPC(economy,onHealed), [player]] call CFUNC(serverEvent);
|
||||
};
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(onActorHealed), {
|
||||
call GVAR(resetMedicalSpectator);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseInitActor), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(ActorRepository) call ["sync", [_data, true]];
|
||||
cutText ["", "PLAIN", 1];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncActor), {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
|
||||
|
||||
GVAR(ActorRepository) call ["sync", [_data, _jip]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(initActor), []] call CFUNC(localEvent);
|
||||
|
||||
[{
|
||||
GETVAR(player,FORGE_isLoaded,false)
|
||||
}, {
|
||||
private _holster = GVAR(ActorRepository) call ["get", ["holster", true]];
|
||||
if (_holster) then { [player] call AFUNC(weaponselect,putWeaponAway); };
|
||||
}] call CFUNC(waitUntilAndExecute);
|
||||
25
arma/client/addons/actor/XEH_preInit.sqf
Normal file
25
arma/client/addons/actor/XEH_preInit.sqf
Normal file
@ -0,0 +1,25 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
PREP_RECOMPILE_START;
|
||||
#include "XEH_PREP.hpp"
|
||||
PREP_RECOMPILE_END;
|
||||
|
||||
private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
|
||||
|
||||
#include "initSettings.inc.sqf"
|
||||
#include "initKeybinds.inc.sqf"
|
||||
|
||||
["ace_refuel_started", {
|
||||
params ["_source", "_target", "", "_unit"];
|
||||
[SRPC(economy,FuelStart), [_source, _target, _unit]] call CFUNC(serverEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
["ace_refuel_tick", {
|
||||
params ["_source", "_target", "_amount"];
|
||||
[SRPC(economy,FuelTick), [_source, _target, _amount]] call CFUNC(serverEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
["ace_refuel_stopped", {
|
||||
params ["_source", "_target"];
|
||||
[SRPC(economy,FuelStop), [_source, _target]] call CFUNC(serverEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
1
arma/client/addons/actor/XEH_preInitClient.sqf
Normal file
1
arma/client/addons/actor/XEH_preInitClient.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "script_component.hpp"
|
||||
2
arma/client/addons/actor/XEH_preStart.sqf
Normal file
2
arma/client/addons/actor/XEH_preStart.sqf
Normal file
@ -0,0 +1,2 @@
|
||||
#include "script_component.hpp"
|
||||
#include "XEH_PREP.hpp"
|
||||
21
arma/client/addons/actor/config.cpp
Normal file
21
arma/client/addons/actor/config.cpp
Normal file
@ -0,0 +1,21 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
class CfgPatches {
|
||||
class ADDON {
|
||||
author = AUTHOR;
|
||||
authors[] = {"J.Schmidt"};
|
||||
url = ECSTRING(main,url);
|
||||
name = COMPONENT_NAME;
|
||||
requiredVersion = REQUIRED_VERSION;
|
||||
requiredAddons[] = {
|
||||
"forge_client_main"
|
||||
};
|
||||
units[] = {};
|
||||
weapons[] = {};
|
||||
VERSION_CONFIG;
|
||||
};
|
||||
};
|
||||
|
||||
#include "CfgEventHandlers.hpp"
|
||||
#include "ui\RscCommon.hpp"
|
||||
#include "ui\RscActorMenu.hpp"
|
||||
66
arma/client/addons/actor/functions/fnc_handleUIEvents.sqf
Normal file
66
arma/client/addons/actor/functions/fnc_handleUIEvents.sqf
Normal file
@ -0,0 +1,66 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_handleUIEvents.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-01-28
|
||||
* Last Update: 2026-04-06
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Handles the UI events.
|
||||
*
|
||||
* Arguments:
|
||||
* 0: [CONTROL] - The control that triggered the event
|
||||
* 1: [BOOL] - Whether the event is from a confirm dialog
|
||||
* 2: [STRING] - The message containing the event data
|
||||
*
|
||||
* Return Value:
|
||||
* UI events handled [BOOL]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_actor_fnc_handleUIEvents;
|
||||
*/
|
||||
|
||||
params ["_control", "_isConfirmDialog", "_message"];
|
||||
|
||||
private _alert = fromJSON _message;
|
||||
private _event = _alert get "event";
|
||||
private _data = _alert get "data";
|
||||
|
||||
diag_log format ["[FORGE:Client:Actor] Handling UI event: %1 with data: %2", _event, _data];
|
||||
|
||||
switch (_event) do {
|
||||
case "actor::get::actions": { GVAR(ActorRepository) call ["getNearbyActions", [_control]]; };
|
||||
case "actor::close::menu": { closeDialog 1; };
|
||||
case "actor::open::atm": { [true] spawn EFUNC(bank,openUI); };
|
||||
case "actor::open::bank": { [] spawn EFUNC(bank,openUI); };
|
||||
case "actor::open::cad": { [] spawn EFUNC(cad,openUI); };
|
||||
case "actor::open::device": { hint "Device interaction is not yet implemented."; };
|
||||
case "actor::open::garage": {
|
||||
private _garageObject = objNull;
|
||||
if (_data isEqualType createHashMap) then {
|
||||
private _netId = _data getOrDefault ["netId", ""];
|
||||
if (_netId isNotEqualTo "") then { _garageObject = objectFromNetId _netId; };
|
||||
};
|
||||
[_garageObject] spawn EFUNC(garage,openUI);
|
||||
};
|
||||
case "actor::open::vgarage": {
|
||||
private _garageObject = objNull;
|
||||
if (_data isEqualType createHashMap) then {
|
||||
private _netId = _data getOrDefault ["netId", ""];
|
||||
if (_netId isNotEqualTo "") then { _garageObject = objectFromNetId _netId; };
|
||||
};
|
||||
[_garageObject] spawn EFUNC(garage,openVG);
|
||||
};
|
||||
case "actor::open::org": { [] spawn EFUNC(org,openUI); };
|
||||
case "actor::open::vlocker": { [FORGE_Locker_Box, player, false] spawn AFUNC(arsenal,openBox) };
|
||||
case "actor::open::phone": { [] spawn EFUNC(phone,openUI); };
|
||||
case "actor::open::iplayer": { hint "Player interaction is not yet implemented." };
|
||||
case "actor::open::store": { [] spawn EFUNC(store,openUI); };
|
||||
default { hint format ["Unhandled UI event: %1", _event]; };
|
||||
};
|
||||
|
||||
if (_event isNotEqualTo "actor::get::actions") then { closeDialog 1; };
|
||||
|
||||
true;
|
||||
140
arma/client/addons/actor/functions/fnc_initRepository.sqf
Normal file
140
arma/client/addons/actor/functions/fnc_initRepository.sqf
Normal file
@ -0,0 +1,140 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initRepository.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the actor repository for managing player actor data.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Actor repository object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_actor_fnc_initRepository;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(ActorRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "ActorRepositoryBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["uid", getPlayerUID player];
|
||||
_self set ["actor", createHashMap];
|
||||
_self set ["isLoaded", false];
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["init", compileFinal {
|
||||
private _uid = _self get "uid";
|
||||
[SRPC(actor,requestInitActor), [_uid]] call CFUNC(serverEvent);
|
||||
_self set ["lastSave", time];
|
||||
|
||||
systemChat format ["Loading actor for %1", name player];
|
||||
diag_log "[FORGE:Client:Actor] Actor Repository Initialized!";
|
||||
}],
|
||||
["save", compileFinal {
|
||||
params [["_sync", false, [false]]];
|
||||
|
||||
private _uid = _self get "uid";
|
||||
[SRPC(actor,requestSaveActor), [_uid, _sync]] call CFUNC(serverEvent);
|
||||
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["sync", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
|
||||
|
||||
private _actor = _self get "actor";
|
||||
private _isLoaded = _self get "isLoaded";
|
||||
|
||||
{
|
||||
_actor set [_x, _y];
|
||||
|
||||
if (_jip) then {
|
||||
switch (_x) do {
|
||||
case "position": { _self call ["applyPosition"]; };
|
||||
case "direction": { _self call ["applyDirection"]; };
|
||||
case "stance": { _self call ["applyStance"]; };
|
||||
case "rank": { _self call ["applyRank"]; };
|
||||
case "loadout": { _self call ["applyLoadout"]; };
|
||||
default {};
|
||||
};
|
||||
};
|
||||
} forEach _data;
|
||||
|
||||
_self set ["actor", _actor];
|
||||
SETPVAR(player,FORGE_isLoaded,true);
|
||||
if !(_isLoaded) then { _self set ["isLoaded", true]; };
|
||||
diag_log "[FORGE:Client:Actor] Sync completed";
|
||||
}],
|
||||
["get", compileFinal {
|
||||
params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
|
||||
private _actor = _self get "actor";
|
||||
_actor getOrDefault [_key, _default];
|
||||
}],
|
||||
["applyPosition", compileFinal {
|
||||
private _position = _self call ["get", ["position", [0, 0, 0]]];
|
||||
if (GVAR(enableLoc)) then {
|
||||
player setPosASL _position;
|
||||
private _pAlt = ((getPosATLVisual player) select 2);
|
||||
private _pVelZ = ((velocity player) select 2);
|
||||
if (_pAlt > 5 && _pVelZ < 0) then {
|
||||
player setVelocity [0, 0, 0];
|
||||
player setPosATL [((getPosATLVisual player) select 0), ((getPosATLVisual player) select 1), 1];
|
||||
hint "You logged off mid air. You were moved to a safe position on the ground";
|
||||
};
|
||||
};
|
||||
}],
|
||||
["applyDirection", compileFinal {
|
||||
private _direction = _self call ["get", ["direction", 0]];
|
||||
if (GVAR(enableLoc)) then { player setDir _direction; };
|
||||
}],
|
||||
["applyStance", compileFinal {
|
||||
private _stance = _self call ["get", ["stance", "STAND"]];
|
||||
if (GVAR(enableLoc)) then { player playAction _stance; };
|
||||
}],
|
||||
["applyRank", compileFinal {
|
||||
private _rank = _self call ["get", ["rank", "PRIVATE"]];
|
||||
player setUnitRank _rank;
|
||||
}],
|
||||
["applyLoadout", compileFinal {
|
||||
private _loadout = _self call ["get", ["loadout", []]];
|
||||
if (GVAR(enableGear) && count _loadout > 0) then { player setUnitLoadout _loadout; };
|
||||
}],
|
||||
["getNearbyActions", compileFinal {
|
||||
params [["_control", controlNull, [controlNull]]];
|
||||
private _nearbyActions = [];
|
||||
{
|
||||
private _isAtm = _x getVariable ["isAtm", false];
|
||||
private _isBank = _x getVariable ["isBank", false];
|
||||
private _isGarage = _x getVariable ["isGarage", false];
|
||||
private _isLocker = _x getVariable ["isLocker", false];
|
||||
private _isStore = _x getVariable ["isStore", false];
|
||||
private _garageType = _x getVariable ["garageType", ""];
|
||||
private _garageContext = createHashMapFromArray [
|
||||
["netId", netId _x],
|
||||
["name", vehicleVarName _x],
|
||||
["garageType", _garageType]
|
||||
];
|
||||
private _deviceType = _x getVariable ["deviceType", ""];
|
||||
private _isPlayer = _x isKindOf "Man" && isPlayer _x;
|
||||
|
||||
if (_isStore) then { _nearbyActions pushBack ["store", true]; };
|
||||
if (_isAtm) then { _nearbyActions pushBack ["atm", true]; };
|
||||
if (_isBank) then { _nearbyActions pushBack ["bank", true]; };
|
||||
if (_isLocker && GVAR(enableVA)) then { _nearbyActions pushBack ["va", true]; };
|
||||
if (_isGarage) then { _nearbyActions pushBack ["garage", _garageContext]; };
|
||||
if (_isGarage && GVAR(enableVG)) then { _nearbyActions pushBack ["vg", _garageContext]; };
|
||||
if (_deviceType isNotEqualTo "") then { _nearbyActions pushBack ["device", _deviceType]; };
|
||||
if (_isPlayer && { _x isNotEqualTo player }) then { _nearbyActions pushBack ["player", name _x]; };
|
||||
} forEach (player nearObjects 5);
|
||||
|
||||
_control ctrlWebBrowserAction ["ExecJS", format ["updateAvailableActions(%1)", (toJSON _nearbyActions)]];
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(ActorRepository) = createHashMapObject [GVAR(ActorRepositoryBaseClass)];
|
||||
GVAR(ActorRepository)
|
||||
35
arma/client/addons/actor/functions/fnc_openUI.sqf
Normal file
35
arma/client/addons/actor/functions/fnc_openUI.sqf
Normal file
@ -0,0 +1,35 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_openUI.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-01-28
|
||||
* Last Update: 2026-01-30
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Opens the player interaction interface.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* UI opened [BOOL]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_actor_fnc_openUI;
|
||||
*/
|
||||
|
||||
private _display = createDialog ["RscActorMenu", true];
|
||||
private _ctrl = _display displayCtrl 1001;
|
||||
|
||||
_ctrl ctrlAddEventHandler ["JSDialog", {
|
||||
params ["_control", "_isConfirmDialog", "_message"];
|
||||
|
||||
[_control, _isConfirmDialog, _message] call FUNC(handleUIEvents);
|
||||
}];
|
||||
|
||||
_ctrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\index.html)];
|
||||
// _ctrl ctrlWebBrowserAction ["OpenDevConsole"];
|
||||
|
||||
true;
|
||||
8
arma/client/addons/actor/initKeybinds.inc.sqf
Normal file
8
arma/client/addons/actor/initKeybinds.inc.sqf
Normal file
@ -0,0 +1,8 @@
|
||||
#include "\forge\forge_client\addons\main\data\hpp\defineDIKCodes.hpp"
|
||||
|
||||
[
|
||||
_category, QGVAR(ForgeIMenu),
|
||||
[LSTRING(iMenu), LSTRING(iMenuTooltip)], {
|
||||
call FUNC(openUI)
|
||||
}, {}, [DIK_TAB, false, false, false] // Default keybind
|
||||
] call CBA_fnc_addKeybind;
|
||||
24
arma/client/addons/actor/initSettings.inc.sqf
Normal file
24
arma/client/addons/actor/initSettings.inc.sqf
Normal file
@ -0,0 +1,24 @@
|
||||
// Can use localize "STR_ACE_Common_Enabled" for name if ACE is required
|
||||
[
|
||||
QGVAR(enableLoc), "CHECKBOX",
|
||||
[LSTRING(enableLoc), LSTRING(enableLocTooltip)],
|
||||
_category, true, true
|
||||
] call CBA_fnc_addSetting;
|
||||
|
||||
[
|
||||
QGVAR(enableGear), "CHECKBOX",
|
||||
[LSTRING(enableGear), LSTRING(enableGearTooltip)],
|
||||
_category, true, true
|
||||
] call CBA_fnc_addSetting;
|
||||
|
||||
[
|
||||
QGVAR(enableVA), "CHECKBOX",
|
||||
[LSTRING(enableVA), LSTRING(enableVATooltip)],
|
||||
_category, true, true
|
||||
] call CBA_fnc_addSetting;
|
||||
|
||||
[
|
||||
QGVAR(enableVG), "CHECKBOX",
|
||||
[LSTRING(enableVG), LSTRING(enableVGTooltip)],
|
||||
_category, true, true
|
||||
] call CBA_fnc_addSetting;
|
||||
9
arma/client/addons/actor/script_component.hpp
Normal file
9
arma/client/addons/actor/script_component.hpp
Normal file
@ -0,0 +1,9 @@
|
||||
#define COMPONENT actor
|
||||
#define COMPONENT_BEAUTIFIED Actor
|
||||
#include "\forge\forge_client\addons\main\script_mod.hpp"
|
||||
|
||||
// #define DEBUG_MODE_FULL
|
||||
// #define DISABLE_COMPILE_CACHE
|
||||
// #define ENABLE_PERFORMANCE_COUNTERS
|
||||
|
||||
#include "\forge\forge_client\addons\main\script_macros.hpp"
|
||||
38
arma/client/addons/actor/stringtable.xml
Normal file
38
arma/client/addons/actor/stringtable.xml
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project name="FFE">
|
||||
<Package name="Actor">
|
||||
<Key ID="STR_forge_client_actor_displayName">
|
||||
<English>Actor</English>
|
||||
</Key>
|
||||
<Key ID="STR_forge_client_actor_enableGear">
|
||||
<English>Persistent Gear</English>
|
||||
</Key>
|
||||
<Key ID="STR_forge_client_actor_enableGearTooltip">
|
||||
<English>Enable Persistent Gear</English>
|
||||
</Key>
|
||||
<Key ID="STR_forge_client_actor_enableLoc">
|
||||
<English>Persistent Location</English>
|
||||
</Key>
|
||||
<Key ID="STR_forge_client_actor_enableLocTooltip">
|
||||
<English>Enable Persistent Location</English>
|
||||
</Key>
|
||||
<Key ID="STR_forge_client_actor_enableVA">
|
||||
<English>Virtual Arsenal</English>
|
||||
</Key>
|
||||
<Key ID="STR_forge_client_actor_enableVATooltip">
|
||||
<English>Enable Virtual Arsenal</English>
|
||||
</Key>
|
||||
<Key ID="STR_forge_client_actor_enableVG">
|
||||
<English>Virtual Garage</English>
|
||||
</Key>
|
||||
<Key ID="STR_forge_client_actor_enableVGTooltip">
|
||||
<English>Enable Virtual Garage</English>
|
||||
</Key>
|
||||
<Key ID="STR_forge_client_actor_iMenu">
|
||||
<English>Interaction Menu</English>
|
||||
</Key>
|
||||
<Key ID="STR_forge_client_actor_iMenuTooltip">
|
||||
<English>Open your interaction menu</English>
|
||||
</Key>
|
||||
</Package>
|
||||
</Project>
|
||||
21
arma/client/addons/actor/ui/RscActorMenu.hpp
Normal file
21
arma/client/addons/actor/ui/RscActorMenu.hpp
Normal file
@ -0,0 +1,21 @@
|
||||
class RscActorMenu {
|
||||
idd = 1000;
|
||||
fadeIn = 0;
|
||||
fadeOut = 0;
|
||||
duration = 1e011;
|
||||
onLoad = "uiNamespace setVariable ['RscActorMenu', _this select 0]";
|
||||
onUnLoad = "uinamespace setVariable ['RscActorMenu', nil]";
|
||||
|
||||
class controlsBackground {};
|
||||
class controls {
|
||||
class IFrame: RscText {
|
||||
type = 106;
|
||||
idc = 1001;
|
||||
x = "safeZoneXAbs";
|
||||
y = "safeZoneY";
|
||||
w = "safeZoneWAbs";
|
||||
h = "safeZoneH";
|
||||
colorBackground[] = {0, 0, 0, 0};
|
||||
};
|
||||
};
|
||||
};
|
||||
98
arma/client/addons/actor/ui/RscCommon.hpp
Normal file
98
arma/client/addons/actor/ui/RscCommon.hpp
Normal file
@ -0,0 +1,98 @@
|
||||
// Control types
|
||||
#define CT_STATIC 0
|
||||
#define CT_BUTTON 1
|
||||
#define CT_EDIT 2
|
||||
#define CT_SLIDER 3
|
||||
#define CT_COMBO 4
|
||||
#define CT_LISTBOX 5
|
||||
#define CT_TOOLBOX 6
|
||||
#define CT_CHECKBOXES 7
|
||||
#define CT_PROGRESS 8
|
||||
#define CT_HTML 9
|
||||
#define CT_STATIC_SKEW 10
|
||||
#define CT_ACTIVETEXT 11
|
||||
#define CT_TREE 12
|
||||
#define CT_STRUCTURED_TEXT 13
|
||||
#define CT_CONTEXT_MENU 14
|
||||
#define CT_CONTROLS_GROUP 15
|
||||
#define CT_SHORTCUTBUTTON 16
|
||||
#define CT_HITZONES 17
|
||||
#define CT_XKEYDESC 40
|
||||
#define CT_XBUTTON 41
|
||||
#define CT_XLISTBOX 42
|
||||
#define CT_XSLIDER 43
|
||||
#define CT_XCOMBO 44
|
||||
#define CT_ANIMATED_TEXTURE 45
|
||||
#define CT_OBJECT 80
|
||||
#define CT_OBJECT_ZOOM 81
|
||||
#define CT_OBJECT_CONTAINER 82
|
||||
#define CT_OBJECT_CONT_ANIM 83
|
||||
#define CT_LINEBREAK 98
|
||||
#define CT_USER 99
|
||||
#define CT_MAP 100
|
||||
#define CT_MAP_MAIN 101
|
||||
#define CT_LISTNBOX 102
|
||||
#define CT_ITEMSLOT 103
|
||||
#define CT_CHECKBOX 77
|
||||
|
||||
// Static styles
|
||||
#define ST_POS 0x0F
|
||||
#define ST_HPOS 0x03
|
||||
#define ST_VPOS 0x0C
|
||||
#define ST_LEFT 0x00
|
||||
#define ST_RIGHT 0x01
|
||||
#define ST_CENTER 0x02
|
||||
#define ST_DOWN 0x04
|
||||
#define ST_UP 0x08
|
||||
#define ST_VCENTER 0x0C
|
||||
|
||||
#define ST_TYPE 0xF0
|
||||
#define ST_SINGLE 0x00
|
||||
#define ST_MULTI 0x10
|
||||
#define ST_TITLE_BAR 0x20
|
||||
#define ST_PICTURE 0x30
|
||||
#define ST_FRAME 0x40
|
||||
#define ST_BACKGROUND 0x50
|
||||
#define ST_GROUP_BOX 0x60
|
||||
#define ST_GROUP_BOX2 0x70
|
||||
#define ST_HUD_BACKGROUND 0x80
|
||||
#define ST_TILE_PICTURE 0x90
|
||||
#define ST_WITH_RECT 0xA0
|
||||
#define ST_LINE 0xB0
|
||||
#define ST_UPPERCASE 0xC0
|
||||
#define ST_LOWERCASE 0xD0
|
||||
|
||||
#define ST_SHADOW 0x100
|
||||
#define ST_NO_RECT 0x200
|
||||
#define ST_KEEP_ASPECT_RATIO 0x800
|
||||
|
||||
// Slider styles
|
||||
#define SL_DIR 0x400
|
||||
#define SL_VERT 0
|
||||
#define SL_HORZ 0x400
|
||||
|
||||
#define SL_TEXTURES 0x10
|
||||
|
||||
// progress bar
|
||||
#define ST_VERTICAL 0x01
|
||||
#define ST_HORIZONTAL 0
|
||||
|
||||
// Listbox styles
|
||||
#define LB_TEXTURES 0x10
|
||||
#define LB_MULTI 0x20
|
||||
|
||||
// Tree styles
|
||||
#define TR_SHOWROOT 1
|
||||
#define TR_AUTOCOLLAPSE 2
|
||||
|
||||
// Default text sizes
|
||||
#define GUI_TEXT_SIZE_SMALL (GUI_GRID_H * 0.8)
|
||||
#define GUI_TEXT_SIZE_MEDIUM (GUI_GRID_H * 1)
|
||||
#define GUI_TEXT_SIZE_LARGE (GUI_GRID_H * 1.2)
|
||||
|
||||
// Pixel grid
|
||||
#define pixelScale 0.50
|
||||
#define GRID_W (pixelW * pixelGrid * pixelScale)
|
||||
#define GRID_H (pixelH * pixelGrid * pixelScale)
|
||||
|
||||
class RscText;
|
||||
37
arma/client/addons/actor/ui/_site/index.html
Normal file
37
arma/client/addons/actor/ui/_site/index.html
Normal file
@ -0,0 +1,37 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Interaction Menu</title>
|
||||
<!-- <link rel="stylesheet" href="style.css"> -->
|
||||
<!--
|
||||
Dynamic Resource Loading
|
||||
The following script loads CSS and JavaScript files dynamically using the A3API
|
||||
This approach is used instead of static HTML imports to work with Arma 3's file system
|
||||
-->
|
||||
<script>
|
||||
Promise.all([
|
||||
A3API.RequestFile(
|
||||
"forge\\forge_client\\addons\\actor\\ui\\_site\\style.css",
|
||||
),
|
||||
A3API.RequestFile(
|
||||
"forge\\forge_client\\addons\\actor\\ui\\_site\\script.js",
|
||||
),
|
||||
]).then(([css, js]) => {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.text = js;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- <script src="script.js"></script> -->
|
||||
</body>
|
||||
</html>
|
||||
532
arma/client/addons/actor/ui/_site/script.js
Normal file
532
arma/client/addons/actor/ui/_site/script.js
Normal file
@ -0,0 +1,532 @@
|
||||
/**
|
||||
* Interaction Menu - Modern UI Implementation
|
||||
* Uses vanilla JS with React-like patterns and Redux-like state management
|
||||
*/
|
||||
|
||||
//=============================================================================
|
||||
// #region LIBRARY - DOM Helper & State Management
|
||||
//=============================================================================
|
||||
|
||||
// Helper to create DOM elements (React-like createElement)
|
||||
function h(tag, props = {}, ...children) {
|
||||
const el = document.createElement(tag);
|
||||
|
||||
if (props) {
|
||||
Object.entries(props).forEach(([key, value]) => {
|
||||
if (key.startsWith("on") && typeof value === "function") {
|
||||
el.addEventListener(key.substring(2).toLowerCase(), value);
|
||||
} else if (key === "className") {
|
||||
el.className = value;
|
||||
} else if (key === "style" && typeof value === "object") {
|
||||
Object.assign(el.style, value);
|
||||
} else {
|
||||
el.setAttribute(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
children.forEach((child) => {
|
||||
if (typeof child === "string" || typeof child === "number") {
|
||||
el.appendChild(document.createTextNode(child));
|
||||
} else if (child instanceof Node) {
|
||||
el.appendChild(child);
|
||||
} else if (Array.isArray(child)) {
|
||||
child.forEach((c) => {
|
||||
if (c instanceof Node) el.appendChild(c);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
// Simple Rendering Logic
|
||||
let _rootContainer = null;
|
||||
let _rootComponent = null;
|
||||
|
||||
function render(component, container) {
|
||||
_rootContainer = container;
|
||||
_rootComponent = component;
|
||||
_render();
|
||||
}
|
||||
|
||||
function _render() {
|
||||
if (_rootContainer && _rootComponent) {
|
||||
_rootContainer.innerHTML = "";
|
||||
_rootContainer.appendChild(_rootComponent());
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// #region ACTIONS
|
||||
//=============================================================================
|
||||
|
||||
const ActionTypes = {
|
||||
SET_AVAILABLE_ACTIONS: "SET_AVAILABLE_ACTIONS",
|
||||
SET_MENU_ITEMS: "SET_MENU_ITEMS",
|
||||
ADD_ACTION: "ADD_ACTION",
|
||||
REMOVE_ACTION: "REMOVE_ACTION",
|
||||
CLEAR_ACTIONS: "CLEAR_ACTIONS",
|
||||
};
|
||||
|
||||
const actions = {
|
||||
setAvailableActions: (actionTypes) => ({
|
||||
type: ActionTypes.SET_AVAILABLE_ACTIONS,
|
||||
payload: actionTypes,
|
||||
}),
|
||||
|
||||
setMenuItems: (menuItems) => ({
|
||||
type: ActionTypes.SET_MENU_ITEMS,
|
||||
payload: menuItems,
|
||||
}),
|
||||
|
||||
addAction: (actionType) => ({
|
||||
type: ActionTypes.ADD_ACTION,
|
||||
payload: actionType,
|
||||
}),
|
||||
|
||||
removeAction: (actionType) => ({
|
||||
type: ActionTypes.REMOVE_ACTION,
|
||||
payload: actionType,
|
||||
}),
|
||||
|
||||
clearActions: () => ({
|
||||
type: ActionTypes.CLEAR_ACTIONS,
|
||||
}),
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// #region REDUCER
|
||||
//=============================================================================
|
||||
|
||||
const baseMenuItems = [
|
||||
{
|
||||
id: "cad",
|
||||
title: "CAD",
|
||||
description: "Access CAD (Computer Aided Dispatch)",
|
||||
action: "actor::open::cad",
|
||||
},
|
||||
{
|
||||
id: "phone",
|
||||
title: "Phone",
|
||||
description: "Access and manage your personal phone",
|
||||
action: "actor::open::phone",
|
||||
},
|
||||
{
|
||||
id: "org",
|
||||
title: "Organization",
|
||||
description: "View and manage your organization data",
|
||||
action: "actor::open::org",
|
||||
},
|
||||
];
|
||||
|
||||
const actionDefinitions = {
|
||||
atm: {
|
||||
id: "atm",
|
||||
title: "ATM",
|
||||
description: "Access the ATM",
|
||||
action: "actor::open::atm",
|
||||
},
|
||||
bank: {
|
||||
id: "bank",
|
||||
title: "Bank",
|
||||
description: "Access your bank account and manage finances",
|
||||
action: "actor::open::bank",
|
||||
},
|
||||
cad: {
|
||||
id: "cad",
|
||||
title: "CAD",
|
||||
description: "Access the CAD",
|
||||
action: "actor::open::cad",
|
||||
},
|
||||
phone: {
|
||||
id: "phone",
|
||||
title: "Phone",
|
||||
description: "Access and manage your personal phone",
|
||||
action: "actor::open::phone",
|
||||
},
|
||||
org: {
|
||||
id: "org",
|
||||
title: "Organization",
|
||||
description: "View and manage your organization data",
|
||||
action: "actor::open::org",
|
||||
},
|
||||
store: {
|
||||
id: "store",
|
||||
title: "Store",
|
||||
description: "Browse and purchase items from the store",
|
||||
action: "actor::open::store",
|
||||
},
|
||||
device: {
|
||||
id: "device",
|
||||
title: "Device",
|
||||
description: "Manage devices and settings",
|
||||
action: "actor::open::device",
|
||||
},
|
||||
garage: {
|
||||
id: "garage",
|
||||
title: "Garage",
|
||||
description: "Access and manage your vehicle collection",
|
||||
action: "actor::open::garage",
|
||||
},
|
||||
player: {
|
||||
id: "player",
|
||||
title: "Player",
|
||||
description: "Interact with player-specific actions",
|
||||
action: "actor::open::iplayer",
|
||||
},
|
||||
store: {
|
||||
id: "store",
|
||||
title: "Store",
|
||||
description: "Browse and purchase items from the store",
|
||||
action: "actor::open::store",
|
||||
},
|
||||
va: {
|
||||
id: "va",
|
||||
title: "Arsenal",
|
||||
description: "Access your virtual arsenal",
|
||||
action: "actor::open::vlocker",
|
||||
},
|
||||
vg: {
|
||||
id: "vg",
|
||||
title: "V. Garage",
|
||||
description: "Access your virtual garage",
|
||||
action: "actor::open::vgarage",
|
||||
},
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
availableActions: [],
|
||||
menuItems: [...baseMenuItems],
|
||||
baseMenuItems: [...baseMenuItems],
|
||||
actionDefinitions: { ...actionDefinitions },
|
||||
};
|
||||
|
||||
function actorReducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_AVAILABLE_ACTIONS:
|
||||
const newMenuItems = [...state.baseMenuItems];
|
||||
|
||||
const actionArray = Array.isArray(action.payload)
|
||||
? action.payload
|
||||
: [];
|
||||
actionArray.forEach((actionItem) => {
|
||||
if (Array.isArray(actionItem) && actionItem.length === 2) {
|
||||
const [type, value] = actionItem;
|
||||
const definition = state.actionDefinitions[type];
|
||||
if (definition) {
|
||||
const context =
|
||||
value && typeof value === "object"
|
||||
? value
|
||||
: { value };
|
||||
const garageLabel =
|
||||
context.name || context.garageType || "";
|
||||
const title =
|
||||
["garage", "vg"].includes(type) && garageLabel
|
||||
? `${definition.title}: ${garageLabel}`
|
||||
: definition.title;
|
||||
newMenuItems.push({
|
||||
...definition,
|
||||
title,
|
||||
context,
|
||||
});
|
||||
} else {
|
||||
console.warn(
|
||||
`No definition found for: ${type} - ${value}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn("Invalid action format:", actionItem);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
availableActions: action.payload,
|
||||
menuItems: newMenuItems,
|
||||
};
|
||||
|
||||
case ActionTypes.SET_MENU_ITEMS:
|
||||
return {
|
||||
...state,
|
||||
menuItems: action.payload,
|
||||
};
|
||||
|
||||
case ActionTypes.ADD_ACTION:
|
||||
const definition = state.actionDefinitions[action.payload];
|
||||
if (
|
||||
definition &&
|
||||
!state.menuItems.find((item) => item.id === definition.id)
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
menuItems: [...state.menuItems, definition],
|
||||
};
|
||||
}
|
||||
return state;
|
||||
|
||||
case ActionTypes.REMOVE_ACTION:
|
||||
return {
|
||||
...state,
|
||||
menuItems: state.menuItems.filter(
|
||||
(item) => item.id !== action.payload,
|
||||
),
|
||||
};
|
||||
|
||||
case ActionTypes.CLEAR_ACTIONS:
|
||||
return {
|
||||
...state,
|
||||
availableActions: [],
|
||||
menuItems: [...state.baseMenuItems],
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// #region STORE
|
||||
//=============================================================================
|
||||
|
||||
class Store {
|
||||
constructor(reducer, initialState) {
|
||||
this.reducer = reducer;
|
||||
this.state = initialState;
|
||||
this.listeners = [];
|
||||
}
|
||||
|
||||
getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
dispatch(action) {
|
||||
console.log("Dispatching action:", action);
|
||||
this.state = this.reducer(this.state, action);
|
||||
this.listeners.forEach((listener) => listener(this.state));
|
||||
_render(); // Re-render on state change
|
||||
}
|
||||
|
||||
subscribe(listener) {
|
||||
this.listeners.push(listener);
|
||||
return () => {
|
||||
this.listeners = this.listeners.filter((l) => l !== listener);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const store = new Store(actorReducer, initialState);
|
||||
|
||||
//=============================================================================
|
||||
// #region SELECTORS
|
||||
//=============================================================================
|
||||
|
||||
const selectors = {
|
||||
getMenuItems: (state) => state.menuItems,
|
||||
getAvailableActions: (state) => state.availableActions,
|
||||
getBaseMenuItems: (state) => state.baseMenuItems,
|
||||
getActionDefinitions: (state) => state.actionDefinitions,
|
||||
getMenuItemById: (state, id) =>
|
||||
state.menuItems.find((item) => item.id === id),
|
||||
getMenuItemsCount: (state) => state.menuItems.length,
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// #region UI COMPONENTS
|
||||
//=============================================================================
|
||||
|
||||
// Tooltip state
|
||||
let tooltipEl = null;
|
||||
|
||||
function createTooltip() {
|
||||
if (!tooltipEl) {
|
||||
tooltipEl = h(
|
||||
"div",
|
||||
{ className: "radial-tooltip" },
|
||||
h("div", { className: "tooltip-title" }),
|
||||
h("div", { className: "tooltip-description" }),
|
||||
);
|
||||
document.body.appendChild(tooltipEl);
|
||||
}
|
||||
return tooltipEl;
|
||||
}
|
||||
|
||||
function showTooltip(item, x, y) {
|
||||
const tooltip = createTooltip();
|
||||
tooltip.querySelector(".tooltip-title").textContent = item.title;
|
||||
tooltip.querySelector(".tooltip-description").textContent =
|
||||
item.description;
|
||||
tooltip.style.left = `${x + 15}px`;
|
||||
tooltip.style.top = `${y + 10}px`;
|
||||
tooltip.classList.add("visible");
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
if (tooltipEl) {
|
||||
tooltipEl.classList.remove("visible");
|
||||
}
|
||||
}
|
||||
|
||||
function RadialItem({ item, index, total, onClick }) {
|
||||
const menuRadius = 160;
|
||||
const itemSize = 80;
|
||||
|
||||
// Calculate position in circle
|
||||
const angleStep = (2 * Math.PI) / total;
|
||||
const angle = angleStep * index - Math.PI / 2; // Start from top
|
||||
|
||||
const centerX = menuRadius + itemSize / 2;
|
||||
const centerY = menuRadius + itemSize / 2;
|
||||
|
||||
const x = centerX + menuRadius * Math.cos(angle) - itemSize / 2;
|
||||
const y = centerY + menuRadius * Math.sin(angle) - itemSize / 2;
|
||||
|
||||
const el = h(
|
||||
"div",
|
||||
{
|
||||
className: "radial-item",
|
||||
style: {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
},
|
||||
onClick: () => onClick(item),
|
||||
},
|
||||
h("div", { className: "radial-item-title" }, item.title),
|
||||
);
|
||||
|
||||
// Add tooltip events
|
||||
el.addEventListener("mouseenter", (e) =>
|
||||
showTooltip(item, e.clientX, e.clientY),
|
||||
);
|
||||
el.addEventListener("mousemove", (e) => {
|
||||
if (tooltipEl && tooltipEl.classList.contains("visible")) {
|
||||
tooltipEl.style.left = `${e.clientX + 15}px`;
|
||||
tooltipEl.style.top = `${e.clientY + 10}px`;
|
||||
}
|
||||
});
|
||||
el.addEventListener("mouseleave", hideTooltip);
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
function RadialCenter({ onClose }) {
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
className: "radial-center",
|
||||
onClick: onClose,
|
||||
},
|
||||
h("div", { className: "center-label" }, "Close"),
|
||||
);
|
||||
}
|
||||
|
||||
function RadialMenu() {
|
||||
const state = store.getState();
|
||||
const menuItems = selectors.getMenuItems(state);
|
||||
|
||||
const handleItemClick = (item) => {
|
||||
console.log("Menu item clicked:", item);
|
||||
const alert = {
|
||||
event: item.action,
|
||||
data: item.context || {},
|
||||
};
|
||||
if (typeof A3API !== "undefined") {
|
||||
A3API.SendAlert(JSON.stringify(alert));
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
console.log("Close menu requested");
|
||||
const alert = {
|
||||
event: "actor::close::menu",
|
||||
data: {},
|
||||
};
|
||||
if (typeof A3API !== "undefined") {
|
||||
A3API.SendAlert(JSON.stringify(alert));
|
||||
}
|
||||
};
|
||||
|
||||
if (menuItems.length === 0) {
|
||||
return h(
|
||||
"div",
|
||||
{ className: "empty-state" },
|
||||
h("p", null, "No actions available"),
|
||||
);
|
||||
}
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "radial-menu" },
|
||||
RadialCenter({ onClose: handleClose }),
|
||||
menuItems.map((item, index) =>
|
||||
RadialItem({
|
||||
item,
|
||||
index,
|
||||
total: menuItems.length,
|
||||
onClick: handleItemClick,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return RadialMenu();
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// #region DATA HANDLERS (A3API Integration)
|
||||
//=============================================================================
|
||||
|
||||
function updateAvailableActions(actionTypes) {
|
||||
console.log("Updating available actions:", actionTypes);
|
||||
store.dispatch(actions.setAvailableActions(actionTypes));
|
||||
}
|
||||
|
||||
function handleGetActionsResponse(data) {
|
||||
console.log("Received actions data:", data);
|
||||
store.dispatch(actions.setAvailableActions(data));
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// #region INITIALIZATION
|
||||
//=============================================================================
|
||||
|
||||
let initialized = false;
|
||||
|
||||
function initializeMenu() {
|
||||
console.log("initializeMenu() called");
|
||||
|
||||
if (initialized) {
|
||||
console.log("Menu already initialized, skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
const root = document.getElementById("app");
|
||||
if (root) {
|
||||
render(App, root);
|
||||
initialized = true;
|
||||
console.log("Interaction menu initialized successfully");
|
||||
|
||||
// Request initial data from A3API
|
||||
if (typeof A3API !== "undefined") {
|
||||
const alert = {
|
||||
event: "actor::get::actions",
|
||||
data: {},
|
||||
};
|
||||
A3API.SendAlert(JSON.stringify(alert));
|
||||
}
|
||||
} else {
|
||||
console.error("Root element #app not found");
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize based on DOM state
|
||||
if (document.readyState !== "loading") {
|
||||
console.log("Script loaded after DOM ready, auto-initializing...");
|
||||
initializeMenu();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
console.log("DOM loaded, initializing menu...");
|
||||
initializeMenu();
|
||||
});
|
||||
}
|
||||
190
arma/client/addons/actor/ui/_site/style.css
Normal file
190
arma/client/addons/actor/ui/_site/style.css
Normal file
@ -0,0 +1,190 @@
|
||||
:root {
|
||||
--bg-app: rgba(0, 0, 0, 0.4);
|
||||
--bg-surface: #ffffff;
|
||||
--bg-surface-hover: #f1f5f9;
|
||||
--primary: #475569;
|
||||
--primary-hover: #1e293b;
|
||||
--text-main: #1f2937;
|
||||
--text-muted: #64748b;
|
||||
--text-inverse: #f8fafc;
|
||||
--border: #e2e8f0;
|
||||
--radius: 8px;
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg:
|
||||
0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--menu-radius: 160px;
|
||||
--item-size: 80px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
"Inter",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background: var(--bg-app);
|
||||
color: var(--text-main);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Radial Menu Container */
|
||||
.radial-menu {
|
||||
position: relative;
|
||||
width: calc(var(--menu-radius) * 2 + var(--item-size));
|
||||
height: calc(var(--menu-radius) * 2 + var(--item-size));
|
||||
}
|
||||
|
||||
/* Center Hub */
|
||||
.radial-center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
background: var(--bg-surface);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 10;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
border-color: var(--primary);
|
||||
transform: translate(-50%, -50%) scale(1.05);
|
||||
}
|
||||
|
||||
.center-icon {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
.center-label {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Menu Items */
|
||||
.radial-item {
|
||||
position: absolute;
|
||||
width: var(--item-size);
|
||||
height: var(--item-size);
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: var(--shadow);
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
border-color: var(--primary);
|
||||
transform: scale(1.15);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 5;
|
||||
|
||||
.radial-item-title {
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.radial-item-icon {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.radial-item-title {
|
||||
font-size: 0.6rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
line-height: 1.2;
|
||||
transition: color 0.2s ease;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
line-clamp: 2;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
/* Tooltip */
|
||||
.radial-tooltip {
|
||||
position: fixed;
|
||||
background: var(--primary-hover);
|
||||
color: var(--text-inverse);
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
z-index: 100;
|
||||
box-shadow: var(--shadow-lg);
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tooltip-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tooltip-description {
|
||||
font-size: 0.65rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-surface);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
p {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
1
arma/client/addons/bank/$PBOPREFIX$
Normal file
1
arma/client/addons/bank/$PBOPREFIX$
Normal file
@ -0,0 +1 @@
|
||||
forge\forge_client\addons\bank
|
||||
19
arma/client/addons/bank/CfgEventHandlers.hpp
Normal file
19
arma/client/addons/bank/CfgEventHandlers.hpp
Normal file
@ -0,0 +1,19 @@
|
||||
class Extended_PreStart_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preStart));
|
||||
};
|
||||
};
|
||||
|
||||
class Extended_PreInit_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
|
||||
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_preInitClient));
|
||||
};
|
||||
};
|
||||
|
||||
class Extended_PostInit_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_postInit));
|
||||
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_postInitClient));
|
||||
};
|
||||
};
|
||||
35
arma/client/addons/bank/README.md
Normal file
35
arma/client/addons/bank/README.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Forge Client Bank
|
||||
|
||||
## Overview
|
||||
The bank addon provides the client banking UI and browser bridge for account
|
||||
hydrate, deposits, withdrawals, transfers, PIN entry, earnings deposits, and
|
||||
credit-line repayment. It also exposes PIN changes from the full bank UI.
|
||||
|
||||
## Dependencies
|
||||
- `forge_client_common`
|
||||
- `forge_client_main`
|
||||
- server bank events from `forge_server_bank`
|
||||
- notifications for server-driven messages
|
||||
|
||||
## Main Components
|
||||
- `fnc_initRepository.sqf` tracks account load state.
|
||||
- `fnc_initUIBridge.sqf` translates browser requests into server RPCs and sends
|
||||
server responses back to the browser.
|
||||
- `fnc_handleUIEvents.sqf` handles `bank::*` browser events.
|
||||
- `fnc_openUI.sqf` opens `RscBank`; ATM mode is supported by passing `true`.
|
||||
|
||||
## Browser Events
|
||||
- `bank::ready`
|
||||
- `bank::refresh`
|
||||
- `bank::deposit::request`
|
||||
- `bank::withdraw::request`
|
||||
- `bank::transfer::request`
|
||||
- `bank::depositEarnings::request`
|
||||
- `bank::repayCreditLine::request`
|
||||
- `bank::pin::request`
|
||||
- `bank::pin::change::request`
|
||||
- `bank::close`
|
||||
|
||||
## Runtime Notes
|
||||
The client only displays and requests account changes. The server bank addon and
|
||||
extension own validation, balances, authorization, and persistence.
|
||||
4
arma/client/addons/bank/XEH_PREP.hpp
Normal file
4
arma/client/addons/bank/XEH_PREP.hpp
Normal file
@ -0,0 +1,4 @@
|
||||
PREP(handleUIEvents);
|
||||
PREP(initRepository);
|
||||
PREP(initUIBridge);
|
||||
PREP(openUI);
|
||||
1
arma/client/addons/bank/XEH_postInit.sqf
Normal file
1
arma/client/addons/bank/XEH_postInit.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "script_component.hpp"
|
||||
72
arma/client/addons/bank/XEH_postInitClient.sqf
Normal file
72
arma/client/addons/bank/XEH_postInitClient.sqf
Normal file
@ -0,0 +1,72 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
if (isNil QGVAR(BankRepository)) then { call FUNC(initRepository); };
|
||||
if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
|
||||
|
||||
GVAR(sendPhoneBankEvent) = {
|
||||
params [["_functionName", "", [""]], ["_arguments", [], [[]]]];
|
||||
|
||||
private _display = uiNamespace getVariable ["RscPhone", displayNull];
|
||||
if (isNull _display || { _functionName isEqualTo "" }) exitWith { false };
|
||||
|
||||
private _control = _display displayCtrl 1001;
|
||||
if (isNull _control) exitWith { false };
|
||||
|
||||
private _serializedArguments = _arguments apply { toJSON _x };
|
||||
private _script = format [
|
||||
"window.%1 && window.%1(%2)",
|
||||
_functionName,
|
||||
_serializedArguments joinString ", "
|
||||
];
|
||||
|
||||
_control ctrlWebBrowserAction ["ExecJS", _script];
|
||||
true
|
||||
};
|
||||
|
||||
[QGVAR(initBank), {
|
||||
GVAR(BankRepository) call ["init", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseInitBank), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(BankRepository) call ["markLoaded", []];
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
|
||||
};
|
||||
["updateMobileBankAccount", [_data]] call GVAR(sendPhoneBankEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncBank), {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
|
||||
|
||||
GVAR(BankRepository) call ["markLoaded", []];
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
|
||||
};
|
||||
["updateMobileBankAccount", [_data]] call GVAR(sendPhoneBankEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseHydrateBank), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleHydrateResponse", [_data, "bank::hydrate"]];
|
||||
};
|
||||
["updateMobileBank", [_data]] call GVAR(sendPhoneBankEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseBankNotice), {
|
||||
params [["_type", "error", [""]], ["_message", "", [""]]];
|
||||
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleNoticeResponse", [_type, _message]];
|
||||
};
|
||||
["showMobileBankNotice", [_type, _message]] call GVAR(sendPhoneBankEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[{
|
||||
EGVAR(actor,ActorRepository) get "isLoaded";
|
||||
}, {
|
||||
[QGVAR(initBank), []] call CFUNC(localEvent);
|
||||
}] call CFUNC(waitUntilAndExecute);
|
||||
10
arma/client/addons/bank/XEH_preInit.sqf
Normal file
10
arma/client/addons/bank/XEH_preInit.sqf
Normal file
@ -0,0 +1,10 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
PREP_RECOMPILE_START;
|
||||
#include "XEH_PREP.hpp"
|
||||
PREP_RECOMPILE_END;
|
||||
|
||||
// private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
|
||||
|
||||
#include "initSettings.inc.sqf"
|
||||
#include "initKeybinds.inc.sqf"
|
||||
1
arma/client/addons/bank/XEH_preInitClient.sqf
Normal file
1
arma/client/addons/bank/XEH_preInitClient.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "script_component.hpp"
|
||||
2
arma/client/addons/bank/XEH_preStart.sqf
Normal file
2
arma/client/addons/bank/XEH_preStart.sqf
Normal file
@ -0,0 +1,2 @@
|
||||
#include "script_component.hpp"
|
||||
#include "XEH_PREP.hpp"
|
||||
22
arma/client/addons/bank/config.cpp
Normal file
22
arma/client/addons/bank/config.cpp
Normal file
@ -0,0 +1,22 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
class CfgPatches {
|
||||
class ADDON {
|
||||
author = AUTHOR;
|
||||
authors[] = {"IDSolutions"};
|
||||
url = ECSTRING(main,url);
|
||||
name = COMPONENT_NAME;
|
||||
requiredVersion = REQUIRED_VERSION;
|
||||
requiredAddons[] = {
|
||||
"forge_client_common",
|
||||
"forge_client_main"
|
||||
};
|
||||
units[] = {};
|
||||
weapons[] = {};
|
||||
VERSION_CONFIG;
|
||||
};
|
||||
};
|
||||
|
||||
#include "CfgEventHandlers.hpp"
|
||||
#include "ui\RscCommon.hpp"
|
||||
#include "ui\RscBank.hpp"
|
||||
91
arma/client/addons/bank/functions/fnc_handleUIEvents.sqf
Normal file
91
arma/client/addons/bank/functions/fnc_handleUIEvents.sqf
Normal file
@ -0,0 +1,91 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_handleUIEvents.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2025-12-16
|
||||
* Last Update: 2026-02-17
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Handles the UI events.
|
||||
*
|
||||
* Arguments:
|
||||
* 0: [CONTROL] - The control that triggered the event
|
||||
* 1: [BOOL] - Whether the event is from a confirm dialog
|
||||
* 2: [STRING] - The message containing the event data
|
||||
*
|
||||
* Return Value:
|
||||
* UI events handled [BOOL]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_bank_fnc_handleUIEvents;
|
||||
*/
|
||||
|
||||
params ["_control", "_isConfirmDialog", "_message"];
|
||||
|
||||
private _alert = fromJSON _message;
|
||||
private _event = _alert get "event";
|
||||
private _data = _alert get "data";
|
||||
|
||||
diag_log format ["[FORGE:Client:Bank] Handling UI event: %1 with data: %2", _event, _data];
|
||||
|
||||
switch (_event) do {
|
||||
case "bank::close": {
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleClose", []];
|
||||
};
|
||||
|
||||
closeDialog 1;
|
||||
};
|
||||
case "bank::ready": {
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleReady", [_control, _data]];
|
||||
};
|
||||
};
|
||||
case "bank::refresh": {
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["refreshSession", []];
|
||||
};
|
||||
};
|
||||
case "bank::deposit::request": {
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleDepositRequest", [_data]];
|
||||
};
|
||||
};
|
||||
case "bank::withdraw::request": {
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleWithdrawRequest", [_data]];
|
||||
};
|
||||
};
|
||||
case "bank::transfer::request": {
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleTransferRequest", [_data]];
|
||||
};
|
||||
};
|
||||
case "bank::depositEarnings::request": {
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleDepositEarningsRequest", [_data]];
|
||||
};
|
||||
};
|
||||
case "bank::repayCreditLine::request": {
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleRepayCreditLineRequest", [_data]];
|
||||
};
|
||||
};
|
||||
case "bank::pin::request": {
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleSubmitPinRequest", [_data]];
|
||||
};
|
||||
};
|
||||
case "bank::pin::change::request": {
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleChangePinRequest", [_data]];
|
||||
};
|
||||
};
|
||||
default {
|
||||
hint format ["Unhandled bank UI event: %1", _event];
|
||||
};
|
||||
};
|
||||
|
||||
true;
|
||||
44
arma/client/addons/bank/functions/fnc_initRepository.sqf
Normal file
44
arma/client/addons/bank/functions/fnc_initRepository.sqf
Normal file
@ -0,0 +1,44 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initRepository.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the bank repository for client bank lifecycle state.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Bank repository object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_bank_fnc_initRepository;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(BankRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "BankRepositoryBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["uid", getPlayerUID player];
|
||||
_self set ["isLoaded", false];
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["init", compileFinal {
|
||||
[SRPC(bank,requestInitBank), [getPlayerUID player]] call CFUNC(serverEvent);
|
||||
_self set ["lastSave", time];
|
||||
|
||||
systemChat format ["Bank loaded for %1", name player];
|
||||
diag_log "[FORGE:Client:Bank] Bank Repository Initialized!";
|
||||
}],
|
||||
["markLoaded", compileFinal {
|
||||
if !(_self getOrDefault ["isLoaded", false]) then { _self set ["isLoaded", true]; };
|
||||
true
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(BankRepository) = createHashMapObject [GVAR(BankRepositoryBaseClass)];
|
||||
GVAR(BankRepository)
|
||||
172
arma/client/addons/bank/functions/fnc_initUIBridge.sqf
Normal file
172
arma/client/addons/bank/functions/fnc_initUIBridge.sqf
Normal file
@ -0,0 +1,172 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initUIBridge.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the bank UI bridge for browser control state and bank UI events.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Bank UI bridge object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_bank_fnc_initUIBridge;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
private _webUIDeclarations = call EFUNC(common,initWebUIBridge);
|
||||
private _webUIBridgeDeclaration = _webUIDeclarations get "bridgeDeclaration";
|
||||
|
||||
GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#base", _webUIBridgeDeclaration],
|
||||
["#type", "BankUIBridgeBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["mode", "bank"];
|
||||
}],
|
||||
["getActiveBrowserControl", compileFinal {
|
||||
private _display = uiNamespace getVariable ["RscBank", displayNull];
|
||||
if (isNull _display) exitWith {
|
||||
_self call ["setActiveBrowserControl", [controlNull]];
|
||||
controlNull
|
||||
};
|
||||
|
||||
private _control = _display displayCtrl 1002;
|
||||
_self call ["setActiveBrowserControl", [_control]];
|
||||
_control
|
||||
}],
|
||||
["getMode", compileFinal {
|
||||
_self getOrDefault ["mode", "bank"]
|
||||
}],
|
||||
["hasOpenScreen", compileFinal {
|
||||
private _screen = _self call ["getScreen", []];
|
||||
private _control = _self call ["getActiveBrowserControl", []];
|
||||
|
||||
!(isNull _control) && { _screen call ["isReady", []] }
|
||||
}],
|
||||
["handleDepositEarningsRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _amount = floor (_data getOrDefault ["amount", 0]);
|
||||
[SRPC(bank,requestDepositEarnings), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["handleDepositRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _amount = floor (_data getOrDefault ["amount", 0]);
|
||||
[SRPC(bank,requestDeposit), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["handleChangePinRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _currentPin = _data getOrDefault ["currentPin", ""];
|
||||
private _newPin = _data getOrDefault ["newPin", ""];
|
||||
if !(_currentPin isEqualType "") then { _currentPin = str _currentPin; };
|
||||
if !(_newPin isEqualType "") then { _newPin = str _newPin; };
|
||||
|
||||
[SRPC(bank,requestChangePin), [getPlayerUID player, _currentPin, _newPin]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["handleRepayCreditLineRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _amount = floor (_data getOrDefault ["amount", 0]);
|
||||
[SRPC(bank,requestRepayCreditLine), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["handleHydrateResponse", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_event", "bank::hydrate", [""]]];
|
||||
|
||||
if !(_self call ["hasOpenScreen", []]) exitWith { false };
|
||||
|
||||
_self call ["sendEvent", [_event, _data, _self call ["getActiveBrowserControl", []]]]
|
||||
}],
|
||||
["handleAccountSyncResponse", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
if !(_self call ["hasOpenScreen", []]) exitWith { false };
|
||||
|
||||
_self call ["sendEvent", ["bank::sync", _data, _self call ["getActiveBrowserControl", []]]]
|
||||
}],
|
||||
["handleNoticeResponse", compileFinal {
|
||||
params [["_type", "error", [""]], ["_message", "", [""]]];
|
||||
|
||||
_self call ["sendNotice", [_type, _message]]
|
||||
}],
|
||||
["handleReady", compileFinal {
|
||||
params [["_control", controlNull, [controlNull]], ["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _screen = _self call ["getScreen", []];
|
||||
_screen call ["setControl", [_control]];
|
||||
_screen call ["markReady", [true]];
|
||||
_self call ["flushPendingEvents", []];
|
||||
|
||||
_self call ["requestHydrate", [true]]
|
||||
}],
|
||||
["handleSubmitPinRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _pin = _data getOrDefault ["pin", ""];
|
||||
if !(_pin isEqualType "") then { _pin = str _pin; };
|
||||
|
||||
[SRPC(bank,requestSubmitPin), [getPlayerUID player, _pin]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["handleTransferRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _amount = floor (_data getOrDefault ["amount", 0]);
|
||||
private _target = _data getOrDefault ["target", ""];
|
||||
private _from = toLowerANSI (_data getOrDefault ["from", "bank"]);
|
||||
|
||||
[SRPC(bank,requestTransfer), [getPlayerUID player, _target, _from, _amount]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["handleWithdrawRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _amount = floor (_data getOrDefault ["amount", 0]);
|
||||
[SRPC(bank,requestWithdraw), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["refreshSession", compileFinal {
|
||||
_self call ["requestHydrate", [false]]
|
||||
}],
|
||||
["requestHydrate", compileFinal {
|
||||
params [["_resetAuthorization", false, [false]]];
|
||||
|
||||
if !(_self call ["hasOpenScreen", []]) exitWith { false };
|
||||
|
||||
[SRPC(bank,requestHydrateBank), [getPlayerUID player, _self call ["getMode", []], _resetAuthorization]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["sendNotice", compileFinal {
|
||||
params [["_type", "error", [""]], ["_message", "", [""]], ["_control", controlNull, [controlNull]]];
|
||||
|
||||
if (_message isEqualTo "" || { !(_self call ["hasOpenScreen", []]) }) exitWith { false };
|
||||
|
||||
_self call ["sendEvent", ["bank::notice", createHashMapFromArray [
|
||||
["message", _message],
|
||||
["type", _type]
|
||||
], _control]]
|
||||
}],
|
||||
["setMode", compileFinal {
|
||||
params [["_mode", "bank", [""]]];
|
||||
|
||||
private _finalMode = toLowerANSI _mode;
|
||||
if !(_finalMode in ["bank", "atm"]) then { _finalMode = "bank"; };
|
||||
|
||||
_self set ["mode", _finalMode];
|
||||
_finalMode
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(BankUIBridge) = createHashMapObject [GVAR(BankUIBridgeBaseClass)];
|
||||
GVAR(BankUIBridge)
|
||||
41
arma/client/addons/bank/functions/fnc_openUI.sqf
Normal file
41
arma/client/addons/bank/functions/fnc_openUI.sqf
Normal file
@ -0,0 +1,41 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_openUI.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-01-28
|
||||
* Last Update: 2026-01-30
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Opens the player bank interaction interface.
|
||||
*
|
||||
* Arguments:
|
||||
* 0: [BOOL] - Whether to open the ATM interface
|
||||
*
|
||||
* Return Value:
|
||||
* UI opened [BOOL]
|
||||
*
|
||||
* Example:
|
||||
* [true] call forge_client_bank_fnc_openUI;
|
||||
*/
|
||||
|
||||
params [["_isATM", false, [false]]];
|
||||
|
||||
private _display = createDialog ["RscBank", true];
|
||||
private _ctrl = _display displayCtrl 1002;
|
||||
|
||||
_ctrl ctrlAddEventHandler ["JSDialog", {
|
||||
params ["_control", "_isConfirmDialog", "_message"];
|
||||
|
||||
[_control, _isConfirmDialog, _message] call FUNC(handleUIEvents);
|
||||
}];
|
||||
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["setMode", [["bank", "atm"] select _isATM]];
|
||||
GVAR(BankUIBridge) call ["setActiveBrowserControl", [_ctrl]];
|
||||
};
|
||||
|
||||
_ctrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\index.html)];
|
||||
|
||||
true;
|
||||
1
arma/client/addons/bank/initKeybinds.inc.sqf
Normal file
1
arma/client/addons/bank/initKeybinds.inc.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "\forge\forge_client\addons\main\data\hpp\defineDIKCodes.hpp"
|
||||
1
arma/client/addons/bank/initSettings.inc.sqf
Normal file
1
arma/client/addons/bank/initSettings.inc.sqf
Normal file
@ -0,0 +1 @@
|
||||
// Can use localize "STR_ACE_Common_Enabled" for name if ACE is required
|
||||
9
arma/client/addons/bank/script_component.hpp
Normal file
9
arma/client/addons/bank/script_component.hpp
Normal file
@ -0,0 +1,9 @@
|
||||
#define COMPONENT bank
|
||||
#define COMPONENT_BEAUTIFIED Bank
|
||||
#include "\forge\forge_client\addons\main\script_mod.hpp"
|
||||
|
||||
// #define DEBUG_MODE_FULL
|
||||
// #define DISABLE_COMPILE_CACHE
|
||||
// #define ENABLE_PERFORMANCE_COUNTERS
|
||||
|
||||
#include "\forge\forge_client\addons\main\script_macros.hpp"
|
||||
8
arma/client/addons/bank/stringtable.xml
Normal file
8
arma/client/addons/bank/stringtable.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project name="FFE">
|
||||
<Package name="Bank">
|
||||
<Key ID="STR_forge_client_bank_displayName">
|
||||
<English>Bank</English>
|
||||
</Key>
|
||||
</Package>
|
||||
</Project>
|
||||
21
arma/client/addons/bank/ui/RscBank.hpp
Normal file
21
arma/client/addons/bank/ui/RscBank.hpp
Normal file
@ -0,0 +1,21 @@
|
||||
class RscBank {
|
||||
idd = 1001;
|
||||
fadeIn = 0;
|
||||
fadeOut = 0;
|
||||
duration = 1e011;
|
||||
onLoad = "uiNamespace setVariable ['RscBank', _this select 0]";
|
||||
onUnLoad = "uinamespace setVariable ['RscBank', nil]";
|
||||
|
||||
class controlsBackground {};
|
||||
class controls {
|
||||
class IFrame: RscText {
|
||||
type = 106;
|
||||
idc = 1002;
|
||||
x = "safeZoneXAbs";
|
||||
y = "safeZoneY";
|
||||
w = "safeZoneWAbs";
|
||||
h = "safeZoneH";
|
||||
colorBackground[] = {0, 0, 0, 0};
|
||||
};
|
||||
};
|
||||
};
|
||||
98
arma/client/addons/bank/ui/RscCommon.hpp
Normal file
98
arma/client/addons/bank/ui/RscCommon.hpp
Normal file
@ -0,0 +1,98 @@
|
||||
// Control types
|
||||
#define CT_STATIC 0
|
||||
#define CT_BUTTON 1
|
||||
#define CT_EDIT 2
|
||||
#define CT_SLIDER 3
|
||||
#define CT_COMBO 4
|
||||
#define CT_LISTBOX 5
|
||||
#define CT_TOOLBOX 6
|
||||
#define CT_CHECKBOXES 7
|
||||
#define CT_PROGRESS 8
|
||||
#define CT_HTML 9
|
||||
#define CT_STATIC_SKEW 10
|
||||
#define CT_ACTIVETEXT 11
|
||||
#define CT_TREE 12
|
||||
#define CT_STRUCTURED_TEXT 13
|
||||
#define CT_CONTEXT_MENU 14
|
||||
#define CT_CONTROLS_GROUP 15
|
||||
#define CT_SHORTCUTBUTTON 16
|
||||
#define CT_HITZONES 17
|
||||
#define CT_XKEYDESC 40
|
||||
#define CT_XBUTTON 41
|
||||
#define CT_XLISTBOX 42
|
||||
#define CT_XSLIDER 43
|
||||
#define CT_XCOMBO 44
|
||||
#define CT_ANIMATED_TEXTURE 45
|
||||
#define CT_OBJECT 80
|
||||
#define CT_OBJECT_ZOOM 81
|
||||
#define CT_OBJECT_CONTAINER 82
|
||||
#define CT_OBJECT_CONT_ANIM 83
|
||||
#define CT_LINEBREAK 98
|
||||
#define CT_USER 99
|
||||
#define CT_MAP 100
|
||||
#define CT_MAP_MAIN 101
|
||||
#define CT_LISTNBOX 102
|
||||
#define CT_ITEMSLOT 103
|
||||
#define CT_CHECKBOX 77
|
||||
|
||||
// Static styles
|
||||
#define ST_POS 0x0F
|
||||
#define ST_HPOS 0x03
|
||||
#define ST_VPOS 0x0C
|
||||
#define ST_LEFT 0x00
|
||||
#define ST_RIGHT 0x01
|
||||
#define ST_CENTER 0x02
|
||||
#define ST_DOWN 0x04
|
||||
#define ST_UP 0x08
|
||||
#define ST_VCENTER 0x0C
|
||||
|
||||
#define ST_TYPE 0xF0
|
||||
#define ST_SINGLE 0x00
|
||||
#define ST_MULTI 0x10
|
||||
#define ST_TITLE_BAR 0x20
|
||||
#define ST_PICTURE 0x30
|
||||
#define ST_FRAME 0x40
|
||||
#define ST_BACKGROUND 0x50
|
||||
#define ST_GROUP_BOX 0x60
|
||||
#define ST_GROUP_BOX2 0x70
|
||||
#define ST_HUD_BACKGROUND 0x80
|
||||
#define ST_TILE_PICTURE 0x90
|
||||
#define ST_WITH_RECT 0xA0
|
||||
#define ST_LINE 0xB0
|
||||
#define ST_UPPERCASE 0xC0
|
||||
#define ST_LOWERCASE 0xD0
|
||||
|
||||
#define ST_SHADOW 0x100
|
||||
#define ST_NO_RECT 0x200
|
||||
#define ST_KEEP_ASPECT_RATIO 0x800
|
||||
|
||||
// Slider styles
|
||||
#define SL_DIR 0x400
|
||||
#define SL_VERT 0
|
||||
#define SL_HORZ 0x400
|
||||
|
||||
#define SL_TEXTURES 0x10
|
||||
|
||||
// progress bar
|
||||
#define ST_VERTICAL 0x01
|
||||
#define ST_HORIZONTAL 0
|
||||
|
||||
// Listbox styles
|
||||
#define LB_TEXTURES 0x10
|
||||
#define LB_MULTI 0x20
|
||||
|
||||
// Tree styles
|
||||
#define TR_SHOWROOT 1
|
||||
#define TR_AUTOCOLLAPSE 2
|
||||
|
||||
// Default text sizes
|
||||
#define GUI_TEXT_SIZE_SMALL (GUI_GRID_H * 0.8)
|
||||
#define GUI_TEXT_SIZE_MEDIUM (GUI_GRID_H * 1)
|
||||
#define GUI_TEXT_SIZE_LARGE (GUI_GRID_H * 1.2)
|
||||
|
||||
// Pixel grid
|
||||
#define pixelScale 0.50
|
||||
#define GRID_W (pixelW * pixelGrid * pixelScale)
|
||||
#define GRID_H (pixelH * pixelGrid * pixelScale)
|
||||
|
||||
class RscText;
|
||||
1
arma/client/addons/bank/ui/_site/bank-ui.css
Normal file
1
arma/client/addons/bank/ui/_site/bank-ui.css
Normal file
File diff suppressed because one or more lines are too long
1
arma/client/addons/bank/ui/_site/bank-ui.js
Normal file
1
arma/client/addons/bank/ui/_site/bank-ui.js
Normal file
File diff suppressed because one or more lines are too long
1
arma/client/addons/bank/ui/_site/index.html
Normal file
1
arma/client/addons/bank/ui/_site/index.html
Normal file
@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>FORGE Banking Console</title><script>window.ForgeSiteConfig={addonName:"bank",logLabel:"Bank UI",styles:["bank-ui.css"],commonScripts:["forge-webui.js"],scripts:["bank-ui.js"]},function(){const e="../../../common/ui/_site/forge-site-loader.js";("undefined"!=typeof A3API&&A3API&&"function"==typeof A3API.RequestFile?A3API.RequestFile("forge\\forge_client\\addons\\common\\ui\\_site\\forge-site-loader.js"):fetch(e).then(o=>{if(!o.ok)throw new Error("Failed to load "+e);return o.text()})).then(function(e){const o=document.createElement("script");o.text=e,document.head.appendChild(o)}).catch(e=>{console.error("[Bank UI] Failed to load Forge site loader.",e)})}()</script></head><body><div id="app"></div></body></html>
|
||||
116
arma/client/addons/bank/ui/src/bootstrap.js
vendored
Normal file
116
arma/client/addons/bank/ui/src/bootstrap.js
vendored
Normal file
@ -0,0 +1,116 @@
|
||||
(function () {
|
||||
const ForgeWebUI = window.ForgeWebUI;
|
||||
const BankApp = window.BankApp;
|
||||
const islandDefinitions = [
|
||||
{
|
||||
id: "bank-notice-root",
|
||||
preserveScroll: false,
|
||||
render: () => BankApp.componentFns.NoticeLayer(),
|
||||
},
|
||||
{
|
||||
id: "bank-sidebar-root",
|
||||
preserveScroll: false,
|
||||
render: () => BankApp.componentFns.BankSidebar(),
|
||||
},
|
||||
{
|
||||
id: "bank-page-header-root",
|
||||
preserveScroll: false,
|
||||
render: () => BankApp.componentFns.BankPageHeader(),
|
||||
},
|
||||
{
|
||||
id: "bank-summary-section-root",
|
||||
preserveScroll: false,
|
||||
render: () => BankApp.componentFns.BankSummarySection(),
|
||||
},
|
||||
{
|
||||
id: "bank-action-sections-root",
|
||||
preserveScroll: false,
|
||||
render: () => BankApp.componentFns.BankActionSections(),
|
||||
},
|
||||
{
|
||||
id: "bank-support-section-root",
|
||||
preserveScroll: false,
|
||||
render: () => BankApp.componentFns.BankSupportSection(),
|
||||
},
|
||||
{
|
||||
id: "bank-history-section-root",
|
||||
preserveScroll: false,
|
||||
render: () => BankApp.componentFns.BankHistorySection(),
|
||||
},
|
||||
{
|
||||
id: "bank-atm-root",
|
||||
preserveScroll: false,
|
||||
render: () => BankApp.componentFns.ATMView(),
|
||||
},
|
||||
{
|
||||
id: "bank-footer-root",
|
||||
preserveScroll: false,
|
||||
render: () => BankApp.componentFns.BankFooter(),
|
||||
},
|
||||
];
|
||||
|
||||
function createIslandManager() {
|
||||
const mounts = new Map();
|
||||
|
||||
function sync() {
|
||||
islandDefinitions.forEach((definition) => {
|
||||
const container = document.getElementById(definition.id);
|
||||
const current = mounts.get(definition.id);
|
||||
|
||||
if (!container) {
|
||||
if (current) {
|
||||
current.handle.dispose();
|
||||
mounts.delete(definition.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (current && current.container === container) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (current) {
|
||||
current.handle.dispose();
|
||||
}
|
||||
|
||||
const handle = ForgeWebUI.mount(container, definition.render, {
|
||||
preserveScroll: definition.preserveScroll,
|
||||
});
|
||||
mounts.set(definition.id, {
|
||||
container,
|
||||
handle,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
sync,
|
||||
};
|
||||
}
|
||||
|
||||
const app = ForgeWebUI.createApp({
|
||||
name: "bank",
|
||||
root: "#app",
|
||||
setup({ root }) {
|
||||
const islandManager = createIslandManager();
|
||||
|
||||
ForgeWebUI.mount(root, () => BankApp.components.App(), {
|
||||
preserveScroll: false,
|
||||
});
|
||||
|
||||
if (BankApp.bridge) {
|
||||
BankApp.bridge.notifyReady();
|
||||
}
|
||||
|
||||
ForgeWebUI.effect(() => {
|
||||
BankApp.store.getMode();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
islandManager.sync();
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
app.start();
|
||||
})();
|
||||
66
arma/client/addons/bank/ui/src/bridge.js
Normal file
66
arma/client/addons/bank/ui/src/bridge.js
Normal file
@ -0,0 +1,66 @@
|
||||
(function () {
|
||||
const BankApp = (window.BankApp = window.BankApp || {});
|
||||
const store = BankApp.store;
|
||||
const bridge = window.ForgeWebUI.createBridge({
|
||||
closeEvent: "bank::close",
|
||||
globalName: "ForgeBridge",
|
||||
readyEvent: "bank::ready",
|
||||
});
|
||||
|
||||
function hydrate(payloadData) {
|
||||
BankApp.data.applyHydratePayload(payloadData);
|
||||
store.hydrateFromPayload(payloadData);
|
||||
}
|
||||
|
||||
function syncAccount(payloadData) {
|
||||
BankApp.data.applyAccountPatch(payloadData);
|
||||
store.syncAccountPatch();
|
||||
}
|
||||
|
||||
bridge.on("bank::hydrate", hydrate);
|
||||
bridge.on("bank::sync", syncAccount);
|
||||
bridge.on("bank::notice", (payloadData) => {
|
||||
store.finishAction();
|
||||
if (BankApp.actions) {
|
||||
BankApp.actions.showNotice(
|
||||
payloadData.type || "error",
|
||||
payloadData.message || "Bank notice received.",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
BankApp.bridge = {
|
||||
notifyReady() {
|
||||
return bridge.ready({ loaded: true });
|
||||
},
|
||||
receive: bridge.receive,
|
||||
requestClose() {
|
||||
return bridge.close({});
|
||||
},
|
||||
requestDeposit(payload) {
|
||||
return bridge.send("bank::deposit::request", payload);
|
||||
},
|
||||
requestDepositEarnings(payload) {
|
||||
return bridge.send("bank::depositEarnings::request", payload);
|
||||
},
|
||||
requestRepayCreditLine(payload) {
|
||||
return bridge.send("bank::repayCreditLine::request", payload);
|
||||
},
|
||||
requestRefresh() {
|
||||
return bridge.send("bank::refresh", {});
|
||||
},
|
||||
requestChangePin(payload) {
|
||||
return bridge.send("bank::pin::change::request", payload);
|
||||
},
|
||||
requestSubmitPin(payload) {
|
||||
return bridge.send("bank::pin::request", payload);
|
||||
},
|
||||
requestTransfer(payload) {
|
||||
return bridge.send("bank::transfer::request", payload);
|
||||
},
|
||||
requestWithdraw(payload) {
|
||||
return bridge.send("bank::withdraw::request", payload);
|
||||
},
|
||||
sendEvent: bridge.send,
|
||||
};
|
||||
})();
|
||||
104
arma/client/addons/bank/ui/src/components/AppShell.js
Normal file
104
arma/client/addons/bank/ui/src/components/AppShell.js
Normal file
@ -0,0 +1,104 @@
|
||||
(function () {
|
||||
const BankApp = (window.BankApp = window.BankApp || {});
|
||||
const { h } = BankApp.runtime;
|
||||
const WindowTitleBar = window.SharedUI.componentFns.WindowTitleBar;
|
||||
const store = BankApp.store;
|
||||
const actions = BankApp.actions;
|
||||
|
||||
BankApp.componentFns = BankApp.componentFns || {};
|
||||
BankApp.componentFns.NoticeLayer = function NoticeLayer() {
|
||||
const notice = store.getNotice();
|
||||
|
||||
if (!notice.text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "bank-notice-stack" },
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
className:
|
||||
notice.type === "error"
|
||||
? "bank-notice is-error"
|
||||
: "bank-notice is-success",
|
||||
},
|
||||
notice.text,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
BankApp.components = BankApp.components || {};
|
||||
BankApp.components.App = function App() {
|
||||
const mode = store.getMode();
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: mode === "atm" ? "bank-shell is-atm" : "bank-shell" },
|
||||
mode === "atm"
|
||||
? null
|
||||
: WindowTitleBar({
|
||||
kicker: "FORGE Finance",
|
||||
title: "Global Banking Network",
|
||||
onClose: () => actions.closeBank(),
|
||||
closeLabel: "Close banking interface",
|
||||
}),
|
||||
h("div", { id: "bank-notice-root" }),
|
||||
mode === "atm"
|
||||
? h("div", { id: "bank-atm-root" })
|
||||
: [
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
className: "bank-scroll-shell",
|
||||
"data-preserve-scroll-id": "bank-page-scroll",
|
||||
},
|
||||
[
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-layout" },
|
||||
h("div", { id: "bank-sidebar-root" }),
|
||||
h(
|
||||
"main",
|
||||
{ className: "bank-main" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-page" },
|
||||
h("div", {
|
||||
id: "bank-page-header-root",
|
||||
}),
|
||||
h(
|
||||
"p",
|
||||
{ className: "bank-page-copy" },
|
||||
"Manage deposits, withdrawals, transfers, and earnings sweeps from the same shared financial console.",
|
||||
),
|
||||
h("div", {
|
||||
className: "bank-page-divider",
|
||||
}),
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-page-body" },
|
||||
h("div", {
|
||||
id: "bank-summary-section-root",
|
||||
}),
|
||||
h("div", {
|
||||
id: "bank-action-sections-root",
|
||||
}),
|
||||
h("div", {
|
||||
id: "bank-support-section-root",
|
||||
}),
|
||||
h("div", {
|
||||
id: "bank-history-section-root",
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
h("div", { id: "bank-footer-root" }),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
};
|
||||
})();
|
||||
91
arma/client/addons/bank/ui/src/components/BankSidebar.js
Normal file
91
arma/client/addons/bank/ui/src/components/BankSidebar.js
Normal file
@ -0,0 +1,91 @@
|
||||
(function () {
|
||||
const BankApp = (window.BankApp = window.BankApp || {});
|
||||
const { h } = BankApp.runtime;
|
||||
const store = BankApp.store;
|
||||
const actions = BankApp.actions;
|
||||
const { account, session } = BankApp.data;
|
||||
const { formatCurrency, statCard } = BankApp.componentFns;
|
||||
|
||||
BankApp.componentFns = BankApp.componentFns || {};
|
||||
BankApp.componentFns.BankSidebar = function BankSidebar() {
|
||||
store.getAccountVersion();
|
||||
store.getSessionVersion();
|
||||
|
||||
return h(
|
||||
"aside",
|
||||
{ className: "bank-sidebar" },
|
||||
h(
|
||||
"section",
|
||||
{ className: "bank-module" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-module-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "bank-eyebrow" }, "Account"),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "bank-section-title" },
|
||||
"Balances",
|
||||
),
|
||||
),
|
||||
h("span", { className: "bank-pill" }, "Live"),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-summary-grid" },
|
||||
statCard("Bank", formatCurrency(account.bank), "accent"),
|
||||
statCard("Cash", formatCurrency(account.cash)),
|
||||
statCard(
|
||||
"Earnings",
|
||||
formatCurrency(account.earnings),
|
||||
account.earnings > 0 ? "warning" : "",
|
||||
),
|
||||
statCard(
|
||||
"Org Funds",
|
||||
formatCurrency(session.orgFunds),
|
||||
session.orgFunds > 0 ? "success" : "",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"section",
|
||||
{ className: "bank-module" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-module-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "bank-eyebrow" }, "Profile"),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "bank-section-title" },
|
||||
"Account Holder",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-secondary",
|
||||
onClick: () => actions.refreshBank(),
|
||||
},
|
||||
"Refresh",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-profile-stack" },
|
||||
statCard("Name", session.playerName || "Unknown"),
|
||||
statCard("UID", session.uid || "-"),
|
||||
statCard(
|
||||
"Organization",
|
||||
session.orgName || "No active organization",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
72
arma/client/addons/bank/ui/src/components/Footer.js
Normal file
72
arma/client/addons/bank/ui/src/components/Footer.js
Normal file
@ -0,0 +1,72 @@
|
||||
(function () {
|
||||
const BankApp = (window.BankApp = window.BankApp || {});
|
||||
const { h } = BankApp.runtime;
|
||||
const store = BankApp.store;
|
||||
const { account, session } = BankApp.data;
|
||||
const { formatCurrency } = BankApp.componentFns;
|
||||
|
||||
BankApp.componentFns = BankApp.componentFns || {};
|
||||
BankApp.componentFns.BankFooter = function BankFooter() {
|
||||
store.getAccountVersion();
|
||||
store.getSessionVersion();
|
||||
|
||||
const sections = [
|
||||
{
|
||||
title: "Banking Resources",
|
||||
items: [
|
||||
"Account Access Policy",
|
||||
"Transfer & Wire Guidelines",
|
||||
"Cash Handling Schedule",
|
||||
"Terminal Security Notice",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Bank Support",
|
||||
items: session.orgName
|
||||
? [
|
||||
`Organization: ${session.orgName}`,
|
||||
`Treasury Reference: ${formatCurrency(session.orgFunds)}`,
|
||||
`${session.transferTargets.length} transfer recipient(s) currently visible.`,
|
||||
`Primary Ledger: ${formatCurrency(account.bank)}`,
|
||||
]
|
||||
: [
|
||||
"Organization: No active treasury link",
|
||||
`${session.transferTargets.length} transfer recipient(s) currently visible.`,
|
||||
`Primary Ledger: ${formatCurrency(account.bank)}`,
|
||||
`Cash On Hand: ${formatCurrency(account.cash)}`,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return h(
|
||||
"footer",
|
||||
{ className: "bank-footer-bar" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-footer" },
|
||||
...sections.map((section) =>
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-footer-block" },
|
||||
h(
|
||||
"h3",
|
||||
{ className: "bank-footer-title" },
|
||||
section.title,
|
||||
),
|
||||
h(
|
||||
"ul",
|
||||
{ className: "bank-footer-list" },
|
||||
...(section.items || []).map((item) =>
|
||||
h(
|
||||
"li",
|
||||
{ className: "bank-footer-copy" },
|
||||
item,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
189
arma/client/addons/bank/ui/src/components/common.js
Normal file
189
arma/client/addons/bank/ui/src/components/common.js
Normal file
@ -0,0 +1,189 @@
|
||||
(function () {
|
||||
const BankApp = (window.BankApp = window.BankApp || {});
|
||||
const { h } = BankApp.runtime;
|
||||
const store = BankApp.store;
|
||||
const { account } = BankApp.data;
|
||||
|
||||
function formatCurrency(value) {
|
||||
return `$${Math.round(Number(value || 0)).toLocaleString()}`;
|
||||
}
|
||||
|
||||
function pending(actionName) {
|
||||
return store.getPendingAction() === actionName;
|
||||
}
|
||||
|
||||
function statCard(label, value, tone = "") {
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
className: tone
|
||||
? `bank-stat-card is-${tone}`
|
||||
: "bank-stat-card",
|
||||
},
|
||||
h("span", { className: "bank-stat-label" }, label),
|
||||
h("span", { className: "bank-stat-value" }, value),
|
||||
);
|
||||
}
|
||||
|
||||
function metricCard(label, value, copy, tone = "") {
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
className: tone
|
||||
? `bank-metric-card is-${tone}`
|
||||
: "bank-metric-card",
|
||||
},
|
||||
h("span", { className: "bank-eyebrow" }, label),
|
||||
h("span", { className: "bank-metric-value" }, value),
|
||||
h("span", { className: "bank-metric-copy" }, copy),
|
||||
);
|
||||
}
|
||||
|
||||
function pinIndicators(value) {
|
||||
const pin = String(value || "");
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "bank-pin-indicators" },
|
||||
[0, 1, 2, 3].map((index) =>
|
||||
h("span", {
|
||||
className:
|
||||
index < pin.length
|
||||
? "bank-pin-indicator is-filled"
|
||||
: "bank-pin-indicator",
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function readInputValue(id) {
|
||||
return document.getElementById(id)?.value || "";
|
||||
}
|
||||
|
||||
function clearInputValue(id) {
|
||||
const input = document.getElementById(id);
|
||||
if (input) {
|
||||
input.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function keypad(onDigit, onBackspace, onClear, onEnter) {
|
||||
const keys = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "bank-keypad" },
|
||||
keys.map((digit) =>
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-key",
|
||||
onClick: () => onDigit(digit),
|
||||
},
|
||||
digit,
|
||||
),
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-key is-muted",
|
||||
onClick: onClear,
|
||||
},
|
||||
"C",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-key",
|
||||
onClick: () => onDigit("0"),
|
||||
},
|
||||
"0",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-key is-accent",
|
||||
onClick: onEnter,
|
||||
},
|
||||
"Enter",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-key is-wide",
|
||||
onClick: onBackspace,
|
||||
},
|
||||
"Backspace",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function transactionRows() {
|
||||
const transactions = Array.isArray(account.transactions)
|
||||
? account.transactions
|
||||
: [];
|
||||
|
||||
if (transactions.length === 0) {
|
||||
return h(
|
||||
"div",
|
||||
{ className: "bank-empty-state" },
|
||||
h("h3", { className: "bank-empty-title" }, "No transactions"),
|
||||
h(
|
||||
"p",
|
||||
{ className: "bank-empty-copy" },
|
||||
"Deposits, withdrawals, and transfers will appear here after the account begins moving funds.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "bank-history-list" },
|
||||
transactions
|
||||
.slice(0, 8)
|
||||
.map((entry) =>
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-history-row" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-history-copy" },
|
||||
h(
|
||||
"span",
|
||||
{ className: "bank-history-title" },
|
||||
entry.type || "Transaction",
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{ className: "bank-history-meta" },
|
||||
entry.date || "Pending timestamp",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{ className: "bank-history-value" },
|
||||
formatCurrency(entry.amount || 0),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BankApp.componentFns = BankApp.componentFns || {};
|
||||
Object.assign(BankApp.componentFns, {
|
||||
clearInputValue,
|
||||
formatCurrency,
|
||||
keypad,
|
||||
metricCard,
|
||||
pending,
|
||||
pinIndicators,
|
||||
readInputValue,
|
||||
statCard,
|
||||
transactionRows,
|
||||
});
|
||||
})();
|
||||
58
arma/client/addons/bank/ui/src/data.js
Normal file
58
arma/client/addons/bank/ui/src/data.js
Normal file
@ -0,0 +1,58 @@
|
||||
(function () {
|
||||
const BankApp = (window.BankApp = window.BankApp || {});
|
||||
|
||||
const defaultSession = {
|
||||
atmAuthorized: false,
|
||||
creditLine: {
|
||||
amountDue: 0,
|
||||
approvedAmount: 0,
|
||||
availableAmount: 0,
|
||||
interestRate: 0.1,
|
||||
outstandingPrincipal: 0,
|
||||
},
|
||||
mode: "bank",
|
||||
orgFunds: 0,
|
||||
orgName: "",
|
||||
playerName: "",
|
||||
transferTargets: [],
|
||||
uid: "",
|
||||
};
|
||||
|
||||
const defaultAccount = {
|
||||
bank: 0,
|
||||
cash: 0,
|
||||
earnings: 0,
|
||||
transactions: [],
|
||||
};
|
||||
|
||||
function cloneValue(value) {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function replaceObject(target, source) {
|
||||
Object.keys(target).forEach((key) => delete target[key]);
|
||||
Object.assign(target, cloneValue(source));
|
||||
}
|
||||
|
||||
BankApp.data = {
|
||||
account: Object.assign({}, defaultAccount),
|
||||
session: Object.assign({}, defaultSession),
|
||||
applyAccountPatch(patch) {
|
||||
const nextAccount = Object.assign({}, this.account, patch || {});
|
||||
replaceObject(
|
||||
this.account,
|
||||
Object.assign({}, defaultAccount, nextAccount),
|
||||
);
|
||||
},
|
||||
applyHydratePayload(payload) {
|
||||
replaceObject(
|
||||
this.session,
|
||||
Object.assign({}, defaultSession, payload?.session || {}),
|
||||
);
|
||||
replaceObject(
|
||||
this.account,
|
||||
Object.assign({}, defaultAccount, payload?.account || {}),
|
||||
);
|
||||
},
|
||||
};
|
||||
})();
|
||||
238
arma/client/addons/bank/ui/src/pages/ATMView.js
Normal file
238
arma/client/addons/bank/ui/src/pages/ATMView.js
Normal file
@ -0,0 +1,238 @@
|
||||
(function () {
|
||||
const BankApp = (window.BankApp = window.BankApp || {});
|
||||
const { h } = BankApp.runtime;
|
||||
const store = BankApp.store;
|
||||
const actions = BankApp.actions;
|
||||
const { account } = BankApp.data;
|
||||
const { formatCurrency, keypad, pinIndicators } = BankApp.componentFns;
|
||||
|
||||
function atmMenuCard() {
|
||||
return h(
|
||||
"div",
|
||||
{ className: "bank-atm-action-grid" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-primary",
|
||||
onClick: () => actions.selectAtmView("withdraw"),
|
||||
},
|
||||
"Withdraw Cash",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-primary",
|
||||
onClick: () => actions.selectAtmView("deposit"),
|
||||
},
|
||||
"Deposit Cash",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-secondary",
|
||||
onClick: () => actions.selectAtmView("balance"),
|
||||
},
|
||||
"Check Balance",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-secondary",
|
||||
onClick: () => actions.closeBank(),
|
||||
},
|
||||
"Exit Terminal",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function atmAmountMenu(kind) {
|
||||
const label = kind === "deposit" ? "Deposit" : "Withdraw";
|
||||
const amounts = [20, 50, 100, 500];
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "bank-atm-action-grid" },
|
||||
amounts.map((amount) =>
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-primary",
|
||||
onClick: () => actions.requestAtmAmount(kind, amount),
|
||||
},
|
||||
`${label} ${formatCurrency(amount)}`,
|
||||
),
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-secondary",
|
||||
onClick: () =>
|
||||
actions.selectAtmView(
|
||||
kind === "deposit"
|
||||
? "customDeposit"
|
||||
: "customWithdraw",
|
||||
),
|
||||
},
|
||||
"Custom Amount",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-secondary",
|
||||
onClick: () => actions.selectAtmView("menu"),
|
||||
},
|
||||
"Back",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function atmCustomAmount(kind) {
|
||||
const label = kind === "deposit" ? "Deposit" : "Withdraw";
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "bank-atm-stack" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-pin-display" },
|
||||
store.getCustomAmount()
|
||||
? formatCurrency(store.getCustomAmount())
|
||||
: "$0",
|
||||
),
|
||||
keypad(
|
||||
actions.appendCustomAmountDigit,
|
||||
actions.backspaceCustomAmount,
|
||||
actions.clearCustomAmount,
|
||||
() => actions.submitCustomAmount(kind),
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-secondary",
|
||||
onClick: () => actions.selectAtmView("menu"),
|
||||
},
|
||||
`Cancel ${label}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BankApp.componentFns = BankApp.componentFns || {};
|
||||
BankApp.componentFns.ATMView = function ATMView() {
|
||||
store.getAccountVersion();
|
||||
const atmViewName = store.getAtmView();
|
||||
const enteredPin = String(store.getEnteredPin() || "");
|
||||
let title = "Terminal Access";
|
||||
let copy =
|
||||
"Authenticate with the four-digit account PIN before using the terminal.";
|
||||
let content = null;
|
||||
|
||||
switch (atmViewName) {
|
||||
case "menu":
|
||||
title = "ATM Menu";
|
||||
copy =
|
||||
"Select a banking action. The ATM can deposit, withdraw, and show the live account balance.";
|
||||
content = atmMenuCard();
|
||||
break;
|
||||
case "withdraw":
|
||||
title = "Withdraw Cash";
|
||||
copy =
|
||||
"Choose a preset amount or enter a custom amount for withdrawal.";
|
||||
content = atmAmountMenu("withdraw");
|
||||
break;
|
||||
case "deposit":
|
||||
title = "Deposit Cash";
|
||||
copy =
|
||||
"Move cash on hand back into the main bank balance from the terminal.";
|
||||
content = atmAmountMenu("deposit");
|
||||
break;
|
||||
case "customWithdraw":
|
||||
title = "Custom Withdraw";
|
||||
copy = "Enter the exact withdrawal amount.";
|
||||
content = atmCustomAmount("withdraw");
|
||||
break;
|
||||
case "customDeposit":
|
||||
title = "Custom Deposit";
|
||||
copy = "Enter the exact deposit amount.";
|
||||
content = atmCustomAmount("deposit");
|
||||
break;
|
||||
case "balance":
|
||||
title = "Available Balance";
|
||||
copy = "Current bank balance available at this terminal.";
|
||||
content = h(
|
||||
"div",
|
||||
{ className: "bank-atm-stack" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-balance-display" },
|
||||
formatCurrency(account.bank),
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-primary",
|
||||
onClick: () => actions.selectAtmView("menu"),
|
||||
},
|
||||
"Return to Menu",
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
content = h(
|
||||
"div",
|
||||
{ className: "bank-atm-stack" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-pin-display" },
|
||||
pinIndicators(enteredPin),
|
||||
),
|
||||
keypad(
|
||||
actions.appendPinDigit,
|
||||
actions.backspacePin,
|
||||
actions.clearPin,
|
||||
actions.submitPin,
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-secondary",
|
||||
onClick: () => actions.closeBank(),
|
||||
},
|
||||
"Exit Terminal",
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "bank-atm-shell" },
|
||||
h(
|
||||
"section",
|
||||
{ className: "bank-atm-panel" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-panel-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "bank-eyebrow" }, "ATM"),
|
||||
h("h1", { className: "bank-title" }, title),
|
||||
),
|
||||
h("span", { className: "bank-pill" }, "Secure Terminal"),
|
||||
),
|
||||
h("p", { className: "bank-panel-copy" }, copy),
|
||||
content,
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
455
arma/client/addons/bank/ui/src/pages/BankView.js
Normal file
455
arma/client/addons/bank/ui/src/pages/BankView.js
Normal file
@ -0,0 +1,455 @@
|
||||
(function () {
|
||||
const BankApp = (window.BankApp = window.BankApp || {});
|
||||
const { h } = BankApp.runtime;
|
||||
const store = BankApp.store;
|
||||
const actions = BankApp.actions;
|
||||
const { account, session } = BankApp.data;
|
||||
const {
|
||||
clearInputValue,
|
||||
formatCurrency,
|
||||
metricCard,
|
||||
pending,
|
||||
readInputValue,
|
||||
transactionRows,
|
||||
} = BankApp.componentFns;
|
||||
|
||||
function trackAccount() {
|
||||
store.getAccountVersion();
|
||||
}
|
||||
|
||||
function trackSession() {
|
||||
store.getSessionVersion();
|
||||
}
|
||||
|
||||
function pageHeader() {
|
||||
trackSession();
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "bank-page-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "bank-eyebrow" }, "Treasury Desk"),
|
||||
h("h1", { className: "bank-title" }, "Personal Banking"),
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{ className: "bank-pill" },
|
||||
session.playerName || "Account Holder",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function summarySection() {
|
||||
trackAccount();
|
||||
trackSession();
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ className: "bank-page-section bank-summary-section" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-section-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "bank-eyebrow" }, "Overview"),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "bank-section-title" },
|
||||
"Financial Position",
|
||||
),
|
||||
),
|
||||
h("span", { className: "bank-pill" }, "Banking Desk"),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-summary-band" },
|
||||
metricCard(
|
||||
"Primary Balance",
|
||||
formatCurrency(account.bank),
|
||||
"Available for transfers and withdrawals.",
|
||||
"accent",
|
||||
),
|
||||
metricCard(
|
||||
"Cash On Hand",
|
||||
formatCurrency(account.cash),
|
||||
"Funds currently carried by the player.",
|
||||
),
|
||||
metricCard(
|
||||
"Pending Earnings",
|
||||
formatCurrency(account.earnings),
|
||||
"Ready to sweep into the main account ledger.",
|
||||
account.earnings > 0 ? "warning" : "",
|
||||
),
|
||||
metricCard(
|
||||
"Org Snapshot",
|
||||
formatCurrency(session.orgFunds),
|
||||
"Reference value pulled from the organization treasury.",
|
||||
session.orgFunds > 0 ? "success" : "",
|
||||
),
|
||||
metricCard(
|
||||
"Credit Due",
|
||||
formatCurrency(session.creditLine?.amountDue || 0),
|
||||
Number(session.creditLine?.amountDue || 0) > 0
|
||||
? `Outstanding principal ${formatCurrency(session.creditLine?.outstandingPrincipal || 0)} at ${Math.round(Number(session.creditLine?.interestRate || 0) * 100)}% interest.`
|
||||
: "No active credit repayment is currently due.",
|
||||
Number(session.creditLine?.amountDue || 0) > 0
|
||||
? "warning"
|
||||
: "",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function actionSections() {
|
||||
trackSession();
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "bank-action-sections" },
|
||||
h(
|
||||
"section",
|
||||
{ className: "bank-page-section" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-section-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "bank-eyebrow" }, "Movement"),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "bank-section-title" },
|
||||
"Deposit / Withdraw",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-form-stack" },
|
||||
h("input", {
|
||||
id: "bank-amount-input",
|
||||
className: "bank-input",
|
||||
type: "number",
|
||||
min: "1",
|
||||
placeholder: "Enter amount",
|
||||
}),
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-action-row" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-primary",
|
||||
disabled: pending("deposit"),
|
||||
onClick: () => {
|
||||
const sent = actions.requestDeposit(
|
||||
readInputValue("bank-amount-input"),
|
||||
);
|
||||
if (sent) {
|
||||
clearInputValue("bank-amount-input");
|
||||
}
|
||||
},
|
||||
},
|
||||
pending("deposit") ? "Depositing..." : "Deposit",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-secondary",
|
||||
disabled: pending("withdraw"),
|
||||
onClick: () => {
|
||||
const sent = actions.requestWithdraw(
|
||||
readInputValue("bank-amount-input"),
|
||||
);
|
||||
if (sent) {
|
||||
clearInputValue("bank-amount-input");
|
||||
}
|
||||
},
|
||||
},
|
||||
pending("withdraw") ? "Withdrawing..." : "Withdraw",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"section",
|
||||
{ className: "bank-page-section" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-section-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "bank-eyebrow" }, "Transfer"),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "bank-section-title" },
|
||||
"Wire Funds",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-form-stack" },
|
||||
h(
|
||||
"select",
|
||||
{
|
||||
id: "bank-transfer-target",
|
||||
className: "bank-select",
|
||||
},
|
||||
h(
|
||||
"option",
|
||||
{ value: "" },
|
||||
session.transferTargets.length > 0
|
||||
? "Select recipient"
|
||||
: "No available recipients",
|
||||
),
|
||||
session.transferTargets.map((entry) =>
|
||||
h(
|
||||
"option",
|
||||
{ value: entry.uid },
|
||||
entry.name || entry.uid,
|
||||
),
|
||||
),
|
||||
),
|
||||
h("input", {
|
||||
id: "bank-transfer-amount",
|
||||
className: "bank-input",
|
||||
type: "number",
|
||||
min: "1",
|
||||
placeholder: "Enter transfer amount",
|
||||
}),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-primary",
|
||||
disabled:
|
||||
pending("transfer") ||
|
||||
session.transferTargets.length === 0,
|
||||
onClick: () => {
|
||||
const sent = actions.requestTransfer(
|
||||
readInputValue("bank-transfer-target"),
|
||||
readInputValue("bank-transfer-amount"),
|
||||
);
|
||||
if (sent) {
|
||||
clearInputValue("bank-transfer-amount");
|
||||
}
|
||||
},
|
||||
},
|
||||
pending("transfer")
|
||||
? "Transferring..."
|
||||
: "Transfer Funds",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"section",
|
||||
{ className: "bank-page-section" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-section-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "bank-eyebrow" }, "Credit"),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "bank-section-title" },
|
||||
"Repay Org Credit",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-form-stack" },
|
||||
h(
|
||||
"p",
|
||||
{ className: "bank-card-copy" },
|
||||
Number(session.creditLine?.amountDue || 0) > 0
|
||||
? `Outstanding due ${formatCurrency(session.creditLine.amountDue || 0)}. Available reserved credit ${formatCurrency(session.creditLine.availableAmount || 0)}.`
|
||||
: "No repayment is currently due on the assigned organization credit line.",
|
||||
),
|
||||
h("input", {
|
||||
id: "bank-credit-line-amount",
|
||||
className: "bank-input",
|
||||
type: "number",
|
||||
min: "1",
|
||||
placeholder: "Enter repayment amount",
|
||||
}),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-primary",
|
||||
disabled:
|
||||
pending("repaycreditline") ||
|
||||
Number(session.creditLine?.amountDue || 0) <= 0,
|
||||
onClick: () => {
|
||||
const sent = actions.requestRepayCreditLine(
|
||||
readInputValue("bank-credit-line-amount"),
|
||||
);
|
||||
if (sent) {
|
||||
clearInputValue("bank-credit-line-amount");
|
||||
}
|
||||
},
|
||||
},
|
||||
pending("repaycreditline")
|
||||
? "Posting Repayment..."
|
||||
: "Repay Credit Line",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function supportSection() {
|
||||
trackAccount();
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "bank-support-sections" },
|
||||
h(
|
||||
"section",
|
||||
{ className: "bank-page-section" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-section-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "bank-eyebrow" }, "Sweep"),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "bank-section-title" },
|
||||
"Deposit Earnings",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"p",
|
||||
{ className: "bank-card-copy" },
|
||||
"Sweep pending earnings into the primary account when you want them reflected in the main balance.",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-primary",
|
||||
disabled:
|
||||
pending("depositearnings") ||
|
||||
Number(account.earnings || 0) <= 0,
|
||||
onClick: () =>
|
||||
actions.requestDepositEarnings(account.earnings),
|
||||
},
|
||||
pending("depositearnings")
|
||||
? "Depositing..."
|
||||
: "Deposit Earnings",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"section",
|
||||
{ className: "bank-page-section" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-section-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "bank-eyebrow" }, "Security"),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "bank-section-title" },
|
||||
"Change ATM PIN",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-form-stack" },
|
||||
h("input", {
|
||||
id: "bank-current-pin",
|
||||
className: "bank-input",
|
||||
type: "password",
|
||||
inputMode: "numeric",
|
||||
maxLength: "4",
|
||||
placeholder: "Current PIN",
|
||||
}),
|
||||
h("input", {
|
||||
id: "bank-new-pin",
|
||||
className: "bank-input",
|
||||
type: "password",
|
||||
inputMode: "numeric",
|
||||
maxLength: "4",
|
||||
placeholder: "New PIN",
|
||||
}),
|
||||
h("input", {
|
||||
id: "bank-confirm-pin",
|
||||
className: "bank-input",
|
||||
type: "password",
|
||||
inputMode: "numeric",
|
||||
maxLength: "4",
|
||||
placeholder: "Confirm new PIN",
|
||||
}),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-primary",
|
||||
disabled: pending("changepin"),
|
||||
onClick: () => {
|
||||
const sent = actions.requestChangePin(
|
||||
readInputValue("bank-current-pin"),
|
||||
readInputValue("bank-new-pin"),
|
||||
readInputValue("bank-confirm-pin"),
|
||||
);
|
||||
if (sent) {
|
||||
clearInputValue("bank-current-pin");
|
||||
clearInputValue("bank-new-pin");
|
||||
clearInputValue("bank-confirm-pin");
|
||||
}
|
||||
},
|
||||
},
|
||||
pending("changepin") ? "Updating PIN..." : "Update PIN",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function historySection() {
|
||||
trackAccount();
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ className: "bank-page-section bank-history-section" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-section-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "bank-eyebrow" }, "History"),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "bank-section-title" },
|
||||
"Recent Transactions",
|
||||
),
|
||||
),
|
||||
),
|
||||
transactionRows(),
|
||||
);
|
||||
}
|
||||
|
||||
BankApp.componentFns = BankApp.componentFns || {};
|
||||
BankApp.componentFns.BankPageHeader = pageHeader;
|
||||
BankApp.componentFns.BankSummarySection = summarySection;
|
||||
BankApp.componentFns.BankActionSections = actionSections;
|
||||
BankApp.componentFns.BankSupportSection = supportSection;
|
||||
BankApp.componentFns.BankHistorySection = historySection;
|
||||
})();
|
||||
337
arma/client/addons/bank/ui/src/registry/events.js
Normal file
337
arma/client/addons/bank/ui/src/registry/events.js
Normal file
@ -0,0 +1,337 @@
|
||||
(function () {
|
||||
const BankApp = (window.BankApp = window.BankApp || {});
|
||||
const store = BankApp.store;
|
||||
|
||||
let noticeTimer = null;
|
||||
|
||||
function normalizeAmount(value) {
|
||||
const amount = Math.floor(Number(value || 0));
|
||||
return Number.isFinite(amount) ? amount : 0;
|
||||
}
|
||||
|
||||
function showNotice(type, text) {
|
||||
store.setNotice({ type, text });
|
||||
|
||||
if (noticeTimer) {
|
||||
clearTimeout(noticeTimer);
|
||||
}
|
||||
|
||||
noticeTimer = setTimeout(() => {
|
||||
store.setNotice({ text: "", type: "" });
|
||||
noticeTimer = null;
|
||||
}, 3200);
|
||||
}
|
||||
|
||||
function closeBank() {
|
||||
const bridge = BankApp.bridge;
|
||||
if (bridge && typeof bridge.requestClose === "function") {
|
||||
const sent = bridge.requestClose();
|
||||
if (sent) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
showNotice("error", "Bank bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
function refreshBank() {
|
||||
const bridge = BankApp.bridge;
|
||||
if (bridge && typeof bridge.requestRefresh === "function") {
|
||||
const sent = bridge.requestRefresh();
|
||||
if (sent) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
showNotice("error", "Bank refresh bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
function requestDeposit(amountValue) {
|
||||
const amount = normalizeAmount(amountValue);
|
||||
const bridge = BankApp.bridge;
|
||||
if (!bridge || typeof bridge.requestDeposit !== "function") {
|
||||
showNotice("error", "Deposit bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
store.startAction("deposit");
|
||||
const sent = bridge.requestDeposit({ amount });
|
||||
if (!sent) {
|
||||
store.finishAction();
|
||||
showNotice("error", "Deposit bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function requestWithdraw(amountValue) {
|
||||
const amount = normalizeAmount(amountValue);
|
||||
const bridge = BankApp.bridge;
|
||||
if (!bridge || typeof bridge.requestWithdraw !== "function") {
|
||||
showNotice("error", "Withdraw bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
store.startAction("withdraw");
|
||||
const sent = bridge.requestWithdraw({ amount });
|
||||
if (!sent) {
|
||||
store.finishAction();
|
||||
showNotice("error", "Withdraw bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function requestTransfer(targetUid, amountValue) {
|
||||
const amount = normalizeAmount(amountValue);
|
||||
const targetId = String(targetUid || "").trim();
|
||||
|
||||
const bridge = BankApp.bridge;
|
||||
if (!bridge || typeof bridge.requestTransfer !== "function") {
|
||||
showNotice("error", "Transfer bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
store.startAction("transfer");
|
||||
const sent = bridge.requestTransfer({
|
||||
amount,
|
||||
from: "bank",
|
||||
target: targetId,
|
||||
});
|
||||
if (!sent) {
|
||||
store.finishAction();
|
||||
showNotice("error", "Transfer bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function requestDepositEarnings(amountValue) {
|
||||
const amount = normalizeAmount(amountValue);
|
||||
const bridge = BankApp.bridge;
|
||||
if (!bridge || typeof bridge.requestDepositEarnings !== "function") {
|
||||
showNotice("error", "Earnings bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
store.startAction("depositearnings");
|
||||
const sent = bridge.requestDepositEarnings({ amount });
|
||||
if (!sent) {
|
||||
store.finishAction();
|
||||
showNotice("error", "Earnings bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function requestRepayCreditLine(amountValue) {
|
||||
const amount = normalizeAmount(amountValue);
|
||||
const bridge = BankApp.bridge;
|
||||
if (!bridge || typeof bridge.requestRepayCreditLine !== "function") {
|
||||
showNotice("error", "Credit repayment bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
store.startAction("repaycreditline");
|
||||
const sent = bridge.requestRepayCreditLine({ amount });
|
||||
if (!sent) {
|
||||
store.finishAction();
|
||||
showNotice("error", "Credit repayment bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function normalizePin(value) {
|
||||
return String(value || "")
|
||||
.replace(/\D/g, "")
|
||||
.slice(0, 4);
|
||||
}
|
||||
|
||||
function requestChangePin(currentPinValue, newPinValue, confirmPinValue) {
|
||||
const currentPin = normalizePin(currentPinValue);
|
||||
const newPin = normalizePin(newPinValue);
|
||||
const confirmPin = normalizePin(confirmPinValue);
|
||||
const bridge = BankApp.bridge;
|
||||
|
||||
if (!bridge || typeof bridge.requestChangePin !== "function") {
|
||||
showNotice("error", "PIN change bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
if (currentPin.length !== 4) {
|
||||
showNotice("error", "Enter your current four-digit PIN.");
|
||||
return false;
|
||||
}
|
||||
if (newPin.length !== 4) {
|
||||
showNotice("error", "Choose a new four-digit PIN.");
|
||||
return false;
|
||||
}
|
||||
if (newPin !== confirmPin) {
|
||||
showNotice("error", "New PIN confirmation does not match.");
|
||||
return false;
|
||||
}
|
||||
if (currentPin === newPin) {
|
||||
showNotice(
|
||||
"error",
|
||||
"Choose a different PIN from your current PIN.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
store.startAction("changepin");
|
||||
const sent = bridge.requestChangePin({ currentPin, newPin });
|
||||
if (!sent) {
|
||||
store.finishAction();
|
||||
showNotice("error", "PIN change bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function appendPinDigit(digit) {
|
||||
const nextDigit = String(digit || "").trim();
|
||||
if (!nextDigit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPin = String(store.getEnteredPin() || "");
|
||||
if (currentPin.length >= 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.setEnteredPin(currentPin + nextDigit);
|
||||
}
|
||||
|
||||
function backspacePin() {
|
||||
const currentPin = String(store.getEnteredPin() || "");
|
||||
store.setEnteredPin(currentPin.slice(0, -1));
|
||||
}
|
||||
|
||||
function clearPin() {
|
||||
store.setEnteredPin("");
|
||||
}
|
||||
|
||||
function submitPin() {
|
||||
const enteredPin = String(store.getEnteredPin() || "");
|
||||
const bridge = BankApp.bridge;
|
||||
if (!bridge || typeof bridge.requestSubmitPin !== "function") {
|
||||
showNotice("error", "PIN bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
store.startAction("pin");
|
||||
const sent = bridge.requestSubmitPin({ pin: enteredPin });
|
||||
if (!sent) {
|
||||
store.finishAction();
|
||||
showNotice("error", "PIN bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
clearPin();
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectAtmView(view) {
|
||||
const nextView = String(view || "").trim();
|
||||
if (!nextView) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nextView === "pin") {
|
||||
store.resetAtm();
|
||||
return true;
|
||||
}
|
||||
|
||||
store.setCustomAmount("");
|
||||
store.setAtmView(nextView);
|
||||
return true;
|
||||
}
|
||||
|
||||
function appendCustomAmountDigit(digit) {
|
||||
const nextDigit = String(digit || "").trim();
|
||||
if (!nextDigit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentValue = String(store.getCustomAmount() || "");
|
||||
if (currentValue.length >= 7) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.setCustomAmount(currentValue + nextDigit);
|
||||
}
|
||||
|
||||
function backspaceCustomAmount() {
|
||||
const currentValue = String(store.getCustomAmount() || "");
|
||||
store.setCustomAmount(currentValue.slice(0, -1));
|
||||
}
|
||||
|
||||
function clearCustomAmount() {
|
||||
store.setCustomAmount("");
|
||||
}
|
||||
|
||||
function submitCustomAmount(kind) {
|
||||
const amount = normalizeAmount(store.getCustomAmount());
|
||||
const nextKind = String(kind || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
if (amount <= 0) {
|
||||
showNotice("error", "Enter a valid transaction amount.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const success =
|
||||
nextKind === "deposit"
|
||||
? requestDeposit(amount)
|
||||
: requestWithdraw(amount);
|
||||
|
||||
if (success) {
|
||||
store.setCustomAmount("");
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
function requestAtmAmount(kind, amount) {
|
||||
const nextKind = String(kind || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const success =
|
||||
nextKind === "deposit"
|
||||
? requestDeposit(amount)
|
||||
: requestWithdraw(amount);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
BankApp.actions = {
|
||||
appendCustomAmountDigit,
|
||||
appendPinDigit,
|
||||
backspaceCustomAmount,
|
||||
backspacePin,
|
||||
clearCustomAmount,
|
||||
clearPin,
|
||||
closeBank,
|
||||
refreshBank,
|
||||
requestAtmAmount,
|
||||
requestChangePin,
|
||||
requestDeposit,
|
||||
requestDepositEarnings,
|
||||
requestRepayCreditLine,
|
||||
requestTransfer,
|
||||
requestWithdraw,
|
||||
selectAtmView,
|
||||
showNotice,
|
||||
submitCustomAmount,
|
||||
submitPin,
|
||||
};
|
||||
})();
|
||||
84
arma/client/addons/bank/ui/src/registry/store.js
Normal file
84
arma/client/addons/bank/ui/src/registry/store.js
Normal file
@ -0,0 +1,84 @@
|
||||
(function () {
|
||||
const BankApp = (window.BankApp = window.BankApp || {});
|
||||
const { createSignal } = BankApp.runtime;
|
||||
|
||||
class BankStore {
|
||||
constructor() {
|
||||
[this.getMode, this.setMode] = createSignal("bank");
|
||||
[this.getNotice, this.setNotice] = createSignal({
|
||||
text: "",
|
||||
type: "",
|
||||
});
|
||||
[this.getPendingAction, this.setPendingAction] = createSignal("");
|
||||
[this.getAtmView, this.setAtmView] = createSignal("pin");
|
||||
[this.getEnteredPin, this.setEnteredPin] = createSignal("");
|
||||
[this.getCustomAmount, this.setCustomAmount] = createSignal("");
|
||||
[this.getAccountVersion, this.setAccountVersion] = createSignal(0);
|
||||
[this.getSessionVersion, this.setSessionVersion] = createSignal(0);
|
||||
}
|
||||
|
||||
finishAction() {
|
||||
this.setPendingAction("");
|
||||
}
|
||||
|
||||
hydrateFromPayload(payload) {
|
||||
const mode = String(payload?.session?.mode || "bank")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const atmAuthorized = Boolean(payload?.session?.atmAuthorized);
|
||||
const currentMode = this.getMode();
|
||||
const currentAtmView = this.getAtmView();
|
||||
const currentPendingAction = this.getPendingAction();
|
||||
|
||||
this.setMode(mode === "atm" ? "atm" : "bank");
|
||||
this.setPendingAction("");
|
||||
this.setEnteredPin("");
|
||||
this.setCustomAmount("");
|
||||
this.setAccountVersion(this.getAccountVersion() + 1);
|
||||
this.setSessionVersion(this.getSessionVersion() + 1);
|
||||
|
||||
if (mode === "atm") {
|
||||
if (!atmAuthorized) {
|
||||
this.setAtmView("pin");
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
currentPendingAction === "deposit" ||
|
||||
currentPendingAction === "withdraw" ||
|
||||
currentAtmView === "pin" ||
|
||||
currentMode !== "atm"
|
||||
) {
|
||||
this.setAtmView("menu");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setAtmView(currentAtmView);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setAtmView("dashboard");
|
||||
}
|
||||
|
||||
syncAccountPatch() {
|
||||
this.setPendingAction("");
|
||||
this.setAccountVersion(this.getAccountVersion() + 1);
|
||||
}
|
||||
|
||||
resetAtm() {
|
||||
this.setEnteredPin("");
|
||||
this.setCustomAmount("");
|
||||
this.setAtmView("pin");
|
||||
}
|
||||
|
||||
startAction(action) {
|
||||
this.setPendingAction(
|
||||
String(action || "")
|
||||
.trim()
|
||||
.toLowerCase(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BankApp.store = new BankStore();
|
||||
})();
|
||||
6
arma/client/addons/bank/ui/src/runtime.js
Normal file
6
arma/client/addons/bank/ui/src/runtime.js
Normal file
@ -0,0 +1,6 @@
|
||||
(function () {
|
||||
const runtime = window.ForgeWebUI;
|
||||
const BankApp = (window.BankApp = window.BankApp || {});
|
||||
BankApp.runtime = runtime;
|
||||
window.AppRuntime = runtime;
|
||||
})();
|
||||
590
arma/client/addons/bank/ui/src/styles.css
Normal file
590
arma/client/addons/bank/ui/src/styles.css
Normal file
@ -0,0 +1,590 @@
|
||||
:root {
|
||||
--bank-shell-bg: #f6f4ee;
|
||||
--bank-surface: linear-gradient(180deg, #ffffff 0%, #f4f8fd 100%);
|
||||
--bank-border: rgba(18, 54, 93, 0.12);
|
||||
--bank-border-strong: rgba(18, 54, 93, 0.18);
|
||||
--bank-text-main: #142f52;
|
||||
--bank-text-muted: #6f86a3;
|
||||
--bank-text-subtle: #8ea2bb;
|
||||
--bank-accent: #275a8c;
|
||||
--bank-accent-soft: #dfeaf9;
|
||||
--bank-accent-line: rgba(39, 90, 140, 0.12);
|
||||
--bank-shadow: 0 16px 30px rgba(18, 36, 57, 0.08);
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
color: var(--bank-text-main);
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.bank-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--bank-shell-bg);
|
||||
}
|
||||
|
||||
.bank-scroll-shell {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bank-layout {
|
||||
min-height: 100%;
|
||||
width: min(100%, 1600px);
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 320px minmax(0, 1fr);
|
||||
gap: 1.25rem;
|
||||
padding: 1.25rem;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.bank-sidebar,
|
||||
.bank-main {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.bank-main {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.bank-module,
|
||||
.bank-card,
|
||||
.bank-atm-panel {
|
||||
background: var(--bank-surface);
|
||||
border: 1px solid var(--bank-border);
|
||||
border-radius: 1.3rem;
|
||||
box-shadow: var(--bank-shadow);
|
||||
}
|
||||
|
||||
.bank-module,
|
||||
.bank-card,
|
||||
.bank-atm-panel {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bank-module-header,
|
||||
.bank-card-header,
|
||||
.bank-section-header,
|
||||
.bank-page-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.bank-module-header,
|
||||
.bank-card-header {
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.bank-page {
|
||||
display: grid;
|
||||
gap: 1.35rem;
|
||||
padding: 0.1rem 0 0;
|
||||
}
|
||||
|
||||
.bank-page-header {
|
||||
padding-top: 0.4rem;
|
||||
}
|
||||
|
||||
.bank-page-copy {
|
||||
margin: 0;
|
||||
color: var(--bank-text-muted);
|
||||
line-height: 1.5;
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.bank-page-divider {
|
||||
border-top: 1px solid var(--bank-accent-line);
|
||||
}
|
||||
|
||||
.bank-page-body {
|
||||
display: grid;
|
||||
gap: 1.25rem;
|
||||
padding-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.bank-page-section {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
padding: 1.15rem 1.2rem 1.25rem;
|
||||
border: 1px solid var(--bank-border);
|
||||
border-radius: 1.3rem;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.bank-title,
|
||||
.bank-section-title {
|
||||
margin: 0;
|
||||
color: var(--bank-text-main);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.bank-title {
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
|
||||
.bank-section-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bank-eyebrow,
|
||||
.bank-footer-title,
|
||||
.bank-stat-label {
|
||||
display: block;
|
||||
font-size: 0.68rem;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: var(--bank-text-subtle);
|
||||
}
|
||||
|
||||
.bank-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.48rem 0.8rem;
|
||||
border-radius: 999px;
|
||||
background: var(--bank-accent-soft);
|
||||
color: var(--bank-accent);
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bank-summary-grid,
|
||||
.bank-profile-stack {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.bank-summary-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.bank-stat-card,
|
||||
.bank-metric-card {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
padding: 0.9rem;
|
||||
border-radius: 0.95rem;
|
||||
border: 1px solid var(--bank-border);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.bank-stat-card.is-accent,
|
||||
.bank-metric-card.is-accent {
|
||||
background: linear-gradient(180deg, #edf4fe 0%, #dfeaf9 100%);
|
||||
}
|
||||
|
||||
.bank-stat-card.is-success,
|
||||
.bank-metric-card.is-success {
|
||||
background: linear-gradient(180deg, #edf9f4 0%, #dff4ea 100%);
|
||||
}
|
||||
|
||||
.bank-stat-card.is-warning,
|
||||
.bank-metric-card.is-warning {
|
||||
background: linear-gradient(180deg, #fdf7ea 0%, #f7edd4 100%);
|
||||
}
|
||||
|
||||
.bank-stat-value,
|
||||
.bank-metric-value {
|
||||
min-width: 0;
|
||||
color: var(--bank-text-main);
|
||||
font-weight: 700;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.bank-stat-value {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.bank-metric-value {
|
||||
font-size: 1.8rem;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.bank-metric-copy,
|
||||
.bank-card-copy,
|
||||
.bank-empty-copy,
|
||||
.bank-footer-copy,
|
||||
.bank-history-meta {
|
||||
color: var(--bank-text-muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.bank-card-copy {
|
||||
margin: 0 0 0.9rem;
|
||||
}
|
||||
|
||||
.bank-summary-band {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.bank-action-sections {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.bank-support-sections {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.bank-form-stack {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.bank-input,
|
||||
.bank-select {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 2.9rem;
|
||||
padding: 0 0.95rem;
|
||||
border-radius: 0.8rem;
|
||||
border: 1px solid var(--bank-border);
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
color: var(--bank-text-main);
|
||||
}
|
||||
|
||||
.bank-action-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.bank-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 2.85rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.8rem;
|
||||
border: 1px solid var(--bank-border);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 160ms ease,
|
||||
color 160ms ease,
|
||||
border-color 160ms ease;
|
||||
}
|
||||
|
||||
.bank-btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.bank-btn-primary {
|
||||
background: #455a77;
|
||||
border-color: #455a77;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bank-btn-primary:hover:not(:disabled) {
|
||||
background: #354863;
|
||||
border-color: #354863;
|
||||
}
|
||||
|
||||
.bank-btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
color: var(--bank-accent);
|
||||
}
|
||||
|
||||
.bank-btn-secondary:hover:not(:disabled) {
|
||||
background: #eef4fd;
|
||||
}
|
||||
|
||||
.bank-history-list {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.bank-history-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.85rem 0.95rem;
|
||||
border-radius: 0.9rem;
|
||||
border: 1px solid var(--bank-border);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.bank-history-copy {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
.bank-history-title,
|
||||
.bank-empty-title {
|
||||
color: var(--bank-text-main);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bank-history-value {
|
||||
white-space: nowrap;
|
||||
font-weight: 700;
|
||||
color: var(--bank-accent);
|
||||
}
|
||||
|
||||
.bank-empty-state {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.bank-notice-stack {
|
||||
position: fixed;
|
||||
top: 1.2rem;
|
||||
right: 1.5rem;
|
||||
z-index: 12;
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.bank-notice {
|
||||
max-width: 24rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 0.9rem;
|
||||
border: 1px solid var(--bank-border);
|
||||
background: #fff;
|
||||
box-shadow: 0 14px 28px rgba(16, 34, 56, 0.14);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.bank-notice.is-success {
|
||||
background: #ecfdf5;
|
||||
border-color: #bbf7d0;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.bank-notice.is-error {
|
||||
background: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.bank-footer-bar {
|
||||
width: 100%;
|
||||
margin-top: auto;
|
||||
background: #1e293b;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.bank-footer {
|
||||
width: min(100%, 1600px);
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 4rem;
|
||||
padding: 3rem 1.25rem;
|
||||
}
|
||||
|
||||
.bank-footer-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.bank-footer-title {
|
||||
margin: 0;
|
||||
color: #f8fafc;
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
font-weight: 700;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #475569;
|
||||
}
|
||||
|
||||
.bank-footer-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.bank-atm-shell {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.bank-atm-panel {
|
||||
width: min(100%, 560px);
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.bank-atm-stack {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.bank-pin-display,
|
||||
.bank-balance-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 5rem;
|
||||
padding: 1rem;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid var(--bank-border-strong);
|
||||
background: rgba(255, 255, 255, 0.68);
|
||||
color: var(--bank-text-main);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bank-pin-display {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.bank-balance-display {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.bank-pin-indicators {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.bank-pin-indicator {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 999px;
|
||||
border: 2px solid var(--bank-accent);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.bank-pin-indicator.is-filled {
|
||||
background: var(--bank-accent);
|
||||
}
|
||||
|
||||
.bank-keypad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.bank-key {
|
||||
min-height: 3.2rem;
|
||||
padding: 0.9rem;
|
||||
border-radius: 0.9rem;
|
||||
border: 1px solid var(--bank-border);
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
color: var(--bank-text-main);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bank-key.is-muted {
|
||||
background: #eef2f8;
|
||||
color: var(--bank-text-muted);
|
||||
}
|
||||
|
||||
.bank-key.is-accent {
|
||||
background: #455a77;
|
||||
border-color: #455a77;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bank-key.is-wide {
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
.bank-atm-action-grid {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.bank-shell.is-atm {
|
||||
background: transparent;
|
||||
min-height: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.bank-shell.is-atm .bank-atm-shell {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.bank-footer-copy {
|
||||
color: #cbd5e1;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.bank-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.bank-main {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.bank-summary-band,
|
||||
.bank-action-sections,
|
||||
.bank-footer {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.bank-summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
38
arma/client/addons/bank/ui/ui.config.mjs
Normal file
38
arma/client/addons/bank/ui/ui.config.mjs
Normal file
@ -0,0 +1,38 @@
|
||||
export default {
|
||||
addonName: "bank",
|
||||
title: "FORGE Banking Console",
|
||||
logLabel: "Bank UI",
|
||||
outputDir: "_site",
|
||||
jsBundles: [
|
||||
{
|
||||
name: "Bank UI app",
|
||||
output: "bank-ui.js",
|
||||
sources: [
|
||||
"src/runtime.js",
|
||||
"src/data.js",
|
||||
"src/registry/store.js",
|
||||
"src/bridge.js",
|
||||
"src/registry/events.js",
|
||||
"src/components/common.js",
|
||||
"src/components/BankSidebar.js",
|
||||
"src/components/Footer.js",
|
||||
"src/pages/BankView.js",
|
||||
"src/pages/ATMView.js",
|
||||
"src/components/AppShell.js",
|
||||
"src/bootstrap.js",
|
||||
],
|
||||
},
|
||||
],
|
||||
cssBundles: [
|
||||
{
|
||||
name: "Bank UI styles",
|
||||
output: "bank-ui.css",
|
||||
sources: ["src/styles.css"],
|
||||
},
|
||||
],
|
||||
site: {
|
||||
styles: ["bank-ui.css"],
|
||||
commonScripts: ["forge-webui.js"],
|
||||
scripts: ["bank-ui.js"],
|
||||
},
|
||||
};
|
||||
1
arma/client/addons/cad/$PBOPREFIX$
Normal file
1
arma/client/addons/cad/$PBOPREFIX$
Normal file
@ -0,0 +1 @@
|
||||
forge\forge_client\addons\cad
|
||||
11
arma/client/addons/cad/CfgEventHandlers.hpp
Normal file
11
arma/client/addons/cad/CfgEventHandlers.hpp
Normal file
@ -0,0 +1,11 @@
|
||||
class Extended_PreInit_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
|
||||
};
|
||||
};
|
||||
|
||||
class Extended_PostInit_EventHandlers {
|
||||
class ADDON {
|
||||
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_postInitClient));
|
||||
};
|
||||
};
|
||||
214
arma/client/addons/cad/MAP_README.md
Normal file
214
arma/client/addons/cad/MAP_README.md
Normal file
@ -0,0 +1,214 @@
|
||||
# Integrated Map Display System (A3API Pattern)
|
||||
|
||||
This system integrates the Arma 3 native map control (`RscMapControl`) within an HTML/CSS/JS UI using Arma's proper WebBrowser control (type 106) and A3API communication pattern.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Layered Architecture
|
||||
|
||||
1. **IFrame Control (type 106)** - Loads HTML content using `ctrlWebBrowserAction`
|
||||
2. **Map Control (RscMapControl)** - Native Arma map positioned behind/within the UI
|
||||
3. **A3API Communication** - Bidirectional communication between JavaScript and SQF
|
||||
|
||||
### Communication Flow
|
||||
|
||||
**JavaScript → SQF:**
|
||||
```javascript
|
||||
// Send alert (no response expected)
|
||||
A3API.SendAlert(JSON.stringify({
|
||||
event: "map::zoomIn",
|
||||
data: null
|
||||
}));
|
||||
|
||||
// Send confirm (expects response via ExecJS)
|
||||
A3API.SendConfirm(JSON.stringify({
|
||||
event: "map::getPosition",
|
||||
data: null
|
||||
}));
|
||||
```
|
||||
|
||||
**SQF → JavaScript:**
|
||||
```sqf
|
||||
_control ctrlWebBrowserAction ["ExecJS", "updateMapState({center: [1000, 2000], scale: 0.5});"];
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
UI/map/
|
||||
├── _site/
|
||||
│ ├── index.html # HTML with A3API dynamic loading
|
||||
│ ├── script.js # JavaScript using A3API
|
||||
│ └── style.css # Styling
|
||||
└── MAP_README.md # This file
|
||||
|
||||
functions/map/
|
||||
├── fn_openMap.sqf # Opens the display
|
||||
├── fn_mapHandleUIEvents.sqf # Handles JS events
|
||||
├── fn_mapDisplay.sqf # Display initialization
|
||||
└── fn_mapDisplayUpdate.sqf # Update loop
|
||||
|
||||
UI/MapDisplay.h # Dialog definition
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Opening the Map
|
||||
|
||||
```sqf
|
||||
[] call FORGE_fnc_openMap;
|
||||
```
|
||||
|
||||
### From Init or Action
|
||||
|
||||
```sqf
|
||||
// Add player action
|
||||
player addAction ["Open Map", {[] call FORGE_fnc_openMap;}];
|
||||
|
||||
// In init.sqf
|
||||
[] call FORGE_fnc_openMap;
|
||||
```
|
||||
|
||||
## Key Differences from Standard HTML/CSS/JS
|
||||
|
||||
### 1. Dynamic Resource Loading
|
||||
|
||||
Instead of `<link>` and `<script>` tags, files are loaded using A3API:
|
||||
|
||||
```html
|
||||
<script>
|
||||
Promise.all([
|
||||
A3API.RequestFile("UI\\map\\_site\\style.css"),
|
||||
A3API.RequestFile("UI\\map\\_site\\script.js")
|
||||
]).then(([css, js]) => {
|
||||
// Apply CSS
|
||||
const style = document.createElement('style');
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Execute JavaScript
|
||||
const script = document.createElement('script');
|
||||
script.text = js;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. Event Communication
|
||||
|
||||
Use **A3API.SendAlert()** for one-way messages:
|
||||
```javascript
|
||||
A3API.SendAlert(JSON.stringify({event: "map::action", data: value}));
|
||||
```
|
||||
|
||||
Use **A3API.SendConfirm()** for messages expecting a response:
|
||||
```javascript
|
||||
A3API.SendConfirm(JSON.stringify({event: "map::getdata", data: null}));
|
||||
```
|
||||
|
||||
### 3. Pointer Events
|
||||
|
||||
UI elements need `pointer-events: auto` while the body has `pointer-events: none`:
|
||||
|
||||
```css
|
||||
body {
|
||||
pointer-events: none; /* Allows clicks through to map */
|
||||
}
|
||||
|
||||
#topBar {
|
||||
pointer-events: auto; /* UI elements catch clicks */
|
||||
}
|
||||
```
|
||||
|
||||
## Dialog Definition Pattern
|
||||
|
||||
```cpp
|
||||
class RscMapDisplay {
|
||||
idd = 9000;
|
||||
onLoad = "['onLoad', _this] call FORGE_fnc_mapDisplay;";
|
||||
|
||||
class Controls {
|
||||
class Browser: RscText {
|
||||
type = 106; // IFrame control type
|
||||
idc = 9001;
|
||||
x = "safeZoneX";
|
||||
y = "safeZoneY";
|
||||
w = "safeZoneW";
|
||||
h = "safeZoneH";
|
||||
};
|
||||
|
||||
class MapControl: RscMapControl {
|
||||
idc = 9002;
|
||||
// Position to fit within HTML UI
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Event Handler Pattern
|
||||
|
||||
In `fn_openMap.sqf`:
|
||||
```sqf
|
||||
private _ctrl = _display displayCtrl 9001;
|
||||
|
||||
// Add JSDialog event handler
|
||||
_ctrl ctrlAddEventHandler ["JSDialog", {
|
||||
params ["_control", "_isConfirmDialog", "_message"];
|
||||
[_control, _isConfirmDialog, _message] call FORGE_fnc_mapHandleUIEvents;
|
||||
}];
|
||||
|
||||
// Load HTML file
|
||||
_ctrl ctrlWebBrowserAction ["LoadFile", "UI\\map\\_site\\index.html"];
|
||||
```
|
||||
|
||||
In `fn_mapHandleUIEvents.sqf`:
|
||||
```sqf
|
||||
params ["_control", "_isConfirmDialog", "_message"];
|
||||
|
||||
private _eventData = fromJSON _message;
|
||||
private _event = _eventData get "event";
|
||||
private _data = _eventData get "data";
|
||||
|
||||
switch (_event) do {
|
||||
case "map::ready": {
|
||||
// Initialize
|
||||
};
|
||||
case "map::zoomIn": {
|
||||
// Handle zoom
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Benefits of This Pattern
|
||||
|
||||
1. **Proper Arma Integration** - Uses native WebBrowser control (type 106)
|
||||
2. **File System Compatibility** - A3API.RequestFile() works with Arma's file system
|
||||
3. **Reliable Communication** - JSDialog event handler is more stable than htmlLoad
|
||||
4. **Modular** - CSS and JS in separate files, dynamically loaded
|
||||
5. **Consistent** - Matches bank module pattern used in FORGE
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Files not loading:**
|
||||
- Check paths use double backslashes: `"UI\\map\\_site\\style.css"`
|
||||
- Verify files exist in the correct directory
|
||||
- Check .rpt log for file loading errors
|
||||
|
||||
**Events not firing:**
|
||||
- Verify JSDialog event handler is attached
|
||||
- Check JSON formatting in A3API calls
|
||||
- Look for JavaScript console errors (use OpenDevConsole)
|
||||
|
||||
**Map not showing:**
|
||||
- Verify MapControl idc matches (9002)
|
||||
- Check map control positioning in MapDisplay.h
|
||||
- Ensure map control is rendered after browser control
|
||||
|
||||
## Developer Tools
|
||||
|
||||
Enable dev console in `fn_openMap.sqf`:
|
||||
```sqf
|
||||
_ctrl ctrlWebBrowserAction ["OpenDevConsole"];
|
||||
```
|
||||
|
||||
This opens Chromium dev tools for debugging JavaScript, CSS, and network requests.
|
||||
37
arma/client/addons/cad/README.md
Normal file
37
arma/client/addons/cad/README.md
Normal file
@ -0,0 +1,37 @@
|
||||
# Forge Client CAD
|
||||
|
||||
## Overview
|
||||
The CAD addon provides the client map and dispatch interface for task
|
||||
assignment, dispatch orders, support requests, group status, group roles, and
|
||||
task acknowledge/decline actions.
|
||||
|
||||
## Dependencies
|
||||
- `forge_client_main`
|
||||
- server CAD events from `forge_server_cad`
|
||||
- server task catalog data exposed through CAD hydrate payloads
|
||||
|
||||
## Main Components
|
||||
- `fnc_initRepository.sqf` caches hydrated CAD view state.
|
||||
- `fnc_initUI.sqf` wires the native map, top bar, bottom bar, side panel, and
|
||||
dispatcher browser controls.
|
||||
- `fnc_initUIBridge.sqf` sends browser actions to server CAD RPCs and pushes
|
||||
state back to the UI.
|
||||
- `fnc_handleUIEvents.sqf` handles `cad::*` browser events.
|
||||
- `fnc_openUI.sqf` opens the CAD display.
|
||||
|
||||
## Supported Actions
|
||||
- hydrate CAD state
|
||||
- assign active tasks to groups
|
||||
- create and close dispatch orders
|
||||
- submit and close support requests
|
||||
- acknowledge or decline assigned tasks
|
||||
- update group status, role, and profile
|
||||
- focus map requests and toggle panels
|
||||
|
||||
## Notes
|
||||
CAD task visibility depends on server-side task catalog entries. Tasks created
|
||||
through Forge task modules or `forge_server_task_fnc_startTask` are the normal
|
||||
CAD-compatible task sources.
|
||||
|
||||
See [MAP_README.md](./MAP_README.md) for details on the integrated native map
|
||||
and browser layout.
|
||||
5
arma/client/addons/cad/XEH_PREP.hpp
Normal file
5
arma/client/addons/cad/XEH_PREP.hpp
Normal file
@ -0,0 +1,5 @@
|
||||
PREP(handleUIEvents);
|
||||
PREP(initRepository);
|
||||
PREP(initUIBridge);
|
||||
PREP(initUI);
|
||||
PREP(openUI);
|
||||
40
arma/client/addons/cad/XEH_postInitClient.sqf
Normal file
40
arma/client/addons/cad/XEH_postInitClient.sqf
Normal file
@ -0,0 +1,40 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
if (isNil QGVAR(CADRepository)) then { call FUNC(initRepository); };
|
||||
if (isNil QGVAR(CADUIBridge)) then { call FUNC(initUIBridge); };
|
||||
|
||||
[QGVAR(openCAD), {
|
||||
call FUNC(openUI);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseHydrateCad), {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(CADUIBridge) call ["handleHydrateResponse", [_payload]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseCadAssignment), {
|
||||
params [["_result", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(CADUIBridge) call ["handleAssignmentResponse", [_result]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseCadGroupUpdate), {
|
||||
params [["_result", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(CADUIBridge) call ["handleGroupUpdateResponse", [_result]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseCadRequest), {
|
||||
params [["_result", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(CADUIBridge) call ["handleRequestResponse", [_result]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(invalidateCadState), {
|
||||
if (isNil QGVAR(CADRepository)) exitWith {};
|
||||
if !(GVAR(CADRepository) getOrDefault ["isOpen", false]) exitWith {};
|
||||
if (isNil QGVAR(CADUIBridge)) exitWith {};
|
||||
|
||||
GVAR(CADUIBridge) call ["requestHydrate", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
5
arma/client/addons/cad/XEH_preInit.sqf
Normal file
5
arma/client/addons/cad/XEH_preInit.sqf
Normal file
@ -0,0 +1,5 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
PREP_RECOMPILE_START;
|
||||
#include "XEH_PREP.hpp"
|
||||
PREP_RECOMPILE_END;
|
||||
1
arma/client/addons/cad/XEH_preInitClient.sqf
Normal file
1
arma/client/addons/cad/XEH_preInitClient.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "script_component.hpp"
|
||||
21
arma/client/addons/cad/config.cpp
Normal file
21
arma/client/addons/cad/config.cpp
Normal file
@ -0,0 +1,21 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
class CfgPatches {
|
||||
class ADDON {
|
||||
author = AUTHOR;
|
||||
authors[] = {"IDSolutions"};
|
||||
url = ECSTRING(main,url);
|
||||
name = COMPONENT_NAME;
|
||||
requiredVersion = REQUIRED_VERSION;
|
||||
requiredAddons[] = {
|
||||
"forge_client_main"
|
||||
};
|
||||
units[] = {};
|
||||
weapons[] = {};
|
||||
VERSION_CONFIG;
|
||||
};
|
||||
};
|
||||
|
||||
#include "CfgEventHandlers.hpp"
|
||||
#include "ui\RscCommon.hpp"
|
||||
#include "ui\RscMapUI.hpp"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user