commit 7ce6c0bcad4fd0b6d38b9b40937427f7322bc320 Author: Jacob Schmidt Date: Sat Nov 22 22:43:37 2025 -0600 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4246311 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dae0c81 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitea/CONTRIBUTING.md b/.gitea/CONTRIBUTING.md new file mode 100644 index 0000000..b7b4679 --- /dev/null +++ b/.gitea/CONTRIBUTING.md @@ -0,0 +1,12 @@ +# 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). diff --git a/.gitea/ISSUE_TEMPLATE/bug-report.md b/.gitea/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..d4c384f --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,25 @@ +--- +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. diff --git a/.gitea/ISSUE_TEMPLATE/feature-request.md b/.gitea/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..709ee6c --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,15 @@ +--- +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. diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..6f72c35 --- /dev/null +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,12 @@ +**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 +- [ ] Issue diff --git a/.gitea/assets/placeholder.txt b/.gitea/assets/placeholder.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.gitea/assets/placeholder.txt @@ -0,0 +1 @@ + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ced595b --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Rust +/target/ +**/*.rs.bk +*.pdb + +# Cargo +Cargo.lock +debug/ +target/ + +# Build artifacts +*.exe +*.dll +*.so +*.dylib + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/Architecture_Diagram.md b/Architecture_Diagram.md new file mode 100644 index 0000000..686faf6 --- /dev/null +++ b/Architecture_Diagram.md @@ -0,0 +1,131 @@ +# Forge Architecture & Data Flow Diagram + +## 🏗️ **System Architecture Overview** + +```mermaid +graph TD + subgraph ForgeSystem [FORGE SYSTEM] + subgraph Clients [Clients #40;Read-Only#41;] + ClientA[CLIENT A] + ClientB[CLIENT B] + ClientN[CLIENT N] + + subgraph OptimisticCache [Optimistic Cache] + ActorObj[Actor Object
- loadout
- position
- stats] + end + + ClientA --- OptimisticCache + ClientB --- OptimisticCache + ClientN --- OptimisticCache + end + + subgraph Server [ArmA 3 SERVER #40;Hot Cache#41;] + ActorRegistry["GVAR(ActorRegistry)
In-Memory HashMap
UID -> {loadout, position, stats...}"] + SessionMgmt[Session Management
- Token Generation
- UID Resolution
- Player State] + end + + subgraph Rust [EXTENSION #40;Cold Storage#41;] + ConnPool["Connection Pool
(bb8-redis)
2-10 connections"] + RedisOps[Redis Operations
- actor_get/set/update
- Async I/O] + end + + subgraph Redis [DATABASE #40;Saved to Disc#41;] + ActorDataStore[Actor Data Store
actor:UID -> JSON] + Modules[Additional Modules
garage, locker, bank, org] + end + + Clients -->|Event Driven
#40;CBA A3 Events#41;| Server + Server -->|Extension Calls
#40;Rust FFI#41;| Rust + Rust -->|Redis Protocol
#40;bb8-redis#41;| Redis + end +``` + +## 🔄 **Data Flow Sequence** + +### **1. Player Connection & Initial Data Load** + +```mermaid +sequenceDiagram + participant Client + participant Server as Server (Hot Cache) + participant Extension as Extension (Cold Storage) + participant Redis as Redis (Database) + + Note over Client, Redis: 1. Player Connection & Initial Data Load + + Client->>Server: 1. Connect + Client->>Server: 2. Request Actor Data + Server->>Server: 3. Check Cache (Cache Miss) + Server->>Extension: 4. Extension Call + Extension->>Redis: 5. Redis Query + Redis-->>Extension: 6. JSON Data + Extension-->>Server: 7. Actor Data + Server->>Server: 8. Store in Hot Cache + Server-->>Client: 9. Secure Response + Client->>Client: 10. Update Local Cache +``` + +### **2. Subsequent Data Access (Cache Hit)** + +```mermaid +sequenceDiagram + participant Client + participant Server as Server (Hot Cache) + participant Extension as Extension (Cold Storage) + participant Redis as Redis (Database) + + Note over Client, Redis: 2. Subsequent Data Access (Cache Hit) + + Client->>Server: 1. Request Actor Data + Server->>Server: 2. Check Cache (Cache Hit!) + Server-->>Client: 3. Instant Response + Client->>Client: 4. Update Local Cache +``` + +### **3. Data Update (Write-Through)** + +```mermaid +sequenceDiagram + participant Client + participant Server as Server (Hot Cache) + participant Extension as Extension (Cold Storage) + participant Redis as Redis (Database) + + Note over Client, Redis: 3. Data Update (Write-Through) + + Client->>Server: 1. Action (Move, etc) + Server->>Server: 2. Validate & Update Cache + Server->>Extension: 3. Persist to Database + Extension->>Redis: 4. Redis Update + Redis-->>Extension: 5. Confirmation + Extension-->>Server: 6. Success + Server-->>Client: 7. Sync to All Clients +``` + +## 🚀 **Performance Characteristics** + +### **Access Times** +- **Hot Cache (Server)**: `< 1ms` (HashMap lookup) +- **Cold Storage (Redis)**: `1-5ms` (Network + Redis) +- **Client Cache**: `< 0.1ms` (Local object access) + +### **Cache Hit Ratios** +- **Hot Cache**: `~95%` (Active players) +- **Cold Storage**: `~5%` (New connections, cache misses) + +### **Memory Usage** +- **Server Registry**: `~1KB per active player` +- **Client Cache**: `~500B per player object` +- **Redis**: `~2KB per player (persistent)` + +## 🔒 **Security & Session Management** + +```mermaid +flowchart TD + subgraph SessionMgmt [SERVER-SIDE #40;Session MGT#41;] + Conn[Player Connection] --> Token[Session Token Generation
#40;Generated on server#41;] + Token --> UID[UID Resolution
#40;Steam UID mapping#41;] + UID --> State[Player State Tracking
#40;Tracked in ActorRegistry#41;] + State --> Access[Data Access Authorized
#40;Authorized via session#41;] + end +``` diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..aabe7fe --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[workspace] +members = [ + "arma/server/extension", + "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" +redis = "1.0.0-rc.1" +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"] } diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..659cbdc --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,119 @@ +![APL-SA](https://www.bohemia.net/assets/img/licenses/APL-SA.png) + +## 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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ae66ab --- /dev/null +++ b/README.md @@ -0,0 +1,294 @@ +# Forge Framework + +**Forge** is a high-performance, production-ready framework for Arma 3 persistent game servers, built with Rust and Redis for optimal performance and reliability. + +## Overview + +Forge provides a complete solution for managing persistent player data, organizations, and game state in Arma 3 multiplayer environments. It combines the performance of Rust with the flexibility of Redis to deliver sub-millisecond response times while maintaining data consistency across server restarts. + +### Key Features + +- **🚀 High Performance**: Sub-millisecond data access through intelligent caching +- **🔒 Data Integrity**: Strict validation and type safety at every layer +- **🏗️ Clean Architecture**: Layered design following SOLID principles +- **📦 Modular Design**: Easy to extend with new entities and features +- **🔄 Real-time Sync**: Automatic state synchronization across all clients +- **💾 Persistent Storage**: Redis-backed storage with automatic failover +- **🧪 Testable**: Mock-friendly architecture for comprehensive testing + +## Architecture + +Forge follows a **layered architecture** pattern: + +```mermaid +graph TD + Extension[Extension Layer
ArmA 3 Interface <---> Rust] + Services[Services Layer
#40;Business Logic#41;] + Repositories[Repositories Layer
#40;Data Persistence#41;] + Models[Models Layer
#40;Data Structures & Validation#41;] + + Extension --> Services + Services --> Repositories + Repositories --> Models +``` + +**Communication Flow**: +- **Clients** → Use events (`CBA_Events`) to communicate with server +- **Server** → Calls Rust extension via `callExtension` +- **Extension** → Manages Redis connection pool and data operations + +For detailed architecture information, see [Diagram](Architecture_Diagram.md). + +## Project Structure + +``` +forge/ +├── arma/ +│ ├── client/ # Client-side SQF mod +│ │ ├── addons/ +│ │ │ ├── main/ # Core initialization & config +│ │ │ ├── common/ # Shared utilities & helpers +│ │ │ ├── actor/ # Actor/player UI, class & events +│ │ │ ├── org/ # Organization UI, class & events +│ │ │ └── bank/ # Banking UI, class & events +│ │ ├── include/ # Header files +│ │ └── tools/ # Build tools +│ ├── server/ +│ │ ├── addons/ +│ │ │ ├── main/ # Core initialization & config +│ │ │ ├── common/ # Shared utilities & helpers +│ │ │ ├── actor/ # Actor/player Registry, Store & events +│ │ │ └── org/ # Organization Registry, Store & events +│ │ ├── include/ # Header files +│ │ ├── tools/ # Build tools +│ │ └── extension/ # Rust extension (Arma 3 interface) +│ │ ├── src/ +│ │ │ ├── actor.rs # Actor/player commands +│ │ │ ├── org.rs # Organization commands +│ │ │ ├── redis/ # Redis operations module +│ │ │ └── adapters/ # Repository adapters +│ │ └── README.md +├── lib/ +│ ├── models/ # Data structures & validation +│ ├── repositories/ # Data persistence layer +│ ├── services/ # Business logic layer +│ ├── shared/ # Common utilities & traits +│ └── README.md +└── FORGE_Architecture_Diagram.md +``` + +## Quick Start + +### Prerequisites + +- Rust 1.70+ with `cargo` +- Redis 6.0+ +- HEMTT + +1. Clone the repository from Gitea +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). + +### Building the Extension + +```bash +# Build for release +cargo build --release + +# The compiled extension will be at: +# target/release/forge_server.dll (Windows) +# target/release/forge_server.so (Linux) +``` + +### Configuration + +Create `@forge_server/config.toml`: + +```toml +[redis] +host = "127.0.0.1" +port = 6379 +password = "" # Optional +max_connections = 10 +min_connections = 2 +idle_timeout = 300 +``` + +### SQF Usage + +```sqf +// Create an actor +private _data = createHashMapFromArray [ + ["name", "John Doe"], + ["bank", 1000], + ["level", 1] +]; +private _result = "forge_server" callExtension ["actor:create", [getPlayerUID player, toJSON _data]]; + +// Get actor data +private _result = "forge_server" callExtension ["actor:get", [getPlayerUID player]]; +private _actorData = fromJSON (_result select 0); + +// Update actor +private _update = createHashMapFromArray [["bank", 1500]]; +"forge_server" callExtension ["actor:update", [getPlayerUID player, toJSON _update]]; +``` + +## Core Modules + +### Models +Defines strict data structures with built-in validation: +- `Actor`: Player data (stats, inventory, position) +- `Org`: Organization/clan data (members, roles, metadata) + +[Documentation](lib/models/README.md) + +### Repositories +Manages data persistence with Redis: +- Hash-based storage for structured data +- Set-based storage for collections +- Generic over Redis client implementations + +[Documentation](lib/repositories/README.md) + +### Services +Implements business logic and orchestration: +- Get-or-create patterns +- Data validation and transformation +- Complex workflows + +[Documentation](lib/services/README.md) + +### Extension +Arma 3 interface layer: +- Command routing and parsing +- Session management +- Error handling and logging + +[Documentation](arma/server/extension/README.md) + +### Client Mod +Client-side SQF addon that provides: +- **UI Components**: Player interfaces for inventory, organizations, banking +- **Event Handlers**: CBA event listeners for server communication +- **Optimistic Caching**: Local data caching for instant UI updates +- **State Management**: Client-side state synchronization +- **Input Validation**: Client-side validation before server requests + +The client mod communicates with the server using **CBA Events**, ensuring: +- No direct extension calls from clients (security) +- Event-driven architecture for scalability +- Automatic state synchronization across all clients +- Reduced server load through client-side caching + +## Available Commands + +### Actor Commands +| Command | Description | +|---------|-------------| +| `actor:get` | Retrieve actor data by UID | +| `actor:create` | Create a new actor | +| `actor:update` | Update actor fields | +| `actor:exists` | Check if actor exists | +| `actor:delete` | Delete actor data | + +### Organization Commands +| Command | Description | +|---------|-------------| +| `org:get` | Retrieve organization data | +| `org:create` | Create a new organization | +| `org:update` | Update organization fields | +| `org:exists` | Check if organization exists | +| `org:delete` | Delete organization | +| `org:add_member` | Add member to organization | +| `org:remove_member` | Remove member from organization | +| `org:get_members` | Get all organization members | + +### Redis Operations +Direct Redis operations for advanced use cases: +- **Common**: Key-value operations (set, get, incr, decr, del) +- **Hash**: Structured data (hset, hget, hgetall, hdel) +- **List**: Ordered collections (lpush, rpush, lrange, lpop, rpop) +- **Set**: Unique collections (sadd, smembers, srem, sismember) + +[Documentation](arma/server/extension/src/redis/README.md) + +## Performance + +- **Hot Cache (Server)**: < 1ms (HashMap lookup) +- **Cold Storage (Redis)**: 1-5ms (Network + Redis query) +- **Cache Hit Ratio**: ~95% for active players +- **Memory Usage**: ~1KB per active player (server), ~2KB per player (Redis) + +## Contributing + +We welcome contributions! Please see the contributing guides for each layer: + +- [Extension Contributing Guide](arma/server/extension/README.md#contributing) +- [Services Contributing Guide](lib/services/README.md#contributing) +- [Repositories Contributing Guide](lib/repositories/README.md#contributing) +- [Models Contributing Guide](lib/models/README.md#contributing) +- [Library Contributing Guide](lib/README.md#contributing) +- [Adapter Contributing Guide](arma/server/extension/src/adapters/#contributing) + +### Development Workflow + +1. **Define Model**: Create data structure with validation +2. **Create Repository**: Implement persistence layer +3. **Build Service**: Add business logic +4. **Expose in Extension**: Create SQF-callable commands +5. **Test**: Verify each layer independently + +## Error Handling + +All commands return consistent error messages: + +```sqf +private _result = "forge_server" callExtension ["actor:get", ["invalid_uid"]]; +private _response = _result select 0; + +if (_response find "Error:" == 0) then { + diag_log format ["Operation failed: %1", _response]; +} else { + private _data = fromJSON _response; + // Use data +}; +``` + +## Logging + +Logs are automatically created in `@forge_server/logs/`: +- `actor.log` - Actor operations +- `org.log` - Organization operations +- `redis.log` - Redis connection and operations +- `debug.log` - General debug information + +## License + +[Your License Here] + +## Support + +- **Issues**: [Gitea Issues](https://gitea.innovativedevsolutions.org/IDSolutions/forge/issues) +- **Documentation**: See individual module READMEs +- **Architecture**: [FORGE_Architecture_Diagram.md](FORGE_Architecture_Diagram.md) + +## Roadmap + +- [ ] Admin system +- [ ] Arsenal system +- [ ] Banking system +- [ ] Economy system +- [ ] Garage system +- [ ] Locker system +- [ ] Mission template + +--- + +Built using **Rust**, **Redis**, and **Arma 3** diff --git a/arma/client/.editorconfig b/arma/client/.editorconfig new file mode 100644 index 0000000..4246311 --- /dev/null +++ b/arma/client/.editorconfig @@ -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 diff --git a/arma/client/.gitattributes b/arma/client/.gitattributes new file mode 100644 index 0000000..dae0c81 --- /dev/null +++ b/arma/client/.gitattributes @@ -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 diff --git a/arma/client/.github/CONTRIBUTING.md b/arma/client/.github/CONTRIBUTING.md new file mode 100644 index 0000000..b7b4679 --- /dev/null +++ b/arma/client/.github/CONTRIBUTING.md @@ -0,0 +1,12 @@ +# 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). diff --git a/arma/client/.github/ISSUE_TEMPLATE/bug-report.md b/arma/client/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..d4c384f --- /dev/null +++ b/arma/client/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,25 @@ +--- +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. diff --git a/arma/client/.github/ISSUE_TEMPLATE/feature-request.md b/arma/client/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..709ee6c --- /dev/null +++ b/arma/client/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,15 @@ +--- +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. diff --git a/arma/client/.github/PULL_REQUEST_TEMPLATE.md b/arma/client/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..6f72c35 --- /dev/null +++ b/arma/client/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,12 @@ +**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 +- [ ] Issue diff --git a/arma/client/.github/assets/placeholder.txt b/arma/client/.github/assets/placeholder.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/arma/client/.github/assets/placeholder.txt @@ -0,0 +1 @@ + diff --git a/arma/client/.github/workflows/check.yml b/arma/client/.github/workflows/check.yml new file mode 100644 index 0000000..9d2f654 --- /dev/null +++ b/arma/client/.github/workflows/check.yml @@ -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 diff --git a/arma/client/.gitignore b/arma/client/.gitignore new file mode 100644 index 0000000..b786b16 --- /dev/null +++ b/arma/client/.gitignore @@ -0,0 +1,17 @@ +# HEMTT +hemtt.exe +.hemtt/missions/~* +.hemttout/ +releases/ + +# Textures +Exports/ +*.spp +*.spp.painter_lock +*.psd + +# Other +*.biprivatekey +*.zip +*.pbo +*.sqfc diff --git a/arma/client/.hemtt/commands/ctrlWebBrowserAction.yml b/arma/client/.hemtt/commands/ctrlWebBrowserAction.yml new file mode 100644 index 0000000..97b2d5f --- /dev/null +++ b/arma/client/.hemtt/commands/ctrlWebBrowserAction.yml @@ -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: + - _control ctrlWebBrowserAction ["ExecJS", "document.getElementById('test').innerHTML = 'Hello World!'"]; + - _control ctrlWebBrowserAction ["LoadURL", "https://community.bistudio.com"]; diff --git a/arma/client/.hemtt/hooks/post_release/01_move_readme.rhai b/arma/client/.hemtt/hooks/post_release/01_move_readme.rhai new file mode 100644 index 0000000..eb59898 --- /dev/null +++ b/arma/client/.hemtt/hooks/post_release/01_move_readme.rhai @@ -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()); diff --git a/arma/client/.hemtt/hooks/pre_build/01_set_version.rhai b/arma/client/.hemtt/hooks/pre_build/01_set_version.rhai new file mode 100644 index 0000000..c7930cd --- /dev/null +++ b/arma/client/.hemtt/hooks/pre_build/01_set_version.rhai @@ -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()); diff --git a/arma/client/.hemtt/launch.toml b/arma/client/.hemtt/launch.toml new file mode 100644 index 0000000..1e2e390 --- /dev/null +++ b/arma/client/.hemtt/launch.toml @@ -0,0 +1,16 @@ +[default] +workshop = [ + "450814997", # CBA_A3 + "3499977893", # Advanced Dev Tools + "623475643", # 3DEN Enhanced +] +presets = [] +dlc = [] +optionals = [] +parameters = [] + +[ace] +extends = "default" +workshop = [ + "463939057", # ACE +] diff --git a/arma/client/.hemtt/lints.toml b/arma/client/.hemtt/lints.toml new file mode 100644 index 0000000..af141f2 --- /dev/null +++ b/arma/client/.hemtt/lints.toml @@ -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 diff --git a/arma/client/.hemtt/project.toml b/arma/client/.hemtt/project.toml new file mode 100644 index 0000000..7c308fc --- /dev/null +++ b/arma/client/.hemtt/project.toml @@ -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", + "logo_forge_client.png", + "logo_forge_client_over.png", + "logo_forge_client_ca.paa", + "logo_forge_client_over_ca.paa", + "LICENSE.md", + "README.md", +] +exclude = [] + +[properties] +author = "J.Schmidt" diff --git a/arma/client/.hemtt/scripts/update_build.rhai b/arma/client/.hemtt/scripts/update_build.rhai new file mode 100644 index 0000000..14bdbeb --- /dev/null +++ b/arma/client/.hemtt/scripts/update_build.rhai @@ -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); diff --git a/arma/client/.hemtt/scripts/update_minor.rhai b/arma/client/.hemtt/scripts/update_minor.rhai new file mode 100644 index 0000000..b8344fb --- /dev/null +++ b/arma/client/.hemtt/scripts/update_minor.rhai @@ -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); diff --git a/arma/client/.hemtt/scripts/update_patch.rhai b/arma/client/.hemtt/scripts/update_patch.rhai new file mode 100644 index 0000000..a90383f --- /dev/null +++ b/arma/client/.hemtt/scripts/update_patch.rhai @@ -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); diff --git a/arma/client/LICENSE.md b/arma/client/LICENSE.md new file mode 100644 index 0000000..659cbdc --- /dev/null +++ b/arma/client/LICENSE.md @@ -0,0 +1,119 @@ +![APL-SA](https://www.bohemia.net/assets/img/licenses/APL-SA.png) + +## 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. \ No newline at end of file diff --git a/arma/client/README.md b/arma/client/README.md new file mode 100644 index 0000000..f65d2bd --- /dev/null +++ b/arma/client/README.md @@ -0,0 +1,27 @@ +

Forge Client

+

+ Version + Issues + + License +
+ HEMTT + CBA A3 +

+ +

+ Requires the latest version of CBA A3 +

+ +**Forge Client** aims to... + +The project is entirely **open-source** and any contributions are welcome. + +## Core Features +- Feature + +## Contributing +For new contributers, see the [Contributing Setup & Guidelines](./.github/CONTRIBUTING.md). + +## License +Forge Client is licensed under [APL-SA](./LICENSE.md). diff --git a/arma/client/addons/actor/$PBOPREFIX$ b/arma/client/addons/actor/$PBOPREFIX$ new file mode 100644 index 0000000..b2c67a6 --- /dev/null +++ b/arma/client/addons/actor/$PBOPREFIX$ @@ -0,0 +1 @@ +forge\forge_client\addons\actor diff --git a/arma/client/addons/actor/CfgEventHandlers.hpp b/arma/client/addons/actor/CfgEventHandlers.hpp new file mode 100644 index 0000000..c6e25db --- /dev/null +++ b/arma/client/addons/actor/CfgEventHandlers.hpp @@ -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)); + }; +}; diff --git a/arma/client/addons/actor/README.md b/arma/client/addons/actor/README.md new file mode 100644 index 0000000..9dcb48a --- /dev/null +++ b/arma/client/addons/actor/README.md @@ -0,0 +1,4 @@ +forge_client_actor +=================== + +Description for this addon diff --git a/arma/client/addons/actor/XEH_PREP.hpp b/arma/client/addons/actor/XEH_PREP.hpp new file mode 100644 index 0000000..0dcc312 --- /dev/null +++ b/arma/client/addons/actor/XEH_PREP.hpp @@ -0,0 +1,3 @@ +PREP(handleUIEvents); +PREP(initActorClass); +PREP(openUI); diff --git a/arma/client/addons/actor/XEH_postInit.sqf b/arma/client/addons/actor/XEH_postInit.sqf new file mode 100644 index 0000000..421c54b --- /dev/null +++ b/arma/client/addons/actor/XEH_postInit.sqf @@ -0,0 +1 @@ +#include "script_component.hpp" diff --git a/arma/client/addons/actor/XEH_postInitClient.sqf b/arma/client/addons/actor/XEH_postInitClient.sqf new file mode 100644 index 0000000..d22f14d --- /dev/null +++ b/arma/client/addons/actor/XEH_postInitClient.sqf @@ -0,0 +1,42 @@ +#include "script_component.hpp" + +removeAllWeapons player; +removeAllAssignedItems player; +removeUniform player; +removeVest player; +removeBackpack player; +removeGoggles player; +removeHeadgear player; + +SETPVAR(player,FORGE_actorIsLoaded,false); +cutText ["Loading In...", "BLACK", 1]; + +if (isNil QGVAR(ActorClass)) then { [] call FUNC(initActorClass); }; + +[QGVAR(initActor), { + GVAR(ActorClass) call ["init", []]; +}] call CFUNC(addEventHandler); + +[QGVAR(responseInitActor), { + params [["_data", createHashMap, [createHashMap]]]; + + GVAR(ActorClass) call ["sync", [_data, true]]; + + SETPVAR(player,FORGE_isLoaded,true); + cutText ["", "PLAIN", 1]; +}] call CFUNC(addEventHandler); + +[QGVAR(responseSyncActor), { + params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]]; + + GVAR(ActorClass) call ["sync", [_data, _jip]]; +}] call CFUNC(addEventHandler); + +[QGVAR(initActor), []] call CFUNC(localEvent); + +[{ + GETVAR(player,FORGE_actorIsLoaded,false) +}, { + private _holster = GVAR(ActorClass) call ["get", ["holster", true]]; + if (_holster) then { [player] call AFUNC(weaponselect,putWeaponAway); }; +}] call CFUNC(waitUntilAndExecute); diff --git a/arma/client/addons/actor/XEH_preInit.sqf b/arma/client/addons/actor/XEH_preInit.sqf new file mode 100644 index 0000000..8d45ad6 --- /dev/null +++ b/arma/client/addons/actor/XEH_preInit.sqf @@ -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" diff --git a/arma/client/addons/actor/XEH_preInitClient.sqf b/arma/client/addons/actor/XEH_preInitClient.sqf new file mode 100644 index 0000000..421c54b --- /dev/null +++ b/arma/client/addons/actor/XEH_preInitClient.sqf @@ -0,0 +1 @@ +#include "script_component.hpp" diff --git a/arma/client/addons/actor/XEH_preStart.sqf b/arma/client/addons/actor/XEH_preStart.sqf new file mode 100644 index 0000000..0228885 --- /dev/null +++ b/arma/client/addons/actor/XEH_preStart.sqf @@ -0,0 +1,3 @@ +#include "script_component.hpp" + +#include "XEH_PREP.hpp" diff --git a/arma/client/addons/actor/config.cpp b/arma/client/addons/actor/config.cpp new file mode 100644 index 0000000..94b4a94 --- /dev/null +++ b/arma/client/addons/actor/config.cpp @@ -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" diff --git a/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf b/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf new file mode 100644 index 0000000..bcde6ec --- /dev/null +++ b/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf @@ -0,0 +1,45 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Handles the UI events. + * + * Arguments: + * None + * + * Return Value: + * None + * + * Example: + * [] call forge_client_actor_fnc_handleUIEvents; + * + * Public: No + */ + +params ["_control", "_isConfirmDialog", "_message"]; + +private _alert = fromJSON _message; +private _event = _alert get "event"; +private _data = _alert get "data"; +private _display = displayChild findDisplay 46; + +diag_log format ["[FORGE:Client:Actor] Handling UI event: %1 with data: %2", _event, _data]; + +switch (_event) do { + case "actor::get::actions": { GVAR(ActorClass) call ["getNearbyActions", [_control]]; }; + // case "actor::open::bank": { [] spawn EFUNC(bank,openUI); }; + case "actor::open::bank": { [] spawn EFUNC(bank,openUI); }; + case "actor::open::device": { hint "Device interaction is not yet implemented."; }; // TODO: Implement device interaction + case "actor::open::garage": { hint "Garage interaction is not yet implemented."; }; // TODO: Implement garage interaction + case "actor::open::org": { [] spawn EFUNC(org,openUI); }; + case "actor::open::locker": { hint "Locker interaction is not yet implemented."; }; // TODO: Implement locker interaction + // case "actor::open::phone": { [] spawn EFUNC(phone,openUI) }; + case "actor::open::phone": { hint "Phone interaction is not yet implemented."; }; // TODO: Implement phone interaction + case "actor::open::iplayer": { hint "Player interaction is not yet implemented." }; // TODO: Implement player interaction + case "actor::open::store": { hint "Store interaction is not yet implemented."; }; // TODO: Implement store interaction + default { hint format ["Unhandled UI event: %1", _event]; }; +}; + +if (_event isNotEqualTo "actor::get::actions") then { _display closeDisplay 1; }; + +true; diff --git a/arma/client/addons/actor/functions/fnc_initActorClass.sqf b/arma/client/addons/actor/functions/fnc_initActorClass.sqf new file mode 100644 index 0000000..b0119ec --- /dev/null +++ b/arma/client/addons/actor/functions/fnc_initActorClass.sqf @@ -0,0 +1,165 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Initializes the actor class. + * + * Arguments: + * None + * + * Return Value: + * None + * + * Examples: + * [] call forge_client_actor_fnc_initActorClass + * + * Public: Yes + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(ActorClass) = createHashMapObject [[ + ["#type", "IActorClass"], + ["#create", { + _self set ["uid", getPlayerUID player]; + _self set ["actor", createHashMap]; + _self set ["isLoaded", false]; + _self set ["lastSave", time]; + + private _actor = createHashMap; + _actor set ["uid", (getPlayerUID player)]; + _actor set ["name", (name player)]; + _actor set ["loadout", [[],[],[],["U_BG_Guerrilla_6_1",[]],[],[],"H_Cap_blk_ION","",[],["ItemMap","ItemGPS","ItemRadio","ItemCompass","ItemWatch",""]]]; + _actor set ["position", (getPosASL player)]; + _actor set ["direction", (getDir player)]; + _actor set ["stance", (stance player)]; + _actor set ["rank", (rank player)]; + _actor set ["state", (lifeState player)]; + _actor set ["phone_number", ""]; + _actor set ["email", ""]; + _actor set ["organization", ""]; + _actor set ["holster", true]; + + _self set ["actor", _actor]; + }], + ["init", { + private _uid = _self get "uid"; + private _actor = _self get "actor"; + + [SRPC(actor,requestInitActor), [_uid, _actor]] call CFUNC(serverEvent); + + systemChat format ["Actor loaded for %1", (name player)]; + diag_log "[FORGE:Client:Actor] Actor Class Initialized!"; + }], + ["save", { + params [["_sync", false, [false]]]; + + private _uid = _self get "uid"; + [SRPC(actor,requestSaveActor), [_uid, _sync]] call CFUNC(serverEvent); + + _self set ["lastSave", time]; + }], + ["sync", { + params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]]; + + private _actor = _self get "actor"; + private _isLoaded = _self get "isLoaded"; + + if !(_isLoaded) then { _self set ["isLoaded", true]; }; + if (_data isEqualTo createHashMap) exitWith { + diag_log "[FORGE:Client:Actor] Empty data received for sync, skipping."; + }; + + { + _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_actorIsLoaded,true); + diag_log "[FORGE:Client:Actor] Sync completed"; + }], + ["get", { + params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]]; + + private _actor = _self get "actor"; + _actor getOrDefault [_key, _default]; + }], + ["applyPosition", { + 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", { + private _direction = _self call ["get", ["direction", 0]]; + + if (GVAR(enableLoc)) then { player setDir _direction; }; + }], + ["applyStance", { + private _stance = _self call ["get", ["stance", "STAND"]]; + + if (GVAR(enableLoc)) then { player playAction _stance; }; + }], + ["applyRank", { + private _rank = _self call ["get", ["rank", "PRIVATE"]]; + + player setUnitRank _rank; + }], + ["applyLoadout", { + private _loadout = _self call ["get", ["loadout", []]]; + + if (GVAR(enableGear) && count _loadout > 0) then { player setUnitLoadout _loadout; }; + }], + ["getNearbyActions", { + params [["_control", controlNull, [controlNull]]]; + + private _nearbyActions = []; + + { + private _storeType = _x getVariable ["storeType", ""]; + private _isBank = _x getVariable ["isBank", false]; + private _isGarage = _x getVariable ["isGarage", false]; + private _isLocker = _x getVariable ["isLocker", false]; + private _garageType = _x getVariable ["garageType", ""]; + private _deviceType = _x getVariable ["deviceType", ""]; + private _isPlayer = _x isKindOf "Man" && isPlayer _x; + + if (_storeType isNotEqualTo "") then { _nearbyActions pushBack ["store", _storeType]; }; + if (_isBank) then { _nearbyActions pushBack ["bank", true]; }; + if (_isLocker) then { _nearbyActions pushBack ["locker", true]; }; + if (_isLocker && GVAR(enableVA)) then { _nearbyActions pushBack ["va", true]; }; + if (_isGarage) then { _nearbyActions pushBack ["garage", _garageType]; }; + if (_isGarage && GVAR(enableVG)) then { _nearbyActions pushBack ["vg", true]; }; + if (_deviceType isNotEqualTo "") then { _nearbyActions pushBack ["device", _deviceType]; }; + if (_isPlayer) then { _nearbyActions pushBack ["player", name _x]; }; + } forEach (player nearObjects 5); + + _control ctrlWebBrowserAction ["ExecJS", format ["updateAvailableActions(%1)", (toJSON _nearbyActions)]]; + }] +]]; + +SETVAR(player,FORGE_ActorClass,GVAR(ActorClass)); +GVAR(ActorClass) diff --git a/arma/client/addons/actor/functions/fnc_openUI.sqf b/arma/client/addons/actor/functions/fnc_openUI.sqf new file mode 100644 index 0000000..3517f5c --- /dev/null +++ b/arma/client/addons/actor/functions/fnc_openUI.sqf @@ -0,0 +1,31 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Opens the player interaction interface. + * + * Arguments: + * None + * + * Return Value: + * None + * + * Example: + * [] call forge_client_actor_fnc_openUI; + * + * Public: No + */ + +private _display = (findDisplay 46) createDisplay "RscActorMenu"; +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; diff --git a/arma/client/addons/actor/initKeybinds.inc.sqf b/arma/client/addons/actor/initKeybinds.inc.sqf new file mode 100644 index 0000000..20eb498 --- /dev/null +++ b/arma/client/addons/actor/initKeybinds.inc.sqf @@ -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; diff --git a/arma/client/addons/actor/initSettings.inc.sqf b/arma/client/addons/actor/initSettings.inc.sqf new file mode 100644 index 0000000..3ff3391 --- /dev/null +++ b/arma/client/addons/actor/initSettings.inc.sqf @@ -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, false, true +] call CBA_fnc_addSetting; + +[ + QGVAR(enableVG), "CHECKBOX", + [LSTRING(enableVG), LSTRING(enableVGTooltip)], + _category, false, true +] call CBA_fnc_addSetting; diff --git a/arma/client/addons/actor/script_component.hpp b/arma/client/addons/actor/script_component.hpp new file mode 100644 index 0000000..95c04da --- /dev/null +++ b/arma/client/addons/actor/script_component.hpp @@ -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" diff --git a/arma/client/addons/actor/stringtable.xml b/arma/client/addons/actor/stringtable.xml new file mode 100644 index 0000000..6e50634 --- /dev/null +++ b/arma/client/addons/actor/stringtable.xml @@ -0,0 +1,38 @@ + + + + + Actor + + + Persistent Gear + + + Enable Persistent Gear + + + Persistent Location + + + Enable Persistent Location + + + Virtual Arsenal + + + Enable Virtual Arsenal + + + Virtual Garage + + + Enable Virtual Garage + + + Interaction Menu + + + Open your interaction menu + + + diff --git a/arma/client/addons/actor/ui/RscActorMenu.hpp b/arma/client/addons/actor/ui/RscActorMenu.hpp new file mode 100644 index 0000000..7b45e60 --- /dev/null +++ b/arma/client/addons/actor/ui/RscActorMenu.hpp @@ -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}; + }; + }; +}; diff --git a/arma/client/addons/actor/ui/RscCommon.hpp b/arma/client/addons/actor/ui/RscCommon.hpp new file mode 100644 index 0000000..8b57936 --- /dev/null +++ b/arma/client/addons/actor/ui/RscCommon.hpp @@ -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; diff --git a/arma/client/addons/actor/ui/_site/garage.css b/arma/client/addons/actor/ui/_site/garage.css new file mode 100644 index 0000000..a5c1ef7 --- /dev/null +++ b/arma/client/addons/actor/ui/_site/garage.css @@ -0,0 +1,605 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + height: 100vh; + width: 100vw; + background: rgba(0, 0, 0, 0.7); + font-family: Arial, sans-serif; + color: rgba(200, 220, 240, 0.95); + overflow: hidden; +} + +.garage-container { + height: 100vh; + width: 100vw; + padding: 2rem; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +/* Header Section */ +.garage-header { + display: flex; + align-items: center; + gap: 1.5rem; + padding: 1.25rem 1.5rem; + background: rgba(15, 20, 30, 0.9); + border: 1px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + box-shadow: + 0 0 20px rgba(100, 150, 200, 0.15), + 0 4px 16px rgba(0, 0, 0, 0.8); +} + +.garage-logo { + width: 60px; + height: 60px; + background: rgba(20, 30, 45, 0.8); + border: 2px solid rgba(100, 150, 200, 0.5); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; +} + +.logo-icon { + font-size: 2rem; +} + +.garage-info { + flex: 1; +} + +.garage-title { + font-size: 1.5rem; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + color: rgba(200, 220, 255, 1); + margin-bottom: 0.25rem; +} + +.garage-subtitle { + font-size: 0.875rem; + color: rgba(140, 160, 180, 0.8); + letter-spacing: 0.5px; +} + +.garage-stats { + display: flex; + gap: 1.5rem; +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + padding: 0.75rem 1.25rem; + background: rgba(20, 30, 45, 0.7); + border: 1px solid rgba(100, 150, 200, 0.3); + border-radius: 4px; +} + +.stat-label { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(140, 160, 180, 0.8); +} + +.stat-value { + font-size: 1.25rem; + font-weight: 600; + color: rgba(100, 200, 150, 1); +} + +.header-actions { + display: flex; + gap: 0.75rem; +} + +.action-btn { + padding: 0.625rem 1.25rem; + background: rgba(20, 30, 45, 0.7); + border: 1px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + color: rgba(200, 220, 240, 0.95); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + transition: all 0.15s ease; +} + +.action-btn:hover { + background: rgba(30, 45, 70, 0.9); + border-color: rgba(150, 200, 255, 0.7); + box-shadow: + 0 0 15px rgba(100, 150, 200, 0.2), + inset 0 0 20px rgba(100, 150, 200, 0.05); +} + +.close-btn { + border-color: rgba(200, 100, 100, 0.4); +} + +.close-btn:hover { + border-color: rgba(255, 100, 100, 0.7); + box-shadow: + 0 0 15px rgba(200, 100, 100, 0.2), + inset 0 0 20px rgba(200, 100, 100, 0.05); +} + +/* Main Content */ +.garage-content { + flex: 1; + display: grid; + grid-template-columns: 250px 1fr 350px; + gap: 1.5rem; + overflow: hidden; +} + +/* Panels */ +.garage-panel { + background: rgba(15, 20, 30, 0.9); + border: 1px solid rgba(100, 150, 200, 0.4); + border-left: 3px solid rgba(100, 150, 200, 0.5); + border-radius: 4px; + display: flex; + flex-direction: column; + box-shadow: + 0 0 20px rgba(100, 150, 200, 0.1), + 0 4px 16px rgba(0, 0, 0, 0.6); +} + +.panel-header { + padding: 1.25rem 1.5rem; + border-bottom: 1px solid rgba(100, 150, 200, 0.2); +} + +.panel-title { + font-size: 1rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(200, 220, 255, 1); +} + +.panel-content { + flex: 1; + padding: 1.5rem; + overflow-y: auto; +} + +/* Custom Scrollbar */ +.panel-content::-webkit-scrollbar { + width: 8px; +} + +.panel-content::-webkit-scrollbar-track { + background: rgba(15, 20, 30, 0.5); + border-radius: 4px; +} + +.panel-content::-webkit-scrollbar-thumb { + background: rgba(100, 150, 200, 0.3); + border-radius: 4px; +} + +.panel-content::-webkit-scrollbar-thumb:hover { + background: rgba(100, 150, 200, 0.5); +} + +/* Filters */ +.filter-section { + margin-bottom: 2rem; +} + +.filter-section:last-child { + margin-bottom: 0; +} + +.filter-title { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(160, 180, 200, 0.85); + margin-bottom: 0.75rem; +} + +.filter-buttons { + display: flex; + gap: 0.5rem; +} + +.filter-btn { + flex: 1; + padding: 0.625rem; + background: rgba(20, 30, 45, 0.7); + border: 1px solid rgba(100, 150, 200, 0.3); + border-radius: 4px; + color: rgba(200, 220, 240, 0.95); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + transition: all 0.15s ease; +} + +.filter-btn:hover { + background: rgba(30, 45, 70, 0.8); + border-color: rgba(150, 200, 255, 0.5); +} + +.filter-btn.active { + background: rgba(100, 150, 200, 0.2); + border-color: rgba(100, 150, 200, 0.6); + box-shadow: + 0 0 10px rgba(100, 150, 200, 0.15), + inset 0 0 15px rgba(100, 150, 200, 0.05); +} + +.type-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.type-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.875rem; + background: rgba(20, 30, 45, 0.7); + border: 1px solid rgba(100, 150, 200, 0.3); + border-left: 3px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + cursor: pointer; + transition: all 0.15s ease; + text-align: left; +} + +.type-item:hover { + background: rgba(30, 45, 70, 0.8); + border-left-color: rgba(150, 200, 255, 0.7); +} + +.type-item.active { + background: rgba(30, 45, 70, 0.9); + border-left-color: rgba(100, 200, 150, 0.8); + box-shadow: + 0 0 15px rgba(100, 200, 150, 0.15), + inset 0 0 20px rgba(100, 200, 150, 0.05); +} + +.type-icon { + font-size: 1.5rem; +} + +.type-name { + font-size: 0.875rem; + color: rgba(200, 220, 240, 0.95); +} + +.search-input { + width: 100%; + padding: 0.75rem 1rem; + background: rgba(20, 30, 45, 0.7); + border: 1px solid rgba(100, 150, 200, 0.3); + border-radius: 4px; + color: rgba(200, 220, 240, 0.95); + font-size: 0.875rem; + transition: all 0.15s ease; +} + +.search-input:focus { + outline: none; + border-color: rgba(150, 200, 255, 0.6); + box-shadow: + 0 0 15px rgba(100, 150, 200, 0.15), + inset 0 0 20px rgba(100, 150, 200, 0.05); +} + +.search-input::placeholder { + color: rgba(100, 120, 140, 0.6); +} + +/* Vehicles Grid */ +.vehicles-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1rem; +} + +.vehicle-card { + background: rgba(20, 30, 45, 0.6); + border: 1px solid rgba(100, 150, 200, 0.3); + border-left: 3px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + padding: 1.25rem; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.vehicle-card:hover { + background: rgba(30, 45, 70, 0.7); + border-left-color: rgba(150, 200, 255, 0.7); + box-shadow: + 0 0 15px rgba(100, 150, 200, 0.15), + inset 0 0 20px rgba(100, 150, 200, 0.05); +} + +.vehicle-card.selected { + background: rgba(30, 45, 70, 0.8); + border-left-color: rgba(100, 200, 150, 0.8); + box-shadow: + 0 0 20px rgba(100, 200, 150, 0.2), + inset 0 0 25px rgba(100, 200, 150, 0.05); +} + +.vehicle-icon { + font-size: 3rem; + text-align: center; +} + +.vehicle-name { + font-size: 0.95rem; + font-weight: 600; + color: rgba(200, 220, 255, 1); + text-align: center; +} + +.vehicle-type { + font-size: 0.75rem; + color: rgba(140, 160, 180, 0.85); + text-align: center; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.vehicle-status { + padding: 0.375rem; + background: rgba(100, 150, 200, 0.2); + border: 1px solid rgba(100, 150, 200, 0.3); + border-radius: 3px; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.5px; + text-align: center; + font-weight: 600; +} + +.vehicle-status.stored { + background: rgba(100, 150, 200, 0.2); + border-color: rgba(100, 150, 200, 0.4); + color: rgba(150, 200, 255, 0.9); +} + +.vehicle-status.active { + background: rgba(100, 200, 150, 0.2); + border-color: rgba(100, 200, 150, 0.4); + color: rgba(150, 255, 200, 0.9); +} + +/* Vehicle Details */ +.no-selection { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + padding: 3rem 1rem; +} + +.no-selection-icon { + font-size: 4rem; + opacity: 0.3; +} + +.no-selection p { + font-size: 0.875rem; + color: rgba(140, 160, 180, 0.7); +} + +.vehicle-details { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.detail-header { + display: flex; + align-items: center; + gap: 1rem; + padding: 1.25rem; + background: rgba(20, 30, 45, 0.6); + border: 1px solid rgba(100, 150, 200, 0.3); + border-left: 3px solid rgba(100, 150, 200, 0.5); + border-radius: 4px; +} + +.detail-icon { + font-size: 3rem; +} + +.detail-info { + flex: 1; +} + +.detail-name { + font-size: 1.125rem; + font-weight: 600; + color: rgba(200, 220, 255, 1); + margin-bottom: 0.25rem; +} + +.detail-type { + font-size: 0.75rem; + color: rgba(140, 160, 180, 0.85); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.detail-stats { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; +} + +.detail-stat { + padding: 0.875rem; + background: rgba(20, 30, 45, 0.6); + border: 1px solid rgba(100, 150, 200, 0.3); + border-radius: 4px; + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.detail-label { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(140, 160, 180, 0.85); +} + +.detail-value { + font-size: 0.95rem; + font-weight: 600; + color: rgba(200, 220, 240, 0.95); +} + +.detail-actions { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.detail-btn { + padding: 1rem; + background: rgba(20, 30, 45, 0.7); + border: 1px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + cursor: pointer; + transition: all 0.15s ease; +} + +.detail-btn:hover { + background: rgba(30, 45, 70, 0.9); + border-color: rgba(150, 200, 255, 0.7); + box-shadow: + 0 0 15px rgba(100, 150, 200, 0.2), + inset 0 0 20px rgba(100, 150, 200, 0.05); +} + +.spawn-btn { + background: rgba(100, 150, 200, 0.2); + border-color: rgba(100, 150, 200, 0.5); +} + +.spawn-btn:hover { + background: rgba(100, 150, 200, 0.3); + border-color: rgba(150, 200, 255, 0.7); +} + +.store-btn { + background: rgba(200, 150, 100, 0.2); + border-color: rgba(200, 150, 100, 0.4); +} + +.store-btn:hover { + background: rgba(200, 150, 100, 0.3); + border-color: rgba(255, 200, 150, 0.6); +} + +.btn-icon { + font-size: 1.25rem; +} + +.btn-text { + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(200, 220, 240, 0.95); +} + +.detail-specs { + padding: 1.25rem; + background: rgba(20, 30, 45, 0.5); + border: 1px solid rgba(100, 150, 200, 0.2); + border-radius: 4px; +} + +.specs-title { + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(180, 200, 220, 0.9); + margin-bottom: 1rem; +} + +.specs-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.spec-item { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 0.75rem; + border-bottom: 1px solid rgba(100, 150, 200, 0.15); +} + +.spec-item:last-child { + padding-bottom: 0; + border-bottom: none; +} + +.spec-label { + font-size: 0.8rem; + color: rgba(140, 160, 180, 0.85); +} + +.spec-value { + font-size: 0.875rem; + font-weight: 600; + color: rgba(200, 220, 240, 0.95); +} + +/* Responsive */ +@media (max-width: 1400px) { + .garage-content { + grid-template-columns: 220px 1fr 320px; + } + + .vehicles-grid { + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + } +} + +@media (max-width: 1200px) { + .garage-content { + grid-template-columns: 1fr 350px; + } + + .filters-panel { + display: none; + } +} diff --git a/arma/client/addons/actor/ui/_site/garage.html b/arma/client/addons/actor/ui/_site/garage.html new file mode 100644 index 0000000..7942d3d --- /dev/null +++ b/arma/client/addons/actor/ui/_site/garage.html @@ -0,0 +1,205 @@ + + + + + + + Vehicle Garage + + + + + + +
+ +
+ +
+

Vehicle Garage

+

Vehicle Management System

+
+
+
+ Stored + 12 +
+
+ Active + 2 +
+
+ Capacity + 20 +
+
+
+ +
+
+ + +
+ +
+
+

Filters

+
+
+ +
+

Status

+
+ + + +
+
+ + +
+

Vehicle Type

+
+ + + + + +
+
+ + +
+

Search

+ +
+
+
+ + +
+
+

Your Vehicles

+
+
+
+ +
+
+
+ + +
+
+

Vehicle Details

+
+
+
+
🚗
+

Select a vehicle to view details

+
+ + +
+
+
+
+ + + + + diff --git a/arma/client/addons/actor/ui/_site/garage.js b/arma/client/addons/actor/ui/_site/garage.js new file mode 100644 index 0000000..6094cac --- /dev/null +++ b/arma/client/addons/actor/ui/_site/garage.js @@ -0,0 +1,317 @@ +/** + * Vehicle Garage Interface + * Handles vehicle management with spawn and store actions + */ + +// Mock data - sample vehicles +const mockData = { + vehicles: [ + // Cars + { id: 1, name: "Sedan", type: "car", icon: "🚗", status: "stored", condition: 95, fuel: 80, location: "Garage A", seats: 4, speed: "180 km/h", cargo: "200 kg" }, + { id: 2, name: "Sports Car", type: "car", icon: "🏎️", status: "stored", condition: 100, fuel: 100, location: "Garage A", seats: 2, speed: "250 km/h", cargo: "50 kg" }, + { id: 3, name: "SUV", type: "car", icon: "🚙", status: "active", condition: 85, fuel: 60, location: "In Use", seats: 6, speed: "160 km/h", cargo: "400 kg" }, + { id: 4, name: "Hatchback", type: "car", icon: "🚗", status: "stored", condition: 90, fuel: 75, location: "Garage B", seats: 4, speed: "170 km/h", cargo: "250 kg" }, + + // Trucks + { id: 5, name: "Pickup Truck", type: "truck", icon: "🚛", status: "stored", condition: 88, fuel: 70, location: "Garage A", seats: 2, speed: "140 km/h", cargo: "800 kg" }, + { id: 6, name: "Delivery Van", type: "truck", icon: "🚚", status: "stored", condition: 92, fuel: 85, location: "Garage B", seats: 3, speed: "130 km/h", cargo: "1200 kg" }, + { id: 7, name: "Heavy Truck", type: "truck", icon: "🚛", status: "active", condition: 75, fuel: 50, location: "In Use", seats: 2, speed: "120 km/h", cargo: "2000 kg" }, + { id: 8, name: "Box Truck", type: "truck", icon: "📦", status: "stored", condition: 80, fuel: 65, location: "Garage A", seats: 3, speed: "110 km/h", cargo: "1500 kg" }, + + // Aircraft + { id: 9, name: "Helicopter", type: "air", icon: "🚁", status: "stored", condition: 95, fuel: 90, location: "Helipad", seats: 6, speed: "280 km/h", cargo: "500 kg" }, + { id: 10, name: "Light Plane", type: "air", icon: "✈️", status: "stored", condition: 100, fuel: 100, location: "Hangar", seats: 4, speed: "320 km/h", cargo: "300 kg" }, + + // Boats + { id: 11, name: "Speedboat", type: "sea", icon: "🚤", status: "stored", condition: 93, fuel: 80, location: "Marina", seats: 4, speed: "100 km/h", cargo: "150 kg" }, + { id: 12, name: "Yacht", type: "sea", icon: "🛥️", status: "stored", condition: 98, fuel: 95, location: "Marina", seats: 12, speed: "60 km/h", cargo: "800 kg" } + ] +}; + +// State +let selectedVehicle = null; +let statusFilter = 'all'; +let typeFilter = 'all'; +let searchQuery = ''; + +// Icons by type +const typeIcons = { + car: '🚗', + truck: '🚛', + air: '🚁', + sea: '🚤' +}; + +// Initialize +function initGarage() { + console.log('Garage interface initializing...'); + + setupEventHandlers(); + renderVehicles(); + updateStats(); + + console.log('Garage interface initialized'); +} + +// Event Handlers +function setupEventHandlers() { + // Close button + const closeBtn = document.querySelector('.close-btn'); + if (closeBtn) { + closeBtn.addEventListener('click', () => { + console.log('Closing garage...'); + sendEvent('garage::close', {}); + }); + } + + // Status filters + const filterBtns = document.querySelectorAll('.filter-btn'); + filterBtns.forEach(btn => { + btn.addEventListener('click', () => { + filterBtns.forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + statusFilter = btn.dataset.filter; + renderVehicles(); + }); + }); + + // Type filters + const typeItems = document.querySelectorAll('.type-item'); + typeItems.forEach(item => { + item.addEventListener('click', () => { + typeItems.forEach(i => i.classList.remove('active')); + item.classList.add('active'); + typeFilter = item.dataset.type; + renderVehicles(); + }); + }); + + // Search + const searchInput = document.getElementById('searchInput'); + if (searchInput) { + searchInput.addEventListener('input', (e) => { + searchQuery = e.target.value.toLowerCase(); + renderVehicles(); + }); + } + + // Spawn button + const spawnBtn = document.getElementById('spawnBtn'); + if (spawnBtn) { + spawnBtn.addEventListener('click', () => { + if (selectedVehicle) { + spawnVehicle(selectedVehicle); + } + }); + } + + // Store button + const storeBtn = document.getElementById('storeBtn'); + if (storeBtn) { + storeBtn.addEventListener('click', () => { + if (selectedVehicle) { + storeVehicle(selectedVehicle); + } + }); + } +} + +// Render vehicles +function renderVehicles() { + const vehiclesGrid = document.getElementById('vehiclesGrid'); + if (!vehiclesGrid) return; + + vehiclesGrid.innerHTML = ''; + + // Filter vehicles + let filtered = mockData.vehicles; + + // Status filter + if (statusFilter !== 'all') { + filtered = filtered.filter(v => v.status === statusFilter); + } + + // Type filter + if (typeFilter !== 'all') { + filtered = filtered.filter(v => v.type === typeFilter); + } + + // Search filter + if (searchQuery) { + filtered = filtered.filter(v => + v.name.toLowerCase().includes(searchQuery) || + v.type.toLowerCase().includes(searchQuery) + ); + } + + // Render vehicles + filtered.forEach(vehicle => { + const card = document.createElement('div'); + card.className = 'vehicle-card'; + if (selectedVehicle && selectedVehicle.id === vehicle.id) { + card.classList.add('selected'); + } + + card.innerHTML = ` +
${vehicle.icon}
+
${vehicle.name}
+
${vehicle.type}
+
${vehicle.status}
+ `; + + card.addEventListener('click', () => selectVehicle(vehicle)); + vehiclesGrid.appendChild(card); + }); + + console.log(`Rendered ${filtered.length} vehicles`); +} + +// Select vehicle +function selectVehicle(vehicle) { + selectedVehicle = vehicle; + + // Update selected state in grid + document.querySelectorAll('.vehicle-card').forEach(card => { + card.classList.remove('selected'); + }); + event.currentTarget.classList.add('selected'); + + // Show details + showVehicleDetails(vehicle); +} + +// Show vehicle details +function showVehicleDetails(vehicle) { + const noSelection = document.getElementById('noSelection'); + const vehicleDetails = document.getElementById('vehicleDetails'); + const spawnBtn = document.getElementById('spawnBtn'); + const storeBtn = document.getElementById('storeBtn'); + + if (noSelection) noSelection.style.display = 'none'; + if (vehicleDetails) vehicleDetails.style.display = 'flex'; + + // Update details + document.getElementById('detailIcon').textContent = vehicle.icon; + document.getElementById('detailName').textContent = vehicle.name; + document.getElementById('detailType').textContent = vehicle.type; + document.getElementById('detailStatus').textContent = vehicle.status; + document.getElementById('detailCondition').textContent = `${vehicle.condition}%`; + document.getElementById('detailFuel').textContent = `${vehicle.fuel}%`; + document.getElementById('detailLocation').textContent = vehicle.location; + document.getElementById('detailSeats').textContent = vehicle.seats; + document.getElementById('detailSpeed').textContent = vehicle.speed; + document.getElementById('detailCargo').textContent = vehicle.cargo; + + // Show/hide action buttons based on status + if (vehicle.status === 'stored') { + spawnBtn.style.display = 'flex'; + storeBtn.style.display = 'none'; + } else { + spawnBtn.style.display = 'none'; + storeBtn.style.display = 'flex'; + } +} + +// Spawn vehicle +function spawnVehicle(vehicle) { + console.log('Spawning vehicle:', vehicle.name); + + // Update local state + vehicle.status = 'active'; + vehicle.location = 'In Use'; + + sendEvent('garage::spawn', { + vehicleId: vehicle.id, + vehicleName: vehicle.name, + vehicleType: vehicle.type + }); + + // Re-render + renderVehicles(); + updateStats(); + if (selectedVehicle && selectedVehicle.id === vehicle.id) { + showVehicleDetails(vehicle); + } +} + +// Store vehicle +function storeVehicle(vehicle) { + console.log('Storing vehicle:', vehicle.name); + + // Update local state + vehicle.status = 'stored'; + vehicle.location = 'Garage A'; + + sendEvent('garage::store', { + vehicleId: vehicle.id, + vehicleName: vehicle.name, + vehicleType: vehicle.type + }); + + // Re-render + renderVehicles(); + updateStats(); + if (selectedVehicle && selectedVehicle.id === vehicle.id) { + showVehicleDetails(vehicle); + } +} + +// Update stats +function updateStats() { + const stored = mockData.vehicles.filter(v => v.status === 'stored').length; + const active = mockData.vehicles.filter(v => v.status === 'active').length; + const capacity = mockData.vehicles.length + 6; // Mock capacity + + document.getElementById('storedCount').textContent = stored; + document.getElementById('activeCount').textContent = active; + document.getElementById('capacityCount').textContent = capacity; +} + +// Update garage data from external source +function updateGarageData(data) { + if (data.vehicles) { + mockData.vehicles = data.vehicles; + renderVehicles(); + updateStats(); + + // Update selected vehicle if it still exists + if (selectedVehicle) { + const updated = mockData.vehicles.find(v => v.id === selectedVehicle.id); + if (updated) { + selectedVehicle = updated; + showVehicleDetails(updated); + } else { + selectedVehicle = null; + const noSelection = document.getElementById('noSelection'); + const vehicleDetails = document.getElementById('vehicleDetails'); + if (noSelection) noSelection.style.display = 'flex'; + if (vehicleDetails) vehicleDetails.style.display = 'none'; + } + } + } +} + +// Send event to Arma +function sendEvent(event, data) { + if (typeof A3API !== 'undefined') { + A3API.SendAlert(JSON.stringify({ + event: event, + data: data + })); + } else { + console.log('Event:', event, 'Data:', data); + } +} + +// Auto-initialize +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initGarage); +} else { + initGarage(); +} + +// Expose functions globally +window.updateGarageData = updateGarageData; +window.selectVehicle = selectVehicle; +window.spawnVehicle = spawnVehicle; +window.storeVehicle = storeVehicle; diff --git a/arma/client/addons/actor/ui/_site/index.html b/arma/client/addons/actor/ui/_site/index.html new file mode 100644 index 0000000..8b482e8 --- /dev/null +++ b/arma/client/addons/actor/ui/_site/index.html @@ -0,0 +1,70 @@ + + + + + + + Interaction Menu + + + + + + +
+
+
+ +
+
+
+ + + + + + diff --git a/arma/client/addons/actor/ui/_site/script.js b/arma/client/addons/actor/ui/_site/script.js new file mode 100644 index 0000000..7f0a62f --- /dev/null +++ b/arma/client/addons/actor/ui/_site/script.js @@ -0,0 +1,424 @@ +/** + * Redux-like Pattern for Actor Menu Management + */ + +//============================================================================= +// #region ACTIONS +//============================================================================= + +// Action Types +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", +}; + +// Action Creators +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: "bank", + title: "Banking Services", + description: "Access your bank account and manage finances", + icon: "", + action: "actor::open::bank", + }, + { + id: "phone", + title: "Personal Phone", + description: "Access and manage your personal phone", + icon: "", + action: "actor::open::phone", + }, + { + id: "org", + title: "Organization Dashboard", + description: "View and manage your organization data", + icon: "", + action: "actor::open::org", + }, +]; + +const actionDefinitions = { + device: { + id: "device", + title: "Device Interaction", + description: "Manage devices and settings", + icon: "", + action: "actor::open::device", + }, + garage: { + id: "garage", + title: "Vehicle Garage", + description: "Access and manage your vehicle collection", + icon: "", + action: "actor::open::garage", + }, + locker: { + id: "locker", + title: "Locker", + description: "Access your personal locker for storage", + icon: "", + action: "actor::open::locker", + }, + player: { + id: "player", + title: "Player Interaction", + description: "Interact with player-specific actions", + icon: "", + action: "actor::open::iplayer", + }, + store: { + id: "store", + title: "Store", + description: "Browse and purchase items from the store", + icon: "", + action: "actor::open::store", + }, + va: { + id: "va", + title: "Virtual Arsenal", + description: "Access your virtual arsenal", + icon: "", + action: "actor::open::arsenal", + }, + vg: { + id: "vg", + title: "Virtual Garage", + description: "Access your virtual garage", + icon: "", + 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]; + + // Process available actions + 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[value]; + if (definition) { + newMenuItems.push(definition); + } 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)); + } + + subscribe(listener) { + this.listeners.push(listener); + return () => { + this.listeners = this.listeners.filter((l) => l !== listener); + }; + } +} + +// Create store instance +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 (Redux-connected) +//============================================================================= + +class ActorUI { + constructor(store) { + this.store = store; + this.unsubscribe = null; + } + + init() { + console.log("ActorUI initializing..."); + + // Subscribe to state changes + this.unsubscribe = this.store.subscribe((state) => { + this.render(state); + }); + + // Initial render + this.render(this.store.getState()); + + // Request initial data + this.requestInitialData(); + + console.log("ActorUI initialized successfully"); + } + + render(state) { + this.updateMenuDisplay(state); + } + + updateMenuDisplay(state) { + const grid = document.getElementById("menuGrid"); + if (!grid) { + console.error("Menu grid element not found"); + return; + } + + // Clear existing menu items + grid.innerHTML = ""; + + // Render menu items + const menuItems = selectors.getMenuItems(state); + menuItems.forEach((item) => { + const menuItem = document.createElement("div"); + menuItem.className = "neu-menu-item"; + menuItem.setAttribute("data-action", item.action); + menuItem.innerHTML = ` +
${item.icon}
+
${item.title}
+
${item.description}
+ `; + menuItem.addEventListener("click", () => + this.handleMenuItemClick(item), + ); + + grid.appendChild(menuItem); + }); + + console.log(`Rendered ${menuItems.length} menu items`); + } + + handleMenuItemClick(item) { + console.log("Menu item clicked:", item); + const alert = { + event: item.action, + data: {}, + }; + A3API.SendAlert(JSON.stringify(alert)); + } + + requestInitialData() { + console.log("Requesting initial actor data..."); + const alert = { + event: "actor::get::actions", + data: {}, + }; + A3API.SendAlert(JSON.stringify(alert)); + } + + destroy() { + if (this.unsubscribe) { + this.unsubscribe(); + } + } +} + +//============================================================================= +// #region DATA HANDLERS (Redux-connected) +//============================================================================= + +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 ACTION HANDLERS +//============================================================================= + +function handleMenuItemClick(item) { + console.log("Legacy menu item click handler:", item); + const alert = { + event: item.action, + data: {}, + }; + A3API.SendAlert(JSON.stringify(alert)); +} + +//============================================================================= +// #region INITIALIZATION FUNCTIONS +//============================================================================= + +// Global flag to prevent double initialization +let actorUIInitialized = false; + +/** + * Initialize the actor interface - called from HTML after script loads + */ +function initializeMenu() { + console.log("initializeMenu() called"); + + if (actorUIInitialized) { + console.log("ActorUI already initialized, skipping..."); + return; + } + + // Check if DOM is ready + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => { + if (!actorUIInitialized) { + console.log("DOM loaded, initializing ActorUI..."); + window.actorUI = new ActorUI(store); + window.actorUI.init(); + actorUIInitialized = true; + } + }); + } else { + // DOM is already ready + console.log("DOM already ready, initializing ActorUI..."); + window.actorUI = new ActorUI(store); + window.actorUI.init(); + actorUIInitialized = true; + } +} + +//============================================================================= +// #region GLOBAL VARIABLES +//============================================================================= + +// Make actorUI globally accessible +let actorUI; + +// Auto-initialize if DOM is already loaded when script executes +if (document.readyState !== "loading") { + console.log("Script loaded after DOM ready, auto-initializing..."); + if (!actorUIInitialized) { + actorUI = new ActorUI(store); + actorUI.init(); + actorUIInitialized = true; + } +} else { + // Wait for DOM to be ready + document.addEventListener("DOMContentLoaded", () => { + if (!actorUIInitialized) { + console.log("DOM loaded, initializing ActorUI..."); + actorUI = new ActorUI(store); + actorUI.init(); + actorUIInitialized = true; + } + }); +} diff --git a/arma/client/addons/actor/ui/_site/script.js.bak b/arma/client/addons/actor/ui/_site/script.js.bak new file mode 100644 index 0000000..288057e --- /dev/null +++ b/arma/client/addons/actor/ui/_site/script.js.bak @@ -0,0 +1,417 @@ +/** + * Redux-like Pattern for Actor Menu Management + */ + +//============================================================================= +// #region ACTIONS +//============================================================================= + +// Action Types +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", +}; + +// Action Creators +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: "bank", + title: "Banking Services", + description: "Access your bank account and manage finances", + icon: "", + action: "actor::open::bank", + }, + { + id: "phone", + title: "Personal Phone", + description: "Access and manage your personal phone", + icon: "", + action: "actor::open::phone", + }, +]; + +const actionDefinitions = { + device: { + id: "device", + title: "Device Interaction", + description: "Manage devices and settings", + icon: "", + action: "actor::open::device", + }, + garage: { + id: "garage", + title: "Vehicle Garage", + description: "Access and manage your vehicle collection", + icon: "", + action: "actor::open::garage", + }, + locker: { + id: "locker", + title: "Locker", + description: "Access your personal locker for storage", + icon: "", + action: "actor::open::locker", + }, + player: { + id: "player", + title: "Player Interaction", + description: "Interact with player-specific actions", + icon: "", + action: "actor::open::iplayer", + }, + store: { + id: "store", + title: "Store", + description: "Browse and purchase items from the store", + icon: "", + action: "actor::open::store", + }, + va: { + id: "va", + title: "Virtual Arsenal", + description: "Access your virtual arsenal", + icon: "", + action: "actor::open::arsenal", + }, + vg: { + id: "vg", + title: "Virtual Garage", + description: "Access your virtual garage", + icon: "", + 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]; + + // Process available actions + 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[value]; + if (definition) { + newMenuItems.push(definition); + } 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)); + } + + subscribe(listener) { + this.listeners.push(listener); + return () => { + this.listeners = this.listeners.filter((l) => l !== listener); + }; + } +} + +// Create store instance +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 (Redux-connected) +//============================================================================= + +class ActorUI { + constructor(store) { + this.store = store; + this.unsubscribe = null; + } + + init() { + console.log("ActorUI initializing..."); + + // Subscribe to state changes + this.unsubscribe = this.store.subscribe((state) => { + this.render(state); + }); + + // Initial render + this.render(this.store.getState()); + + // Request initial data + this.requestInitialData(); + + console.log("ActorUI initialized successfully"); + } + + render(state) { + this.updateMenuDisplay(state); + } + + updateMenuDisplay(state) { + const grid = document.getElementById("menuGrid"); + if (!grid) { + console.error("Menu grid element not found"); + return; + } + + // Clear existing menu items + grid.innerHTML = ""; + + // Render menu items + const menuItems = selectors.getMenuItems(state); + menuItems.forEach((item) => { + const menuItem = document.createElement("div"); + menuItem.className = "neu-menu-item"; + menuItem.setAttribute("data-action", item.action); + menuItem.innerHTML = ` +
${item.icon}
+
${item.title}
+
${item.description}
+ `; + menuItem.addEventListener("click", () => + this.handleMenuItemClick(item), + ); + + grid.appendChild(menuItem); + }); + + console.log(`Rendered ${menuItems.length} menu items`); + } + + handleMenuItemClick(item) { + console.log("Menu item clicked:", item); + const alert = { + event: item.action, + data: {}, + }; + A3API.SendAlert(JSON.stringify(alert)); + } + + requestInitialData() { + console.log("Requesting initial actor data..."); + const alert = { + event: "actor::get::actions", + data: {}, + }; + A3API.SendAlert(JSON.stringify(alert)); + } + + destroy() { + if (this.unsubscribe) { + this.unsubscribe(); + } + } +} + +//============================================================================= +// #region DATA HANDLERS (Redux-connected) +//============================================================================= + +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 ACTION HANDLERS +//============================================================================= + +function handleMenuItemClick(item) { + console.log("Legacy menu item click handler:", item); + const alert = { + event: item.action, + data: {}, + }; + A3API.SendAlert(JSON.stringify(alert)); +} + +//============================================================================= +// #region INITIALIZATION FUNCTIONS +//============================================================================= + +// Global flag to prevent double initialization +let actorUIInitialized = false; + +/** + * Initialize the actor interface - called from HTML after script loads + */ +function initializeMenu() { + console.log("initializeMenu() called"); + + if (actorUIInitialized) { + console.log("ActorUI already initialized, skipping..."); + return; + } + + // Check if DOM is ready + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => { + if (!actorUIInitialized) { + console.log("DOM loaded, initializing ActorUI..."); + window.actorUI = new ActorUI(store); + window.actorUI.init(); + actorUIInitialized = true; + } + }); + } else { + // DOM is already ready + console.log("DOM already ready, initializing ActorUI..."); + window.actorUI = new ActorUI(store); + window.actorUI.init(); + actorUIInitialized = true; + } +} + +//============================================================================= +// #region GLOBAL VARIABLES +//============================================================================= + +// Make actorUI globally accessible +let actorUI; + +// Auto-initialize if DOM is already loaded when script executes +if (document.readyState !== "loading") { + console.log("Script loaded after DOM ready, auto-initializing..."); + if (!actorUIInitialized) { + actorUI = new ActorUI(store); + actorUI.init(); + actorUIInitialized = true; + } +} else { + // Wait for DOM to be ready + document.addEventListener("DOMContentLoaded", () => { + if (!actorUIInitialized) { + console.log("DOM loaded, initializing ActorUI..."); + actorUI = new ActorUI(store); + actorUI.init(); + actorUIInitialized = true; + } + }); +} diff --git a/arma/client/addons/actor/ui/_site/store.css b/arma/client/addons/actor/ui/_site/store.css new file mode 100644 index 0000000..4974a9b --- /dev/null +++ b/arma/client/addons/actor/ui/_site/store.css @@ -0,0 +1,567 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + height: 100vh; + width: 100vw; + background: rgba(0, 0, 0, 0.7); + font-family: Arial, sans-serif; + color: rgba(200, 220, 240, 0.95); + overflow: hidden; +} + +.store-container { + height: 100vh; + width: 100vw; + padding: 2rem; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +/* Header Section */ +.store-header { + display: flex; + align-items: center; + gap: 1.5rem; + padding: 1.25rem 1.5rem; + background: rgba(15, 20, 30, 0.9); + border: 1px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + box-shadow: + 0 0 20px rgba(100, 150, 200, 0.15), + 0 4px 16px rgba(0, 0, 0, 0.8); +} + +.store-logo { + width: 60px; + height: 60px; + background: rgba(20, 30, 45, 0.8); + border: 2px solid rgba(100, 150, 200, 0.5); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; +} + +.logo-icon { + font-size: 2rem; +} + +.store-info { + flex: 1; +} + +.store-title { + font-size: 1.5rem; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + color: rgba(200, 220, 255, 1); + margin-bottom: 0.25rem; +} + +.store-subtitle { + font-size: 0.875rem; + color: rgba(140, 160, 180, 0.8); + letter-spacing: 0.5px; +} + +.balance-display { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.25rem; + padding: 0.75rem 1.25rem; + background: rgba(20, 30, 45, 0.7); + border: 1px solid rgba(100, 200, 150, 0.4); + border-radius: 4px; +} + +.balance-label { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(140, 160, 180, 0.8); +} + +.balance-amount { + font-size: 1.25rem; + font-weight: 600; + color: rgba(100, 200, 150, 1); +} + +.header-actions { + display: flex; + gap: 0.75rem; +} + +.action-btn { + padding: 0.625rem 1.25rem; + background: rgba(20, 30, 45, 0.7); + border: 1px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + color: rgba(200, 220, 240, 0.95); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + transition: all 0.15s ease; +} + +.action-btn:hover { + background: rgba(30, 45, 70, 0.9); + border-color: rgba(150, 200, 255, 0.7); + box-shadow: + 0 0 15px rgba(100, 150, 200, 0.2), + inset 0 0 20px rgba(100, 150, 200, 0.05); +} + +.cart-btn { + display: flex; + align-items: center; + gap: 0.5rem; + position: relative; +} + +.cart-icon { + font-size: 1.25rem; +} + +.cart-count { + min-width: 24px; + height: 24px; + padding: 0 0.5rem; + background: rgba(100, 150, 200, 0.3); + border: 1px solid rgba(100, 150, 200, 0.5); + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 600; +} + +.close-btn { + border-color: rgba(200, 100, 100, 0.4); +} + +.close-btn:hover { + border-color: rgba(255, 100, 100, 0.7); + box-shadow: + 0 0 15px rgba(200, 100, 100, 0.2), + inset 0 0 20px rgba(200, 100, 100, 0.05); +} + +/* Main Content */ +.store-content { + flex: 1; + display: grid; + grid-template-columns: 250px 1fr; + gap: 1.5rem; + overflow: hidden; +} + +.store-content.cart-open { + grid-template-columns: 250px 1fr 350px; +} + +/* Panels */ +.store-panel { + background: rgba(15, 20, 30, 0.9); + border: 1px solid rgba(100, 150, 200, 0.4); + border-left: 3px solid rgba(100, 150, 200, 0.5); + border-radius: 4px; + display: flex; + flex-direction: column; + box-shadow: + 0 0 20px rgba(100, 150, 200, 0.1), + 0 4px 16px rgba(0, 0, 0, 0.6); +} + +.panel-header { + padding: 1.25rem 1.5rem; + border-bottom: 1px solid rgba(100, 150, 200, 0.2); + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.panel-title { + font-size: 1rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(200, 220, 255, 1); +} + +.panel-content { + flex: 1; + padding: 1.5rem; + overflow-y: auto; +} + +/* Custom Scrollbar */ +.panel-content::-webkit-scrollbar { + width: 8px; +} + +.panel-content::-webkit-scrollbar-track { + background: rgba(15, 20, 30, 0.5); + border-radius: 4px; +} + +.panel-content::-webkit-scrollbar-thumb { + background: rgba(100, 150, 200, 0.3); + border-radius: 4px; +} + +.panel-content::-webkit-scrollbar-thumb:hover { + background: rgba(100, 150, 200, 0.5); +} + +/* Search Box */ +.search-box { + flex: 1; + max-width: 300px; +} + +.search-input { + width: 100%; + padding: 0.625rem 1rem; + background: rgba(20, 30, 45, 0.7); + border: 1px solid rgba(100, 150, 200, 0.3); + border-radius: 4px; + color: rgba(200, 220, 240, 0.95); + font-size: 0.875rem; + transition: all 0.15s ease; +} + +.search-input:focus { + outline: none; + border-color: rgba(150, 200, 255, 0.6); + box-shadow: + 0 0 15px rgba(100, 150, 200, 0.15), + inset 0 0 20px rgba(100, 150, 200, 0.05); +} + +.search-input::placeholder { + color: rgba(100, 120, 140, 0.6); +} + +/* Category List */ +.category-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.category-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1rem; + background: rgba(20, 30, 45, 0.6); + border: 1px solid rgba(100, 150, 200, 0.3); + border-left: 3px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + color: rgba(200, 220, 240, 0.95); + cursor: pointer; + transition: all 0.15s ease; + text-align: left; +} + +.category-item:hover { + background: rgba(30, 45, 70, 0.7); + border-left-color: rgba(150, 200, 255, 0.7); +} + +.category-item.active { + background: rgba(30, 45, 70, 0.8); + border-left-color: rgba(100, 200, 150, 0.8); + box-shadow: + 0 0 15px rgba(100, 200, 150, 0.15), + inset 0 0 20px rgba(100, 200, 150, 0.05); +} + +.category-icon { + font-size: 1.5rem; +} + +.category-name { + flex: 1; + font-size: 0.875rem; + font-weight: 500; +} + +.category-count { + padding: 0.25rem 0.5rem; + background: rgba(100, 150, 200, 0.2); + border: 1px solid rgba(100, 150, 200, 0.3); + border-radius: 3px; + font-size: 0.75rem; + font-weight: 600; +} + +/* Items Grid */ +.items-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1rem; +} + +.item-card { + background: rgba(20, 30, 45, 0.6); + border: 1px solid rgba(100, 150, 200, 0.3); + border-left: 3px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + padding: 1.25rem; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.item-card:hover { + background: rgba(30, 45, 70, 0.7); + border-left-color: rgba(150, 200, 255, 0.7); + box-shadow: + 0 0 15px rgba(100, 150, 200, 0.15), + inset 0 0 20px rgba(100, 150, 200, 0.05); +} + +.item-icon { + font-size: 3rem; + text-align: center; +} + +.item-name { + font-size: 0.95rem; + font-weight: 600; + color: rgba(200, 220, 255, 1); + text-align: center; +} + +.item-description { + font-size: 0.75rem; + color: rgba(140, 160, 180, 0.85); + text-align: center; + line-height: 1.3; + min-height: 2.6rem; +} + +.item-price { + font-size: 1.125rem; + font-weight: 600; + color: rgba(100, 200, 150, 1); + text-align: center; + margin-top: auto; +} + +.item-actions { + display: flex; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.add-to-cart-btn { + flex: 1; + padding: 0.625rem; + background: rgba(100, 150, 200, 0.2); + border: 1px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + color: rgba(200, 220, 240, 0.95); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + transition: all 0.15s ease; +} + +.add-to-cart-btn:hover { + background: rgba(100, 150, 200, 0.3); + border-color: rgba(150, 200, 255, 0.6); +} + +/* Cart Panel */ +.cart-items { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 1.5rem; +} + +.empty-cart { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + padding: 3rem 1rem; +} + +.empty-icon { + font-size: 3rem; + opacity: 0.3; +} + +.empty-text { + font-size: 0.875rem; + color: rgba(140, 160, 180, 0.7); +} + +.cart-item { + padding: 1rem; + background: rgba(20, 30, 45, 0.6); + border: 1px solid rgba(100, 150, 200, 0.3); + border-left: 3px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; +} + +.cart-item-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.cart-item-name { + font-size: 0.875rem; + font-weight: 600; + color: rgba(200, 220, 255, 1); +} + +.cart-item-remove { + padding: 0.25rem 0.5rem; + background: rgba(200, 100, 100, 0.2); + border: 1px solid rgba(200, 100, 100, 0.4); + border-radius: 3px; + color: rgba(255, 150, 150, 0.9); + font-size: 0.7rem; + cursor: pointer; + transition: all 0.15s ease; +} + +.cart-item-remove:hover { + background: rgba(200, 100, 100, 0.3); +} + +.cart-item-details { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.8rem; + color: rgba(160, 180, 200, 0.85); +} + +.cart-item-price { + color: rgba(100, 200, 150, 1); + font-weight: 600; +} + +.clear-cart-btn { + padding: 0.5rem 0.75rem; + background: rgba(200, 100, 100, 0.2); + border: 1px solid rgba(200, 100, 100, 0.4); + border-radius: 4px; + color: rgba(255, 150, 150, 0.9); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + transition: all 0.15s ease; +} + +.clear-cart-btn:hover { + background: rgba(200, 100, 100, 0.3); +} + +/* Cart Summary */ +.cart-summary { + padding-top: 1.5rem; + border-top: 1px solid rgba(100, 150, 200, 0.2); +} + +.summary-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.summary-label { + font-size: 0.875rem; + color: rgba(160, 180, 200, 0.85); +} + +.summary-value { + font-size: 0.95rem; + font-weight: 600; + color: rgba(200, 220, 240, 0.95); +} + +.summary-total { + padding-top: 0.75rem; + border-top: 1px solid rgba(100, 150, 200, 0.2); + margin-top: 0.5rem; +} + +.summary-total .summary-label { + font-size: 1rem; + font-weight: 600; + color: rgba(200, 220, 255, 1); +} + +.summary-total .summary-value { + font-size: 1.25rem; + color: rgba(100, 200, 150, 1); +} + +.action-btn-primary { + width: 100%; + padding: 0.875rem; + margin-top: 1rem; + background: rgba(100, 150, 200, 0.2); + border: 1px solid rgba(100, 150, 200, 0.5); +} + +.action-btn-primary:hover { + background: rgba(100, 150, 200, 0.3); + border-color: rgba(150, 200, 255, 0.7); +} + +.checkout-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.checkout-btn:disabled:hover { + background: rgba(100, 150, 200, 0.2); + border-color: rgba(100, 150, 200, 0.5); + box-shadow: none; +} + +/* Responsive adjustments */ +@media (max-width: 1400px) { + .items-grid { + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + } +} + +@media (max-width: 1200px) { + .store-content { + grid-template-columns: 1fr; + } + + .store-content.cart-open { + grid-template-columns: 1fr 350px; + } + + .categories-panel { + display: none; + } +} diff --git a/arma/client/addons/actor/ui/_site/store.html b/arma/client/addons/actor/ui/_site/store.html new file mode 100644 index 0000000..debed7c --- /dev/null +++ b/arma/client/addons/actor/ui/_site/store.html @@ -0,0 +1,149 @@ + + + + + + + Store + + + + + + +
+ +
+ +
+

Supply Store

+

Equipment & Resources

+
+
+ Available Funds + $45,750 +
+
+ + +
+
+ + +
+ +
+
+

Categories

+
+
+
+ + + + + +
+
+
+ + +
+
+

Available Items

+ +
+
+
+ +
+
+
+ + + +
+
+ + + + + diff --git a/arma/client/addons/actor/ui/_site/store.js b/arma/client/addons/actor/ui/_site/store.js new file mode 100644 index 0000000..8d85756 --- /dev/null +++ b/arma/client/addons/actor/ui/_site/store.js @@ -0,0 +1,339 @@ +/** + * Store Interface + * Handles item browsing, cart management, and purchases + */ + +// Mock data +const mockData = { + balance: 45750, + items: [ + // Weapons + { id: 1, name: "Assault Rifle", category: "weapons", icon: "🔫", description: "Standard military-grade rifle", price: 2500 }, + { id: 2, name: "Sniper Rifle", category: "weapons", icon: "🎯", description: "Long-range precision weapon", price: 4500 }, + { id: 3, name: "SMG", category: "weapons", icon: "🔫", description: "Close-quarters combat", price: 1800 }, + { id: 4, name: "Pistol", category: "weapons", icon: "🔫", description: "Sidearm backup weapon", price: 800 }, + { id: 5, name: "Shotgun", category: "weapons", icon: "🔫", description: "Close-range powerhouse", price: 1500 }, + { id: 6, name: "LMG", category: "weapons", icon: "🔫", description: "Heavy suppression weapon", price: 3500 }, + { id: 7, name: "Grenade Launcher", category: "weapons", icon: "💣", description: "Explosive ordnance", price: 5000 }, + { id: 8, name: "Rocket Launcher", category: "weapons", icon: "🚀", description: "Anti-vehicle weapon", price: 8000 }, + + // Equipment + { id: 9, name: "Body Armor", category: "equipment", icon: "🎽", description: "Ballistic protection", price: 3000 }, + { id: 10, name: "Helmet", category: "equipment", icon: "⛑️", description: "Head protection", price: 1200 }, + { id: 11, name: "Night Vision", category: "equipment", icon: "🕶️", description: "See in the dark", price: 2500 }, + { id: 12, name: "GPS Device", category: "equipment", icon: "📡", description: "Navigation system", price: 800 }, + { id: 13, name: "Radio", category: "equipment", icon: "📻", description: "Team communication", price: 600 }, + { id: 14, name: "Backpack", category: "equipment", icon: "🎒", description: "Extra storage capacity", price: 500 }, + + // Medical + { id: 15, name: "First Aid Kit", category: "medical", icon: "💊", description: "Basic medical supplies", price: 400 }, + { id: 16, name: "Med Kit", category: "medical", icon: "⚕️", description: "Advanced medical kit", price: 1000 }, + { id: 17, name: "Bandages", category: "medical", icon: "🩹", description: "Stop bleeding", price: 150 }, + { id: 18, name: "Morphine", category: "medical", icon: "💉", description: "Pain management", price: 300 }, + { id: 19, name: "Blood Bag", category: "medical", icon: "🩸", description: "Restore blood level", price: 500 }, + + // Supplies + { id: 20, name: "Ammunition Box", category: "supplies", icon: "📦", description: "Mixed ammunition", price: 800 }, + { id: 21, name: "Explosive Charges", category: "supplies", icon: "💣", description: "Demolition supplies", price: 1500 }, + { id: 22, name: "Toolkit", category: "supplies", icon: "🔧", description: "Repair equipment", price: 600 }, + { id: 23, name: "Food Rations", category: "supplies", icon: "🥫", description: "Emergency supplies", price: 200 }, + { id: 24, name: "Water Canteen", category: "supplies", icon: "🧃", description: "Hydration supply", price: 150 } + ] +}; + +// State +let cart = []; +let selectedCategory = 'all'; +let searchQuery = ''; + +// Initialize +function initStore() { + console.log('Store interface initializing...'); + + setupEventHandlers(); + renderItems(); + updateBalance(); + + console.log('Store interface initialized'); +} + +// Event Handlers +function setupEventHandlers() { + // Close button + const closeBtn = document.querySelector('.close-btn'); + if (closeBtn) { + closeBtn.addEventListener('click', () => { + console.log('Closing store...'); + sendEvent('actor::close::store', {}); + }); + } + + // Cart toggle + const cartToggle = document.getElementById('cartToggle'); + const cartPanel = document.getElementById('cartPanel'); + const storeContent = document.querySelector('.store-content'); + + if (cartToggle && cartPanel) { + cartToggle.addEventListener('click', () => { + const isOpen = cartPanel.style.display !== 'none'; + cartPanel.style.display = isOpen ? 'none' : 'flex'; + storeContent.classList.toggle('cart-open', !isOpen); + }); + } + + // Category filters + const categoryItems = document.querySelectorAll('.category-item'); + categoryItems.forEach(item => { + item.addEventListener('click', () => { + categoryItems.forEach(c => c.classList.remove('active')); + item.classList.add('active'); + selectedCategory = item.dataset.category; + renderItems(); + }); + }); + + // Search + const searchInput = document.getElementById('searchInput'); + if (searchInput) { + searchInput.addEventListener('input', (e) => { + searchQuery = e.target.value.toLowerCase(); + renderItems(); + }); + } + + // Clear cart + const clearCartBtn = document.getElementById('clearCart'); + if (clearCartBtn) { + clearCartBtn.addEventListener('click', () => { + if (confirm('Clear all items from cart?')) { + cart = []; + renderCart(); + updateCartCount(); + } + }); + } + + // Checkout + const checkoutBtn = document.getElementById('checkoutBtn'); + if (checkoutBtn) { + checkoutBtn.addEventListener('click', handleCheckout); + } +} + +// Render items +function renderItems() { + const itemsGrid = document.getElementById('itemsGrid'); + if (!itemsGrid) return; + + itemsGrid.innerHTML = ''; + + // Filter items + let filteredItems = mockData.items; + + if (selectedCategory !== 'all') { + filteredItems = filteredItems.filter(item => item.category === selectedCategory); + } + + if (searchQuery) { + filteredItems = filteredItems.filter(item => + item.name.toLowerCase().includes(searchQuery) || + item.description.toLowerCase().includes(searchQuery) + ); + } + + // Render filtered items + filteredItems.forEach(item => { + const card = document.createElement('div'); + card.className = 'item-card'; + + card.innerHTML = ` +
${item.icon}
+
${item.name}
+
${item.description}
+
$${item.price.toLocaleString()}
+
+ +
+ `; + + const addBtn = card.querySelector('.add-to-cart-btn'); + addBtn.addEventListener('click', (e) => { + e.stopPropagation(); + addToCart(item); + }); + + itemsGrid.appendChild(card); + }); + + console.log(`Rendered ${filteredItems.length} items`); +} + +// Cart functions +function addToCart(item) { + const existingItem = cart.find(c => c.id === item.id); + + if (existingItem) { + existingItem.quantity++; + } else { + cart.push({ ...item, quantity: 1 }); + } + + renderCart(); + updateCartCount(); + + // Show cart panel if not visible + const cartPanel = document.getElementById('cartPanel'); + const storeContent = document.querySelector('.store-content'); + if (cartPanel.style.display === 'none') { + cartPanel.style.display = 'flex'; + storeContent.classList.add('cart-open'); + } + + console.log('Added to cart:', item.name); +} + +function removeFromCart(itemId) { + cart = cart.filter(item => item.id !== itemId); + renderCart(); + updateCartCount(); +} + +function renderCart() { + const cartItems = document.getElementById('cartItems'); + if (!cartItems) return; + + cartItems.innerHTML = ''; + + if (cart.length === 0) { + cartItems.innerHTML = ` +
+ 🛒 + Your cart is empty +
+ `; + } else { + cart.forEach(item => { + const cartItem = document.createElement('div'); + cartItem.className = 'cart-item'; + + cartItem.innerHTML = ` +
+ ${item.name} + +
+
+ Qty: ${item.quantity} + $${(item.price * item.quantity).toLocaleString()} +
+ `; + + const removeBtn = cartItem.querySelector('.cart-item-remove'); + removeBtn.addEventListener('click', () => removeFromCart(item.id)); + + cartItems.appendChild(cartItem); + }); + } + + updateCartSummary(); +} + +function updateCartCount() { + const cartCount = document.querySelector('.cart-count'); + if (cartCount) { + const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0); + cartCount.textContent = totalItems; + } +} + +function updateCartSummary() { + const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0); + const tax = subtotal * 0.05; + const total = subtotal + tax; + + document.getElementById('cartSubtotal').textContent = `$${subtotal.toLocaleString()}`; + document.getElementById('cartTax').textContent = `$${tax.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + document.getElementById('cartTotal').textContent = `$${total.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + + const checkoutBtn = document.getElementById('checkoutBtn'); + if (checkoutBtn) { + checkoutBtn.disabled = cart.length === 0 || total > mockData.balance; + } +} + +function handleCheckout() { + const total = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0); + const tax = total * 0.05; + const grandTotal = total + tax; + + if (grandTotal > mockData.balance) { + alert('Insufficient funds!'); + return; + } + + const purchaseData = { + items: cart.map(item => ({ + id: item.id, + name: item.name, + quantity: item.quantity, + price: item.price + })), + subtotal: total, + tax: tax, + total: grandTotal + }; + + console.log('Purchase request:', purchaseData); + sendEvent('actor::store::purchase', purchaseData); + + // Clear cart after purchase + cart = []; + renderCart(); + updateCartCount(); + + // Update balance (this would normally come from server) + mockData.balance -= grandTotal; + updateBalance(); +} + +function updateBalance() { + const balanceAmount = document.querySelector('.balance-amount'); + if (balanceAmount) { + balanceAmount.textContent = `$${mockData.balance.toLocaleString()}`; + } +} + +// Update store data from external source +function updateStoreData(data) { + if (data.balance !== undefined) { + mockData.balance = data.balance; + updateBalance(); + } + + if (data.items) { + mockData.items = data.items; + renderItems(); + } +} + +// Send event to Arma +function sendEvent(event, data) { + if (typeof A3API !== 'undefined') { + A3API.SendAlert(JSON.stringify({ + event: event, + data: data + })); + } else { + console.log('Event:', event, 'Data:', data); + } +} + +// Auto-initialize +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initStore); +} else { + initStore(); +} + +// Expose functions globally +window.updateStoreData = updateStoreData; +window.addToCart = addToCart; diff --git a/arma/client/addons/actor/ui/_site/style.css b/arma/client/addons/actor/ui/_site/style.css new file mode 100644 index 0000000..6bca60b --- /dev/null +++ b/arma/client/addons/actor/ui/_site/style.css @@ -0,0 +1,116 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + height: 100vh; + width: 100vw; + background: rgba(0, 0, 0, 0.5); + font-family: Arial, sans-serif; +} + +.container { + align-items: flex-end; + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + padding-right: 5%; + perspective: 1200px; +} + +.neu-menu { + background: rgba(15, 20, 30, 0.9); + border: 1px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + margin-right: 25%; + max-height: 640px; + width: 480px; + transform: rotateY(-10deg) translateZ(0); + transform-style: preserve-3d; + box-shadow: + -5px 0 15px rgba(100, 150, 200, 0.2), + 0 8px 32px rgba(0, 0, 0, 0.8); + + .neu-menu-content { + height: 100%; + overflow: hidden; + padding: 1rem; + + .neu-menu-grid { + display: grid; + max-height: 380px; + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: thin; + -webkit-scrollbar-width: thin; + + .neu-menu-item { + align-items: flex-start; + background: rgba(20, 30, 45, 0.7); + border-left: 3px solid rgba(100, 150, 200, 0.5); + border-radius: 2px; + color: rgba(200, 220, 240, 0.95); + display: flex; + flex-direction: column; + justify-content: center; + margin-bottom: 0.5rem; + min-height: 70px; + padding: 0.75rem 1rem; + text-align: left; + transition: all 0.15s ease; + position: relative; + + &::before { + content: ''; + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 3px; + background: rgba(100, 150, 200, 0.8); + opacity: 0; + transition: opacity 0.15s ease; + } + + &:last-child { + margin-bottom: 0 !important; + } + + &:hover { + background: rgba(30, 45, 70, 0.9); + border-left-color: rgba(150, 200, 255, 0.9); + box-shadow: + 0 0 20px rgba(100, 150, 200, 0.2), + inset 0 0 30px rgba(100, 150, 200, 0.05); + cursor: pointer; + + &::before { + opacity: 1; + } + } + + .neu-menu-item-description { + color: rgba(140, 160, 180, 0.85); + font-size: 0.8rem; + line-height: 1.3; + margin-top: 0.35rem; + } + + .neu-menu-item-icon { + display: none; + } + + .neu-menu-item-title { + color: rgba(200, 220, 255, 1); + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + } + } + } + } +} diff --git a/arma/client/addons/actor/ui/_site/style.css.bak b/arma/client/addons/actor/ui/_site/style.css.bak new file mode 100644 index 0000000..ad7d0ec --- /dev/null +++ b/arma/client/addons/actor/ui/_site/style.css.bak @@ -0,0 +1,186 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #3b82f6; + --primary-hover: #2563eb; + --secondary-color: #1e293b; + --background-color: rgba(15, 23, 42, 0.85); + --card-background: rgba(30, 41, 59, 0.95); + --text-primary: #f8fafc; + --text-secondary: #94a3b8; + --border-color: #334155; + --success-color: #16a34a; + --success-hover: #15803d; + --button-hover: #4f46e5; +} + +body { + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + sans-serif; + line-height: 1.6; + background-color: transparent; + color: var(--text-primary); + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +} + +.menu-container { + background-color: var(--background-color); + border-radius: 16px; + width: 90%; + max-width: 800px; + backdrop-filter: blur(10px); + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + overflow: hidden; + + .menu-header { + background-color: var(--secondary-color); + padding: 1.5rem; + border-bottom: 1px solid var(--border-color); + margin-bottom: 20px; + + h1 { + color: var(--text-primary); + font-size: 1.5rem; + font-weight: 600; + letter-spacing: -0.025em; + margin: 0; + } + } + + .menu-content { + padding: 1.5rem; + + .menu-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 1rem; + } + } +} + +.menu-item { + background-color: var(--card-background); + border-radius: 12px; + padding: 1.25rem; + transition: all 0.3s ease; + cursor: pointer; + border: 1px solid var(--border-color); + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + animation: fadeIn 0.3s ease; + + &:hover { + transform: translateY(-4px); + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + border-color: var(--primary-color); + } + + .menu-item-icon { + font-size: 2rem; + margin-bottom: 1rem; + background: linear-gradient( + 135deg, + var(--primary-color), + var(--button-hover) + ); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + + svg { + width: 24px; + height: 24px; + stroke: currentColor; + fill: none; + } + } + + .menu-item-title { + font-size: 1.1rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.5rem; + } + + .menu-item-description { + font-size: 0.875rem; + color: var(--text-secondary); + line-height: 1.4; + } +} + +.loading-state { + text-align: center; + padding: 40px 20px; + color: var(--text-secondary); + + .loading-spinner { + width: 40px; + height: 40px; + border: 4px solid rgba(248, 250, 252, 0.1); + border-top: 4px solid var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 20px; + } + + .loading-text { + font-size: 16px; + color: var(--text-secondary); + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 640px) { + .menu-container { + width: 95%; + margin: 1rem; + + .menu-content { + .menu-grid { + grid-template-columns: 1fr; + } + } + + .menu-header { + h1 { + font-size: 1.25rem; + } + } + } +} diff --git a/arma/client/addons/bank/$PBOPREFIX$ b/arma/client/addons/bank/$PBOPREFIX$ new file mode 100644 index 0000000..92cfac2 --- /dev/null +++ b/arma/client/addons/bank/$PBOPREFIX$ @@ -0,0 +1 @@ +forge\forge_client\addons\bank diff --git a/arma/client/addons/bank/CfgEventHandlers.hpp b/arma/client/addons/bank/CfgEventHandlers.hpp new file mode 100644 index 0000000..c6e25db --- /dev/null +++ b/arma/client/addons/bank/CfgEventHandlers.hpp @@ -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)); + }; +}; diff --git a/arma/client/addons/bank/README.md b/arma/client/addons/bank/README.md new file mode 100644 index 0000000..677cfc9 --- /dev/null +++ b/arma/client/addons/bank/README.md @@ -0,0 +1,4 @@ +forge_client_bank +=================== + +Description for this addon diff --git a/arma/client/addons/bank/XEH_PREP.hpp b/arma/client/addons/bank/XEH_PREP.hpp new file mode 100644 index 0000000..c6ce19a --- /dev/null +++ b/arma/client/addons/bank/XEH_PREP.hpp @@ -0,0 +1,3 @@ +PREP(handleUIEvents); +PREP(initBankClass); +PREP(openUI); diff --git a/arma/client/addons/bank/XEH_postInit.sqf b/arma/client/addons/bank/XEH_postInit.sqf new file mode 100644 index 0000000..421c54b --- /dev/null +++ b/arma/client/addons/bank/XEH_postInit.sqf @@ -0,0 +1 @@ +#include "script_component.hpp" diff --git a/arma/client/addons/bank/XEH_postInitClient.sqf b/arma/client/addons/bank/XEH_postInitClient.sqf new file mode 100644 index 0000000..92b0995 --- /dev/null +++ b/arma/client/addons/bank/XEH_postInitClient.sqf @@ -0,0 +1,28 @@ +#include "script_component.hpp" + +if (isNil QGVAR(BankClass)) then { [] call FUNC(initBankClass); }; + +[QGVAR(initBank), { + GVAR(BankClass) call ["init", []]; +}] call CFUNC(addEventHandler); + +[QGVAR(responseInitBank), { + params [["_data", createHashMap, [createHashMap]]]; + + GVAR(BankClass) call ["sync", [_data, true]]; + + SETPVAR(player,FORGE_isLoaded,true); + cutText ["", "PLAIN", 1]; +}] call CFUNC(addEventHandler); + +[QGVAR(responseSyncBank), { + params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]]; + + GVAR(BankClass) call ["sync", [_data, _jip]]; +}] call CFUNC(addEventHandler); + +[{ + EGVAR(org,OrgClass) get "isLoaded"; +}, { + [QGVAR(initBank), []] call CFUNC(localEvent); +}] call CFUNC(waitUntilAndExecute); diff --git a/arma/client/addons/bank/XEH_preInit.sqf b/arma/client/addons/bank/XEH_preInit.sqf new file mode 100644 index 0000000..640756c --- /dev/null +++ b/arma/client/addons/bank/XEH_preInit.sqf @@ -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" diff --git a/arma/client/addons/bank/XEH_preInitClient.sqf b/arma/client/addons/bank/XEH_preInitClient.sqf new file mode 100644 index 0000000..421c54b --- /dev/null +++ b/arma/client/addons/bank/XEH_preInitClient.sqf @@ -0,0 +1 @@ +#include "script_component.hpp" diff --git a/arma/client/addons/bank/XEH_preStart.sqf b/arma/client/addons/bank/XEH_preStart.sqf new file mode 100644 index 0000000..0228885 --- /dev/null +++ b/arma/client/addons/bank/XEH_preStart.sqf @@ -0,0 +1,3 @@ +#include "script_component.hpp" + +#include "XEH_PREP.hpp" diff --git a/arma/client/addons/bank/config.cpp b/arma/client/addons/bank/config.cpp new file mode 100644 index 0000000..efc69e3 --- /dev/null +++ b/arma/client/addons/bank/config.cpp @@ -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\RscBank.hpp" diff --git a/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf b/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf new file mode 100644 index 0000000..6ac4f9a --- /dev/null +++ b/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf @@ -0,0 +1,33 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Handles the UI events. + * + * Arguments: + * None + * + * Return Value: + * None + * + * Example: + * [] call forge_client_bank_fnc_handleUIEvents; + * + * Public: No + */ + +params ["_control", "_isConfirmDialog", "_message"]; + +private _alert = fromJSON _message; +private _event = _alert get "event"; +private _data = _alert get "data"; +private _display = displayChild findDisplay 46; + +diag_log format ["[FORGE:Client:Bank] Handling UI event: %1 with data: %2", _event, _data]; + +switch (_event) do { + case "bank::close": { _display closeDisplay 1; }; + default { hint format ["Unhandled UI event: %1", _event]; }; +}; + +true; diff --git a/arma/client/addons/bank/functions/fnc_initBankClass.sqf b/arma/client/addons/bank/functions/fnc_initBankClass.sqf new file mode 100644 index 0000000..edd6258 --- /dev/null +++ b/arma/client/addons/bank/functions/fnc_initBankClass.sqf @@ -0,0 +1,88 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Initializes the bank class. + * + * Arguments: + * None + * + * Return Value: + * None + * + * Examples: + * [] call forge_client_bank_fnc_initBankClass + * + * Public: Yes + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(BankClass) = createHashMapObject [[ + ["#type", "IBankClass"], + ["#create", { + _self set ["uid", getPlayerUID player]; + _self set ["account", createHashMap]; + _self set ["isLoaded", false]; + _self set ["lastSave", time]; + + private _actor = EGVAR(actor,ActorClass) get "actor"; + private _phone_number = _actor get "phone_number"; + private _email = _actor get "email"; + + private _account = createHashMap; + _account set ["uid", (getPlayerUID player)]; + _account set ["name", (name player)]; + _account set ["bank", 0]; + _account set ["cash", 0]; + _account set ["earnings", 0]; + _account set ["transactions", []]; + _account set ["phone_number", _phone_number]; + _account set ["email", _email]; + + _self set ["account", _account]; + }], + ["init", { + private _uid = _self get "uid"; + private _account = _self get "account"; + + [SRPC(bank,requestInitBank), [_uid, _account]] call CFUNC(serverEvent); + + systemChat format ["Bank loaded for %1", (name player)]; + diag_log "[FORGE:Client:Bank] Bank Class Initialized!"; + }], + ["save", { + params [["_sync", false, [false]]]; + + private _uid = _self get "uid"; + [SRPC(bank,requestSaveBank), [_uid, _sync]] call CFUNC(serverEvent); + + _self set ["lastSave", time]; + }], + ["sync", { + params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]]; + + private _account = _self get "account"; + private _isLoaded = _self get "isLoaded"; + + if !(_isLoaded) then { _self set ["isLoaded", true]; }; + if (_data isEqualTo createHashMap) exitWith { + diag_log "[FORGE:Client:Bank] Empty data received for sync, skipping."; + }; + + { + _account set [_x, _y]; + } forEach _data; + + _self set ["account", _account]; + diag_log "[FORGE:Client:Bank] Sync completed"; + }], + ["get", { + params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]]; + + private _account = _self get "account"; + _account getOrDefault [_key, _default]; + }] +]]; + +SETVAR(player,FORGE_BankClass,GVAR(BankClass)); +GVAR(BankClass) diff --git a/arma/client/addons/bank/functions/fnc_openUI.sqf b/arma/client/addons/bank/functions/fnc_openUI.sqf new file mode 100644 index 0000000..bf40ab3 --- /dev/null +++ b/arma/client/addons/bank/functions/fnc_openUI.sqf @@ -0,0 +1,31 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Opens the player bank interaction interface. + * + * Arguments: + * None + * + * Return Value: + * None + * + * Example: + * [] call forge_client_bank_fnc_openUI; + * + * Public: No + */ + +private _display = (findDisplay 46) createDisplay "RscBank"; +private _ctrl = (_display displayCtrl 1002); + +_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; diff --git a/arma/client/addons/bank/initKeybinds.inc.sqf b/arma/client/addons/bank/initKeybinds.inc.sqf new file mode 100644 index 0000000..2922c52 --- /dev/null +++ b/arma/client/addons/bank/initKeybinds.inc.sqf @@ -0,0 +1 @@ +#include "\forge\forge_client\addons\main\data\hpp\defineDIKCodes.hpp" diff --git a/arma/client/addons/bank/initSettings.inc.sqf b/arma/client/addons/bank/initSettings.inc.sqf new file mode 100644 index 0000000..416ff52 --- /dev/null +++ b/arma/client/addons/bank/initSettings.inc.sqf @@ -0,0 +1 @@ +// Can use localize "STR_ACE_Common_Enabled" for name if ACE is required diff --git a/arma/client/addons/bank/script_component.hpp b/arma/client/addons/bank/script_component.hpp new file mode 100644 index 0000000..61a928d --- /dev/null +++ b/arma/client/addons/bank/script_component.hpp @@ -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" diff --git a/arma/client/addons/bank/stringtable.xml b/arma/client/addons/bank/stringtable.xml new file mode 100644 index 0000000..a995b52 --- /dev/null +++ b/arma/client/addons/bank/stringtable.xml @@ -0,0 +1,8 @@ + + + + + Bank + + + diff --git a/arma/client/addons/bank/ui/RscBank.hpp b/arma/client/addons/bank/ui/RscBank.hpp new file mode 100644 index 0000000..dc9ec5b --- /dev/null +++ b/arma/client/addons/bank/ui/RscBank.hpp @@ -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}; + }; + }; +}; diff --git a/arma/client/addons/bank/ui/RscCommon.hpp b/arma/client/addons/bank/ui/RscCommon.hpp new file mode 100644 index 0000000..8b57936 --- /dev/null +++ b/arma/client/addons/bank/ui/RscCommon.hpp @@ -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; diff --git a/arma/client/addons/bank/ui/_site/atm.css b/arma/client/addons/bank/ui/_site/atm.css new file mode 100644 index 0000000..4d7ba74 --- /dev/null +++ b/arma/client/addons/bank/ui/_site/atm.css @@ -0,0 +1,585 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + height: 100vh; + width: 100vw; + background: rgba(0, 0, 0, 0.5); + font-family: Arial, sans-serif; + color: rgba(200, 220, 240, 0.95); + overflow: hidden; +} + +.atm-container { + height: 100vh; + width: 100vw; + display: flex; + align-items: center; + justify-content: flex-end; + padding-right: 5%; + perspective: 1200px; +} + +.atm-screen { + width: 480px; + height: 640px; + background: rgba(15, 20, 30, 0.95); + border: 2px solid rgba(100, 150, 200, 0.5); + border-radius: 8px; + transform: rotateY(-10deg) translateZ(0); + transform-style: preserve-3d; + box-shadow: + -8px 0 20px rgba(100, 150, 200, 0.25), + 0 8px 32px rgba(0, 0, 0, 0.8); + display: grid; + grid-template-rows: auto 1fr auto; + overflow: hidden; + margin-right: 25%; +} + +/* Header */ +.atm-header { + padding: 1.25rem 1.5rem; + background: rgba(20, 30, 45, 0.9); + border-bottom: 2px solid rgba(100, 150, 200, 0.3); + display: flex; + align-items: center; + gap: 1rem; +} + +.atm-logo { + font-size: 2rem; +} + +.atm-title { + font-size: 1rem; + font-weight: 600; + letter-spacing: 1px; + text-transform: uppercase; + color: rgba(100, 150, 200, 1); +} + +/* Content */ +.atm-content { + flex: 1; + padding: 1.5rem; + overflow-y: auto; + overflow-x: hidden; +} + +.atm-content::-webkit-scrollbar { + width: 6px; +} + +.atm-content::-webkit-scrollbar-track { + background: rgba(15, 20, 30, 0.5); +} + +.atm-content::-webkit-scrollbar-thumb { + background: rgba(100, 150, 200, 0.3); + border-radius: 3px; +} + +.atm-view { + display: flex; + flex-direction: column; + gap: 1.5rem; + height: 100%; + justify-content: space-between; +} + +.atm-view h3 { + font-size: 1.125rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(200, 220, 255, 1); + text-align: center; + padding-bottom: 1rem; + border-bottom: 1px solid rgba(100, 150, 200, 0.2); +} + +/* Welcome Screen */ +.welcome-message { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + padding: 2rem 1rem; + flex: 1; +} + +.welcome-icon { + font-size: 4rem; + opacity: 0.6; +} + +.welcome-message h2 { + font-size: 1.5rem; + font-weight: 600; + color: rgba(200, 220, 255, 1); +} + +.welcome-message p { + font-size: 0.875rem; + color: rgba(140, 160, 180, 0.85); +} + +/* PIN Entry */ +.pin-entry { + display: flex; + flex-direction: column; + gap: 1.5rem; + flex: 1; + justify-content: center; +} + +.pin-entry h3 { + margin: 0; + padding: 0; + border: none; +} + +.pin-display { + display: flex; + justify-content: center; + gap: 1rem; + padding: 1.5rem; +} + +.pin-dot { + width: 16px; + height: 16px; + border-radius: 50%; + background: rgba(100, 150, 200, 0.2); + border: 2px solid rgba(100, 150, 200, 0.4); + transition: all 0.2s ease; +} + +.pin-dot.filled { + background: rgba(100, 150, 200, 0.8); + border-color: rgba(150, 200, 255, 0.8); + box-shadow: 0 0 10px rgba(100, 150, 200, 0.5); +} + +.keypad { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem; +} + +.key-btn { + padding: 1rem; + background: rgba(20, 30, 45, 0.7); + border: 1px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + color: rgba(200, 220, 240, 0.95); + font-size: 1.125rem; + font-weight: 600; + cursor: pointer; + transition: all 0.15s ease; +} + +.key-btn:hover { + background: rgba(30, 45, 70, 0.9); + border-color: rgba(150, 200, 255, 0.7); + box-shadow: + 0 0 15px rgba(100, 150, 200, 0.2), + inset 0 0 20px rgba(100, 150, 200, 0.05); +} + +.key-btn:active { + transform: scale(0.95); +} + +.key-clear { + background: rgba(200, 150, 100, 0.2); + border-color: rgba(200, 150, 100, 0.4); +} + +.key-clear:hover { + background: rgba(200, 150, 100, 0.3); + border-color: rgba(255, 200, 150, 0.6); +} + +.key-enter { + background: rgba(100, 150, 200, 0.2); + border-color: rgba(100, 150, 200, 0.5); +} + +.key-enter:hover { + background: rgba(100, 150, 200, 0.3); + border-color: rgba(150, 200, 255, 0.7); +} + +/* Account Summary */ +.account-summary { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + flex-shrink: 0; +} + +.summary-item { + padding: 1.25rem; + background: rgba(20, 30, 45, 0.6); + border: 1px solid rgba(100, 150, 200, 0.3); + border-left: 3px solid rgba(100, 150, 200, 0.5); + border-radius: 4px; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.summary-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(140, 160, 180, 0.85); +} + +.summary-value { + font-size: 1.25rem; + font-weight: 600; + color: rgba(100, 200, 150, 1); +} + +/* Menu Options */ +.menu-options { + display: grid; + grid-template-rows: 1fr; + gap: 1rem; +} + +.menu-btn { + padding: 1rem; + background: rgba(20, 30, 45, 0.6); + border: 1px solid rgba(100, 150, 200, 0.3); + border-left: 3px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + cursor: pointer; + transition: all 0.15s ease; +} + +.menu-btn:hover { + background: rgba(30, 45, 70, 0.8); + border-left-color: rgba(150, 200, 255, 0.7); + box-shadow: + 0 0 15px rgba(100, 150, 200, 0.15), + inset 0 0 20px rgba(100, 150, 200, 0.05); +} + +.menu-icon { + font-size: 2rem; +} + +.menu-text { + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(200, 220, 240, 0.95); +} + +/* Quick Amounts */ +.withdraw-display, +.deposit-display, +.transfer-display { + flex: 1; + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.quick-amounts { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; +} + +.amount-btn { + padding: 1rem; + background: rgba(20, 30, 45, 0.7); + border: 1px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + color: rgba(100, 200, 150, 1); + font-size: 1.125rem; + font-weight: 600; + cursor: pointer; + transition: all 0.15s ease; +} + +.amount-btn:hover { + background: rgba(30, 45, 70, 0.9); + border-color: rgba(150, 200, 255, 0.6); + box-shadow: + 0 0 15px rgba(100, 150, 200, 0.15), + inset 0 0 20px rgba(100, 150, 200, 0.05); +} + +/* Custom Amount */ +.custom-amount { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.custom-amount label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(140, 160, 180, 0.85); +} + +/* Form Fields */ +.transfer-form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.form-field { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.form-field label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(140, 160, 180, 0.85); +} + +.amount-input, +.text-input { + padding: 0.875rem 1rem; + background: rgba(20, 30, 45, 0.7); + border: 1px solid rgba(100, 150, 200, 0.3); + border-radius: 4px; + color: rgba(200, 220, 240, 0.95); + font-size: 1rem; + transition: all 0.15s ease; +} + +.amount-input:focus, +.text-input:focus { + outline: none; + border-color: rgba(150, 200, 255, 0.6); + box-shadow: + 0 0 15px rgba(100, 150, 200, 0.15), + inset 0 0 20px rgba(100, 150, 200, 0.05); +} + +.amount-input::placeholder, +.text-input::placeholder { + color: rgba(100, 120, 140, 0.6); +} + +/* Balance Display */ +.balance-display { + display: flex; + flex-direction: column; + gap: 1rem; + flex: 1; +} + +.balance-item { + padding: 1.25rem; + background: rgba(20, 30, 45, 0.6); + border: 1px solid rgba(100, 150, 200, 0.3); + border-left: 3px solid rgba(100, 150, 200, 0.5); + border-radius: 4px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.balance-label { + font-size: 0.875rem; + color: rgba(160, 180, 200, 0.85); +} + +.balance-amount { + font-size: 1.25rem; + font-weight: 600; + color: rgba(100, 200, 150, 1); +} + +.balance-total { + border-left-color: rgba(100, 200, 150, 0.6); + background: rgba(30, 45, 70, 0.7); +} + +.balance-total .balance-label { + font-size: 1rem; + font-weight: 600; + color: rgba(200, 220, 255, 1); +} + +.balance-total .balance-amount { + font-size: 1.5rem; +} + +/* Deposit Info */ +.atm-btn-group { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.deposit-info { + padding: 1rem; + background: rgba(20, 30, 45, 0.5); + border: 1px solid rgba(100, 150, 200, 0.2); + border-radius: 4px; + text-align: center; +} + +.deposit-info p { + font-size: 0.875rem; + color: rgba(160, 180, 200, 0.85); +} + +.deposit-info span { + font-weight: 600; + color: rgba(100, 200, 150, 1); +} + +/* Transaction Result */ +.transaction-result { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + padding: 2rem 1rem; + flex: 1; +} + +.result-icon { + width: 80px; + height: 80px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 3rem; + font-weight: bold; +} + +.transaction-result.success .result-icon { + background: rgba(100, 200, 150, 0.2); + border: 3px solid rgba(100, 200, 150, 0.6); + color: rgba(150, 255, 200, 1); + box-shadow: 0 0 20px rgba(100, 200, 150, 0.3); +} + +.transaction-result.error .result-icon { + background: rgba(200, 100, 100, 0.2); + border: 3px solid rgba(200, 100, 100, 0.6); + color: rgba(255, 150, 150, 1); + box-shadow: 0 0 20px rgba(200, 100, 100, 0.3); +} + +.transaction-result h3 { + margin: 0; + padding: 0; + border: none; +} + +.transaction-result p { + font-size: 0.875rem; + color: rgba(160, 180, 200, 0.85); + text-align: center; +} + +/* Buttons */ +.atm-btn { + padding: 1rem; + background: rgba(20, 30, 45, 0.7); + border: 1px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + color: rgba(200, 220, 240, 0.95); + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + transition: all 0.15s ease; +} + +.atm-btn:hover { + background: rgba(30, 45, 70, 0.9); + border-color: rgba(150, 200, 255, 0.7); + box-shadow: + 0 0 15px rgba(100, 150, 200, 0.2), + inset 0 0 20px rgba(100, 150, 200, 0.05); +} + +.atm-btn-primary { + background: rgba(100, 150, 200, 0.2); + border-color: rgba(100, 150, 200, 0.5); +} + +.atm-btn-primary:hover { + background: rgba(100, 150, 200, 0.3); + border-color: rgba(150, 200, 255, 0.7); +} + +.atm-btn-secondary { + background: rgba(200, 150, 100, 0.2); + border-color: rgba(200, 150, 100, 0.4); +} + +.atm-btn-secondary:hover { + background: rgba(200, 150, 100, 0.3); + border-color: rgba(255, 200, 150, 0.6); +} + +.atm-btn-full { + background: rgba(100, 200, 150, 0.2); + border-color: rgba(100, 200, 150, 0.4); +} + +.atm-btn-full:hover { + background: rgba(100, 200, 150, 0.3); + border-color: rgba(150, 255, 200, 0.6); +} + +/* Footer */ +.atm-footer { + padding: 1rem 1.5rem; + background: rgba(20, 30, 45, 0.9); + border-top: 2px solid rgba(100, 150, 200, 0.3); + text-align: center; +} + +.footer-text { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 1px; + color: rgba(100, 150, 200, 0.7); +} + +/* Responsive */ +@media (max-width: 768px) { + .atm-container { + justify-content: center; + padding: 1rem; + } + + .atm-screen { + transform: none; + width: 100%; + max-width: 450px; + } +} diff --git a/arma/client/addons/bank/ui/_site/atm.html b/arma/client/addons/bank/ui/_site/atm.html new file mode 100644 index 0000000..6fba7a5 --- /dev/null +++ b/arma/client/addons/bank/ui/_site/atm.html @@ -0,0 +1,255 @@ + + + + + + + ATM + + + + + + +
+
+ +
+ +
AUTOMATED TELLER
+
+ + +
+ +
+
+
👤
+

Welcome

+

Insert your card to begin

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ + + + + diff --git a/arma/client/addons/bank/ui/_site/atm.js b/arma/client/addons/bank/ui/_site/atm.js new file mode 100644 index 0000000..89ed2df --- /dev/null +++ b/arma/client/addons/bank/ui/_site/atm.js @@ -0,0 +1,284 @@ +/** + * ATM Interface + * Handles banking transactions with PIN authentication + */ + +// Mock data +const mockData = { + cash: 2500, + bank: 45750, + pin: '1234' // For demo purposes +}; + +// State +let enteredPin = ''; +let currentView = 'welcomeView'; + +// View Management +function showView(viewId) { + // Hide all views + document.querySelectorAll('.atm-view').forEach(view => { + view.style.display = 'none'; + }); + + // Show selected view + const view = document.getElementById(viewId); + if (view) { + view.style.display = 'flex'; + currentView = viewId; + + // Update balance displays when showing certain views + if (viewId === 'menuView' || viewId === 'balanceView' || viewId === 'depositView') { + updateBalances(); + } + } +} + +// PIN Entry +function enterPin(digit) { + if (enteredPin.length < 4) { + enteredPin += digit; + updatePinDisplay(); + } +} + +function clearPin() { + enteredPin = ''; + updatePinDisplay(); +} + +function updatePinDisplay() { + const dots = document.querySelectorAll('.pin-dot'); + dots.forEach((dot, index) => { + if (index < enteredPin.length) { + dot.classList.add('filled'); + } else { + dot.classList.remove('filled'); + } + }); +} + +function submitPin() { + if (enteredPin.length !== 4) { + showError('Please enter a 4-digit PIN'); + return; + } + + // In a real implementation, this would validate with the server + if (enteredPin === mockData.pin) { + enteredPin = ''; + updatePinDisplay(); + showView('menuView'); + } else { + showError('Incorrect PIN'); + clearPin(); + } +} + +// Balance Updates +function updateBalances() { + // Update all balance displays + const cashElements = ['cashBalance', 'cashBalanceDetail', 'availableCash']; + const bankElements = ['bankBalance', 'bankBalanceDetail']; + + cashElements.forEach(id => { + const el = document.getElementById(id); + if (el) el.textContent = `$${mockData.cash.toLocaleString()}`; + }); + + bankElements.forEach(id => { + const el = document.getElementById(id); + if (el) el.textContent = `$${mockData.bank.toLocaleString()}`; + }); + + const totalEl = document.getElementById('totalBalance'); + if (totalEl) { + const total = mockData.cash + mockData.bank; + totalEl.textContent = `$${total.toLocaleString()}`; + } +} + +// Withdraw Functions +function withdrawAmount(amount) { + if (amount > mockData.bank) { + showError('Insufficient funds'); + return; + } + + mockData.bank -= amount; + mockData.cash += amount; + + // sendEvent('atm::withdraw', { amount: amount }); + showSuccess(`Withdrew $${amount.toLocaleString()}`); +} + +function withdrawCustom() { + const input = document.getElementById('withdrawInput'); + const amount = parseFloat(input.value); + + if (!amount || amount <= 0) { + showError('Please enter a valid amount'); + return; + } + + if (amount > mockData.bank) { + showError('Insufficient funds'); + return; + } + + mockData.bank -= amount; + mockData.cash += amount; + + // sendEvent('atm::withdraw', { amount: amount }); + input.value = ''; + showSuccess(`Withdrew $${amount.toLocaleString()}`); +} + +// Deposit Functions +function depositAmount() { + const input = document.getElementById('depositInput'); + const amount = parseFloat(input.value); + + if (!amount || amount <= 0) { + showError('Please enter a valid amount'); + return; + } + + if (amount > mockData.cash) { + showError('Insufficient cash'); + return; + } + + mockData.cash -= amount; + mockData.bank += amount; + + // sendEvent('atm::deposit', { amount: amount }); + input.value = ''; + showSuccess(`Deposited $${amount.toLocaleString()}`); +} + +function depositAll() { + if (mockData.cash <= 0) { + showError('No cash to deposit'); + return; + } + + const amount = mockData.cash; + mockData.cash = 0; + mockData.bank += amount; + + // sendEvent('atm::deposit', { amount: amount }); + showSuccess(`Deposited $${amount.toLocaleString()}`); +} + +// Transfer Function +function transferFunds() { + const playerIdInput = document.getElementById('transferPlayerId'); + const amountInput = document.getElementById('transferAmount'); + + const playerId = playerIdInput.value.trim(); + const amount = parseFloat(amountInput.value); + + if (!playerId) { + showError('Please enter a player ID'); + return; + } + + if (!amount || amount <= 0) { + showError('Please enter a valid amount'); + return; + } + + if (amount > mockData.bank) { + showError('Insufficient funds'); + return; + } + + mockData.bank -= amount; + + // sendEvent('atm::transfer', { + // playerId: playerId, + // amount: amount + // }); + + playerIdInput.value = ''; + amountInput.value = ''; + + showSuccess(`Transferred $${amount.toLocaleString()} to Player ${playerId}`); +} + +// Result Screens +function showSuccess(message) { + document.getElementById('successMessage').textContent = message; + showView('successView'); + updateBalances(); +} + +function showError(message) { + document.getElementById('errorMessage').textContent = message; + showView('errorView'); +} + +// Exit ATM +function exitATM() { + enteredPin = ''; + updatePinDisplay(); + sendEvent('atm::exit', {}); + showView('welcomeView'); +} + +// Send event to Arma +function sendEvent(event, data) { + if (typeof A3API !== 'undefined') { + A3API.SendAlert(JSON.stringify({ + event: event, + data: data + })); + } else { + console.log('Event:', event, 'Data:', data); + } +} + +// Update ATM data from external source +function updateATMData(data) { + if (data.cash !== undefined) { + mockData.cash = data.cash; + } + if (data.bank !== undefined) { + mockData.bank = data.bank; + } + updateBalances(); +} + +// Initialize +function initATM() { + console.log('ATM interface initializing...'); + + // Show welcome screen + showView('welcomeView'); + + // Update initial balances + updateBalances(); + + console.log('ATM interface initialized'); +} + +// Auto-initialize +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initATM); +} else { + initATM(); +} + +// Expose functions globally +window.showView = showView; +window.enterPin = enterPin; +window.clearPin = clearPin; +window.submitPin = submitPin; +window.withdrawAmount = withdrawAmount; +window.withdrawCustom = withdrawCustom; +window.depositAmount = depositAmount; +window.depositAll = depositAll; +window.transferFunds = transferFunds; +window.exitATM = exitATM; +window.updateATMData = updateATMData; diff --git a/arma/client/addons/bank/ui/_site/index.html b/arma/client/addons/bank/ui/_site/index.html new file mode 100644 index 0000000..0e64929 --- /dev/null +++ b/arma/client/addons/bank/ui/_site/index.html @@ -0,0 +1,237 @@ + + + + + + + Banking Services + + + + + + +
+ +
+ +
+

Banking Services

+

Secure Financial Management

+
+
+ +
+
+ + +
+ +
+
+

Your Accounts

+
+
+ + + + + + + + +
+
+ + +
+
+

Quick Actions

+
+
+ +
+

Transfer Funds

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+

Quick Access

+
+ + + + +
+
+
+
+ + +
+
+

Recent Transactions

+
+
+
+
+
+ Deposit + +$5,000 +
+
+ From Cash + 2 hours ago +
+
+ +
+
+ Withdrawal + -$1,200 +
+
+ To Cash + 5 hours ago +
+
+ +
+
+ Transfer + -$500 +
+
+ To Player #1234 + 1 day ago +
+
+ +
+
+ Deposit + +$10,000 +
+
+ Mission Reward + 2 days ago +
+
+ +
+
+ Transfer + +$2,000 +
+
+ From Player #5678 + 3 days ago +
+
+
+
+
+
+
+ + + + + diff --git a/arma/client/addons/bank/ui/_site/script.js b/arma/client/addons/bank/ui/_site/script.js new file mode 100644 index 0000000..b1cd040 --- /dev/null +++ b/arma/client/addons/bank/ui/_site/script.js @@ -0,0 +1,248 @@ +/** + * Banking Interface + * Handles transfers, deposits, withdrawals, and account management + */ + +// Mock data +const mockData = { + accounts: { + cash: { name: "Cash", balance: 2500, type: "Physical Currency" }, + bank: { name: "Bank Account", balance: 45750, type: "Savings • Protected" }, + org: { name: "Organization", balance: 125000, type: "Shared • View Only", readOnly: true } + }, + transactions: [ + { type: "deposit", amount: 5000, desc: "From Cash", time: "2 hours ago" }, + { type: "withdrawal", amount: -1200, desc: "To Cash", time: "5 hours ago" }, + { type: "transfer", amount: -500, desc: "To Player #1234", time: "1 day ago" }, + { type: "deposit", amount: 10000, desc: "Mission Reward", time: "2 days ago" }, + { type: "transfer", amount: 2000, desc: "From Player #5678", time: "3 days ago" } + ] +}; + +// State +let selectedAccount = 'cash'; + +// Initialize +function initBank() { + console.log('Banking interface initializing...'); + + setupEventHandlers(); + updateBalances(); + + console.log('Banking interface initialized'); +} + +// Event Handlers +function setupEventHandlers() { + // Close button + const closeBtn = document.querySelector('.close-btn'); + if (closeBtn) { + closeBtn.addEventListener('click', () => { + console.log('Closing bank...'); + sendEvent('bank::close', {}); + }); + } + + // Account card selection + const accountCards = document.querySelectorAll('.account-card'); + accountCards.forEach(card => { + card.addEventListener('click', () => { + accountCards.forEach(c => c.classList.remove('active')); + card.classList.add('active'); + selectedAccount = card.dataset.account; + console.log('Selected account:', selectedAccount); + }); + }); + + // Transfer form + const transferBtn = document.getElementById('transferBtn'); + const transferFrom = document.getElementById('transferFrom'); + const transferTo = document.getElementById('transferTo'); + const transferAmount = document.getElementById('transferAmount'); + const playerId = document.getElementById('playerId'); + const playerIdGroup = document.getElementById('playerIdGroup'); + + // Show/hide player ID field + if (transferTo) { + transferTo.addEventListener('change', () => { + if (transferTo.value === 'player') { + playerIdGroup.style.display = 'flex'; + } else { + playerIdGroup.style.display = 'none'; + } + }); + } + + // Transfer button + if (transferBtn) { + transferBtn.addEventListener('click', () => { + const from = transferFrom.value; + const to = transferTo.value; + const amount = parseFloat(transferAmount.value); + + if (!amount || amount <= 0) { + alert('Please enter a valid amount'); + return; + } + + if (from === to) { + alert('Source and destination must be different'); + return; + } + + const fromAccount = mockData.accounts[from]; + if (amount > fromAccount.balance) { + alert('Insufficient funds'); + return; + } + + if (to === 'player' && !playerId.value) { + alert('Please enter a player ID'); + return; + } + + const transferData = { + from: from, + to: to, + amount: amount, + playerId: to === 'player' ? playerId.value : null + }; + + console.log('Transfer request:', transferData); + sendEvent('bank::transfer', transferData); + + // Clear form + transferAmount.value = ''; + if (to === 'player') playerId.value = ''; + }); + } + + // Quick action buttons + const quickActionBtns = document.querySelectorAll('.quick-action-btn'); + quickActionBtns.forEach(btn => { + btn.addEventListener('click', () => { + const action = btn.dataset.action; + console.log('Quick action:', action); + + switch (action) { + case 'deposit': + const cashBalance = mockData.accounts.cash.balance; + if (cashBalance <= 0) { + alert('No cash to deposit'); + return; + } + sendEvent('bank::deposit', { amount: cashBalance }); + break; + case 'withdraw': + const amount = prompt('Enter amount to withdraw:'); + if (amount && parseFloat(amount) > 0) { + sendEvent('bank::withdraw', { amount: parseFloat(amount) }); + } + break; + case 'statement': + sendEvent('bank::statement', {}); + break; + } + }); + }); + + // Transaction items + const transactionItems = document.querySelectorAll('.transaction-item'); + transactionItems.forEach((item, index) => { + item.addEventListener('click', () => { + console.log('Transaction clicked:', mockData.transactions[index]); + sendEvent('bank::transaction::view', { transaction: mockData.transactions[index] }); + }); + }); +} + +// Update balances +function updateBalances() { + const balanceElements = document.querySelectorAll('.balance-amount'); + balanceElements[0].textContent = `$${mockData.accounts.cash.balance.toLocaleString()}`; + balanceElements[1].textContent = `$${mockData.accounts.bank.balance.toLocaleString()}`; + balanceElements[2].textContent = `$${mockData.accounts.org.balance.toLocaleString()}`; + + // Update form options + const transferFrom = document.getElementById('transferFrom'); + const transferTo = document.getElementById('transferTo'); + + if (transferFrom) { + transferFrom.innerHTML = ` + + + `; + } +} + +// Update bank data +function updateBankData(data) { + if (data.accounts) { + Object.assign(mockData.accounts, data.accounts); + updateBalances(); + } + + if (data.transactions) { + // Update transaction list + mockData.transactions = data.transactions; + // Re-render transaction list + renderTransactions(); + } +} + +// Render transactions +function renderTransactions() { + const transactionList = document.querySelector('.transaction-list'); + if (!transactionList) return; + + transactionList.innerHTML = ''; + + mockData.transactions.forEach((transaction, index) => { + const item = document.createElement('div'); + item.className = 'transaction-item'; + + const isPositive = transaction.amount > 0; + const amountClass = isPositive ? 'positive' : 'negative'; + const displayAmount = isPositive ? `+$${transaction.amount.toLocaleString()}` : `-$${Math.abs(transaction.amount).toLocaleString()}`; + + item.innerHTML = ` +
+ ${transaction.type} + ${displayAmount} +
+
+ ${transaction.desc} + ${transaction.time} +
+ `; + + item.addEventListener('click', () => { + console.log('Transaction clicked:', transaction); + sendEvent('bank::transaction::view', { transaction: transaction }); + }); + + transactionList.appendChild(item); + }); +} + +// Send event to Arma +function sendEvent(event, data) { + if (typeof A3API !== 'undefined') { + A3API.SendAlert(JSON.stringify({ + event: event, + data: data + })); + } else { + console.log('Event:', event, 'Data:', data); + } +} + +// Auto-initialize +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initBank); +} else { + initBank(); +} + +// Expose functions globally +window.updateBankData = updateBankData; diff --git a/arma/client/addons/bank/ui/_site/style.css b/arma/client/addons/bank/ui/_site/style.css new file mode 100644 index 0000000..6974c7e --- /dev/null +++ b/arma/client/addons/bank/ui/_site/style.css @@ -0,0 +1,471 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + height: 100vh; + width: 100vw; + background: rgba(0, 0, 0, 0.7); + font-family: Arial, sans-serif; + color: rgba(200, 220, 240, 0.95); + overflow: hidden; +} + +.bank-container { + height: 100vh; + width: 100vw; + padding: 2rem; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +/* Header Section */ +.bank-header { + display: flex; + align-items: center; + gap: 1.5rem; + padding: 1.25rem 1.5rem; + background: rgba(15, 20, 30, 0.9); + border: 1px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + box-shadow: + 0 0 20px rgba(100, 150, 200, 0.15), + 0 4px 16px rgba(0, 0, 0, 0.8); +} + +.bank-logo { + width: 60px; + height: 60px; + background: rgba(20, 30, 45, 0.8); + border: 2px solid rgba(100, 150, 200, 0.5); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; +} + +.logo-icon { + font-size: 2rem; +} + +.bank-info { + flex: 1; +} + +.bank-title { + font-size: 1.5rem; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + color: rgba(200, 220, 255, 1); + margin-bottom: 0.25rem; +} + +.bank-subtitle { + font-size: 0.875rem; + color: rgba(140, 160, 180, 0.8); + letter-spacing: 0.5px; +} + +.header-actions { + display: flex; + gap: 0.75rem; +} + +.action-btn { + padding: 0.625rem 1.25rem; + background: rgba(20, 30, 45, 0.7); + border: 1px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + color: rgba(200, 220, 240, 0.95); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + transition: all 0.15s ease; +} + +.action-btn:hover { + background: rgba(30, 45, 70, 0.9); + border-color: rgba(150, 200, 255, 0.7); + box-shadow: + 0 0 15px rgba(100, 150, 200, 0.2), + inset 0 0 20px rgba(100, 150, 200, 0.05); +} + +.action-btn-primary { + background: rgba(100, 150, 200, 0.2); + border-color: rgba(100, 150, 200, 0.5); + width: 100%; + margin-top: 0.5rem; +} + +.action-btn-primary:hover { + background: rgba(100, 150, 200, 0.3); + border-color: rgba(150, 200, 255, 0.7); +} + +.close-btn { + border-color: rgba(200, 100, 100, 0.4); +} + +.close-btn:hover { + border-color: rgba(255, 100, 100, 0.7); + box-shadow: + 0 0 15px rgba(200, 100, 100, 0.2), + inset 0 0 20px rgba(200, 100, 100, 0.05); +} + +/* Main Content */ +.bank-content { + flex: 1; + display: grid; + grid-template-columns: 300px 1fr 350px; + gap: 1.5rem; + overflow: hidden; +} + +/* Panels */ +.bank-panel { + background: rgba(15, 20, 30, 0.9); + border: 1px solid rgba(100, 150, 200, 0.4); + border-left: 3px solid rgba(100, 150, 200, 0.5); + border-radius: 4px; + display: flex; + flex-direction: column; + box-shadow: + 0 0 20px rgba(100, 150, 200, 0.1), + 0 4px 16px rgba(0, 0, 0, 0.6); +} + +.panel-main { + grid-column: 2; +} + +.panel-header { + padding: 1.25rem 1.5rem; + border-bottom: 1px solid rgba(100, 150, 200, 0.2); +} + +.panel-title { + font-size: 1rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(200, 220, 255, 1); +} + +.panel-content { + flex: 1; + padding: 1.5rem; + overflow-y: auto; +} + +/* Custom Scrollbar */ +.panel-content::-webkit-scrollbar { + width: 8px; +} + +.panel-content::-webkit-scrollbar-track { + background: rgba(15, 20, 30, 0.5); + border-radius: 4px; +} + +.panel-content::-webkit-scrollbar-thumb { + background: rgba(100, 150, 200, 0.3); + border-radius: 4px; +} + +.panel-content::-webkit-scrollbar-thumb:hover { + background: rgba(100, 150, 200, 0.5); +} + +/* Account Cards */ +.account-card { + padding: 1.25rem; + margin-bottom: 1rem; + background: rgba(20, 30, 45, 0.6); + border: 1px solid rgba(100, 150, 200, 0.3); + border-left: 3px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + cursor: pointer; + transition: all 0.15s ease; +} + +.account-card:last-child { + margin-bottom: 0; +} + +.account-card:hover { + background: rgba(30, 45, 70, 0.7); + border-left-color: rgba(150, 200, 255, 0.7); + box-shadow: + 0 0 15px rgba(100, 150, 200, 0.15), + inset 0 0 20px rgba(100, 150, 200, 0.05); +} + +.account-card.active { + background: rgba(30, 45, 70, 0.8); + border-left-color: rgba(100, 200, 150, 0.8); + box-shadow: + 0 0 20px rgba(100, 200, 150, 0.2), + inset 0 0 25px rgba(100, 200, 150, 0.05); +} + +.account-header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; +} + +.account-icon { + font-size: 1.75rem; +} + +.account-info { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.account-name { + font-size: 1rem; + font-weight: 600; + color: rgba(200, 220, 255, 1); +} + +.account-type { + font-size: 0.75rem; + color: rgba(140, 160, 180, 0.8); +} + +.account-balance { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 1rem; + border-top: 1px solid rgba(100, 150, 200, 0.2); +} + +.balance-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(140, 160, 180, 0.8); +} + +.balance-amount { + font-size: 1.25rem; + font-weight: 600; + color: rgba(100, 200, 150, 1); +} + +/* Action Section */ +.action-section { + margin-bottom: 2rem; +} + +.action-section:last-child { + margin-bottom: 0; +} + +.section-title { + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(180, 200, 220, 0.9); + margin-bottom: 1rem; +} + +/* Transfer Form */ +.transfer-form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.form-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(140, 160, 180, 0.9); +} + +.form-select, +.form-input { + padding: 0.75rem 1rem; + background: rgba(20, 30, 45, 0.7); + border: 1px solid rgba(100, 150, 200, 0.3); + border-radius: 4px; + color: rgba(200, 220, 240, 0.95); + font-size: 0.875rem; + transition: all 0.15s ease; +} + +.form-select:focus, +.form-input:focus { + outline: none; + border-color: rgba(150, 200, 255, 0.6); + box-shadow: + 0 0 15px rgba(100, 150, 200, 0.15), + inset 0 0 20px rgba(100, 150, 200, 0.05); +} + +.form-input::placeholder { + color: rgba(100, 120, 140, 0.6); +} + +/* Quick Actions */ +.quick-actions { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 1rem; +} + +.quick-action-btn { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + padding: 1.25rem; + background: rgba(20, 30, 45, 0.6); + border: 1px solid rgba(100, 150, 200, 0.3); + border-radius: 4px; + cursor: pointer; + transition: all 0.15s ease; +} + +.quick-action-btn:hover { + background: rgba(30, 45, 70, 0.8); + border-color: rgba(150, 200, 255, 0.5); + box-shadow: + 0 0 15px rgba(100, 150, 200, 0.15), + inset 0 0 20px rgba(100, 150, 200, 0.05); +} + +.quick-action-icon { + font-size: 2rem; +} + +.quick-action-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + text-align: center; + color: rgba(180, 200, 220, 0.9); +} + +/* Transaction List */ +.transaction-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.transaction-item { + padding: 1rem; + background: rgba(20, 30, 45, 0.6); + border: 1px solid rgba(100, 150, 200, 0.2); + border-left: 3px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + transition: all 0.15s ease; +} + +.transaction-item:hover { + background: rgba(30, 45, 70, 0.7); + border-left-color: rgba(150, 200, 255, 0.6); +} + +.transaction-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.transaction-type { + padding: 0.25rem 0.625rem; + border-radius: 3px; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 600; +} + +.transaction-type.deposit { + background: rgba(100, 200, 150, 0.2); + border: 1px solid rgba(100, 200, 150, 0.4); + color: rgba(150, 255, 200, 0.9); +} + +.transaction-type.withdrawal { + background: rgba(200, 150, 100, 0.2); + border: 1px solid rgba(200, 150, 100, 0.4); + color: rgba(255, 200, 150, 0.9); +} + +.transaction-type.transfer { + background: rgba(100, 150, 200, 0.2); + border: 1px solid rgba(100, 150, 200, 0.4); + color: rgba(150, 200, 255, 0.9); +} + +.transaction-amount { + font-size: 1rem; + font-weight: 600; +} + +.transaction-amount.positive { + color: rgba(100, 200, 150, 1); +} + +.transaction-amount.negative { + color: rgba(200, 150, 100, 1); +} + +.transaction-details { + display: flex; + justify-content: space-between; + align-items: center; +} + +.transaction-desc { + font-size: 0.875rem; + color: rgba(180, 200, 220, 0.9); +} + +.transaction-time { + font-size: 0.7rem; + color: rgba(100, 150, 200, 0.7); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Responsive adjustments */ +@media (max-width: 1400px) { + .bank-content { + grid-template-columns: 280px 1fr 300px; + } +} + +@media (max-width: 1200px) { + .bank-content { + grid-template-columns: 1fr; + grid-template-rows: auto 1fr auto; + } + + .panel-main { + grid-column: 1; + } +} diff --git a/arma/client/addons/common/$PBOPREFIX$ b/arma/client/addons/common/$PBOPREFIX$ new file mode 100644 index 0000000..c2897bb --- /dev/null +++ b/arma/client/addons/common/$PBOPREFIX$ @@ -0,0 +1 @@ +forge\forge_client\addons\common diff --git a/arma/client/addons/common/CfgEventHandlers.hpp b/arma/client/addons/common/CfgEventHandlers.hpp new file mode 100644 index 0000000..865276c --- /dev/null +++ b/arma/client/addons/common/CfgEventHandlers.hpp @@ -0,0 +1,11 @@ +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)); + }; +}; diff --git a/arma/client/addons/common/README.md b/arma/client/addons/common/README.md new file mode 100644 index 0000000..9ce0564 --- /dev/null +++ b/arma/client/addons/common/README.md @@ -0,0 +1,4 @@ +forge_client_common +=================== + +Common functionality shared between addons. diff --git a/arma/client/addons/common/XEH_PREP.hpp b/arma/client/addons/common/XEH_PREP.hpp new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/arma/client/addons/common/XEH_PREP.hpp @@ -0,0 +1 @@ + diff --git a/arma/client/addons/common/XEH_preInit.sqf b/arma/client/addons/common/XEH_preInit.sqf new file mode 100644 index 0000000..dbef1ae --- /dev/null +++ b/arma/client/addons/common/XEH_preInit.sqf @@ -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" diff --git a/arma/client/addons/common/XEH_preStart.sqf b/arma/client/addons/common/XEH_preStart.sqf new file mode 100644 index 0000000..0228885 --- /dev/null +++ b/arma/client/addons/common/XEH_preStart.sqf @@ -0,0 +1,3 @@ +#include "script_component.hpp" + +#include "XEH_PREP.hpp" diff --git a/arma/client/addons/common/config.cpp b/arma/client/addons/common/config.cpp new file mode 100644 index 0000000..595b77a --- /dev/null +++ b/arma/client/addons/common/config.cpp @@ -0,0 +1,19 @@ +#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" diff --git a/arma/client/addons/common/script_component.hpp b/arma/client/addons/common/script_component.hpp new file mode 100644 index 0000000..39adea8 --- /dev/null +++ b/arma/client/addons/common/script_component.hpp @@ -0,0 +1,9 @@ +#define COMPONENT common +#define COMPONENT_BEAUTIFIED Common +#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" diff --git a/arma/client/addons/common/stringtable.xml b/arma/client/addons/common/stringtable.xml new file mode 100644 index 0000000..1ba94dc --- /dev/null +++ b/arma/client/addons/common/stringtable.xml @@ -0,0 +1,8 @@ + + + + + Common + + + diff --git a/arma/client/addons/main/$PBOPREFIX$ b/arma/client/addons/main/$PBOPREFIX$ new file mode 100644 index 0000000..1d61036 --- /dev/null +++ b/arma/client/addons/main/$PBOPREFIX$ @@ -0,0 +1 @@ +forge\forge_client\addons\main diff --git a/arma/client/addons/main/CfgSettings.hpp b/arma/client/addons/main/CfgSettings.hpp new file mode 100644 index 0000000..05d0b0b --- /dev/null +++ b/arma/client/addons/main/CfgSettings.hpp @@ -0,0 +1,13 @@ +class CfgSettings { + class CBA { + class Versioning { + class PREFIX { + main_addon = QUOTE(ADDON); + + class dependencies { + CBA[] = {"cba_main", REQUIRED_CBA_VERSION, "true"}; + }; + }; + }; + }; +}; diff --git a/arma/client/addons/main/README.md b/arma/client/addons/main/README.md new file mode 100644 index 0000000..d2499a7 --- /dev/null +++ b/arma/client/addons/main/README.md @@ -0,0 +1,4 @@ +forge_client_main +=================== + +Main Addon for forge-client diff --git a/arma/client/addons/main/config.cpp b/arma/client/addons/main/config.cpp new file mode 100644 index 0000000..2b42b1e --- /dev/null +++ b/arma/client/addons/main/config.cpp @@ -0,0 +1,19 @@ +#include "script_component.hpp" + +class CfgPatches { + class ADDON { + author = AUTHOR; + authors[] = {"J.Schmidt"}; + url = CSTRING(url); + name = COMPONENT_NAME; + requiredVersion = REQUIRED_VERSION; + requiredAddons[] = { + "cba_main" + }; + units[] = {}; + weapons[] = {}; + VERSION_CONFIG; + }; +}; + +#include "CfgSettings.hpp" diff --git a/arma/client/addons/main/data/hpp/defineDIKCodes.hpp b/arma/client/addons/main/data/hpp/defineDIKCodes.hpp new file mode 100644 index 0000000..6eca2b4 --- /dev/null +++ b/arma/client/addons/main/data/hpp/defineDIKCodes.hpp @@ -0,0 +1,33 @@ +#include "\a3\ui_f\hpp\defineDIKCodes.inc" + +#define DIK_MOUSELEFT 0xF0 +#define DIK_MOUSERIGHT 0xF1 +#define DIK_MOUSEMIDDLE 0xF2 +#define DIK_MOUSE4 0xF3 +#define DIK_MOUSE5 0xF4 +#define DIK_MOUSE6 0xF5 +#define DIK_MOUSE7 0xF6 +#define DIK_MOUSE8 0xF7 +#define DIK_MOUSEUP 0xF8 +#define DIK_MOUSEDOWN 0xF9 + +#define DIK_USERCUSTOM1 0xFA +#define DIK_USERCUSTOM2 0xFB +#define DIK_USERCUSTOM3 0xFC +#define DIK_USERCUSTOM4 0xFD +#define DIK_USERCUSTOM5 0xFE +#define DIK_USERCUSTOM6 0xFF +#define DIK_USERCUSTOM7 0x100 +#define DIK_USERCUSTOM8 0x101 +#define DIK_USERCUSTOM9 0x102 +#define DIK_USERCUSTOM10 0x103 +#define DIK_USERCUSTOM11 0x104 +#define DIK_USERCUSTOM12 0x105 +#define DIK_USERCUSTOM13 0x106 +#define DIK_USERCUSTOM14 0x107 +#define DIK_USERCUSTOM15 0x108 +#define DIK_USERCUSTOM16 0x109 +#define DIK_USERCUSTOM17 0x10A +#define DIK_USERCUSTOM18 0x10B +#define DIK_USERCUSTOM19 0x10C +#define DIK_USERCUSTOM20 0x10D diff --git a/arma/client/addons/main/data/ui/placeholder.txt b/arma/client/addons/main/data/ui/placeholder.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/arma/client/addons/main/data/ui/placeholder.txt @@ -0,0 +1 @@ + diff --git a/arma/client/addons/main/script_component.hpp b/arma/client/addons/main/script_component.hpp new file mode 100644 index 0000000..32d193a --- /dev/null +++ b/arma/client/addons/main/script_component.hpp @@ -0,0 +1,9 @@ +#define COMPONENT main +#define COMPONENT_BEAUTIFIED Main +#include "script_mod.hpp" + +// #define DEBUG_MODE_FULL +// #define DISABLE_COMPILE_CACHE +// #define ENABLE_PERFORMANCE_COUNTERS + +#include "script_macros.hpp" diff --git a/arma/client/addons/main/script_macros.hpp b/arma/client/addons/main/script_macros.hpp new file mode 100644 index 0000000..415ad5c --- /dev/null +++ b/arma/client/addons/main/script_macros.hpp @@ -0,0 +1,148 @@ +// Global toggles for caching/logging +// #define DISABLE_COMPILE_CACHE +// #define DEBUG_MODE_FULL +#define DEBUG_SYNCHRONOUS + +#include "\x\cba\addons\main\script_macros_common.hpp" +#include "\x\cba\addons\xeh\script_xeh.hpp" + +// Functions +#define AFUNC(var1,var2) TRIPLES(DOUBLES(ace,var1),fnc,var2) +#define BFUNC(var1) TRIPLES(BIS,fnc,var1) +#define CFUNC(var1) TRIPLES(CBA,fnc,var1) + +// Remote Procedure Calls +#define CRPC(var1,var2) QUOTE(DOUBLES(DOUBLES(forge_client,var1),var2)) +#define SRPC(var1,var2) QUOTE(DOUBLES(DOUBLES(forge_server,var1),var2)) + +#define QQUOTE(var1) QUOTE(QUOTE(var1)) + +// QPATHTOF but without a leading slash +#define PATHTOF2(var1) MAINPREFIX\PREFIX\SUBPREFIX\COMPONENT\var1 +#define QPATHTOF2(var1) QUOTE(PATHTOF2(var1)) + +#ifdef SUBCOMPONENT + #define SUBADDON DOUBLES(ADDON,SUBCOMPONENT) + + // Update PATHTO macros to point to subaddon instead + #undef PATHTO + #define PATHTO(var1) \MAINPREFIX\PREFIX\SUBPREFIX\COMPONENT\SUBCOMPONENT\var1.sqf + #undef PATHTOF + #define PATHTOF(var1) \MAINPREFIX\PREFIX\SUBPREFIX\COMPONENT\SUBCOMPONENT\var1 + #undef PATHTO2 + #define PATHTO2(var1) MAINPREFIX\PREFIX\SUBPREFIX\COMPONENT\SUBCOMPONENT\var1.sqf + #undef PATHTOF2 + #define PATHTOF2(var1) MAINPREFIX\PREFIX\SUBPREFIX\COMPONENT\SUBCOMPONENT\var1 +#endif + +#undef PREP +#ifdef DISABLE_COMPILE_CACHE + #define LINKFUNC(x) {call FUNC(x)} + #define PREP(fncName) FUNC(fncName) = compile preprocessFileLineNumbers QPATHTOF(functions\DOUBLES(fnc,fncName).sqf) + #define PREP_RECOMPILE_START if (isNil "forge_client_fnc_recompile") then {forge_client_recompiles = []; forge_client_fnc_recompile = {{call _x} forEach forge_client_recompiles;}}; private _recomp = { + #define PREP_RECOMPILE_END }; call _recomp; forge_client_recompiles pushBack _recomp; +#else + #define LINKFUNC(x) FUNC(x) + #define PREP(fncName) [QPATHTOF(functions\DOUBLES(fnc,fncName).sqf), QFUNC(fncName)] call CBA_fnc_compileFunction + #define PREP_RECOMPILE_START ; /* disabled */ + #define PREP_RECOMPILE_END ; /* disabled */ +#endif + +#define GETVAR_SYS(var1,var2) getVariable [ARR_2(QUOTE(var1),var2)] +#define SETVAR_SYS(var1,var2) setVariable [ARR_2(QUOTE(var1),var2)] +#define SETPVAR_SYS(var1,var2) setVariable [ARR_3(QUOTE(var1),var2,true)] + +#undef GETVAR +#define GETVAR(var1,var2,var3) (var1 GETVAR_SYS(var2,var3)) +#define GETMVAR(var1,var2) (missionNamespace GETVAR_SYS(var1,var2)) +#define GETUVAR(var1,var2) (uiNamespace GETVAR_SYS(var1,var2)) +#define GETPRVAR(var1,var2) (profileNamespace GETVAR_SYS(var1,var2)) +#define GETPAVAR(var1,var2) (parsingNamespace GETVAR_SYS(var1,var2)) + +#undef SETVAR +#define SETVAR(var1,var2,var3) var1 SETVAR_SYS(var2,var3) +#define SETPVAR(var1,var2,var3) var1 SETPVAR_SYS(var2,var3) +#define SETMVAR(var1,var2) missionNamespace SETVAR_SYS(var1,var2) +#define SETUVAR(var1,var2) uiNamespace SETVAR_SYS(var1,var2) +#define SETPRVAR(var1,var2) profileNamespace SETVAR_SYS(var1,var2) +#define SETPAVAR(var1,var2) parsingNamespace SETVAR_SYS(var1,var2) + +#define GETGVAR(var1,var2) GETMVAR(GVAR(var1),var2) +#define GETEGVAR(var1,var2,var3) GETMVAR(EGVAR(var1,var2),var3) + +#define WEAP_XX(WEAP, COUNT) class DOUBLES(_xx,WEAP) { \ + weapon = QUOTE(WEAP); \ + count = COUNT; \ +} + +#define MAG_XX(MAG, COUNT) class DOUBLES(_xx,MAG) { \ + magazine = QUOTE(MAG); \ + count = COUNT; \ +} + +#define ITEM_XX(ITEM, COUNT) class DOUBLES(_xx,ITEM) { \ + name = QUOTE(ITEM); \ + count = COUNT; \ +} + +// ACE Cargo +#define CARGO_XX(ITEM, COUNT) class ITEM { \ + type = QUOTE(ITEM); \ + amount = COUNT; \ +} + +#define MAG_CSW(var1,var2) class DOUBLES(var1,csw): var1 { \ + scope = var2; \ + type = TYPE_MAGAZINE_PRIMARY_AND_THROW; \ +} + +// Debug textures, mainly for testing hiddenSelections +#define DBUG_TEX_RED "#(rgb,8,8,3)color(1,0,0,1)" +#define DBUG_TEX_GRN "#(rgb,8,8,3)color(0,1,0,1)" +#define DBUG_TEX_BLU "#(rgb,8,8,3)color(0,0,1,1)" +#define DBUG_TEX_PUR "#(rgb,8,8,3)color(1,0,1,1)" +#define DBUG_TEX_YEL "#(rgb,8,8,3)color(1,1,0,1)" + +// Statements and conditions +#define CLAMP(var1,lower,upper) (lower max (var1 min upper)) + +// Weapon types +#define TYPE_WEAPON_PRIMARY 1 +#define TYPE_WEAPON_HANDGUN 2 +#define TYPE_WEAPON_SECONDARY 4 +// Magazine types +#define TYPE_MAGAZINE_HANDGUN_AND_GL 16 // mainly +#define TYPE_MAGAZINE_PRIMARY_AND_THROW 256 +#define TYPE_MAGAZINE_SECONDARY_AND_PUT 512 // mainly +#define TYPE_MAGAZINE_MISSILE 768 +// More types +#define TYPE_BINOCULAR_AND_NVG 4096 +#define TYPE_WEAPON_VEHICLE 65536 +#define TYPE_ITEM 131072 +// Item types +#define TYPE_DEFAULT 0 +#define TYPE_MUZZLE 101 +#define TYPE_OPTICS 201 +#define TYPE_FLASHLIGHT 301 +#define TYPE_BIPOD 302 +#define TYPE_FIRST_AID_KIT 401 +#define TYPE_FINS 501 // not implemented +#define TYPE_BREATHING_BOMB 601 // not implemented +#define TYPE_NVG 602 +#define TYPE_GOGGLE 603 +#define TYPE_SCUBA 604 // not implemented +#define TYPE_HEADGEAR 605 +#define TYPE_FACTOR 607 +#define TYPE_MAP 608 +#define TYPE_COMPASS 609 +#define TYPE_WATCH 610 +#define TYPE_RADIO 611 +#define TYPE_GPS 612 +#define TYPE_HMD 616 +#define TYPE_BINOCULAR 617 +#define TYPE_MEDIKIT 619 +#define TYPE_TOOLKIT 620 +#define TYPE_UAV_TERMINAL 621 +#define TYPE_VEST 701 +#define TYPE_UNIFORM 801 +#define TYPE_BACKPACK 901 diff --git a/arma/client/addons/main/script_mod.hpp b/arma/client/addons/main/script_mod.hpp new file mode 100644 index 0000000..d1337cc --- /dev/null +++ b/arma/client/addons/main/script_mod.hpp @@ -0,0 +1,26 @@ +#define MAINPREFIX forge +#define PREFIX forge_client +#define MOD_NAME forge-client +#define AUTHOR "J.Schmidt" + +#define REQUIRED_VERSION 2.20 +#define REQUIRED_CBA_VERSION {3,18,4} +#define REQUIRED_ACE_VERSION {3,20,0} + +#include "script_version.hpp" + +#define VERSION MAJOR.MINOR +#define VERSION_STR MAJOR.MINOR.PATCH.BUILD +#define VERSION_AR MAJOR,MINOR,PATCH,BUILD + +#ifndef COMPONENT_BEAUTIFIED + #define COMPONENT_BEAUTIFIED COMPONENT +#endif +#ifdef SUBCOMPONENT + #ifndef SUBCOMPONENT_BEAUTIFIED + #define SUBCOMPONENT_BEAUTIFIED SUBCOMPONENT + #endif + #define COMPONENT_NAME QUOTE(MOD_NAME - COMPONENT_BEAUTIFIED (SUBCOMPONENT_BEAUTIFIED)) +#else + #define COMPONENT_NAME QUOTE(MOD_NAME - COMPONENT_BEAUTIFIED) +#endif diff --git a/arma/client/addons/main/script_version.hpp b/arma/client/addons/main/script_version.hpp new file mode 100644 index 0000000..3e6aaab --- /dev/null +++ b/arma/client/addons/main/script_version.hpp @@ -0,0 +1,4 @@ +#define MAJOR 1 +#define MINOR 0 +#define PATCH 0 +#define BUILD 0 diff --git a/arma/client/addons/main/stringtable.xml b/arma/client/addons/main/stringtable.xml new file mode 100644 index 0000000..99b18ab --- /dev/null +++ b/arma/client/addons/main/stringtable.xml @@ -0,0 +1,24 @@ + + + + + Main + + + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + + + diff --git a/arma/client/addons/org/$PBOPREFIX$ b/arma/client/addons/org/$PBOPREFIX$ new file mode 100644 index 0000000..cc69cc0 --- /dev/null +++ b/arma/client/addons/org/$PBOPREFIX$ @@ -0,0 +1 @@ +forge\forge_client\addons\org diff --git a/arma/client/addons/org/CfgEventHandlers.hpp b/arma/client/addons/org/CfgEventHandlers.hpp new file mode 100644 index 0000000..c6e25db --- /dev/null +++ b/arma/client/addons/org/CfgEventHandlers.hpp @@ -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)); + }; +}; diff --git a/arma/client/addons/org/README.md b/arma/client/addons/org/README.md new file mode 100644 index 0000000..1c007ba --- /dev/null +++ b/arma/client/addons/org/README.md @@ -0,0 +1,4 @@ +forge_client_org +=================== + +Description for this addon diff --git a/arma/client/addons/org/XEH_PREP.hpp b/arma/client/addons/org/XEH_PREP.hpp new file mode 100644 index 0000000..d83118a --- /dev/null +++ b/arma/client/addons/org/XEH_PREP.hpp @@ -0,0 +1,3 @@ +PREP(handleUIEvents); +PREP(initOrgClass); +PREP(openUI); diff --git a/arma/client/addons/org/XEH_postInit.sqf b/arma/client/addons/org/XEH_postInit.sqf new file mode 100644 index 0000000..421c54b --- /dev/null +++ b/arma/client/addons/org/XEH_postInit.sqf @@ -0,0 +1 @@ +#include "script_component.hpp" diff --git a/arma/client/addons/org/XEH_postInitClient.sqf b/arma/client/addons/org/XEH_postInitClient.sqf new file mode 100644 index 0000000..5982469 --- /dev/null +++ b/arma/client/addons/org/XEH_postInitClient.sqf @@ -0,0 +1,28 @@ +#include "script_component.hpp" + +if (isNil QGVAR(OrgClass)) then { [] call FUNC(initOrgClass); }; + +[QGVAR(initOrg), { + GVAR(OrgClass) call ["init", []]; +}] call CFUNC(addEventHandler); + +[QGVAR(responseInitOrg), { + params [["_data", createHashMap, [createHashMap]]]; + + GVAR(OrgClass) call ["sync", [_data, true]]; + + SETPVAR(player,FORGE_isLoaded,true); + cutText ["", "PLAIN", 1]; +}] call CFUNC(addEventHandler); + +[QGVAR(responseSyncOrg), { + params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]]; + + GVAR(OrgClass) call ["sync", [_data, _jip]]; +}] call CFUNC(addEventHandler); + +[{ + EGVAR(actor,ActorClass) get "isLoaded"; +}, { + [QGVAR(initOrg), []] call CFUNC(localEvent); +}] call CFUNC(waitUntilAndExecute); diff --git a/arma/client/addons/org/XEH_preInit.sqf b/arma/client/addons/org/XEH_preInit.sqf new file mode 100644 index 0000000..640756c --- /dev/null +++ b/arma/client/addons/org/XEH_preInit.sqf @@ -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" diff --git a/arma/client/addons/org/XEH_preInitClient.sqf b/arma/client/addons/org/XEH_preInitClient.sqf new file mode 100644 index 0000000..421c54b --- /dev/null +++ b/arma/client/addons/org/XEH_preInitClient.sqf @@ -0,0 +1 @@ +#include "script_component.hpp" diff --git a/arma/client/addons/org/XEH_preStart.sqf b/arma/client/addons/org/XEH_preStart.sqf new file mode 100644 index 0000000..0228885 --- /dev/null +++ b/arma/client/addons/org/XEH_preStart.sqf @@ -0,0 +1,3 @@ +#include "script_component.hpp" + +#include "XEH_PREP.hpp" diff --git a/arma/client/addons/org/config.cpp b/arma/client/addons/org/config.cpp new file mode 100644 index 0000000..2b01e51 --- /dev/null +++ b/arma/client/addons/org/config.cpp @@ -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\RscOrg.hpp" diff --git a/arma/client/addons/org/functions/fnc_handleUIEvents.sqf b/arma/client/addons/org/functions/fnc_handleUIEvents.sqf new file mode 100644 index 0000000..ce70c0f --- /dev/null +++ b/arma/client/addons/org/functions/fnc_handleUIEvents.sqf @@ -0,0 +1,33 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Handles the UI events. + * + * Arguments: + * None + * + * Return Value: + * None + * + * Example: + * [] call forge_client_org_fnc_handleUIEvents; + * + * Public: No + */ + +params ["_control", "_isConfirmDialog", "_message"]; + +private _alert = fromJSON _message; +private _event = _alert get "event"; +private _data = _alert get "data"; +private _display = displayChild findDisplay 46; + +diag_log format ["[FORGE:Client:Org] Handling UI event: %1 with data: %2", _event, _data]; + +switch (_event) do { + case "org::close": { _display closeDisplay 1; }; + default { hint format ["Unhandled UI event: %1", _event]; }; +}; + +true; diff --git a/arma/client/addons/org/functions/fnc_initOrgClass.sqf b/arma/client/addons/org/functions/fnc_initOrgClass.sqf new file mode 100644 index 0000000..ae1a4df --- /dev/null +++ b/arma/client/addons/org/functions/fnc_initOrgClass.sqf @@ -0,0 +1,81 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Initializes the org class. + * + * Arguments: + * None + * + * Return Value: + * None + * + * Examples: + * [] call forge_client_org_fnc_initOrgClass + * + * Public: Yes + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(OrgClass) = createHashMapObject [[ + ["#type", "IOrgClass"], + ["#create", { + _self set ["uid", getPlayerUID player]; + _self set ["org", createHashMap]; + _self set ["isLoaded", false]; + _self set ["lastSave", time]; + + private _org = createHashMap; + _org set ["id", ""]; + _org set ["owner", ""]; + _org set ["name", ""]; + _org set ["funds", 0]; + _org set ["reputation", 0]; + _org set ["assets", createHashMap]; + _org set ["members", createHashMap]; + + _self set ["org", _org]; + }], + ["init", { + private _uid = _self get "uid"; + private _org = _self get "org"; + + [SRPC(org,requestInitOrg), [_uid, _org]] call CFUNC(serverEvent); + + systemChat format ["Org loaded for %1", (name player)]; + diag_log "[FORGE:Client:Org] Org Class Initialized!"; + }], + ["save", { + params [["_sync", false, [false]]]; + + private _uid = _self get "uid"; + [SRPC(org,requestSaveOrg), [_uid, _sync]] call CFUNC(serverEvent); + + _self set ["lastSave", time]; + }], + ["sync", { + params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]]; + + private _isLoaded = _self get "isLoaded"; + private _org = _self get "org"; + + if !(_isLoaded) then { _self set ["isLoaded", true]; }; + if (_data isEqualTo createHashMap) exitWith { + diag_log "[FORGE:Client:Org] Empty data received for sync, skipping."; + }; + + { _org set [_x, _y]; } forEach _data; + + _self set ["org", _org]; + diag_log "[FORGE:Client:Org] Sync completed"; + }], + ["get", { + params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]]; + + private _org = _self get "org"; + _org getOrDefault [_key, _default]; + }] +]]; + +SETVAR(player,FORGE_OrgClass,GVAR(OrgClass)); +GVAR(OrgClass) diff --git a/arma/client/addons/org/functions/fnc_openUI.sqf b/arma/client/addons/org/functions/fnc_openUI.sqf new file mode 100644 index 0000000..f36395d --- /dev/null +++ b/arma/client/addons/org/functions/fnc_openUI.sqf @@ -0,0 +1,31 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Opens the player interaction interface. + * + * Arguments: + * None + * + * Return Value: + * None + * + * Example: + * [] call forge_client_org_fnc_openUI; + * + * Public: No + */ + +private _display = (findDisplay 46) createDisplay "RscOrg"; +private _ctrl = (_display displayCtrl 1003); + +_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; diff --git a/arma/client/addons/org/initKeybinds.inc.sqf b/arma/client/addons/org/initKeybinds.inc.sqf new file mode 100644 index 0000000..2922c52 --- /dev/null +++ b/arma/client/addons/org/initKeybinds.inc.sqf @@ -0,0 +1 @@ +#include "\forge\forge_client\addons\main\data\hpp\defineDIKCodes.hpp" diff --git a/arma/client/addons/org/initSettings.inc.sqf b/arma/client/addons/org/initSettings.inc.sqf new file mode 100644 index 0000000..416ff52 --- /dev/null +++ b/arma/client/addons/org/initSettings.inc.sqf @@ -0,0 +1 @@ +// Can use localize "STR_ACE_Common_Enabled" for name if ACE is required diff --git a/arma/client/addons/org/script_component.hpp b/arma/client/addons/org/script_component.hpp new file mode 100644 index 0000000..746956a --- /dev/null +++ b/arma/client/addons/org/script_component.hpp @@ -0,0 +1,9 @@ +#define COMPONENT org +#define COMPONENT_BEAUTIFIED Org +#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" diff --git a/arma/client/addons/org/stringtable.xml b/arma/client/addons/org/stringtable.xml new file mode 100644 index 0000000..cd5b9ba --- /dev/null +++ b/arma/client/addons/org/stringtable.xml @@ -0,0 +1,8 @@ + + + + + Org + + + diff --git a/arma/client/addons/org/ui/RscCommon.hpp b/arma/client/addons/org/ui/RscCommon.hpp new file mode 100644 index 0000000..8b57936 --- /dev/null +++ b/arma/client/addons/org/ui/RscCommon.hpp @@ -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; diff --git a/arma/client/addons/org/ui/RscOrg.hpp b/arma/client/addons/org/ui/RscOrg.hpp new file mode 100644 index 0000000..d63a41c --- /dev/null +++ b/arma/client/addons/org/ui/RscOrg.hpp @@ -0,0 +1,21 @@ +class RscOrg { + idd = 1002; + fadeIn = 0; + fadeOut = 0; + duration = 1e011; + onLoad = "uiNamespace setVariable ['RscOrg', _this select 0]"; + onUnLoad = "uinamespace setVariable ['RscOrg', nil]"; + + class controlsBackground {}; + class controls { + class IFrame: RscText { + type = 106; + idc = 1003; + x = "safeZoneXAbs"; + y = "safeZoneY"; + w = "safeZoneWAbs"; + h = "safeZoneH"; + colorBackground[] = {0, 0, 0, 0}; + }; + }; +}; diff --git a/arma/client/addons/org/ui/_site.7z b/arma/client/addons/org/ui/_site.7z new file mode 100644 index 0000000..22e7cf6 Binary files /dev/null and b/arma/client/addons/org/ui/_site.7z differ diff --git a/arma/client/addons/org/ui/_site/index.html b/arma/client/addons/org/ui/_site/index.html new file mode 100644 index 0000000..b68277e --- /dev/null +++ b/arma/client/addons/org/ui/_site/index.html @@ -0,0 +1,243 @@ + + + + + + + Organization Dashboard + + + + + + +
+ +
+ +
+

Organization Name

+

FACTION-001

+
+
+ + +
+
+ + +
+ +
+
+

Overview

+
Active
+
+
+
+
+ Total Members + 24 +
+
+ Online Now + 8 +
+
+ Org Balance + $125,000 +
+
+ Reputation + Level 5 +
+
+
+
+ + +
+
+

Members Online

+ 8 +
+
+
+
+
+
+ John Doe + Leader +
+
+
+
+
+ Jane Smith + Officer +
+
+
+
+
+ Mike Johnson + Member +
+
+
+
+
+ Sarah Wilson + Member +
+
+
+
+
+ + +
+
+

Recent Activity

+
+
+
+
+
2m ago
+
Mike Johnson completed mission "Alpha Strike"
+
+
+
15m ago
+
Jane Smith deposited $5,000 to org bank
+
+
+
1h ago
+
New member Alex Brown joined the organization
+
+
+
2h ago
+
Organization captured territory: Zone-7
+
+
+
+
+ + +
+
+

Assets

+
+
+
+
+ 🏢 +
+ Headquarters + Downtown +
+
+
+ 🚁 +
+ Helicopters + 3 units +
+
+
+ 🚗 +
+ Vehicles + 12 units +
+
+
+ 📦 +
+ Storage Units + 5 locations +
+
+
+
+
+ + +
+
+

Active Missions

+ 3 +
+
+
+
+
+ Supply Run + High Priority +
+
Deliver supplies to northern outpost
+
+
+
+
+ 65% +
+
+
+
+ Recon Operation + Medium Priority +
+
Scout enemy positions in Zone-4
+
+
+
+
+ 30% +
+
+
+
+ Territory Defense + Low Priority +
+
Maintain control of captured zones
+
+
+
+
+ 90% +
+
+
+
+
+
+
+ + + + + diff --git a/arma/client/addons/org/ui/_site/script.js b/arma/client/addons/org/ui/_site/script.js new file mode 100644 index 0000000..43e1821 --- /dev/null +++ b/arma/client/addons/org/ui/_site/script.js @@ -0,0 +1,179 @@ +/** + * Organization Dashboard + * Handles real-time updates and interactions + */ + +// Mock data for demonstration +const mockData = { + org: { + name: "Black Phoenix Initiative", + tag: "BPI-001", + status: "Active" + }, + stats: { + totalMembers: 24, + onlineMembers: 8, + balance: 125000, + reputation: 5 + }, + membersOnline: [ + { name: "John Doe", rank: "Leader", online: true }, + { name: "Jane Smith", rank: "Officer", online: true }, + { name: "Mike Johnson", rank: "Member", online: true }, + { name: "Sarah Wilson", rank: "Member", online: true }, + { name: "Alex Brown", rank: "Member", online: true }, + { name: "Chris Davis", rank: "Member", online: true }, + { name: "Pat Lee", rank: "Recruit", online: true }, + { name: "Sam Taylor", rank: "Recruit", online: true } + ], + activities: [ + { time: "2m ago", text: "Mike Johnson completed mission \"Alpha Strike\"" }, + { time: "15m ago", text: "Jane Smith deposited $5,000 to org bank" }, + { time: "1h ago", text: "New member Alex Brown joined the organization" }, + { time: "2h ago", text: "Organization captured territory: Zone-7" } + ], + assets: [ + { icon: "🏢", name: "Headquarters", location: "Downtown" }, + { icon: "🚁", name: "Helicopters", location: "3 units" }, + { icon: "🚗", name: "Vehicles", location: "12 units" }, + { icon: "📦", name: "Storage Units", location: "5 locations" } + ], + missions: [ + { + name: "Supply Run", + priority: "high", + description: "Deliver supplies to northern outpost", + progress: 65 + }, + { + name: "Recon Operation", + priority: "medium", + description: "Scout enemy positions in Zone-4", + progress: 30 + }, + { + name: "Territory Defense", + priority: "low", + description: "Maintain control of captured zones", + progress: 90 + } + ] +}; + +// Update dashboard with data +function updateDashboard(data) { + // Update header + if (data.org) { + const orgName = document.querySelector('.org-name'); + const orgTag = document.querySelector('.org-tag'); + if (orgName) orgName.textContent = data.org.name; + if (orgTag) orgTag.textContent = data.org.tag; + } + + // Update stats + if (data.stats) { + const statValues = document.querySelectorAll('.stat-value'); + if (statValues[0]) statValues[0].textContent = data.stats.totalMembers; + if (statValues[1]) statValues[1].textContent = data.stats.onlineMembers; + if (statValues[2]) statValues[2].textContent = `$${data.stats.balance.toLocaleString()}`; + if (statValues[3]) statValues[3].textContent = `Level ${data.stats.reputation}`; + } +} + +// Event handlers +function setupEventHandlers() { + // Close button + const closeBtn = document.querySelector('.close-btn'); + if (closeBtn) { + closeBtn.addEventListener('click', () => { + console.log('Closing dashboard...'); + // Send close event to Arma + if (typeof A3API !== 'undefined') { + A3API.SendAlert(JSON.stringify({ + event: 'org::close', + data: {} + })); + } + }); + } + + // Settings button + const settingsBtn = document.querySelectorAll('.action-btn')[0]; + if (settingsBtn && !settingsBtn.classList.contains('close-btn')) { + settingsBtn.addEventListener('click', () => { + console.log('Opening settings...'); + // Send settings event to Arma + if (typeof A3API !== 'undefined') { + A3API.SendAlert(JSON.stringify({ + event: 'org::settings', + data: {} + })); + } + }); + } + + // Member item clicks + const memberItems = document.querySelectorAll('.member-item'); + memberItems.forEach((item, index) => { + item.addEventListener('click', () => { + console.log('Member clicked:', mockData.membersOnline[index]); + if (typeof A3API !== 'undefined') { + A3API.SendAlert(JSON.stringify({ + event: 'org::member::view', + data: { member: mockData.membersOnline[index] } + })); + } + }); + }); + + // Asset item clicks + const assetItems = document.querySelectorAll('.asset-item'); + assetItems.forEach((item, index) => { + item.addEventListener('click', () => { + console.log('Asset clicked:', mockData.assets[index]); + if (typeof A3API !== 'undefined') { + A3API.SendAlert(JSON.stringify({ + event: 'org::asset::view', + data: { asset: mockData.assets[index] } + })); + } + }); + }); + + // Mission item clicks + const missionItems = document.querySelectorAll('.mission-item'); + missionItems.forEach((item, index) => { + item.addEventListener('click', () => { + console.log('Mission clicked:', mockData.missions[index]); + if (typeof A3API !== 'undefined') { + A3API.SendAlert(JSON.stringify({ + event: 'org::mission::view', + data: { mission: mockData.missions[index] } + })); + } + }); + }); +} + +// Initialize dashboard +function initDashboard() { + console.log('Organization Dashboard initializing...'); + + // Update with mock data + updateDashboard(mockData); + + // Setup event handlers + setupEventHandlers(); + + console.log('Organization Dashboard initialized'); +} + +// Auto-initialize when DOM is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initDashboard); +} else { + initDashboard(); +} + +// Expose functions globally for Arma integration +window.updateOrgDashboard = updateDashboard; diff --git a/arma/client/addons/org/ui/_site/style.css b/arma/client/addons/org/ui/_site/style.css new file mode 100644 index 0000000..a4f93c1 --- /dev/null +++ b/arma/client/addons/org/ui/_site/style.css @@ -0,0 +1,469 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + height: 100vh; + width: 100vw; + background: rgba(0, 0, 0, 0.6); + font-family: Arial, sans-serif; + color: rgba(200, 220, 240, 0.95); + overflow: hidden; +} + +.dashboard-container { + height: 100vh; + width: 100vw; + padding: 2rem; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +/* Header Section */ +.dashboard-header { + display: flex; + align-items: center; + gap: 1.5rem; + padding: 1.25rem 1.5rem; + background: rgba(15, 20, 30, 0.9); + border: 1px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + box-shadow: + 0 0 20px rgba(100, 150, 200, 0.15), + 0 4px 16px rgba(0, 0, 0, 0.8); +} + +.org-logo { + width: 60px; + height: 60px; + background: rgba(20, 30, 45, 0.8); + border: 2px solid rgba(100, 150, 200, 0.5); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; +} + +.logo-placeholder { + font-size: 1.5rem; + font-weight: bold; + color: rgba(100, 150, 200, 0.9); +} + +.org-info { + flex: 1; +} + +.org-name { + font-size: 1.5rem; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + color: rgba(200, 220, 255, 1); + margin-bottom: 0.25rem; +} + +.org-tag { + font-size: 0.875rem; + color: rgba(140, 160, 180, 0.8); + letter-spacing: 1px; +} + +.header-actions { + display: flex; + gap: 0.75rem; +} + +.action-btn { + padding: 0.625rem 1.25rem; + background: rgba(20, 30, 45, 0.7); + border: 1px solid rgba(100, 150, 200, 0.4); + border-radius: 4px; + color: rgba(200, 220, 240, 0.95); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + transition: all 0.15s ease; +} + +.action-btn:hover { + background: rgba(30, 45, 70, 0.9); + border-color: rgba(150, 200, 255, 0.7); + box-shadow: + 0 0 15px rgba(100, 150, 200, 0.2), + inset 0 0 20px rgba(100, 150, 200, 0.05); +} + +.close-btn { + border-color: rgba(200, 100, 100, 0.4); +} + +.close-btn:hover { + border-color: rgba(255, 100, 100, 0.7); + box-shadow: + 0 0 15px rgba(200, 100, 100, 0.2), + inset 0 0 20px rgba(200, 100, 100, 0.05); +} + +/* Dashboard Grid */ +.dashboard-grid { + flex: 1; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.25rem; + overflow-y: auto; + padding-right: 0.5rem; +} + +/* Custom Scrollbar */ +.dashboard-grid::-webkit-scrollbar { + width: 8px; +} + +.dashboard-grid::-webkit-scrollbar-track { + background: rgba(15, 20, 30, 0.5); + border-radius: 4px; +} + +.dashboard-grid::-webkit-scrollbar-thumb { + background: rgba(100, 150, 200, 0.3); + border-radius: 4px; +} + +.dashboard-grid::-webkit-scrollbar-thumb:hover { + background: rgba(100, 150, 200, 0.5); +} + +/* Dashboard Cards */ +.dashboard-card { + background: rgba(15, 20, 30, 0.9); + border: 1px solid rgba(100, 150, 200, 0.4); + border-left: 3px solid rgba(100, 150, 200, 0.5); + border-radius: 4px; + padding: 1.25rem; + box-shadow: + 0 0 20px rgba(100, 150, 200, 0.1), + 0 4px 16px rgba(0, 0, 0, 0.6); + transition: all 0.2s ease; +} + +.dashboard-card:hover { + border-left-color: rgba(150, 200, 255, 0.8); + box-shadow: + 0 0 25px rgba(100, 150, 200, 0.2), + 0 4px 20px rgba(0, 0, 0, 0.7); +} + +.card-wide { + grid-column: span 2; +} + +.card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid rgba(100, 150, 200, 0.2); +} + +.card-title { + font-size: 1rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(200, 220, 255, 1); +} + +.card-status { + padding: 0.25rem 0.75rem; + background: rgba(100, 200, 150, 0.2); + border: 1px solid rgba(100, 200, 150, 0.4); + border-radius: 3px; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(150, 255, 200, 0.9); +} + +.card-badge { + padding: 0.25rem 0.625rem; + background: rgba(100, 150, 200, 0.2); + border: 1px solid rgba(100, 150, 200, 0.4); + border-radius: 3px; + font-size: 0.75rem; + font-weight: 600; + color: rgba(150, 200, 255, 0.9); +} + +.card-content { + color: rgba(180, 200, 220, 0.9); +} + +/* Stat Grid */ +.stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 1.25rem; +} + +.stat-item { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 1rem; + background: rgba(20, 30, 45, 0.6); + border: 1px solid rgba(100, 150, 200, 0.3); + border-radius: 4px; +} + +.stat-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(140, 160, 180, 0.85); +} + +.stat-value { + font-size: 1.75rem; + font-weight: 600; + color: rgba(200, 220, 255, 1); +} + +/* Member List */ +.member-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.member-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem; + background: rgba(20, 30, 45, 0.5); + border: 1px solid rgba(100, 150, 200, 0.2); + border-radius: 4px; + transition: all 0.15s ease; +} + +.member-item:hover { + background: rgba(30, 45, 70, 0.7); + border-color: rgba(100, 150, 200, 0.4); +} + +.member-status { + width: 10px; + height: 10px; + border-radius: 50%; + background: rgba(100, 100, 100, 0.5); +} + +.member-status.online { + background: rgba(100, 200, 150, 0.9); + box-shadow: 0 0 8px rgba(100, 200, 150, 0.5); +} + +.member-info { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.member-name { + font-size: 0.875rem; + font-weight: 500; + color: rgba(200, 220, 240, 0.95); +} + +.member-rank { + font-size: 0.75rem; + color: rgba(140, 160, 180, 0.8); +} + +/* Activity List */ +.activity-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.activity-item { + padding: 0.75rem; + background: rgba(20, 30, 45, 0.5); + border-left: 2px solid rgba(100, 150, 200, 0.4); + border-radius: 2px; +} + +.activity-time { + font-size: 0.7rem; + color: rgba(100, 150, 200, 0.7); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 0.375rem; +} + +.activity-text { + font-size: 0.875rem; + color: rgba(180, 200, 220, 0.9); + line-height: 1.4; +} + +/* Asset List */ +.asset-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.asset-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.75rem; + background: rgba(20, 30, 45, 0.5); + border: 1px solid rgba(100, 150, 200, 0.2); + border-radius: 4px; + transition: all 0.15s ease; +} + +.asset-item:hover { + background: rgba(30, 45, 70, 0.7); + border-color: rgba(100, 150, 200, 0.4); +} + +.asset-icon { + font-size: 1.5rem; +} + +.asset-info { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.asset-name { + font-size: 0.875rem; + font-weight: 500; + color: rgba(200, 220, 240, 0.95); +} + +.asset-location { + font-size: 0.75rem; + color: rgba(140, 160, 180, 0.8); +} + +/* Mission List */ +.mission-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.mission-item { + padding: 1rem; + background: rgba(20, 30, 45, 0.6); + border: 1px solid rgba(100, 150, 200, 0.3); + border-left: 3px solid rgba(100, 150, 200, 0.5); + border-radius: 4px; +} + +.mission-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.5rem; +} + +.mission-name { + font-size: 0.95rem; + font-weight: 600; + color: rgba(200, 220, 255, 1); +} + +.mission-priority { + padding: 0.25rem 0.625rem; + border-radius: 3px; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 600; +} + +.mission-priority.high { + background: rgba(200, 100, 100, 0.2); + border: 1px solid rgba(200, 100, 100, 0.4); + color: rgba(255, 150, 150, 0.9); +} + +.mission-priority.medium { + background: rgba(200, 150, 100, 0.2); + border: 1px solid rgba(200, 150, 100, 0.4); + color: rgba(255, 200, 150, 0.9); +} + +.mission-priority.low { + background: rgba(100, 150, 200, 0.2); + border: 1px solid rgba(100, 150, 200, 0.4); + color: rgba(150, 200, 255, 0.9); +} + +.mission-description { + font-size: 0.85rem; + color: rgba(160, 180, 200, 0.85); + margin-bottom: 0.75rem; + line-height: 1.4; +} + +.mission-progress { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.progress-bar { + flex: 1; + height: 6px; + background: rgba(20, 30, 45, 0.8); + border: 1px solid rgba(100, 150, 200, 0.3); + border-radius: 3px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, + rgba(100, 150, 200, 0.6), + rgba(150, 200, 255, 0.8)); + box-shadow: 0 0 10px rgba(100, 150, 200, 0.5); + transition: width 0.3s ease; +} + +.progress-text { + font-size: 0.75rem; + font-weight: 600; + color: rgba(100, 150, 200, 0.9); + min-width: 40px; +} + +/* Responsive adjustments */ +@media (max-width: 1200px) { + .card-wide { + grid-column: span 1; + } +} + +@media (max-width: 768px) { + .dashboard-container { + padding: 1rem; + } + + .dashboard-grid { + grid-template-columns: 1fr; + } +} diff --git a/arma/client/client.code-workspace b/arma/client/client.code-workspace new file mode 100644 index 0000000..e24ee21 --- /dev/null +++ b/arma/client/client.code-workspace @@ -0,0 +1,22 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "editor.insertSpaces": true, + "editor.detectIndentation": false, + "files.autoSave": "onFocusChange", + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "files.associations": { + "*.cpp": "arma-config", + "*.hpp": "arma-config", + "*.inc": "arma-config", + "*.cfg": "arma-config", + "*.rvmat": "arma-config" + } + } +} diff --git a/arma/client/docs/README.md b/arma/client/docs/README.md new file mode 100644 index 0000000..8320d12 --- /dev/null +++ b/arma/client/docs/README.md @@ -0,0 +1,49 @@ + + +

forge-client

+

+ + forge-client Version + + + forge-client Issues + + + forge-client Downloads + + + forge-client License + +
+ HEMTT +

+ +

+ Requires the latest version of CBA A3 +

+ +# Initial Project Setup! +Delete this section after the project has been initially set up: +1. Find and replace all instances of `forge-client` with the mod's name. +2. Find and replace all instances of `MOD_REPO` with the mod's name *and no spaces*. + - This should be the name of the repository on GitHub. +3. Find and replace all instances of `forge_client` with the mod's prefix. + - This should be all lowercase. +4. Find and replace all instances of `MOD_ACRONYM` with the mod's acronym. + - This should be all uppercase. +5. After the initial Steam upload, find and replace all instances of `MOD_ID` with the mod's Steam Workshop id. + +For third parties, make sure to also replace `IDSolutions` with your Github username / organization name, and to replace `DartRuffian` with your username. + +**forge-client** (MOD_ACRONYM) aims to... + +The project is entirely **open-source** and any contributions are welcome. + +## Core Features +- Feature + +## Contributing +For new contributers, see the [Contributing Setup & Guidelines](./.github/CONTRIBUTING.md). + +## License +forge-client is licensed under [APL-ND](./LICENSE.md). diff --git a/arma/client/extra/example_addon/$PBOPREFIX$ b/arma/client/extra/example_addon/$PBOPREFIX$ new file mode 100644 index 0000000..ae5f222 --- /dev/null +++ b/arma/client/extra/example_addon/$PBOPREFIX$ @@ -0,0 +1 @@ +forge\forge_client\addons\addonName diff --git a/arma/client/extra/example_addon/CfgEventHandlers.hpp b/arma/client/extra/example_addon/CfgEventHandlers.hpp new file mode 100644 index 0000000..865276c --- /dev/null +++ b/arma/client/extra/example_addon/CfgEventHandlers.hpp @@ -0,0 +1,11 @@ +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)); + }; +}; diff --git a/arma/client/extra/example_addon/README.md b/arma/client/extra/example_addon/README.md new file mode 100644 index 0000000..87592d8 --- /dev/null +++ b/arma/client/extra/example_addon/README.md @@ -0,0 +1,4 @@ +forge_client_addonName +=================== + +Description for this addon diff --git a/arma/client/extra/example_addon/XEH_PREP.hpp b/arma/client/extra/example_addon/XEH_PREP.hpp new file mode 100644 index 0000000..171d26d --- /dev/null +++ b/arma/client/extra/example_addon/XEH_PREP.hpp @@ -0,0 +1 @@ +// PREP(empty); diff --git a/arma/client/extra/example_addon/XEH_preInit.sqf b/arma/client/extra/example_addon/XEH_preInit.sqf new file mode 100644 index 0000000..8d45ad6 --- /dev/null +++ b/arma/client/extra/example_addon/XEH_preInit.sqf @@ -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" diff --git a/arma/client/extra/example_addon/XEH_preStart.sqf b/arma/client/extra/example_addon/XEH_preStart.sqf new file mode 100644 index 0000000..0228885 --- /dev/null +++ b/arma/client/extra/example_addon/XEH_preStart.sqf @@ -0,0 +1,3 @@ +#include "script_component.hpp" + +#include "XEH_PREP.hpp" diff --git a/arma/client/extra/example_addon/config.cpp b/arma/client/extra/example_addon/config.cpp new file mode 100644 index 0000000..78d0972 --- /dev/null +++ b/arma/client/extra/example_addon/config.cpp @@ -0,0 +1,19 @@ +#include "script_component.hpp" + +class CfgPatches { + class ADDON { + author = AUTHOR; + authors[] = {"You!"}; + url = ECSTRING(main,url); + name = COMPONENT_NAME; + requiredVersion = REQUIRED_VERSION; + requiredAddons[] = { + "forge_client_main" + }; + units[] = {}; + weapons[] = {}; + VERSION_CONFIG; + }; +}; + +#include "CfgEventHandlers.hpp" diff --git a/arma/client/extra/example_addon/functions/fnc_empty.sqf b/arma/client/extra/example_addon/functions/fnc_empty.sqf new file mode 100644 index 0000000..07b0282 --- /dev/null +++ b/arma/client/extra/example_addon/functions/fnc_empty.sqf @@ -0,0 +1,16 @@ +#include "..\script_component.hpp" +/* + * Author: You! + * An empty function that does nothing. + * + * Arguments: + * None + * + * Return Value: + * None + * + * Example: + * [] call forge_client_addonName_fnc_empty; + * + * Public: No + */ diff --git a/arma/client/extra/example_addon/initKeybinds.inc.sqf b/arma/client/extra/example_addon/initKeybinds.inc.sqf new file mode 100644 index 0000000..0e6dd20 --- /dev/null +++ b/arma/client/extra/example_addon/initKeybinds.inc.sqf @@ -0,0 +1,8 @@ +#include "\forge\forge_client\addons\main\data\hpp\defineDIKCodes.hpp" + +[ + _category, QGVAR(key_doTheThing), + [LSTRING(doTheThing_name), LSTRING(doTheThing_tooltip)], { + // ... + }, {}, [DIK_KEYNAME, _shift, _ctrl, _alt] // Default keybind +] call CBA_fnc_addKeybind; diff --git a/arma/client/extra/example_addon/initSettings.inc.sqf b/arma/client/extra/example_addon/initSettings.inc.sqf new file mode 100644 index 0000000..b29e374 --- /dev/null +++ b/arma/client/extra/example_addon/initSettings.inc.sqf @@ -0,0 +1,6 @@ +// Can use localize "STR_ACE_Common_Enabled" for name if ACE is required +[ + QGVAR(enabled), "CHECKBOX", + [LSTRING(enabled_name), LSTRING(enabled_tooltip)], + _category, true, true +] call CBA_fnc_addSetting; diff --git a/arma/client/extra/example_addon/script_component.hpp b/arma/client/extra/example_addon/script_component.hpp new file mode 100644 index 0000000..ef74027 --- /dev/null +++ b/arma/client/extra/example_addon/script_component.hpp @@ -0,0 +1,9 @@ +#define COMPONENT addonName +#define COMPONENT_BEAUTIFIED Addon Name +#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" diff --git a/arma/client/extra/example_addon/stringtable.xml b/arma/client/extra/example_addon/stringtable.xml new file mode 100644 index 0000000..51ac06f --- /dev/null +++ b/arma/client/extra/example_addon/stringtable.xml @@ -0,0 +1,8 @@ + + + + + Addon Name + + + diff --git a/arma/client/extra/example_subaddon/config.cpp b/arma/client/extra/example_subaddon/config.cpp new file mode 100644 index 0000000..ed5ce38 --- /dev/null +++ b/arma/client/extra/example_subaddon/config.cpp @@ -0,0 +1,18 @@ +#include "script_component.hpp" + +class CfgPatches { + class SUBADDON { + author = AUTHOR; + authors[] = {"You!"}; + url = ECSTRING(main,url); + name = COMPONENT_NAME; + addonRootClass = QUOTE(ADDON); + requiredVersion = REQUIRED_VERSION; + requiredAddons[] = { + QUOTE(ADDON) + }; + units[] = {}; + weapons[] = {}; + VERSION_CONFIG; + }; +}; diff --git a/arma/client/extra/example_subaddon/script_component.hpp b/arma/client/extra/example_subaddon/script_component.hpp new file mode 100644 index 0000000..63dfda0 --- /dev/null +++ b/arma/client/extra/example_subaddon/script_component.hpp @@ -0,0 +1,4 @@ +#define SUBCOMPONENT subaddonName +#define SUBCOMPONENT_BEAUTIFIED Sub Addon + +#include "..\script_component.hpp" diff --git a/arma/client/icon_128_ca.paa b/arma/client/icon_128_ca.paa new file mode 100644 index 0000000..02dc39e Binary files /dev/null and b/arma/client/icon_128_ca.paa differ diff --git a/arma/client/icon_128_highlight_ca.paa b/arma/client/icon_128_highlight_ca.paa new file mode 100644 index 0000000..8374f2a Binary files /dev/null and b/arma/client/icon_128_highlight_ca.paa differ diff --git a/arma/client/icon_64_ca.paa b/arma/client/icon_64_ca.paa new file mode 100644 index 0000000..afcab39 Binary files /dev/null and b/arma/client/icon_64_ca.paa differ diff --git a/arma/client/include/a3/ui_f/hpp/defineDIKCodes.inc b/arma/client/include/a3/ui_f/hpp/defineDIKCodes.inc new file mode 100644 index 0000000..4031b6b --- /dev/null +++ b/arma/client/include/a3/ui_f/hpp/defineDIKCodes.inc @@ -0,0 +1,190 @@ +#ifndef DIK_ESCAPE + +/**************************************************************************** + * + * DirectInput keyboard scan codes + * + ****************************************************************************/ + +#define DIK_ESCAPE 0x01 +#define DIK_1 0x02 +#define DIK_2 0x03 +#define DIK_3 0x04 +#define DIK_4 0x05 +#define DIK_5 0x06 +#define DIK_6 0x07 +#define DIK_7 0x08 +#define DIK_8 0x09 +#define DIK_9 0x0A +#define DIK_0 0x0B +#define DIK_MINUS 0x0C /* - on main keyboard */ +#define DIK_EQUALS 0x0D +#define DIK_BACK 0x0E /* backspace */ +#define DIK_TAB 0x0F +#define DIK_Q 0x10 +#define DIK_W 0x11 +#define DIK_E 0x12 +#define DIK_R 0x13 +#define DIK_T 0x14 +#define DIK_Y 0x15 +#define DIK_U 0x16 +#define DIK_I 0x17 +#define DIK_O 0x18 +#define DIK_P 0x19 +#define DIK_LBRACKET 0x1A +#define DIK_RBRACKET 0x1B +#define DIK_RETURN 0x1C /* Enter on main keyboard */ +#define DIK_LCONTROL 0x1D +#define DIK_A 0x1E +#define DIK_S 0x1F +#define DIK_D 0x20 +#define DIK_F 0x21 +#define DIK_G 0x22 +#define DIK_H 0x23 +#define DIK_J 0x24 +#define DIK_K 0x25 +#define DIK_L 0x26 +#define DIK_SEMICOLON 0x27 +#define DIK_APOSTROPHE 0x28 +#define DIK_GRAVE 0x29 /* accent grave */ +#define DIK_LSHIFT 0x2A +#define DIK_BACKSLASH 0x2B +#define DIK_Z 0x2C +#define DIK_X 0x2D +#define DIK_C 0x2E +#define DIK_V 0x2F +#define DIK_B 0x30 +#define DIK_N 0x31 +#define DIK_M 0x32 +#define DIK_COMMA 0x33 +#define DIK_PERIOD 0x34 /* . on main keyboard */ +#define DIK_SLASH 0x35 /* / on main keyboard */ +#define DIK_RSHIFT 0x36 +#define DIK_MULTIPLY 0x37 /* * on numeric keypad */ +#define DIK_LMENU 0x38 /* left Alt */ +#define DIK_SPACE 0x39 +#define DIK_CAPITAL 0x3A +#define DIK_F1 0x3B +#define DIK_F2 0x3C +#define DIK_F3 0x3D +#define DIK_F4 0x3E +#define DIK_F5 0x3F +#define DIK_F6 0x40 +#define DIK_F7 0x41 +#define DIK_F8 0x42 +#define DIK_F9 0x43 +#define DIK_F10 0x44 +#define DIK_NUMLOCK 0x45 +#define DIK_SCROLL 0x46 /* Scroll Lock */ +#define DIK_NUMPAD7 0x47 +#define DIK_NUMPAD8 0x48 +#define DIK_NUMPAD9 0x49 +#define DIK_SUBTRACT 0x4A /* - on numeric keypad */ +#define DIK_NUMPAD4 0x4B +#define DIK_NUMPAD5 0x4C +#define DIK_NUMPAD6 0x4D +#define DIK_ADD 0x4E /* + on numeric keypad */ +#define DIK_NUMPAD1 0x4F +#define DIK_NUMPAD2 0x50 +#define DIK_NUMPAD3 0x51 +#define DIK_NUMPAD0 0x52 +#define DIK_DECIMAL 0x53 /* . on numeric keypad */ +#define DIK_OEM_102 0x56 /* < > | on UK/Germany keyboards */ +#define DIK_F11 0x57 +#define DIK_F12 0x58 + +#define DIK_F13 0x64 /* (NEC PC98) */ +#define DIK_F14 0x65 /* (NEC PC98) */ +#define DIK_F15 0x66 /* (NEC PC98) */ + +#define DIK_KANA 0x70 /* (Japanese keyboard) */ +#define DIK_ABNT_C1 0x73 /* / ? on Portugese (Brazilian) keyboards */ +#define DIK_CONVERT 0x79 /* (Japanese keyboard) */ +#define DIK_NOCONVERT 0x7B /* (Japanese keyboard) */ +#define DIK_YEN 0x7D /* (Japanese keyboard) */ +#define DIK_ABNT_C2 0x7E /* Numpad . on Portugese (Brazilian) keyboards */ +#define DIK_NUMPADEQUALS 0x8D /* = on numeric keypad (NEC PC98) */ +#define DIK_PREVTRACK 0x90 /* Previous Track (DIK_CIRCUMFLEX on Japanese keyboard) */ +#define DIK_AT 0x91 /* (NEC PC98) */ +#define DIK_COLON 0x92 /* (NEC PC98) */ +#define DIK_UNDERLINE 0x93 /* (NEC PC98) */ +#define DIK_KANJI 0x94 /* (Japanese keyboard) */ +#define DIK_STOP 0x95 /* (NEC PC98) */ +#define DIK_AX 0x96 /* (Japan AX) */ +#define DIK_UNLABELED 0x97 /* (J3100) */ +#define DIK_NEXTTRACK 0x99 /* Next Track */ +#define DIK_NUMPADENTER 0x9C /* Enter on numeric keypad */ +#define DIK_RCONTROL 0x9D +#define DIK_MUTE 0xA0 /* Mute */ +#define DIK_CALCULATOR 0xA1 /* Calculator */ +#define DIK_PLAYPAUSE 0xA2 /* Play / Pause */ +#define DIK_MEDIASTOP 0xA4 /* Media Stop */ +#define DIK_VOLUMEDOWN 0xAE /* Volume - */ +#define DIK_VOLUMEUP 0xB0 /* Volume + */ +#define DIK_WEBHOME 0xB2 /* Web home */ +#define DIK_NUMPADCOMMA 0xB3 /* , on numeric keypad (NEC PC98) */ +#define DIK_DIVIDE 0xB5 /* / on numeric keypad */ +#define DIK_SYSRQ 0xB7 +#define DIK_RMENU 0xB8 /* right Alt */ +#define DIK_PAUSE 0xC5 /* Pause */ +#define DIK_HOME 0xC7 /* Home on arrow keypad */ +#define DIK_UP 0xC8 /* UpArrow on arrow keypad */ +#define DIK_PRIOR 0xC9 /* PgUp on arrow keypad */ +#define DIK_LEFT 0xCB /* LeftArrow on arrow keypad */ +#define DIK_RIGHT 0xCD /* RightArrow on arrow keypad */ +#define DIK_END 0xCF /* End on arrow keypad */ +#define DIK_DOWN 0xD0 /* DownArrow on arrow keypad */ +#define DIK_NEXT 0xD1 /* PgDn on arrow keypad */ +#define DIK_INSERT 0xD2 /* Insert on arrow keypad */ +#define DIK_DELETE 0xD3 /* Delete on arrow keypad */ +#define DIK_LWIN 0xDB /* Left Windows key */ +#define DIK_RWIN 0xDC /* Right Windows key */ +#define DIK_APPS 0xDD /* AppMenu key */ +#define DIK_POWER 0xDE /* System Power */ +#define DIK_SLEEP 0xDF /* System Sleep */ +#define DIK_WAKE 0xE3 /* System Wake */ +#define DIK_WEBSEARCH 0xE5 /* Web Search */ +#define DIK_WEBFAVORITES 0xE6 /* Web Favorites */ +#define DIK_WEBREFRESH 0xE7 /* Web Refresh */ +#define DIK_WEBSTOP 0xE8 /* Web Stop */ +#define DIK_WEBFORWARD 0xE9 /* Web Forward */ +#define DIK_WEBBACK 0xEA /* Web Back */ +#define DIK_MYCOMPUTER 0xEB /* My Computer */ +#define DIK_MAIL 0xEC /* Mail */ +#define DIK_MEDIASELECT 0xED /* Media Select */ + +/* + * Alternate names for keys, to facilitate transition from DOS. + */ +#define DIK_BACKSPACE DIK_BACK /* backspace */ +#define DIK_NUMPADSTAR DIK_MULTIPLY /* * on numeric keypad */ +#define DIK_LALT DIK_LMENU /* left Alt */ +#define DIK_CAPSLOCK DIK_CAPITAL /* CapsLock */ +#define DIK_NUMPADMINUS DIK_SUBTRACT /* - on numeric keypad */ +#define DIK_NUMPADPLUS DIK_ADD /* + on numeric keypad */ +#define DIK_NUMPADPERIOD DIK_DECIMAL /* . on numeric keypad */ +#define DIK_NUMPADSLASH DIK_DIVIDE /* / on numeric keypad */ +#define DIK_RALT DIK_RMENU /* right Alt */ +#define DIK_UPARROW DIK_UP /* UpArrow on arrow keypad */ +#define DIK_PGUP DIK_PRIOR /* PgUp on arrow keypad */ +#define DIK_LEFTARROW DIK_LEFT /* LeftArrow on arrow keypad */ +#define DIK_RIGHTARROW DIK_RIGHT /* RightArrow on arrow keypad */ +#define DIK_DOWNARROW DIK_DOWN /* DownArrow on arrow keypad */ +#define DIK_PGDN DIK_NEXT /* PgDn on arrow keypad */ + +/* + * Alternate names for keys originally not used on US keyboards. + */ +#define DIK_CIRCUMFLEX DIK_PREVTRACK /* Japanese keyboard */ + + +/* + * Combination keys + */ +#define INPUT_CTRL_OFFSET 512 +#define INPUT_SHIFT_OFFSET 1024 +#define INPUT_ALT_OFFSET 2048 + + +#endif /* DIK_ESCAPE */ + diff --git a/arma/client/include/x/cba/addons/main/script_macros_common.hpp b/arma/client/include/x/cba/addons/main/script_macros_common.hpp new file mode 100644 index 0000000..be13021 --- /dev/null +++ b/arma/client/include/x/cba/addons/main/script_macros_common.hpp @@ -0,0 +1,1833 @@ +/* + Header: script_macros_common.hpp + + Description: + A general set of useful macro functions for use by CBA itself or by any module that uses CBA. + + Authors: + Sickboy and Spooner +*/ + +/* **************************************************** + New - Should be exported to general addon + Aim: + - Simplify (shorten) the amount of characters required for repetitive tasks + - Provide a solid structure that can be dynamic and easy editable (Which sometimes means we cannot adhere to Aim #1 ;-) + An example is the path that is built from defines. Some available in this file, others in mods and addons. + + Follows Standard: + Object variables: PREFIX_COMPONENT + Main-object variables: PREFIX_main + Paths: MAINPREFIX\PREFIX\SUBPREFIX\COMPONENT\SCRIPTNAME.sqf + e.g: x\six\addons\sys_menu\fDate.sqf + + Usage: + define PREFIX and COMPONENT, then include this file + (Note, you could have a main addon for your mod, define the PREFIX in a macros.hpp, + and include this script_macros_common.hpp file. + Then in your addons, add a component.hpp, define the COMPONENT, + and include your mod's script_macros.hpp + In your scripts you can then include the addon's component.hpp with relative path) + + TODO: + - Try only to use 1 string type " vs ' + - Evaluate double functions, and simplification + - Evaluate naming scheme; current = prototype + - Evaluate "Debug" features.. + - Evaluate "create mini function per precompiled script, that will load the script on first usage, rather than on init" + - Also saw "Namespace" typeName, evaluate which we need :P + - Single/Multi player gamelogics? (Incase of MP, you would want only 1 gamelogic per component, which is pv'ed from server, etc) + */ + +#ifndef MAINPREFIX + #define MAINPREFIX x +#endif + +#ifndef SUBPREFIX + #define SUBPREFIX addons +#endif + +#ifndef MAINLOGIC + #define MAINLOGIC main +#endif + +#define ADDON DOUBLES(PREFIX,COMPONENT) +#define MAIN_ADDON DOUBLES(PREFIX,main) + +/* ------------------------------------------- +Macro: VERSION_CONFIG + Define CBA Versioning System config entries. + + VERSION should be a floating-point number (1 separator). + VERSION_STR is a string representation of the version. + VERSION_AR is an array representation of the version. + + VERSION must always be defined, otherwise it is 0. + VERSION_STR and VERSION_AR default to VERSION if undefined. + +Parameters: + None + +Example: + (begin example) + #define VERSION 1.0 + #define VERSION_STR 1.0.1 + #define VERSION_AR 1,0,1 + + class CfgPatches { + class MyMod_main { + VERSION_CONFIG; + }; + }; + (end) + +Author: + ?, Jonpas +------------------------------------------- */ +#ifndef VERSION + #define VERSION 0 +#endif + +#ifndef VERSION_STR + #define VERSION_STR VERSION +#endif + +#ifndef VERSION_AR + #define VERSION_AR VERSION +#endif + +#ifndef VERSION_CONFIG + #define VERSION_CONFIG version = VERSION; versionStr = QUOTE(VERSION_STR); versionAr[] = {VERSION_AR} +#endif + +/* ------------------------------------------- +Group: Debugging +------------------------------------------- */ + +/* ------------------------------------------- +Macros: DEBUG_MODE_x + Managing debugging based on debug level. + + According to the *highest* level of debugging that has been defined *before* script_macros_common.hpp is included, + only the appropriate debugging commands will be functional. With no level explicitely defined, assume DEBUG_MODE_NORMAL. + + DEBUG_MODE_FULL - Full debugging output. + DEBUG_MODE_NORMAL - All debugging except and (Default setting if none specified). + DEBUG_MODE_MINIMAL - Only and enabled. + +Examples: + In order to turn on full debugging for a single file, + (begin example) + // Top of individual script file. + #define DEBUG_MODE_FULL + #include "script_component.hpp" + (end) + + In order to force minimal debugging for a single component, + (begin example) + // Top of addons\\script_component.hpp + // Ensure that any FULL and NORMAL setting from the individual files are undefined and MINIMAL is set. + #ifdef DEBUG_MODE_FULL + #undef DEBUG_MODE_FULL + #endif + #ifdef DEBUG_MODE_NORMAL + #undef DEBUG_MODE_NORMAL + #endif + #ifndef DEBUG_MODE_MINIMAL + #define DEBUG_MODE_MINIMAL + #endif + #include "script_macros.hpp" + (end) + + In order to turn on full debugging for a whole addon, + (begin example) + // Top of addons\main\script_macros.hpp + #ifndef DEBUG_MODE_FULL + #define DEBUG_MODE_FULL + #endif + #include "\x\cba\addons\main\script_macros_common.hpp" + (end) + +Author: + Spooner +------------------------------------------- */ + +// If DEBUG_MODE_FULL, then also enable DEBUG_MODE_NORMAL. +#ifdef DEBUG_MODE_FULL +#define DEBUG_MODE_NORMAL +#endif + +// If DEBUG_MODE_NORMAL, then also enable DEBUG_MODE_MINIMAL. +#ifdef DEBUG_MODE_NORMAL +#define DEBUG_MODE_MINIMAL +#endif + +// If no debug modes specified, use DEBUG_MODE_NORMAL (+ DEBUG_MODE_MINIMAL). +#ifndef DEBUG_MODE_MINIMAL +#define DEBUG_MODE_NORMAL +#define DEBUG_MODE_MINIMAL +#endif + +#define LOG_SYS_FORMAT(LEVEL,MESSAGE) format ['[%1] (%2) %3: %4', toUpper 'PREFIX', 'COMPONENT', LEVEL, MESSAGE] + +#ifdef DEBUG_SYNCHRONOUS +#define LOG_SYS(LEVEL,MESSAGE) diag_log text LOG_SYS_FORMAT(LEVEL,MESSAGE) +#else +#define LOG_SYS(LEVEL,MESSAGE) LOG_SYS_FORMAT(LEVEL,MESSAGE) call CBA_fnc_log +#endif + +#define LOG_SYS_FILELINENUMBERS(LEVEL,MESSAGE) LOG_SYS(LEVEL,format [ARR_4('%1 %2:%3',MESSAGE,__FILE__,__LINE__ + 1)]) + +/* ------------------------------------------- +Macro: LOG() + Log a debug message into the RPT log. + + Only run if is defined. + +Parameters: + MESSAGE - Message to record + +Example: + (begin example) + LOG("Initiated clog-dancing simulator."); + (end) + +Author: + Spooner +------------------------------------------- */ +#ifdef DEBUG_MODE_FULL + +#define LOG(MESSAGE) LOG_SYS('LOG',MESSAGE) +#define LOG_1(MESSAGE,ARG1) LOG(FORMAT_1(MESSAGE,ARG1)) +#define LOG_2(MESSAGE,ARG1,ARG2) LOG(FORMAT_2(MESSAGE,ARG1,ARG2)) +#define LOG_3(MESSAGE,ARG1,ARG2,ARG3) LOG(FORMAT_3(MESSAGE,ARG1,ARG2,ARG3)) +#define LOG_4(MESSAGE,ARG1,ARG2,ARG3,ARG4) LOG(FORMAT_4(MESSAGE,ARG1,ARG2,ARG3,ARG4)) +#define LOG_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5) LOG(FORMAT_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5)) +#define LOG_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) LOG(FORMAT_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6)) +#define LOG_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) LOG(FORMAT_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7)) +#define LOG_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) LOG(FORMAT_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8)) + +#else + +#define LOG(MESSAGE) /* disabled */ +#define LOG_1(MESSAGE,ARG1) /* disabled */ +#define LOG_2(MESSAGE,ARG1,ARG2) /* disabled */ +#define LOG_3(MESSAGE,ARG1,ARG2,ARG3) /* disabled */ +#define LOG_4(MESSAGE,ARG1,ARG2,ARG3,ARG4) /* disabled */ +#define LOG_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5) /* disabled */ +#define LOG_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) /* disabled */ +#define LOG_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) /* disabled */ +#define LOG_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) /* disabled */ + +#endif + +/* ------------------------------------------- +Macro: INFO() + Record a message without file and line number in the RPT log. + +Parameters: + MESSAGE - Message to record + +Example: + (begin example) + INFO("Mod X is loaded, do Y"); + (end) + +Author: + commy2 +------------------------------------------- */ +#define INFO(MESSAGE) LOG_SYS('INFO',MESSAGE) +#define INFO_1(MESSAGE,ARG1) INFO(FORMAT_1(MESSAGE,ARG1)) +#define INFO_2(MESSAGE,ARG1,ARG2) INFO(FORMAT_2(MESSAGE,ARG1,ARG2)) +#define INFO_3(MESSAGE,ARG1,ARG2,ARG3) INFO(FORMAT_3(MESSAGE,ARG1,ARG2,ARG3)) +#define INFO_4(MESSAGE,ARG1,ARG2,ARG3,ARG4) INFO(FORMAT_4(MESSAGE,ARG1,ARG2,ARG3,ARG4)) +#define INFO_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5) INFO(FORMAT_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5)) +#define INFO_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) INFO(FORMAT_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6)) +#define INFO_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) INFO(FORMAT_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7)) +#define INFO_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) INFO(FORMAT_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8)) + +/* ------------------------------------------- +Macro: WARNING() + Record a non-critical error in the RPT log. + + Only run if or higher is defined. + +Parameters: + MESSAGE - Message to record + +Example: + (begin example) + WARNING("This function has been deprecated. Please don't use it in future!"); + (end) + +Author: + Spooner +------------------------------------------- */ +#ifdef DEBUG_MODE_NORMAL + +#define WARNING(MESSAGE) LOG_SYS('WARNING',MESSAGE) +#define WARNING_1(MESSAGE,ARG1) WARNING(FORMAT_1(MESSAGE,ARG1)) +#define WARNING_2(MESSAGE,ARG1,ARG2) WARNING(FORMAT_2(MESSAGE,ARG1,ARG2)) +#define WARNING_3(MESSAGE,ARG1,ARG2,ARG3) WARNING(FORMAT_3(MESSAGE,ARG1,ARG2,ARG3)) +#define WARNING_4(MESSAGE,ARG1,ARG2,ARG3,ARG4) WARNING(FORMAT_4(MESSAGE,ARG1,ARG2,ARG3,ARG4)) +#define WARNING_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5) WARNING(FORMAT_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5)) +#define WARNING_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) WARNING(FORMAT_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6)) +#define WARNING_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) WARNING(FORMAT_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7)) +#define WARNING_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) WARNING(FORMAT_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8)) + +#else + +#define WARNING(MESSAGE) /* disabled */ +#define WARNING_1(MESSAGE,ARG1) /* disabled */ +#define WARNING_2(MESSAGE,ARG1,ARG2) /* disabled */ +#define WARNING_3(MESSAGE,ARG1,ARG2,ARG3) /* disabled */ +#define WARNING_4(MESSAGE,ARG1,ARG2,ARG3,ARG4) /* disabled */ +#define WARNING_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5) /* disabled */ +#define WARNING_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) /* disabled */ +#define WARNING_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) /* disabled */ +#define WARNING_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) /* disabled */ + +#endif + +/* ------------------------------------------- +Macro: ERROR() + Record a critical error in the RPT log. + +Parameters: + MESSAGE - Message to record + +Example: + (begin example) + ERROR("value of frog not found in config ...yada...yada..."); + (end) + +Author: + Spooner +------------------------------------------- */ +#define ERROR(MESSAGE) LOG_SYS('ERROR',MESSAGE) +#define ERROR_1(MESSAGE,ARG1) ERROR(FORMAT_1(MESSAGE,ARG1)) +#define ERROR_2(MESSAGE,ARG1,ARG2) ERROR(FORMAT_2(MESSAGE,ARG1,ARG2)) +#define ERROR_3(MESSAGE,ARG1,ARG2,ARG3) ERROR(FORMAT_3(MESSAGE,ARG1,ARG2,ARG3)) +#define ERROR_4(MESSAGE,ARG1,ARG2,ARG3,ARG4) ERROR(FORMAT_4(MESSAGE,ARG1,ARG2,ARG3,ARG4)) +#define ERROR_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5) ERROR(FORMAT_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5)) +#define ERROR_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) ERROR(FORMAT_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6)) +#define ERROR_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) ERROR(FORMAT_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7)) +#define ERROR_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) ERROR(FORMAT_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8)) + +/* ------------------------------------------- +Macro: ERROR_MSG() + Record a critical error in the RPT log and display on screen error message. + + Newlines (\n) in the MESSAGE will be put on separate lines. + +Parameters: + MESSAGE - Message to record + +Example: + (begin example) + ERROR_MSG("value of frog not found in config ...yada...yada..."); + (end) + +Author: + commy2 +------------------------------------------- */ +#define ERROR_MSG(MESSAGE) ['PREFIX', 'COMPONENT', nil, MESSAGE, __FILE__, __LINE__ + 1] call CBA_fnc_error +#define ERROR_MSG_1(MESSAGE,ARG1) ERROR_MSG(FORMAT_1(MESSAGE,ARG1)) +#define ERROR_MSG_2(MESSAGE,ARG1,ARG2) ERROR_MSG(FORMAT_2(MESSAGE,ARG1,ARG2)) +#define ERROR_MSG_3(MESSAGE,ARG1,ARG2,ARG3) ERROR_MSG(FORMAT_3(MESSAGE,ARG1,ARG2,ARG3)) +#define ERROR_MSG_4(MESSAGE,ARG1,ARG2,ARG3,ARG4) ERROR_MSG(FORMAT_4(MESSAGE,ARG1,ARG2,ARG3,ARG4)) +#define ERROR_MSG_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5) ERROR_MSG(FORMAT_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5)) +#define ERROR_MSG_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) ERROR_MSG(FORMAT_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6)) +#define ERROR_MSG_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) ERROR_MSG(FORMAT_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7)) +#define ERROR_MSG_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) ERROR_MSG(FORMAT_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8)) + +/* ------------------------------------------- +Macro: ERROR_WITH_TITLE() + Record a critical error in the RPT log. + + The title can be specified (in the heading is always just "ERROR") + Newlines (\n) in the MESSAGE will be put on separate lines. + +Parameters: + TITLE - Title of error message + MESSAGE - Body of error message + +Example: + (begin example) + ERROR_WITH_TITLE("Value not found","Value of frog not found in config ...yada...yada..."); + (end) + +Author: + Spooner +------------------------------------------- */ +#define ERROR_WITH_TITLE(TITLE,MESSAGE) ['PREFIX', 'COMPONENT', TITLE, MESSAGE, __FILE__, __LINE__ + 1] call CBA_fnc_error +#define ERROR_WITH_TITLE_1(TITLE,MESSAGE,ARG1) ERROR_WITH_TITLE(TITLE,FORMAT_1(MESSAGE,ARG1)) +#define ERROR_WITH_TITLE_2(TITLE,MESSAGE,ARG1,ARG2) ERROR_WITH_TITLE(TITLE,FORMAT_2(MESSAGE,ARG1,ARG2)) +#define ERROR_WITH_TITLE_3(TITLE,MESSAGE,ARG1,ARG2,ARG3) ERROR_WITH_TITLE(TITLE,FORMAT_3(MESSAGE,ARG1,ARG2,ARG3)) +#define ERROR_WITH_TITLE_4(TITLE,MESSAGE,ARG1,ARG2,ARG3,ARG4) ERROR_WITH_TITLE(TITLE,FORMAT_4(MESSAGE,ARG1,ARG2,ARG3,ARG4)) +#define ERROR_WITH_TITLE_5(TITLE,MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5) ERROR_WITH_TITLE(TITLE,FORMAT_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5)) +#define ERROR_WITH_TITLE_6(TITLE,MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) ERROR_WITH_TITLE(TITLE,FORMAT_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6)) +#define ERROR_WITH_TITLE_7(TITLE,MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) ERROR_WITH_TITLE(TITLE,FORMAT_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7)) +#define ERROR_WITH_TITLE_8(TITLE,MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) ERROR_WITH_TITLE(TITLE,FORMAT_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8)) + +/* ------------------------------------------- +Macro: MESSAGE_WITH_TITLE() + Record a single line in the RPT log. + +Parameters: + TITLE - Title of log message + MESSAGE - Body of message + +Example: + (begin example) + MESSAGE_WITH_TITLE("Value found","Value of frog found in config "); + (end) + +Author: + Killswitch +------------------------------------------- */ +#define MESSAGE_WITH_TITLE(TITLE,MESSAGE) LOG_SYS_FILELINENUMBERS(TITLE,MESSAGE) + +/* ------------------------------------------- +Macro: RETDEF() + If a variable is undefined, return the default value. Otherwise, return the + variable itself. + +Parameters: + VARIABLE - the variable to check + DEFAULT_VALUE - the default value to use if variable is undefined + +Example: + (begin example) + // _var is undefined + hintSilent format ["_var=%1", RETDEF(_var,5)]; // "_var=5" + _var = 7; + hintSilent format ["_var=%1", RETDEF(_var,5)]; // "_var=7" + (end example) +Author: + 654wak654 +------------------------------------------- */ +#define RETDEF(VARIABLE,DEFAULT_VALUE) (if (isNil {VARIABLE}) then [{DEFAULT_VALUE}, {VARIABLE}]) + +/* ------------------------------------------- +Macro: RETNIL() + If a variable is undefined, return the value nil. Otherwise, return the + variable itself. + +Parameters: + VARIABLE - the variable to check + +Example: + (begin example) + // _var is undefined + hintSilent format ["_var=%1", RETNIL(_var)]; // "_var=any" + (end example) + +Author: + Alef (see CBA issue #8514) +------------------------------------------- */ +#define RETNIL(VARIABLE) RETDEF(VARIABLE,nil) + +/* ------------------------------------------- +Macros: TRACE_n() + Log a message and 1-8 variables to the RPT log. + + Only run if is defined. + + TRACE_1(MESSAGE,A) - Log 1 variable. + TRACE_2(MESSAGE,A,B) - Log 2 variables. + TRACE_3(MESSAGE,A,B,C) - Log 3 variables. + TRACE_4(MESSAGE,A,B,C,D) - Log 4 variables. + TRACE_5(MESSAGE,A,B,C,D,E) - Log 5 variables. + TRACE_6(MESSAGE,A,B,C,D,E,F) - Log 6 variables. + TRACE_7(MESSAGE,A,B,C,D,E,F,G) - Log 7 variables. + TRACE_8(MESSAGE,A,B,C,D,E,F,G,H) - Log 8 variables. + TRACE_9(MESSAGE,A,B,C,D,E,F,G,H,I) - Log 9 variables. + +Parameters: + MESSAGE - Message to add to the trace [String] + A..H - Variable names to log values of [Any] + +Example: + (begin example) + TRACE_3("After takeoff",_vehicle player,getPos (_vehicle player), getPosASL (_vehicle player)); + (end) + +Author: + Spooner +------------------------------------------- */ +#define PFORMAT_1(MESSAGE,A) \ + format ['%1: A=%2', MESSAGE, RETNIL(A)] + +#define PFORMAT_2(MESSAGE,A,B) \ + format ['%1: A=%2, B=%3', MESSAGE, RETNIL(A), RETNIL(B)] + +#define PFORMAT_3(MESSAGE,A,B,C) \ + format ['%1: A=%2, B=%3, C=%4', MESSAGE, RETNIL(A), RETNIL(B), RETNIL(C)] + +#define PFORMAT_4(MESSAGE,A,B,C,D) \ + format ['%1: A=%2, B=%3, C=%4, D=%5', MESSAGE, RETNIL(A), RETNIL(B), RETNIL(C), RETNIL(D)] + +#define PFORMAT_5(MESSAGE,A,B,C,D,E) \ + format ['%1: A=%2, B=%3, C=%4, D=%5, E=%6', MESSAGE, RETNIL(A), RETNIL(B), RETNIL(C), RETNIL(D), RETNIL(E)] + +#define PFORMAT_6(MESSAGE,A,B,C,D,E,F) \ + format ['%1: A=%2, B=%3, C=%4, D=%5, E=%6, F=%7', MESSAGE, RETNIL(A), RETNIL(B), RETNIL(C), RETNIL(D), RETNIL(E), RETNIL(F)] + +#define PFORMAT_7(MESSAGE,A,B,C,D,E,F,G) \ + format ['%1: A=%2, B=%3, C=%4, D=%5, E=%6, F=%7, G=%8', MESSAGE, RETNIL(A), RETNIL(B), RETNIL(C), RETNIL(D), RETNIL(E), RETNIL(F), RETNIL(G)] + +#define PFORMAT_8(MESSAGE,A,B,C,D,E,F,G,H) \ + format ['%1: A=%2, B=%3, C=%4, D=%5, E=%6, F=%7, G=%8, H=%9', MESSAGE, RETNIL(A), RETNIL(B), RETNIL(C), RETNIL(D), RETNIL(E), RETNIL(F), RETNIL(G), RETNIL(H)] + +#define PFORMAT_9(MESSAGE,A,B,C,D,E,F,G,H,I) \ + format ['%1: A=%2, B=%3, C=%4, D=%5, E=%6, F=%7, G=%8, H=%9, I=%10', MESSAGE, RETNIL(A), RETNIL(B), RETNIL(C), RETNIL(D), RETNIL(E), RETNIL(F), RETNIL(G), RETNIL(H), RETNIL(I)] + + +#ifdef DEBUG_MODE_FULL +#define TRACE_1(MESSAGE,A) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_1(str diag_frameNo + ' ' + (MESSAGE),A)) +#define TRACE_2(MESSAGE,A,B) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_2(str diag_frameNo + ' ' + (MESSAGE),A,B)) +#define TRACE_3(MESSAGE,A,B,C) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_3(str diag_frameNo + ' ' + (MESSAGE),A,B,C)) +#define TRACE_4(MESSAGE,A,B,C,D) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_4(str diag_frameNo + ' ' + (MESSAGE),A,B,C,D)) +#define TRACE_5(MESSAGE,A,B,C,D,E) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_5(str diag_frameNo + ' ' + (MESSAGE),A,B,C,D,E)) +#define TRACE_6(MESSAGE,A,B,C,D,E,F) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_6(str diag_frameNo + ' ' + (MESSAGE),A,B,C,D,E,F)) +#define TRACE_7(MESSAGE,A,B,C,D,E,F,G) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_7(str diag_frameNo + ' ' + (MESSAGE),A,B,C,D,E,F,G)) +#define TRACE_8(MESSAGE,A,B,C,D,E,F,G,H) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_8(str diag_frameNo + ' ' + (MESSAGE),A,B,C,D,E,F,G,H)) +#define TRACE_9(MESSAGE,A,B,C,D,E,F,G,H,I) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_9(str diag_frameNo + ' ' + (MESSAGE),A,B,C,D,E,F,G,H,I)) +#else +#define TRACE_1(MESSAGE,A) /* disabled */ +#define TRACE_2(MESSAGE,A,B) /* disabled */ +#define TRACE_3(MESSAGE,A,B,C) /* disabled */ +#define TRACE_4(MESSAGE,A,B,C,D) /* disabled */ +#define TRACE_5(MESSAGE,A,B,C,D,E) /* disabled */ +#define TRACE_6(MESSAGE,A,B,C,D,E,F) /* disabled */ +#define TRACE_7(MESSAGE,A,B,C,D,E,F,G) /* disabled */ +#define TRACE_8(MESSAGE,A,B,C,D,E,F,G,H) /* disabled */ +#define TRACE_9(MESSAGE,A,B,C,D,E,F,G,H,I) /* disabled */ +#endif + +/* ------------------------------------------- +Group: General +------------------------------------------- */ + +// ************************************* +// Internal Functions +#define DOUBLES(var1,var2) var1##_##var2 +#define TRIPLES(var1,var2,var3) var1##_##var2##_##var3 +#define QUOTE(var1) #var1 + +#ifdef MODULAR + #define COMPONENT_T DOUBLES(t,COMPONENT) + #define COMPONENT_M DOUBLES(m,COMPONENT) + #define COMPONENT_S DOUBLES(s,COMPONENT) + #define COMPONENT_C DOUBLES(c,COMPONENT) + #define COMPONENT_F COMPONENT_C +#else + #define COMPONENT_T COMPONENT + #define COMPONENT_M COMPONENT + #define COMPONENT_S COMPONENT + #define COMPONENT_F COMPONENT + #define COMPONENT_C COMPONENT +#endif + +/* ------------------------------------------- +Macro: INC() + +Description: + Increase a number by one. + +Parameters: + VAR - Variable to increment [Number] + +Example: + (begin example) + _counter = 0; + INC(_counter); + // _counter => 1 + (end) + +Author: + Spooner +------------------------------------------- */ +#define INC(var) var = (var) + 1 + +/* ------------------------------------------- +Macro: DEC() + +Description: + Decrease a number by one. + +Parameters: + VAR - Variable to decrement [Number] + +Example: + (begin example) + _counter = 99; + DEC(_counter); + // _counter => 98 + (end) + +Author: + Spooner +------------------------------------------- */ +#define DEC(var) var = (var) - 1 + +/* ------------------------------------------- +Macro: ADD() + +Description: + Add a value to a variable. Variable and value should be both Numbers or both Strings. + +Parameters: + VAR - Variable to add to [Number or String] + VALUE - Value to add [Number or String] + +Examples: + (begin example) + _counter = 2; + ADD(_counter,3); + // _counter => 5 + (end) + (begin example) + _str = "hello"; + ADD(_str," "); + ADD(_str,"Fred"); + // _str => "hello Fred" + (end) + +Author: + Sickboy +------------------------------------------- */ +#define ADD(var1,var2) var1 = (var1) + (var2) + +/* ------------------------------------------- +Macro: SUB() + +Description: + Subtract a value from a number variable. VAR and VALUE should both be Numbers. + +Parameters: + VAR - Variable to subtract from [Number] + VALUE - Value to subtract [Number] + +Examples: + (begin example) + _numChickens = 2; + SUB(_numChickens,3); + // _numChickens => -1 + (end) +------------------------------------------- */ +#define SUB(var1,var2) var1 = (var1) - (var2) + +/* ------------------------------------------- +Macro: REM() + +Description: + Remove an element from an array each time it occurs. + + This recreates the entire array, so use BIS_fnc_removeIndex if modification of the original array is required + or if only one of the elements that matches ELEMENT needs to be removed. + +Parameters: + ARRAY - Array to modify [Array] + ELEMENT - Element to remove [Any] + +Examples: + (begin example) + _array = [1, 2, 3, 4, 3, 8]; + REM(_array,3); + // _array = [1, 2, 4, 8]; + (end) + +Author: + Spooner +------------------------------------------- */ +#define REM(var1,var2) SUB(var1,[var2]) + +/* ------------------------------------------- +Macro: PUSH() + +Description: + Appends a single value onto the end of an ARRAY. Change is made to the ARRAY itself, not creating a new array. + +Parameters: + ARRAY - Array to push element onto [Array] + ELEMENT - Element to push [Any] + +Examples: + (begin example) + _fish = ["blue", "green", "smelly"]; + PUSH(_fish,"monkey-flavoured"); + // _fish => ["blue", "green", "smelly", "monkey-flavoured"] + (end) + +Author: + Spooner +------------------------------------------- */ +#define PUSH(var1,var2) (var1) pushBack (var2) + +/* ------------------------------------------- +Macro: MAP() +Description: + Applies given code to each element of the array, then assigns the + resulting array to the original +Parameters: + ARRAY - Array to be modified + CODE - Code that'll be applied to each element of the array. +Example: + (begin example) + _array = [1, 2, 3, 4, 3, 8]; + MAP(_array,_x + 1); + // _array is now [2, 3, 4, 5, 4, 9]; + (end) +Author: + 654wak654 +------------------------------------------- */ +#define MAP(ARR,CODE) ARR = ARR apply {CODE} + +/* ------------------------------------------- +Macro: FILTER() +Description: + Filters an array based on given code, then assigns the resulting array + to the original +Parameters: + ARRAY - Array to be filtered + CODE - Condition to pick elements +Example: + (begin example) + _array = [1, 2, 3, 4, 3, 8]; + FILTER(_array,_x % 2 == 0) + // _array is now [2, 4, 8]; + (end) +Author: + Commy2 +------------------------------------------- */ +#define FILTER(ARR,CODE) ARR = ARR select {CODE} + +/* ------------------------------------------- +Macro: UNIQUE() +Description: + Removes duplicate values in given array +Parameters: + ARRAY - The array to be modified +Example: + (begin example) + _someArray = [4, 4, 5, 5, 5, 2]; + UNIQUE(_someArray); + // _someArray is now [4, 5, 2] + (end) +Author: + Commy2 +------------------------------------------- */ +#define UNIQUE(ARR) ARR = ARR arrayIntersect ARR + +/* ------------------------------------------- +Macro: INTERSECTION() +Description: + Finds unique common elements between two arrays and assigns them + to the first array +Parameters: + ARRAY0 - The array to be modified + ARRAY1 - The array to find intersections with +Example: + (begin example) + _someArray = [1, 2, 3, 4, 5, 5]; + _anotherArray = [4, 5, 6, 7]; + INTERSECTION(_someArray,_anotherArray); + // _someArray is now [4, 5] + (end) +Author: + 654wak654 +------------------------------------------- */ +#define INTERSECTION(ARG0,ARG1) ARG0 = ARG0 arrayIntersect (ARG1) + +/* ------------------------------------------- +Macro: ISNILS() + +Description: + Sets a variable with a value, but only if it is undefined. + +Parameters: + VARIABLE - Variable to set [Any, not nil] + DEFAULT_VALUE - Value to set VARIABLE to if it is undefined [Any, not nil] + +Examples: + (begin example) + // _fish is undefined + ISNILS(_fish,0); + // _fish => 0 + (end) + (begin example) + _fish = 12; + // ...later... + ISNILS(_fish,0); + // _fish => 12 + (end) + +Author: + Sickboy +------------------------------------------- */ +#define ISNILS(VARIABLE,DEFAULT_VALUE) if (isNil #VARIABLE) then { VARIABLE = DEFAULT_VALUE } +#define ISNILS2(var1,var2,var3,var4) ISNILS(TRIPLES(var1,var2,var3),var4) +#define ISNILS3(var1,var2,var3) ISNILS(DOUBLES(var1,var2),var3) +#define ISNIL(var1,var2) ISNILS2(PREFIX,COMPONENT,var1,var2) +#define ISNILMAIN(var1,var2) ISNILS3(PREFIX,var1,var2) + +#define CREATELOGICS(var1,var2) var1##_##var2 = ([sideLogic] call CBA_fnc_getSharedGroup) createUnit ["LOGIC", [0, 0, 0], [], 0, "NONE"] +#define CREATELOGICLOCALS(var1,var2) var1##_##var2 = "LOGIC" createVehicleLocal [0, 0, 0] +#define CREATELOGICGLOBALS(var1,var2) var1##_##var2 = ([sideLogic] call CBA_fnc_getSharedGroup) createUnit ["LOGIC", [0, 0, 0], [], 0, "NONE"]; publicVariable QUOTE(DOUBLES(var1,var2)) +#define CREATELOGICGLOBALTESTS(var1,var2) var1##_##var2 = ([sideLogic] call CBA_fnc_getSharedGroup) createUnit [QUOTE(DOUBLES(ADDON,logic)), [0, 0, 0], [], 0, "NONE"] + +#define GETVARS(var1,var2,var3) (var1##_##var2 getVariable #var3) +#define GETVARMAINS(var1,var2) GETVARS(var1,MAINLOGIC,var2) + +#ifndef PATHTO_SYS + #define PATHTO_SYS(var1,var2,var3) \MAINPREFIX\var1\SUBPREFIX\var2\var3.sqf +#endif +#ifndef PATHTOF_SYS + #define PATHTOF_SYS(var1,var2,var3) \MAINPREFIX\var1\SUBPREFIX\var2\var3 +#endif + +#ifndef PATHTOF2_SYS + #define PATHTOF2_SYS(var1,var2,var3) MAINPREFIX\var1\SUBPREFIX\var2\var3 +#endif + +#define PATHTO_R(var1) PATHTOF2_SYS(PREFIX,COMPONENT_C,var1) +#define PATHTO_T(var1) PATHTOF_SYS(PREFIX,COMPONENT_T,var1) +#define PATHTO_M(var1) PATHTOF_SYS(PREFIX,COMPONENT_M,var1) +#define PATHTO_S(var1) PATHTOF_SYS(PREFIX,COMPONENT_S,var1) +#define PATHTO_C(var1) PATHTOF_SYS(PREFIX,COMPONENT_C,var1) +#define PATHTO_F(var1) PATHTO_SYS(PREFIX,COMPONENT_F,var1) + +// Already quoted "" +#define QPATHTO_R(var1) QUOTE(PATHTO_R(var1)) +#define QPATHTO_T(var1) QUOTE(PATHTO_T(var1)) +#define QPATHTO_M(var1) QUOTE(PATHTO_M(var1)) +#define QPATHTO_S(var1) QUOTE(PATHTO_S(var1)) +#define QPATHTO_C(var1) QUOTE(PATHTO_C(var1)) +#define QPATHTO_F(var1) QUOTE(PATHTO_F(var1)) + +// This only works for binarized configs after recompiling the pbos +// TODO: Reduce amount of calls / code.. +#define COMPILE_FILE2_CFG_SYS(var1) compile preprocessFileLineNumbers var1 +#define COMPILE_FILE2_SYS(var1) COMPILE_FILE2_CFG_SYS(var1) + +#define COMPILE_FILE_SYS(var1,var2,var3) COMPILE_FILE2_SYS('PATHTO_SYS(var1,var2,var3)') +#define COMPILE_FILE_CFG_SYS(var1,var2,var3) COMPILE_FILE2_CFG_SYS('PATHTO_SYS(var1,var2,var3)') + +#define SETVARS(var1,var2) var1##_##var2 setVariable +#define SETVARMAINS(var1) SETVARS(var1,MAINLOGIC) +#define GVARMAINS(var1,var2) var1##_##var2 +#define CFGSETTINGSS(var1,var2) configFile >> "CfgSettings" >> #var1 >> #var2 +//#define SETGVARS(var1,var2,var3) var1##_##var2##_##var3 = +//#define SETGVARMAINS(var1,var2) var1##_##var2 = + +// Compile-Once, JIT: On first use. +// #define PREPMAIN_SYS(var1,var2,var3) var1##_fnc_##var3 = { var1##_fnc_##var3 = COMPILE_FILE_SYS(var1,var2,DOUBLES(fnc,var3)); if (isNil "_this") then { call var1##_fnc_##var3 } else { _this call var1##_fnc_##var3 } } +// #define PREP_SYS(var1,var2,var3) var1##_##var2##_fnc_##var3 = { var1##_##var2##_fnc_##var3 = COMPILE_FILE_SYS(var1,var2,DOUBLES(fnc,var3)); if (isNil "_this") then { call var1##_##var2##_fnc_##var3 } else { _this call var1##_##var2##_fnc_##var3 } } +// #define PREP_SYS2(var1,var2,var3,var4) var1##_##var2##_fnc_##var4 = { var1##_##var2##_fnc_##var4 = COMPILE_FILE_SYS(var1,var3,DOUBLES(fnc,var4)); if (isNil "_this") then { call var1##_##var2##_fnc_##var4 } else { _this call var1##_##var2##_fnc_##var4 } } + +// Compile-Once, at Macro. As opposed to Compile-Once, on first use. +#define PREPMAIN_SYS(var1,var2,var3) var1##_fnc_##var3 = COMPILE_FILE_SYS(var1,var2,DOUBLES(fnc,var3)) +#define PREP_SYS(var1,var2,var3) var1##_##var2##_fnc_##var3 = COMPILE_FILE_SYS(var1,var2,DOUBLES(fnc,var3)) +#define PREP_SYS2(var1,var2,var3,var4) var1##_##var2##_fnc_##var4 = COMPILE_FILE_SYS(var1,var3,DOUBLES(fnc,var4)) + +#define LSTR(var1) TRIPLES(ADDON,STR,var1) + +#ifndef DEBUG_SETTINGS + #define DEBUG_SETTINGS [false, true, false] +#endif + +#define MSG_INIT QUOTE(Initializing: ADDON version: VERSION) + +// ************************************* +// User Functions +#define CFGSETTINGS CFGSETTINGSS(PREFIX,COMPONENT) +#define PATHTO(var1) PATHTO_SYS(PREFIX,COMPONENT_F,var1) +#define PATHTOF(var1) PATHTOF_SYS(PREFIX,COMPONENT,var1) +#define PATHTOEF(var1,var2) PATHTOF_SYS(PREFIX,var1,var2) +#define QPATHTOF(var1) QUOTE(PATHTOF(var1)) +#define QPATHTOEF(var1,var2) QUOTE(PATHTOEF(var1,var2)) + +#define COMPILE_FILE(var1) COMPILE_FILE_SYS(PREFIX,COMPONENT_F,var1) +#define COMPILE_FILE_CFG(var1) COMPILE_FILE_CFG_SYS(PREFIX,COMPONENT_F,var1) +#define COMPILE_FILE2(var1) COMPILE_FILE2_SYS('var1') +#define COMPILE_FILE2_CFG(var1) COMPILE_FILE2_CFG_SYS('var1') + +#define COMPILE_SCRIPT(var1) compileScript ['PATHTO_SYS(PREFIX,COMPONENT_F,var1)'] + + +#define VERSIONING_SYS(var1) class CfgSettings \ +{ \ + class CBA \ + { \ + class Versioning \ + { \ + class var1 \ + { \ + }; \ + }; \ + }; \ +}; + +#define VERSIONING VERSIONING_SYS(PREFIX) + +/* ------------------------------------------- +Macro: GVAR() + Get full variable identifier for a global variable owned by this component. + +Parameters: + VARIABLE - Partial name of global variable owned by this component [Any]. + +Example: + (begin example) + GVAR(frog) = 12; + // In SPON_FrogDancing component, equivalent to SPON_FrogDancing_frog = 12 + (end) + +Author: + Sickboy +------------------------------------------- */ +#define GVAR(var1) DOUBLES(ADDON,var1) +#define EGVAR(var1,var2) TRIPLES(PREFIX,var1,var2) +#define QGVAR(var1) QUOTE(GVAR(var1)) +#define QEGVAR(var1,var2) QUOTE(EGVAR(var1,var2)) +#define QQGVAR(var1) QUOTE(QGVAR(var1)) +#define QQEGVAR(var1,var2) QUOTE(QEGVAR(var1,var2)) + +/* ------------------------------------------- +Macro: GVARMAIN() + Get full variable identifier for a global variable owned by this addon. + +Parameters: + VARIABLE - Partial name of global variable owned by this addon [Any]. + +Example: + (begin example) + GVARMAIN(frog) = 12; + // In SPON_FrogDancing component, equivalent to SPON_frog = 12 + (end) + +Author: + Sickboy +------------------------------------------- */ +#define GVARMAIN(var1) GVARMAINS(PREFIX,var1) +#define QGVARMAIN(var1) QUOTE(GVARMAIN(var1)) +#define QQGVARMAIN(var1) QUOTE(QGVARMAIN(var1)) +// TODO: What's this? +#define SETTINGS DOUBLES(PREFIX,settings) +#define CREATELOGIC CREATELOGICS(PREFIX,COMPONENT) +#define CREATELOGICGLOBAL CREATELOGICGLOBALS(PREFIX,COMPONENT) +#define CREATELOGICGLOBALTEST CREATELOGICGLOBALTESTS(PREFIX,COMPONENT) +#define CREATELOGICLOCAL CREATELOGICLOCALS(PREFIX,COMPONENT) +#define CREATELOGICMAIN CREATELOGICS(PREFIX,MAINLOGIC) +#define GETVAR(var1) GETVARS(PREFIX,COMPONENT,var1) +#define SETVAR SETVARS(PREFIX,COMPONENT) +#define SETVARMAIN SETVARMAINS(PREFIX) +#define IFCOUNT(var1,var2,var3) if (count var1 > var2) then { var3 = var1 select var2 }; + +/* ------------------------------------------- +Macro: PREP() + +Description: + Defines a function. + + Full file path: + '\MAINPREFIX\PREFIX\SUBPREFIX\COMPONENT\fnc_.sqf' + + Resulting function name: + 'PREFIX_COMPONENT_' + + The PREP macro should be placed in a script run by a XEH preStart and XEH preInit event. + + The PREP macro allows for CBA function caching, which drastically speeds up load times. + Beware though that function caching is enabled by default and as such to disable it, you need to + #define DISABLE_COMPILE_CACHE above your #include "script_components.hpp" include! + + The function will be defined in ui and mission namespace. It can not be overwritten without + a mission restart. + +Parameters: + FUNCTION NAME - Name of the function, unquoted + +Examples: + (begin example) + PREP(banana); + call FUNC(banana); + (end) + +Author: + dixon13 + ------------------------------------------- */ +//#define PREP(var1) PREP_SYS(PREFIX,COMPONENT_F,var1) + +#ifdef DISABLE_COMPILE_CACHE + #define PREP(var1) TRIPLES(ADDON,fnc,var1) = compile preProcessFileLineNumbers 'PATHTO_SYS(PREFIX,COMPONENT_F,DOUBLES(fnc,var1))' + #define PREPMAIN(var1) TRIPLES(PREFIX,fnc,var1) = compile preProcessFileLineNumbers 'PATHTO_SYS(PREFIX,COMPONENT_F,DOUBLES(fnc,var1))' +#else + #define PREP(var1) ['PATHTO_SYS(PREFIX,COMPONENT_F,DOUBLES(fnc,var1))', 'TRIPLES(ADDON,fnc,var1)'] call SLX_XEH_COMPILE_NEW + #define PREPMAIN(var1) ['PATHTO_SYS(PREFIX,COMPONENT_F,DOUBLES(fnc,var1))', 'TRIPLES(PREFIX,fnc,var1)'] call SLX_XEH_COMPILE_NEW +#endif + +/* ------------------------------------------- +Macro: PATHTO_FNC() + +Description: + Defines a function inside CfgFunctions. + + Full file path in addons: + '\MAINPREFIX\PREFIX\SUBPREFIX\COMPONENT\fnc_.sqf' + Define 'RECOMPILE' to enable recompiling. + Define 'SKIP_FUNCTION_HEADER' to skip adding function header. + +Parameters: + FUNCTION NAME - Name of the function, unquoted + +Examples: + (begin example) + // file name: fnc_addPerFrameHandler.sqf + class CfgFunctions { + class CBA { + class Misc { + PATHTO_FNC(addPerFrameHandler); + }; + }; + }; + // -> CBA_fnc_addPerFrameHandler + (end) + +Author: + dixon13, commy2 + ------------------------------------------- */ +#ifdef RECOMPILE + #undef RECOMPILE + #define RECOMPILE recompile = 1 +#else + #define RECOMPILE recompile = 0 +#endif +// Set function header type: -1 - no header; 0 - default header; 1 - system header. +#ifdef SKIP_FUNCTION_HEADER + #define CFGFUNCTION_HEADER headerType = -1 +#else + #define CFGFUNCTION_HEADER headerType = 0 +#endif + +#define PATHTO_FNC(func) class func {\ + file = QPATHTOF(DOUBLES(fnc,func).sqf);\ + CFGFUNCTION_HEADER;\ + RECOMPILE;\ +} + +#define FUNC(var1) TRIPLES(ADDON,fnc,var1) +#define FUNCMAIN(var1) TRIPLES(PREFIX,fnc,var1) +#define FUNC_INNER(var1,var2) TRIPLES(DOUBLES(PREFIX,var1),fnc,var2) +#define EFUNC(var1,var2) FUNC_INNER(var1,var2) +#define QFUNC(var1) QUOTE(FUNC(var1)) +#define QFUNCMAIN(var1) QUOTE(FUNCMAIN(var1)) +#define QFUNC_INNER(var1,var2) QUOTE(FUNC_INNER(var1,var2)) +#define QEFUNC(var1,var2) QUOTE(EFUNC(var1,var2)) +#define QQFUNC(var1) QUOTE(QFUNC(var1)) +#define QQFUNCMAIN(var1) QUOTE(QFUNCMAIN(var1)) +#define QQFUNC_INNER(var1,var2) QUOTE(QFUNC_INNER(var1,var2)) +#define QQEFUNC(var1,var2) QUOTE(QEFUNC(var1,var2)) + +#ifndef PRELOAD_ADDONS + #define PRELOAD_ADDONS class CfgAddons \ +{ \ + class PreloadAddons \ + { \ + class ADDON \ + { \ + list[]={ QUOTE(ADDON) }; \ + }; \ + }; \ +} +#endif + +/* ------------------------------------------- +Macros: ARG_#() + Select from list of array arguments + +Parameters: + VARIABLE(1-8) - elements for the list + +Author: + Rommel +------------------------------------------- */ +#define ARG_1(A,B) ((A) select (B)) +#define ARG_2(A,B,C) (ARG_1(ARG_1(A,B),C)) +#define ARG_3(A,B,C,D) (ARG_1(ARG_2(A,B,C),D)) +#define ARG_4(A,B,C,D,E) (ARG_1(ARG_3(A,B,C,D),E)) +#define ARG_5(A,B,C,D,E,F) (ARG_1(ARG_4(A,B,C,D,E),F)) +#define ARG_6(A,B,C,D,E,F,G) (ARG_1(ARG_5(A,B,C,D,E,F),G)) +#define ARG_7(A,B,C,D,E,F,G,H) (ARG_1(ARG_6(A,B,C,D,E,E,F,G),H)) +#define ARG_8(A,B,C,D,E,F,G,H,I) (ARG_1(ARG_7(A,B,C,D,E,E,F,G,H),I)) + +/* ------------------------------------------- +Macros: ARR_#() + Create list from arguments. Useful for working around , in macro parameters. + 1-8 arguments possible. + +Parameters: + VARIABLE(1-8) - elements for the list + +Author: + Nou +------------------------------------------- */ +#define ARR_1(ARG1) ARG1 +#define ARR_2(ARG1,ARG2) ARG1, ARG2 +#define ARR_3(ARG1,ARG2,ARG3) ARG1, ARG2, ARG3 +#define ARR_4(ARG1,ARG2,ARG3,ARG4) ARG1, ARG2, ARG3, ARG4 +#define ARR_5(ARG1,ARG2,ARG3,ARG4,ARG5) ARG1, ARG2, ARG3, ARG4, ARG5 +#define ARR_6(ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) ARG1, ARG2, ARG3, ARG4, ARG5, ARG6 +#define ARR_7(ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) ARG1, ARG2, ARG3, ARG4, ARG5, ARG6, ARG7 +#define ARR_8(ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) ARG1, ARG2, ARG3, ARG4, ARG5, ARG6, ARG7, ARG8 + +/* ------------------------------------------- +Macros: FORMAT_#(STR, ARG1) + Format - Useful for working around , in macro parameters. + 1-8 arguments possible. + +Parameters: + STRING - string used by format + VARIABLE(1-8) - elements for usage in format + +Author: + Nou & Sickboy +------------------------------------------- */ +#define FORMAT_1(STR,ARG1) format[STR, ARG1] +#define FORMAT_2(STR,ARG1,ARG2) format[STR, ARG1, ARG2] +#define FORMAT_3(STR,ARG1,ARG2,ARG3) format[STR, ARG1, ARG2, ARG3] +#define FORMAT_4(STR,ARG1,ARG2,ARG3,ARG4) format[STR, ARG1, ARG2, ARG3, ARG4] +#define FORMAT_5(STR,ARG1,ARG2,ARG3,ARG4,ARG5) format[STR, ARG1, ARG2, ARG3, ARG4, ARG5] +#define FORMAT_6(STR,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) format[STR, ARG1, ARG2, ARG3, ARG4, ARG5, ARG6] +#define FORMAT_7(STR,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) format[STR, ARG1, ARG2, ARG3, ARG4, ARG5, ARG6, ARG7] +#define FORMAT_8(STR,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) format[STR, ARG1, ARG2, ARG3, ARG4, ARG5, ARG6, ARG7, ARG8] + +// CONTROL(46) 12 +#define DISPLAY(A) (findDisplay A) +#define CONTROL(A) DISPLAY(A) displayCtrl + +/* ------------------------------------------- +Macros: IS_x() + Checking the data types of variables. + + IS_ARRAY() - Array + IS_BOOL() - Boolean + IS_BOOLEAN() - UI display handle(synonym for ) + IS_CODE() - Code block (i.e a compiled function) + IS_CONFIG() - Configuration + IS_CONTROL() - UI control handle. + IS_DISPLAY() - UI display handle. + IS_FUNCTION() - A compiled function (synonym for ) + IS_GROUP() - Group. + IS_INTEGER() - Is a number a whole number? + IS_LOCATION() - World location. + IS_NUMBER() - A floating point number (synonym for ) + IS_OBJECT() - World object. + IS_SCALAR() - Floating point number. + IS_SCRIPT() - A script handle (as returned by execVM and spawn commands). + IS_SIDE() - Game side. + IS_STRING() - World object. + IS_TEXT() - Structured text. + +Parameters: + VARIABLE - Variable to check if it is of a particular type [Any, not nil] + +Author: + Spooner +------------------------------------------- */ +#define IS_META_SYS(VAR,TYPE) (if (isNil {VAR}) then {false} else {(VAR) isEqualType TYPE}) +#define IS_ARRAY(VAR) IS_META_SYS(VAR,[]) +#define IS_BOOL(VAR) IS_META_SYS(VAR,false) +#define IS_CODE(VAR) IS_META_SYS(VAR,{}) +#define IS_CONFIG(VAR) IS_META_SYS(VAR,configNull) +#define IS_CONTROL(VAR) IS_META_SYS(VAR,controlNull) +#define IS_DISPLAY(VAR) IS_META_SYS(VAR,displayNull) +#define IS_GROUP(VAR) IS_META_SYS(VAR,grpNull) +#define IS_OBJECT(VAR) IS_META_SYS(VAR,objNull) +#define IS_SCALAR(VAR) IS_META_SYS(VAR,0) +#define IS_SCRIPT(VAR) IS_META_SYS(VAR,scriptNull) +#define IS_SIDE(VAR) IS_META_SYS(VAR,west) +#define IS_STRING(VAR) IS_META_SYS(VAR,"STRING") +#define IS_TEXT(VAR) IS_META_SYS(VAR,text "") +#define IS_LOCATION(VAR) IS_META_SYS(VAR,locationNull) + +#define IS_BOOLEAN(VAR) IS_BOOL(VAR) +#define IS_FUNCTION(VAR) IS_CODE(VAR) +#define IS_INTEGER(VAR) (if (IS_SCALAR(VAR)) then {floor (VAR) == (VAR)} else {false}) +#define IS_NUMBER(VAR) IS_SCALAR(VAR) + +#define FLOAT_TO_STRING(num) (if (_this == 0) then {"0"} else {str parseNumber (str (_this % _this) + str floor abs _this) + "." + (str (abs _this - floor abs _this) select [2]) + "0"}) + +/* ------------------------------------------- +Macro: SCRIPT() + Sets name of script (relies on PREFIX and COMPONENT values being #defined). + Define 'SKIP_SCRIPT_NAME' to skip adding scriptName. + +Parameters: + NAME - Name of script [Indentifier] + +Example: + (begin example) + SCRIPT(eradicateMuppets); + (end) + +Author: + Spooner +------------------------------------------- */ +#ifndef SKIP_SCRIPT_NAME + #define SCRIPT(NAME) scriptName 'PREFIX\COMPONENT\NAME' +#else + #define SCRIPT(NAME) /* nope */ +#endif + +/* ------------------------------------------- +Macros: EXPLODE_n() + DEPRECATED - Use param/params commands added in Arma 3 1.48 + + Splitting an ARRAY into a number of variables (A, B, C, etc). + + Note that this NOT does make the created variables private. + _PVT variants do. + + EXPLODE_1(ARRAY,A,B) - Split a 1-element array into separate variable. + EXPLODE_2(ARRAY,A,B) - Split a 2-element array into separate variables. + EXPLODE_3(ARRAY,A,B,C) - Split a 3-element array into separate variables. + EXPLODE_4(ARRAY,A,B,C,D) - Split a 4-element array into separate variables. + EXPLODE_5(ARRAY,A,B,C,D,E) - Split a 5-element array into separate variables. + EXPLODE_6(ARRAY,A,B,C,D,E,F) - Split a 6-element array into separate variables. + EXPLODE_7(ARRAY,A,B,C,D,E,F,G) - Split a 7-element array into separate variables. + EXPLODE_8(ARRAY,A,B,C,D,E,F,G,H) - Split a 8-element array into separate variables. + EXPLODE_9(ARRAY,A,B,C,D,E,F,G,H,I) - Split a 9-element array into separate variables. + +Parameters: + ARRAY - Array to read from [Array] + A..H - Names of variables to set from array [Identifier] + +Example: + (begin example) + _array = ["fred", 156.8, 120.9]; + EXPLODE_3(_array,_name,_height,_weight); + (end) + +Author: + Spooner +------------------------------------------- */ +#define EXPLODE_1_SYS(ARRAY,A) A = ARRAY param [0] +#define EXPLODE_1(ARRAY,A) EXPLODE_1_SYS(ARRAY,A); TRACE_1("EXPLODE_1, " + QUOTE(ARRAY),A) +#define EXPLODE_1_PVT(ARRAY,A) ARRAY params [#A]; TRACE_1("EXPLODE_1, " + QUOTE(ARRAY),A) + +#define EXPLODE_2_SYS(ARRAY,A,B) EXPLODE_1_SYS(ARRAY,A); B = ARRAY param [1] +#define EXPLODE_2(ARRAY,A,B) EXPLODE_2_SYS(ARRAY,A,B); TRACE_2("EXPLODE_2, " + QUOTE(ARRAY),A,B) +#define EXPLODE_2_PVT(ARRAY,A,B) ARRAY params [#A,#B]; TRACE_2("EXPLODE_2, " + QUOTE(ARRAY),A,B) + +#define EXPLODE_3_SYS(ARRAY,A,B,C) EXPLODE_2_SYS(ARRAY,A,B); C = ARRAY param [2] +#define EXPLODE_3(ARRAY,A,B,C) EXPLODE_3_SYS(ARRAY,A,B,C); TRACE_3("EXPLODE_3, " + QUOTE(ARRAY),A,B,C) +#define EXPLODE_3_PVT(ARRAY,A,B,C) ARRAY params [#A,#B,#C]; TRACE_3("EXPLODE_3, " + QUOTE(ARRAY),A,B,C) + +#define EXPLODE_4_SYS(ARRAY,A,B,C,D) EXPLODE_3_SYS(ARRAY,A,B,C); D = ARRAY param [3] +#define EXPLODE_4(ARRAY,A,B,C,D) EXPLODE_4_SYS(ARRAY,A,B,C,D); TRACE_4("EXPLODE_4, " + QUOTE(ARRAY),A,B,C,D) +#define EXPLODE_4_PVT(ARRAY,A,B,C,D) ARRAY params [#A,#B,#C,#D]; TRACE_4("EXPLODE_4, " + QUOTE(ARRAY),A,B,C,D) + +#define EXPLODE_5_SYS(ARRAY,A,B,C,D,E) EXPLODE_4_SYS(ARRAY,A,B,C,D); E = ARRAY param [4] +#define EXPLODE_5(ARRAY,A,B,C,D,E) EXPLODE_5_SYS(ARRAY,A,B,C,D,E); TRACE_5("EXPLODE_5, " + QUOTE(ARRAY),A,B,C,D,E) +#define EXPLODE_5_PVT(ARRAY,A,B,C,D,E) ARRAY params [#A,#B,#C,#D,#E]; TRACE_5("EXPLODE_5, " + QUOTE(ARRAY),A,B,C,D,E) + +#define EXPLODE_6_SYS(ARRAY,A,B,C,D,E,F) EXPLODE_5_SYS(ARRAY,A,B,C,D,E); F = ARRAY param [5] +#define EXPLODE_6(ARRAY,A,B,C,D,E,F) EXPLODE_6_SYS(ARRAY,A,B,C,D,E,F); TRACE_6("EXPLODE_6, " + QUOTE(ARRAY),A,B,C,D,E,F) +#define EXPLODE_6_PVT(ARRAY,A,B,C,D,E,F) ARRAY params [#A,#B,#C,#D,#E,#F]; TRACE_6("EXPLODE_6, " + QUOTE(ARRAY),A,B,C,D,E,F) + +#define EXPLODE_7_SYS(ARRAY,A,B,C,D,E,F,G) EXPLODE_6_SYS(ARRAY,A,B,C,D,E,F); G = ARRAY param [6] +#define EXPLODE_7(ARRAY,A,B,C,D,E,F,G) EXPLODE_7_SYS(ARRAY,A,B,C,D,E,F,G); TRACE_7("EXPLODE_7, " + QUOTE(ARRAY),A,B,C,D,E,F,G) +#define EXPLODE_7_PVT(ARRAY,A,B,C,D,E,F,G) ARRAY params [#A,#B,#C,#D,#E,#F,#G]; TRACE_7("EXPLODE_7, " + QUOTE(ARRAY),A,B,C,D,E,F,G) + +#define EXPLODE_8_SYS(ARRAY,A,B,C,D,E,F,G,H) EXPLODE_7_SYS(ARRAY,A,B,C,D,E,F,G); H = ARRAY param [7] +#define EXPLODE_8(ARRAY,A,B,C,D,E,F,G,H) EXPLODE_8_SYS(ARRAY,A,B,C,D,E,F,G,H); TRACE_8("EXPLODE_8, " + QUOTE(ARRAY),A,B,C,D,E,F,G,H) +#define EXPLODE_8_PVT(ARRAY,A,B,C,D,E,F,G,H) ARRAY params [#A,#B,#C,#D,#E,#F,#G,#H]; TRACE_8("EXPLODE_8, " + QUOTE(ARRAY),A,B,C,D,E,F,G,H) + +#define EXPLODE_9_SYS(ARRAY,A,B,C,D,E,F,G,H,I) EXPLODE_8_SYS(ARRAY,A,B,C,D,E,F,G,H); I = ARRAY param [8] +#define EXPLODE_9(ARRAY,A,B,C,D,E,F,G,H,I) EXPLODE_9_SYS(ARRAY,A,B,C,D,E,F,G,H,I); TRACE_9("EXPLODE_9, " + QUOTE(ARRAY),A,B,C,D,E,F,G,H,I) +#define EXPLODE_9_PVT(ARRAY,A,B,C,D,E,F,G,H,I) ARRAY params [#A,#B,#C,#D,#E,#F,#G,#H,#I]; TRACE_9("EXPLODE_9, " + QUOTE(ARRAY),A,B,C,D,E,F,G,H,I) + +/* ------------------------------------------- +Macro: xSTRING() + Get full string identifier from a stringtable owned by this component. + +Parameters: + VARIABLE - Partial name of global variable owned by this component [Any]. + +Example: + ADDON is CBA_Balls. + (begin example) + // Localized String (localize command must still be used with it) + LSTRING(Example); // STR_CBA_Balls_Example; + // Config String (note the $) + CSTRING(Example); // $STR_CBA_Balls_Example; + (end) + +Author: + Jonpas +------------------------------------------- */ +#ifndef STRING_MACROS_GUARD +#define STRING_MACROS_GUARD + #define LSTRING(var1) QUOTE(TRIPLES(STR,ADDON,var1)) + #define ELSTRING(var1,var2) QUOTE(TRIPLES(STR,DOUBLES(PREFIX,var1),var2)) + #define CSTRING(var1) QUOTE(TRIPLES($STR,ADDON,var1)) + #define ECSTRING(var1,var2) QUOTE(TRIPLES($STR,DOUBLES(PREFIX,var1),var2)) + + #define LLSTRING(var1) localize QUOTE(TRIPLES(STR,ADDON,var1)) + #define LELSTRING(var1,var2) localize QUOTE(TRIPLES(STR,DOUBLES(PREFIX,var1),var2)) +#endif + + +/* ------------------------------------------- +Group: Managing Function Parameters +------------------------------------------- */ + +/* ------------------------------------------- +Macros: PARAMS_n() + DEPRECATED - Use param/params commands added in Arma 3 1.48 + + Setting variables based on parameters passed to a function. + + Each parameter is defines as private and set to the appropriate value from _this. + + PARAMS_1(A) - Get 1 parameter from the _this array (or _this if it's not an array). + PARAMS_2(A,B) - Get 2 parameters from the _this array. + PARAMS_3(A,B,C) - Get 3 parameters from the _this array. + PARAMS_4(A,B,C,D) - Get 4 parameters from the _this array. + PARAMS_5(A,B,C,D,E) - Get 5 parameters from the _this array. + PARAMS_6(A,B,C,D,E,F) - Get 6 parameters from the _this array. + PARAMS_7(A,B,C,D,E,F,G) - Get 7 parameters from the _this array. + PARAMS_8(A,B,C,D,E,F,G,H) - Get 8 parameters from the _this array. + +Parameters: + A..H - Name of variable to read from _this [Identifier] + +Example: + A function called like this: + (begin example) + [_name,_address,_telephone] call recordPersonalDetails; + (end) + expects 3 parameters and those variables could be initialised at the start of the function definition with: + (begin example) + recordPersonalDetails = { + PARAMS_3(_name,_address,_telephone); + // Rest of function follows... + }; + (end) + +Author: + Spooner +------------------------------------------- */ +#define PARAMS_1(A) EXPLODE_1_PVT(_this,A) +#define PARAMS_2(A,B) EXPLODE_2_PVT(_this,A,B) +#define PARAMS_3(A,B,C) EXPLODE_3_PVT(_this,A,B,C) +#define PARAMS_4(A,B,C,D) EXPLODE_4_PVT(_this,A,B,C,D) +#define PARAMS_5(A,B,C,D,E) EXPLODE_5_PVT(_this,A,B,C,D,E) +#define PARAMS_6(A,B,C,D,E,F) EXPLODE_6_PVT(_this,A,B,C,D,E,F) +#define PARAMS_7(A,B,C,D,E,F,G) EXPLODE_7_PVT(_this,A,B,C,D,E,F,G) +#define PARAMS_8(A,B,C,D,E,F,G,H) EXPLODE_8_PVT(_this,A,B,C,D,E,F,G,H) +#define PARAMS_9(A,B,C,D,E,F,G,H,I) EXPLODE_9_PVT(_this,A,B,C,D,E,F,G,H,I) + +/* ------------------------------------------- +Macro: DEFAULT_PARAM() + DEPRECATED - Use param/params commands added in Arma 3 1.48 + + Getting a default function parameter. This may be used together with to have a mix of required and + optional parameters. + +Parameters: + INDEX - Index of parameter in _this [Integer, 0+] + NAME - Name of the variable to set [Identifier] + DEF_VALUE - Default value to use in case the array is too short or the value at INDEX is nil [Any] + +Example: + A function called with optional parameters: + (begin example) + [_name] call myFunction; + [_name, _numberOfLegs] call myFunction; + [_name, _numberOfLegs, _hasAHead] call myFunction; + (end) + 1 required parameter and 2 optional parameters. Those variables could be initialised at the start of the function + definition with: + (begin example) + myFunction = { + PARAMS_1(_name); + DEFAULT_PARAM(1,_numberOfLegs,2); + DEFAULT_PARAM(2,_hasAHead,true); + // Rest of function follows... + }; + (end) + +Author: + Spooner +------------------------------------------- */ +#define DEFAULT_PARAM(INDEX,NAME,DEF_VALUE) \ + private [#NAME,"_this"]; \ + ISNILS(_this,[]); \ + NAME = _this param [INDEX, DEF_VALUE]; \ + TRACE_3("DEFAULT_PARAM",INDEX,NAME,DEF_VALUE) + +/* ------------------------------------------- +Macro: KEY_PARAM() + Get value from key in _this list, return default when key is not included in list. + +Parameters: + KEY - Key name [String] + NAME - Name of the variable to set [Identifier] + DEF_VALUE - Default value to use in case key not found [ANY] + +Example: + + +Author: + Muzzleflash +------------------------------------------- */ +#define KEY_PARAM(KEY,NAME,DEF_VALUE) \ + private #NAME; \ + NAME = [toLower KEY, toUpper KEY, DEF_VALUE, RETNIL(_this)] call CBA_fnc_getArg; \ + TRACE_3("KEY_PARAM",KEY,NAME,DEF_VALUE) + +/* ------------------------------------------- +Group: Assertions +------------------------------------------- */ + +#define ASSERTION_ERROR(MESSAGE) ERROR_WITH_TITLE("Assertion failed!",MESSAGE) + +/* ------------------------------------------- +Macro: ASSERT_TRUE() + Asserts that a CONDITION is true. When an assertion fails, an error is raised with the given MESSAGE. + +Parameters: + CONDITION - Condition to assert as true [Boolean] + MESSSAGE - Message to display if (A OPERATOR B) is false [String] + +Example: + (begin example) + ASSERT_TRUE(_frogIsDead,"The frog is alive"); + (end) + +Author: + Spooner +------------------------------------------- */ +#define ASSERT_TRUE(CONDITION,MESSAGE) \ + if (not (CONDITION)) then \ + { \ + ASSERTION_ERROR('Assertion (CONDITION) failed!\n\n' + (MESSAGE)); \ + } + +/* ------------------------------------------- +Macro: ASSERT_FALSE() + Asserts that a CONDITION is false. When an assertion fails, an error is raised with the given MESSAGE. + +Parameters: + CONDITION - Condition to assert as false [Boolean] + MESSSAGE - Message to display if (A OPERATOR B) is true [String] + +Example: + (begin example) + ASSERT_FALSE(_frogIsDead,"The frog died"); + (end) + +Author: + Spooner +------------------------------------------- */ +#define ASSERT_FALSE(CONDITION,MESSAGE) \ + if (CONDITION) then \ + { \ + ASSERTION_ERROR('Assertion (not (CONDITION)) failed!\n\n' + (MESSAGE)) \ + } + +/* ------------------------------------------- +Macro: ASSERT_OP() + Asserts that (A OPERATOR B) is true. When an assertion fails, an error is raised with the given MESSAGE. + +Parameters: + A - First value [Any] + OPERATOR - Binary operator to use [Operator] + B - Second value [Any] + MESSSAGE - Message to display if (A OPERATOR B) is false. [String] + +Example: + (begin example) + ASSERT_OP(_fish,>,5,"Too few fish!"); + (end) + +Author: + Spooner +------------------------------------------- */ +#define ASSERT_OP(A,OPERATOR,B,MESSAGE) \ + if (not ((A) OPERATOR (B))) then \ + { \ + ASSERTION_ERROR('Assertion (A OPERATOR B) failed!\n' + 'A: ' + (str (A)) + '\n' + 'B: ' + (str (B)) + "\n\n" + (MESSAGE)); \ + } + +/* ------------------------------------------- +Macro: ASSERT_DEFINED() + Asserts that a VARIABLE is defined. When an assertion fails, an error is raised with the given MESSAGE.. + +Parameters: + VARIABLE - Variable to test if defined [String or Function]. + MESSAGE - Message to display if variable is undefined [String]. + +Examples: + (begin example) + ASSERT_DEFINED("_anUndefinedVar","Too few fish!"); + ASSERT_DEFINED({ obj getVariable "anUndefinedVar" },"Too many fish!"); + (end) + +Author: + Spooner +------------------------------------------- */ +#define ASSERT_DEFINED(VARIABLE,MESSAGE) \ + if (isNil VARIABLE) then \ + { \ + ASSERTION_ERROR('Assertion (VARIABLE is defined) failed!\n\n' + (MESSAGE)); \ + } + +/* ------------------------------------------- +Group: Unit tests +------------------------------------------- */ +#define TEST_SUCCESS(MESSAGE) MESSAGE_WITH_TITLE("Test OK",MESSAGE) +#define TEST_FAIL(MESSAGE) ERROR_WITH_TITLE("Test FAIL",MESSAGE) + +/* ------------------------------------------- +Macro: TEST_TRUE() + Tests that a CONDITION is true. + If the condition is not true, an error is raised with the given MESSAGE. + +Parameters: + CONDITION - Condition to assert as true [Boolean] + MESSSAGE - Message to display if (A OPERATOR B) is false [String] + +Example: + (begin example) + TEST_TRUE(_frogIsDead,"The frog is alive"); + (end) + +Author: + Killswitch +------------------------------------------- */ +#define TEST_TRUE(CONDITION, MESSAGE) \ + if (CONDITION) then \ + { \ + TEST_SUCCESS('(CONDITION)'); \ + } \ + else \ + { \ + TEST_FAIL('(CONDITION) ' + (MESSAGE)); \ + } + +/* ------------------------------------------- +Macro: TEST_FALSE() + Tests that a CONDITION is false. + If the condition is not false, an error is raised with the given MESSAGE. + +Parameters: + CONDITION - Condition to test as false [Boolean] + MESSSAGE - Message to display if (A OPERATOR B) is true [String] + +Example: + (begin example) + TEST_FALSE(_frogIsDead,"The frog died"); + (end) + +Author: + Killswitch +------------------------------------------- */ +#define TEST_FALSE(CONDITION, MESSAGE) \ + if (not (CONDITION)) then \ + { \ + TEST_SUCCESS('(not (CONDITION))'); \ + } \ + else \ + { \ + TEST_FAIL('(not (CONDITION)) ' + (MESSAGE)); \ + } + +/* ------------------------------------------- +Macro: TEST_OP() + Tests that (A OPERATOR B) is true. + If the test fails, an error is raised with the given MESSAGE. + +Parameters: + A - First value [Any] + OPERATOR - Binary operator to use [Operator] + B - Second value [Any] + MESSSAGE - Message to display if (A OPERATOR B) is false. [String] + +Example: + (begin example) + TEST_OP(_fish,>,5,"Too few fish!"); + (end) + +Author: + Killswitch +------------------------------------------- */ +#define TEST_OP(A,OPERATOR,B,MESSAGE) \ + if ((A) OPERATOR (B)) then \ + { \ + TEST_SUCCESS('(A OPERATOR B)') \ + } \ + else \ + { \ + TEST_FAIL('(A OPERATOR B)') \ + }; + +/* ------------------------------------------- +Macro: TEST_DEFINED_AND_OP() + Tests that A and B are defined and (A OPERATOR B) is true. + If the test fails, an error is raised with the given MESSAGE. + +Parameters: + A - First value [Any] + OPERATOR - Binary operator to use [Operator] + B - Second value [Any] + MESSSAGE - Message to display [String] + +Example: + (begin example) + TEST_OP(_fish,>,5,"Too few fish!"); + (end) + +Author: + Killswitch, PabstMirror +------------------------------------------- */ +#define TEST_DEFINED_AND_OP(A,OPERATOR,B,MESSAGE) \ + if (isNil #A) then { \ + TEST_FAIL('(A is not defined) ' + (MESSAGE)); \ + } else { \ + if (isNil #B) then { \ + TEST_FAIL('(B is not defined) ' + (MESSAGE)); \ + } else { \ + if ((A) OPERATOR (B)) then { \ + TEST_SUCCESS('(A OPERATOR B) ' + (MESSAGE)) \ + } else { \ + TEST_FAIL('(A OPERATOR B) ' + (MESSAGE)) \ + }; }; }; + + +/* ------------------------------------------- +Macro: TEST_DEFINED() + Tests that a VARIABLE is defined. + +Parameters: + VARIABLE - Variable to test if defined [String or Function]. + MESSAGE - Message to display if variable is undefined [String]. + +Examples: + (begin example) + TEST_DEFINED("_anUndefinedVar","Too few fish!"); + TEST_DEFINED({ obj getVariable "anUndefinedVar" },"Too many fish!"); + (end) + +Author: + Killswitch +------------------------------------------- */ +#define TEST_DEFINED(VARIABLE,MESSAGE) \ + if (not isNil VARIABLE) then \ + { \ + TEST_SUCCESS('(' + VARIABLE + ' is defined)'); \ + } \ + else \ + { \ + TEST_FAIL('(' + VARIABLE + ' is not defined)' + (MESSAGE)); \ + } + +/* ------------------------------------------- +Group: Managing Deprecation +------------------------------------------- */ + +/* ------------------------------------------- +Macro: DEPRECATE_SYS() + Allow deprecation of a function that has been renamed. + + Replaces an old OLD_FUNCTION (which will have PREFIX_ prepended) with a NEW_FUNCTION + (PREFIX_ prepended) with the intention that the old function will be disabled in the future. + + Shows a warning in RPT each time the deprecated function is used, but runs the new function. + +Parameters: + OLD_FUNCTION - Full name of old function [Identifier for function that does not exist any more] + NEW_FUNCTION - Full name of new function [Function] + +Example: + (begin example) + // After renaming CBA_fnc_frog as CBA_fnc_fish + DEPRECATE_SYS(CBA_fnc_frog,CBA_fnc_fish); + (end) + +Author: + Sickboy +------------------------------------------- */ +#define DEPRECATE_SYS(OLD_FUNCTION,NEW_FUNCTION) \ + OLD_FUNCTION = { \ + WARNING('Deprecated function used: OLD_FUNCTION (new: NEW_FUNCTION) in ADDON'); \ + if (isNil "_this") then { call NEW_FUNCTION } else { _this call NEW_FUNCTION }; \ + } + +/* ------------------------------------------- +Macro: DEPRECATE() + Allow deprecation of a function, in the current component, that has been renamed. + + Replaces an OLD_FUNCTION (which will have PREFIX_ prepended) with a NEW_FUNCTION + (PREFIX_ prepended) with the intention that the old function will be disabled in the future. + + Shows a warning in RPT each time the deprecated function is used, but runs the new function. + +Parameters: + OLD_FUNCTION - Name of old function, assuming PREFIX [Identifier for function that does not exist any more] + NEW_FUNCTION - Name of new function, assuming PREFIX [Function] + +Example: + (begin example) + // After renaming CBA_fnc_frog as CBA_fnc_fish + DEPRECATE(fnc_frog,fnc_fish); + (end) + +Author: + Sickboy +------------------------------------------- */ +#define DEPRECATE(OLD_FUNCTION,NEW_FUNCTION) \ + DEPRECATE_SYS(DOUBLES(PREFIX,OLD_FUNCTION),DOUBLES(PREFIX,NEW_FUNCTION)) + +/* ------------------------------------------- +Macro: OBSOLETE_SYS() + Replace a function that has become obsolete. + + Replace an obsolete OLD_FUNCTION with a simple COMMAND_FUNCTION, with the intention that anyone + using the function should replace it with the simple command, since the function will be disabled in the future. + + Shows a warning in RPT each time the deprecated function is used, and runs the command function. + +Parameters: + OLD_FUNCTION - Full name of old function [Identifier for function that does not exist any more] + COMMAND_CODE - Code to replace the old function [Function] + +Example: + (begin example) + // In Arma2, currentWeapon command made the CBA_fMyWeapon function obsolete: + OBSOLETE_SYS(CBA_fMyWeapon,{ currentWeapon player }); + (end) + +Author: + Spooner +------------------------------------------- */ +#define OBSOLETE_SYS(OLD_FUNCTION,COMMAND_CODE) \ + OLD_FUNCTION = { \ + WARNING('Obsolete function used: (use: OLD_FUNCTION) in ADDON'); \ + if (isNil "_this") then { call COMMAND_CODE } else { _this call COMMAND_CODE }; \ + } + +/* ------------------------------------------- +Macro: OBSOLETE() + Replace a function, in the current component, that has become obsolete. + + Replace an obsolete OLD_FUNCTION (which will have PREFIX_ prepended) with a simple + COMMAND_CODE, with the intention that anyone using the function should replace it with the simple + command. + + Shows a warning in RPT each time the deprecated function is used. + +Parameters: + OLD_FUNCTION - Name of old function, assuming PREFIX [Identifier for function that does not exist any more] + COMMAND_CODE - Code to replace the old function [Function] + +Example: + (begin example) + // In Arma2, currentWeapon command made the CBA_fMyWeapon function obsolete: + OBSOLETE(fMyWeapon,{ currentWeapon player }); + (end) + +Author: + Spooner +------------------------------------------- */ +#define OBSOLETE(OLD_FUNCTION,COMMAND_CODE) \ + OBSOLETE_SYS(DOUBLES(PREFIX,OLD_FUNCTION),COMMAND_CODE) + +#define BWC_CONFIG(NAME) class NAME { \ + units[] = {}; \ + weapons[] = {}; \ + requiredVersion = REQUIRED_VERSION; \ + requiredAddons[] = {}; \ + version = VERSION; \ +} + +// XEH Specific +#define XEH_CLASS CBA_Extended_EventHandlers +#define XEH_CLASS_BASE DOUBLES(XEH_CLASS,base) +#define XEH_DISABLED class EventHandlers { class XEH_CLASS {}; }; SLX_XEH_DISABLED = 1 +#define XEH_ENABLED class EventHandlers { class XEH_CLASS { EXTENDED_EVENTHANDLERS }; }; SLX_XEH_DISABLED = 0 + +// TODO: These are actually outdated; _Once ? +#define XEH_PRE_INIT QUOTE(call COMPILE_FILE(XEH_PreInit_Once)) +#define XEH_PRE_CINIT QUOTE(call COMPILE_FILE(XEH_PreClientInit_Once)) +#define XEH_PRE_SINIT QUOTE(call COMPILE_FILE(XEH_PreServerInit_Once)) + +#define XEH_POST_INIT QUOTE(call COMPILE_FILE(XEH_PostInit_Once)) +#define XEH_POST_CINIT QUOTE(call COMPILE_FILE(XEH_PostClientInit_Once)) +#define XEH_POST_SINIT QUOTE(call COMPILE_FILE(XEH_PostServerInit_Once)) + +/* ------------------------------------------- +Macro: IS_ADMIN + Check if the local machine is an admin in the multiplayer environment. + + Reports 'true' for logged and voted in admins. + +Parameters: + None + +Example: + (begin example) + // print "true" if player is admin + systemChat str IS_ADMIN; + (end) + +Author: + commy2 +------------------------------------------- */ +#define IS_ADMIN_SYS(x) x##kick +#define IS_ADMIN serverCommandAvailable 'IS_ADMIN_SYS(#)' + +/* ------------------------------------------- +Macro: IS_ADMIN_LOGGED + Check if the local machine is a logged in admin in the multiplayer environment. + + Reports 'false' if the player was voted to be the admin. + +Parameters: + None + +Example: + (begin example) + // print "true" if player is admin and entered in the server password + systemChat str IS_ADMIN_LOGGED; + (end) + +Author: + commy2 +------------------------------------------- */ +#define IS_ADMIN_LOGGED_SYS(x) x##shutdown +#define IS_ADMIN_LOGGED serverCommandAvailable 'IS_ADMIN_LOGGED_SYS(#)' + +/* ------------------------------------------- +Macro: FILE_EXISTS + Check if a file exists + + Reports "false" if the file does not exist. + +Parameters: + FILE - Path to the file + +Example: + (begin example) + // print "true" if file exists + systemChat str FILE_EXISTS("\A3\ui_f\data\igui\cfg\cursors\weapon_ca.paa"); + (end) + +Author: + commy2 +------------------------------------------- */ +#define FILE_EXISTS(FILE) (fileExists (FILE)) diff --git a/arma/client/include/x/cba/addons/xeh/script_xeh.hpp b/arma/client/include/x/cba/addons/xeh/script_xeh.hpp new file mode 100644 index 0000000..2eba000 --- /dev/null +++ b/arma/client/include/x/cba/addons/xeh/script_xeh.hpp @@ -0,0 +1,118 @@ +/* + Header: script_xeh.hpp + + Description: + Used internally. +*/ +///////////////////////////////////////////////////////////////////////////////// +// MACRO: EXTENDED_EVENTHANDLERS +// Add all XEH event handlers +///////////////////////////////////////////////////////////////////////////////// + +#define EXTENDED_EVENTHANDLERS init = "call cba_xeh_fnc_init"; \ +fired = "call cba_xeh_fnc_fired"; \ +animChanged = "call cba_xeh_fnc_animChanged"; \ +animDone = "call cba_xeh_fnc_animDone"; \ +animStateChanged = "call cba_xeh_fnc_animStateChanged"; \ +containerClosed = "call cba_xeh_fnc_containerClosed"; \ +containerOpened = "call cba_xeh_fnc_containerOpened"; \ +controlsShifted = "call cba_xeh_fnc_controlsShifted"; \ +dammaged = "call cba_xeh_fnc_dammaged"; \ +engine = "call cba_xeh_fnc_engine"; \ +epeContact = "call cba_xeh_fnc_epeContact"; \ +epeContactEnd = "call cba_xeh_fnc_epeContactEnd"; \ +epeContactStart = "call cba_xeh_fnc_epeContactStart"; \ +explosion = "call cba_xeh_fnc_explosion"; \ +firedNear = "call cba_xeh_fnc_firedNear"; \ +fuel = "call cba_xeh_fnc_cba_xeh_fuel"; \ +gear = "call cba_xeh_fnc_gear"; \ +getIn = "call cba_xeh_fnc_getIn"; \ +getInMan = "call cba_xeh_fnc_getInMan"; \ +getOut = "call cba_xeh_fnc_getOut"; \ +getOutMan = "call cba_xeh_fnc_getOutMan"; \ +handleHeal = "call cba_xeh_fnc_handleHeal"; \ +hit = "call cba_xeh_fnc_hit"; \ +hitPart = "call cba_xeh_fnc_hitPart"; \ +incomingMissile = "call cba_xeh_fnc_incomingMissile"; \ +inventoryClosed = "call cba_xeh_fnc_inventoryClosed"; \ +inventoryOpened = "call cba_xeh_fnc_inventoryOpened"; \ +killed = "call cba_xeh_fnc_killed"; \ +landedTouchDown = "call cba_xeh_fnc_landedTouchDown"; \ +landedStopped = "call cba_xeh_fnc_landedStopped"; \ +local = "call cba_xeh_fnc_local"; \ +respawn = "call cba_xeh_fnc_respawn"; \ +put = "call cba_xeh_fnc_put"; \ +take = "call cba_xeh_fnc_take"; \ +seatSwitched = "call cba_xeh_fnc_seatSwitched"; \ +seatSwitchedMan = "call cba_xeh_fnc_seatSwitchedMan"; \ +soundPlayed = "call cba_xeh_fnc_soundPlayed"; \ +weaponAssembled = "call cba_xeh_fnc_weaponAssembled"; \ +weaponDisassembled = "call cba_xeh_fnc_weaponDisassembled"; \ +weaponDeployed = "call cba_xeh_fnc_weaponDeployed"; \ +weaponRested = "call cba_xeh_fnc_weaponRested"; \ +reloaded = "call cba_xeh_fnc_reloaded"; \ +firedMan = "call cba_xeh_fnc_firedMan"; \ +turnIn = "call cba_xeh_fnc_turnIn"; \ +turnOut = "call cba_xeh_fnc_turnOut"; \ +deleted = "call cba_xeh_fnc_deleted"; \ +disassembled = "call cba_xeh_fnc_disassembled"; \ +Suppressed = "call cba_xeh_fnc_Suppressed"; \ +gestureChanged = "call cba_xeh_fnc_gestureChanged"; \ +gestureDone = "call cba_xeh_fnc_gestureDone"; + +/* + MACRO: DELETE_EVENTHANDLERS + + Removes all event handlers. +*/ + +#define DELETE_EVENTHANDLERS init = ""; \ +fired = ""; \ +animChanged = ""; \ +animDone = ""; \ +animStateChanged = ""; \ +containerClosed = ""; \ +containerOpened = ""; \ +controlsShifted = ""; \ +dammaged = ""; \ +engine = ""; \ +epeContact = ""; \ +epeContactEnd = ""; \ +epeContactStart = ""; \ +explosion = ""; \ +firedNear = ""; \ +fuel = ""; \ +gear = ""; \ +getIn = ""; \ +getInMan = ""; \ +getOut = ""; \ +getOutMan = ""; \ +handleHeal = ""; \ +hit = ""; \ +hitPart = ""; \ +incomingMissile = ""; \ +inventoryClosed = ""; \ +inventoryOpened = ""; \ +killed = ""; \ +landedTouchDown = ""; \ +landedStopped = ""; \ +local = ""; \ +respawn = ""; \ +put = ""; \ +take = ""; \ +seatSwitched = ""; \ +seatSwitchedMan = ""; \ +soundPlayed = ""; \ +weaponAssembled = ""; \ +weaponDisassembled = ""; \ +weaponDeployed = ""; \ +weaponRested = ""; \ +reloaded = ""; \ +firedMan = ""; \ +turnIn = ""; \ +turnOut = ""; \ +deleted = ""; \ +disassembled = ""; \ +Suppressed = ""; \ +gestureChanged = ""; \ +gestureDone = "" diff --git a/arma/client/meta.cpp b/arma/client/meta.cpp new file mode 100644 index 0000000..1ef1072 --- /dev/null +++ b/arma/client/meta.cpp @@ -0,0 +1,4 @@ +protocol = 1; +publishedid = MOD_ID; +name = "forge-client"; +timestamp = 5250140732737923549; diff --git a/arma/client/mod.cpp b/arma/client/mod.cpp new file mode 100644 index 0000000..c833d00 --- /dev/null +++ b/arma/client/mod.cpp @@ -0,0 +1,15 @@ +dir = "@forge_client"; +author = "J.Schmidt"; +name = "Forge Client"; +description = "Forge Client - Version 1.0.0"; +overview = ""; +overviewPicture = "title_ca.paa"; +picture = "title_ca.paa"; +logoSmall = "icon_64_ca.paa"; +logo = "icon_128_ca.paa"; +logoOver = "icon_128_highlight_ca.paa"; +tooltip = "Forge Client"; +tooltipOwned = "IDS Owned"; +action = "https://innovativedevsolutions.org"; +actionName = "Website"; +dlcColor[] = {0.45, 0.47, 0.41, 1}; diff --git a/arma/client/title_ca.paa b/arma/client/title_ca.paa new file mode 100644 index 0000000..f46f6d0 Binary files /dev/null and b/arma/client/title_ca.paa differ diff --git a/arma/client/tools/config_style_checker.py b/arma/client/tools/config_style_checker.py new file mode 100644 index 0000000..2332750 --- /dev/null +++ b/arma/client/tools/config_style_checker.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 + +import fnmatch +import os +import re +import ntpath +import sys +import argparse + +def check_config_style(filepath): + bad_count_file = 0 + def pushClosing(t): + closingStack.append(closing.expr) + closing << Literal( closingFor[t[0]] ) + + def popClosing(): + closing << closingStack.pop() + + reIsClass = re.compile(r'^\s*class(.*)') + reIsClassInherit = re.compile(r'^\s*class(.*):') + reIsClassBody = re.compile(r'^\s*class(.*){') + reBadColon = re.compile(r'\s*class (.*) :') + reSpaceAfterColon = re.compile(r'\s*class (.*): ') + reSpaceBeforeCurly = re.compile(r'\s*class (.*) {') + reClassSingleLine = re.compile(r'\s*class (.*)[{;]') + + with open(filepath, 'r', encoding='utf-8', errors='ignore') as file: + content = file.read() + + # Store all brackets we find in this file, so we can validate everything on the end + brackets_list = [] + + # To check if we are in a comment block + isInCommentBlock = False + checkIfInComment = False + # Used in case we are in a line comment (//) + ignoreTillEndOfLine = False + # Used in case we are in a comment block (/* */). This is true if we detect a * inside a comment block. + # If the next character is a /, it means we end our comment block. + checkIfNextIsClosingBlock = False + + # We ignore everything inside a string + isInString = False + # Used to store the starting type of a string, so we can match that to the end of a string + inStringType = ''; + + lastIsCurlyBrace = False + checkForSemiColumn = False + + # Extra information so we know what line we find errors at + lineNumber = 1 + + indexOfCharacter = 0 + # Parse all characters in the content of this file to search for potential errors + for c in content: + if (lastIsCurlyBrace): + lastIsCurlyBrace = False + if c == '\n': # Keeping track of our line numbers + lineNumber += 1 # so we can print accurate line number information when we detect a possible error + if (isInString): # while we are in a string, we can ignore everything else, except the end of the string + if (c == inStringType): + isInString = False + # if we are not in a comment block, we will check if we are at the start of one or count the () {} and [] + elif (isInCommentBlock == False): + + # This means we have encountered a /, so we are now checking if this is an inline comment or a comment block + if (checkIfInComment): + checkIfInComment = False + if c == '*': # if the next character after / is a *, we are at the start of a comment block + isInCommentBlock = True + elif (c == '/'): # Otherwise, will check if we are in an line comment + ignoreTillEndOfLine = True # and an line comment is a / followed by another / (//) We won't care about anything that comes after it + + if (isInCommentBlock == False): + if (ignoreTillEndOfLine): # we are in a line comment, just continue going through the characters until we find an end of line + if (c == '\n'): + ignoreTillEndOfLine = False + else: # validate brackets + if (c == '"' or c == "'"): + isInString = True + inStringType = c + elif (c == '/'): + checkIfInComment = True + elif (c == '('): + brackets_list.append('(') + elif (c == ')'): + if (len(brackets_list) > 0 and brackets_list[-1] in ['{', '[']): + print("ERROR: Possible missing round bracket ')' detected at {0} Line number: {1}".format(filepath,lineNumber)) + bad_count_file += 1 + brackets_list.append(')') + elif (c == '['): + brackets_list.append('[') + elif (c == ']'): + if (len(brackets_list) > 0 and brackets_list[-1] in ['{', '(']): + print("ERROR: Possible missing square bracket ']' detected at {0} Line number: {1}".format(filepath,lineNumber)) + bad_count_file += 1 + brackets_list.append(']') + elif (c == '{'): + brackets_list.append('{') + elif (c == '}'): + lastIsCurlyBrace = True + if (len(brackets_list) > 0 and brackets_list[-1] in ['(', '[']): + print("ERROR: Possible missing curly brace '}}' detected at {0} Line number: {1}".format(filepath,lineNumber)) + bad_count_file += 1 + brackets_list.append('}') + elif (c== '\t'): + print("ERROR: Tab detected at {0} Line number: {1}".format(filepath,lineNumber)) + bad_count_file += 1 + + else: # Look for the end of our comment block + if (c == '*'): + checkIfNextIsClosingBlock = True; + elif (checkIfNextIsClosingBlock): + if (c == '/'): + isInCommentBlock = False + elif (c != '*'): + checkIfNextIsClosingBlock = False + indexOfCharacter += 1 + + if brackets_list.count('[') != brackets_list.count(']'): + print("ERROR: A possible missing square bracket [ or ] in file {0} [ = {1} ] = {2}".format(filepath,brackets_list.count('['),brackets_list.count(']'))) + bad_count_file += 1 + if brackets_list.count('(') != brackets_list.count(')'): + print("ERROR: A possible missing round bracket ( or ) in file {0} ( = {1} ) = {2}".format(filepath,brackets_list.count('('),brackets_list.count(')'))) + bad_count_file += 1 + if brackets_list.count('{') != brackets_list.count('}'): + print("ERROR: A possible missing curly brace {{ or }} in file {0} {{ = {1} }} = {2}".format(filepath,brackets_list.count('{'),brackets_list.count('}'))) + bad_count_file += 1 + + file.seek(0) + for lineNumber, line in enumerate(file.readlines()): + if reIsClass.match(line): + if reBadColon.match(line): + print(f"WARNING: bad class colon {filepath} Line number: {lineNumber+1}") + # bad_count_file += 1 + if reIsClassInherit.match(line): + if not reSpaceAfterColon.match(line): + print(f"WARNING: bad class missing space after colon {filepath} Line number: {lineNumber+1}") + if reIsClassBody.match(line): + if not reSpaceBeforeCurly.match(line): + print(f"WARNING: bad class inherit missing space before curly braces {filepath} Line number: {lineNumber+1}") + if not reClassSingleLine.match(line): + print(f"WARNING: bad class braces placement {filepath} Line number: {lineNumber+1}") + # bad_count_file += 1 + + return bad_count_file + +def main(): + + print("Validating Config Style") + + sqf_list = [] + bad_count = 0 + + parser = argparse.ArgumentParser() + parser.add_argument('-m','--module', help='only search specified module addon folder', required=False, default="") + args = parser.parse_args() + + for folder in ['addons', 'optionals']: + # Allow running from root directory as well as from inside the tools directory + rootDir = "../" + folder + if (os.path.exists(folder)): + rootDir = folder + + for root, dirnames, filenames in os.walk(rootDir + '/' + args.module): + for filename in fnmatch.filter(filenames, '*.cpp'): + sqf_list.append(os.path.join(root, filename)) + for filename in fnmatch.filter(filenames, '*.hpp'): + sqf_list.append(os.path.join(root, filename)) + for filename in fnmatch.filter(filenames, '*.rvmat'): + sqf_list.append(os.path.join(root, filename)) + for filename in fnmatch.filter(filenames, '*.cfg'): + sqf_list.append(os.path.join(root, filename)) + + for filename in sqf_list: + bad_count = bad_count + check_config_style(filename) + + print("------\nChecked {0} files\nErrors detected: {1}".format(len(sqf_list), bad_count)) + if (bad_count == 0): + print("Config validation PASSED") + else: + print("Config validation FAILED") + + return bad_count + +if __name__ == "__main__": + sys.exit(main()) diff --git a/arma/client/tools/release.bat b/arma/client/tools/release.bat new file mode 100644 index 0000000..667a74c --- /dev/null +++ b/arma/client/tools/release.bat @@ -0,0 +1,4 @@ +@ECHO off +hemtt script update_build.rhai +hemtt script update_minor.rhai +hemtt release diff --git a/arma/client/tools/release_patch.bat b/arma/client/tools/release_patch.bat new file mode 100644 index 0000000..ec4db02 --- /dev/null +++ b/arma/client/tools/release_patch.bat @@ -0,0 +1,4 @@ +@ECHO off +hemtt script update_build.rhai +hemtt script update_patch.rhai +hemtt release diff --git a/arma/example_data.sqf b/arma/example_data.sqf new file mode 100644 index 0000000..c3ddf3c --- /dev/null +++ b/arma/example_data.sqf @@ -0,0 +1,5 @@ +[["76561198027566824",[["rank","PRIVATE"],["earnings",0],["holster",true],["state","HEALTHY"],["bank",0],["organization","0160566824_org"],["transactions",[]],["loadout",[[],[],[],["U_BG_Guerrilla_6_1",[]],[],[],"H_Cap_blk_ION","",[],["ItemMap","ItemGPS","ItemRadio","ItemCompass","ItemWatch",""]]],["stance","CROUCH"],["cash",0],["uid","76561198027566824"],["phone_number","0160566824"],["direction",0],["position",[4000,4000,0]],["email","0160566824@spearnet.mil"]]]]; + +["{""uid"":""76561198027566824"",""loadout"":[[],[],[],[""U_BG_Guerrilla_6_1"",[]],[],[],""H_Cap_blk_ION"","""",[],[""ItemMap"",""ItemGPS"",""ItemRadio"",""ItemCompass"",""ItemWatch"",""""]],""position"":[4000.0,4000.0,0.0],""direction"":0.0,""stance"":""CROUCH"",""email"":""0160566824@spearnet.mil"",""phone_number"":""0160566824"",""bank"":0.0,""cash"":0.0,""earnings"":0.0,""state"":""HEALTHY"",""holster"":true,""rank"":""PRIVATE"",""organization"":""0160566824_org"",""transactions"":[]}",0,0]; + +["{""id"":""0160566824_org"",""owner"":""76561198027566824"",""name"":""Black Rifle Company"",""funds"":0.0,""reputation"":0}",0,0]; diff --git a/arma/server/.editorconfig b/arma/server/.editorconfig new file mode 100644 index 0000000..4246311 --- /dev/null +++ b/arma/server/.editorconfig @@ -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 diff --git a/arma/server/.gitattributes b/arma/server/.gitattributes new file mode 100644 index 0000000..dae0c81 --- /dev/null +++ b/arma/server/.gitattributes @@ -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 diff --git a/arma/server/.github/CONTRIBUTING.md b/arma/server/.github/CONTRIBUTING.md new file mode 100644 index 0000000..b7b4679 --- /dev/null +++ b/arma/server/.github/CONTRIBUTING.md @@ -0,0 +1,12 @@ +# 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). diff --git a/arma/server/.github/ISSUE_TEMPLATE/bug-report.md b/arma/server/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..d4c384f --- /dev/null +++ b/arma/server/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,25 @@ +--- +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. diff --git a/arma/server/.github/ISSUE_TEMPLATE/feature-request.md b/arma/server/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..709ee6c --- /dev/null +++ b/arma/server/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,15 @@ +--- +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. diff --git a/arma/server/.github/PULL_REQUEST_TEMPLATE.md b/arma/server/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..6f72c35 --- /dev/null +++ b/arma/server/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,12 @@ +**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 +- [ ] Issue diff --git a/arma/server/.github/assets/placeholder.txt b/arma/server/.github/assets/placeholder.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/arma/server/.github/assets/placeholder.txt @@ -0,0 +1 @@ + diff --git a/arma/server/.github/workflows/check.yml b/arma/server/.github/workflows/check.yml new file mode 100644 index 0000000..9d2f654 --- /dev/null +++ b/arma/server/.github/workflows/check.yml @@ -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 diff --git a/arma/server/.gitignore b/arma/server/.gitignore new file mode 100644 index 0000000..b786b16 --- /dev/null +++ b/arma/server/.gitignore @@ -0,0 +1,17 @@ +# HEMTT +hemtt.exe +.hemtt/missions/~* +.hemttout/ +releases/ + +# Textures +Exports/ +*.spp +*.spp.painter_lock +*.psd + +# Other +*.biprivatekey +*.zip +*.pbo +*.sqfc diff --git a/arma/server/.hemtt/commands/ctrlWebBrowserAction.yml b/arma/server/.hemtt/commands/ctrlWebBrowserAction.yml new file mode 100644 index 0000000..97b2d5f --- /dev/null +++ b/arma/server/.hemtt/commands/ctrlWebBrowserAction.yml @@ -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: + - _control ctrlWebBrowserAction ["ExecJS", "document.getElementById('test').innerHTML = 'Hello World!'"]; + - _control ctrlWebBrowserAction ["LoadURL", "https://community.bistudio.com"]; diff --git a/arma/server/.hemtt/hooks/post_release/01_move_readme.rhai b/arma/server/.hemtt/hooks/post_release/01_move_readme.rhai new file mode 100644 index 0000000..eb59898 --- /dev/null +++ b/arma/server/.hemtt/hooks/post_release/01_move_readme.rhai @@ -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()); diff --git a/arma/server/.hemtt/hooks/pre_build/01_set_version.rhai b/arma/server/.hemtt/hooks/pre_build/01_set_version.rhai new file mode 100644 index 0000000..c7930cd --- /dev/null +++ b/arma/server/.hemtt/hooks/pre_build/01_set_version.rhai @@ -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()); diff --git a/arma/server/.hemtt/launch.toml b/arma/server/.hemtt/launch.toml new file mode 100644 index 0000000..1e2e390 --- /dev/null +++ b/arma/server/.hemtt/launch.toml @@ -0,0 +1,16 @@ +[default] +workshop = [ + "450814997", # CBA_A3 + "3499977893", # Advanced Dev Tools + "623475643", # 3DEN Enhanced +] +presets = [] +dlc = [] +optionals = [] +parameters = [] + +[ace] +extends = "default" +workshop = [ + "463939057", # ACE +] diff --git a/arma/server/.hemtt/lints.toml b/arma/server/.hemtt/lints.toml new file mode 100644 index 0000000..98a617b --- /dev/null +++ b/arma/server/.hemtt/lints.toml @@ -0,0 +1,35 @@ +[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.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 diff --git a/arma/server/.hemtt/project.toml b/arma/server/.hemtt/project.toml new file mode 100644 index 0000000..11b6a3c --- /dev/null +++ b/arma/server/.hemtt/project.toml @@ -0,0 +1,24 @@ +name = "forge-server" +author = "J.Schmidt" +prefix = "forge_server" +mainprefix = "forge" + +[version] +path = "addons/main/script_version.hpp" +git_hash = 0 + +[files] +include = [ + "mod.cpp", + "meta.cpp", + "logo_forge_server.png", + "logo_forge_server_over.png", + "logo_forge_server_ca.paa", + "logo_forge_server_over_ca.paa", + "LICENSE.md", + "README.md", +] +exclude = [] + +[properties] +author = "J.Schmidt" diff --git a/arma/server/.hemtt/scripts/update_build.rhai b/arma/server/.hemtt/scripts/update_build.rhai new file mode 100644 index 0000000..14bdbeb --- /dev/null +++ b/arma/server/.hemtt/scripts/update_build.rhai @@ -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); diff --git a/arma/server/.hemtt/scripts/update_minor.rhai b/arma/server/.hemtt/scripts/update_minor.rhai new file mode 100644 index 0000000..b8344fb --- /dev/null +++ b/arma/server/.hemtt/scripts/update_minor.rhai @@ -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); diff --git a/arma/server/.hemtt/scripts/update_patch.rhai b/arma/server/.hemtt/scripts/update_patch.rhai new file mode 100644 index 0000000..a90383f --- /dev/null +++ b/arma/server/.hemtt/scripts/update_patch.rhai @@ -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); diff --git a/arma/server/LICENSE.md b/arma/server/LICENSE.md new file mode 100644 index 0000000..659cbdc --- /dev/null +++ b/arma/server/LICENSE.md @@ -0,0 +1,119 @@ +![APL-SA](https://www.bohemia.net/assets/img/licenses/APL-SA.png) + +## 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. \ No newline at end of file diff --git a/arma/server/README.md b/arma/server/README.md new file mode 100644 index 0000000..970fcd3 --- /dev/null +++ b/arma/server/README.md @@ -0,0 +1,27 @@ +

Forge Server

+

+ Version + Issues + + License +
+ HEMTT + CBA A3 +

+ +

+ Requires the latest version of CBA A3 +

+ +**Forge Server** aims to... + +The project is entirely **open-source** and any contributions are welcome. + +## Core Features +- Feature + +## Contributing +For new contributers, see the [Contributing Setup & Guidelines](./.github/CONTRIBUTING.md). + +## License +Forge Server is licensed under [APL-SA](./LICENSE.md). diff --git a/arma/server/addons/actor/$PBOPREFIX$ b/arma/server/addons/actor/$PBOPREFIX$ new file mode 100644 index 0000000..0d8ae67 --- /dev/null +++ b/arma/server/addons/actor/$PBOPREFIX$ @@ -0,0 +1 @@ +forge\forge_server\addons\actor diff --git a/arma/server/addons/actor/CfgEventHandlers.hpp b/arma/server/addons/actor/CfgEventHandlers.hpp new file mode 100644 index 0000000..f6503c2 --- /dev/null +++ b/arma/server/addons/actor/CfgEventHandlers.hpp @@ -0,0 +1,17 @@ +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)); + }; +}; + +class Extended_PostInit_EventHandlers { + class ADDON { + init = QUOTE(call COMPILE_SCRIPT(XEH_postInit)); + }; +}; diff --git a/arma/server/addons/actor/README.md b/arma/server/addons/actor/README.md new file mode 100644 index 0000000..66b48ec --- /dev/null +++ b/arma/server/addons/actor/README.md @@ -0,0 +1,4 @@ +forge_server_actor +=================== + +Description for this addon diff --git a/arma/server/addons/actor/XEH_PREP.hpp b/arma/server/addons/actor/XEH_PREP.hpp new file mode 100644 index 0000000..50e6a39 --- /dev/null +++ b/arma/server/addons/actor/XEH_PREP.hpp @@ -0,0 +1 @@ +PREP(initActorStore); diff --git a/arma/server/addons/actor/XEH_postInit.sqf b/arma/server/addons/actor/XEH_postInit.sqf new file mode 100644 index 0000000..421c54b --- /dev/null +++ b/arma/server/addons/actor/XEH_postInit.sqf @@ -0,0 +1 @@ +#include "script_component.hpp" diff --git a/arma/server/addons/actor/XEH_preInit.sqf b/arma/server/addons/actor/XEH_preInit.sqf new file mode 100644 index 0000000..e46d7bd --- /dev/null +++ b/arma/server/addons/actor/XEH_preInit.sqf @@ -0,0 +1,85 @@ +#include "script_component.hpp" + +PREP_RECOMPILE_START; +#include "XEH_PREP.hpp" +PREP_RECOMPILE_END; + +// private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)]; + +if (isNil QGVAR(ActorStore)) then { [] call FUNC(initActorStore); }; + +[QGVAR(requestInitActor), { + params [["_uid", "", [""]], ["_actor", createHashMap, [createHashMap]]]; + + if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid UID!" }; + if (_actor isEqualTo createHashMap) exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid Actor data!" }; + + GVAR(ActorStore) call ["init", [_uid, _actor]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestGetActor), { + params [["_uid", "", [""]], ["_sync", false, [false]]]; + + if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid UID!" }; + + private _session = GVAR(PlayerSessions) getOrDefault [_uid, nil]; + if (isNil "_session") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid Session!" }; + + GVAR(ActorStore) call ["get", [_uid, _sync]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestSetActor), { + params [["_uid", "", [""]], ["_key", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]]; + + if (_uid isEqualTo "" || _key isEqualTo "") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid UID or Key!" }; + + private _session = GVAR(PlayerSessions) getOrDefault [_uid, nil]; + if (isNil "_session") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid Session!" }; + + GVAR(ActorStore) call ["set", [_uid, _key, _value, _sync]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestMSetActor), { + params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; + + if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid UID!" }; + if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid field pairs!" }; + + private _session = GVAR(PlayerSessions) getOrDefault [_uid, nil]; + if (isNil "_session") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid Session!" }; + + GVAR(ActorStore) call ["mset", [_uid, _fieldValuePairs, _sync]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestSaveActor), { + params [["_uid", "", [""]], ["_sync", false, [false]]]; + + if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid UID!" }; + + private _session = GVAR(PlayerSessions) getOrDefault [_uid, nil]; + if (isNil "_session") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid Session!" }; + + GVAR(ActorStore) call ["save", [_uid, _sync]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestRemoveActor), { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid UID!" }; + + GVAR(ActorStore) call ["remove", [_uid]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestResync), { + params ["_player"]; + + private _uid = getPlayerUID _player; + private _actor = GVAR(ActorStore) call ["get", [_uid, true]]; + + private _session = GVAR(PlayerSessions) getOrDefault [_uid, nil]; + if (isNil "_session") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid Session!" }; + + [CRPC(actor,responseSyncActor), [_actor, true], _player] call CFUNC(targetEvent); + + diag_log format ["[FORGE:Server:Actor] Resync completed for %1", _uid]; +}] call CFUNC(addEventHandler); diff --git a/arma/server/addons/actor/XEH_preStart.sqf b/arma/server/addons/actor/XEH_preStart.sqf new file mode 100644 index 0000000..0228885 --- /dev/null +++ b/arma/server/addons/actor/XEH_preStart.sqf @@ -0,0 +1,3 @@ +#include "script_component.hpp" + +#include "XEH_PREP.hpp" diff --git a/arma/server/addons/actor/config.cpp b/arma/server/addons/actor/config.cpp new file mode 100644 index 0000000..05c825e --- /dev/null +++ b/arma/server/addons/actor/config.cpp @@ -0,0 +1,19 @@ +#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_server_main" + }; + units[] = {}; + weapons[] = {}; + VERSION_CONFIG; + }; +}; + +#include "CfgEventHandlers.hpp" diff --git a/arma/server/addons/actor/functions/fnc_initActorStore.sqf b/arma/server/addons/actor/functions/fnc_initActorStore.sqf new file mode 100644 index 0000000..b0447e0 --- /dev/null +++ b/arma/server/addons/actor/functions/fnc_initActorStore.sqf @@ -0,0 +1,181 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Initializes the actor store. + * + * Arguments: + * None + * + * Return Value: + * None + * + * Examples: + * [] call forge_server_actor_fnc_initActorStore + * + * Public: Yes + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(ActorStore) = createHashMapObject [[ + ["#type", "IActorStore"], + ["#create", { + GVAR(ActorRegistry) = createHashMap; + GVAR(PlayerSessions) = createHashMap; + diag_log "[FORGE:Server:Actor] Actor Store Initialized!"; + }], + ["generateSessionToken", { + params [["_uid", "", [""]]]; + + private _token = format ["%1_%2_%3", _uid, floor(random 999999), time]; + private _sessionToken = _token call EFUNC(common,generateHash); + + GVAR(PlayerSessions) set [_uid, _sessionToken]; + _sessionToken + }], + ["init", { + params [["_uid", "", [""]], ["_defaultActor", createHashMap, [createHashMap]]]; + + _self call ["generateSessionToken", [_uid]]; + private _finalActor = createHashMap; + + EXTCALL("actor:exists",[ARR_1(_uid)]); + private _exists = (_ext_res select 0) == "true"; + + if !(_exists) then { + _finalActor = _defaultActor; + _finalActor set ["uid", _uid]; + + private _json = _self call ["toJSON", [_finalActor]]; + EXTCALL("actor:create",[ARR_2(_uid,_json)]); + + private _phone_number = _finalActor getOrDefault ["phone_number", ""]; + private _email = _finalActor getOrDefault ["email", ""]; + + diag_log format ["[FORGE:Server:Actor] New player %1 registered with phone number: %2, email: %3", _uid, _phone_number, _email]; + } else { + private _existingActor = _self call ["fetch", [_uid]]; + _finalActor = _existingActor; + + { + if !(_x in _finalActor) then { _finalActor set [_x, _y]; }; + } forEach _defaultActor; + }; + + GVAR(ActorRegistry) set [_uid, _finalActor]; + + private _player = [_uid] call EFUNC(common,getPlayer); + [CRPC(actor,responseInitActor), [_finalActor], _player] call CFUNC(targetEvent); + + _finalActor + }], + ["fetch", { + params [["_uid", "", [""]]]; + + private _actor = createHashMap; + + EXTCALL("actor:get",[ARR_1(_uid)]); + diag_log format ["[FORGE:Server:Actor] Data: %1", _ext_res]; + + private _ext_res_actor = _ext_res select 0; + if (count _ext_res_actor > 0) then { _actor = _self call ["toHashMap", [_ext_res_actor]]; }; + + _actor + }], + ["get", { + params [["_uid", "", [""]], ["_sync", false, [false]]]; + + private _finalActor = createHashMap; + + if (_sync) then { + private _existingActor = _self call ["fetch", [_uid]]; + _finalActor = _existingActor; + + GVAR(ActorRegistry) set [_uid, _finalActor]; + } else { + _finalActor = GVAR(ActorRegistry) get _uid; + }; + + _finalActor + }], + ["set", { + params [["_uid", "", [""]], ["_field", "", [""]], ["_value", nil], ["_sync", false, [false]]]; + + private _existingActor = GVAR(ActorRegistry) get _uid; + private _finalActor = +_existingActor; + private _hashMap = createHashMap; + + _finalActor set [_field, _value]; + _hashMap set [_field, _value]; + + GVAR(ActorRegistry) set [_uid, _finalActor]; + + if (_sync) then { + private _json = _self call ["toJSON", [_hashMap]]; + EXTCALL("actor:update",[ARR_2(_uid,_json)]); + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + [CRPC(actor,responseSyncActor), [_hashMap], _player] call CFUNC(targetEvent); + + _hashMap + }], + ["mset", { + params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; + + private _existingActor = GVAR(ActorRegistry) get _uid; + private _finalActor = +_existingActor; + private _hashMap = createHashMap; + + { _finalActor set [_x, _y]; } forEach _fieldValuePairs; + { _hashMap set [_x, _y]; } forEach _fieldValuePairs; + + GVAR(ActorRegistry) set [_uid, _finalActor]; + + if (_sync) then { + private _json = _self call ["toJSON", [_hashMap]]; + EXTCALL("actor:update",[ARR_2(_uid,_json)]); + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + [CRPC(actor,responseSyncActor), [_hashMap], _player] call CFUNC(targetEvent); + + _hashMap + }], + ["save", { + params [["_uid", "", [""]], ["_sync", false, [false]]]; + + private _existingActor = GVAR(ActorRegistry) get _uid; + private _finalActor = +_existingActor; + private _json = _self call ["toJSON", [_finalActor]]; + + EXTCALL("actor:update",[ARR_2(_uid,_json)]); + + if (_sync) then { + private _player = [_uid] call EFUNC(common,getPlayer); + [CRPC(actor,responseSyncActor), [_finalActor], _player] call CFUNC(targetEvent); + }; + + _finalActor + }], + ["remove", { + params [["_uid", "", [""]]]; + + GVAR(ActorRegistry) deleteAt _uid; + }], + ["toHashMap", { + params [["_data", "", [""]]]; + + private _hashMap = fromJSON _data; + _hashMap + }], + ["toJSON", { + params [["_data", createHashMap, [createHashMap]]]; + + private _json = toJSON _data; + _json + }] +]]; + +SETMVAR(FORGE_ActorStore,GVAR(ActorStore)); +GVAR(ActorStore) diff --git a/arma/server/addons/actor/script_component.hpp b/arma/server/addons/actor/script_component.hpp new file mode 100644 index 0000000..dec8071 --- /dev/null +++ b/arma/server/addons/actor/script_component.hpp @@ -0,0 +1,9 @@ +#define COMPONENT actor +#define COMPONENT_BEAUTIFIED Actor +#include "\forge\forge_server\addons\main\script_mod.hpp" + +// #define DEBUG_MODE_FULL +// #define DISABLE_COMPILE_CACHE +// #define ENABLE_PERFORMANCE_COUNTERS + +#include "\forge\forge_server\addons\main\script_macros.hpp" diff --git a/arma/server/addons/actor/stringtable.xml b/arma/server/addons/actor/stringtable.xml new file mode 100644 index 0000000..95427c0 --- /dev/null +++ b/arma/server/addons/actor/stringtable.xml @@ -0,0 +1,8 @@ + + + + + Actor + + + diff --git a/arma/server/addons/bank/$PBOPREFIX$ b/arma/server/addons/bank/$PBOPREFIX$ new file mode 100644 index 0000000..d4d3bbc --- /dev/null +++ b/arma/server/addons/bank/$PBOPREFIX$ @@ -0,0 +1 @@ +forge\forge_server\addons\bank diff --git a/arma/server/addons/bank/CfgEventHandlers.hpp b/arma/server/addons/bank/CfgEventHandlers.hpp new file mode 100644 index 0000000..f6503c2 --- /dev/null +++ b/arma/server/addons/bank/CfgEventHandlers.hpp @@ -0,0 +1,17 @@ +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)); + }; +}; + +class Extended_PostInit_EventHandlers { + class ADDON { + init = QUOTE(call COMPILE_SCRIPT(XEH_postInit)); + }; +}; diff --git a/arma/server/addons/bank/README.md b/arma/server/addons/bank/README.md new file mode 100644 index 0000000..3617706 --- /dev/null +++ b/arma/server/addons/bank/README.md @@ -0,0 +1,4 @@ +forge_server_bank +=================== + +Description for this addon diff --git a/arma/server/addons/bank/XEH_PREP.hpp b/arma/server/addons/bank/XEH_PREP.hpp new file mode 100644 index 0000000..5dbb105 --- /dev/null +++ b/arma/server/addons/bank/XEH_PREP.hpp @@ -0,0 +1 @@ +PREP(initBankStore); diff --git a/arma/server/addons/bank/XEH_postInit.sqf b/arma/server/addons/bank/XEH_postInit.sqf new file mode 100644 index 0000000..421c54b --- /dev/null +++ b/arma/server/addons/bank/XEH_postInit.sqf @@ -0,0 +1 @@ +#include "script_component.hpp" diff --git a/arma/server/addons/bank/XEH_preInit.sqf b/arma/server/addons/bank/XEH_preInit.sqf new file mode 100644 index 0000000..276041b --- /dev/null +++ b/arma/server/addons/bank/XEH_preInit.sqf @@ -0,0 +1,85 @@ +#include "script_component.hpp" + +PREP_RECOMPILE_START; +#include "XEH_PREP.hpp" +PREP_RECOMPILE_END; + +// private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)]; + +if (isNil QGVAR(BankStore)) then { [] call FUNC(initBankStore); }; + +[QGVAR(requestInitBank), { + params [["_uid", "", [""]], ["_bank", createHashMap, [createHashMap]]]; + + if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID!" }; + if (_bank isEqualTo createHashMap) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid Bank data!" }; + + GVAR(BankStore) call ["init", [_uid, _bank]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestGetBank), { + params [["_uid", "", [""]], ["_sync", false, [false]]]; + + if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID!" }; + + private _session = EGVAR(actor,PlayerSessions) getOrDefault [_uid, nil]; + if (isNil "_session") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid Session!" }; + + GVAR(BankStore) call ["get", [_uid, _sync]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestSetBank), { + params [["_uid", "", [""]], ["_key", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]]; + + if (_uid isEqualTo "" || _key isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID or Key!" }; + + private _session = EGVAR(actor,PlayerSessions) getOrDefault [_uid, nil]; + if (isNil "_session") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid Session!" }; + + GVAR(BankStore) call ["set", [_uid, _key, _value, _sync]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestMSetBank), { + params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; + + if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID!" }; + if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid field pairs!" }; + + private _session = EGVAR(actor,PlayerSessions) getOrDefault [_uid, nil]; + if (isNil "_session") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid Session!" }; + + GVAR(BankStore) call ["mset", [_uid, _fieldValuePairs, _sync]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestSaveBank), { + params [["_uid", "", [""]], ["_sync", false, [false]]]; + + if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID!" }; + + private _session = EGVAR(actor,PlayerSessions) getOrDefault [_uid, nil]; + if (isNil "_session") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid Session!" }; + + GVAR(BankStore) call ["save", [_uid, _sync]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestRemoveBank), { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID!" }; + + GVAR(BankStore) call ["remove", [_uid]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestResync), { + params ["_player"]; + + private _uid = getPlayerUID _player; + private _bank = GVAR(BankStore) call ["get", [_uid, true]]; + + private _session = EGVAR(actor,PlayerSessions) getOrDefault [_uid, nil]; + if (isNil "_session") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid Session!" }; + + [CRPC(bank,responseSyncBank), [_bank, true], _player] call CFUNC(targetEvent); + + diag_log format ["[FORGE:Server:Bank] Resync completed for %1", _uid]; +}] call CFUNC(addEventHandler); diff --git a/arma/server/addons/bank/XEH_preStart.sqf b/arma/server/addons/bank/XEH_preStart.sqf new file mode 100644 index 0000000..0228885 --- /dev/null +++ b/arma/server/addons/bank/XEH_preStart.sqf @@ -0,0 +1,3 @@ +#include "script_component.hpp" + +#include "XEH_PREP.hpp" diff --git a/arma/server/addons/bank/config.cpp b/arma/server/addons/bank/config.cpp new file mode 100644 index 0000000..05c825e --- /dev/null +++ b/arma/server/addons/bank/config.cpp @@ -0,0 +1,19 @@ +#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_server_main" + }; + units[] = {}; + weapons[] = {}; + VERSION_CONFIG; + }; +}; + +#include "CfgEventHandlers.hpp" diff --git a/arma/server/addons/bank/functions/fnc_initBankStore.sqf b/arma/server/addons/bank/functions/fnc_initBankStore.sqf new file mode 100644 index 0000000..0abbc71 --- /dev/null +++ b/arma/server/addons/bank/functions/fnc_initBankStore.sqf @@ -0,0 +1,170 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Initializes the bank store. + * + * Arguments: + * None + * + * Return Value: + * None + * + * Examples: + * [] call forge_server_bank_fnc_initBankStore + * + * Public: Yes + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(BankStore) = createHashMapObject [[ + ["#type", "IBankStore"], + ["#create", { + GVAR(BankRegistry) = createHashMap; + diag_log "[FORGE:Server:Bank] Bank Store Initialized!"; + }], + ["init", { + params [["_uid", "", [""]], ["_defaultAccount", createHashMap, [createHashMap]]]; + + private _finalAccount = createHashMap; + + EXTCALL("bank:exists",[ARR_1(_uid)]); + + private _exists = (_ext_res select 0) == "true"; + + if !(_exists) then { + _finalAccount = _defaultAccount; + _finalAccount set ["uid", _uid]; + + private _json = _self call ["toJSON", [_finalAccount]]; + EXTCALL("bank:create",[ARR_2(_uid,_json)]); + + diag_log format ["[FORGE:Server:Bank] Created new bank account for %1", _uid]; + } else { + private _existingAccount = _self call ["fetch", [_uid]]; + _finalAccount = _existingAccount; + + { + if !(_x in _finalAccount) then { _finalAccount set [_x, _y]; }; + } forEach _defaultAccount; + + diag_log format ["[FORGE:Server:Bank] Found bank account for %1", _uid]; + }; + + GVAR(BankRegistry) set [_uid, _finalAccount, true]; + + private _player = [_uid] call EFUNC(common,getPlayer); + [CRPC(bank,responseInitBank), [_finalAccount], _player] call CFUNC(targetEvent); + + _finalAccount + }], + ["fetch", { + params [["_uid", "", [""]]]; + + private _account = createHashMap; + + EXTCALL("bank:get",[ARR_1(_uid)]); + diag_log format ["[FORGE:Server:Bank] Data: %1", _ext_res]; + + private _ext_res_account = _ext_res select 0; + if (count _ext_res_account > 0) then { _account = _self call ["toHashMap", [_ext_res_account]]; }; + + _account + }], + ["get", { + params [["_uid", "", [""]], ["_sync", false, [false]]]; + + private _finalAccount = createHashMap; + + if (_sync) then { + private _existingAccount = _self call ["fetch", [_uid]]; + _finalAccount = _existingAccount; + + GVAR(BankRegistry) set [_uid, _finalAccount]; + } else { + _finalAccount = GVAR(BankRegistry) get _uid; + }; + + _finalAccount + }], + ["set", { + params [["_uid", "", [""]], ["_field", "", [""]], ["_value", nil], ["_sync", false, [false]]]; + + private _existingAccount = GVAR(BankRegistry) get _uid; + private _finalAccount = +_existingAccount; + private _hashMap = createHashMap; + + _finalAccount set [_field, _value]; + _hashMap set [_field, _value]; + + GVAR(BankRegistry) set [_uid, _finalAccount]; + + if (_sync) then { + private _json = _self call ["toJSON", [_hashMap]]; + EXTCALL("bank:update",[ARR_2(_uid,_json)]); + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + [CRPC(bank,responseSyncBank), [_hashMap], _player] call CFUNC(targetEvent); + + _hashMap + }], + ["mset", { + params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; + + private _existingAccount = GVAR(BankRegistry) get _uid; + private _finalAccount = +_existingAccount; + private _hashMap = createHashMap; + + { _finalAccount set [_x, _y]; } forEach _fieldValuePairs; + { _hashMap set [_x, _y]; } forEach _fieldValuePairs; + + GVAR(BankRegistry) set [_uid, _finalAccount]; + + if (_sync) then { + private _json = _self call ["toJSON", [_hashMap]]; + EXTCALL("bank:update",[ARR_2(_uid,_json)]); + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + [CRPC(bank,responseSyncBank), [_hashMap], _player] call CFUNC(targetEvent); + + _hashMap + }], + ["save", { + params [["_uid", "", [""]], ["_sync", false, [false]]]; + + private _existingAccount = GVAR(BankRegistry) get _uid; + private _finalAccount = +_existingAccount; + private _json = _self call ["toJSON", [_finalAccount]]; + + EXTCALL("bank:update",[ARR_2(_uid,_json)]); + + if (_sync) then { + private _player = [_uid] call EFUNC(common,getPlayer); + [CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent); + }; + + _finalAccount + }], + ["remove", { + params [["_uid", "", [""]]]; + + GVAR(BankRegistry) deleteAt _uid; + }], + ["toHashMap", { + params [["_data", "", [""]]]; + + private _hashMap = fromJSON _data; + _hashMap + }], + ["toJSON", { + params [["_data", createHashMap, [createHashMap]]]; + + private _json = toJSON _data; + _json + }] +]]; + +SETMVAR(FORGE_BankStore,GVAR(BankStore)); +GVAR(BankStore) diff --git a/arma/server/addons/bank/script_component.hpp b/arma/server/addons/bank/script_component.hpp new file mode 100644 index 0000000..7b0884d --- /dev/null +++ b/arma/server/addons/bank/script_component.hpp @@ -0,0 +1,9 @@ +#define COMPONENT bank +#define COMPONENT_BEAUTIFIED Bank +#include "\forge\forge_server\addons\main\script_mod.hpp" + +// #define DEBUG_MODE_FULL +// #define DISABLE_COMPILE_CACHE +// #define ENABLE_PERFORMANCE_COUNTERS + +#include "\forge\forge_server\addons\main\script_macros.hpp" diff --git a/arma/server/addons/bank/stringtable.xml b/arma/server/addons/bank/stringtable.xml new file mode 100644 index 0000000..0cd5b23 --- /dev/null +++ b/arma/server/addons/bank/stringtable.xml @@ -0,0 +1,8 @@ + + + + + Bank + + + diff --git a/arma/server/addons/common/$PBOPREFIX$ b/arma/server/addons/common/$PBOPREFIX$ new file mode 100644 index 0000000..dd94028 --- /dev/null +++ b/arma/server/addons/common/$PBOPREFIX$ @@ -0,0 +1 @@ +forge\forge_server\addons\common diff --git a/arma/server/addons/common/CfgEventHandlers.hpp b/arma/server/addons/common/CfgEventHandlers.hpp new file mode 100644 index 0000000..865276c --- /dev/null +++ b/arma/server/addons/common/CfgEventHandlers.hpp @@ -0,0 +1,11 @@ +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)); + }; +}; diff --git a/arma/server/addons/common/README.md b/arma/server/addons/common/README.md new file mode 100644 index 0000000..f6ac3d5 --- /dev/null +++ b/arma/server/addons/common/README.md @@ -0,0 +1,4 @@ +forge_server_common +=================== + +Common functionality shared between addons. diff --git a/arma/server/addons/common/XEH_PREP.hpp b/arma/server/addons/common/XEH_PREP.hpp new file mode 100644 index 0000000..b7ecf76 --- /dev/null +++ b/arma/server/addons/common/XEH_PREP.hpp @@ -0,0 +1,4 @@ +PREP(getPlayer); +PREP(generateHash); +PREP(generateSecureData); +PREP(timeToSeconds); diff --git a/arma/server/addons/common/XEH_preInit.sqf b/arma/server/addons/common/XEH_preInit.sqf new file mode 100644 index 0000000..9dd4ebf --- /dev/null +++ b/arma/server/addons/common/XEH_preInit.sqf @@ -0,0 +1,7 @@ +#include "script_component.hpp" + +PREP_RECOMPILE_START; +#include "XEH_PREP.hpp" +PREP_RECOMPILE_END; + +// private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)]; diff --git a/arma/server/addons/common/XEH_preStart.sqf b/arma/server/addons/common/XEH_preStart.sqf new file mode 100644 index 0000000..0228885 --- /dev/null +++ b/arma/server/addons/common/XEH_preStart.sqf @@ -0,0 +1,3 @@ +#include "script_component.hpp" + +#include "XEH_PREP.hpp" diff --git a/arma/server/addons/common/config.cpp b/arma/server/addons/common/config.cpp new file mode 100644 index 0000000..4023b3c --- /dev/null +++ b/arma/server/addons/common/config.cpp @@ -0,0 +1,19 @@ +#include "script_component.hpp" + +class CfgPatches { + class ADDON { + author = AUTHOR; + authors[] = {"IDSolutions"}; + url = ECSTRING(main,url); + name = COMPONENT_NAME; + requiredVersion = REQUIRED_VERSION; + requiredAddons[] = { + "forge_server_main" + }; + units[] = {}; + weapons[] = {}; + VERSION_CONFIG; + }; +}; + +#include "CfgEventHandlers.hpp" diff --git a/arma/server/addons/common/functions/fnc_generateHash.sqf b/arma/server/addons/common/functions/fnc_generateHash.sqf new file mode 100644 index 0000000..9dcdb99 --- /dev/null +++ b/arma/server/addons/common/functions/fnc_generateHash.sqf @@ -0,0 +1,33 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Generates a 6-digit hash from input string using DJB2 algorithm. + * + * Arguments: + * 0: Input string to hash + * + * Return Value: + * 6-digit hash string + * + * Example: + * ["test_input"] call forge_server_common_fnc_generateHash + * // Returns: "461324" + * + * Public: Yes + */ + +params [["_input", "", [""]]]; + +private _hash = 5381; +private _chars = toArray _input; + +{ + _hash = ((_hash * 33) + _x) mod 999999; +} forEach _chars; + +private _result = str _hash; + +while { count _result < 6 } do { _result = "0" + _result; }; + +_result diff --git a/arma/server/addons/common/functions/fnc_generateSecureData.sqf b/arma/server/addons/common/functions/fnc_generateSecureData.sqf new file mode 100644 index 0000000..486b992 --- /dev/null +++ b/arma/server/addons/common/functions/fnc_generateSecureData.sqf @@ -0,0 +1,36 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Generates a secure data object with timestamp, signature, and token. + * + * Arguments: + * 0: Player UID + * 1: Data to secure + * + * Return Value: + * Secure data object + * + * Example: + * ["test_uid", createHashMap] call forge_server_common_fnc_generateSecureData + * // Returns: ["data", createHashMap], ["timestamp", ], ["signature", ], ["token", ] + * + * Public: Yes + */ + +params [["_uid", "", [""]], ["_data", createHashMap, [createHashMap]]]; + +private _timestamp = systemTime; +private _sessionToken = EGVAR(actor,PlayerSessions) getOrDefault [_uid, ""]; +private _sigInput = format ["%1|%2|%3|%4", _uid, str _data, _timestamp, _sessionToken]; +private _signature = _sigInput call EFUNC(common,generateHash); + +private _secureData = createHashMap; +_secureData set ["data", _data]; +_secureData set ["timestamp", _timestamp]; +_secureData set ["signature", _signature]; +_secureData set ["token", _sessionToken]; + +diag_log format ["[SECURITY] Generated secure data for %1: sig=%2", _uid, _signature]; + +_secureData diff --git a/arma/server/addons/common/functions/fnc_getPlayer.sqf b/arma/server/addons/common/functions/fnc_getPlayer.sqf new file mode 100644 index 0000000..d86f7f6 --- /dev/null +++ b/arma/server/addons/common/functions/fnc_getPlayer.sqf @@ -0,0 +1,27 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Gets a player object by UID. + * + * Arguments: + * 0: Player UID + * + * Return Value: + * Player object or objNull if not found + * + * Example: + * ["0123456789"] call forge_server_common_fnc_getPlayer + * + * Public: Yes + */ + +params ["_uid"]; + +private _player = objNull; + +{ + if ((getPlayerUID _x) isEqualTo _uid) exitWith { _player = _x; }; +} forEach allPlayers; + +_player diff --git a/arma/server/addons/common/functions/fnc_timeToSeconds.sqf b/arma/server/addons/common/functions/fnc_timeToSeconds.sqf new file mode 100644 index 0000000..9674445 --- /dev/null +++ b/arma/server/addons/common/functions/fnc_timeToSeconds.sqf @@ -0,0 +1,36 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Converts systemTime array to total seconds since midnight. + * + * Arguments: + * 0: System time array from systemTime command + * + * Return Value: + * Total seconds since midnight + * + * Example: + * [systemTime] call forge_server_common_fnc_timeToSeconds + * // Returns: 43200 (for 12:00:00) + * + * Public: Yes + */ + +params [["_systemTime", [], [[]]]]; + +if (typeName _systemTime != "ARRAY") exitWith { + diag_log format ["[ERROR] timeToSeconds received %1 instead of ARRAY: %2", typeName _systemTime, _systemTime]; + 0 +}; + +if (count _systemTime < 6) exitWith { + diag_log format ["[ERROR] timeToSeconds received array with %1 elements, need at least 6: %2", count _systemTime, _systemTime]; + 0 +}; + +private _hours = _systemTime select 3; +private _minutes = _systemTime select 4; +private _seconds = _systemTime select 5; + +(_hours * 3600) + (_minutes * 60) + _seconds diff --git a/arma/server/addons/common/script_component.hpp b/arma/server/addons/common/script_component.hpp new file mode 100644 index 0000000..26eec82 --- /dev/null +++ b/arma/server/addons/common/script_component.hpp @@ -0,0 +1,9 @@ +#define COMPONENT common +#define COMPONENT_BEAUTIFIED Common +#include "\forge\forge_server\addons\main\script_mod.hpp" + +// #define DEBUG_MODE_FULL +// #define DISABLE_COMPILE_CACHE +// #define ENABLE_PERFORMANCE_COUNTERS + +#include "\forge\forge_server\addons\main\script_macros.hpp" diff --git a/arma/server/addons/common/stringtable.xml b/arma/server/addons/common/stringtable.xml new file mode 100644 index 0000000..936e702 --- /dev/null +++ b/arma/server/addons/common/stringtable.xml @@ -0,0 +1,8 @@ + + + + + Common + + + diff --git a/arma/server/addons/main/$PBOPREFIX$ b/arma/server/addons/main/$PBOPREFIX$ new file mode 100644 index 0000000..9e6049b --- /dev/null +++ b/arma/server/addons/main/$PBOPREFIX$ @@ -0,0 +1 @@ +forge\forge_server\addons\main diff --git a/arma/server/addons/main/CfgSettings.hpp b/arma/server/addons/main/CfgSettings.hpp new file mode 100644 index 0000000..05d0b0b --- /dev/null +++ b/arma/server/addons/main/CfgSettings.hpp @@ -0,0 +1,13 @@ +class CfgSettings { + class CBA { + class Versioning { + class PREFIX { + main_addon = QUOTE(ADDON); + + class dependencies { + CBA[] = {"cba_main", REQUIRED_CBA_VERSION, "true"}; + }; + }; + }; + }; +}; diff --git a/arma/server/addons/main/README.md b/arma/server/addons/main/README.md new file mode 100644 index 0000000..4de302b --- /dev/null +++ b/arma/server/addons/main/README.md @@ -0,0 +1,4 @@ +forge_server_main +=================== + +Main Addon for forge-server diff --git a/arma/server/addons/main/config.cpp b/arma/server/addons/main/config.cpp new file mode 100644 index 0000000..2b42b1e --- /dev/null +++ b/arma/server/addons/main/config.cpp @@ -0,0 +1,19 @@ +#include "script_component.hpp" + +class CfgPatches { + class ADDON { + author = AUTHOR; + authors[] = {"J.Schmidt"}; + url = CSTRING(url); + name = COMPONENT_NAME; + requiredVersion = REQUIRED_VERSION; + requiredAddons[] = { + "cba_main" + }; + units[] = {}; + weapons[] = {}; + VERSION_CONFIG; + }; +}; + +#include "CfgSettings.hpp" diff --git a/arma/server/addons/main/script_component.hpp b/arma/server/addons/main/script_component.hpp new file mode 100644 index 0000000..32d193a --- /dev/null +++ b/arma/server/addons/main/script_component.hpp @@ -0,0 +1,9 @@ +#define COMPONENT main +#define COMPONENT_BEAUTIFIED Main +#include "script_mod.hpp" + +// #define DEBUG_MODE_FULL +// #define DISABLE_COMPILE_CACHE +// #define ENABLE_PERFORMANCE_COUNTERS + +#include "script_macros.hpp" diff --git a/arma/server/addons/main/script_macros.hpp b/arma/server/addons/main/script_macros.hpp new file mode 100644 index 0000000..e64fe52 --- /dev/null +++ b/arma/server/addons/main/script_macros.hpp @@ -0,0 +1,159 @@ +// Global toggles for caching/logging +// #define DISABLE_COMPILE_CACHE +// #define DEBUG_MODE_FULL +#define DEBUG_SYNCHRONOUS + +#include "\x\cba\addons\main\script_macros_common.hpp" +#include "\x\cba\addons\xeh\script_xeh.hpp" + +// Functions +#define AFUNC(var1,var2) TRIPLES(DOUBLES(ace,var1),fnc,var2) +#define BFUNC(var1) TRIPLES(BIS,fnc,var1) +#define CFUNC(var1) TRIPLES(CBA,fnc,var1) + +// Remote Procedure Calls +#define CRPC(var1,var2) QUOTE(DOUBLES(DOUBLES(forge_client,var1),var2)) +#define SRPC(var1,var2) QUOTE(DOUBLES(DOUBLES(forge_server,var1),var2)) + +#define QQUOTE(var1) QUOTE(QUOTE(var1)) + +// QPATHTOF but without a leading slash +#define PATHTOF2(var1) MAINPREFIX\PREFIX\SUBPREFIX\COMPONENT\var1 +#define QPATHTOF2(var1) QUOTE(PATHTOF2(var1)) + +#ifdef SUBCOMPONENT + #define SUBADDON DOUBLES(ADDON,SUBCOMPONENT) + + // Update PATHTO macros to point to subaddon instead + #undef PATHTO + #define PATHTO(var1) \MAINPREFIX\PREFIX\SUBPREFIX\COMPONENT\SUBCOMPONENT\var1.sqf + #undef PATHTOF + #define PATHTOF(var1) \MAINPREFIX\PREFIX\SUBPREFIX\COMPONENT\SUBCOMPONENT\var1 + #undef PATHTO2 + #define PATHTO2(var1) MAINPREFIX\PREFIX\SUBPREFIX\COMPONENT\SUBCOMPONENT\var1.sqf + #undef PATHTOF2 + #define PATHTOF2(var1) MAINPREFIX\PREFIX\SUBPREFIX\COMPONENT\SUBCOMPONENT\var1 +#endif + +#undef PREP +#ifdef DISABLE_COMPILE_CACHE + #define LINKFUNC(x) {call FUNC(x)} + #define PREP(fncName) FUNC(fncName) = compile preprocessFileLineNumbers QPATHTOF(functions\DOUBLES(fnc,fncName).sqf) + #define PREP_RECOMPILE_START if (isNil "forge_server_fnc_recompile") then {forge_server_recompiles = []; forge_server_fnc_recompile = {{call _x} forEach forge_server_recompiles;}}; private _recomp = { + #define PREP_RECOMPILE_END }; call _recomp; forge_server_recompiles pushBack _recomp; +#else + #define LINKFUNC(x) FUNC(x) + #define PREP(fncName) [QPATHTOF(functions\DOUBLES(fnc,fncName).sqf), QFUNC(fncName)] call CBA_fnc_compileFunction + #define PREP_RECOMPILE_START ; /* disabled */ + #define PREP_RECOMPILE_END ; /* disabled */ +#endif + +#define GETVAR_SYS(var1,var2) getVariable [ARR_2(QUOTE(var1),var2)] +#define SETVAR_SYS(var1,var2) setVariable [ARR_2(QUOTE(var1),var2)] +#define SETPVAR_SYS(var1,var2) setVariable [ARR_3(QUOTE(var1),var2,true)] + +#undef GETVAR +#define GETVAR(var1,var2,var3) (var1 GETVAR_SYS(var2,var3)) +#define GETMVAR(var1,var2) (missionNamespace GETVAR_SYS(var1,var2)) +#define GETUVAR(var1,var2) (uiNamespace GETVAR_SYS(var1,var2)) +#define GETPRVAR(var1,var2) (profileNamespace GETVAR_SYS(var1,var2)) +#define GETPAVAR(var1,var2) (parsingNamespace GETVAR_SYS(var1,var2)) + +#undef SETVAR +#define SETVAR(var1,var2,var3) var1 SETVAR_SYS(var2,var3) +#define SETPVAR(var1,var2,var3) var1 SETPVAR_SYS(var2,var3) +#define SETMVAR(var1,var2) missionNamespace SETVAR_SYS(var1,var2) +#define SETUVAR(var1,var2) uiNamespace SETVAR_SYS(var1,var2) +#define SETPRVAR(var1,var2) profileNamespace SETVAR_SYS(var1,var2) +#define SETPAVAR(var1,var2) parsingNamespace SETVAR_SYS(var1,var2) + +#define GETGVAR(var1,var2) GETMVAR(GVAR(var1),var2) +#define GETEGVAR(var1,var2,var3) GETMVAR(EGVAR(var1,var2),var3) + +// Extension +#define EXT "forge_server" + +#define EXTCALL(function,args) private _ext_res = EXT callExtension [function, args]; \ +if ((_ext_res select 1) != 0) then { \ + ERROR_2("Error calling %1: %2",function,(_ext_res select 1)); \ + ERROR_1("ARGS: %1",args); \ +} + +#define EXTFUNC(function) EXTCALL(function,[]) + +#define WEAP_XX(WEAP, COUNT) class DOUBLES(_xx,WEAP) { \ + weapon = QUOTE(WEAP); \ + count = COUNT; \ +} + +#define MAG_XX(MAG, COUNT) class DOUBLES(_xx,MAG) { \ + magazine = QUOTE(MAG); \ + count = COUNT; \ +} + +#define ITEM_XX(ITEM, COUNT) class DOUBLES(_xx,ITEM) { \ + name = QUOTE(ITEM); \ + count = COUNT; \ +} + +// ACE Cargo +#define CARGO_XX(ITEM, COUNT) class ITEM { \ + type = QUOTE(ITEM); \ + amount = COUNT; \ +} + +#define MAG_CSW(var1,var2) class DOUBLES(var1,csw): var1 { \ + scope = var2; \ + type = TYPE_MAGAZINE_PRIMARY_AND_THROW; \ +} + +// Debug textures, mainly for testing hiddenSelections +#define DBUG_TEX_RED "#(rgb,8,8,3)color(1,0,0,1)" +#define DBUG_TEX_GRN "#(rgb,8,8,3)color(0,1,0,1)" +#define DBUG_TEX_BLU "#(rgb,8,8,3)color(0,0,1,1)" +#define DBUG_TEX_PUR "#(rgb,8,8,3)color(1,0,1,1)" +#define DBUG_TEX_YEL "#(rgb,8,8,3)color(1,1,0,1)" + +// Statements and conditions +#define CLAMP(var1,lower,upper) (lower max (var1 min upper)) + +// Weapon types +#define TYPE_WEAPON_PRIMARY 1 +#define TYPE_WEAPON_HANDGUN 2 +#define TYPE_WEAPON_SECONDARY 4 +// Magazine types +#define TYPE_MAGAZINE_HANDGUN_AND_GL 16 // mainly +#define TYPE_MAGAZINE_PRIMARY_AND_THROW 256 +#define TYPE_MAGAZINE_SECONDARY_AND_PUT 512 // mainly +#define TYPE_MAGAZINE_MISSILE 768 +// More types +#define TYPE_BINOCULAR_AND_NVG 4096 +#define TYPE_WEAPON_VEHICLE 65536 +#define TYPE_ITEM 131072 +// Item types +#define TYPE_DEFAULT 0 +#define TYPE_MUZZLE 101 +#define TYPE_OPTICS 201 +#define TYPE_FLASHLIGHT 301 +#define TYPE_BIPOD 302 +#define TYPE_FIRST_AID_KIT 401 +#define TYPE_FINS 501 // not implemented +#define TYPE_BREATHING_BOMB 601 // not implemented +#define TYPE_NVG 602 +#define TYPE_GOGGLE 603 +#define TYPE_SCUBA 604 // not implemented +#define TYPE_HEADGEAR 605 +#define TYPE_FACTOR 607 +#define TYPE_MAP 608 +#define TYPE_COMPASS 609 +#define TYPE_WATCH 610 +#define TYPE_RADIO 611 +#define TYPE_GPS 612 +#define TYPE_HMD 616 +#define TYPE_BINOCULAR 617 +#define TYPE_MEDIKIT 619 +#define TYPE_TOOLKIT 620 +#define TYPE_UAV_TERMINAL 621 +#define TYPE_VEST 701 +#define TYPE_UNIFORM 801 +#define TYPE_BACKPACK 901 diff --git a/arma/server/addons/main/script_mod.hpp b/arma/server/addons/main/script_mod.hpp new file mode 100644 index 0000000..475c6bf --- /dev/null +++ b/arma/server/addons/main/script_mod.hpp @@ -0,0 +1,26 @@ +#define MAINPREFIX forge +#define PREFIX forge_server +#define MOD_NAME forge-server +#define AUTHOR "J.Schmidt" + +#define REQUIRED_VERSION 2.20 +#define REQUIRED_CBA_VERSION {3,18,4} +#define REQUIRED_ACE_VERSION {3,20,0} + +#include "script_version.hpp" + +#define VERSION MAJOR.MINOR +#define VERSION_STR MAJOR.MINOR.PATCH.BUILD +#define VERSION_AR MAJOR,MINOR,PATCH,BUILD + +#ifndef COMPONENT_BEAUTIFIED + #define COMPONENT_BEAUTIFIED COMPONENT +#endif +#ifdef SUBCOMPONENT + #ifndef SUBCOMPONENT_BEAUTIFIED + #define SUBCOMPONENT_BEAUTIFIED SUBCOMPONENT + #endif + #define COMPONENT_NAME QUOTE(MOD_NAME - COMPONENT_BEAUTIFIED (SUBCOMPONENT_BEAUTIFIED)) +#else + #define COMPONENT_NAME QUOTE(MOD_NAME - COMPONENT_BEAUTIFIED) +#endif diff --git a/arma/server/addons/main/script_version.hpp b/arma/server/addons/main/script_version.hpp new file mode 100644 index 0000000..3e6aaab --- /dev/null +++ b/arma/server/addons/main/script_version.hpp @@ -0,0 +1,4 @@ +#define MAJOR 1 +#define MINOR 0 +#define PATCH 0 +#define BUILD 0 diff --git a/arma/server/addons/main/stringtable.xml b/arma/server/addons/main/stringtable.xml new file mode 100644 index 0000000..c40749d --- /dev/null +++ b/arma/server/addons/main/stringtable.xml @@ -0,0 +1,24 @@ + + + + + Main + + + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + https://github.com/IDSolutions/MOD_REPO + + + diff --git a/arma/server/addons/org/$PBOPREFIX$ b/arma/server/addons/org/$PBOPREFIX$ new file mode 100644 index 0000000..75b39af --- /dev/null +++ b/arma/server/addons/org/$PBOPREFIX$ @@ -0,0 +1 @@ +forge\forge_server\addons\org diff --git a/arma/server/addons/org/CfgEventHandlers.hpp b/arma/server/addons/org/CfgEventHandlers.hpp new file mode 100644 index 0000000..f6503c2 --- /dev/null +++ b/arma/server/addons/org/CfgEventHandlers.hpp @@ -0,0 +1,17 @@ +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)); + }; +}; + +class Extended_PostInit_EventHandlers { + class ADDON { + init = QUOTE(call COMPILE_SCRIPT(XEH_postInit)); + }; +}; diff --git a/arma/server/addons/org/README.md b/arma/server/addons/org/README.md new file mode 100644 index 0000000..d498671 --- /dev/null +++ b/arma/server/addons/org/README.md @@ -0,0 +1,4 @@ +forge_server_org +=================== + +Description for this addon diff --git a/arma/server/addons/org/XEH_PREP.hpp b/arma/server/addons/org/XEH_PREP.hpp new file mode 100644 index 0000000..c7b9330 --- /dev/null +++ b/arma/server/addons/org/XEH_PREP.hpp @@ -0,0 +1 @@ +PREP(initOrgStore); diff --git a/arma/server/addons/org/XEH_postInit.sqf b/arma/server/addons/org/XEH_postInit.sqf new file mode 100644 index 0000000..421c54b --- /dev/null +++ b/arma/server/addons/org/XEH_postInit.sqf @@ -0,0 +1 @@ +#include "script_component.hpp" diff --git a/arma/server/addons/org/XEH_preInit.sqf b/arma/server/addons/org/XEH_preInit.sqf new file mode 100644 index 0000000..f1e7190 --- /dev/null +++ b/arma/server/addons/org/XEH_preInit.sqf @@ -0,0 +1,85 @@ +#include "script_component.hpp" + +PREP_RECOMPILE_START; +#include "XEH_PREP.hpp" +PREP_RECOMPILE_END; + +// private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)]; + +if (isNil QGVAR(OrgStore)) then { [] call FUNC(initOrgStore); }; + +[QGVAR(requestInitOrg), { + params [["_uid", "", [""]], ["_org", createHashMap, [createHashMap]]]; + + if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" }; + if (_org isEqualTo createHashMap) exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid Org data!" }; + + GVAR(OrgStore) call ["init", [_uid, _org]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestGetOrg), { + params [["_uid", "", [""]], ["_sync", false, [false]]]; + + if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" }; + + private _session = EGVAR(actor,PlayerSessions) getOrDefault [_uid, nil]; + if (isNil "_session") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid Session!" }; + + GVAR(OrgStore) call ["get", [_uid, _sync]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestSetOrg), { + params [["_uid", "", [""]], ["_key", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]]; + + if (_uid isEqualTo "" || _key isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID or Key!" }; + + private _session = EGVAR(actor,PlayerSessions) getOrDefault [_uid, nil]; + if (isNil "_session") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid Session!" }; + + GVAR(OrgStore) call ["set", [_uid, _key, _value, _sync]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestMSetOrg), { + params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; + + if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" }; + if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid field pairs!" }; + + private _session = EGVAR(actor,PlayerSessions) getOrDefault [_uid, nil]; + if (isNil "_session") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid Session!" }; + + GVAR(OrgStore) call ["mset", [_uid, _fieldValuePairs, _sync]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestSaveOrg), { + params [["_uid", "", [""]], ["_sync", false, [false]]]; + + if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" }; + + private _session = EGVAR(actor,PlayerSessions) getOrDefault [_uid, nil]; + if (isNil "_session") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid Session!" }; + + GVAR(OrgStore) call ["save", [_uid, _sync]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestRemoveOrg), { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" }; + + GVAR(OrgStore) call ["remove", [_uid]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestResync), { + params ["_player"]; + + private _uid = getPlayerUID _player; + private _org = GVAR(OrgStore) call ["get", [_uid, true]]; + + private _session = EGVAR(actor,PlayerSessions) getOrDefault [_uid, nil]; + if (isNil "_session") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid Session!" }; + + [CRPC(org,responseSyncOrg), [_org, true], _player] call CFUNC(targetEvent); + + diag_log format ["[FORGE:Server:Org] Resync completed for %1", _uid]; +}] call CFUNC(addEventHandler); diff --git a/arma/server/addons/org/XEH_preStart.sqf b/arma/server/addons/org/XEH_preStart.sqf new file mode 100644 index 0000000..0228885 --- /dev/null +++ b/arma/server/addons/org/XEH_preStart.sqf @@ -0,0 +1,3 @@ +#include "script_component.hpp" + +#include "XEH_PREP.hpp" diff --git a/arma/server/addons/org/config.cpp b/arma/server/addons/org/config.cpp new file mode 100644 index 0000000..05c825e --- /dev/null +++ b/arma/server/addons/org/config.cpp @@ -0,0 +1,19 @@ +#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_server_main" + }; + units[] = {}; + weapons[] = {}; + VERSION_CONFIG; + }; +}; + +#include "CfgEventHandlers.hpp" diff --git a/arma/server/addons/org/functions/fnc_initOrgStore.sqf b/arma/server/addons/org/functions/fnc_initOrgStore.sqf new file mode 100644 index 0000000..c65e8f3 --- /dev/null +++ b/arma/server/addons/org/functions/fnc_initOrgStore.sqf @@ -0,0 +1,205 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Initializes the org store. + * + * Arguments: + * None + * + * Return Value: + * None + * + * Examples: + * [] call forge_server_org_fnc_initOrgStore + * + * Public: Yes + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(OrgStore) = createHashMapObject [[ + ["#type", "IOrgStore"], + ["#create", { + GVAR(IndexRegistry) = createHashMap; + GVAR(OrgRegistry) = createHashMap; + diag_log "[FORGE:Server:Org] Org Store Initialized!"; + }], + ["init", { + params [["_uid", "", [""]], ["_defaultOrg", createHashMap, [createHashMap]]]; + + private _actor = EGVAR(actor,ActorRegistry) get _uid; + private _orgID = _actor get "organization"; + + EXTCALL("org:exists",[ARR_1(_orgID)]); + + private _exists = (_ext_res select 0) == "true"; + private _player = [_uid] call EFUNC(common,getPlayer); + + if !(_exists) exitWith { + [CRPC(org,responseInitOrg), [createHashMap], _player] call CFUNC(targetEvent); + diag_log format ["[FORGE:Server:Org] No existing org found for %1, using default org.", _uid]; + }; + + GVAR(IndexRegistry) set [_uid, _orgID]; + + private _organization = _self call ["fetch", [_uid]]; + private _members = _self call ["fetchMembers", [_uid]]; + // private _assets = _self call ["fetchAssets", [_uid]]; + + private _finalOrg = GVAR(OrgRegistry) getOrDefault [_orgID, _organization]; + _finalOrg set ["members", _members]; + // _finalOrg set ["assets", _assets]; + + GVAR(OrgRegistry) set [_orgID, _finalOrg, true]; + + [CRPC(org,responseInitOrg), [_finalOrg], _player] call CFUNC(targetEvent); + + _finalOrg + }], + ["fetch", { + params [["_uid", "", [""]]]; + + private _organization = createHashMap; + private _orgID = GVAR(IndexRegistry) get _uid; + + EXTCALL("org:get",[ARR_1(_orgID)]); + diag_log format ["[FORGE:Server:Org] Data: %1", _ext_res]; + + private _ext_res_org = _ext_res select 0; + if (count _ext_res_org > 0) then { _organization = _self call ["toHashMap", [_ext_res_org]]; }; + + _organization + }], + ["fetchAssets", { + params [["_uid", "", [""]]]; + + private _assets = createHashMap; + private _orgID = GVAR(IndexRegistry) get _uid; + + EXTCALL("org:get_assets",[ARR_1(_orgID)]); + diag_log format ["[FORGE:Server:Org] Assets: %1", _ext_res]; + + private _ext_res_assets = _ext_res select 0; + if (count _ext_res_assets > 0) then { _assets = _self call ["toHashMap", [_ext_res_assets]]; }; + + _assets + }], + ["fetchMembers", { + params [["_uid", "", [""]]]; + + private _members = createHashMap; + private _orgID = GVAR(IndexRegistry) get _uid; + + EXTCALL("org:get_members",[ARR_1(_orgID)]); + diag_log format ["[FORGE:Server:Org] Members: %1", _ext_res]; + + private _ext_res_members = _ext_res select 0; + private _raw_members = _self call ["toHashMap", [_ext_res_members]]; + + { + private _uid = _x get "uid"; + _members set [_uid, _x]; + } forEach _raw_members; + + _members + }], + ["get", { + params [["_uid", "", [""]], ["_sync", false, [false]]]; + + private _finalOrg = createHashMap; + private _orgID = GVAR(IndexRegistry) get _uid; + + if (_sync) then { + _finalOrg = _self call ["fetch", [_uid]]; + GVAR(OrgRegistry) set [_orgID, _finalOrg]; + } else { + _finalOrg = GVAR(OrgRegistry) getOrDefault [_orgID, createHashMap]; + }; + + _finalOrg + }], + ["set", { + params [["_uid", "", [""]], ["_field", "", [""]], ["_value", nil], ["_sync", false, [false]]]; + + private _orgID = GVAR(IndexRegistry) get _uid; + private _organization = GVAR(OrgRegistry) get _orgID; + private _finalOrg = +_organization; + private _hashMap = createHashMap; + + _finalOrg set [_field, _value]; + _hashMap set [_field, _value]; + + GVAR(OrgRegistry) set [_orgID, _finalOrg]; + + if (_sync) then { + private _json = _self call ["toJSON", [_hashMap]]; + EXTCALL("org:update",[ARR_2(_orgID,_json)]); + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + [CRPC(org,responseSyncOrg), [_hashMap], _player] call CFUNC(targetEvent); + + _hashMap + }], + ["mset", { + params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; + + private _orgID = GVAR(IndexRegistry) get _uid; + private _organization = GVAR(OrgRegistry) get _orgID; + private _finalOrg = +_organization; + private _hashMap = createHashMap; + + { _finalOrg set [_x, _y]; } forEach _fieldValuePairs; + { _hashMap set [_x, _y]; } forEach _fieldValuePairs; + + GVAR(OrgRegistry) set [_orgID, _finalOrg]; + + if (_sync) then { + private _json = _self call ["toJSON", [_hashMap]]; + EXTCALL("org:update",[ARR_2(_orgID,_json)]); + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + [CRPC(org,responseSyncOrg), [_hashMap], _player] call CFUNC(targetEvent); + + _hashMap + }], + ["save", { + params [["_uid", "", [""]], ["_sync", false, [false]]]; + + private _orgID = GVAR(IndexRegistry) get _uid; + private _organization = GVAR(OrgRegistry) get _orgID; + private _finalOrg = +_organization; + private _json = _self call ["toJSON", [_finalOrg]]; + + EXTCALL("org:update",[ARR_2(_orgID,_json)]); + + if (_sync) then { + private _player = [_uid] call EFUNC(common,getPlayer); + [CRPC(org,responseSyncOrg), [_finalOrg], _player] call CFUNC(targetEvent); + }; + + _finalOrg + }], + ["remove", { + params [["_uid", "", [""]]]; + + private _orgID = GVAR(IndexRegistry) get _uid; + GVAR(OrgRegistry) deleteAt _orgID; + }], + ["toHashMap", { + params [["_data", "", [""]]]; + + private _hashMap = fromJSON _data; + _hashMap + }], + ["toJSON", { + params [["_data", createHashMap, [createHashMap]]]; + + private _json = toJSON _data; + _json + }] +]]; + +SETMVAR(FORGE_OrgStore,GVAR(OrgStore)); +GVAR(OrgStore) diff --git a/arma/server/addons/org/script_component.hpp b/arma/server/addons/org/script_component.hpp new file mode 100644 index 0000000..b51fe86 --- /dev/null +++ b/arma/server/addons/org/script_component.hpp @@ -0,0 +1,9 @@ +#define COMPONENT org +#define COMPONENT_BEAUTIFIED Org +#include "\forge\forge_server\addons\main\script_mod.hpp" + +// #define DEBUG_MODE_FULL +// #define DISABLE_COMPILE_CACHE +// #define ENABLE_PERFORMANCE_COUNTERS + +#include "\forge\forge_server\addons\main\script_macros.hpp" diff --git a/arma/server/addons/org/stringtable.xml b/arma/server/addons/org/stringtable.xml new file mode 100644 index 0000000..4c73262 --- /dev/null +++ b/arma/server/addons/org/stringtable.xml @@ -0,0 +1,8 @@ + + + + + Org + + + diff --git a/arma/server/config.example.toml b/arma/server/config.example.toml new file mode 100644 index 0000000..eddb170 --- /dev/null +++ b/arma/server/config.example.toml @@ -0,0 +1,35 @@ +# Crate Server Configuration +# Copy this file to config.toml and modify as needed +# Place this file in the same directory as your crate_server_x64.dll + +[redis] +# Redis server connection settings +host = "127.0.0.1" +port = 6379 +db = 0 # Redis database number (0-15) + +# Optional authentication +# username = "your_username" +# password = "your_password" + +# Optional connection pool settings +max_connections = 10 # Maximum number of connections in pool +min_connections = 2 # Minimum number of idle connections +idle_timeout = 60 # Idle connection timeout in seconds + +# Example configurations for different environments: + +# Development (local Redis) +# host = "127.0.0.1" +# port = 6379 +# max_connections = 5 +# min_connections = 1 + +# Production (remote Redis with auth) +# host = "redis.example.com" +# port = 6379 +# username = "arma_server" +# password = "secure_password_here" +# max_connections = 20 +# min_connections = 5 +# idle_timeout = 30 diff --git a/arma/server/docs/README.md b/arma/server/docs/README.md new file mode 100644 index 0000000..5aea712 --- /dev/null +++ b/arma/server/docs/README.md @@ -0,0 +1,268 @@ +# Forge Server - Redis Client Module + +A high-performance arma-rs extension for Arma 3, featuring a **low-level Redis data access layer** that provides raw Redis operations as a foundation for higher-level game modules. + +## 🎯 Overview + +The Forge Server Redis module is designed as a **foundational data access layer** that: +- **Returns raw Redis responses** for maximum performance and flexibility +- **Serves as the foundation** for higher-level game modules (actor, garage, locker, bank, etc.) +- **Provides connection pooling** and error handling for Redis operations +- **Enables persistent data storage** across server restarts +- **Supports cross-server data sharing** in multi-server environments + +## 🏗️ Layered Architecture + +``` +SQF Scripts + ↓ (JSON responses) +Game Modules (actor, garage, locker, bank) + ↓ (raw Redis responses) +Redis Client Module + ↓ (Redis protocol) +Redis Server +``` + +**This module handles the bottom layer** - raw Redis operations with connection pooling and error handling. + +## 🏗️ Internal Architecture + +``` +forge_server_x64.dll Extension +├── lib.rs # Core extension initialization & global runtime +├── config.example.toml # Example configuration file +└── redis/ # Redis Client module + ├── mod.rs # Group definitions & module exports + ├── client.rs # Connection pool management + ├── config.rs # Configuration system + ├── macros.rs # redis_operation! macro for boilerplate elimination + ├── common.rs # String/key operations + ├── hash.rs # Hash operations (HSET, HGET, etc.) + ├── list.rs # List operations (LPUSH, LPOP, etc.) + └── set.rs # Set operations (SADD, SMEMBERS, etc.) +``` + +### Key Components +- **lib.rs**: Manages global Redis pool and single Tokio runtime +- **macros.rs**: Provides `redis_operation!` macro to eliminate boilerplate +- **Operation modules**: Focus purely on Redis logic using the macro +- **Synchronous Interface**: All functions appear synchronous to Arma while using async Redis internally + +## 🚀 Features + +### Raw Redis Operations +- **String Operations**: SET, GET, INCR, DECR, DEL, KEYS +- **Hash Operations**: HSET, HGET, HMSET, HGETALL, HDEL, HKEYS, HVALS, HLEN +- **List Operations**: LSET, LGET, LLEN, LRANGE, LPUSH, RPUSH, LPOP, RPOP, LTRIM, LREM +- **Set Operations**: SADD, SMEMBERS, SCARD, SREM, SISMEMBER, SPOP, SRANDMEMBER + +### Performance Features +- **Connection Pooling**: bb8-redis pool with configurable size and timeouts +- **Single Runtime**: One shared Tokio runtime for all async operations +- **Macro-Based**: `redis_operation!` macro eliminates boilerplate while maintaining performance +- **Synchronous Interface**: Functions block until completion, compatible with Arma's threading model +- **Raw Responses**: Returns native Redis values for maximum performance +- **Thread Safety**: Safe concurrent access from multiple Arma threads + +## ⚙️ Configuration System + +Forge Server uses a TOML-based configuration system for flexible Redis connection management. + +### Configuration File + +Create a `config.toml` file in your extension directory: + +```toml +[redis] +host = "127.0.0.1" +port = 6379 +# db = 0 # Optional: Redis database number +# username = "user" # Optional: Redis username +# password = "password" # Optional: Redis password +# max_connections = 10 # Optional: Maximum connections in pool +# min_connections = 2 # Optional: Minimum idle connections in pool +# idle_timeout = 60 # Optional: Connection idle timeout (seconds) +``` + +### Fallback Behavior + +The extension uses a robust fallback system: +1. **Loads `config.toml`** if present in the extension directory +2. **Falls back to defaults** if configuration fails or file is missing +3. **Only fails** if both config and defaults cannot establish connection + +**Default Settings:** +- **Host**: `127.0.0.1` +- **Port**: `6379` +- **Max Connections**: `10` +- **Min Connections**: `2` +- **Idle Timeout**: `60 seconds` + +### Common Configurations + +**Development (Local Redis)**: +```toml +[redis] +host = "127.0.0.1" +port = 6379 +max_connections = 5 +min_connections = 1 +``` + +**Production (Remote Redis with Authentication)**: +```toml +[redis] +host = "redis.example.com" +port = 6379 +username = "arma_server" +password = "secure_password" +max_connections = 20 +min_connections = 5 +idle_timeout = 60 +``` + +### Troubleshooting + +**Connection Issues:** +- Verify Redis server is running: `redis-cli ping` +- Check host/port settings in `config.toml` +- Ensure firewall allows connection + +**Authentication Issues:** +- Verify username/password in config +- Check Redis server auth settings + +**Config File Issues:** +- Check TOML syntax with online validators +- Ensure quotes are properly closed +- Verify file permissions + +**Connection Pool Benefits:** +- Pre-warmed connections for zero-latency operations +- Automatic connection recovery on network issues +- Resource-efficient connection sharing +- Configurable pool sizing for different deployment scenarios + +## 🔧 Installation + +1. **Prerequisites**: + - Redis server (local or remote) + - Arma 3 server with extension support + +2. **Extension Setup**: + - Build the extension: `cargo build --release` + - Copy the compiled `forge_server_x64.dll` to your Arma 3 server + - Copy `config.example.toml` to `config.toml` and configure as needed + - Load in server config or mission + +3. **Redis Server**: + ```bash + # Start Redis server + redis-server + + # Verify connection + redis-cli ping + ``` + +## 📝 Documentation + +- **[API Reference](./api-reference.md)** - Complete Redis command reference +- **[Usage Examples](./usage-examples.md)** - Practical SQF integration examples + +## 📊 Performance + +- **Connection Pool**: 2-10 persistent connections using bb8-redis +- **Single Runtime**: One shared Tokio runtime eliminates overhead from multiple runtimes +- **Macro Efficiency**: Zero-cost abstraction – macros expand to optimal code at compile time +- **Synchronous Blocking**: Functions use `block_on()` for Arma compatibility without sacrificing async I/O benefits +- **Response Format**: Raw Redis responses for minimal overhead +- **Thread Safety**: Multiple Arma threads can safely call operations concurrently +- **Memory Efficient**: Minimal resource usage per operation + +## 🔄 Response Format + +This module returns **raw Redis responses** as strings for maximum performance: + +### Success Responses +- **String values**: `"John"` (raw string) +- **Numbers**: `"42"` (number as string) +- **Lists/Arrays**: `"item1,item2,item3"` (comma-separated) +- **Hashes**: `"key1,value1,key2,value2"` (comma-separated key-value pairs) +- **Boolean**: `"1"` or `"0"` (for exists checks) +- **Status**: `"OK"` (for successful SET operations) + +### Error Responses +- **Format**: `"Error: "` +- **Pool errors**: `"Error: Redis pool not initialized"` +- **Connection errors**: `"Error: "` +- **Redis errors**: `"Error: "` + +### Higher-Level JSON Formatting +Game modules (actor, garage, etc.) will wrap these raw responses in structured JSON for SQF consumption. + +## ⚙️ Macro-Based Implementation + +This extension uses a **macro-based architecture** to eliminate boilerplate while maintaining performance: + +### The `redis_operation!` Macro + +```rust +pub fn set_key(key: String, value: String) -> String { + redis_operation!(conn => { + match conn.set::<_, _, ()>(&key, &value).await { + Ok(()) => "OK".to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} +``` + +### What the Macro Handles + +- **Pool Management**: Retrieves Redis connection pool +- **Error Handling**: Returns "Error: ..." for pool/connection failures +- **Async Bridging**: Uses shared Tokio runtime via `block_on()` +- **Connection Acquisition**: Gets connection from pool with error handling +- **Cleanup**: Automatic connection return to pool + +### Benefits + +- **Reduced Code**: 70% less boilerplate per function +- **Consistency**: Identical error handling across all operations +- **Maintainability**: Changes to connection logic in one place +- **Performance**: No runtime overhead from abstraction +- **Synchronous Interface**: Functions block until Redis operation completes + +## 🛠️ Development + +- **Language**: Rust (Edition 2024) +- **Dependencies**: arma-rs, bb8-redis, redis, tokio +- **Architecture**: Macro-based design with single runtime and connection pool +- **Key Patterns**: + - Global state management in `lib.rs` + - Boilerplate elimination via `redis_operation!` macro + - Synchronous interfaces over async operations + - Raw Redis responses for minimal overhead +- **Testing**: Unit tests for core functionality + + +## 🚨 Error Handling + +The extension provides comprehensive error handling: +- Connection failures +- Redis operation errors +- Invalid parameters +- Pool initialization errors + +All errors include descriptive messages for debugging. + +## 🔍 Monitoring + +Connection pool status and Redis operations can be monitored through: +- Extension logs +- Redis server logs +- Connection pool metrics + +--- + +**Built with ❤️ for the Arma 3 community** \ No newline at end of file diff --git a/arma/server/docs/api-reference.md b/arma/server/docs/api-reference.md new file mode 100644 index 0000000..a039827 --- /dev/null +++ b/arma/server/docs/api-reference.md @@ -0,0 +1,388 @@ +# Redis Client API Reference + +Complete reference for **raw Redis operations** available in the Forge Server extension. This module returns native Redis values without JSON formatting. + +> **Note**: This is a low-level data access layer. Higher-level game modules (actor, garage, etc.) will provide structured JSON responses for SQF consumption. + +## 🏗️ Implementation + +All Redis operations are implemented using the `redis_operation!` macro for: +- **Consistent Error Handling**: All functions return identical error formats +- **Connection Management**: Automatic pool and connection handling +- **Synchronous Interface**: Functions block until Redis operations complete +- **Performance**: Zero-cost abstraction with compile-time optimization + +## 🔗 Command Structure + +All redis client commands follow the pattern: `"forge_server" callExtension ["redis:command", [parameters]]` + +## 📝 Common Operations + +### SET - Store a key-value pair +**Command**: `redis:common:set` +**Parameters**: `[key, value]` + +```sqf +"forge_server" callExtension ["redis:common:set", ["player_name", "John"]] +``` + +**Raw Response**: `"OK"` + +### GET - Retrieve a value by key +**Command**: `redis:common:get` +**Parameters**: `[key]` + +```sqf +"forge_server" callExtension ["redis:common:get", ["player_name"]] +``` + +**Raw Response**: `"John"` (the actual stored value) + +### INCR - Increment a numeric value +**Command**: `redis:common:incr` +**Parameters**: `[key, increment_amount]` + +```sqf +"forge_server" callExtension ["redis:common:incr", ["player_score", 10]] +``` + +**Raw Response**: `"110"` (the new value as string) + +### DECR - Decrement a numeric value +**Command**: `redis:common:decr` +**Parameters**: `[key, decrement_amount]` + +```sqf +"forge_server" callExtension ["redis:common:decr", ["player_lives", 1]] +``` + +**Raw Response**: `"2"` (the new value as string) + +### DEL - Delete a key +**Command**: `redis:common:del` +**Parameters**: `[key]` + +```sqf +"forge_server" callExtension ["redis:common:del", ["temp_data"]] +``` + +**Raw Response**: `"1"` (number of keys deleted) + +### KEYS - List all keys matching pattern +**Command**: `redis:common:keys` +**Parameters**: `[]` (currently returns all keys with "*" pattern) + +```sqf +"forge_server" callExtension ["redis:common:keys", []] +``` + +**Raw Response**: `"player_name,player_score,mission_state"` (comma-separated list) + +## 🗂️ Hash Operations + +### HSET - Set hash field +**Command**: `redis:hash:set` +**Parameters**: `[hash_key, field, value]` + +```sqf +"forge_server" callExtension ["redis:hash:set", ["player:123", "name", "John"]] +``` + +**Raw Response**: `"1"` (number of fields added) + +### HGET - Get hash field +**Command**: `redis:hash:get` +**Parameters**: `[hash_key, field]` + +```sqf +"forge_server" callExtension ["redis:hash:get", ["player:123", "name"]] +``` + +**Raw Response**: `"John"` (the field value) + +### HMSET - Set multiple hash fields +**Command**: `redis:hash:mset` +**Parameters**: `[hash_key, [[field1, value1], [field2, value2], ...]]` + +```sqf +"forge_server" callExtension ["redis:hash:mset", ["player:123", [["name", "John"], ["score", "100"]]]] +``` + +**Raw Response**: `"OK"` + +### HGETALL - Get all hash fields +**Command**: `redis:hash:getall` +**Parameters**: `[hash_key]` + +```sqf +"forge_server" callExtension ["redis:hash:getall", ["player:123"]] +``` + +**Raw Response**: `"name,John,score,100,level,5"` (comma-separated key-value pairs) + +### HDEL - Delete hash field +**Command**: `redis:hash:del` +**Parameters**: `[hash_key, field]` + +```sqf +"forge_server" callExtension ["redis:hash:del", ["player:123", "temp_field"]] +``` + +**Raw Response**: `"1"` (number of fields removed) + +### HKEYS - Get all hash field names +**Command**: `redis:hash:keys` +**Parameters**: `[hash_key]` + +```sqf +"forge_server" callExtension ["redis:hash:keys", ["player:123"]] +``` + +**Raw Response**: `"name,score,level"` (comma-separated field names) + +### HVALS - Get all hash values +**Command**: `redis:hash:vals` +**Parameters**: `[hash_key]` + +```sqf +"forge_server" callExtension ["redis:hash:vals", ["player:123"]] +``` + +**Raw Response**: `"John,100,5"` (comma-separated values) + +### HLEN - Get hash field count +**Command**: `redis:hash:len` +**Parameters**: `[hash_key]` + +```sqf +"forge_server" callExtension ["redis:hash:len", ["player:123"]] +``` + +**Raw Response**: `"3"` (number of fields in hash) + +## 📋 List Operations + +### LSET - Set list element by index +**Command**: `redis:list:set` +**Parameters**: `[list_key, index, value]` + +```sqf +"forge_server" callExtension ["redis:list:set", ["mission_queue", 0, "patrol_alpha"]] +``` + +**Raw Response**: `"OK"` + +### LGET - Get list element by index +**Command**: `redis:list:get` +**Parameters**: `[list_key, index]` + +```sqf +"forge_server" callExtension ["redis:list:get", ["mission_queue", 0]] +``` + +**Raw Response**: `"patrol_alpha"` (the element value) + +### LLEN - Get list length +**Command**: `redis:list:len` +**Parameters**: `[list_key]` + +```sqf +"forge_server" callExtension ["redis:list:len", ["mission_queue"]] +``` + +**Raw Response**: `"5"` (list length) + +### LRANGE - Get list elements in range +**Command**: `redis:list:range` +**Parameters**: `[list_key, start_index, end_index]` + +```sqf +"forge_server" callExtension ["redis:list:range", ["mission_queue", 0, 2]] +``` + +**Raw Response**: `"patrol_alpha,escort_beta,defend_gamma"` (comma-separated values) + +### LPUSH - Add element to list head +**Command**: `redis:list:lpush` +**Parameters**: `[list_key, value]` + +```sqf +"forge_server" callExtension ["redis:list:lpush", ["recent_actions", "player_joined"]] +``` + +**Raw Response**: `"6"` (new list length) + +### RPUSH - Add element to list tail +**Command**: `redis:list:rpush` +**Parameters**: `[list_key, value]` + +```sqf +"forge_server" callExtension ["redis:list:rpush", ["mission_queue", "new_objective"]] +``` + +**Raw Response**: `"6"` (new list length) + +### LPOP - Remove and return element from list head +**Command**: `redis:list:lpop` +**Parameters**: `[list_key, count]` + +```sqf +"forge_server" callExtension ["redis:list:lpop", ["recent_actions", 1]] +``` + +**Raw Response**: `"player_joined"` (removed element) or `"item1,item2"` (if count > 1) + +### RPOP - Remove and return element from list tail +**Command**: `redis:list:rpop` +**Parameters**: `[list_key, count]` + +```sqf +"forge_server" callExtension ["redis:list:rpop", ["mission_queue", 1]] +``` + +**Raw Response**: `"new_objective"` (removed element) or `"item1,item2"` (if count > 1) + +### LTRIM - Trim list to specified range +**Command**: `redis:list:trim` +**Parameters**: `[list_key, start_index, end_index]` + +```sqf +"forge_server" callExtension ["redis:list:trim", ["recent_actions", 0, 9]] // Keep only last 10 items +``` + +**Raw Response**: `"OK"` + +### LREM - Remove elements from list +**Command**: `redis:list:del` +**Parameters**: `[list_key, count, value]` + +```sqf +"forge_server" callExtension ["redis:list:del", ["mission_queue", 1, "completed_mission"]] +``` + +**Raw Response**: `"1"` (number of elements removed) + +## 🎯 Set Operations + +### SADD - Add element to set +**Command**: `redis:set:add` +**Parameters**: `[set_key, value]` + +```sqf +"forge_server" callExtension ["redis:set:add", ["online_players", "player_123"]] +``` + +**Raw Response**: `"1"` (1 if element was added, 0 if already existed) + +### SMEMBERS - Get all set members +**Command**: `redis:set:members` +**Parameters**: `[set_key]` + +```sqf +"forge_server" callExtension ["redis:set:members", ["online_players"]] +``` + +**Raw Response**: `"player_123,player_456,player_789"` (comma-separated members) + +### SCARD - Get set size +**Command**: `redis:set:card` +**Parameters**: `[set_key]` + +```sqf +"forge_server" callExtension ["redis:set:card", ["online_players"]] +``` + +**Raw Response**: `"3"` (number of elements in set) + +### SREM - Remove element from set +**Command**: `redis:set:del` +**Parameters**: `[set_key, value]` + +```sqf +"forge_server" callExtension ["redis:set:del", ["online_players", "player_456"]] +``` + +**Raw Response**: `"1"` (1 if element was removed, 0 if didn't exist) + +### SISMEMBER - Check if element is in set +**Command**: `redis:set:ismember` +**Parameters**: `[set_key, value]` + +```sqf +"forge_server" callExtension ["redis:set:ismember", ["online_players", "player_123"]] +``` + +**Raw Response**: `"1"` (1 if member exists, 0 if not) + +### SPOP - Remove and return random element +**Command**: `redis:set:pop` +**Parameters**: `[set_key]` + +```sqf +"forge_server" callExtension ["redis:set:pop", ["available_missions"]] +``` + +**Raw Response**: `"mission_alpha"` (the removed element) + +### SRANDMEMBER - Get random element without removing +**Command**: `redis:set:randmember` +**Parameters**: `[set_key]` + +```sqf +"forge_server" callExtension ["redis:set:randmember", ["available_missions"]] +``` + +**Raw Response**: `"mission_beta"` (a random element) + +### SRANDMEMBER - Get multiple random elements +**Command**: `redis:set:randmembers` +**Parameters**: `[set_key, count]` + +```sqf +"forge_server" callExtension ["redis:set:randmembers", ["available_missions", 3]] +``` + +**Raw Response**: `"mission_alpha,mission_gamma,mission_delta"` (comma-separated random elements) + +## ⚠️ Error Responses + +All commands may return error responses in this format: + +**Raw Error Response**: `"Error: "` + +### Common Error Types +- **Pool not initialized**: `"Error: Redis pool not initialized"` +- **Connection failed**: `"Error: Connection refused (os error 61)"` +- **Key not found**: `"Error: key not found"` (for operations on non-existent keys) +- **Invalid type**: `"Error: WRONGTYPE Operation against a key holding the wrong kind of value"` +- **Index out of range**: `"Error: index out of range"` (for list operations) + +### Error Handling in Game Modules +Higher-level game modules should check if the response starts with `"Error: "` to distinguish between successful responses and errors. + +```json +{ + "status": "error", + "error": "Failed to connect to Redis server" +} +``` + +Common error types: +- **Connection errors**: Redis server unavailable +- **Operation errors**: Invalid data type for operation +- **Parameter errors**: Missing or invalid parameters +- **Pool errors**: Connection pool exhausted + +## 📊 Response Fields + +### Common Fields +- `status`: Always present - "success" or "error" +- `key`: The Redis key being operated on +- `error`: Error message (only on error responses) + +### Success-Specific Fields +- `data`: The retrieved data (for GET operations) +- `value`: The stored value (for SET operations) +- `was_new`: Boolean indicating if operation created new data +- `removed_count`: Number of elements removed +- `fields_set`: Number of fields set in hash operations diff --git a/arma/server/docs/usage-examples.md b/arma/server/docs/usage-examples.md new file mode 100644 index 0000000..24515e2 --- /dev/null +++ b/arma/server/docs/usage-examples.md @@ -0,0 +1,427 @@ +# Redis Client Usage Examples + +Practical examples of using the **raw Redis client module** as a foundation for higher-level game modules. These examples show low-level Redis operations that would typically be wrapped by game-specific modules (actor, garage, locker, bank). + +> **Note**: These examples show raw Redis responses. In practice, your game modules would wrap these calls and return structured JSON to SQF scripts. + +## 🚀 Function Behavior + +All Redis functions are **synchronous from SQF's perspective**: +- Functions **block** until Redis operation completes +- **No callbacks** or async handling needed in SQF +- **Direct return values** – either data or error strings +- **Thread-safe** – multiple scripts can call simultaneously + +The extension handles all async complexity internally using a macro-based architecture. + +## 🎮 Player Management + +### Player Join/Leave Tracking +```sqf +// When player joins +_playerUID = getPlayerUID player; +_playerName = name player; + +// Store player info in hash +"forge_server" callExtension ["redis:hash:set", [format ["player:%1", _playerUID], "name", _playerName]]; +"forge_server" callExtension ["redis:hash:set", [format ["player:%1", _playerUID], "join_time", str time]]; + +// Add to online players set +"forge_server" callExtension ["redis:set:add", ["online_players", _playerUID]]; + +// When player leaves +"forge_server" callExtension ["redis:set:del", ["online_players", _playerUID]]; +"forge_server" callExtension ["redis:hash:set", [format ["player:%1", _playerUID], "leave_time", str time]]; +``` + +### Player Statistics System +```sqf +// Initialize player stats +fnc_initPlayerStats = { + params ["_playerUID"]; + + _playerKey = format ["stats:%1", _playerUID]; + "forge_server" callExtension ["redis:hash:mset", [_playerKey, [ + ["kills", "0"], + ["deaths", "0"], + ["score", "0"], + ["playtime", "0"] + ]]]; +}; + +// Update player kill +fnc_addPlayerKill = { + params ["_playerUID"]; + + _playerKey = format ["stats:%1", _playerUID]; + "forge_server" callExtension ["redis:hash:incr", [_playerKey, "kills", 1]]; + "forge_server" callExtension ["redis:hash:incr", [_playerKey, "score", 10]]; +}; + +// Get player stats (raw response) +fnc_getPlayerStats = { + params ["_playerUID"]; + + _playerKey = format ["stats:%1", _playerUID]; + _rawResult = "forge_server" callExtension ["redis:hash:getall", [_playerKey]]; + // _rawResult is now "kills,15,deaths,3,score,150,playtime,7200" + + // Game modules would parse this into structured data + // For now, return raw comma-separated response + _rawResult select 0; +}; +``` + +## 🏆 Leaderboards and Rankings + +### Global Kill Leaderboard +```sqf +// Add score to sorted leaderboard (using list for simplicity) +fnc_updateLeaderboard = { + params ["_playerName", "_kills"]; + + // Store individual score + "forge_server" callExtension ["redis:common:set", [format ["kills:%1", _playerName], str _kills]]; + + // Add to leaderboard tracking + "forge_server" callExtension ["redis:set:add", ["leaderboard_players", _playerName]]; +}; + +// Get top 10 players (raw response handling) +fnc_getTopPlayers = { + // Get all leaderboard players - returns comma-separated list + _playersResult = "forge_server" callExtension ["redis:set:members", ["leaderboard_players"]]; + _rawPlayers = _playersResult select 0; + + // Check for error + if (_rawPlayers find "Error:" == 0) exitWith { [] }; + + // Split comma-separated player list + _players = _rawPlayers splitString ","; + _scoreArray = []; + + // Get scores for all players + { + _killsResult = "forge_server" callExtension ["redis:common:get", [format ["kills:%1", _x]]]; + _rawKills = _killsResult select 0; + + // Check for valid response (not an error) + if (_rawKills find "Error:" != 0) then { + _scoreArray pushBack [_x, parseNumber _rawKills]; + }; + } forEach _players; + + // Sort by score (highest first) + _scoreArray sort false; + _scoreArray resize (10 min (count _scoreArray)); // Top 10 + + _scoreArray; +}; +``` + +## 🎯 Mission State Management + +### Objective System +```sqf +// Set mission objectives +fnc_initMissionObjectives = { + "forge_server" callExtension ["redis:list:rpush", ["objectives", "Secure Alpha Base"]]; + "forge_server" callExtension ["redis:list:rpush", ["objectives", "Extract Intel"]]; + "forge_server" callExtension ["redis:list:rpush", ["objectives", "Eliminate HVT"]]; + + // Set current objective pointer + "forge_server" callExtension ["redis:common:set", ["current_objective", "0"]]; +}; + +// Complete current objective +fnc_completeObjective = { + // Get current objective index - returns raw string + _indexResult = "forge_server" callExtension ["redis:common:get", ["current_objective"]]; + _rawIndex = _indexResult select 0; + + // Check for error + if (_rawIndex find "Error:" == 0) exitWith {}; + + _currentIndex = parseNumber _rawIndex; + + // Get objective name - returns raw string + _objResult = "forge_server" callExtension ["redis:list:get", ["objectives", _currentIndex]]; + _objectiveName = _objResult select 0; + + // Check for valid response + if (_objectiveName find "Error:" != 0) then { + // Move to completed objectives - returns new list length + "forge_server" callExtension ["redis:list:rpush", ["completed_objectives", _objectiveName]]; + + // Move to next objective - returns "OK" + "forge_server" callExtension ["redis:common:set", ["current_objective", str (_currentIndex + 1)]]; + + // Broadcast completion + [format ["Objective Complete: %1", _objectiveName]] remoteExec ["hint"]; + }; +}; + +// Get mission progress - raw responses +fnc_getMissionProgress = { + _totalResult = "forge_server" callExtension ["redis:list:len", ["objectives"]]; + _completedResult = "forge_server" callExtension ["redis:list:len", ["completed_objectives"]]; + + _rawTotal = _totalResult select 0; + _rawCompleted = _completedResult select 0; + + // Check for errors + if (_rawTotal find "Error:" == 0 || _rawCompleted find "Error:" == 0) exitWith { + "Mission Progress: Unknown"; + }; + + _total = parseNumber _rawTotal; + _completed = parseNumber _rawCompleted; + + format ["Mission Progress: %1/%2 objectives completed", _completed, _total]; +}; +``` + +## 🚁 Vehicle and Equipment Tracking + +### Vehicle Pool System +```sqf +// Initialize vehicle pool +fnc_initVehiclePool = { + params ["_vehicleClass", "_count"]; + + for "_i" from 1 to _count do { + _vehicleId = format ["%1_%2", _vehicleClass, _i]; + "forge_server" callExtension ["redis:set:add", ["available_vehicles", _vehicleId]]; + "forge_server" callExtension ["redis:hash:mset", [format ["vehicle:%1", _vehicleId], [ + ["class", _vehicleClass], + ["status", "available"], + ["condition", "100"] + ]]]; + }; +}; + +// Request vehicle +fnc_requestVehicle = { + params ["_playerUID"]; + + // Get random available vehicle + _result = "forge_server" callExtension ["redis:set:pop", ["available_vehicles"]]; + _data = fromJSON (_result select 0); + + if ((_data select "status") == "success") then { + _vehicleId = _data select "data"; + + // Mark as in use + "forge_server" callExtension ["redis:hash:set", [format ["vehicle:%1", _vehicleId], "status", "in_use"]]; + "forge_server" callExtension ["redis:hash:set", [format ["vehicle:%1", _vehicleId], "user", _playerUID]]; + "forge_server" callExtension ["redis:set:add", ["used_vehicles", _vehicleId]]; + + _vehicleId; + } else { + ""; // No vehicles available + }; +}; + +// Return vehicle +fnc_returnVehicle = { + params ["_vehicleId", "_condition"]; + + // Update condition + "forge_server" callExtension ["redis:hash:set", [format ["vehicle:%1", _vehicleId], "condition", str _condition]]; + + // Return to pool if condition is good + if (_condition > 50) then { + "forge_server" callExtension ["redis:hash:set", [format ["vehicle:%1", _vehicleId], "status", "available"]]; + "forge_server" callExtension ["redis:hash:del", [format ["vehicle:%1", _vehicleId], "user"]]; + "forge_server" callExtension ["redis:set:del", ["used_vehicles", _vehicleId]]; + "forge_server" callExtension ["redis:set:add", ["available_vehicles", _vehicleId]]; + } else { + "forge_server" callExtension ["redis:hash:set", [format ["vehicle:%1", _vehicleId], "status", "maintenance"]]; + "forge_server" callExtension ["redis:set:add", ["maintenance_vehicles", _vehicleId]]; + }; +}; +``` + +## 📊 Server Analytics + +### Player Session Tracking +```sqf +// Track player session start +fnc_startPlayerSession = { + params ["_playerUID"]; + + _sessionId = format ["%1_%2", _playerUID, floor time]; + _sessionKey = format ["session:%1", _sessionId]; + + "forge_server" callExtension ["redis:hash:mset", [_sessionKey, [ + ["player_uid", _playerUID], + ["start_time", str time], + ["server_id", serverName], + ["player_count", str (count allPlayers)] + ]]]; + + // Store current session for player + "forge_server" callExtension ["redis:common:set", [format ["current_session:%1", _playerUID], _sessionId]]; + + _sessionId; +}; + +// End player session +fnc_endPlayerSession = { + params ["_playerUID", "_sessionStats"]; + + // Get current session + _result = "forge_server" callExtension ["redis:common:get", [format ["current_session:%1", _playerUID]]]; + _data = fromJSON (_result select 0); + + if ((_data select "status") == "success") then { + _sessionId = _data select "data"; + _sessionKey = format ["session:%1", _sessionId]; + + // Update session with end data + "forge_server" callExtension ["redis:hash:mset", [_sessionKey, [ + ["end_time", str time], + ["duration", str (_sessionStats select "duration")], + ["kills", str (_sessionStats select "kills")], + ["deaths", str (_sessionStats select "deaths")] + ]]]; + + // Clean up current session tracking + "forge_server" callExtension ["redis:common:del", [format ["current_session:%1", _playerUID]]]; + }; +}; +``` + +## 🔄 Cross-Server Communication + +### Message Queue System +```sqf +// Send message to other servers +fnc_sendCrossServerMessage = { + params ["_targetServer", "_messageType", "_messageData"]; + + _message = createHashMap; + _message set ["from_server", serverName]; + _message set ["type", _messageType]; + _message set ["data", _messageData]; + _message set ["timestamp", str time]; + + _queueKey = format ["messages:%1", _targetServer]; + "forge_server" callExtension ["redis:list:rpush", [_queueKey, str _message]]; +}; + +// Check for incoming messages +fnc_checkMessages = { + _queueKey = format ["messages:%1", serverName]; + + // Get next message + _result = "forge_server" callExtension ["redis:list:lpop", [_queueKey, 1]]; + _data = fromJSON (_result select 0); + + if ((_data select "status") == "success") then { + _messages = _data select "data"; + if (count _messages > 0) then { + _messageStr = _messages select 0; + _message = fromJSON _messageStr; + + // Process message based on type + _type = _message select "type"; + _messageData = _message select "data"; + + switch (_type) do { + case "player_transfer": { + [_messageData] call fnc_handlePlayerTransfer; + }; + case "server_status": { + [_messageData] call fnc_handleServerStatus; + }; + case "admin_broadcast": { + [_messageData select "message"] remoteExec ["hint"]; + }; + }; + }; + }; +}; + +// Run message checker periodically +[] spawn { + while {true} do { + call fnc_checkMessages; + sleep 5; // Check every 5 seconds + }; +}; +``` + +## 🛠️ Utility Functions + +### Redis Helper Functions +```sqf +// Parse Redis response safely +fnc_parseRedisResponse = { + params ["_response"]; + + try { + _data = fromJSON (_response select 0); + if ((_data select "status") == "success") then { + _data select "data"; + } else { + diag_log format ["Redis Error: %1", _data select "error"]; + nil; + }; + } catch { + diag_log format ["JSON Parse Error: %1", _exception]; + nil; + }; +}; + +// Batch Redis operations +fnc_redisBatch = { + params ["_operations"]; + + _results = []; + { + _op = _x; + _result = "forge_server" callExtension [_op select 0, _op select 1]; + _results pushBack (fromJSON (_result select 0)); + } forEach _operations; + + _results; +}; + +// Example batch usage: +_batchOps = [ + ["redis:common:set", ["key1", "value1"]], + ["redis:common:set", ["key2", "value2"]], + ["redis:common:get", ["key1"]] +]; +_results = [_batchOps] call fnc_redisBatch; +``` + +## 🎯 Best Practices + +### Error Handling Pattern +```sqf +fnc_safeRedisCall = { + params ["_command", "_params", ["_defaultValue", nil]]; + + try { + _result = "forge_server" callExtension [_command, _params]; + _data = fromJSON (_result select 0); + + if ((_data select "status") == "success") then { + _data select "data"; + } else { + diag_log format ["Redis operation failed: %1 - %2", _command, _data select "error"]; + _defaultValue; + }; + } catch { + diag_log format ["Redis call exception: %1 - %2", _command, _exception]; + _defaultValue; + }; +}; + +// Usage: +_playerName = ["redis:common:get", ["player_name"], "Unknown"] call fnc_safeRedisCall; +``` + +These examples demonstrate real-world usage patterns for the Redis extension in Arma 3 environments, covering player management, mission state, analytics, and cross-server communication. \ No newline at end of file diff --git a/arma/server/extension/Cargo.toml b/arma/server/extension/Cargo.toml new file mode 100644 index 0000000..613facc --- /dev/null +++ b/arma/server/extension/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "forge-server" +version = "0.1.0" +edition = "2024" + +[lib] +name = "forge_server_x64" +crate-type = ["cdylib"] + +[dependencies] +arma-rs = { workspace = true } +base64 = "0.22.1" +bb8-redis = "0.25.0-rc.1" +chrono = { workspace = true } +forge-models = { path = "../../../lib/models", features = ["actor"] } +forge-repositories = { path = "../../../lib/repositories" } +forge-services = { path = "../../../lib/services" } +forge-shared = { path = "../../../lib/shared" } +redis = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tokio = { workspace = true } +toml = "0.9.8" +uuid = { workspace = true } diff --git a/arma/server/extension/README.md b/arma/server/extension/README.md new file mode 100644 index 0000000..f1c535a --- /dev/null +++ b/arma/server/extension/README.md @@ -0,0 +1,399 @@ +# Forge Arma 3 Server Extension + +This extension provides the core server-side functionality for the Forge framework, handling persistent data storage, actor management, and game state synchronization through a high-performance Rust backend. + +## Architecture + +The extension follows a layered architecture designed for reliability, performance, and maintainability: + +- **Extension Layer**: Handles the raw Arma 3 `callExtension` interface, parameter parsing, and command routing. +- **Service Layer**: Implements business logic, validation, and orchestration of operations (e.g., `ActorService`). +- **Repository Layer**: Manages data persistence and retrieval using Redis (e.g., `RedisActorRepository`). +- **Model Layer**: Defines strict data structures and validation rules (e.g., `Actor` model). + +This separation ensures that game logic is decoupled from data storage and that all data entering the system is validated before persistence. + +### Module Documentation + +For detailed information about specific modules, see: + +- **[Redis Operations](src/redis/README.md)**: Comprehensive guide to Redis commands (hash, list, set, common operations) +- **[Adapters](src/adapters/README.md)**: Adapter pattern implementation bridging repositories with Redis + +## Organization Management + +The Organization module handles guild/clan management, allowing players to form groups, manage members, and persist organizational data. It supports role management, automatic UID resolution, and robust error handling. + +### Available Commands + +| Command | Description | +|---------|-------------| +| `org:get` | Retrieve organization data by key or ID. | +| `org:create` | Create a new organization with provided JSON data. | +| `org:update` | Update an existing organization with partial JSON data. | +| `org:delete` | Permanently remove an organization and its data. | +| `org:exists` | Check if an organization exists. | +| `org:get_members` | Retrieve a list of organization members. | +| `org:add_member` | Add a member to an organization. | +| `org:remove_member` | Remove a member from an organization. | + +### SQF Examples + +#### Retrieving an Organization + +```sqf +// Get organization by ID +private _result = "forge_server" callExtension ["org:get", ["elite_squad"]]; +private _orgData = fromJSON (_result select 0); + +// Access data +private _name = _orgData get "name"; +private _leader = _orgData get "leader"; +``` + +#### Creating an Organization + +```sqf +// Prepare data using HashMap +private _data = createHashMapFromArray [ + ["name", "Elite Squad"], + ["description", "Best players"], + ["leader", getPlayerUID player], + ["max_members", 50], + ["type", "military"] +]; + +// Create the organization +private _result = "forge_server" callExtension ["org:create", ["elite_squad", toJSON _data]]; + +if ((_result select 0) find "Error:" == 0) then { + diag_log format ["Failed to create org: %1", _result select 0]; +} else { + private _createdOrg = fromJSON (_result select 0); + systemChat format ["Created organization: %1", _createdOrg get "name"]; +}; +``` + +#### Updating an Organization + +```sqf +// Prepare partial update +private _update = createHashMapFromArray [ + ["description", "Updated description"], + ["max_members", 100] +]; + +// Apply update +private _result = "forge_server" callExtension ["org:update", ["elite_squad", toJSON _update]]; +``` + +#### Managing Members + +```sqf +// Get members +private _result = "forge_server" callExtension ["org:get_members", ["elite_squad"]]; +private _members = fromJSON (_result select 0); + +// Add a member +private _addResult = "forge_server" callExtension ["org:add_member", ["elite_squad", "76561198123456789"]]; + +// Remove a member +private _removeResult = "forge_server" callExtension ["org:remove_member", ["elite_squad", "76561198123456789"]]; +``` + +#### Checking Existence + +```sqf +private _exists = "forge_server" callExtension ["org:exists", ["elite_squad"]]; + +if ((_exists select 0) == "true") then { + systemChat "Organization exists."; +}; +``` + +#### Deleting an Organization + +```sqf +// Permanently delete organization +private _result = "forge_server" callExtension ["org:delete", ["elite_squad"]]; + +if ((_result select 0) == "OK") then { + systemChat "Organization deleted."; +}; +``` + +## Actor Management + +The Actor module handles all player-related operations, including data retrieval, creation, updates, and existence checks. It features automatic Steam UID resolution and robust error handling. + +### Available Commands + +| Command | Description | +|---------|-------------| +| `actor:get` | Retrieve actor data by key or UID. | +| `actor:create` | Create a new actor with provided JSON data. | +| `actor:update` | Update an existing actor with partial JSON data. | +| `actor:exists` | Check if an actor exists in the database. | +| `actor:delete` | Permanently remove an actor and their data. | + +### SQF Examples + +The extension is designed to work seamlessly with modern Arma 3 SQF features like `HashMap` and `toJSON`/`fromJSON`. + +#### Retrieving an Actor + +```sqf +// Get actor by Steam UID +private _result = "forge_server" callExtension ["actor:get", ["76561198123456789"]]; +private _actorData = fromJSON (_result select 0); + +// Access data +private _name = _actorData get "name"; +private _bank = _actorData get "bank"; +``` + +#### Creating an Actor + +```sqf +// Prepare data using HashMap +private _data = createHashMapFromArray [ + ["name", "John Doe"], + ["bank", 1000], + ["cash", 100], + ["level", 1], + ["class", "civilian"] +]; + +// Create the actor +private _result = "forge_server" callExtension ["actor:create", ["player123", toJSON _data]]; + +if ((_result select 0) find "Error:" == 0) then { + diag_log format ["Failed to create actor: %1", _result select 0]; +} else { + private _createdActor = fromJSON (_result select 0); + systemChat format ["Welcome, %1!", _createdActor get "name"]; +}; +``` + +#### Updating an Actor + +```sqf +// Prepare partial update +private _update = createHashMapFromArray [ + ["bank", 1500], + ["level", 2] +]; + +// Apply update +private _result = "forge_server" callExtension ["actor:update", ["player123", toJSON _update]]; +``` + +#### Checking Existence + +```sqf +private _exists = "forge_server" callExtension ["actor:exists", ["player123"]]; + +if ((_exists select 0) == "true") then { + systemChat "Player profile found."; +} else { + systemChat "Player profile not found."; +}; +``` + +#### Deleting an Actor + +```sqf +// Permanently delete actor data +private _result = "forge_server" callExtension ["actor:delete", ["player123"]]; + +if ((_result select 0) == "OK") then { + systemChat "Actor deleted successfully."; +}; +``` + +## Error Handling + +The extension uses a consistent error reporting format. If an operation fails, the returned string will start with `Error: ` followed by a descriptive message. + +- **Consistent Responses**: All commands return JSON on success or an error message on failure. +- **No Fallbacks**: `actor:get` and `org:get` will return error messages if the requested entity cannot be found, rather than fallback objects with dummy data. +- **Validation**: All input data is validated against the strict schema defined in the models. Invalid data will result in an error message. + +### Example Error Handling + +```sqf +private _result = "forge_server" callExtension ["actor:get", ["76561198123456789"]]; +private _response = _result select 0; + +if (_response find "Error:" == 0) then { + diag_log format ["Failed to get actor: %1", _response]; +} else { + private _actorData = fromJSON _response; + systemChat format ["Welcome, %1!", _actorData get "name"]; +}; +``` + +## Performance + +- **Asynchronous Core**: Built on `tokio`, the extension performs heavy I/O operations (like database writes) without blocking the Arma 3 simulation thread. +- **Connection Pooling**: Uses a Redis connection pool to efficiently manage database connections. +- **Lazy Initialization**: Services are initialized only when first needed, reducing startup time. +- **Minimal Serialization**: Only necessary data is serialized and transferred between Rust and SQF to minimize overhead. + +## Contributing + +We welcome contributions to the Forge Extension! This guide will help you understand how to add new commands and maintain the existing codebase. + +### Adding a Command to an Existing Module + +To add a new command to an existing module (e.g., `actor:set_position`), follow these steps: + +1. **Register the Command**: In the module file (e.g., `src/actor.rs`), add the command to the `group()` function. + ```rust + pub fn group() -> Group { + Group::new() + .command("get", get_actor) + .command("exists", exists_actor) + .command("create", create_actor) + .command("update", update_actor) + .command("delete", delete_actor) + .command("set_position", set_actor_position) // New command + } + ``` + +2. **Implement the Handler Function**: Create the function that handles the command logic. + ```rust + use crate::log::log; + + /// Sets the position of an actor. + pub fn set_actor_position(call_context: CallContext, key: String, position: String) -> String { + log("actor", "DEBUG", &format!("Setting position for key: {}", key)); + + // 1. Resolve UID + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => { + let error_msg = format!("Error: Failed to resolve UID for key: {}", key); + log("actor", "ERROR", &error_msg); + return error_msg; + } + }; + + // 2. Parse and validate input + let position_data: Vec = match serde_json::from_str(&position) { + Ok(data) => data, + Err(e) => { + let error_msg = format!("Error: Invalid position JSON: {}", e); + log("actor", "ERROR", &error_msg); + return error_msg; + } + }; + + // 3. Get the actor, update position, and save + match ACTOR_SERVICE.get_actor(resolved_uid.clone()) { + Ok(mut actor) => { + actor.set_position(position_data); + + match ACTOR_SERVICE.update_actor(actor.clone()) { + Ok(_) => { + log("actor", "INFO", &format!("Updated position for: {}", resolved_uid)); + match serde_json::to_string(&actor) { + Ok(json) => json, + Err(e) => format!("Error: Failed to serialize actor: {}", e), + } + } + Err(e) => format!("Error: {}", e), + } + } + Err(e) => format!("Error: {}", e), + } + } + ``` + +### Creating a New Module + +To create a new module (e.g., `vehicle`), follow these steps: + +1. **Create the Module File**: Add `src/vehicle.rs`. +2. **Create the Global Service Instance**: Define a lazily initialized singleton service. + ```rust + use std::sync::LazyLock; + use forge_services::VehicleService; + use forge_repositories::RedisVehicleRepository; + use crate::adapters::ExtensionRedisClient; + + static VEHICLE_SERVICE: LazyLock>> = + LazyLock::new(|| { + let redis_client = ExtensionRedisClient::new(); + let repository = RedisVehicleRepository::new(redis_client); + VehicleService::new(repository) + }); + ``` +3. **Register the Command**: In the module file, register the command in the `group()` function. + ```rust + pub fn group() -> Group { + Group::new() + .command("get", get_vehicle) + .command("create", create_vehicle) + // ... other commands + } + ``` +4. **Use Logging**: Import and use the generic `log` function in your handler functions. + ```rust + use crate::log::log; + + pub fn get_vehicle(key: String) -> String { + log("vehicle", "DEBUG", &format!("Getting vehicle for key: {}", key)); + + // Call service layer + match VEHICLE_SERVICE.get_vehicle(key.clone()) { + Ok(vehicle) => { + log("vehicle", "INFO", &format!("Successfully retrieved vehicle: {}", key)); + match serde_json::to_string(&vehicle) { + Ok(json) => { + log("vehicle", "DEBUG", &format!("Serialized vehicle to JSON: {}", json)); + json + } + Err(e) => { + let error_msg = format!("Error: Failed to serialize vehicle: {}", e); + log("vehicle", "ERROR", &error_msg); + error_msg + } + } + } + Err(e) => { + let error_msg = format!("Error: {}", e); + log("vehicle", "ERROR", &format!("Failed to get vehicle '{}': {}", key, e)); + error_msg + } + } + } + ``` + + The `log` function takes three parameters: + - `category`: The log category (e.g., "vehicle", "actor", "org") + - `level`: The log level ("INFO", "DEBUG", "WARN", "ERROR") + - `message`: The message to log + + Log files are created automatically in `@forge_server/logs/{category}.log`. + +5. **Register the Module** (if new): If you created a new module, add it to `src/lib.rs`. + ```rust + pub mod vehicle; + + // In the extension function, register the group + extension.group("vehicle", vehicle::group()); + ``` + +### Testing + +- **In-Game Testing**: Test your commands in Arma 3 to ensure they work correctly with SQF. +- **Error Cases**: Test error scenarios (invalid input, missing entities, etc.) to ensure proper error messages. + +### Best Practices + +- **Return Types**: Always return `String` (JSON on success, error message on failure). +- **Error Messages**: Prefix all error messages with `"Error: "` for consistency. +- **Logging**: Use the `log(category, level, message)` function to track operations. +- **Service Layer**: Delegate business logic to the service layer. The extension layer should only handle parameter parsing and response formatting. +- **Validation**: Validate inputs before calling the service layer to provide clear error messages. diff --git a/arma/server/extension/config.example.toml b/arma/server/extension/config.example.toml new file mode 100644 index 0000000..2f0e061 --- /dev/null +++ b/arma/server/extension/config.example.toml @@ -0,0 +1,35 @@ +# Crate Server Configuration +# Copy this file to config.toml and modify as needed +# Place this file in the same directory as your crate_server_x64.dll + +[redis] +# Redis server connection settings +host = "127.0.0.1" +port = 6379 +db = 0 # Redis database number (0-15) + +# Optional authentication +# username = "your_username" +# password = "your_password" + +# Optional connection pool settings +max_connections = 10 # Maximum number of connections in pool +min_connections = 2 # Minimum number of idle connections +idle_timeout = 60 # Idle connection timeout in seconds + +# Example configurations for different environments: + +# Development (local Redis) +# host = "127.0.0.1" +# port = 6379 +# max_connections = 5 +# min_connections = 1 + +# Production (remote Redis with auth) +# host = "redis.example.com" +# port = 6379 +# username = "arma_server" +# password = "secure_password_here" +# max_connections = 20 +# min_connections = 5 +# idle_timeout = 30 diff --git a/arma/server/extension/src/actor.rs b/arma/server/extension/src/actor.rs new file mode 100644 index 0000000..892b268 --- /dev/null +++ b/arma/server/extension/src/actor.rs @@ -0,0 +1,308 @@ +//! Actor management operations for the Arma 3 server extension. +//! +//! Provides Arma 3 extension commands for player data storage, retrieval, and updates. +//! Handles SQF command mapping and parameter validation. + +use arma_rs::{CallContext, Group}; +use forge_repositories::RedisActorRepository; +use forge_services::ActorService; +use std::sync::LazyLock; + +use crate::adapters::ExtensionRedisClient; +use crate::helpers::resolve_uid; +use crate::log::log; + +/// Global actor service instance. +/// +/// Lazily initialized singleton combining Redis adapter, repository, and service layers. +static ACTOR_SERVICE: LazyLock>> = + LazyLock::new(|| { + let redis_client = ExtensionRedisClient::new(); + let repository = RedisActorRepository::new(redis_client); + ActorService::new(repository) + }); + +/// Creates the Arma 3 command group for actor operations. +/// +/// Registers commands: `get`, `exists`, `create`, `update`, `delete`. +pub fn group() -> Group { + Group::new() + .command("get", get_actor) + .command("exists", actor_exists) + .command("create", create_actor) + .command("update", update_actor) + .command("delete", delete_actor) +} + +/// Retrieves an actor by key/UID. +/// +/// Resolves the key to a Steam UID and returns the actor as JSON. +/// Returns an error message if resolution fails or retrieval fails. +pub fn get_actor(call_context: CallContext, key: String) -> String { + log("actor", "DEBUG", &format!("Getting actor for key: {}", key)); + + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => { + log("actor", "DEBUG", &format!("Resolved UID: {}", uid)); + uid + } + None => { + let error_msg = format!("Error: Failed to resolve UID for key: {}", key); + log("actor", "ERROR", &error_msg); + return error_msg; + } + }; + + match ACTOR_SERVICE.get_actor(resolved_uid.clone()) { + Ok(actor) => { + log( + "actor", + "INFO", + &format!("Successfully retrieved actor: {}", resolved_uid), + ); + match serde_json::to_string(&actor) { + Ok(json) => { + log( + "actor", + "DEBUG", + &format!("Serialized actor to JSON: {}", json), + ); + json + } + Err(e) => { + let error_msg = format!("Error: Failed to serialize actor: {}", e); + log("actor", "ERROR", &error_msg); + error_msg + } + } + } + Err(e) => { + let error_msg = format!("Error: {}", e); + log( + "actor", + "ERROR", + &format!("Failed to get actor '{}': {}", resolved_uid, e), + ); + error_msg + } + } +} + +/// Creates a new actor with the provided JSON data. +/// +/// Resolves key to UID, validates JSON data, and persists the new actor. +pub fn create_actor(call_context: CallContext, key: String, json_data: String) -> String { + log( + "actor", + "DEBUG", + &format!("Creating actor for key: {} with data: {}", key, json_data), + ); + + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => { + log( + "actor", + "DEBUG", + &format!("Resolved UID for creation: {}", uid), + ); + uid + } + None => { + let error_msg = format!("Error: Failed to resolve UID for key: {}", key); + log("actor", "ERROR", &error_msg); + return error_msg; + } + }; + + match ACTOR_SERVICE.create_actor(resolved_uid.clone(), json_data) { + Ok(actor) => { + log( + "actor", + "INFO", + &format!("Successfully created actor: {}", resolved_uid), + ); + match serde_json::to_string(&actor) { + Ok(json) => { + log( + "actor", + "DEBUG", + &format!("Serialized actor to JSON: {}", json), + ); + json + } + Err(e) => { + let error_msg = format!("Error: Failed to serialize actor: {}", e); + log("actor", "ERROR", &error_msg); + error_msg + } + } + } + Err(e) => { + let error_msg = format!("Error: {}", e); + log( + "actor", + "ERROR", + &format!("Failed to create actor '{}': {}", resolved_uid, e), + ); + error_msg + } + } +} + +/// Updates an existing actor with JSON data. +/// +/// Resolves key to UID, applies partial updates from JSON, and persists changes. +pub fn update_actor(call_context: CallContext, key: String, json_update: String) -> String { + log( + "actor", + "DEBUG", + &format!("Updating actor for key: {} with data: {}", key, json_update), + ); + + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => { + log( + "actor", + "DEBUG", + &format!("Resolved UID for update: {}", uid), + ); + uid + } + None => { + let error_msg = format!("Error: Failed to resolve UID for key: {}", key); + log("actor", "ERROR", &error_msg); + return error_msg; + } + }; + + match ACTOR_SERVICE.update_actor(resolved_uid.clone(), json_update) { + Ok(actor) => { + log( + "actor", + "INFO", + &format!("Successfully updated actor: {}", resolved_uid), + ); + match serde_json::to_string(&actor) { + Ok(json) => { + log( + "actor", + "DEBUG", + &format!("Serialized updated actor to JSON: {}", json), + ); + json + } + Err(e) => { + let error_msg = format!("Error: Failed to serialize actor: {}", e); + log("actor", "ERROR", &error_msg); + error_msg + } + } + } + Err(e) => { + let error_msg = format!("Error: {}", e); + log( + "actor", + "ERROR", + &format!("Failed to update actor '{}': {}", resolved_uid, e), + ); + error_msg + } + } +} + +/// Checks if an actor exists in the database. +/// +/// Returns "true" if the actor exists, "false" otherwise. +pub fn actor_exists(call_context: CallContext, key: String) -> String { + log( + "actor", + "DEBUG", + &format!("Checking if actor exists for key: {}", key), + ); + + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => { + log( + "actor", + "DEBUG", + &format!("Resolved UID for existence check: {}", uid), + ); + uid + } + None => { + log( + "actor", + "WARN", + &format!("Failed to resolve UID for key: {}", key), + ); + return "false".to_string(); + } + }; + + match ACTOR_SERVICE.actor_exists(resolved_uid.clone()) { + Ok(exists) => { + let result = if exists { "true" } else { "false" }; + log( + "actor", + "DEBUG", + &format!("Actor '{}' exists: {}", resolved_uid, result), + ); + result.to_string() + } + Err(e) => { + log( + "actor", + "ERROR", + &format!("Failed to check if actor '{}' exists: {}", resolved_uid, e), + ); + "false".to_string() + } + } +} + +/// Permanently deletes an actor. +/// +/// Resolves key to UID and removes the actor and associated data. +pub fn delete_actor(call_context: CallContext, key: String) -> String { + log( + "actor", + "DEBUG", + &format!("Deleting actor for key: {}", key), + ); + + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => { + log( + "actor", + "DEBUG", + &format!("Resolved UID for deletion: {}", uid), + ); + uid + } + None => { + let error_msg = format!("Error: Failed to resolve UID for key: {}", key); + log("actor", "ERROR", &error_msg); + return error_msg; + } + }; + + match ACTOR_SERVICE.delete_actor(resolved_uid.clone()) { + Ok(_) => { + log( + "actor", + "INFO", + &format!("Successfully deleted actor: {}", resolved_uid), + ); + "OK".to_string() + } + Err(e) => { + let error_msg = format!("Error: {}", e); + log( + "actor", + "ERROR", + &format!("Failed to delete actor '{}': {}", resolved_uid, e), + ); + error_msg + } + } +} diff --git a/arma/server/extension/src/adapters/README.md b/arma/server/extension/src/adapters/README.md new file mode 100644 index 0000000..311a2f4 --- /dev/null +++ b/arma/server/extension/src/adapters/README.md @@ -0,0 +1,261 @@ +# Adapters Module + +This module provides adapter implementations that bridge the repository layer with the extension's Redis operations. Adapters translate between the generic `RedisClient` trait and the extension-specific Redis module. + +## Architecture + +The adapters module follows the **Adapter Pattern**, allowing the repository layer to remain decoupled from the specific Redis implementation: + +```mermaid +graph TD + Repo[Repository Layer
#40;forge-repositories#41;] + Trait[RedisClient Trait
#40;forge-shared#41;] + Adapter[ExtensionRedisClient
#40;adapter#41;] + Redis[Redis Module
#40;extension#41;] + + Repo --> Trait + Trait --> Adapter + Adapter --> Redis +``` + +This design enables: +- **Testability**: Repositories can use mock adapters for testing +- **Flexibility**: Different Redis implementations can be swapped without changing repositories +- **Separation of Concerns**: Repository logic is independent of Redis connection details + +## ExtensionRedisClient + +The `ExtensionRedisClient` is the primary adapter that implements the `RedisClient` trait from `forge_shared`. + +### Responsibilities + +- **Translate Calls**: Convert trait method calls to Redis module function calls +- **Error Handling**: Parse Redis operation results and convert to `Result` types +- **Data Transformation**: Handle response parsing (e.g., JSON arrays for lists/sets) +- **Logging**: Log debug information for Redis operations + +### Implemented Operations + +#### Hash Operations + +| Method | Description | Returns | +|--------|-------------|---------| +| `hash_mset` | Set multiple fields atomically | `Result<(), String>` | +| `hash_get_all` | Get all fields and values | `Result` | +| `hash_get` | Get a single field value | `Result` | +| `hash_del` | Delete a field | `Result<(), String>` | + +#### List Operations + +| Method | Description | Returns | +|--------|-------------|---------| +| `list_rpush` | Append to list | `Result<(), String>` | +| `list_range` | Get range of elements | `Result, String>` | +| `list_del` | Remove by value | `Result<(), String>` | + +#### Set Operations + +| Method | Description | Returns | +|--------|-------------|---------| +| `set_add` | Add member | `Result<(), String>` | +| `set_members` | Get all members | `Result, String>` | +| `set_del` | Remove member | `Result<(), String>` | + +#### Common Operations + +| Method | Description | Returns | +|--------|-------------|---------| +| `key_exists` | Check if key exists | `Result` | +| `delete_key` | Delete key | `Result<(), String>` | + +### Usage Example + +```rust +use crate::adapters::ExtensionRedisClient; +use forge_shared::RedisClient; + +// Create the adapter +let client = ExtensionRedisClient::new(); + +// Use it with the RedisClient trait +let fields = vec![ + ("name".to_string(), "John".to_string()), + ("age".to_string(), "30".to_string()), +]; +client.hash_mset("user:123".to_string(), fields)?; + +// Retrieve data +let data = client.hash_get_all("user:123".to_string())?; +``` + +## Error Handling + +The adapter translates Redis string responses to Rust `Result` types: + +- **Success**: Returns `Ok(value)` with the appropriate type +- **Error**: Returns `Err(message)` if the response starts with "Error:" + +```rust +// Redis module returns "OK" → Adapter returns Ok(()) +// Redis module returns "Error: Connection failed" → Adapter returns Err("Error: Connection failed") +``` + +### Response Parsing + +For operations that return collections, the adapter parses JSON responses: + +```rust +// list_range returns JSON: ["item1", "item2", "item3"] +let items = client.list_range("mylist".to_string(), 0, -1)?; +// items: Vec = vec!["item1", "item2", "item3"] +``` + +## Contributing + +We welcome contributions to the adapters module! Follow these guidelines to add new adapter methods or create new adapters. + +### Adding a New Method to ExtensionRedisClient + +To add a new method (e.g., `hash_exists`), follow these steps: + +1. **Check the Trait**: Ensure the method is defined in the `RedisClient` trait in `forge_shared`. + ```rust + // In forge_shared/src/redis_client.rs + pub trait RedisClient: Send + Sync { + fn hash_exists(&self, key: String, field: String) -> Result; + } + ``` + +2. **Implement the Method**: Add the implementation to `ExtensionRedisClient`. + ```rust + impl RedisClient for ExtensionRedisClient { + fn hash_exists(&self, key: String, field: String) -> Result { + // Call the Redis module function + let result = redis::hash::hash_exists(key, field); + + // Parse the response + match result.as_str() { + "1" => Ok(true), + "0" => Ok(false), + _ if result.starts_with("Error:") => Err(result), + _ => Err(format!("Unexpected response: {}", result)), + } + } + } + ``` + +3. **Add Logging** (if needed): For debugging, log the operation. + ```rust + fn hash_exists(&self, key: String, field: String) -> Result { + let result = redis::hash::hash_exists(key, field); + log("debug", "DEBUG", &format!("hash_exists({}, {}): {}", key, field, result)); + + match result.as_str() { + "1" => Ok(true), + "0" => Ok(false), + _ if result.starts_with("Error:") => Err(result), + _ => Err(format!("Unexpected response: {}", result)), + } + } + ``` + +4. **Handle Response Types**: Match the return type to the trait signature. + - **Unit type** (`()`): Return `Ok(())` on success + - **Boolean**: Parse "1"/"0" to `true`/`false` + - **String**: Return the value directly + - **Vec**: Parse JSON array response + - **Number**: Parse string to number + +### Creating a New Adapter + +To create a new adapter (e.g., `MockRedisClient` for testing): + +1. **Create the Module File**: Add `src/adapters/mock_client.rs`. +2. **Define the Struct**: Create the adapter struct. + ```rust + use forge_shared::RedisClient; + use std::collections::HashMap; + use std::sync::RwLock; + + /// Mock Redis client for testing. + /// + /// Uses RwLock to allow multiple concurrent readers while maintaining thread safety. + pub struct MockRedisClient { + data: RwLock>, + } + + impl MockRedisClient { + pub fn new() -> Self { + Self { + data: RwLock::new(HashMap::new()), + } + } + } + ``` + +3. **Implement the Trait**: Implement all `RedisClient` methods. + ```rust + impl RedisClient for MockRedisClient { + fn hash_mset(&self, key: String, fields: Vec<(String, String)>) -> Result<(), String> { + // Acquire write lock only when modifying data + let mut data = self.data.write().unwrap(); + for (field, value) in fields { + let hash_key = format!("{}:{}", key, field); + data.insert(hash_key, value); + } + Ok(()) + } + + fn hash_get(&self, key: String, field: String) -> Result { + // Acquire read lock - multiple threads can read concurrently + let data = self.data.read().unwrap(); + let hash_key = format!("{}:{}", key, field); + Ok(data.get(&hash_key) + .map(|v| v.clone()) + .unwrap_or_default()) + } + + // ... implement other methods + } + ``` + +4. **Register the Module**: Add to `src/adapters/mod.rs`. + ```rust + pub mod redis_client; + pub mod mock_client; + + pub use redis_client::ExtensionRedisClient; + pub use mock_client::MockRedisClient; + ``` + +### Concurrency Best Practices + +> [!IMPORTANT] +> Choose the right synchronization primitive based on your access patterns and performance requirements. + +**Recommended Synchronization Primitives:** + +| Primitive | Use Case | Performance | Dependency | +|-----------|----------|-------------|------------| +| **`RwLock`** | Read-heavy workloads, concurrent readers | Good (multiple readers) | Standard library | +| **`Mutex`** | Write-heavy or exclusive access required | Fair (single lock) | Standard library | +| **`DashMap`** | Extreme high-frequency reads/writes | Excellent (lock-free) | External crate | + +**When to use each:** +- **`RwLock`**: Best for most use cases. Allows multiple concurrent readers, only blocks on writes. Use this by default. +- **`Mutex`**: Only when you need exclusive access or operations are very lightweight (< 1μs). +- **`DashMap`**: When profiling shows `RwLock` is a bottleneck and you need lock-free performance. + +**Why avoid `Mutex` for read-heavy workloads?** +- Blocks all threads (readers and writers) on every access +- No concurrent reads possible +- Can cause performance bottlenecks in high-concurrency scenarios + +### Best Practices + +- **Error Consistency**: Always check for "Error:" prefix in Redis responses +- **Type Safety**: Ensure return types match the trait signature exactly +- **Logging**: Log operations at DEBUG level for troubleshooting +- **Response Parsing**: Handle all possible response formats (success, error, unexpected) +- **Documentation**: Document the purpose and behavior of each method +- **Testing**: Test adapters with both success and error scenarios diff --git a/arma/server/extension/src/adapters/mod.rs b/arma/server/extension/src/adapters/mod.rs new file mode 100644 index 0000000..6fa6c1c --- /dev/null +++ b/arma/server/extension/src/adapters/mod.rs @@ -0,0 +1,3 @@ +pub mod redis_client; + +pub use redis_client::ExtensionRedisClient; diff --git a/arma/server/extension/src/adapters/redis_client.rs b/arma/server/extension/src/adapters/redis_client.rs new file mode 100644 index 0000000..298d963 --- /dev/null +++ b/arma/server/extension/src/adapters/redis_client.rs @@ -0,0 +1,148 @@ +use crate::log::log; +use crate::redis; +use forge_shared::RedisClient; + +/// Redis client implementation that bridges the repository layer with the extension's Redis module. +pub struct ExtensionRedisClient; + +impl ExtensionRedisClient { + /// Creates a new instance of the Redis client adapter. + pub fn new() -> Self { + Self + } +} + +impl RedisClient for ExtensionRedisClient { + /// Sets multiple fields in a Redis hash. + fn hash_mset(&self, key: String, fields: Vec<(String, String)>) -> Result<(), String> { + let result = redis::hash::hash_mset(key, fields); + if result == "OK" { Ok(()) } else { Err(result) } + } + + /// Retrieves all fields and values from a Redis hash. + fn hash_get_all(&self, key: String) -> Result { + let result = redis::hash::hash_get_all(key); + log("debug", "DEBUG", &result); + + if result.starts_with("Error:") { + Err(result) + } else { + Ok(result) + } + } + + /// Retrieves a single field value from a Redis hash. + fn hash_get(&self, key: String, field: String) -> Result { + let result = redis::hash::hash_get(key, field); + log("debug", "DEBUG", &result); + + if result.starts_with("Error:") { + Err(result) + } else { + Ok(result) + } + } + + /// Deletes a specific field from a Redis hash. + fn hash_del(&self, key: String, field: String) -> Result<(), String> { + let result = redis::hash::hash_del(key, field); + log("debug", "DEBUG", &result); + + if result.starts_with("Error:") { + Err(result) + } else { + Ok(()) + } + } + + /// Appends a value to the end of a Redis list. + fn list_rpush(&self, key: String, value: String) -> Result<(), String> { + let result = redis::list::list_rpush(key, value); + if result.starts_with("Error:") { + Err(result) + } else { + Ok(()) + } + } + + /// Retrieves a range of elements from a Redis list. + fn list_range(&self, key: String, start: isize, end: isize) -> Result, String> { + let result = redis::list::list_range(key, start, end); + log("debug", "DEBUG", &result); + + if result.starts_with("Error:") { + Err(result) + } else { + // Parse the JSON array response + match serde_json::from_str::>(&result) { + Ok(values) => Ok(values), + Err(e) => Err(format!("Failed to parse list response: {}", e)), + } + } + } + + /// Removes elements from a Redis list by value. + fn list_del(&self, key: String, count: isize, value: String) -> Result<(), String> { + let result = redis::list::list_del(key, count, value); + if result.starts_with("Error:") { + Err(result) + } else { + Ok(()) + } + } + + /// # Set operations + + /// Adds a member to a Redis set. + fn set_add(&self, key: String, member: String) -> Result<(), String> { + let result = redis::set::set_add(key, member); + if result.starts_with("Error:") { + Err(result) + } else { + Ok(()) + } + } + + /// Retrieves all members from a Redis set. + fn set_members(&self, key: String) -> Result, String> { + let result = redis::set::set_members(key); + if result.starts_with("Error:") { + Err(result) + } else { + match serde_json::from_str::>(&result) { + Ok(values) => Ok(values), + Err(e) => Err(format!("Failed to parse set members response: {}", e)), + } + } + } + + /// Removes a member from a Redis set. + fn set_del(&self, key: String, member: String) -> Result<(), String> { + let result = redis::set::set_del(key, member); + if result.starts_with("Error:") { + Err(result) + } else { + Ok(()) + } + } + + /// Checks if a Redis key exists. + fn key_exists(&self, key: String) -> Result { + let result = redis::common::key_exists(key); + match result.as_str() { + "1" => Ok(true), + "0" => Ok(false), + _ => Err(format!("Unexpected Redis response: {}", result)), + } + } + + /// Deletes a Redis key and all its associated data. + fn delete_key(&self, key: String) -> Result<(), String> { + let result = redis::common::delete_key(key); + if result.starts_with("Error:") { + Err(result) + } else { + Ok(()) + } + } +} diff --git a/arma/server/extension/src/bank.rs b/arma/server/extension/src/bank.rs new file mode 100644 index 0000000..46a67a2 --- /dev/null +++ b/arma/server/extension/src/bank.rs @@ -0,0 +1,304 @@ +//! Bank management operations for the Arma 3 server extension. +//! +//! Provides Arma 3 extension commands for player data storage, retrieval, and updates. +//! Handles SQF command mapping and parameter validation. + +use arma_rs::{CallContext, Group}; +use forge_repositories::RedisBankRepository; +use forge_services::BankService; +use std::sync::LazyLock; + +use crate::adapters::ExtensionRedisClient; +use crate::helpers::resolve_uid; +use crate::log::log; + +/// Global bank service instance. +/// +/// Lazily initialized singleton combining Redis adapter, repository, and service layers. +static BANK_SERVICE: LazyLock>> = + LazyLock::new(|| { + let redis_client = ExtensionRedisClient::new(); + let repository = RedisBankRepository::new(redis_client); + BankService::new(repository) + }); + +/// Creates the Arma 3 command group for bank operations. +/// +/// Registers commands: `get`, `exists`, `create`, `update`, `delete`. +pub fn group() -> Group { + Group::new() + .command("get", get_bank) + .command("exists", bank_exists) + .command("create", create_bank) + .command("update", update_bank) + .command("delete", delete_bank) +} + +/// Retrieves an bank by key/UID. +/// +/// Resolves the key to a Steam UID and returns the bank as JSON. +/// Returns an error message if resolution fails or retrieval fails. +pub fn get_bank(call_context: CallContext, key: String) -> String { + log("bank", "DEBUG", &format!("Getting bank for key: {}", key)); + + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => { + log("bank", "DEBUG", &format!("Resolved UID: {}", uid)); + uid + } + None => { + let error_msg = format!("Error: Failed to resolve UID for key: {}", key); + log("bank", "ERROR", &error_msg); + return error_msg; + } + }; + + match BANK_SERVICE.get_bank(resolved_uid.clone()) { + Ok(bank) => { + log( + "bank", + "INFO", + &format!("Successfully retrieved bank: {}", resolved_uid), + ); + match serde_json::to_string(&bank) { + Ok(json) => { + log( + "bank", + "DEBUG", + &format!("Serialized bank to JSON: {}", json), + ); + json + } + Err(e) => { + let error_msg = format!("Error: Failed to serialize bank: {}", e); + log("bank", "ERROR", &error_msg); + error_msg + } + } + } + Err(e) => { + let error_msg = format!("Error: {}", e); + log( + "bank", + "ERROR", + &format!("Failed to get bank '{}': {}", resolved_uid, e), + ); + error_msg + } + } +} + +/// Creates a new bank with the provided JSON data. +/// +/// Resolves key to UID, validates JSON data, and persists the new bank. +pub fn create_bank(call_context: CallContext, key: String, json_data: String) -> String { + log( + "bank", + "DEBUG", + &format!("Creating bank for key: {} with data: {}", key, json_data), + ); + + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => { + log( + "bank", + "DEBUG", + &format!("Resolved UID for creation: {}", uid), + ); + uid + } + None => { + let error_msg = format!("Error: Failed to resolve UID for key: {}", key); + log("bank", "ERROR", &error_msg); + return error_msg; + } + }; + + match BANK_SERVICE.create(resolved_uid.clone(), json_data) { + Ok(bank) => { + log( + "bank", + "INFO", + &format!("Successfully created bank: {}", resolved_uid), + ); + match serde_json::to_string(&bank) { + Ok(json) => { + log( + "bank", + "DEBUG", + &format!("Serialized bank to JSON: {}", json), + ); + json + } + Err(e) => { + let error_msg = format!("Error: Failed to serialize bank: {}", e); + log("bank", "ERROR", &error_msg); + error_msg + } + } + } + Err(e) => { + let error_msg = format!("Error: {}", e); + log( + "bank", + "ERROR", + &format!("Failed to create bank '{}': {}", resolved_uid, e), + ); + error_msg + } + } +} + +/// Updates an existing bank with JSON data. +/// +/// Resolves key to UID, applies partial updates from JSON, and persists changes. +pub fn update_bank(call_context: CallContext, key: String, json_update: String) -> String { + log( + "bank", + "DEBUG", + &format!("Updating bank for key: {} with data: {}", key, json_update), + ); + + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => { + log( + "bank", + "DEBUG", + &format!("Resolved UID for update: {}", uid), + ); + uid + } + None => { + let error_msg = format!("Error: Failed to resolve UID for key: {}", key); + log("bank", "ERROR", &error_msg); + return error_msg; + } + }; + + match BANK_SERVICE.update_bank(resolved_uid.clone(), json_update) { + Ok(bank) => { + log( + "bank", + "INFO", + &format!("Successfully updated bank: {}", resolved_uid), + ); + match serde_json::to_string(&bank) { + Ok(json) => { + log( + "bank", + "DEBUG", + &format!("Serialized updated bank to JSON: {}", json), + ); + json + } + Err(e) => { + let error_msg = format!("Error: Failed to serialize bank: {}", e); + log("bank", "ERROR", &error_msg); + error_msg + } + } + } + Err(e) => { + let error_msg = format!("Error: {}", e); + log( + "bank", + "ERROR", + &format!("Failed to update bank '{}': {}", resolved_uid, e), + ); + error_msg + } + } +} + +/// Checks if an bank exists in the database. +/// +/// Returns "true" if the bank exists, "false" otherwise. +pub fn bank_exists(call_context: CallContext, key: String) -> String { + log( + "bank", + "DEBUG", + &format!("Checking if bank exists for key: {}", key), + ); + + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => { + log( + "bank", + "DEBUG", + &format!("Resolved UID for existence check: {}", uid), + ); + uid + } + None => { + log( + "bank", + "WARN", + &format!("Failed to resolve UID for key: {}", key), + ); + return "false".to_string(); + } + }; + + match BANK_SERVICE.bank_exists(resolved_uid.clone()) { + Ok(exists) => { + let result = if exists { "true" } else { "false" }; + log( + "bank", + "DEBUG", + &format!("Bank '{}' exists: {}", resolved_uid, result), + ); + result.to_string() + } + Err(e) => { + log( + "bank", + "ERROR", + &format!("Failed to check if bank '{}' exists: {}", resolved_uid, e), + ); + "false".to_string() + } + } +} + +/// Permanently deletes an bank. +/// +/// Resolves key to UID and removes the bank and associated data. +pub fn delete_bank(call_context: CallContext, key: String) -> String { + log("bank", "DEBUG", &format!("Deleting bank for key: {}", key)); + + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => { + log( + "bank", + "DEBUG", + &format!("Resolved UID for deletion: {}", uid), + ); + uid + } + None => { + let error_msg = format!("Error: Failed to resolve UID for key: {}", key); + log("bank", "ERROR", &error_msg); + return error_msg; + } + }; + + match BANK_SERVICE.delete_bank(resolved_uid.clone()) { + Ok(_) => { + log( + "bank", + "INFO", + &format!("Successfully deleted bank: {}", resolved_uid), + ); + "OK".to_string() + } + Err(e) => { + let error_msg = format!("Error: {}", e); + log( + "bank", + "ERROR", + &format!("Failed to delete bank '{}': {}", resolved_uid, e), + ); + error_msg + } + } +} diff --git a/arma/server/extension/src/helpers.rs b/arma/server/extension/src/helpers.rs new file mode 100644 index 0000000..dfd6b6d --- /dev/null +++ b/arma/server/extension/src/helpers.rs @@ -0,0 +1,12 @@ +use arma_rs::CallContext; + +pub fn resolve_uid(uid: &str, call_context: &CallContext) -> Option { + if !uid.is_empty() && uid != "_SP_PLAYER_" { + return Some(uid.to_string()); + } + + match call_context.caller() { + arma_rs::Caller::Steam(steam_id) => Some(steam_id.to_string()), + arma_rs::Caller::Unknown => None, + } +} diff --git a/arma/server/extension/src/lib.rs b/arma/server/extension/src/lib.rs new file mode 100644 index 0000000..e780192 --- /dev/null +++ b/arma/server/extension/src/lib.rs @@ -0,0 +1,106 @@ +//! Entry point and runtime bootstrap for the Forge Arma server extension. +//! +//! Initializes a global async runtime, the Redis connection pool, and registers +//! all command groups. Provides status/version commands and maintains a shared +//! Arma `Context` for engine interop. +//! +use arma_rs::{Context, Extension, arma}; +use std::sync::{LazyLock, OnceLock, RwLock as StdRwLock}; +use tokio::runtime::{Builder, Runtime}; +use tokio::sync::RwLock as TokioRwLock; + +pub mod actor; +pub mod adapters; +pub mod bank; +pub mod helpers; +mod log; +pub mod org; +pub mod redis; + +/// Global Arma `Context` captured at initialization and made available to +/// commands that need engine interop. Stored inside an async `RwLock` to +/// allow mutation by the startup task and later reads. +static CONTEXT: LazyLock>> = LazyLock::new(|| TokioRwLock::new(None)); +/// Global Redis connection pool, created once and shared by all commands. +/// Initialized asynchronously after `init()` returns so the extension starts +/// quickly without blocking the main thread. +static REDIS_POOL: OnceLock = OnceLock::new(); +/// Global multi-threaded Tokio runtime used to execute async operations from +/// command handlers and startup tasks. +static RUNTIME: LazyLock = LazyLock::new(|| { + Builder::new_multi_thread() + .enable_all() + .build() + .expect("Failed to create tokio runtime") +}); + +#[derive(Clone, Copy, PartialEq)] +/// Connection state for the Redis pool so SQF can gate behavior on readiness. +enum ConnectionState { + Initializing, + Connected, + Failed, +} +static CONNECTION_STATE: LazyLock> = + LazyLock::new(|| StdRwLock::new(ConnectionState::Initializing)); + +#[arma] +/// Initializes the extension, registers commands/groups, and asynchronously +/// creates the Redis connection pool on the global runtime. +fn init() -> Extension { + let config = redis::config::load(); + let ext = Extension::build() + .command("version", get_version) + .command("status", get_status) + .group("redis", redis::group()) + .group("actor", actor::group()) + .group("bank", bank::group()) + .group("org", org::group()) + .finish(); + + let ctx = ext.context(); + + // Spawn the connection task on the runtime. + // This returns immediately, allowing Arma to continue. + RUNTIME.spawn(async move { + // Store context immediately if needed, or pass it to the connection logic + *CONTEXT.write().await = Some(ctx); + + let pool_result = redis::client::create_redis_pool(&config.redis).await; + let pool = match pool_result { + Ok(pool) => pool, + Err(_e) => { + // Log error, maybe try default config, or set state to Failed + let default_config = redis::RedisConfig::default(); + redis::client::create_redis_pool(&default_config) + .await + .expect("Failed to create default Redis pool") + } + }; + + if REDIS_POOL.set(pool).is_ok() { + *CONNECTION_STATE.write().unwrap() = ConnectionState::Connected; + } else { + *CONNECTION_STATE.write().unwrap() = ConnectionState::Failed; + } + }); + + ext +} + +/// Returns current Redis connection state as a string: `initializing`, +/// `connected`, or `failed`. Intended for SQF polling before issuing +/// operations that require Redis. +fn get_status() -> String { + let state = *CONNECTION_STATE.read().unwrap(); + match state { + ConnectionState::Initializing => "initializing".into(), + ConnectionState::Connected => "connected".into(), + ConnectionState::Failed => "failed".into(), + } +} + +/// Returns the extension version string for diagnostics and tooling. +pub fn get_version() -> String { + format!("forge-server v{}", env!("CARGO_PKG_VERSION")) +} diff --git a/arma/server/extension/src/lib.rs.bak b/arma/server/extension/src/lib.rs.bak new file mode 100644 index 0000000..68937f5 --- /dev/null +++ b/arma/server/extension/src/lib.rs.bak @@ -0,0 +1,54 @@ +use arma_rs::{Context, Extension, arma}; +use std::sync::{LazyLock, OnceLock}; +use tokio::runtime::{Builder, Runtime}; +use tokio::sync::RwLock; + +pub mod actor; +pub mod adapters; +pub mod helpers; +mod log; +pub mod org; +pub mod redis; + +static CONTEXT: LazyLock>> = LazyLock::new(|| RwLock::new(None)); +static REDIS_POOL: OnceLock = OnceLock::new(); +static RUNTIME: LazyLock = LazyLock::new(|| { + Builder::new_multi_thread() + .enable_all() + .build() + .expect("Failed to create tokio runtime") +}); + +#[arma] +fn init() -> Extension { + let config = redis::config::load(); + let ext = Extension::build() + .command("version", get_version) + .group("redis", redis::group()) + .group("actor", actor::group()) + .group("org", org::group()) + .finish(); + + let ctx = ext.context(); + + RUNTIME.block_on(async { + let pool = match redis::client::create_redis_pool(&config.redis).await { + Ok(pool) => pool, + Err(_) => { + let default_config = redis::RedisConfig::default(); + redis::client::create_redis_pool(&default_config) + .await + .expect("Failed to create Redis pool") + } + }; + + REDIS_POOL.set(pool).ok(); + *CONTEXT.write().await = Some(ctx); + }); + + ext +} + +pub fn get_version() -> String { + format!("forge-server v{}", env!("CARGO_PKG_VERSION")) +} diff --git a/arma/server/extension/src/log.rs b/arma/server/extension/src/log.rs new file mode 100644 index 0000000..8ec4660 --- /dev/null +++ b/arma/server/extension/src/log.rs @@ -0,0 +1,49 @@ +#![allow(dead_code)] + +use std::collections::HashMap; +use std::fs::{File, OpenOptions, create_dir_all}; +use std::io::Write; +use std::path::Path; +use std::sync::LazyLock; +use std::sync::Mutex; + +static LOG_FILES: LazyLock>> = LazyLock::new(|| { + let logs_dir = Path::new("@forge_server/logs"); + create_dir_all(logs_dir).expect("Failed to create logs directory"); + Mutex::new(HashMap::new()) +}); + +/// Generic logging function that creates log files on-demand. +/// +/// # Arguments +/// * `category` - The log category (e.g., "actor", "org", "vehicle") +/// * `level` - The log level (e.g., "INFO", "DEBUG", "WARN", "ERROR") +/// * `message` - The message to log +/// +/// # Example +/// ``` +/// log("actor", "INFO", "Actor created successfully"); +/// log("vehicle", "ERROR", "Failed to spawn vehicle"); +/// ``` +pub fn log(category: &str, level: &str, message: &str) { + let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"); + let log_entry = format!("[{}] [{}] {}\n", timestamp, level, message); + + if let Ok(mut files) = LOG_FILES.lock() { + // Get or create the log file for this category + let file = files.entry(category.to_string()).or_insert_with(|| { + let logs_dir = Path::new("@forge_server/logs"); + let filename = format!("{}.log", category); + let path = logs_dir.join(filename); + + OpenOptions::new() + .create(true) + .append(true) + .open(path) + .expect(&format!("Failed to open {} log file", category)) + }); + + let _ = file.write_all(log_entry.as_bytes()); + let _ = file.flush(); + } +} diff --git a/arma/server/extension/src/org.rs b/arma/server/extension/src/org.rs new file mode 100644 index 0000000..3f42545 --- /dev/null +++ b/arma/server/extension/src/org.rs @@ -0,0 +1,189 @@ +//! Organization management operations for the Arma 3 server extension. +//! +//! Provides Arma 3 extension commands for organization data storage, retrieval, and updates. +//! Handles SQF command mapping and parameter validation. + +use arma_rs::Group; +use forge_repositories::RedisOrgRepository; +use forge_services::OrgService; +use std::sync::LazyLock; + +use crate::adapters::ExtensionRedisClient; +use crate::log::log; + +/// Global organization service instance. +/// +/// Lazily initialized singleton combining Redis adapter, repository, and service layers. +static ORG_SERVICE: LazyLock>> = + LazyLock::new(|| { + let redis_client = ExtensionRedisClient::new(); + let repository = RedisOrgRepository::new(redis_client); + OrgService::new(repository) + }); + +/// Creates the Arma 3 command group for organization operations. +/// +/// Registers commands: `get`, `get_members`, `add_member`, `remove_member`, `exists`, `create`, `update`, `delete`. +pub fn group() -> Group { + Group::new() + .command("get", get_org) + .command("get_members", get_members) + .command("add_member", add_member) + .command("remove_member", remove_member) + .command("exists", org_exists) + .command("create", create_org) + .command("update", update_org) + .command("delete", delete_org) +} + +/// Retrieves an organization by key/ID. +/// +/// Returns the organization as JSON or an error message if not found. +pub fn get_org(key: String) -> String { + log( + "org", + "DEBUG", + &format!("Getting organization for key: {}", key), + ); + + match ORG_SERVICE.get_org(key.clone()) { + Ok(org) => { + log( + "org", + "INFO", + &format!("Successfully retrieved organization: {}", key), + ); + match serde_json::to_string(&org) { + Ok(json) => { + log("org", "DEBUG", &format!("Serialized org to JSON: {}", json)); + json + } + Err(e) => { + let error_msg = format!("Error: Failed to serialize org: {}", e); + log("org", "ERROR", &error_msg); + error_msg + } + } + } + Err(e) => { + let error_msg = format!("Error: {}", e); + log( + "org", + "ERROR", + &format!("Failed to get organization '{}': {}", key, e), + ); + error_msg + } + } +} + +/// Retrieves organization members as a JSON object. +/// +/// Returns a map of member UIDs to names. Returns empty object if not found. +pub fn get_members(key: String) -> String { + match ORG_SERVICE.get_members(key) { + Ok(members) => match serde_json::to_string(&members) { + Ok(json) => json, + Err(_) => "{}".to_string(), + }, + Err(_) => "{}".to_string(), + } +} + +/// Creates a new organization with the provided JSON data. +/// +/// Resolves key to ID, validates JSON data, and persists the new organization. +pub fn create_org(key: String, json_data: String) -> String { + log( + "org", + "DEBUG", + &format!( + "Creating organization for key: {} with data: {}", + key, json_data + ), + ); + + match ORG_SERVICE.create_org(key.clone(), json_data) { + Ok(org) => { + log( + "org", + "INFO", + &format!("Successfully created organization: {}", key), + ); + match serde_json::to_string(&org) { + Ok(json) => { + log("org", "DEBUG", &format!("Serialized org to JSON: {}", json)); + json + } + Err(e) => { + let error_msg = format!("Error: Failed to serialize org: {}", e); + log("org", "ERROR", &error_msg); + error_msg + } + } + } + Err(e) => { + let error_msg = format!("Error: {}", e); + log( + "org", + "ERROR", + &format!("Failed to create organization '{}': {}", key, e), + ); + error_msg + } + } +} + +/// Updates an existing organization with JSON data. +/// +/// Resolves key to ID, applies partial updates from JSON, and persists changes. +pub fn update_org(key: String, json_update: String) -> String { + match ORG_SERVICE.update_org(key, json_update) { + Ok(org) => match serde_json::to_string(&org) { + Ok(json) => json, + Err(e) => format!("Error: Failed to serialize org: {}", e), + }, + Err(e) => format!("Error: {}", e), + } +} + +/// Checks if an organization exists in the database. +/// +/// Returns "true" if the organization exists, "false" otherwise. +pub fn org_exists(key: String) -> String { + match ORG_SERVICE.org_exists(key) { + Ok(exists) => if exists { "true" } else { "false" }.to_string(), + Err(_) => "false".to_string(), + } +} + +/// Permanently deletes an organization. +/// +/// Resolves key to ID and removes the organization and associated data. +pub fn delete_org(key: String) -> String { + match ORG_SERVICE.delete_org(key) { + Ok(_) => "OK".to_string(), + Err(e) => format!("Error: {}", e), + } +} + +/// Adds a new member to an organization by their UID. +/// +/// Resolves organization key to ID and adds the member UID. +/// Redis sets automatically prevent duplicate members. +pub fn add_member(key: String, member_uid: String) -> String { + match ORG_SERVICE.add_member(key, member_uid) { + Ok(_) => "OK".to_string(), + Err(e) => format!("Error: {}", e), + } +} + +/// Removes a member from an organization by their UID. +/// +/// Resolves organization key to ID and removes the member UID. +pub fn remove_member(key: String, member_uid: String) -> String { + match ORG_SERVICE.remove_member(key, member_uid) { + Ok(_) => "OK".to_string(), + Err(e) => format!("Error: {}", e), + } +} diff --git a/arma/server/extension/src/redis/README.md b/arma/server/extension/src/redis/README.md new file mode 100644 index 0000000..7183ace --- /dev/null +++ b/arma/server/extension/src/redis/README.md @@ -0,0 +1,278 @@ +# Redis Module + +This module provides comprehensive Redis operations for the Forge extension, enabling persistent data storage and retrieval from SQF scripts. + +## Architecture + +The Redis module is organized into specialized operation groups: + +- **Common**: Basic key-value operations +- **Hash**: Structured data storage (field-value pairs) +- **List**: Ordered collections and queues +- **Set**: Unique collections and membership tracking + +## Connection Management + +### Connection Pool + +The module uses `bb8` for connection pooling, providing: +- **Automatic connection reuse**: Reduces overhead +- **Configurable pool size**: Control max/min connections +- **Idle timeout**: Prevents stale connections +- **Lazy initialization**: Pool created on first use + +### Configuration + +Redis connection settings are loaded from `@forge_server/config.toml`: + +```toml +[redis] +host = "127.0.0.1" +port = 6379 +password = "" # Optional +max_connections = 10 +min_connections = 2 +idle_timeout = 300 # seconds +``` + +## Common Operations + +Basic key-value operations for simple data storage. + +### Available Commands + +| Command | Description | Returns | +|---------|-------------|---------| +| `redis:common:set` | Set a string value | "OK" | +| `redis:common:get` | Get a string value | Value or empty string | +| `redis:common:incr` | Increment a numeric value | New value | +| `redis:common:decr` | Decrement a numeric value | New value | +| `redis:common:del` | Delete a key | Number of keys removed | +| `redis:common:keys` | List all keys | Comma-separated keys | + +### SQF Examples + +```sqf +// Set a value +"forge_server" callExtension ["redis:common:set", ["player_count", "42"]]; + +// Get a value +private _result = "forge_server" callExtension ["redis:common:get", ["player_count"]]; +private _count = _result select 0; // "42" + +// Increment +"forge_server" callExtension ["redis:common:incr", ["player_count", 1]]; + +// Delete +"forge_server" callExtension ["redis:common:del", ["player_count"]]; +``` + +## Hash Operations + +Hash operations store structured data as field-value pairs, ideal for objects and entities. + +### Available Commands + +| Command | Description | Returns | +|---------|-------------|---------| +| `redis:hash:set` | Set a single field | 1 if new, 0 if updated | +| `redis:hash:mset` | Set multiple fields atomically | "OK" | +| `redis:hash:get` | Get a field value | Value or empty string | +| `redis:hash:getall` | Get all fields and values | Comma-separated pairs | +| `redis:hash:del` | Delete a field | Number of fields removed | +| `redis:hash:keys` | Get all field names | Comma-separated keys | +| `redis:hash:vals` | Get all values | Comma-separated values | +| `redis:hash:len` | Get number of fields | Field count | +| `redis:hash:exists` | Check if field exists | "1" or "0" | + +### SQF Examples + +```sqf +// Set a single field +"forge_server" callExtension ["redis:hash:set", ["actor:76561198123456789", "name", "John Doe"]]; + +// Set multiple fields atomically +private _fields = [ + ["name", "John Doe"], + ["bank", "1000"], + ["level", "5"] +]; +"forge_server" callExtension ["redis:hash:mset", ["actor:76561198123456789", _fields]]; + +// Get a field +private _result = "forge_server" callExtension ["redis:hash:get", ["actor:76561198123456789", "name"]]; +private _name = _result select 0; // "John Doe" + +// Get all fields +private _result = "forge_server" callExtension ["redis:hash:getall", ["actor:76561198123456789"]]; +// Returns: "name, John Doe, bank, 1000, level, 5" + +// Check if field exists +private _result = "forge_server" callExtension ["redis:hash:exists", ["actor:76561198123456789", "name"]]; +private _exists = (_result select 0) == "1"; +``` + +## List Operations + +List operations manage ordered collections, useful for queues, logs, and sequential data. + +### Available Commands + +| Command | Description | Returns | +|---------|-------------|---------| +| `redis:list:set` | Set element at index | "OK" | +| `redis:list:get` | Get element at index | Value (base64 decoded) | +| `redis:list:len` | Get list length | Element count | +| `redis:list:range` | Get range of elements | JSON array | +| `redis:list:lpush` | Prepend to list | New length | +| `redis:list:rpush` | Append to list | New length | +| `redis:list:lpop` | Remove from beginning | JSON array of removed elements | +| `redis:list:rpop` | Remove from end | JSON array of removed elements | +| `redis:list:trim` | Trim to range | "OK" | +| `redis:list:del` | Remove by value | Number removed | + +### SQF Examples + +```sqf +// Append to list +"forge_server" callExtension ["redis:list:rpush", ["event_log", "Player joined"]]; +"forge_server" callExtension ["redis:list:rpush", ["event_log", "Player spawned"]]; + +// Get range +private _result = "forge_server" callExtension ["redis:list:range", ["event_log", 0, -1]]; +private _events = parseJSON (_result select 0); // Array of all events + +// Pop from end +private _result = "forge_server" callExtension ["redis:list:rpop", ["event_log", 1]]; +private _lastEvent = parseJSON (_result select 0); // ["Player spawned"] + +// Trim to last 100 entries +"forge_server" callExtension ["redis:list:trim", ["event_log", -100, -1]]; +``` + +> [!NOTE] +> List values are automatically base64 encoded/decoded to handle special characters safely. + +## Set Operations + +Set operations manage unique collections, perfect for membership tracking and preventing duplicates. + +### Available Commands + +| Command | Description | Returns | +|---------|-------------|---------| +| `redis:set:add` | Add member to set | 1 if new, 0 if exists | +| `redis:set:members` | Get all members | Comma-separated members | +| `redis:set:card` | Get member count | Cardinality | +| `redis:set:ismember` | Check membership | "1" or "0" | +| `redis:set:randmember` | Get random member | Member value | +| `redis:set:randmembers` | Get N random members | Comma-separated members | +| `redis:set:pop` | Remove random member | Removed member | +| `redis:set:del` | Remove specific member | 1 if removed, 0 if not found | + +### SQF Examples + +```sqf +// Add members to a set +"forge_server" callExtension ["redis:set:add", ["org:elite_squad:members", "76561198123456789"]]; +"forge_server" callExtension ["redis:set:add", ["org:elite_squad:members", "76561198987654321"]]; + +// Check membership +private _result = "forge_server" callExtension ["redis:set:ismember", ["org:elite_squad:members", "76561198123456789"]]; +private _isMember = (_result select 0) == "1"; + +// Get all members +private _result = "forge_server" callExtension ["redis:set:members", ["org:elite_squad:members"]]; +private _memberUIDs = (_result select 0) splitString ","; + +// Get member count +private _result = "forge_server" callExtension ["redis:set:card", ["org:elite_squad:members"]]; +private _memberCount = parseNumber (_result select 0); + +// Remove member +"forge_server" callExtension ["redis:set:del", ["org:elite_squad:members", "76561198123456789"]]; +``` + +## Helper Utilities + +### Base64 Encoding + +List operations use base64 encoding to safely store complex strings: + +```rust +use crate::redis::helpers::{encode_b64, decode_b64}; + +let encoded = encode_b64("Complex [string] with {special} chars"); +let decoded = decode_b64(&encoded)?; // Original string +``` + +### Value Parsing + +The `parse_redis_value` function intelligently converts Redis strings to JSON types: + +```rust +use crate::redis::helpers::parse_redis_value; + +parse_redis_value("42"); // Number(42) +parse_redis_value("true"); // Bool(true) +parse_redis_value("{\"key\":1}"); // Object +parse_redis_value("text"); // String("text") +``` + +## Macro Usage + +The `redis_operation!` macro handles all connection and async boilerplate: + +```rust +use crate::redis_operation; +use bb8_redis::redis::AsyncCommands; + +pub fn my_redis_command(key: String) -> String { + redis_operation!(conn => { + match conn.get::<_, String>(&key).await { + Ok(value) => value, + Err(e) => format!("Error: {}", e), + } + }) +} +``` + +The macro automatically: +- Acquires a connection from the pool +- Handles lazy initialization if needed +- Executes the operation asynchronously +- Returns the result to SQF + +## Error Handling + +All Redis operations return strings: +- **Success**: Operation result (e.g., "OK", value, count) +- **Error**: String starting with "Error: " followed by the error message + +```sqf +private _result = "forge_server" callExtension ["redis:common:get", ["mykey"]]; +private _value = _result select 0; + +if (_value find "Error:" == 0) then { + diag_log format ["Redis error: %1", _value]; +} else { + // Use the value + systemChat format ["Value: %1", _value]; +}; +``` + +## Performance Considerations + +- **Connection Pooling**: Reuses connections to minimize overhead +- **Async Operations**: Non-blocking I/O prevents server lag +- **Atomic Operations**: `hash:mset` sets multiple fields in one operation +- **Batch Operations**: Use lists and sets for bulk data + +## Best Practices + +1. **Use Hashes for Objects**: Store actor/org data as hash fields +2. **Use Sets for Membership**: Track organization members, online players +3. **Use Lists for Logs**: Event logs, chat history, audit trails +4. **Prefix Keys**: Use namespaces like `actor:`, `org:`, `vehicle:` +5. **Handle Errors**: Always check for "Error:" prefix in results +6. **Atomic Updates**: Use `hash:mset` instead of multiple `hash:set` calls diff --git a/arma/server/extension/src/redis/client.rs b/arma/server/extension/src/redis/client.rs new file mode 100644 index 0000000..3067ac9 --- /dev/null +++ b/arma/server/extension/src/redis/client.rs @@ -0,0 +1,43 @@ +use super::config::RedisConfig; +use bb8_redis::{RedisConnectionManager, bb8}; +use std::error::Error; + +/// Redis connection pool type alias. +pub type RedisClient = bb8::Pool; + +/// Creates a Redis connection pool with the specified configuration. +pub async fn create_redis_pool( + config: &RedisConfig, +) -> Result> { + // Generate the Redis connection string from configuration + let connection_string = config.connection_string(); + + // Create the connection manager that will handle individual connections + let manager = RedisConnectionManager::new(connection_string)?; + + // Start building the connection pool with default settings + let mut pool_builder = bb8::Pool::builder(); + + // Configure maximum number of connections if specified + // This prevents overwhelming the Redis server with too many connections + if let Some(max_conn) = config.max_connections { + pool_builder = pool_builder.max_size(max_conn as u32); + } + + // Configure minimum idle connections if specified + // This ensures quick response times by keeping connections ready + if let Some(min_conn) = config.min_connections { + pool_builder = pool_builder.min_idle(Some(min_conn as u32)); + } + + // Configure idle connection timeout if specified + // This prevents keeping stale connections that might be closed by the server + if let Some(idle_timeout) = config.idle_timeout { + use std::time::Duration; + pool_builder = pool_builder.idle_timeout(Some(Duration::from_secs(idle_timeout))); + } + + // Build the final connection pool with all configured parameters + let pool = pool_builder.build(manager).await?; + Ok(pool) +} diff --git a/arma/server/extension/src/redis/common.rs b/arma/server/extension/src/redis/common.rs new file mode 100644 index 0000000..4ddce19 --- /dev/null +++ b/arma/server/extension/src/redis/common.rs @@ -0,0 +1,74 @@ +//! Common Redis operations for basic key-value functionality. + +use crate::redis_operation; +use bb8_redis::redis::AsyncCommands; + +/// Sets a string value for the specified Redis key. +pub fn set_key(key: String, value: String) -> String { + redis_operation!(conn => { + match conn.set(&key, &value).await { + Ok(()) => "OK".to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Retrieves the string value for the specified Redis key. +pub fn get_key(key: String) -> String { + redis_operation!(conn => { + match conn.get::<_, String>(&key).await { + Ok(value) => value, + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Increments a numeric value stored at the specified key. +pub fn incr_key(key: String, count: usize) -> String { + redis_operation!(conn => { + match conn.incr::<_, _, i64>(&key, count).await { + Ok(value) => value.to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Decrements a numeric value stored at the specified key. +pub fn decr_key(key: String, count: usize) -> String { + redis_operation!(conn => { + match conn.decr::<_, _, i64>(&key, count).await { + Ok(value) => value.to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Checks if a Redis key exists. +pub fn key_exists(key: String) -> String { + redis_operation!(conn => { + match conn.exists::<_, i32>(&key).await { + Ok(exists) => exists.to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Deletes a Redis key and its associated value. +pub fn delete_key(key: String) -> String { + redis_operation!(conn => { + match conn.del::<_, usize>(&key).await { + Ok(removed) => removed.to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Lists all Redis keys matching the wildcard pattern "*". +pub fn list_keys() -> String { + redis_operation!(conn => { + match conn.keys::<_, Vec>("*").await { + Ok(keys) => keys.join(","), + Err(e) => format!("Error: {}", e), + } + }) +} diff --git a/arma/server/extension/src/redis/config.rs b/arma/server/extension/src/redis/config.rs new file mode 100644 index 0000000..eaadfa5 --- /dev/null +++ b/arma/server/extension/src/redis/config.rs @@ -0,0 +1,122 @@ +//! Configuration management for Redis connection and application settings. + +use serde::Deserialize; +use std::fs; +use std::path::PathBuf; + +use crate::log::log; + +/// Main configuration structure for the entire application. +#[derive(Debug, Clone, Deserialize)] +pub struct Config { + /// Redis configuration with automatic defaults if not specified + #[serde(default)] + pub redis: RedisConfig, +} + +impl Default for Config { + /// Creates a default configuration with sensible values for development. + fn default() -> Self { + Self { + redis: RedisConfig::default(), + } + } +} + +/// Redis connection and connection pool configuration. +#[derive(Debug, Clone, Deserialize)] +pub struct RedisConfig { + /// Redis server hostname or IP address + pub host: String, + /// Redis server port number + pub port: u16, + /// Redis database number (0-15) + pub db: u8, + /// Username for Redis ACL authentication (Redis 6.0+) + pub username: Option, + /// Password for Redis authentication + pub password: Option, + /// Maximum number of connections in the pool + pub max_connections: Option, + /// Minimum number of idle connections to maintain + pub min_connections: Option, + /// Idle connection timeout in seconds + pub idle_timeout: Option, +} + +impl Default for RedisConfig { + /// Creates default Redis configuration suitable for local development. + fn default() -> Self { + Self { + host: "127.0.0.1".to_string(), + port: 6379, + db: 0, + username: None, + password: None, + max_connections: Some(10), + min_connections: Some(2), + idle_timeout: Some(60), + } + } +} + +impl RedisConfig { + /// Generates a Redis connection string from the configuration. + pub fn connection_string(&self) -> String { + // Build authentication part of the URL + let auth_part = match (&self.username, &self.password) { + (Some(username), Some(password)) => format!("{}:{}@", username, password), + (None, Some(password)) => format!(":{}@", password), + (Some(username), None) => format!("{}@", username), + (None, None) => String::new(), + }; + + let mut conn_str = format!("redis://{}{}", auth_part, self.host); + + if self.port != 6379 { + conn_str.push_str(&format!(":{}", self.port)); + } + + if self.db != 0 { + conn_str.push_str(&format!("/{}", self.db)); + } + + log( + "main", + "INFO", + &format!("Redis connection string: {}", conn_str), + ); + + conn_str + } +} + +/// Loads configuration from the `config.toml` file with graceful fallback to defaults. +pub fn load() -> Config { + let config_path = std::env::current_exe() + .ok() + .and_then(|exe| { + exe.parent() + .map(|dir| dir.join("@forge_server").join("config.toml")) + }) + .filter(|p| p.exists()) + .unwrap_or_else(|| PathBuf::from("@forge_server/config.toml")); + + match fs::read_to_string(&config_path) { + Ok(contents) => { + log("main", "INFO", &format!("Config file found! Loading...")); + match toml::from_str::(&contents) { + Ok(config) => config, + Err(_) => Config::default(), + } + } + Err(_) => { + log( + "main", + "INFO", + &format!("Config file not found. Using default configuration."), + ); + Config::default() + } + } +} diff --git a/arma/server/extension/src/redis/hash.rs b/arma/server/extension/src/redis/hash.rs new file mode 100644 index 0000000..8b53240 --- /dev/null +++ b/arma/server/extension/src/redis/hash.rs @@ -0,0 +1,102 @@ +//! Redis hash operations for structured data storage. + +use crate::redis_operation; +use bb8_redis::redis::AsyncCommands; +use std::collections::HashMap; + +/// Sets a single field in a Redis hash. +pub fn hash_set(key: String, field: String, value: String) -> String { + redis_operation!(conn => { + match conn.hset::<_, _, _, i32>(&key, &field, &value).await { + Ok(added) => added.to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Sets multiple fields in a Redis hash atomically. +pub fn hash_mset(key: String, items: Vec<(String, String)>) -> String { + redis_operation!(conn => { + match conn.hset_multiple(&key, &items).await { + Ok(()) => "OK".to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Retrieves the value of a specific field from a Redis hash. +pub fn hash_get(key: String, field: String) -> String { + redis_operation!(conn => { + match conn.hget::<_, _, Option>(&key, &field).await { + Ok(Some(value)) => value, + Ok(None) => String::new(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Retrieves all fields and values from a Redis hash. +pub fn hash_get_all(key: String) -> String { + redis_operation!(conn => { + match conn.hgetall::<_, HashMap>(&key).await { + Ok(hash_map) => { + let formatted_pairs: Vec = hash_map + .iter() + .map(|(k, v)| format!("{}, {}", k, v)) + .collect(); + formatted_pairs.join(", ") + } + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Removes a field from a Redis hash. +pub fn hash_del(key: String, field: String) -> String { + redis_operation!(conn => { + match conn.hdel::<_, _, i32>(&key, &field).await { + Ok(removed) => removed.to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Retrieves all field names from a Redis hash. +pub fn hash_keys(key: String) -> String { + redis_operation!(conn => { + match conn.hkeys::<_, Vec>(&key).await { + Ok(fields) => fields.join(","), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Retrieves all values from a Redis hash. +pub fn hash_values(key: String) -> String { + redis_operation!(conn => { + match conn.hvals::<_, Vec>(&key).await { + Ok(values) => values.join(","), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Returns the number of fields in a Redis hash. +pub fn hash_len(key: String) -> String { + redis_operation!(conn => { + match conn.hlen::<_, i32>(&key).await { + Ok(len) => len.to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Tests if a field exists in a Redis hash. +pub fn hash_exists(key: String, field: String) -> String { + redis_operation!(conn => { + match conn.hexists::<_, _, bool>(&key, &field).await { + Ok(exists) => if exists { "1" } else { "0" }.to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} diff --git a/arma/server/extension/src/redis/helpers.rs b/arma/server/extension/src/redis/helpers.rs new file mode 100644 index 0000000..04bfcd5 --- /dev/null +++ b/arma/server/extension/src/redis/helpers.rs @@ -0,0 +1,73 @@ +//! Helper utilities for Redis data processing and encoding. + +use serde_json; + +/// Intelligently parses a Redis string value into the appropriate JSON type. +#[allow(dead_code)] +pub fn parse_redis_value(value: &str) -> serde_json::Value { + // Handle empty strings as null values + if value.is_empty() { + return serde_json::Value::Null; + } + + // Try to parse as JSON first (handles objects, arrays, and JSON primitives) + if let Ok(json_val) = serde_json::from_str(value) { + // Special handling: unwrap single-element arrays + if let serde_json::Value::Array(arr) = &json_val { + if arr.len() == 1 { + return arr[0].clone(); + } + } + return json_val; + } + + // Try to parse as integer + if let Ok(int_val) = value.parse::() { + return serde_json::Value::Number(serde_json::Number::from(int_val)); + } + + // Try to parse as float + if let Ok(float_val) = value.parse::() { + if let Some(num) = serde_json::Number::from_f64(float_val) { + return serde_json::Value::Number(num); + } + } + + // Try to parse as boolean (case-insensitive) + match value.to_lowercase().as_str() { + "true" => return serde_json::Value::Bool(true), + "false" => return serde_json::Value::Bool(false), + _ => {} + } + + // Fallback: treat as string + serde_json::Value::String(value.to_string()) +} + +/// Converts a JSON value to a string by wrapping it in an array. +#[allow(dead_code)] +pub fn parse_json_value(value: &serde_json::Value) -> String { + // Wrap the value in a single-element array + let wrapped = serde_json::Value::Array(vec![value.clone()]); + + // Serialize the wrapped array to a JSON string + wrapped.to_string() +} + +/// Encodes a string to base64 for safe Redis storage. +pub fn encode_b64(data: &str) -> String { + use base64::{Engine as _, engine::general_purpose}; + general_purpose::STANDARD.encode(data.as_bytes()) +} + +/// Decodes a base64 string back to its original form. +pub fn decode_b64(encoded: &str) -> Result { + use base64::{Engine as _, engine::general_purpose}; + match general_purpose::STANDARD.decode(encoded) { + Ok(bytes) => match String::from_utf8(bytes) { + Ok(string) => Ok(string), + Err(e) => Err(format!("Invalid UTF-8: {}", e)), + }, + Err(e) => Err(format!("Invalid base64: {}", e)), + } +} diff --git a/arma/server/extension/src/redis/list.rs b/arma/server/extension/src/redis/list.rs new file mode 100644 index 0000000..07068d8 --- /dev/null +++ b/arma/server/extension/src/redis/list.rs @@ -0,0 +1,167 @@ +//! Redis list operations for ordered collections and queues. + +use crate::redis_operation; +use bb8_redis::redis::AsyncCommands; + +/// Sets the value of an element at a specific index in a Redis list. +pub fn list_set(key: String, index: isize, value: String) -> String { + use crate::redis::helpers::encode_b64; + let encoded_value = encode_b64(&value); + redis_operation!(conn => { + match conn.lset(&key, index, &encoded_value).await { + Ok(()) => "OK".to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Retrieves the value of an element at a specific index in a Redis list. +pub fn list_get(key: String, index: isize) -> String { + use crate::redis::helpers::decode_b64; + redis_operation!(conn => { + match conn.lindex::<_, String>(&key, index).await { + Ok(encoded_value) => { + match decode_b64(&encoded_value) { + Ok(decoded) => decoded, + Err(e) => format!("Error decoding base64: {}", e), + } + }, + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Returns the length (number of elements) of a Redis list. +pub fn list_len(key: String) -> String { + redis_operation!(conn => { + match conn.llen::<_, i32>(&key).await { + Ok(len) => len.to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Retrieves a range of elements from a Redis list. +pub fn list_range(key: String, start: isize, end: isize) -> String { + use crate::redis::helpers::decode_b64; + redis_operation!(conn => { + match conn.lrange::<_, Vec>(&key, start, end).await { + Ok(encoded_values) => { + let mut decoded_values = Vec::new(); + for encoded in encoded_values { + match decode_b64(&encoded) { + Ok(decoded) => decoded_values.push(decoded), + Err(e) => return format!("Error decoding base64: {}", e), + } + } + match serde_json::to_string(&decoded_values) { + Ok(json_array) => json_array, + Err(e) => format!("Error: Failed to serialize to JSON: {}", e), + } + }, + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Prepends a value to the beginning (left) of a Redis list. +pub fn list_lpush(key: String, value: String) -> String { + use crate::redis::helpers::encode_b64; + let encoded_value = encode_b64(&value); + redis_operation!(conn => { + match conn.lpush::<_, _, usize>(&key, &encoded_value).await { + Ok(len) => len.to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Appends a value to the end (right) of a Redis list. +pub fn list_rpush(key: String, value: String) -> String { + use crate::redis::helpers::encode_b64; + let encoded_value = encode_b64(&value); + redis_operation!(conn => { + match conn.rpush::<_, _, usize>(&key, &encoded_value).await { + Ok(len) => len.to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Removes and returns elements from the beginning (left) of a Redis list. +pub fn list_lpop(key: String, count: usize) -> String { + use crate::redis::helpers::decode_b64; + redis_operation!(conn => { + let count_option = if count == 0 { + None + } else { + std::num::NonZeroUsize::new(count) + }; + match conn.lpop::<_, Vec>(&key, count_option).await { + Ok(encoded_values) => { + let mut decoded_values = Vec::new(); + for encoded in encoded_values { + match decode_b64(&encoded) { + Ok(decoded) => decoded_values.push(decoded), + Err(e) => return format!("Error decoding base64: {}", e), + } + } + match serde_json::to_string(&decoded_values) { + Ok(json_array) => json_array, + Err(e) => format!("Error: Failed to serialize to JSON: {}", e), + } + }, + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Removes and returns elements from the end (right) of a Redis list. +pub fn list_rpop(key: String, count: usize) -> String { + use crate::redis::helpers::decode_b64; + redis_operation!(conn => { + let count_option = if count == 0 { + None + } else { + std::num::NonZeroUsize::new(count) + }; + match conn.rpop::<_, Vec>(&key, count_option).await { + Ok(encoded_values) => { + let mut decoded_values = Vec::new(); + for encoded in encoded_values { + match decode_b64(&encoded) { + Ok(decoded) => decoded_values.push(decoded), + Err(e) => return format!("Error decoding base64: {}", e), + } + } + match serde_json::to_string(&decoded_values) { + Ok(json_array) => json_array, + Err(e) => format!("Error: Failed to serialize to JSON: {}", e), + } + }, + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Trims a Redis list to keep only elements within the specified range. +pub fn list_trim(key: String, start: isize, end: isize) -> String { + redis_operation!(conn => { + match conn.ltrim(&key, start, end).await { + Ok(()) => "OK".to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Removes elements from a Redis list by value. +pub fn list_del(key: String, count: isize, value: String) -> String { + use crate::redis::helpers::encode_b64; + let encoded_value = encode_b64(&value); + redis_operation!(conn => { + match conn.lrem::<_, _, i32>(&key, count, &encoded_value).await { + Ok(removed) => removed.to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} diff --git a/arma/server/extension/src/redis/macros.rs b/arma/server/extension/src/redis/macros.rs new file mode 100644 index 0000000..6115b6e --- /dev/null +++ b/arma/server/extension/src/redis/macros.rs @@ -0,0 +1,65 @@ +//! Macros for Redis operation boilerplate reduction. + +/// Macro for Redis operations that handles all connection and async boilerplate. +#[macro_export] +macro_rules! redis_operation { + ($conn:ident => $operation:block) => {{ + use $crate::redis; + use $crate::{CONNECTION_STATE, ConnectionState, REDIS_POOL, RUNTIME}; + + // Get the Redis connection pool (initialized at startup) + let pool = match REDIS_POOL.get() { + Some(pool) => pool, + None => { + // Attempt lazy initialization if not already initialized + let rt = &RUNTIME; + let init_result = rt.block_on(async move { + let cfg = redis::config::load(); + match redis::client::create_redis_pool(&cfg.redis).await { + Ok(pool) => { + let _ = REDIS_POOL.set(pool); + Ok(()) + } + Err(_e) => { + let default_cfg = redis::RedisConfig::default(); + match redis::client::create_redis_pool(&default_cfg).await { + Ok(pool) => { + let _ = REDIS_POOL.set(pool); + Ok(()) + } + Err(e) => Err(format!("{}", e)), + } + } + } + }); + + match init_result { + Ok(()) => { + *CONNECTION_STATE.write().unwrap() = ConnectionState::Connected; + match REDIS_POOL.get() { + Some(pool) => pool, + None => return "Error: Redis pool not initialized".to_string(), + } + } + Err(err) => { + *CONNECTION_STATE.write().unwrap() = ConnectionState::Failed; + return format!("Error: {}", err); + } + } + } + }; + + // Use the global tokio runtime to execute async operations + let rt = &RUNTIME; + rt.block_on(async move { + // Acquire a connection from the pool + let mut $conn = match pool.get().await { + Ok(conn) => conn, + Err(e) => return format!("Error: {}", e), + }; + + // Execute the user-provided Redis operation + $operation + }) + }}; +} diff --git a/arma/server/extension/src/redis/mod.rs b/arma/server/extension/src/redis/mod.rs new file mode 100644 index 0000000..5a13dc7 --- /dev/null +++ b/arma/server/extension/src/redis/mod.rs @@ -0,0 +1,69 @@ +//! Redis operations and utilities for the Arma 3 server extension. + +use arma_rs::Group; + +pub use client::create_redis_pool; +pub use config::RedisConfig; +pub use helpers::{decode_b64, encode_b64}; + +pub mod client; +pub mod common; +pub mod config; +pub mod hash; +pub mod helpers; +pub mod list; +pub mod macros; +pub mod set; + +pub fn group() -> Group { + Group::new() + .group( + "common", + Group::new() + .command("set", common::set_key) + .command("get", common::get_key) + .command("incr", common::incr_key) + .command("decr", common::decr_key) + .command("del", common::delete_key) + .command("keys", common::list_keys), + ) + .group( + "hash", + Group::new() + .command("set", hash::hash_set) + .command("mset", hash::hash_mset) + .command("get", hash::hash_get) + .command("getall", hash::hash_get_all) + .command("del", hash::hash_del) + .command("keys", hash::hash_keys) + .command("vals", hash::hash_values) + .command("len", hash::hash_len) + .command("exists", hash::hash_exists), + ) + .group( + "list", + Group::new() + .command("set", list::list_set) + .command("get", list::list_get) + .command("len", list::list_len) + .command("range", list::list_range) + .command("lpush", list::list_lpush) + .command("rpush", list::list_rpush) + .command("lpop", list::list_lpop) + .command("rpop", list::list_rpop) + .command("trim", list::list_trim) + .command("del", list::list_del), + ) + .group( + "set", + Group::new() + .command("add", set::set_add) + .command("members", set::set_members) + .command("card", set::set_card) + .command("ismember", set::set_is_member) + .command("randmember", set::set_random_member) + .command("randmembers", set::set_random_members) + .command("pop", set::set_pop) + .command("del", set::set_del), + ) +} diff --git a/arma/server/extension/src/redis/set.rs b/arma/server/extension/src/redis/set.rs new file mode 100644 index 0000000..1be6ce5 --- /dev/null +++ b/arma/server/extension/src/redis/set.rs @@ -0,0 +1,87 @@ +//! Redis set operations for unique collections and membership tracking. + +use crate::redis_operation; +use bb8_redis::redis::AsyncCommands; + +/// Adds a value to a Redis set. +pub fn set_add(key: String, value: String) -> String { + redis_operation!(conn => { + match conn.sadd::<_, _, i32>(&key, &value).await { + Ok(added) => added.to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Retrieves all members of a Redis set. +pub fn set_members(key: String) -> String { + redis_operation!(conn => { + match conn.smembers::<_, Vec>(&key).await { + Ok(members) => members.join(","), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Returns the number of members in a Redis set (cardinality). +pub fn set_card(key: String) -> String { + redis_operation!(conn => { + match conn.scard::<_, i32>(&key).await { + Ok(card) => card.to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Removes a value from a Redis set. +pub fn set_del(key: String, value: String) -> String { + redis_operation!(conn => { + match conn.srem::<_, _, i32>(&key, &value).await { + Ok(removed) => removed.to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Tests if a value is a member of a Redis set. +pub fn set_is_member(key: String, value: String) -> String { + redis_operation!(conn => { + match conn.sismember::<_, _, bool>(&key, &value).await { + Ok(is_member) => if is_member { "1" } else { "0" }.to_string(), + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Removes and returns a random member from a Redis set. +pub fn set_pop(key: String) -> String { + redis_operation!(conn => { + match conn.spop::<_, String>(&key).await { + Ok(value) => value, + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Returns a random member from a Redis set without removing it. +pub fn set_random_member(key: String) -> String { + redis_operation!(conn => { + match conn.srandmember::<_, String>(&key).await { + Ok(value) => value, + Err(e) => format!("Error: {}", e), + } + }) +} + +/// Returns multiple random members from a Redis set without removing them. +pub fn set_random_members(key: String, count: isize) -> String { + redis_operation!(conn => { + match conn + .srandmember_multiple::<_, Vec>(&key, count.try_into().unwrap_or(0)) + .await + { + Ok(values) => values.join(","), + Err(e) => format!("Error: {}", e), + } + }) +} diff --git a/arma/server/icon_128_ca.paa b/arma/server/icon_128_ca.paa new file mode 100644 index 0000000..02dc39e Binary files /dev/null and b/arma/server/icon_128_ca.paa differ diff --git a/arma/server/icon_128_highlight_ca.paa b/arma/server/icon_128_highlight_ca.paa new file mode 100644 index 0000000..8374f2a Binary files /dev/null and b/arma/server/icon_128_highlight_ca.paa differ diff --git a/arma/server/icon_64_ca.paa b/arma/server/icon_64_ca.paa new file mode 100644 index 0000000..afcab39 Binary files /dev/null and b/arma/server/icon_64_ca.paa differ diff --git a/arma/server/include/a3/ui_f/hpp/defineDIKCodes.inc b/arma/server/include/a3/ui_f/hpp/defineDIKCodes.inc new file mode 100644 index 0000000..4031b6b --- /dev/null +++ b/arma/server/include/a3/ui_f/hpp/defineDIKCodes.inc @@ -0,0 +1,190 @@ +#ifndef DIK_ESCAPE + +/**************************************************************************** + * + * DirectInput keyboard scan codes + * + ****************************************************************************/ + +#define DIK_ESCAPE 0x01 +#define DIK_1 0x02 +#define DIK_2 0x03 +#define DIK_3 0x04 +#define DIK_4 0x05 +#define DIK_5 0x06 +#define DIK_6 0x07 +#define DIK_7 0x08 +#define DIK_8 0x09 +#define DIK_9 0x0A +#define DIK_0 0x0B +#define DIK_MINUS 0x0C /* - on main keyboard */ +#define DIK_EQUALS 0x0D +#define DIK_BACK 0x0E /* backspace */ +#define DIK_TAB 0x0F +#define DIK_Q 0x10 +#define DIK_W 0x11 +#define DIK_E 0x12 +#define DIK_R 0x13 +#define DIK_T 0x14 +#define DIK_Y 0x15 +#define DIK_U 0x16 +#define DIK_I 0x17 +#define DIK_O 0x18 +#define DIK_P 0x19 +#define DIK_LBRACKET 0x1A +#define DIK_RBRACKET 0x1B +#define DIK_RETURN 0x1C /* Enter on main keyboard */ +#define DIK_LCONTROL 0x1D +#define DIK_A 0x1E +#define DIK_S 0x1F +#define DIK_D 0x20 +#define DIK_F 0x21 +#define DIK_G 0x22 +#define DIK_H 0x23 +#define DIK_J 0x24 +#define DIK_K 0x25 +#define DIK_L 0x26 +#define DIK_SEMICOLON 0x27 +#define DIK_APOSTROPHE 0x28 +#define DIK_GRAVE 0x29 /* accent grave */ +#define DIK_LSHIFT 0x2A +#define DIK_BACKSLASH 0x2B +#define DIK_Z 0x2C +#define DIK_X 0x2D +#define DIK_C 0x2E +#define DIK_V 0x2F +#define DIK_B 0x30 +#define DIK_N 0x31 +#define DIK_M 0x32 +#define DIK_COMMA 0x33 +#define DIK_PERIOD 0x34 /* . on main keyboard */ +#define DIK_SLASH 0x35 /* / on main keyboard */ +#define DIK_RSHIFT 0x36 +#define DIK_MULTIPLY 0x37 /* * on numeric keypad */ +#define DIK_LMENU 0x38 /* left Alt */ +#define DIK_SPACE 0x39 +#define DIK_CAPITAL 0x3A +#define DIK_F1 0x3B +#define DIK_F2 0x3C +#define DIK_F3 0x3D +#define DIK_F4 0x3E +#define DIK_F5 0x3F +#define DIK_F6 0x40 +#define DIK_F7 0x41 +#define DIK_F8 0x42 +#define DIK_F9 0x43 +#define DIK_F10 0x44 +#define DIK_NUMLOCK 0x45 +#define DIK_SCROLL 0x46 /* Scroll Lock */ +#define DIK_NUMPAD7 0x47 +#define DIK_NUMPAD8 0x48 +#define DIK_NUMPAD9 0x49 +#define DIK_SUBTRACT 0x4A /* - on numeric keypad */ +#define DIK_NUMPAD4 0x4B +#define DIK_NUMPAD5 0x4C +#define DIK_NUMPAD6 0x4D +#define DIK_ADD 0x4E /* + on numeric keypad */ +#define DIK_NUMPAD1 0x4F +#define DIK_NUMPAD2 0x50 +#define DIK_NUMPAD3 0x51 +#define DIK_NUMPAD0 0x52 +#define DIK_DECIMAL 0x53 /* . on numeric keypad */ +#define DIK_OEM_102 0x56 /* < > | on UK/Germany keyboards */ +#define DIK_F11 0x57 +#define DIK_F12 0x58 + +#define DIK_F13 0x64 /* (NEC PC98) */ +#define DIK_F14 0x65 /* (NEC PC98) */ +#define DIK_F15 0x66 /* (NEC PC98) */ + +#define DIK_KANA 0x70 /* (Japanese keyboard) */ +#define DIK_ABNT_C1 0x73 /* / ? on Portugese (Brazilian) keyboards */ +#define DIK_CONVERT 0x79 /* (Japanese keyboard) */ +#define DIK_NOCONVERT 0x7B /* (Japanese keyboard) */ +#define DIK_YEN 0x7D /* (Japanese keyboard) */ +#define DIK_ABNT_C2 0x7E /* Numpad . on Portugese (Brazilian) keyboards */ +#define DIK_NUMPADEQUALS 0x8D /* = on numeric keypad (NEC PC98) */ +#define DIK_PREVTRACK 0x90 /* Previous Track (DIK_CIRCUMFLEX on Japanese keyboard) */ +#define DIK_AT 0x91 /* (NEC PC98) */ +#define DIK_COLON 0x92 /* (NEC PC98) */ +#define DIK_UNDERLINE 0x93 /* (NEC PC98) */ +#define DIK_KANJI 0x94 /* (Japanese keyboard) */ +#define DIK_STOP 0x95 /* (NEC PC98) */ +#define DIK_AX 0x96 /* (Japan AX) */ +#define DIK_UNLABELED 0x97 /* (J3100) */ +#define DIK_NEXTTRACK 0x99 /* Next Track */ +#define DIK_NUMPADENTER 0x9C /* Enter on numeric keypad */ +#define DIK_RCONTROL 0x9D +#define DIK_MUTE 0xA0 /* Mute */ +#define DIK_CALCULATOR 0xA1 /* Calculator */ +#define DIK_PLAYPAUSE 0xA2 /* Play / Pause */ +#define DIK_MEDIASTOP 0xA4 /* Media Stop */ +#define DIK_VOLUMEDOWN 0xAE /* Volume - */ +#define DIK_VOLUMEUP 0xB0 /* Volume + */ +#define DIK_WEBHOME 0xB2 /* Web home */ +#define DIK_NUMPADCOMMA 0xB3 /* , on numeric keypad (NEC PC98) */ +#define DIK_DIVIDE 0xB5 /* / on numeric keypad */ +#define DIK_SYSRQ 0xB7 +#define DIK_RMENU 0xB8 /* right Alt */ +#define DIK_PAUSE 0xC5 /* Pause */ +#define DIK_HOME 0xC7 /* Home on arrow keypad */ +#define DIK_UP 0xC8 /* UpArrow on arrow keypad */ +#define DIK_PRIOR 0xC9 /* PgUp on arrow keypad */ +#define DIK_LEFT 0xCB /* LeftArrow on arrow keypad */ +#define DIK_RIGHT 0xCD /* RightArrow on arrow keypad */ +#define DIK_END 0xCF /* End on arrow keypad */ +#define DIK_DOWN 0xD0 /* DownArrow on arrow keypad */ +#define DIK_NEXT 0xD1 /* PgDn on arrow keypad */ +#define DIK_INSERT 0xD2 /* Insert on arrow keypad */ +#define DIK_DELETE 0xD3 /* Delete on arrow keypad */ +#define DIK_LWIN 0xDB /* Left Windows key */ +#define DIK_RWIN 0xDC /* Right Windows key */ +#define DIK_APPS 0xDD /* AppMenu key */ +#define DIK_POWER 0xDE /* System Power */ +#define DIK_SLEEP 0xDF /* System Sleep */ +#define DIK_WAKE 0xE3 /* System Wake */ +#define DIK_WEBSEARCH 0xE5 /* Web Search */ +#define DIK_WEBFAVORITES 0xE6 /* Web Favorites */ +#define DIK_WEBREFRESH 0xE7 /* Web Refresh */ +#define DIK_WEBSTOP 0xE8 /* Web Stop */ +#define DIK_WEBFORWARD 0xE9 /* Web Forward */ +#define DIK_WEBBACK 0xEA /* Web Back */ +#define DIK_MYCOMPUTER 0xEB /* My Computer */ +#define DIK_MAIL 0xEC /* Mail */ +#define DIK_MEDIASELECT 0xED /* Media Select */ + +/* + * Alternate names for keys, to facilitate transition from DOS. + */ +#define DIK_BACKSPACE DIK_BACK /* backspace */ +#define DIK_NUMPADSTAR DIK_MULTIPLY /* * on numeric keypad */ +#define DIK_LALT DIK_LMENU /* left Alt */ +#define DIK_CAPSLOCK DIK_CAPITAL /* CapsLock */ +#define DIK_NUMPADMINUS DIK_SUBTRACT /* - on numeric keypad */ +#define DIK_NUMPADPLUS DIK_ADD /* + on numeric keypad */ +#define DIK_NUMPADPERIOD DIK_DECIMAL /* . on numeric keypad */ +#define DIK_NUMPADSLASH DIK_DIVIDE /* / on numeric keypad */ +#define DIK_RALT DIK_RMENU /* right Alt */ +#define DIK_UPARROW DIK_UP /* UpArrow on arrow keypad */ +#define DIK_PGUP DIK_PRIOR /* PgUp on arrow keypad */ +#define DIK_LEFTARROW DIK_LEFT /* LeftArrow on arrow keypad */ +#define DIK_RIGHTARROW DIK_RIGHT /* RightArrow on arrow keypad */ +#define DIK_DOWNARROW DIK_DOWN /* DownArrow on arrow keypad */ +#define DIK_PGDN DIK_NEXT /* PgDn on arrow keypad */ + +/* + * Alternate names for keys originally not used on US keyboards. + */ +#define DIK_CIRCUMFLEX DIK_PREVTRACK /* Japanese keyboard */ + + +/* + * Combination keys + */ +#define INPUT_CTRL_OFFSET 512 +#define INPUT_SHIFT_OFFSET 1024 +#define INPUT_ALT_OFFSET 2048 + + +#endif /* DIK_ESCAPE */ + diff --git a/arma/server/include/x/cba/addons/main/script_macros_common.hpp b/arma/server/include/x/cba/addons/main/script_macros_common.hpp new file mode 100644 index 0000000..be13021 --- /dev/null +++ b/arma/server/include/x/cba/addons/main/script_macros_common.hpp @@ -0,0 +1,1833 @@ +/* + Header: script_macros_common.hpp + + Description: + A general set of useful macro functions for use by CBA itself or by any module that uses CBA. + + Authors: + Sickboy and Spooner +*/ + +/* **************************************************** + New - Should be exported to general addon + Aim: + - Simplify (shorten) the amount of characters required for repetitive tasks + - Provide a solid structure that can be dynamic and easy editable (Which sometimes means we cannot adhere to Aim #1 ;-) + An example is the path that is built from defines. Some available in this file, others in mods and addons. + + Follows Standard: + Object variables: PREFIX_COMPONENT + Main-object variables: PREFIX_main + Paths: MAINPREFIX\PREFIX\SUBPREFIX\COMPONENT\SCRIPTNAME.sqf + e.g: x\six\addons\sys_menu\fDate.sqf + + Usage: + define PREFIX and COMPONENT, then include this file + (Note, you could have a main addon for your mod, define the PREFIX in a macros.hpp, + and include this script_macros_common.hpp file. + Then in your addons, add a component.hpp, define the COMPONENT, + and include your mod's script_macros.hpp + In your scripts you can then include the addon's component.hpp with relative path) + + TODO: + - Try only to use 1 string type " vs ' + - Evaluate double functions, and simplification + - Evaluate naming scheme; current = prototype + - Evaluate "Debug" features.. + - Evaluate "create mini function per precompiled script, that will load the script on first usage, rather than on init" + - Also saw "Namespace" typeName, evaluate which we need :P + - Single/Multi player gamelogics? (Incase of MP, you would want only 1 gamelogic per component, which is pv'ed from server, etc) + */ + +#ifndef MAINPREFIX + #define MAINPREFIX x +#endif + +#ifndef SUBPREFIX + #define SUBPREFIX addons +#endif + +#ifndef MAINLOGIC + #define MAINLOGIC main +#endif + +#define ADDON DOUBLES(PREFIX,COMPONENT) +#define MAIN_ADDON DOUBLES(PREFIX,main) + +/* ------------------------------------------- +Macro: VERSION_CONFIG + Define CBA Versioning System config entries. + + VERSION should be a floating-point number (1 separator). + VERSION_STR is a string representation of the version. + VERSION_AR is an array representation of the version. + + VERSION must always be defined, otherwise it is 0. + VERSION_STR and VERSION_AR default to VERSION if undefined. + +Parameters: + None + +Example: + (begin example) + #define VERSION 1.0 + #define VERSION_STR 1.0.1 + #define VERSION_AR 1,0,1 + + class CfgPatches { + class MyMod_main { + VERSION_CONFIG; + }; + }; + (end) + +Author: + ?, Jonpas +------------------------------------------- */ +#ifndef VERSION + #define VERSION 0 +#endif + +#ifndef VERSION_STR + #define VERSION_STR VERSION +#endif + +#ifndef VERSION_AR + #define VERSION_AR VERSION +#endif + +#ifndef VERSION_CONFIG + #define VERSION_CONFIG version = VERSION; versionStr = QUOTE(VERSION_STR); versionAr[] = {VERSION_AR} +#endif + +/* ------------------------------------------- +Group: Debugging +------------------------------------------- */ + +/* ------------------------------------------- +Macros: DEBUG_MODE_x + Managing debugging based on debug level. + + According to the *highest* level of debugging that has been defined *before* script_macros_common.hpp is included, + only the appropriate debugging commands will be functional. With no level explicitely defined, assume DEBUG_MODE_NORMAL. + + DEBUG_MODE_FULL - Full debugging output. + DEBUG_MODE_NORMAL - All debugging except and (Default setting if none specified). + DEBUG_MODE_MINIMAL - Only and enabled. + +Examples: + In order to turn on full debugging for a single file, + (begin example) + // Top of individual script file. + #define DEBUG_MODE_FULL + #include "script_component.hpp" + (end) + + In order to force minimal debugging for a single component, + (begin example) + // Top of addons\\script_component.hpp + // Ensure that any FULL and NORMAL setting from the individual files are undefined and MINIMAL is set. + #ifdef DEBUG_MODE_FULL + #undef DEBUG_MODE_FULL + #endif + #ifdef DEBUG_MODE_NORMAL + #undef DEBUG_MODE_NORMAL + #endif + #ifndef DEBUG_MODE_MINIMAL + #define DEBUG_MODE_MINIMAL + #endif + #include "script_macros.hpp" + (end) + + In order to turn on full debugging for a whole addon, + (begin example) + // Top of addons\main\script_macros.hpp + #ifndef DEBUG_MODE_FULL + #define DEBUG_MODE_FULL + #endif + #include "\x\cba\addons\main\script_macros_common.hpp" + (end) + +Author: + Spooner +------------------------------------------- */ + +// If DEBUG_MODE_FULL, then also enable DEBUG_MODE_NORMAL. +#ifdef DEBUG_MODE_FULL +#define DEBUG_MODE_NORMAL +#endif + +// If DEBUG_MODE_NORMAL, then also enable DEBUG_MODE_MINIMAL. +#ifdef DEBUG_MODE_NORMAL +#define DEBUG_MODE_MINIMAL +#endif + +// If no debug modes specified, use DEBUG_MODE_NORMAL (+ DEBUG_MODE_MINIMAL). +#ifndef DEBUG_MODE_MINIMAL +#define DEBUG_MODE_NORMAL +#define DEBUG_MODE_MINIMAL +#endif + +#define LOG_SYS_FORMAT(LEVEL,MESSAGE) format ['[%1] (%2) %3: %4', toUpper 'PREFIX', 'COMPONENT', LEVEL, MESSAGE] + +#ifdef DEBUG_SYNCHRONOUS +#define LOG_SYS(LEVEL,MESSAGE) diag_log text LOG_SYS_FORMAT(LEVEL,MESSAGE) +#else +#define LOG_SYS(LEVEL,MESSAGE) LOG_SYS_FORMAT(LEVEL,MESSAGE) call CBA_fnc_log +#endif + +#define LOG_SYS_FILELINENUMBERS(LEVEL,MESSAGE) LOG_SYS(LEVEL,format [ARR_4('%1 %2:%3',MESSAGE,__FILE__,__LINE__ + 1)]) + +/* ------------------------------------------- +Macro: LOG() + Log a debug message into the RPT log. + + Only run if is defined. + +Parameters: + MESSAGE - Message to record + +Example: + (begin example) + LOG("Initiated clog-dancing simulator."); + (end) + +Author: + Spooner +------------------------------------------- */ +#ifdef DEBUG_MODE_FULL + +#define LOG(MESSAGE) LOG_SYS('LOG',MESSAGE) +#define LOG_1(MESSAGE,ARG1) LOG(FORMAT_1(MESSAGE,ARG1)) +#define LOG_2(MESSAGE,ARG1,ARG2) LOG(FORMAT_2(MESSAGE,ARG1,ARG2)) +#define LOG_3(MESSAGE,ARG1,ARG2,ARG3) LOG(FORMAT_3(MESSAGE,ARG1,ARG2,ARG3)) +#define LOG_4(MESSAGE,ARG1,ARG2,ARG3,ARG4) LOG(FORMAT_4(MESSAGE,ARG1,ARG2,ARG3,ARG4)) +#define LOG_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5) LOG(FORMAT_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5)) +#define LOG_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) LOG(FORMAT_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6)) +#define LOG_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) LOG(FORMAT_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7)) +#define LOG_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) LOG(FORMAT_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8)) + +#else + +#define LOG(MESSAGE) /* disabled */ +#define LOG_1(MESSAGE,ARG1) /* disabled */ +#define LOG_2(MESSAGE,ARG1,ARG2) /* disabled */ +#define LOG_3(MESSAGE,ARG1,ARG2,ARG3) /* disabled */ +#define LOG_4(MESSAGE,ARG1,ARG2,ARG3,ARG4) /* disabled */ +#define LOG_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5) /* disabled */ +#define LOG_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) /* disabled */ +#define LOG_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) /* disabled */ +#define LOG_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) /* disabled */ + +#endif + +/* ------------------------------------------- +Macro: INFO() + Record a message without file and line number in the RPT log. + +Parameters: + MESSAGE - Message to record + +Example: + (begin example) + INFO("Mod X is loaded, do Y"); + (end) + +Author: + commy2 +------------------------------------------- */ +#define INFO(MESSAGE) LOG_SYS('INFO',MESSAGE) +#define INFO_1(MESSAGE,ARG1) INFO(FORMAT_1(MESSAGE,ARG1)) +#define INFO_2(MESSAGE,ARG1,ARG2) INFO(FORMAT_2(MESSAGE,ARG1,ARG2)) +#define INFO_3(MESSAGE,ARG1,ARG2,ARG3) INFO(FORMAT_3(MESSAGE,ARG1,ARG2,ARG3)) +#define INFO_4(MESSAGE,ARG1,ARG2,ARG3,ARG4) INFO(FORMAT_4(MESSAGE,ARG1,ARG2,ARG3,ARG4)) +#define INFO_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5) INFO(FORMAT_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5)) +#define INFO_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) INFO(FORMAT_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6)) +#define INFO_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) INFO(FORMAT_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7)) +#define INFO_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) INFO(FORMAT_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8)) + +/* ------------------------------------------- +Macro: WARNING() + Record a non-critical error in the RPT log. + + Only run if or higher is defined. + +Parameters: + MESSAGE - Message to record + +Example: + (begin example) + WARNING("This function has been deprecated. Please don't use it in future!"); + (end) + +Author: + Spooner +------------------------------------------- */ +#ifdef DEBUG_MODE_NORMAL + +#define WARNING(MESSAGE) LOG_SYS('WARNING',MESSAGE) +#define WARNING_1(MESSAGE,ARG1) WARNING(FORMAT_1(MESSAGE,ARG1)) +#define WARNING_2(MESSAGE,ARG1,ARG2) WARNING(FORMAT_2(MESSAGE,ARG1,ARG2)) +#define WARNING_3(MESSAGE,ARG1,ARG2,ARG3) WARNING(FORMAT_3(MESSAGE,ARG1,ARG2,ARG3)) +#define WARNING_4(MESSAGE,ARG1,ARG2,ARG3,ARG4) WARNING(FORMAT_4(MESSAGE,ARG1,ARG2,ARG3,ARG4)) +#define WARNING_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5) WARNING(FORMAT_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5)) +#define WARNING_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) WARNING(FORMAT_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6)) +#define WARNING_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) WARNING(FORMAT_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7)) +#define WARNING_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) WARNING(FORMAT_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8)) + +#else + +#define WARNING(MESSAGE) /* disabled */ +#define WARNING_1(MESSAGE,ARG1) /* disabled */ +#define WARNING_2(MESSAGE,ARG1,ARG2) /* disabled */ +#define WARNING_3(MESSAGE,ARG1,ARG2,ARG3) /* disabled */ +#define WARNING_4(MESSAGE,ARG1,ARG2,ARG3,ARG4) /* disabled */ +#define WARNING_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5) /* disabled */ +#define WARNING_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) /* disabled */ +#define WARNING_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) /* disabled */ +#define WARNING_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) /* disabled */ + +#endif + +/* ------------------------------------------- +Macro: ERROR() + Record a critical error in the RPT log. + +Parameters: + MESSAGE - Message to record + +Example: + (begin example) + ERROR("value of frog not found in config ...yada...yada..."); + (end) + +Author: + Spooner +------------------------------------------- */ +#define ERROR(MESSAGE) LOG_SYS('ERROR',MESSAGE) +#define ERROR_1(MESSAGE,ARG1) ERROR(FORMAT_1(MESSAGE,ARG1)) +#define ERROR_2(MESSAGE,ARG1,ARG2) ERROR(FORMAT_2(MESSAGE,ARG1,ARG2)) +#define ERROR_3(MESSAGE,ARG1,ARG2,ARG3) ERROR(FORMAT_3(MESSAGE,ARG1,ARG2,ARG3)) +#define ERROR_4(MESSAGE,ARG1,ARG2,ARG3,ARG4) ERROR(FORMAT_4(MESSAGE,ARG1,ARG2,ARG3,ARG4)) +#define ERROR_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5) ERROR(FORMAT_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5)) +#define ERROR_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) ERROR(FORMAT_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6)) +#define ERROR_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) ERROR(FORMAT_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7)) +#define ERROR_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) ERROR(FORMAT_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8)) + +/* ------------------------------------------- +Macro: ERROR_MSG() + Record a critical error in the RPT log and display on screen error message. + + Newlines (\n) in the MESSAGE will be put on separate lines. + +Parameters: + MESSAGE - Message to record + +Example: + (begin example) + ERROR_MSG("value of frog not found in config ...yada...yada..."); + (end) + +Author: + commy2 +------------------------------------------- */ +#define ERROR_MSG(MESSAGE) ['PREFIX', 'COMPONENT', nil, MESSAGE, __FILE__, __LINE__ + 1] call CBA_fnc_error +#define ERROR_MSG_1(MESSAGE,ARG1) ERROR_MSG(FORMAT_1(MESSAGE,ARG1)) +#define ERROR_MSG_2(MESSAGE,ARG1,ARG2) ERROR_MSG(FORMAT_2(MESSAGE,ARG1,ARG2)) +#define ERROR_MSG_3(MESSAGE,ARG1,ARG2,ARG3) ERROR_MSG(FORMAT_3(MESSAGE,ARG1,ARG2,ARG3)) +#define ERROR_MSG_4(MESSAGE,ARG1,ARG2,ARG3,ARG4) ERROR_MSG(FORMAT_4(MESSAGE,ARG1,ARG2,ARG3,ARG4)) +#define ERROR_MSG_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5) ERROR_MSG(FORMAT_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5)) +#define ERROR_MSG_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) ERROR_MSG(FORMAT_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6)) +#define ERROR_MSG_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) ERROR_MSG(FORMAT_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7)) +#define ERROR_MSG_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) ERROR_MSG(FORMAT_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8)) + +/* ------------------------------------------- +Macro: ERROR_WITH_TITLE() + Record a critical error in the RPT log. + + The title can be specified (in the heading is always just "ERROR") + Newlines (\n) in the MESSAGE will be put on separate lines. + +Parameters: + TITLE - Title of error message + MESSAGE - Body of error message + +Example: + (begin example) + ERROR_WITH_TITLE("Value not found","Value of frog not found in config ...yada...yada..."); + (end) + +Author: + Spooner +------------------------------------------- */ +#define ERROR_WITH_TITLE(TITLE,MESSAGE) ['PREFIX', 'COMPONENT', TITLE, MESSAGE, __FILE__, __LINE__ + 1] call CBA_fnc_error +#define ERROR_WITH_TITLE_1(TITLE,MESSAGE,ARG1) ERROR_WITH_TITLE(TITLE,FORMAT_1(MESSAGE,ARG1)) +#define ERROR_WITH_TITLE_2(TITLE,MESSAGE,ARG1,ARG2) ERROR_WITH_TITLE(TITLE,FORMAT_2(MESSAGE,ARG1,ARG2)) +#define ERROR_WITH_TITLE_3(TITLE,MESSAGE,ARG1,ARG2,ARG3) ERROR_WITH_TITLE(TITLE,FORMAT_3(MESSAGE,ARG1,ARG2,ARG3)) +#define ERROR_WITH_TITLE_4(TITLE,MESSAGE,ARG1,ARG2,ARG3,ARG4) ERROR_WITH_TITLE(TITLE,FORMAT_4(MESSAGE,ARG1,ARG2,ARG3,ARG4)) +#define ERROR_WITH_TITLE_5(TITLE,MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5) ERROR_WITH_TITLE(TITLE,FORMAT_5(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5)) +#define ERROR_WITH_TITLE_6(TITLE,MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) ERROR_WITH_TITLE(TITLE,FORMAT_6(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6)) +#define ERROR_WITH_TITLE_7(TITLE,MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) ERROR_WITH_TITLE(TITLE,FORMAT_7(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7)) +#define ERROR_WITH_TITLE_8(TITLE,MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) ERROR_WITH_TITLE(TITLE,FORMAT_8(MESSAGE,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8)) + +/* ------------------------------------------- +Macro: MESSAGE_WITH_TITLE() + Record a single line in the RPT log. + +Parameters: + TITLE - Title of log message + MESSAGE - Body of message + +Example: + (begin example) + MESSAGE_WITH_TITLE("Value found","Value of frog found in config "); + (end) + +Author: + Killswitch +------------------------------------------- */ +#define MESSAGE_WITH_TITLE(TITLE,MESSAGE) LOG_SYS_FILELINENUMBERS(TITLE,MESSAGE) + +/* ------------------------------------------- +Macro: RETDEF() + If a variable is undefined, return the default value. Otherwise, return the + variable itself. + +Parameters: + VARIABLE - the variable to check + DEFAULT_VALUE - the default value to use if variable is undefined + +Example: + (begin example) + // _var is undefined + hintSilent format ["_var=%1", RETDEF(_var,5)]; // "_var=5" + _var = 7; + hintSilent format ["_var=%1", RETDEF(_var,5)]; // "_var=7" + (end example) +Author: + 654wak654 +------------------------------------------- */ +#define RETDEF(VARIABLE,DEFAULT_VALUE) (if (isNil {VARIABLE}) then [{DEFAULT_VALUE}, {VARIABLE}]) + +/* ------------------------------------------- +Macro: RETNIL() + If a variable is undefined, return the value nil. Otherwise, return the + variable itself. + +Parameters: + VARIABLE - the variable to check + +Example: + (begin example) + // _var is undefined + hintSilent format ["_var=%1", RETNIL(_var)]; // "_var=any" + (end example) + +Author: + Alef (see CBA issue #8514) +------------------------------------------- */ +#define RETNIL(VARIABLE) RETDEF(VARIABLE,nil) + +/* ------------------------------------------- +Macros: TRACE_n() + Log a message and 1-8 variables to the RPT log. + + Only run if is defined. + + TRACE_1(MESSAGE,A) - Log 1 variable. + TRACE_2(MESSAGE,A,B) - Log 2 variables. + TRACE_3(MESSAGE,A,B,C) - Log 3 variables. + TRACE_4(MESSAGE,A,B,C,D) - Log 4 variables. + TRACE_5(MESSAGE,A,B,C,D,E) - Log 5 variables. + TRACE_6(MESSAGE,A,B,C,D,E,F) - Log 6 variables. + TRACE_7(MESSAGE,A,B,C,D,E,F,G) - Log 7 variables. + TRACE_8(MESSAGE,A,B,C,D,E,F,G,H) - Log 8 variables. + TRACE_9(MESSAGE,A,B,C,D,E,F,G,H,I) - Log 9 variables. + +Parameters: + MESSAGE - Message to add to the trace [String] + A..H - Variable names to log values of [Any] + +Example: + (begin example) + TRACE_3("After takeoff",_vehicle player,getPos (_vehicle player), getPosASL (_vehicle player)); + (end) + +Author: + Spooner +------------------------------------------- */ +#define PFORMAT_1(MESSAGE,A) \ + format ['%1: A=%2', MESSAGE, RETNIL(A)] + +#define PFORMAT_2(MESSAGE,A,B) \ + format ['%1: A=%2, B=%3', MESSAGE, RETNIL(A), RETNIL(B)] + +#define PFORMAT_3(MESSAGE,A,B,C) \ + format ['%1: A=%2, B=%3, C=%4', MESSAGE, RETNIL(A), RETNIL(B), RETNIL(C)] + +#define PFORMAT_4(MESSAGE,A,B,C,D) \ + format ['%1: A=%2, B=%3, C=%4, D=%5', MESSAGE, RETNIL(A), RETNIL(B), RETNIL(C), RETNIL(D)] + +#define PFORMAT_5(MESSAGE,A,B,C,D,E) \ + format ['%1: A=%2, B=%3, C=%4, D=%5, E=%6', MESSAGE, RETNIL(A), RETNIL(B), RETNIL(C), RETNIL(D), RETNIL(E)] + +#define PFORMAT_6(MESSAGE,A,B,C,D,E,F) \ + format ['%1: A=%2, B=%3, C=%4, D=%5, E=%6, F=%7', MESSAGE, RETNIL(A), RETNIL(B), RETNIL(C), RETNIL(D), RETNIL(E), RETNIL(F)] + +#define PFORMAT_7(MESSAGE,A,B,C,D,E,F,G) \ + format ['%1: A=%2, B=%3, C=%4, D=%5, E=%6, F=%7, G=%8', MESSAGE, RETNIL(A), RETNIL(B), RETNIL(C), RETNIL(D), RETNIL(E), RETNIL(F), RETNIL(G)] + +#define PFORMAT_8(MESSAGE,A,B,C,D,E,F,G,H) \ + format ['%1: A=%2, B=%3, C=%4, D=%5, E=%6, F=%7, G=%8, H=%9', MESSAGE, RETNIL(A), RETNIL(B), RETNIL(C), RETNIL(D), RETNIL(E), RETNIL(F), RETNIL(G), RETNIL(H)] + +#define PFORMAT_9(MESSAGE,A,B,C,D,E,F,G,H,I) \ + format ['%1: A=%2, B=%3, C=%4, D=%5, E=%6, F=%7, G=%8, H=%9, I=%10', MESSAGE, RETNIL(A), RETNIL(B), RETNIL(C), RETNIL(D), RETNIL(E), RETNIL(F), RETNIL(G), RETNIL(H), RETNIL(I)] + + +#ifdef DEBUG_MODE_FULL +#define TRACE_1(MESSAGE,A) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_1(str diag_frameNo + ' ' + (MESSAGE),A)) +#define TRACE_2(MESSAGE,A,B) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_2(str diag_frameNo + ' ' + (MESSAGE),A,B)) +#define TRACE_3(MESSAGE,A,B,C) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_3(str diag_frameNo + ' ' + (MESSAGE),A,B,C)) +#define TRACE_4(MESSAGE,A,B,C,D) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_4(str diag_frameNo + ' ' + (MESSAGE),A,B,C,D)) +#define TRACE_5(MESSAGE,A,B,C,D,E) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_5(str diag_frameNo + ' ' + (MESSAGE),A,B,C,D,E)) +#define TRACE_6(MESSAGE,A,B,C,D,E,F) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_6(str diag_frameNo + ' ' + (MESSAGE),A,B,C,D,E,F)) +#define TRACE_7(MESSAGE,A,B,C,D,E,F,G) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_7(str diag_frameNo + ' ' + (MESSAGE),A,B,C,D,E,F,G)) +#define TRACE_8(MESSAGE,A,B,C,D,E,F,G,H) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_8(str diag_frameNo + ' ' + (MESSAGE),A,B,C,D,E,F,G,H)) +#define TRACE_9(MESSAGE,A,B,C,D,E,F,G,H,I) LOG_SYS_FILELINENUMBERS('TRACE',PFORMAT_9(str diag_frameNo + ' ' + (MESSAGE),A,B,C,D,E,F,G,H,I)) +#else +#define TRACE_1(MESSAGE,A) /* disabled */ +#define TRACE_2(MESSAGE,A,B) /* disabled */ +#define TRACE_3(MESSAGE,A,B,C) /* disabled */ +#define TRACE_4(MESSAGE,A,B,C,D) /* disabled */ +#define TRACE_5(MESSAGE,A,B,C,D,E) /* disabled */ +#define TRACE_6(MESSAGE,A,B,C,D,E,F) /* disabled */ +#define TRACE_7(MESSAGE,A,B,C,D,E,F,G) /* disabled */ +#define TRACE_8(MESSAGE,A,B,C,D,E,F,G,H) /* disabled */ +#define TRACE_9(MESSAGE,A,B,C,D,E,F,G,H,I) /* disabled */ +#endif + +/* ------------------------------------------- +Group: General +------------------------------------------- */ + +// ************************************* +// Internal Functions +#define DOUBLES(var1,var2) var1##_##var2 +#define TRIPLES(var1,var2,var3) var1##_##var2##_##var3 +#define QUOTE(var1) #var1 + +#ifdef MODULAR + #define COMPONENT_T DOUBLES(t,COMPONENT) + #define COMPONENT_M DOUBLES(m,COMPONENT) + #define COMPONENT_S DOUBLES(s,COMPONENT) + #define COMPONENT_C DOUBLES(c,COMPONENT) + #define COMPONENT_F COMPONENT_C +#else + #define COMPONENT_T COMPONENT + #define COMPONENT_M COMPONENT + #define COMPONENT_S COMPONENT + #define COMPONENT_F COMPONENT + #define COMPONENT_C COMPONENT +#endif + +/* ------------------------------------------- +Macro: INC() + +Description: + Increase a number by one. + +Parameters: + VAR - Variable to increment [Number] + +Example: + (begin example) + _counter = 0; + INC(_counter); + // _counter => 1 + (end) + +Author: + Spooner +------------------------------------------- */ +#define INC(var) var = (var) + 1 + +/* ------------------------------------------- +Macro: DEC() + +Description: + Decrease a number by one. + +Parameters: + VAR - Variable to decrement [Number] + +Example: + (begin example) + _counter = 99; + DEC(_counter); + // _counter => 98 + (end) + +Author: + Spooner +------------------------------------------- */ +#define DEC(var) var = (var) - 1 + +/* ------------------------------------------- +Macro: ADD() + +Description: + Add a value to a variable. Variable and value should be both Numbers or both Strings. + +Parameters: + VAR - Variable to add to [Number or String] + VALUE - Value to add [Number or String] + +Examples: + (begin example) + _counter = 2; + ADD(_counter,3); + // _counter => 5 + (end) + (begin example) + _str = "hello"; + ADD(_str," "); + ADD(_str,"Fred"); + // _str => "hello Fred" + (end) + +Author: + Sickboy +------------------------------------------- */ +#define ADD(var1,var2) var1 = (var1) + (var2) + +/* ------------------------------------------- +Macro: SUB() + +Description: + Subtract a value from a number variable. VAR and VALUE should both be Numbers. + +Parameters: + VAR - Variable to subtract from [Number] + VALUE - Value to subtract [Number] + +Examples: + (begin example) + _numChickens = 2; + SUB(_numChickens,3); + // _numChickens => -1 + (end) +------------------------------------------- */ +#define SUB(var1,var2) var1 = (var1) - (var2) + +/* ------------------------------------------- +Macro: REM() + +Description: + Remove an element from an array each time it occurs. + + This recreates the entire array, so use BIS_fnc_removeIndex if modification of the original array is required + or if only one of the elements that matches ELEMENT needs to be removed. + +Parameters: + ARRAY - Array to modify [Array] + ELEMENT - Element to remove [Any] + +Examples: + (begin example) + _array = [1, 2, 3, 4, 3, 8]; + REM(_array,3); + // _array = [1, 2, 4, 8]; + (end) + +Author: + Spooner +------------------------------------------- */ +#define REM(var1,var2) SUB(var1,[var2]) + +/* ------------------------------------------- +Macro: PUSH() + +Description: + Appends a single value onto the end of an ARRAY. Change is made to the ARRAY itself, not creating a new array. + +Parameters: + ARRAY - Array to push element onto [Array] + ELEMENT - Element to push [Any] + +Examples: + (begin example) + _fish = ["blue", "green", "smelly"]; + PUSH(_fish,"monkey-flavoured"); + // _fish => ["blue", "green", "smelly", "monkey-flavoured"] + (end) + +Author: + Spooner +------------------------------------------- */ +#define PUSH(var1,var2) (var1) pushBack (var2) + +/* ------------------------------------------- +Macro: MAP() +Description: + Applies given code to each element of the array, then assigns the + resulting array to the original +Parameters: + ARRAY - Array to be modified + CODE - Code that'll be applied to each element of the array. +Example: + (begin example) + _array = [1, 2, 3, 4, 3, 8]; + MAP(_array,_x + 1); + // _array is now [2, 3, 4, 5, 4, 9]; + (end) +Author: + 654wak654 +------------------------------------------- */ +#define MAP(ARR,CODE) ARR = ARR apply {CODE} + +/* ------------------------------------------- +Macro: FILTER() +Description: + Filters an array based on given code, then assigns the resulting array + to the original +Parameters: + ARRAY - Array to be filtered + CODE - Condition to pick elements +Example: + (begin example) + _array = [1, 2, 3, 4, 3, 8]; + FILTER(_array,_x % 2 == 0) + // _array is now [2, 4, 8]; + (end) +Author: + Commy2 +------------------------------------------- */ +#define FILTER(ARR,CODE) ARR = ARR select {CODE} + +/* ------------------------------------------- +Macro: UNIQUE() +Description: + Removes duplicate values in given array +Parameters: + ARRAY - The array to be modified +Example: + (begin example) + _someArray = [4, 4, 5, 5, 5, 2]; + UNIQUE(_someArray); + // _someArray is now [4, 5, 2] + (end) +Author: + Commy2 +------------------------------------------- */ +#define UNIQUE(ARR) ARR = ARR arrayIntersect ARR + +/* ------------------------------------------- +Macro: INTERSECTION() +Description: + Finds unique common elements between two arrays and assigns them + to the first array +Parameters: + ARRAY0 - The array to be modified + ARRAY1 - The array to find intersections with +Example: + (begin example) + _someArray = [1, 2, 3, 4, 5, 5]; + _anotherArray = [4, 5, 6, 7]; + INTERSECTION(_someArray,_anotherArray); + // _someArray is now [4, 5] + (end) +Author: + 654wak654 +------------------------------------------- */ +#define INTERSECTION(ARG0,ARG1) ARG0 = ARG0 arrayIntersect (ARG1) + +/* ------------------------------------------- +Macro: ISNILS() + +Description: + Sets a variable with a value, but only if it is undefined. + +Parameters: + VARIABLE - Variable to set [Any, not nil] + DEFAULT_VALUE - Value to set VARIABLE to if it is undefined [Any, not nil] + +Examples: + (begin example) + // _fish is undefined + ISNILS(_fish,0); + // _fish => 0 + (end) + (begin example) + _fish = 12; + // ...later... + ISNILS(_fish,0); + // _fish => 12 + (end) + +Author: + Sickboy +------------------------------------------- */ +#define ISNILS(VARIABLE,DEFAULT_VALUE) if (isNil #VARIABLE) then { VARIABLE = DEFAULT_VALUE } +#define ISNILS2(var1,var2,var3,var4) ISNILS(TRIPLES(var1,var2,var3),var4) +#define ISNILS3(var1,var2,var3) ISNILS(DOUBLES(var1,var2),var3) +#define ISNIL(var1,var2) ISNILS2(PREFIX,COMPONENT,var1,var2) +#define ISNILMAIN(var1,var2) ISNILS3(PREFIX,var1,var2) + +#define CREATELOGICS(var1,var2) var1##_##var2 = ([sideLogic] call CBA_fnc_getSharedGroup) createUnit ["LOGIC", [0, 0, 0], [], 0, "NONE"] +#define CREATELOGICLOCALS(var1,var2) var1##_##var2 = "LOGIC" createVehicleLocal [0, 0, 0] +#define CREATELOGICGLOBALS(var1,var2) var1##_##var2 = ([sideLogic] call CBA_fnc_getSharedGroup) createUnit ["LOGIC", [0, 0, 0], [], 0, "NONE"]; publicVariable QUOTE(DOUBLES(var1,var2)) +#define CREATELOGICGLOBALTESTS(var1,var2) var1##_##var2 = ([sideLogic] call CBA_fnc_getSharedGroup) createUnit [QUOTE(DOUBLES(ADDON,logic)), [0, 0, 0], [], 0, "NONE"] + +#define GETVARS(var1,var2,var3) (var1##_##var2 getVariable #var3) +#define GETVARMAINS(var1,var2) GETVARS(var1,MAINLOGIC,var2) + +#ifndef PATHTO_SYS + #define PATHTO_SYS(var1,var2,var3) \MAINPREFIX\var1\SUBPREFIX\var2\var3.sqf +#endif +#ifndef PATHTOF_SYS + #define PATHTOF_SYS(var1,var2,var3) \MAINPREFIX\var1\SUBPREFIX\var2\var3 +#endif + +#ifndef PATHTOF2_SYS + #define PATHTOF2_SYS(var1,var2,var3) MAINPREFIX\var1\SUBPREFIX\var2\var3 +#endif + +#define PATHTO_R(var1) PATHTOF2_SYS(PREFIX,COMPONENT_C,var1) +#define PATHTO_T(var1) PATHTOF_SYS(PREFIX,COMPONENT_T,var1) +#define PATHTO_M(var1) PATHTOF_SYS(PREFIX,COMPONENT_M,var1) +#define PATHTO_S(var1) PATHTOF_SYS(PREFIX,COMPONENT_S,var1) +#define PATHTO_C(var1) PATHTOF_SYS(PREFIX,COMPONENT_C,var1) +#define PATHTO_F(var1) PATHTO_SYS(PREFIX,COMPONENT_F,var1) + +// Already quoted "" +#define QPATHTO_R(var1) QUOTE(PATHTO_R(var1)) +#define QPATHTO_T(var1) QUOTE(PATHTO_T(var1)) +#define QPATHTO_M(var1) QUOTE(PATHTO_M(var1)) +#define QPATHTO_S(var1) QUOTE(PATHTO_S(var1)) +#define QPATHTO_C(var1) QUOTE(PATHTO_C(var1)) +#define QPATHTO_F(var1) QUOTE(PATHTO_F(var1)) + +// This only works for binarized configs after recompiling the pbos +// TODO: Reduce amount of calls / code.. +#define COMPILE_FILE2_CFG_SYS(var1) compile preprocessFileLineNumbers var1 +#define COMPILE_FILE2_SYS(var1) COMPILE_FILE2_CFG_SYS(var1) + +#define COMPILE_FILE_SYS(var1,var2,var3) COMPILE_FILE2_SYS('PATHTO_SYS(var1,var2,var3)') +#define COMPILE_FILE_CFG_SYS(var1,var2,var3) COMPILE_FILE2_CFG_SYS('PATHTO_SYS(var1,var2,var3)') + +#define SETVARS(var1,var2) var1##_##var2 setVariable +#define SETVARMAINS(var1) SETVARS(var1,MAINLOGIC) +#define GVARMAINS(var1,var2) var1##_##var2 +#define CFGSETTINGSS(var1,var2) configFile >> "CfgSettings" >> #var1 >> #var2 +//#define SETGVARS(var1,var2,var3) var1##_##var2##_##var3 = +//#define SETGVARMAINS(var1,var2) var1##_##var2 = + +// Compile-Once, JIT: On first use. +// #define PREPMAIN_SYS(var1,var2,var3) var1##_fnc_##var3 = { var1##_fnc_##var3 = COMPILE_FILE_SYS(var1,var2,DOUBLES(fnc,var3)); if (isNil "_this") then { call var1##_fnc_##var3 } else { _this call var1##_fnc_##var3 } } +// #define PREP_SYS(var1,var2,var3) var1##_##var2##_fnc_##var3 = { var1##_##var2##_fnc_##var3 = COMPILE_FILE_SYS(var1,var2,DOUBLES(fnc,var3)); if (isNil "_this") then { call var1##_##var2##_fnc_##var3 } else { _this call var1##_##var2##_fnc_##var3 } } +// #define PREP_SYS2(var1,var2,var3,var4) var1##_##var2##_fnc_##var4 = { var1##_##var2##_fnc_##var4 = COMPILE_FILE_SYS(var1,var3,DOUBLES(fnc,var4)); if (isNil "_this") then { call var1##_##var2##_fnc_##var4 } else { _this call var1##_##var2##_fnc_##var4 } } + +// Compile-Once, at Macro. As opposed to Compile-Once, on first use. +#define PREPMAIN_SYS(var1,var2,var3) var1##_fnc_##var3 = COMPILE_FILE_SYS(var1,var2,DOUBLES(fnc,var3)) +#define PREP_SYS(var1,var2,var3) var1##_##var2##_fnc_##var3 = COMPILE_FILE_SYS(var1,var2,DOUBLES(fnc,var3)) +#define PREP_SYS2(var1,var2,var3,var4) var1##_##var2##_fnc_##var4 = COMPILE_FILE_SYS(var1,var3,DOUBLES(fnc,var4)) + +#define LSTR(var1) TRIPLES(ADDON,STR,var1) + +#ifndef DEBUG_SETTINGS + #define DEBUG_SETTINGS [false, true, false] +#endif + +#define MSG_INIT QUOTE(Initializing: ADDON version: VERSION) + +// ************************************* +// User Functions +#define CFGSETTINGS CFGSETTINGSS(PREFIX,COMPONENT) +#define PATHTO(var1) PATHTO_SYS(PREFIX,COMPONENT_F,var1) +#define PATHTOF(var1) PATHTOF_SYS(PREFIX,COMPONENT,var1) +#define PATHTOEF(var1,var2) PATHTOF_SYS(PREFIX,var1,var2) +#define QPATHTOF(var1) QUOTE(PATHTOF(var1)) +#define QPATHTOEF(var1,var2) QUOTE(PATHTOEF(var1,var2)) + +#define COMPILE_FILE(var1) COMPILE_FILE_SYS(PREFIX,COMPONENT_F,var1) +#define COMPILE_FILE_CFG(var1) COMPILE_FILE_CFG_SYS(PREFIX,COMPONENT_F,var1) +#define COMPILE_FILE2(var1) COMPILE_FILE2_SYS('var1') +#define COMPILE_FILE2_CFG(var1) COMPILE_FILE2_CFG_SYS('var1') + +#define COMPILE_SCRIPT(var1) compileScript ['PATHTO_SYS(PREFIX,COMPONENT_F,var1)'] + + +#define VERSIONING_SYS(var1) class CfgSettings \ +{ \ + class CBA \ + { \ + class Versioning \ + { \ + class var1 \ + { \ + }; \ + }; \ + }; \ +}; + +#define VERSIONING VERSIONING_SYS(PREFIX) + +/* ------------------------------------------- +Macro: GVAR() + Get full variable identifier for a global variable owned by this component. + +Parameters: + VARIABLE - Partial name of global variable owned by this component [Any]. + +Example: + (begin example) + GVAR(frog) = 12; + // In SPON_FrogDancing component, equivalent to SPON_FrogDancing_frog = 12 + (end) + +Author: + Sickboy +------------------------------------------- */ +#define GVAR(var1) DOUBLES(ADDON,var1) +#define EGVAR(var1,var2) TRIPLES(PREFIX,var1,var2) +#define QGVAR(var1) QUOTE(GVAR(var1)) +#define QEGVAR(var1,var2) QUOTE(EGVAR(var1,var2)) +#define QQGVAR(var1) QUOTE(QGVAR(var1)) +#define QQEGVAR(var1,var2) QUOTE(QEGVAR(var1,var2)) + +/* ------------------------------------------- +Macro: GVARMAIN() + Get full variable identifier for a global variable owned by this addon. + +Parameters: + VARIABLE - Partial name of global variable owned by this addon [Any]. + +Example: + (begin example) + GVARMAIN(frog) = 12; + // In SPON_FrogDancing component, equivalent to SPON_frog = 12 + (end) + +Author: + Sickboy +------------------------------------------- */ +#define GVARMAIN(var1) GVARMAINS(PREFIX,var1) +#define QGVARMAIN(var1) QUOTE(GVARMAIN(var1)) +#define QQGVARMAIN(var1) QUOTE(QGVARMAIN(var1)) +// TODO: What's this? +#define SETTINGS DOUBLES(PREFIX,settings) +#define CREATELOGIC CREATELOGICS(PREFIX,COMPONENT) +#define CREATELOGICGLOBAL CREATELOGICGLOBALS(PREFIX,COMPONENT) +#define CREATELOGICGLOBALTEST CREATELOGICGLOBALTESTS(PREFIX,COMPONENT) +#define CREATELOGICLOCAL CREATELOGICLOCALS(PREFIX,COMPONENT) +#define CREATELOGICMAIN CREATELOGICS(PREFIX,MAINLOGIC) +#define GETVAR(var1) GETVARS(PREFIX,COMPONENT,var1) +#define SETVAR SETVARS(PREFIX,COMPONENT) +#define SETVARMAIN SETVARMAINS(PREFIX) +#define IFCOUNT(var1,var2,var3) if (count var1 > var2) then { var3 = var1 select var2 }; + +/* ------------------------------------------- +Macro: PREP() + +Description: + Defines a function. + + Full file path: + '\MAINPREFIX\PREFIX\SUBPREFIX\COMPONENT\fnc_.sqf' + + Resulting function name: + 'PREFIX_COMPONENT_' + + The PREP macro should be placed in a script run by a XEH preStart and XEH preInit event. + + The PREP macro allows for CBA function caching, which drastically speeds up load times. + Beware though that function caching is enabled by default and as such to disable it, you need to + #define DISABLE_COMPILE_CACHE above your #include "script_components.hpp" include! + + The function will be defined in ui and mission namespace. It can not be overwritten without + a mission restart. + +Parameters: + FUNCTION NAME - Name of the function, unquoted + +Examples: + (begin example) + PREP(banana); + call FUNC(banana); + (end) + +Author: + dixon13 + ------------------------------------------- */ +//#define PREP(var1) PREP_SYS(PREFIX,COMPONENT_F,var1) + +#ifdef DISABLE_COMPILE_CACHE + #define PREP(var1) TRIPLES(ADDON,fnc,var1) = compile preProcessFileLineNumbers 'PATHTO_SYS(PREFIX,COMPONENT_F,DOUBLES(fnc,var1))' + #define PREPMAIN(var1) TRIPLES(PREFIX,fnc,var1) = compile preProcessFileLineNumbers 'PATHTO_SYS(PREFIX,COMPONENT_F,DOUBLES(fnc,var1))' +#else + #define PREP(var1) ['PATHTO_SYS(PREFIX,COMPONENT_F,DOUBLES(fnc,var1))', 'TRIPLES(ADDON,fnc,var1)'] call SLX_XEH_COMPILE_NEW + #define PREPMAIN(var1) ['PATHTO_SYS(PREFIX,COMPONENT_F,DOUBLES(fnc,var1))', 'TRIPLES(PREFIX,fnc,var1)'] call SLX_XEH_COMPILE_NEW +#endif + +/* ------------------------------------------- +Macro: PATHTO_FNC() + +Description: + Defines a function inside CfgFunctions. + + Full file path in addons: + '\MAINPREFIX\PREFIX\SUBPREFIX\COMPONENT\fnc_.sqf' + Define 'RECOMPILE' to enable recompiling. + Define 'SKIP_FUNCTION_HEADER' to skip adding function header. + +Parameters: + FUNCTION NAME - Name of the function, unquoted + +Examples: + (begin example) + // file name: fnc_addPerFrameHandler.sqf + class CfgFunctions { + class CBA { + class Misc { + PATHTO_FNC(addPerFrameHandler); + }; + }; + }; + // -> CBA_fnc_addPerFrameHandler + (end) + +Author: + dixon13, commy2 + ------------------------------------------- */ +#ifdef RECOMPILE + #undef RECOMPILE + #define RECOMPILE recompile = 1 +#else + #define RECOMPILE recompile = 0 +#endif +// Set function header type: -1 - no header; 0 - default header; 1 - system header. +#ifdef SKIP_FUNCTION_HEADER + #define CFGFUNCTION_HEADER headerType = -1 +#else + #define CFGFUNCTION_HEADER headerType = 0 +#endif + +#define PATHTO_FNC(func) class func {\ + file = QPATHTOF(DOUBLES(fnc,func).sqf);\ + CFGFUNCTION_HEADER;\ + RECOMPILE;\ +} + +#define FUNC(var1) TRIPLES(ADDON,fnc,var1) +#define FUNCMAIN(var1) TRIPLES(PREFIX,fnc,var1) +#define FUNC_INNER(var1,var2) TRIPLES(DOUBLES(PREFIX,var1),fnc,var2) +#define EFUNC(var1,var2) FUNC_INNER(var1,var2) +#define QFUNC(var1) QUOTE(FUNC(var1)) +#define QFUNCMAIN(var1) QUOTE(FUNCMAIN(var1)) +#define QFUNC_INNER(var1,var2) QUOTE(FUNC_INNER(var1,var2)) +#define QEFUNC(var1,var2) QUOTE(EFUNC(var1,var2)) +#define QQFUNC(var1) QUOTE(QFUNC(var1)) +#define QQFUNCMAIN(var1) QUOTE(QFUNCMAIN(var1)) +#define QQFUNC_INNER(var1,var2) QUOTE(QFUNC_INNER(var1,var2)) +#define QQEFUNC(var1,var2) QUOTE(QEFUNC(var1,var2)) + +#ifndef PRELOAD_ADDONS + #define PRELOAD_ADDONS class CfgAddons \ +{ \ + class PreloadAddons \ + { \ + class ADDON \ + { \ + list[]={ QUOTE(ADDON) }; \ + }; \ + }; \ +} +#endif + +/* ------------------------------------------- +Macros: ARG_#() + Select from list of array arguments + +Parameters: + VARIABLE(1-8) - elements for the list + +Author: + Rommel +------------------------------------------- */ +#define ARG_1(A,B) ((A) select (B)) +#define ARG_2(A,B,C) (ARG_1(ARG_1(A,B),C)) +#define ARG_3(A,B,C,D) (ARG_1(ARG_2(A,B,C),D)) +#define ARG_4(A,B,C,D,E) (ARG_1(ARG_3(A,B,C,D),E)) +#define ARG_5(A,B,C,D,E,F) (ARG_1(ARG_4(A,B,C,D,E),F)) +#define ARG_6(A,B,C,D,E,F,G) (ARG_1(ARG_5(A,B,C,D,E,F),G)) +#define ARG_7(A,B,C,D,E,F,G,H) (ARG_1(ARG_6(A,B,C,D,E,E,F,G),H)) +#define ARG_8(A,B,C,D,E,F,G,H,I) (ARG_1(ARG_7(A,B,C,D,E,E,F,G,H),I)) + +/* ------------------------------------------- +Macros: ARR_#() + Create list from arguments. Useful for working around , in macro parameters. + 1-8 arguments possible. + +Parameters: + VARIABLE(1-8) - elements for the list + +Author: + Nou +------------------------------------------- */ +#define ARR_1(ARG1) ARG1 +#define ARR_2(ARG1,ARG2) ARG1, ARG2 +#define ARR_3(ARG1,ARG2,ARG3) ARG1, ARG2, ARG3 +#define ARR_4(ARG1,ARG2,ARG3,ARG4) ARG1, ARG2, ARG3, ARG4 +#define ARR_5(ARG1,ARG2,ARG3,ARG4,ARG5) ARG1, ARG2, ARG3, ARG4, ARG5 +#define ARR_6(ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) ARG1, ARG2, ARG3, ARG4, ARG5, ARG6 +#define ARR_7(ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) ARG1, ARG2, ARG3, ARG4, ARG5, ARG6, ARG7 +#define ARR_8(ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) ARG1, ARG2, ARG3, ARG4, ARG5, ARG6, ARG7, ARG8 + +/* ------------------------------------------- +Macros: FORMAT_#(STR, ARG1) + Format - Useful for working around , in macro parameters. + 1-8 arguments possible. + +Parameters: + STRING - string used by format + VARIABLE(1-8) - elements for usage in format + +Author: + Nou & Sickboy +------------------------------------------- */ +#define FORMAT_1(STR,ARG1) format[STR, ARG1] +#define FORMAT_2(STR,ARG1,ARG2) format[STR, ARG1, ARG2] +#define FORMAT_3(STR,ARG1,ARG2,ARG3) format[STR, ARG1, ARG2, ARG3] +#define FORMAT_4(STR,ARG1,ARG2,ARG3,ARG4) format[STR, ARG1, ARG2, ARG3, ARG4] +#define FORMAT_5(STR,ARG1,ARG2,ARG3,ARG4,ARG5) format[STR, ARG1, ARG2, ARG3, ARG4, ARG5] +#define FORMAT_6(STR,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6) format[STR, ARG1, ARG2, ARG3, ARG4, ARG5, ARG6] +#define FORMAT_7(STR,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7) format[STR, ARG1, ARG2, ARG3, ARG4, ARG5, ARG6, ARG7] +#define FORMAT_8(STR,ARG1,ARG2,ARG3,ARG4,ARG5,ARG6,ARG7,ARG8) format[STR, ARG1, ARG2, ARG3, ARG4, ARG5, ARG6, ARG7, ARG8] + +// CONTROL(46) 12 +#define DISPLAY(A) (findDisplay A) +#define CONTROL(A) DISPLAY(A) displayCtrl + +/* ------------------------------------------- +Macros: IS_x() + Checking the data types of variables. + + IS_ARRAY() - Array + IS_BOOL() - Boolean + IS_BOOLEAN() - UI display handle(synonym for ) + IS_CODE() - Code block (i.e a compiled function) + IS_CONFIG() - Configuration + IS_CONTROL() - UI control handle. + IS_DISPLAY() - UI display handle. + IS_FUNCTION() - A compiled function (synonym for ) + IS_GROUP() - Group. + IS_INTEGER() - Is a number a whole number? + IS_LOCATION() - World location. + IS_NUMBER() - A floating point number (synonym for ) + IS_OBJECT() - World object. + IS_SCALAR() - Floating point number. + IS_SCRIPT() - A script handle (as returned by execVM and spawn commands). + IS_SIDE() - Game side. + IS_STRING() - World object. + IS_TEXT() - Structured text. + +Parameters: + VARIABLE - Variable to check if it is of a particular type [Any, not nil] + +Author: + Spooner +------------------------------------------- */ +#define IS_META_SYS(VAR,TYPE) (if (isNil {VAR}) then {false} else {(VAR) isEqualType TYPE}) +#define IS_ARRAY(VAR) IS_META_SYS(VAR,[]) +#define IS_BOOL(VAR) IS_META_SYS(VAR,false) +#define IS_CODE(VAR) IS_META_SYS(VAR,{}) +#define IS_CONFIG(VAR) IS_META_SYS(VAR,configNull) +#define IS_CONTROL(VAR) IS_META_SYS(VAR,controlNull) +#define IS_DISPLAY(VAR) IS_META_SYS(VAR,displayNull) +#define IS_GROUP(VAR) IS_META_SYS(VAR,grpNull) +#define IS_OBJECT(VAR) IS_META_SYS(VAR,objNull) +#define IS_SCALAR(VAR) IS_META_SYS(VAR,0) +#define IS_SCRIPT(VAR) IS_META_SYS(VAR,scriptNull) +#define IS_SIDE(VAR) IS_META_SYS(VAR,west) +#define IS_STRING(VAR) IS_META_SYS(VAR,"STRING") +#define IS_TEXT(VAR) IS_META_SYS(VAR,text "") +#define IS_LOCATION(VAR) IS_META_SYS(VAR,locationNull) + +#define IS_BOOLEAN(VAR) IS_BOOL(VAR) +#define IS_FUNCTION(VAR) IS_CODE(VAR) +#define IS_INTEGER(VAR) (if (IS_SCALAR(VAR)) then {floor (VAR) == (VAR)} else {false}) +#define IS_NUMBER(VAR) IS_SCALAR(VAR) + +#define FLOAT_TO_STRING(num) (if (_this == 0) then {"0"} else {str parseNumber (str (_this % _this) + str floor abs _this) + "." + (str (abs _this - floor abs _this) select [2]) + "0"}) + +/* ------------------------------------------- +Macro: SCRIPT() + Sets name of script (relies on PREFIX and COMPONENT values being #defined). + Define 'SKIP_SCRIPT_NAME' to skip adding scriptName. + +Parameters: + NAME - Name of script [Indentifier] + +Example: + (begin example) + SCRIPT(eradicateMuppets); + (end) + +Author: + Spooner +------------------------------------------- */ +#ifndef SKIP_SCRIPT_NAME + #define SCRIPT(NAME) scriptName 'PREFIX\COMPONENT\NAME' +#else + #define SCRIPT(NAME) /* nope */ +#endif + +/* ------------------------------------------- +Macros: EXPLODE_n() + DEPRECATED - Use param/params commands added in Arma 3 1.48 + + Splitting an ARRAY into a number of variables (A, B, C, etc). + + Note that this NOT does make the created variables private. + _PVT variants do. + + EXPLODE_1(ARRAY,A,B) - Split a 1-element array into separate variable. + EXPLODE_2(ARRAY,A,B) - Split a 2-element array into separate variables. + EXPLODE_3(ARRAY,A,B,C) - Split a 3-element array into separate variables. + EXPLODE_4(ARRAY,A,B,C,D) - Split a 4-element array into separate variables. + EXPLODE_5(ARRAY,A,B,C,D,E) - Split a 5-element array into separate variables. + EXPLODE_6(ARRAY,A,B,C,D,E,F) - Split a 6-element array into separate variables. + EXPLODE_7(ARRAY,A,B,C,D,E,F,G) - Split a 7-element array into separate variables. + EXPLODE_8(ARRAY,A,B,C,D,E,F,G,H) - Split a 8-element array into separate variables. + EXPLODE_9(ARRAY,A,B,C,D,E,F,G,H,I) - Split a 9-element array into separate variables. + +Parameters: + ARRAY - Array to read from [Array] + A..H - Names of variables to set from array [Identifier] + +Example: + (begin example) + _array = ["fred", 156.8, 120.9]; + EXPLODE_3(_array,_name,_height,_weight); + (end) + +Author: + Spooner +------------------------------------------- */ +#define EXPLODE_1_SYS(ARRAY,A) A = ARRAY param [0] +#define EXPLODE_1(ARRAY,A) EXPLODE_1_SYS(ARRAY,A); TRACE_1("EXPLODE_1, " + QUOTE(ARRAY),A) +#define EXPLODE_1_PVT(ARRAY,A) ARRAY params [#A]; TRACE_1("EXPLODE_1, " + QUOTE(ARRAY),A) + +#define EXPLODE_2_SYS(ARRAY,A,B) EXPLODE_1_SYS(ARRAY,A); B = ARRAY param [1] +#define EXPLODE_2(ARRAY,A,B) EXPLODE_2_SYS(ARRAY,A,B); TRACE_2("EXPLODE_2, " + QUOTE(ARRAY),A,B) +#define EXPLODE_2_PVT(ARRAY,A,B) ARRAY params [#A,#B]; TRACE_2("EXPLODE_2, " + QUOTE(ARRAY),A,B) + +#define EXPLODE_3_SYS(ARRAY,A,B,C) EXPLODE_2_SYS(ARRAY,A,B); C = ARRAY param [2] +#define EXPLODE_3(ARRAY,A,B,C) EXPLODE_3_SYS(ARRAY,A,B,C); TRACE_3("EXPLODE_3, " + QUOTE(ARRAY),A,B,C) +#define EXPLODE_3_PVT(ARRAY,A,B,C) ARRAY params [#A,#B,#C]; TRACE_3("EXPLODE_3, " + QUOTE(ARRAY),A,B,C) + +#define EXPLODE_4_SYS(ARRAY,A,B,C,D) EXPLODE_3_SYS(ARRAY,A,B,C); D = ARRAY param [3] +#define EXPLODE_4(ARRAY,A,B,C,D) EXPLODE_4_SYS(ARRAY,A,B,C,D); TRACE_4("EXPLODE_4, " + QUOTE(ARRAY),A,B,C,D) +#define EXPLODE_4_PVT(ARRAY,A,B,C,D) ARRAY params [#A,#B,#C,#D]; TRACE_4("EXPLODE_4, " + QUOTE(ARRAY),A,B,C,D) + +#define EXPLODE_5_SYS(ARRAY,A,B,C,D,E) EXPLODE_4_SYS(ARRAY,A,B,C,D); E = ARRAY param [4] +#define EXPLODE_5(ARRAY,A,B,C,D,E) EXPLODE_5_SYS(ARRAY,A,B,C,D,E); TRACE_5("EXPLODE_5, " + QUOTE(ARRAY),A,B,C,D,E) +#define EXPLODE_5_PVT(ARRAY,A,B,C,D,E) ARRAY params [#A,#B,#C,#D,#E]; TRACE_5("EXPLODE_5, " + QUOTE(ARRAY),A,B,C,D,E) + +#define EXPLODE_6_SYS(ARRAY,A,B,C,D,E,F) EXPLODE_5_SYS(ARRAY,A,B,C,D,E); F = ARRAY param [5] +#define EXPLODE_6(ARRAY,A,B,C,D,E,F) EXPLODE_6_SYS(ARRAY,A,B,C,D,E,F); TRACE_6("EXPLODE_6, " + QUOTE(ARRAY),A,B,C,D,E,F) +#define EXPLODE_6_PVT(ARRAY,A,B,C,D,E,F) ARRAY params [#A,#B,#C,#D,#E,#F]; TRACE_6("EXPLODE_6, " + QUOTE(ARRAY),A,B,C,D,E,F) + +#define EXPLODE_7_SYS(ARRAY,A,B,C,D,E,F,G) EXPLODE_6_SYS(ARRAY,A,B,C,D,E,F); G = ARRAY param [6] +#define EXPLODE_7(ARRAY,A,B,C,D,E,F,G) EXPLODE_7_SYS(ARRAY,A,B,C,D,E,F,G); TRACE_7("EXPLODE_7, " + QUOTE(ARRAY),A,B,C,D,E,F,G) +#define EXPLODE_7_PVT(ARRAY,A,B,C,D,E,F,G) ARRAY params [#A,#B,#C,#D,#E,#F,#G]; TRACE_7("EXPLODE_7, " + QUOTE(ARRAY),A,B,C,D,E,F,G) + +#define EXPLODE_8_SYS(ARRAY,A,B,C,D,E,F,G,H) EXPLODE_7_SYS(ARRAY,A,B,C,D,E,F,G); H = ARRAY param [7] +#define EXPLODE_8(ARRAY,A,B,C,D,E,F,G,H) EXPLODE_8_SYS(ARRAY,A,B,C,D,E,F,G,H); TRACE_8("EXPLODE_8, " + QUOTE(ARRAY),A,B,C,D,E,F,G,H) +#define EXPLODE_8_PVT(ARRAY,A,B,C,D,E,F,G,H) ARRAY params [#A,#B,#C,#D,#E,#F,#G,#H]; TRACE_8("EXPLODE_8, " + QUOTE(ARRAY),A,B,C,D,E,F,G,H) + +#define EXPLODE_9_SYS(ARRAY,A,B,C,D,E,F,G,H,I) EXPLODE_8_SYS(ARRAY,A,B,C,D,E,F,G,H); I = ARRAY param [8] +#define EXPLODE_9(ARRAY,A,B,C,D,E,F,G,H,I) EXPLODE_9_SYS(ARRAY,A,B,C,D,E,F,G,H,I); TRACE_9("EXPLODE_9, " + QUOTE(ARRAY),A,B,C,D,E,F,G,H,I) +#define EXPLODE_9_PVT(ARRAY,A,B,C,D,E,F,G,H,I) ARRAY params [#A,#B,#C,#D,#E,#F,#G,#H,#I]; TRACE_9("EXPLODE_9, " + QUOTE(ARRAY),A,B,C,D,E,F,G,H,I) + +/* ------------------------------------------- +Macro: xSTRING() + Get full string identifier from a stringtable owned by this component. + +Parameters: + VARIABLE - Partial name of global variable owned by this component [Any]. + +Example: + ADDON is CBA_Balls. + (begin example) + // Localized String (localize command must still be used with it) + LSTRING(Example); // STR_CBA_Balls_Example; + // Config String (note the $) + CSTRING(Example); // $STR_CBA_Balls_Example; + (end) + +Author: + Jonpas +------------------------------------------- */ +#ifndef STRING_MACROS_GUARD +#define STRING_MACROS_GUARD + #define LSTRING(var1) QUOTE(TRIPLES(STR,ADDON,var1)) + #define ELSTRING(var1,var2) QUOTE(TRIPLES(STR,DOUBLES(PREFIX,var1),var2)) + #define CSTRING(var1) QUOTE(TRIPLES($STR,ADDON,var1)) + #define ECSTRING(var1,var2) QUOTE(TRIPLES($STR,DOUBLES(PREFIX,var1),var2)) + + #define LLSTRING(var1) localize QUOTE(TRIPLES(STR,ADDON,var1)) + #define LELSTRING(var1,var2) localize QUOTE(TRIPLES(STR,DOUBLES(PREFIX,var1),var2)) +#endif + + +/* ------------------------------------------- +Group: Managing Function Parameters +------------------------------------------- */ + +/* ------------------------------------------- +Macros: PARAMS_n() + DEPRECATED - Use param/params commands added in Arma 3 1.48 + + Setting variables based on parameters passed to a function. + + Each parameter is defines as private and set to the appropriate value from _this. + + PARAMS_1(A) - Get 1 parameter from the _this array (or _this if it's not an array). + PARAMS_2(A,B) - Get 2 parameters from the _this array. + PARAMS_3(A,B,C) - Get 3 parameters from the _this array. + PARAMS_4(A,B,C,D) - Get 4 parameters from the _this array. + PARAMS_5(A,B,C,D,E) - Get 5 parameters from the _this array. + PARAMS_6(A,B,C,D,E,F) - Get 6 parameters from the _this array. + PARAMS_7(A,B,C,D,E,F,G) - Get 7 parameters from the _this array. + PARAMS_8(A,B,C,D,E,F,G,H) - Get 8 parameters from the _this array. + +Parameters: + A..H - Name of variable to read from _this [Identifier] + +Example: + A function called like this: + (begin example) + [_name,_address,_telephone] call recordPersonalDetails; + (end) + expects 3 parameters and those variables could be initialised at the start of the function definition with: + (begin example) + recordPersonalDetails = { + PARAMS_3(_name,_address,_telephone); + // Rest of function follows... + }; + (end) + +Author: + Spooner +------------------------------------------- */ +#define PARAMS_1(A) EXPLODE_1_PVT(_this,A) +#define PARAMS_2(A,B) EXPLODE_2_PVT(_this,A,B) +#define PARAMS_3(A,B,C) EXPLODE_3_PVT(_this,A,B,C) +#define PARAMS_4(A,B,C,D) EXPLODE_4_PVT(_this,A,B,C,D) +#define PARAMS_5(A,B,C,D,E) EXPLODE_5_PVT(_this,A,B,C,D,E) +#define PARAMS_6(A,B,C,D,E,F) EXPLODE_6_PVT(_this,A,B,C,D,E,F) +#define PARAMS_7(A,B,C,D,E,F,G) EXPLODE_7_PVT(_this,A,B,C,D,E,F,G) +#define PARAMS_8(A,B,C,D,E,F,G,H) EXPLODE_8_PVT(_this,A,B,C,D,E,F,G,H) +#define PARAMS_9(A,B,C,D,E,F,G,H,I) EXPLODE_9_PVT(_this,A,B,C,D,E,F,G,H,I) + +/* ------------------------------------------- +Macro: DEFAULT_PARAM() + DEPRECATED - Use param/params commands added in Arma 3 1.48 + + Getting a default function parameter. This may be used together with to have a mix of required and + optional parameters. + +Parameters: + INDEX - Index of parameter in _this [Integer, 0+] + NAME - Name of the variable to set [Identifier] + DEF_VALUE - Default value to use in case the array is too short or the value at INDEX is nil [Any] + +Example: + A function called with optional parameters: + (begin example) + [_name] call myFunction; + [_name, _numberOfLegs] call myFunction; + [_name, _numberOfLegs, _hasAHead] call myFunction; + (end) + 1 required parameter and 2 optional parameters. Those variables could be initialised at the start of the function + definition with: + (begin example) + myFunction = { + PARAMS_1(_name); + DEFAULT_PARAM(1,_numberOfLegs,2); + DEFAULT_PARAM(2,_hasAHead,true); + // Rest of function follows... + }; + (end) + +Author: + Spooner +------------------------------------------- */ +#define DEFAULT_PARAM(INDEX,NAME,DEF_VALUE) \ + private [#NAME,"_this"]; \ + ISNILS(_this,[]); \ + NAME = _this param [INDEX, DEF_VALUE]; \ + TRACE_3("DEFAULT_PARAM",INDEX,NAME,DEF_VALUE) + +/* ------------------------------------------- +Macro: KEY_PARAM() + Get value from key in _this list, return default when key is not included in list. + +Parameters: + KEY - Key name [String] + NAME - Name of the variable to set [Identifier] + DEF_VALUE - Default value to use in case key not found [ANY] + +Example: + + +Author: + Muzzleflash +------------------------------------------- */ +#define KEY_PARAM(KEY,NAME,DEF_VALUE) \ + private #NAME; \ + NAME = [toLower KEY, toUpper KEY, DEF_VALUE, RETNIL(_this)] call CBA_fnc_getArg; \ + TRACE_3("KEY_PARAM",KEY,NAME,DEF_VALUE) + +/* ------------------------------------------- +Group: Assertions +------------------------------------------- */ + +#define ASSERTION_ERROR(MESSAGE) ERROR_WITH_TITLE("Assertion failed!",MESSAGE) + +/* ------------------------------------------- +Macro: ASSERT_TRUE() + Asserts that a CONDITION is true. When an assertion fails, an error is raised with the given MESSAGE. + +Parameters: + CONDITION - Condition to assert as true [Boolean] + MESSSAGE - Message to display if (A OPERATOR B) is false [String] + +Example: + (begin example) + ASSERT_TRUE(_frogIsDead,"The frog is alive"); + (end) + +Author: + Spooner +------------------------------------------- */ +#define ASSERT_TRUE(CONDITION,MESSAGE) \ + if (not (CONDITION)) then \ + { \ + ASSERTION_ERROR('Assertion (CONDITION) failed!\n\n' + (MESSAGE)); \ + } + +/* ------------------------------------------- +Macro: ASSERT_FALSE() + Asserts that a CONDITION is false. When an assertion fails, an error is raised with the given MESSAGE. + +Parameters: + CONDITION - Condition to assert as false [Boolean] + MESSSAGE - Message to display if (A OPERATOR B) is true [String] + +Example: + (begin example) + ASSERT_FALSE(_frogIsDead,"The frog died"); + (end) + +Author: + Spooner +------------------------------------------- */ +#define ASSERT_FALSE(CONDITION,MESSAGE) \ + if (CONDITION) then \ + { \ + ASSERTION_ERROR('Assertion (not (CONDITION)) failed!\n\n' + (MESSAGE)) \ + } + +/* ------------------------------------------- +Macro: ASSERT_OP() + Asserts that (A OPERATOR B) is true. When an assertion fails, an error is raised with the given MESSAGE. + +Parameters: + A - First value [Any] + OPERATOR - Binary operator to use [Operator] + B - Second value [Any] + MESSSAGE - Message to display if (A OPERATOR B) is false. [String] + +Example: + (begin example) + ASSERT_OP(_fish,>,5,"Too few fish!"); + (end) + +Author: + Spooner +------------------------------------------- */ +#define ASSERT_OP(A,OPERATOR,B,MESSAGE) \ + if (not ((A) OPERATOR (B))) then \ + { \ + ASSERTION_ERROR('Assertion (A OPERATOR B) failed!\n' + 'A: ' + (str (A)) + '\n' + 'B: ' + (str (B)) + "\n\n" + (MESSAGE)); \ + } + +/* ------------------------------------------- +Macro: ASSERT_DEFINED() + Asserts that a VARIABLE is defined. When an assertion fails, an error is raised with the given MESSAGE.. + +Parameters: + VARIABLE - Variable to test if defined [String or Function]. + MESSAGE - Message to display if variable is undefined [String]. + +Examples: + (begin example) + ASSERT_DEFINED("_anUndefinedVar","Too few fish!"); + ASSERT_DEFINED({ obj getVariable "anUndefinedVar" },"Too many fish!"); + (end) + +Author: + Spooner +------------------------------------------- */ +#define ASSERT_DEFINED(VARIABLE,MESSAGE) \ + if (isNil VARIABLE) then \ + { \ + ASSERTION_ERROR('Assertion (VARIABLE is defined) failed!\n\n' + (MESSAGE)); \ + } + +/* ------------------------------------------- +Group: Unit tests +------------------------------------------- */ +#define TEST_SUCCESS(MESSAGE) MESSAGE_WITH_TITLE("Test OK",MESSAGE) +#define TEST_FAIL(MESSAGE) ERROR_WITH_TITLE("Test FAIL",MESSAGE) + +/* ------------------------------------------- +Macro: TEST_TRUE() + Tests that a CONDITION is true. + If the condition is not true, an error is raised with the given MESSAGE. + +Parameters: + CONDITION - Condition to assert as true [Boolean] + MESSSAGE - Message to display if (A OPERATOR B) is false [String] + +Example: + (begin example) + TEST_TRUE(_frogIsDead,"The frog is alive"); + (end) + +Author: + Killswitch +------------------------------------------- */ +#define TEST_TRUE(CONDITION, MESSAGE) \ + if (CONDITION) then \ + { \ + TEST_SUCCESS('(CONDITION)'); \ + } \ + else \ + { \ + TEST_FAIL('(CONDITION) ' + (MESSAGE)); \ + } + +/* ------------------------------------------- +Macro: TEST_FALSE() + Tests that a CONDITION is false. + If the condition is not false, an error is raised with the given MESSAGE. + +Parameters: + CONDITION - Condition to test as false [Boolean] + MESSSAGE - Message to display if (A OPERATOR B) is true [String] + +Example: + (begin example) + TEST_FALSE(_frogIsDead,"The frog died"); + (end) + +Author: + Killswitch +------------------------------------------- */ +#define TEST_FALSE(CONDITION, MESSAGE) \ + if (not (CONDITION)) then \ + { \ + TEST_SUCCESS('(not (CONDITION))'); \ + } \ + else \ + { \ + TEST_FAIL('(not (CONDITION)) ' + (MESSAGE)); \ + } + +/* ------------------------------------------- +Macro: TEST_OP() + Tests that (A OPERATOR B) is true. + If the test fails, an error is raised with the given MESSAGE. + +Parameters: + A - First value [Any] + OPERATOR - Binary operator to use [Operator] + B - Second value [Any] + MESSSAGE - Message to display if (A OPERATOR B) is false. [String] + +Example: + (begin example) + TEST_OP(_fish,>,5,"Too few fish!"); + (end) + +Author: + Killswitch +------------------------------------------- */ +#define TEST_OP(A,OPERATOR,B,MESSAGE) \ + if ((A) OPERATOR (B)) then \ + { \ + TEST_SUCCESS('(A OPERATOR B)') \ + } \ + else \ + { \ + TEST_FAIL('(A OPERATOR B)') \ + }; + +/* ------------------------------------------- +Macro: TEST_DEFINED_AND_OP() + Tests that A and B are defined and (A OPERATOR B) is true. + If the test fails, an error is raised with the given MESSAGE. + +Parameters: + A - First value [Any] + OPERATOR - Binary operator to use [Operator] + B - Second value [Any] + MESSSAGE - Message to display [String] + +Example: + (begin example) + TEST_OP(_fish,>,5,"Too few fish!"); + (end) + +Author: + Killswitch, PabstMirror +------------------------------------------- */ +#define TEST_DEFINED_AND_OP(A,OPERATOR,B,MESSAGE) \ + if (isNil #A) then { \ + TEST_FAIL('(A is not defined) ' + (MESSAGE)); \ + } else { \ + if (isNil #B) then { \ + TEST_FAIL('(B is not defined) ' + (MESSAGE)); \ + } else { \ + if ((A) OPERATOR (B)) then { \ + TEST_SUCCESS('(A OPERATOR B) ' + (MESSAGE)) \ + } else { \ + TEST_FAIL('(A OPERATOR B) ' + (MESSAGE)) \ + }; }; }; + + +/* ------------------------------------------- +Macro: TEST_DEFINED() + Tests that a VARIABLE is defined. + +Parameters: + VARIABLE - Variable to test if defined [String or Function]. + MESSAGE - Message to display if variable is undefined [String]. + +Examples: + (begin example) + TEST_DEFINED("_anUndefinedVar","Too few fish!"); + TEST_DEFINED({ obj getVariable "anUndefinedVar" },"Too many fish!"); + (end) + +Author: + Killswitch +------------------------------------------- */ +#define TEST_DEFINED(VARIABLE,MESSAGE) \ + if (not isNil VARIABLE) then \ + { \ + TEST_SUCCESS('(' + VARIABLE + ' is defined)'); \ + } \ + else \ + { \ + TEST_FAIL('(' + VARIABLE + ' is not defined)' + (MESSAGE)); \ + } + +/* ------------------------------------------- +Group: Managing Deprecation +------------------------------------------- */ + +/* ------------------------------------------- +Macro: DEPRECATE_SYS() + Allow deprecation of a function that has been renamed. + + Replaces an old OLD_FUNCTION (which will have PREFIX_ prepended) with a NEW_FUNCTION + (PREFIX_ prepended) with the intention that the old function will be disabled in the future. + + Shows a warning in RPT each time the deprecated function is used, but runs the new function. + +Parameters: + OLD_FUNCTION - Full name of old function [Identifier for function that does not exist any more] + NEW_FUNCTION - Full name of new function [Function] + +Example: + (begin example) + // After renaming CBA_fnc_frog as CBA_fnc_fish + DEPRECATE_SYS(CBA_fnc_frog,CBA_fnc_fish); + (end) + +Author: + Sickboy +------------------------------------------- */ +#define DEPRECATE_SYS(OLD_FUNCTION,NEW_FUNCTION) \ + OLD_FUNCTION = { \ + WARNING('Deprecated function used: OLD_FUNCTION (new: NEW_FUNCTION) in ADDON'); \ + if (isNil "_this") then { call NEW_FUNCTION } else { _this call NEW_FUNCTION }; \ + } + +/* ------------------------------------------- +Macro: DEPRECATE() + Allow deprecation of a function, in the current component, that has been renamed. + + Replaces an OLD_FUNCTION (which will have PREFIX_ prepended) with a NEW_FUNCTION + (PREFIX_ prepended) with the intention that the old function will be disabled in the future. + + Shows a warning in RPT each time the deprecated function is used, but runs the new function. + +Parameters: + OLD_FUNCTION - Name of old function, assuming PREFIX [Identifier for function that does not exist any more] + NEW_FUNCTION - Name of new function, assuming PREFIX [Function] + +Example: + (begin example) + // After renaming CBA_fnc_frog as CBA_fnc_fish + DEPRECATE(fnc_frog,fnc_fish); + (end) + +Author: + Sickboy +------------------------------------------- */ +#define DEPRECATE(OLD_FUNCTION,NEW_FUNCTION) \ + DEPRECATE_SYS(DOUBLES(PREFIX,OLD_FUNCTION),DOUBLES(PREFIX,NEW_FUNCTION)) + +/* ------------------------------------------- +Macro: OBSOLETE_SYS() + Replace a function that has become obsolete. + + Replace an obsolete OLD_FUNCTION with a simple COMMAND_FUNCTION, with the intention that anyone + using the function should replace it with the simple command, since the function will be disabled in the future. + + Shows a warning in RPT each time the deprecated function is used, and runs the command function. + +Parameters: + OLD_FUNCTION - Full name of old function [Identifier for function that does not exist any more] + COMMAND_CODE - Code to replace the old function [Function] + +Example: + (begin example) + // In Arma2, currentWeapon command made the CBA_fMyWeapon function obsolete: + OBSOLETE_SYS(CBA_fMyWeapon,{ currentWeapon player }); + (end) + +Author: + Spooner +------------------------------------------- */ +#define OBSOLETE_SYS(OLD_FUNCTION,COMMAND_CODE) \ + OLD_FUNCTION = { \ + WARNING('Obsolete function used: (use: OLD_FUNCTION) in ADDON'); \ + if (isNil "_this") then { call COMMAND_CODE } else { _this call COMMAND_CODE }; \ + } + +/* ------------------------------------------- +Macro: OBSOLETE() + Replace a function, in the current component, that has become obsolete. + + Replace an obsolete OLD_FUNCTION (which will have PREFIX_ prepended) with a simple + COMMAND_CODE, with the intention that anyone using the function should replace it with the simple + command. + + Shows a warning in RPT each time the deprecated function is used. + +Parameters: + OLD_FUNCTION - Name of old function, assuming PREFIX [Identifier for function that does not exist any more] + COMMAND_CODE - Code to replace the old function [Function] + +Example: + (begin example) + // In Arma2, currentWeapon command made the CBA_fMyWeapon function obsolete: + OBSOLETE(fMyWeapon,{ currentWeapon player }); + (end) + +Author: + Spooner +------------------------------------------- */ +#define OBSOLETE(OLD_FUNCTION,COMMAND_CODE) \ + OBSOLETE_SYS(DOUBLES(PREFIX,OLD_FUNCTION),COMMAND_CODE) + +#define BWC_CONFIG(NAME) class NAME { \ + units[] = {}; \ + weapons[] = {}; \ + requiredVersion = REQUIRED_VERSION; \ + requiredAddons[] = {}; \ + version = VERSION; \ +} + +// XEH Specific +#define XEH_CLASS CBA_Extended_EventHandlers +#define XEH_CLASS_BASE DOUBLES(XEH_CLASS,base) +#define XEH_DISABLED class EventHandlers { class XEH_CLASS {}; }; SLX_XEH_DISABLED = 1 +#define XEH_ENABLED class EventHandlers { class XEH_CLASS { EXTENDED_EVENTHANDLERS }; }; SLX_XEH_DISABLED = 0 + +// TODO: These are actually outdated; _Once ? +#define XEH_PRE_INIT QUOTE(call COMPILE_FILE(XEH_PreInit_Once)) +#define XEH_PRE_CINIT QUOTE(call COMPILE_FILE(XEH_PreClientInit_Once)) +#define XEH_PRE_SINIT QUOTE(call COMPILE_FILE(XEH_PreServerInit_Once)) + +#define XEH_POST_INIT QUOTE(call COMPILE_FILE(XEH_PostInit_Once)) +#define XEH_POST_CINIT QUOTE(call COMPILE_FILE(XEH_PostClientInit_Once)) +#define XEH_POST_SINIT QUOTE(call COMPILE_FILE(XEH_PostServerInit_Once)) + +/* ------------------------------------------- +Macro: IS_ADMIN + Check if the local machine is an admin in the multiplayer environment. + + Reports 'true' for logged and voted in admins. + +Parameters: + None + +Example: + (begin example) + // print "true" if player is admin + systemChat str IS_ADMIN; + (end) + +Author: + commy2 +------------------------------------------- */ +#define IS_ADMIN_SYS(x) x##kick +#define IS_ADMIN serverCommandAvailable 'IS_ADMIN_SYS(#)' + +/* ------------------------------------------- +Macro: IS_ADMIN_LOGGED + Check if the local machine is a logged in admin in the multiplayer environment. + + Reports 'false' if the player was voted to be the admin. + +Parameters: + None + +Example: + (begin example) + // print "true" if player is admin and entered in the server password + systemChat str IS_ADMIN_LOGGED; + (end) + +Author: + commy2 +------------------------------------------- */ +#define IS_ADMIN_LOGGED_SYS(x) x##shutdown +#define IS_ADMIN_LOGGED serverCommandAvailable 'IS_ADMIN_LOGGED_SYS(#)' + +/* ------------------------------------------- +Macro: FILE_EXISTS + Check if a file exists + + Reports "false" if the file does not exist. + +Parameters: + FILE - Path to the file + +Example: + (begin example) + // print "true" if file exists + systemChat str FILE_EXISTS("\A3\ui_f\data\igui\cfg\cursors\weapon_ca.paa"); + (end) + +Author: + commy2 +------------------------------------------- */ +#define FILE_EXISTS(FILE) (fileExists (FILE)) diff --git a/arma/server/include/x/cba/addons/xeh/script_xeh.hpp b/arma/server/include/x/cba/addons/xeh/script_xeh.hpp new file mode 100644 index 0000000..2eba000 --- /dev/null +++ b/arma/server/include/x/cba/addons/xeh/script_xeh.hpp @@ -0,0 +1,118 @@ +/* + Header: script_xeh.hpp + + Description: + Used internally. +*/ +///////////////////////////////////////////////////////////////////////////////// +// MACRO: EXTENDED_EVENTHANDLERS +// Add all XEH event handlers +///////////////////////////////////////////////////////////////////////////////// + +#define EXTENDED_EVENTHANDLERS init = "call cba_xeh_fnc_init"; \ +fired = "call cba_xeh_fnc_fired"; \ +animChanged = "call cba_xeh_fnc_animChanged"; \ +animDone = "call cba_xeh_fnc_animDone"; \ +animStateChanged = "call cba_xeh_fnc_animStateChanged"; \ +containerClosed = "call cba_xeh_fnc_containerClosed"; \ +containerOpened = "call cba_xeh_fnc_containerOpened"; \ +controlsShifted = "call cba_xeh_fnc_controlsShifted"; \ +dammaged = "call cba_xeh_fnc_dammaged"; \ +engine = "call cba_xeh_fnc_engine"; \ +epeContact = "call cba_xeh_fnc_epeContact"; \ +epeContactEnd = "call cba_xeh_fnc_epeContactEnd"; \ +epeContactStart = "call cba_xeh_fnc_epeContactStart"; \ +explosion = "call cba_xeh_fnc_explosion"; \ +firedNear = "call cba_xeh_fnc_firedNear"; \ +fuel = "call cba_xeh_fnc_cba_xeh_fuel"; \ +gear = "call cba_xeh_fnc_gear"; \ +getIn = "call cba_xeh_fnc_getIn"; \ +getInMan = "call cba_xeh_fnc_getInMan"; \ +getOut = "call cba_xeh_fnc_getOut"; \ +getOutMan = "call cba_xeh_fnc_getOutMan"; \ +handleHeal = "call cba_xeh_fnc_handleHeal"; \ +hit = "call cba_xeh_fnc_hit"; \ +hitPart = "call cba_xeh_fnc_hitPart"; \ +incomingMissile = "call cba_xeh_fnc_incomingMissile"; \ +inventoryClosed = "call cba_xeh_fnc_inventoryClosed"; \ +inventoryOpened = "call cba_xeh_fnc_inventoryOpened"; \ +killed = "call cba_xeh_fnc_killed"; \ +landedTouchDown = "call cba_xeh_fnc_landedTouchDown"; \ +landedStopped = "call cba_xeh_fnc_landedStopped"; \ +local = "call cba_xeh_fnc_local"; \ +respawn = "call cba_xeh_fnc_respawn"; \ +put = "call cba_xeh_fnc_put"; \ +take = "call cba_xeh_fnc_take"; \ +seatSwitched = "call cba_xeh_fnc_seatSwitched"; \ +seatSwitchedMan = "call cba_xeh_fnc_seatSwitchedMan"; \ +soundPlayed = "call cba_xeh_fnc_soundPlayed"; \ +weaponAssembled = "call cba_xeh_fnc_weaponAssembled"; \ +weaponDisassembled = "call cba_xeh_fnc_weaponDisassembled"; \ +weaponDeployed = "call cba_xeh_fnc_weaponDeployed"; \ +weaponRested = "call cba_xeh_fnc_weaponRested"; \ +reloaded = "call cba_xeh_fnc_reloaded"; \ +firedMan = "call cba_xeh_fnc_firedMan"; \ +turnIn = "call cba_xeh_fnc_turnIn"; \ +turnOut = "call cba_xeh_fnc_turnOut"; \ +deleted = "call cba_xeh_fnc_deleted"; \ +disassembled = "call cba_xeh_fnc_disassembled"; \ +Suppressed = "call cba_xeh_fnc_Suppressed"; \ +gestureChanged = "call cba_xeh_fnc_gestureChanged"; \ +gestureDone = "call cba_xeh_fnc_gestureDone"; + +/* + MACRO: DELETE_EVENTHANDLERS + + Removes all event handlers. +*/ + +#define DELETE_EVENTHANDLERS init = ""; \ +fired = ""; \ +animChanged = ""; \ +animDone = ""; \ +animStateChanged = ""; \ +containerClosed = ""; \ +containerOpened = ""; \ +controlsShifted = ""; \ +dammaged = ""; \ +engine = ""; \ +epeContact = ""; \ +epeContactEnd = ""; \ +epeContactStart = ""; \ +explosion = ""; \ +firedNear = ""; \ +fuel = ""; \ +gear = ""; \ +getIn = ""; \ +getInMan = ""; \ +getOut = ""; \ +getOutMan = ""; \ +handleHeal = ""; \ +hit = ""; \ +hitPart = ""; \ +incomingMissile = ""; \ +inventoryClosed = ""; \ +inventoryOpened = ""; \ +killed = ""; \ +landedTouchDown = ""; \ +landedStopped = ""; \ +local = ""; \ +respawn = ""; \ +put = ""; \ +take = ""; \ +seatSwitched = ""; \ +seatSwitchedMan = ""; \ +soundPlayed = ""; \ +weaponAssembled = ""; \ +weaponDisassembled = ""; \ +weaponDeployed = ""; \ +weaponRested = ""; \ +reloaded = ""; \ +firedMan = ""; \ +turnIn = ""; \ +turnOut = ""; \ +deleted = ""; \ +disassembled = ""; \ +Suppressed = ""; \ +gestureChanged = ""; \ +gestureDone = "" diff --git a/arma/server/meta.cpp b/arma/server/meta.cpp new file mode 100644 index 0000000..eed91f1 --- /dev/null +++ b/arma/server/meta.cpp @@ -0,0 +1,4 @@ +protocol = 1; +publishedid = MOD_ID; +name = "forge-server"; +timestamp = 5250140732737923549; diff --git a/arma/server/mod.cpp b/arma/server/mod.cpp new file mode 100644 index 0000000..0455a32 --- /dev/null +++ b/arma/server/mod.cpp @@ -0,0 +1,15 @@ +dir = "@forge_server"; +author = "J.Schmidt"; +name = "Forge Server"; +description = "Forge Server - Version 1.0.0"; +overview = ""; +overviewPicture = "title_ca.paa"; +picture = "title_ca.paa"; +logoSmall = "icon_64_ca.paa"; +logo = "icon_128_ca.paa"; +logoOver = "icon_128_highlight_ca.paa"; +tooltip = "Forge Server"; +tooltipOwned = "IDS Owned"; +action = "https://innovativedevsolutions.org"; +actionName = "Website"; +dlcColor[] = {0.45, 0.47, 0.41, 1}; diff --git a/arma/server/server.code-workspace b/arma/server/server.code-workspace new file mode 100644 index 0000000..e372f7e --- /dev/null +++ b/arma/server/server.code-workspace @@ -0,0 +1,24 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "editor.insertSpaces": true, + "editor.detectIndentation": false, + + "files.autoSave": "onFocusChange", + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + + "files.associations": { + "*.cpp": "arma-config", + "*.hpp": "arma-config", + "*.inc": "arma-config", + "*.cfg": "arma-config", + "*.rvmat": "arma-config" + } + } +} diff --git a/arma/server/title_ca.paa b/arma/server/title_ca.paa new file mode 100644 index 0000000..f46f6d0 Binary files /dev/null and b/arma/server/title_ca.paa differ diff --git a/arma/server/tools/config_style_checker.py b/arma/server/tools/config_style_checker.py new file mode 100644 index 0000000..2332750 --- /dev/null +++ b/arma/server/tools/config_style_checker.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 + +import fnmatch +import os +import re +import ntpath +import sys +import argparse + +def check_config_style(filepath): + bad_count_file = 0 + def pushClosing(t): + closingStack.append(closing.expr) + closing << Literal( closingFor[t[0]] ) + + def popClosing(): + closing << closingStack.pop() + + reIsClass = re.compile(r'^\s*class(.*)') + reIsClassInherit = re.compile(r'^\s*class(.*):') + reIsClassBody = re.compile(r'^\s*class(.*){') + reBadColon = re.compile(r'\s*class (.*) :') + reSpaceAfterColon = re.compile(r'\s*class (.*): ') + reSpaceBeforeCurly = re.compile(r'\s*class (.*) {') + reClassSingleLine = re.compile(r'\s*class (.*)[{;]') + + with open(filepath, 'r', encoding='utf-8', errors='ignore') as file: + content = file.read() + + # Store all brackets we find in this file, so we can validate everything on the end + brackets_list = [] + + # To check if we are in a comment block + isInCommentBlock = False + checkIfInComment = False + # Used in case we are in a line comment (//) + ignoreTillEndOfLine = False + # Used in case we are in a comment block (/* */). This is true if we detect a * inside a comment block. + # If the next character is a /, it means we end our comment block. + checkIfNextIsClosingBlock = False + + # We ignore everything inside a string + isInString = False + # Used to store the starting type of a string, so we can match that to the end of a string + inStringType = ''; + + lastIsCurlyBrace = False + checkForSemiColumn = False + + # Extra information so we know what line we find errors at + lineNumber = 1 + + indexOfCharacter = 0 + # Parse all characters in the content of this file to search for potential errors + for c in content: + if (lastIsCurlyBrace): + lastIsCurlyBrace = False + if c == '\n': # Keeping track of our line numbers + lineNumber += 1 # so we can print accurate line number information when we detect a possible error + if (isInString): # while we are in a string, we can ignore everything else, except the end of the string + if (c == inStringType): + isInString = False + # if we are not in a comment block, we will check if we are at the start of one or count the () {} and [] + elif (isInCommentBlock == False): + + # This means we have encountered a /, so we are now checking if this is an inline comment or a comment block + if (checkIfInComment): + checkIfInComment = False + if c == '*': # if the next character after / is a *, we are at the start of a comment block + isInCommentBlock = True + elif (c == '/'): # Otherwise, will check if we are in an line comment + ignoreTillEndOfLine = True # and an line comment is a / followed by another / (//) We won't care about anything that comes after it + + if (isInCommentBlock == False): + if (ignoreTillEndOfLine): # we are in a line comment, just continue going through the characters until we find an end of line + if (c == '\n'): + ignoreTillEndOfLine = False + else: # validate brackets + if (c == '"' or c == "'"): + isInString = True + inStringType = c + elif (c == '/'): + checkIfInComment = True + elif (c == '('): + brackets_list.append('(') + elif (c == ')'): + if (len(brackets_list) > 0 and brackets_list[-1] in ['{', '[']): + print("ERROR: Possible missing round bracket ')' detected at {0} Line number: {1}".format(filepath,lineNumber)) + bad_count_file += 1 + brackets_list.append(')') + elif (c == '['): + brackets_list.append('[') + elif (c == ']'): + if (len(brackets_list) > 0 and brackets_list[-1] in ['{', '(']): + print("ERROR: Possible missing square bracket ']' detected at {0} Line number: {1}".format(filepath,lineNumber)) + bad_count_file += 1 + brackets_list.append(']') + elif (c == '{'): + brackets_list.append('{') + elif (c == '}'): + lastIsCurlyBrace = True + if (len(brackets_list) > 0 and brackets_list[-1] in ['(', '[']): + print("ERROR: Possible missing curly brace '}}' detected at {0} Line number: {1}".format(filepath,lineNumber)) + bad_count_file += 1 + brackets_list.append('}') + elif (c== '\t'): + print("ERROR: Tab detected at {0} Line number: {1}".format(filepath,lineNumber)) + bad_count_file += 1 + + else: # Look for the end of our comment block + if (c == '*'): + checkIfNextIsClosingBlock = True; + elif (checkIfNextIsClosingBlock): + if (c == '/'): + isInCommentBlock = False + elif (c != '*'): + checkIfNextIsClosingBlock = False + indexOfCharacter += 1 + + if brackets_list.count('[') != brackets_list.count(']'): + print("ERROR: A possible missing square bracket [ or ] in file {0} [ = {1} ] = {2}".format(filepath,brackets_list.count('['),brackets_list.count(']'))) + bad_count_file += 1 + if brackets_list.count('(') != brackets_list.count(')'): + print("ERROR: A possible missing round bracket ( or ) in file {0} ( = {1} ) = {2}".format(filepath,brackets_list.count('('),brackets_list.count(')'))) + bad_count_file += 1 + if brackets_list.count('{') != brackets_list.count('}'): + print("ERROR: A possible missing curly brace {{ or }} in file {0} {{ = {1} }} = {2}".format(filepath,brackets_list.count('{'),brackets_list.count('}'))) + bad_count_file += 1 + + file.seek(0) + for lineNumber, line in enumerate(file.readlines()): + if reIsClass.match(line): + if reBadColon.match(line): + print(f"WARNING: bad class colon {filepath} Line number: {lineNumber+1}") + # bad_count_file += 1 + if reIsClassInherit.match(line): + if not reSpaceAfterColon.match(line): + print(f"WARNING: bad class missing space after colon {filepath} Line number: {lineNumber+1}") + if reIsClassBody.match(line): + if not reSpaceBeforeCurly.match(line): + print(f"WARNING: bad class inherit missing space before curly braces {filepath} Line number: {lineNumber+1}") + if not reClassSingleLine.match(line): + print(f"WARNING: bad class braces placement {filepath} Line number: {lineNumber+1}") + # bad_count_file += 1 + + return bad_count_file + +def main(): + + print("Validating Config Style") + + sqf_list = [] + bad_count = 0 + + parser = argparse.ArgumentParser() + parser.add_argument('-m','--module', help='only search specified module addon folder', required=False, default="") + args = parser.parse_args() + + for folder in ['addons', 'optionals']: + # Allow running from root directory as well as from inside the tools directory + rootDir = "../" + folder + if (os.path.exists(folder)): + rootDir = folder + + for root, dirnames, filenames in os.walk(rootDir + '/' + args.module): + for filename in fnmatch.filter(filenames, '*.cpp'): + sqf_list.append(os.path.join(root, filename)) + for filename in fnmatch.filter(filenames, '*.hpp'): + sqf_list.append(os.path.join(root, filename)) + for filename in fnmatch.filter(filenames, '*.rvmat'): + sqf_list.append(os.path.join(root, filename)) + for filename in fnmatch.filter(filenames, '*.cfg'): + sqf_list.append(os.path.join(root, filename)) + + for filename in sqf_list: + bad_count = bad_count + check_config_style(filename) + + print("------\nChecked {0} files\nErrors detected: {1}".format(len(sqf_list), bad_count)) + if (bad_count == 0): + print("Config validation PASSED") + else: + print("Config validation FAILED") + + return bad_count + +if __name__ == "__main__": + sys.exit(main()) diff --git a/arma/server/tools/release.bat b/arma/server/tools/release.bat new file mode 100644 index 0000000..667a74c --- /dev/null +++ b/arma/server/tools/release.bat @@ -0,0 +1,4 @@ +@ECHO off +hemtt script update_build.rhai +hemtt script update_minor.rhai +hemtt release diff --git a/arma/server/tools/release_patch.bat b/arma/server/tools/release_patch.bat new file mode 100644 index 0000000..ec4db02 --- /dev/null +++ b/arma/server/tools/release_patch.bat @@ -0,0 +1,4 @@ +@ECHO off +hemtt script update_build.rhai +hemtt script update_patch.rhai +hemtt release diff --git a/build-arma.ps1 b/build-arma.ps1 new file mode 100644 index 0000000..fe6a29f --- /dev/null +++ b/build-arma.ps1 @@ -0,0 +1,69 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Build both arma/client and arma/server using hemtt + +.DESCRIPTION + This script runs hemtt build for both the client and server Arma mods. + It changes to each directory and runs the build command. + +.PARAMETER Target + Specify which target to build: 'client', 'server', or 'both' (default) + +.EXAMPLE + .\build-arma.ps1 + Builds both client and server + +.EXAMPLE + .\build-arma.ps1 -Target client + Builds only the client +#> + +param( + [Parameter(Mandatory=$false)] + [ValidateSet('client', 'server', 'both')] + [string]$Target = 'both' +) + +$ErrorActionPreference = "Stop" +$scriptDir = $PSScriptRoot + +function Build-HemttProject { + param( + [string]$ProjectPath, + [string]$ProjectName + ) + + Write-Host "`n=== Building $ProjectName ===" -ForegroundColor Cyan + + Push-Location $ProjectPath + try { + & hemtt utils fnl && hemtt build + if ($LASTEXITCODE -ne 0) { + throw "hemtt build failed for $ProjectName with exit code $LASTEXITCODE" + } + Write-Host "✓ $ProjectName build successful" -ForegroundColor Green + } + finally { + Pop-Location + } +} + +$clientPath = Join-Path $scriptDir "arma\client" +$serverPath = Join-Path $scriptDir "arma\server" + +try { + if ($Target -eq 'client' -or $Target -eq 'both') { + Build-HemttProject -ProjectPath $clientPath -ProjectName "Client" + } + + if ($Target -eq 'server' -or $Target -eq 'both') { + Build-HemttProject -ProjectPath $serverPath -ProjectName "Server" + } + + Write-Host "`n=== Build Complete ===" -ForegroundColor Green +} +catch { + Write-Host "`n✗ Build failed: $_" -ForegroundColor Red + exit 1 +} diff --git a/forge.code-workspace b/forge.code-workspace new file mode 100644 index 0000000..e24ee21 --- /dev/null +++ b/forge.code-workspace @@ -0,0 +1,22 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "editor.insertSpaces": true, + "editor.detectIndentation": false, + "files.autoSave": "onFocusChange", + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "files.associations": { + "*.cpp": "arma-config", + "*.hpp": "arma-config", + "*.inc": "arma-config", + "*.cfg": "arma-config", + "*.rvmat": "arma-config" + } + } +} diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000..c82ce4a --- /dev/null +++ b/lib/README.md @@ -0,0 +1,244 @@ +# Forge Library + +This directory contains the core business logic and data layers for the Forge framework, organized into modular, reusable crates that follow clean architecture principles. + +## Architecture Overview + +The library follows a **layered architecture** pattern, ensuring separation of concerns and maintainability: + +```mermaid +graph TD + Extension[Extension Layer
#40;ArmA 3 Interface#41;] + Services[Services Layer
#40;Business Logic#41;] + Repositories[Repositories Layer
#40;Data Persistence#41;] + Models[Models Layer
#40;Data Structures#41;] + + Extension --> Services + Services --> Repositories + Repositories --> Models +``` + +## Modules + +### Models (`lib/models`) + +**Purpose**: Defines strict data structures and validation rules for domain entities. + +**Responsibilities**: +- Define entity structures (`Actor`, `Org`) +- Implement validation logic +- Handle serialization/deserialization (JSON, Arma) +- Enforce business rules at the data level + +**Key Features**: +- Strong typing with Rust structs +- Built-in validation on creation and updates +- Automatic email generation for actors +- Arma-specific type conversions + +**Documentation**: [models/README.md](models/README.md) + +### Repositories (`lib/repositories`) + +**Purpose**: Manages data persistence and retrieval using Redis. + +**Responsibilities**: +- Abstract database operations +- Implement CRUD operations +- Handle data serialization to Redis formats +- Manage Redis keys and data structures + +**Key Features**: +- Generic over Redis client implementations +- Hash-based storage for structured data +- Set-based storage for collections (e.g., org members) +- Thread-safe operations (`Send + Sync`) + +**Documentation**: [repositories/README.md](repositories/README.md) + +### Services (`lib/services`) + +**Purpose**: Implements business logic, validation, and orchestration of operations. + +**Responsibilities**: +- Coordinate between repositories +- Enforce business rules +- Handle complex workflows +- Provide high-level APIs for the extension layer + +**Key Features**: +- Generic over repository implementations +- Stateless service design +- Get-or-create patterns for entities +- Comprehensive error handling + +**Documentation**: [services/README.md](services/README.md) + +### Shared (`lib/shared`) + +**Purpose**: Provides common utilities, traits, and helper functions used across all layers. + +**Responsibilities**: +- Define shared traits (`RedisClient`) +- Provide utility functions +- Common type definitions +- Cross-cutting concerns + +**Key Features**: +- `RedisClient` trait for repository abstraction +- JSON/Redis value parsing utilities +- Arma value conversion helpers +- Reusable helper functions + +## How It All Works Together + +### Example: Creating an Actor + +Here's how the layers interact when creating a new actor: + +1. **Extension Layer** receives SQF command: + ```rust + // arma/server/extension/src/actor.rs + pub fn create_actor(key: String, data: String) -> String { + // Parse JSON and call service + ACTOR_SERVICE.create_actor(uid, json_data) + } + ``` + +2. **Service Layer** validates and orchestrates: + ```rust + // lib/services/src/actor.rs + impl ActorService { + pub fn create_actor(&self, uid: String, data: String) -> Result { + // Create actor model (validates data) + let actor = Actor::new(uid, data)?; + + // Persist via repository + self.repository.create(&actor)?; + + Ok(actor) + } + } + ``` + +3. **Repository Layer** persists to Redis: + ```rust + // lib/repositories/src/actor.rs + impl ActorRepository for RedisActorRepository { + fn create(&self, actor: &Actor) -> Result<(), String> { + // Convert actor to Redis hash + let fields = actor.to_redis_fields(); + + // Store in Redis + self.client.hash_mset(format!("actor:{}", actor.uid), fields) + } + } + ``` + +4. **Model Layer** ensures data integrity: + ```rust + // lib/models/src/actor.rs + impl Actor { + pub fn new(uid: String, data: String) -> Result { + // Validate all fields + Self::validate(&uid, &data)?; + + // Create actor with validated data + Ok(Actor { uid, /* ... */ }) + } + } + ``` + +## Contributing + +We welcome contributions to the Forge library! Follow these guidelines to maintain consistency and quality. + +### Adding a New Model + +See [models/README.md - Contributing](models/README.md#contributing) + +**Summary**: +1. Define struct with validation rules +2. Implement `new` and `validate` methods +3. Add serialization traits (`Serialize`, `Deserialize`) +4. Implement Arma conversions (`FromArma`, `IntoArma`) + +### Adding a New Repository + +See [repositories/README.md - Contributing](repositories/README.md#contributing) + +**Summary**: +1. Define repository trait with `Send + Sync` +2. Implement trait for `RedisXRepository` +3. Use `forge_shared::RedisClient` for operations +4. Register module in `lib.rs` + +### Adding a New Service + +See [services/README.md - Contributing](services/README.md#contributing) + +**Summary**: +1. Create service struct generic over repository +2. Implement constructor and business logic methods +3. Delegate data operations to repository +4. Register module in `lib.rs` + +### Best Practices + +#### Separation of Concerns +- **Models**: Only data structures and validation +- **Repositories**: Only data persistence logic +- **Services**: Only business logic and orchestration +- **Shared**: Only common utilities and traits + +#### Error Handling +- Use `Result` for all fallible operations +- Provide descriptive error messages +- Propagate errors up the stack with `?` + +#### Testing +- **Models**: Test validation rules +- **Repositories**: Test with mock Redis clients +- **Services**: Test with mock repositories +- **Integration**: Test full stack in extension layer + +#### Dependencies +- Models should have minimal dependencies +- Repositories depend on models and shared +- Services depend on repositories and models +- Avoid circular dependencies + +#### Thread Safety +- All repository traits require `Send + Sync` +- Services are stateless and thread-safe +- Use appropriate synchronization primitives when needed + +## Module Dependencies + +```mermaid +graph TD + Shared[Shared
#40;No Dependencies#41;] + Models[Models
#40;Depends on Shared#41;] + Repositories[Repositories
#40;Depends on Models, Shared#41;] + Services[Services
#40;Depends on Repositories, Models#41;] + Extension[Extension
#40;Depends on Services, Repositories, Models, Shared#41;] + + Shared --> Services + Services --> Repositories + Repositories --> Models + Models --> Extension +``` + +## Development Workflow + +1. **Define Model**: Start with data structure and validation +2. **Create Repository**: Implement persistence layer +3. **Build Service**: Add business logic +4. **Expose in Extension**: Create SQF-callable commands +5. **Test**: Verify each layer independently and together + +## Additional Resources + +- [Extension Documentation](../arma/server/extension/README.md) +- [Redis Operations](../arma/server/extension/src/redis/README.md) +- [Adapters](../arma/server/extension/src/adapters/README.md) diff --git a/lib/models/Cargo.toml b/lib/models/Cargo.toml new file mode 100644 index 0000000..0e4470c --- /dev/null +++ b/lib/models/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "forge-models" +version = "0.1.0" +edition = "2024" +description = "FORGE data models" +authors = ["IDSolutions", "J. Schmidt"] + +[dependencies] +arma-rs = { workspace = true, optional = true } +serde = { workspace = true } +serde_json = { workspace = true, optional = true } +forge-shared = { path = "../shared" } + +[features] +default = ["actor", "bank", "member", "org"] + +actor = ["arma-rs", "serde_json"] +bank = ["arma-rs", "serde_json"] +member = ["arma-rs", "serde_json"] +org = ["arma-rs", "serde_json"] +arma-rs = ["arma-rs/serde_json"] diff --git a/lib/models/README.md b/lib/models/README.md new file mode 100644 index 0000000..2fbd5d6 --- /dev/null +++ b/lib/models/README.md @@ -0,0 +1,140 @@ +# Forge Models + +This crate defines the shared data structures (models) used throughout the Forge application. These models represent the core entities of the game and are shared between the Extension, Service, and Repository layers. + +## Actor Model + +The `Actor` struct represents a player in the game. It contains all persistent data associated with a character. + +### Fields + +| Field | Type | Description | +| :--- | :--- | :--- | +| `uid` | `String` | Unique Steam ID (64-bit). Immutable. | +| `name` | `Option` | Player's display name. | +| `loadout` | `serde_json::Value` | JSON representation of the player's equipment. | +| `position` | `Option>` | `[x, y, z]` coordinates. | +| `direction` | `f64` | Compass direction (0-360). | +| `stance` | `Option` | Player stance (e.g., "STAND", "CROUCH"). | +| `email` | `String` | In-game email address (auto-generated). | +| `phone_number` | `String` | In-game phone number (auto-generated). | +| `bank` | `f64` | Money in the bank. | +| `cash` | `f64` | Money on hand. | +| `earnings` | `f64` | Total earnings. | +| `state` | `String` | Health/Status state (default: "HEALTHY"). | +| `holster` | `bool` | Whether the weapon is holstered. | +| `rank` | `Option` | Rank within an organization. | +| `organization` | `String` | ID of the organization the player belongs to. | +| `transactions` | `Vec` | History of financial transactions. | + +### Validation Rules + +- **UID**: Must be a 17-digit numeric string. +- **Name**: Max 50 characters, cannot be empty if set. +- **Position**: Must be an array of 3 finite numbers. +- **Direction**: Must be between 0.0 and 360.0. +- **Phone Number**: Must start with "0160" and be 10 digits long. +- **Email**: Must end with "@spearnet.mil". + +### Arma Integration + +The `Actor` struct implements `FromArma` and `IntoArma` for seamless conversion between Rust structs and SQF values. +- **From Arma**: Expects a JSON string. +- **To Arma**: Returns a JSON string. + +## Organization Model + +The `Org` struct represents a guild, clan, or group of players. + +### Fields + +| Field | Type | Description | +| :--- | :--- | :--- | +| `id` | `String` | Unique identifier (slug). | +| `owner` | `String` | UID of the organization leader. | +| `name` | `String` | Display name of the organization. | +| `funds` | `f64` | Shared organization funds. | +| `reputation` | `i64` | Organization's reputation score. | + +### Validation Rules + +- **ID**: Alphanumeric and underscores only. Cannot be empty. +- **Owner**: Must be a valid 17-digit UID. +- **Name**: Max 100 characters, no control characters. +- **Funds**: Cannot be negative. + +## Contributing + +We welcome contributions to the Forge Models crate! When adding a new model, please follow these guidelines to ensure consistency. + +### Adding a New Model + +To add a new model (e.g., `Vehicle`), follow these steps: + +1. **Define the Struct**: Create a new file in `src/` (e.g., `src/vehicle.rs`) and define your struct. + - Derive `Debug`, `Clone`, `Serialize`, and `Deserialize`. + - Use `#[serde(default)]` for optional fields that should have default values. + + ```rust + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct Vehicle { + pub id: String, + pub class_name: String, + #[serde(default)] + pub damage: f64, + } + ``` + +2. **Implement `new`**: Provide a constructor that initializes the struct with valid defaults. + ```rust + impl Vehicle { + pub fn new(id: String, class_name: String) -> Result { + let vehicle = Self { + id, + class_name, + damage: 0.0, + }; + vehicle.validate()?; + Ok(vehicle) + } + } + ``` + +3. **Implement `validate`**: Create a method to enforce business rules and data integrity. + ```rust + impl Vehicle { + pub fn validate(&self) -> Result<(), String> { + if self.id.is_empty() { + return Err("ID cannot be empty".to_string()); + } + if self.damage < 0.0 || self.damage > 1.0 { + return Err("Damage must be between 0.0 and 1.0".to_string()); + } + Ok(()) + } + } + ``` + +4. **Implement Arma Traits**: Implement `FromArma` and `IntoArma` for SQF interoperability. + ```rust + use arma_rs::{FromArma, IntoArma, Value}; + + impl FromArma for Vehicle { + fn from_arma(s: String) -> Result { + serde_json::from_str(&s).map_err(|e| ... ) + } + } + + impl IntoArma for Vehicle { + fn to_arma(&self) -> Value { + let json = serde_json::to_string(self).unwrap_or_default(); + Value::String(json) + } + } + ``` + +5. **Register the Module**: Add your new module to `src/lib.rs`. + ```rust + pub mod vehicle; + pub use vehicle::Vehicle; + ``` diff --git a/lib/models/src/actor.rs b/lib/models/src/actor.rs new file mode 100644 index 0000000..a2c65fc --- /dev/null +++ b/lib/models/src/actor.rs @@ -0,0 +1,347 @@ +use arma_rs::{ + FromArma, IntoArma, + loadout::{AssignedItems, Loadout as ArmaLoadout}, +}; +use forge_shared::{ActorValidationError, arma_value_to_json}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Actor { + pub uid: String, + pub name: Option, + + #[serde(default)] + pub loadout: serde_json::Value, + + pub position: Option>, + #[serde(default)] + pub direction: f64, + pub stance: Option, + + #[serde(default)] + pub email: String, + #[serde(default)] + pub phone_number: String, + + #[serde(default)] + pub state: String, + #[serde(default)] + pub holster: bool, + pub rank: Option, + #[serde(default)] + pub organization: String, +} + +impl Actor { + pub fn new>(uid: S) -> Result { + let uid_string = uid.into(); + + if uid_string.trim().is_empty() { + return Err(ActorValidationError::EmptyUid); + } + + if !uid_string.chars().all(|c| c.is_numeric()) || uid_string.len() != 17 { + return Err(ActorValidationError::InvalidUid(uid_string)); + } + + let phone_number = Self::generate_phone_number(&uid_string); + let email = Self::generate_email(&phone_number); + + let actor = Self { + uid: uid_string, + name: None, + loadout: Self::default_loadout_json(), + position: None, + direction: 0.0, + stance: None, + email, + phone_number, + state: "HEALTHY".to_string(), + holster: true, + rank: None, + organization: "".to_string(), + }; + + actor.validate()?; + Ok(actor) + } + + pub fn validate(&self) -> Result<(), ActorValidationError> { + if self.uid.trim().is_empty() { + return Err(ActorValidationError::EmptyUid); + } + + if !self.uid.chars().all(|c| c.is_numeric()) || self.uid.len() != 17 { + return Err(ActorValidationError::InvalidUid(self.uid.clone())); + } + + if let Some(ref name) = self.name { + if name.trim().is_empty() || name.len() > 50 { + return Err(ActorValidationError::InvalidName(name.clone())); + } + } + + if let Some(ref pos) = self.position { + if pos.len() != 3 { + return Err(ActorValidationError::InvalidPosition( + "Position must have exactly 3 coordinates".to_string(), + )); + } + for coord in pos { + if !coord.is_finite() { + return Err(ActorValidationError::InvalidPosition( + "Position coordinates must be finite numbers".to_string(), + )); + } + } + } + + if !self.direction.is_finite() || self.direction < 0.0 || self.direction >= 360.0 { + return Err(ActorValidationError::InvalidDirection(self.direction)); + } + + if !self.phone_number.is_empty() { + if !self.phone_number.starts_with("0160") || self.phone_number.len() != 10 { + return Err(ActorValidationError::InvalidPhoneNumber( + self.phone_number.clone(), + )); + } + } + + if !self.email.is_empty() { + if !self.email.contains('@') || !self.email.ends_with(".mil") { + return Err(ActorValidationError::InvalidEmail(self.email.clone())); + } + } + + if !self.organization.is_empty() && self.organization.len() > 100 { + return Err(ActorValidationError::InvalidOrganization( + self.organization.clone(), + )); + } + + Ok(()) + } + + pub fn uid(&self) -> &str { + &self.uid + } + + fn generate_phone_number(uid: &str) -> String { + let uid_chars: Vec = uid.chars().collect(); + let uid_len = uid_chars.len(); + + if uid_len >= 6 { + let last_six: String = uid_chars[uid_len - 6..].iter().collect(); + format!("0160{}", last_six) + } else { + format!("0160{:0>6}", uid) + } + } + + fn generate_email(phone_number: &str) -> String { + if phone_number.is_empty() { + String::new() + } else { + format!("{}@spearnet.mil", phone_number) + } + } + + fn default_loadout_json() -> serde_json::Value { + let mut loadout = ArmaLoadout::default(); + + let uniform = loadout.uniform_mut(); + uniform.set_class("U_BG_Guerrilla_6_1".to_string()); + + loadout.set_headgear("H_Cap_blk_ION".to_string()); + + let mut items = AssignedItems::default(); + items.set_map("ItemMap".to_string()); + items.set_terminal("ItemGPS".to_string()); + items.set_radio("ItemRadio".to_string()); + items.set_compass("ItemCompass".to_string()); + items.set_watch("ItemWatch".to_string()); + loadout.set_assigned_items(items); + + let arma_value = loadout.to_arma(); + arma_value_to_json(&arma_value) + } + + pub fn get_loadout(&self) -> Result { + let loadout_str = serde_json::to_string(&self.loadout) + .map_err(|e| format!("Failed to serialize loadout: {}", e))?; + ArmaLoadout::from_arma(loadout_str).map_err(|e| format!("Failed to parse loadout: {}", e)) + } + + pub fn set_loadout(&mut self, loadout: ArmaLoadout) { + let arma_value = loadout.to_arma(); + self.loadout = arma_value_to_json(&arma_value); + } + + pub fn update_from_json(&mut self, json_obj: serde_json::Value) -> Result<(), String> { + if let serde_json::Value::Object(map) = json_obj { + let mut temp_actor = self.clone(); + + for (field, value) in map { + match field.as_str() { + "uid" => { + return Err( + "UID cannot be modified - it's the player's permanent Steam ID" + .to_string(), + ); + } + "name" => { + temp_actor.name = if value.is_null() { + None + } else { + value.as_str().map(|s| s.to_string()) + }; + } + "position" => { + temp_actor.position = if value.is_null() { + None + } else if let Some(arr) = value.as_array() { + let coords: Result, _> = arr + .iter() + .map(|v| v.as_f64().ok_or("Invalid coordinate")) + .collect(); + match coords { + Ok(pos) if pos.len() == 3 => Some(pos), + _ => return Err("Position must be [x, y, z] array".to_string()), + } + } else { + return Err("Position must be an array".to_string()); + }; + } + "direction" => { + if let Some(dir_val) = value.as_f64() { + temp_actor.direction = dir_val % 360.0; + if temp_actor.direction < 0.0 { + temp_actor.direction += 360.0; + } + } else { + return Err("Direction must be a number".to_string()); + } + } + "stance" => { + temp_actor.stance = if value.is_null() { + None + } else { + value.as_str().map(|s| s.to_string()) + }; + } + "email" => { + if let Some(email_str) = value.as_str() { + temp_actor.email = email_str.to_string(); + } else { + return Err("Email must be a string".to_string()); + } + } + "phone_number" => { + if let Some(phone_str) = value.as_str() { + let old_email = format!("{}@spearnet.mil", self.phone_number); + temp_actor.phone_number = phone_str.to_string(); + + let email_needs_update = + temp_actor.email.is_empty() || temp_actor.email == old_email; + + if email_needs_update { + temp_actor.email = Self::generate_email(&temp_actor.phone_number); + } + } else { + return Err("Phone number must be a string".to_string()); + } + } + "state" => { + if let Some(state_str) = value.as_str() { + temp_actor.state = state_str.to_uppercase(); + } else { + return Err("State must be a string".to_string()); + } + } + "holster" => { + if let Some(holster_val) = value.as_bool() { + temp_actor.holster = holster_val; + } else { + return Err("Holster must be a boolean".to_string()); + } + } + "rank" => { + temp_actor.rank = if value.is_null() { + None + } else if let Some(rank_str) = value.as_str() { + Some(rank_str.to_string()) + } else { + return Err("Rank must be a string or null".to_string()); + }; + } + "organization" => { + temp_actor.organization = if value.is_null() { + String::new() + } else if let Some(org_str) = value.as_str() { + org_str.to_string() + } else { + return Err("Organization must be a string or null".to_string()); + }; + } + "loadout" => { + temp_actor.loadout = value; + } + _ => return Err(format!("Unknown field: {}", field)), + } + } + + if temp_actor.phone_number.is_empty() { + temp_actor.phone_number = Actor::generate_phone_number(&temp_actor.uid); + } + if temp_actor.email.is_empty() { + if temp_actor.phone_number.is_empty() { + temp_actor.phone_number = Actor::generate_phone_number(&temp_actor.uid); + } + temp_actor.email = Actor::generate_email(&temp_actor.phone_number); + } + if temp_actor.organization.trim().is_empty() { + temp_actor.organization = String::new(); + } + + temp_actor + .validate() + .map_err(|e| format!("Validation failed: {}", e))?; + + *self = temp_actor; + Ok(()) + } else { + Err("Expected JSON object".to_string()) + } + } +} + +impl FromArma for Actor { + fn from_arma(s: String) -> Result { + let mut actor: Actor = serde_json::from_str(&s).map_err(|e| { + arma_rs::FromArmaError::InvalidPrimitive(format!("Invalid JSON: {}", e)) + })?; + + if actor.phone_number.is_empty() { + actor.phone_number = Actor::generate_phone_number(&actor.uid); + } + if actor.email.is_empty() { + if actor.phone_number.is_empty() { + actor.phone_number = Actor::generate_phone_number(&actor.uid); + } + actor.email = Actor::generate_email(&actor.phone_number); + } + if actor.organization.trim().is_empty() { + actor.organization = String::new(); + } + + Ok(actor) + } +} + +impl IntoArma for Actor { + fn to_arma(&self) -> arma_rs::Value { + let json_str = serde_json::to_string(self).unwrap_or_default(); + arma_rs::Value::String(json_str) + } +} diff --git a/lib/models/src/bank.rs b/lib/models/src/bank.rs new file mode 100644 index 0000000..0174778 --- /dev/null +++ b/lib/models/src/bank.rs @@ -0,0 +1,259 @@ +use arma_rs::{FromArma, IntoArma}; +use forge_shared::BankValidationError; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Bank { + pub uid: String, + pub name: String, + pub bank: f64, + pub cash: f64, + pub earnings: f64, + pub transactions: Vec, + pub phone_number: String, + pub email: String, +} + +impl Bank { + pub fn new>( + uid: S, + name: S, + phone_number: S, + email: S, + ) -> Result { + let bank = Self { + uid: uid.into(), + name: name.into(), + bank: 0.0, + cash: 0.0, + earnings: 0.0, + transactions: Vec::new(), + phone_number: phone_number.into(), + email: email.into(), + }; + + bank.validate()?; + Ok(bank) + } + + pub fn validate(&self) -> Result<(), BankValidationError> { + if self.uid.trim().is_empty() { + return Err(BankValidationError::UidEmpty); + } + + if self.name.trim().is_empty() { + return Err(BankValidationError::NameEmpty); + } + + if self.bank < 0.0 { + return Err(BankValidationError::BankNegative); + } + + if self.cash < 0.0 { + return Err(BankValidationError::CashNegative); + } + + if self.phone_number.trim().is_empty() { + return Err(BankValidationError::InvalidPhoneNumber( + self.phone_number.clone(), + )); + } + + if self.email.trim().is_empty() { + return Err(BankValidationError::InvalidEmail(self.email.clone())); + } + + Ok(()) + } + + pub fn uid(&self) -> &str { + &self.uid + } + + pub fn update_field(&mut self, field: &str, value: serde_json::Value) -> Result<(), String> { + let mut temp_bank = self.clone(); + + match field { + "uid" => { + if let Some(uid_str) = value.as_str() { + temp_bank.uid = uid_str.to_string(); + } else { + return Err("UID must be a string".to_string()); + } + } + "name" => { + if let Some(name_str) = value.as_str() { + temp_bank.name = name_str.to_string(); + } else { + return Err("Name must be a string".to_string()); + } + } + "bank" => { + if let Some(bank_val) = value.as_f64() { + temp_bank.bank = bank_val; + } else { + return Err("Bank must be a number".to_string()); + } + } + "cash" => { + if let Some(cash_val) = value.as_f64() { + temp_bank.cash = cash_val; + } else { + return Err("Cash must be a number".to_string()); + } + } + "earnings" => { + if let Some(earnings_val) = value.as_f64() { + temp_bank.earnings = earnings_val; + } else { + return Err("Earnings must be a number".to_string()); + } + } + "transactions" => { + if let serde_json::Value::Array(arr) = value { + temp_bank.transactions = arr + .into_iter() + .filter_map(|val| val.as_str().map(|s| s.to_string())) + .collect(); + } else { + return Err("Transactions must be an array of strings".to_string()); + } + } + "email" => { + if let Some(email_str) = value.as_str() { + temp_bank.email = email_str.to_string(); + } else { + return Err("Email must be a string".to_string()); + } + } + "phone_number" => { + if let Some(phone_str) = value.as_str() { + temp_bank.phone_number = phone_str.to_string(); + } else { + return Err("Phone number must be a string".to_string()); + } + } + _ => return Err(format!("Unknown field: {}", field)), + } + + temp_bank + .validate() + .map_err(|e| format!("Validation failed: {}", e))?; + + *self = temp_bank; + Ok(()) + } + + pub fn get_field(&self, field: &str) -> Option { + match field { + "uid" => Some(serde_json::Value::String(self.uid.clone())), + "name" => Some(serde_json::Value::String(self.name.clone())), + "bank" => Some(serde_json::Value::from(self.bank)), + "cash" => Some(serde_json::Value::from(self.cash)), + "earnings" => Some(serde_json::Value::from(self.earnings)), + "transactions" => Some(serde_json::Value::Array( + self.transactions + .iter() + .map(|s| serde_json::Value::String(s.clone())) + .collect(), + )), + "phone_number" => Some(serde_json::Value::String(self.phone_number.clone())), + "email" => Some(serde_json::Value::String(self.email.clone())), + _ => None, + } + } + + pub fn update_from_json(&mut self, json_obj: serde_json::Value) -> Result<(), String> { + if let serde_json::Value::Object(map) = json_obj { + let mut temp_bank = self.clone(); + + for (field, value) in map { + match field.as_str() { + "uid" => { + if let Some(uid_str) = value.as_str() { + temp_bank.uid = uid_str.to_string(); + } else { + return Err("UID must be a string".to_string()); + } + } + "name" => { + if let Some(name_str) = value.as_str() { + temp_bank.name = name_str.to_string(); + } else { + return Err("Name must be a string".to_string()); + } + } + "bank" => { + if let Some(bank_val) = value.as_f64() { + temp_bank.bank = bank_val; + } else { + return Err("Bank must be a number".to_string()); + } + } + "cash" => { + if let Some(cash_val) = value.as_f64() { + temp_bank.cash = cash_val; + } else { + return Err("Cash must be a number".to_string()); + } + } + "earnings" => { + if let Some(earnings_val) = value.as_f64() { + temp_bank.earnings = earnings_val; + } else { + return Err("Earnings must be a number".to_string()); + } + } + "transactions" => { + if let serde_json::Value::Array(arr) = value { + temp_bank.transactions = arr + .into_iter() + .filter_map(|val| val.as_str().map(|s| s.to_string())) + .collect(); + } else { + return Err("Transactions must be an array of strings".to_string()); + } + } + "phone_number" => { + if let Some(phone_str) = value.as_str() { + temp_bank.phone_number = phone_str.to_string(); + } else { + return Err("Phone number must be a string".to_string()); + } + } + "email" => { + if let Some(email_str) = value.as_str() { + temp_bank.email = email_str.to_string(); + } else { + return Err("Email must be a string".to_string()); + } + } + _ => return Err(format!("Unknown field: {}", field)), + } + } + + temp_bank + .validate() + .map_err(|e| format!("Validation failed: {}", e))?; + + *self = temp_bank; + Ok(()) + } else { + Err("Invalid JSON object".to_string()) + } + } +} + +impl FromArma for Bank { + fn from_arma(s: String) -> Result { + serde_json::from_str(&s) + .map_err(|e| arma_rs::FromArmaError::InvalidPrimitive(format!("Invalid JSON: {}", e))) + } +} + +impl IntoArma for Bank { + fn to_arma(&self) -> arma_rs::Value { + let json_str = serde_json::to_string(self).unwrap_or_default(); + arma_rs::Value::String(json_str) + } +} diff --git a/lib/models/src/lib.rs b/lib/models/src/lib.rs new file mode 100644 index 0000000..8d64619 --- /dev/null +++ b/lib/models/src/lib.rs @@ -0,0 +1,7 @@ +pub mod actor; +pub mod bank; +pub mod org; + +pub use actor::Actor; +pub use bank::Bank; +pub use org::{MemberSummary, Org}; diff --git a/lib/models/src/org.rs b/lib/models/src/org.rs new file mode 100644 index 0000000..43c3ffd --- /dev/null +++ b/lib/models/src/org.rs @@ -0,0 +1,203 @@ +use arma_rs::{FromArma, IntoArma}; +use forge_shared::OrgValidationError; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Org { + pub id: String, + pub owner: String, + pub name: String, + + #[serde(default)] + pub funds: f64, + #[serde(default)] + pub reputation: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MemberSummary { + pub uid: String, + pub name: String, +} + +impl Org { + pub fn new>(id: S, owner: S, name: S) -> Result { + let org = Self { + id: id.into(), + owner: owner.into(), + name: name.into(), + funds: 0.0, + reputation: 0, + }; + + org.validate()?; + Ok(org) + } + + pub fn validate(&self) -> Result<(), OrgValidationError> { + if self.id.trim().is_empty() { + return Err(OrgValidationError::EmptyId); + } + + if self.owner.trim().is_empty() { + return Err(OrgValidationError::EmptyOwner); + } + + if self.name.trim().is_empty() { + return Err(OrgValidationError::EmptyName); + } + + if self.funds < 0.0 { + return Err(OrgValidationError::NegativeFunds); + } + + if !self.id.chars().all(|c| c.is_alphanumeric() || c == '_') { + return Err(OrgValidationError::InvalidId(self.id.clone())); + } + + if !self.owner.chars().all(|c| c.is_numeric()) || self.owner.len() != 17 { + return Err(OrgValidationError::InvalidOwner(self.owner.clone())); + } + + if self.name.len() > 100 || self.name.chars().any(|c| c.is_control()) { + return Err(OrgValidationError::InvalidName(self.name.clone())); + } + + Ok(()) + } + + pub fn id(&self) -> &str { + &self.id + } + + pub fn update_field(&mut self, field: &str, value: serde_json::Value) -> Result<(), String> { + let mut temp_org = self.clone(); + + match field { + "id" => { + if let Some(id_str) = value.as_str() { + temp_org.id = id_str.to_string(); + } else { + return Err("ID must be a string".to_string()); + } + } + "owner" => { + if let Some(owner_str) = value.as_str() { + temp_org.owner = owner_str.to_string(); + } else { + return Err("Owner must be a string".to_string()); + } + } + "name" => { + if let Some(name_str) = value.as_str() { + temp_org.name = name_str.to_string(); + } else { + return Err("Name must be a string".to_string()); + } + } + "funds" => { + if let Some(funds_val) = value.as_f64() { + temp_org.funds = funds_val; + } else { + return Err("Funds must be a number".to_string()); + } + } + "reputation" => { + if let Some(rep_val) = value.as_i64() { + temp_org.reputation = rep_val; + } else { + return Err("Reputation must be an integer".to_string()); + } + } + _ => return Err(format!("Unknown field: {}", field)), + } + + temp_org + .validate() + .map_err(|e| format!("Validation failed: {}", e))?; + + *self = temp_org; + Ok(()) + } + + pub fn get_field(&self, field: &str) -> Option { + match field { + "id" => Some(serde_json::Value::String(self.id.clone())), + "owner" => Some(serde_json::Value::String(self.owner.clone())), + "name" => Some(serde_json::Value::String(self.name.clone())), + "funds" => Some(serde_json::Value::from(self.funds)), + "reputation" => Some(serde_json::Value::from(self.reputation)), + _ => None, + } + } + + pub fn update_from_json(&mut self, json_obj: serde_json::Value) -> Result<(), String> { + if let serde_json::Value::Object(map) = json_obj { + let mut temp_org = self.clone(); + + for (field, value) in map { + match field.as_str() { + "id" => { + if let Some(id_str) = value.as_str() { + temp_org.id = id_str.to_string(); + } else { + return Err("ID must be a string".to_string()); + } + } + "owner" => { + if let Some(owner_str) = value.as_str() { + temp_org.owner = owner_str.to_string(); + } else { + return Err("Owner must be a string".to_string()); + } + } + "name" => { + if let Some(name_str) = value.as_str() { + temp_org.name = name_str.to_string(); + } else { + return Err("Name must be a string".to_string()); + } + } + "funds" => { + if let Some(funds_val) = value.as_f64() { + temp_org.funds = funds_val; + } else { + return Err("Funds must be a number".to_string()); + } + } + "reputation" => { + if let Some(rep_val) = value.as_i64() { + temp_org.reputation = rep_val; + } else { + return Err("Reputation must be an integer".to_string()); + } + } + _ => return Err(format!("Unknown field: {}", field)), + } + } + + temp_org + .validate() + .map_err(|e| format!("Validation failed: {}", e))?; + + *self = temp_org; + Ok(()) + } else { + Err("Expected JSON object".to_string()) + } + } +} + +impl FromArma for Org { + fn from_arma(s: String) -> Result { + serde_json::from_str(&s) + .map_err(|e| arma_rs::FromArmaError::InvalidPrimitive(format!("Invalid JSON: {}", e))) + } +} + +impl IntoArma for Org { + fn to_arma(&self) -> arma_rs::Value { + let json_str = serde_json::to_string(self).unwrap_or_default(); + arma_rs::Value::String(json_str) + } +} diff --git a/lib/repositories/Cargo.toml b/lib/repositories/Cargo.toml new file mode 100644 index 0000000..05bc30f --- /dev/null +++ b/lib/repositories/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "forge-repositories" +version = "0.1.0" +edition = "2024" + +[dependencies] +forge-models = { path = "../models" } +forge-shared = { path = "../shared" } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tokio = { workspace = true } + +# Redis dependencies (only in repository layer) +redis = { workspace = true } +bb8-redis = "0.25.0-rc.1" +base64 = "0.22.1" diff --git a/lib/repositories/README.md b/lib/repositories/README.md new file mode 100644 index 0000000..f58a89d --- /dev/null +++ b/lib/repositories/README.md @@ -0,0 +1,197 @@ +# Forge Repositories + +This crate provides the data access layer for the Forge application, implementing the repository pattern to abstract database operations from business logic. + +## Architecture + +The repository layer sits between the service layer and the database, providing a clean abstraction for data persistence. + +```mermaid +graph TD + Services[Services Layer] + Repositories[Repositories Layer
#40;This Module#41;] + Database[Database] + + Services --> Repositories + Repositories --> Database +``` + +### Dual Storage Strategy +The implementation uses a dual storage strategy in Redis to optimize for different access patterns: +- **Hash Maps (`HMSET`/`HGETALL`):** Used for entity data (Actors, Organizations) to allow O(1) access to specific fields and efficient partial updates. +- **Sets (`SADD`/`SMEMBERS`):** Used for relationships (e.g., Organization Members) to ensure uniqueness and provide efficient membership testing. + +## Key Features + +- **Redis Integration:** Efficient hash-based storage for data +- **JSON Serialization:** Automatic conversion between Rust structs and Redis +- **Type Safety:** Strong typing with error handling +- **Performance Optimized:** Hash operations for fast field-level access +- **Flexible Client:** Generic over Redis client implementations +- **Atomic Operations:** Uses Redis atomicity guarantees for data integrity + +## Actor Repository + +The `ActorRepository` handles persistence for player data. + +### Storage Format +Actors are stored in Redis as hash maps: +```text +actor:{uid} -> Hash { + "uid": "76561198123456789", + "name": "PlayerName", + "bank": "1500.0", + ... +} +``` + +### Usage Example + +```rust +use forge_repositories::ActorRepository; +use forge_models::Actor; + +async fn example_usage(repo: &R) -> Result<(), String> { + // 1. Create + let actor = Actor::new("76561198123456789".to_string())?; + repo.create(&actor)?; + + // 2. Retrieve + if let Some(retrieved) = repo.get_by_id("76561198123456789")? { + println!("Found actor: {}", retrieved.name()); + } + + // 3. Update + // Updates are atomic and preserve fields not present in the update + let mut actor_to_update = repo.get_by_id("76561198123456789")?.unwrap(); + // ... modify actor ... + repo.update(&actor_to_update)?; + + // 4. Check Existence + if repo.exists("76561198123456789")? { + println!("Actor exists"); + } + + // 5. Delete + repo.delete("76561198123456789")?; + + Ok(()) +} +``` + +## Organization Repository + +The `OrgRepository` handles persistence for organizations (guilds/clans) and their members. + +### Storage Format +- **Org Data:** `org:{org_id}` (Hash) +- **Members:** `org:{org_id}:members` (Set) + +### Usage Example + +```rust +use forge_repositories::OrgRepository; +use forge_models::{Org, MemberSummary}; + +async fn example_usage(repo: &R) -> Result<(), String> { + // 1. Create Organization + let org = Org::new("elite_squad".to_string(), "leader_uid".to_string(), "Elite Squad".to_string())?; + repo.create(&org)?; + + // 2. Manage Members + // Add a member (idempotent, handles duplicates) + repo.add_member("elite_squad", "member_uid_1")?; + + // Get all members + let members = repo.get_members("elite_squad")?; + for member in members { + println!("Member: {} ({})", member.name, member.uid); + } + + // Remove a member + repo.remove_member("elite_squad", "member_uid_1")?; + + // 3. Update Organization + let mut org_update = repo.get_by_id("elite_squad")?.unwrap(); + // ... modify org ... + repo.update(&org_update)?; + + // 4. Delete Organization + // Note: This removes the org data but may require separate cleanup for members depending on implementation + repo.delete("elite_squad")?; + + Ok(()) +} +``` + +## Performance & Implementation Details + +### Atomicity +- **Upserts:** `create` and `update` operations use `HMSET` which is atomic. This means either all fields are updated or none are. +- **Schema Evolution:** New fields added to the Rust structs are automatically persisted to Redis. Old fields in Redis that are no longer in the struct are **preserved** (not deleted) during updates, allowing for safe backward compatibility. + +### Thread Safety +All repository implementations are `Send + Sync`, allowing them to be safely shared across threads. The underlying `RedisClient` handles connection pooling and concurrent access. + +### Error Handling +Repositories return `Result` (or custom error types) to propagate database failures, serialization errors, or validation issues up to the service layer. + +## Contributing + +We welcome contributions to the Forge Repository Layer! This guide will help you understand how to add new repositories and maintain the existing codebase. + +### Adding a New Repository + +To add a new repository (e.g., `ItemRepository`), follow these steps: + +1. **Create the Module**: Create a new file in `src/` (e.g., `src/item.rs`). +2. **Define the Trait**: Define a trait that specifies the data access operations. Ensure it requires `Send + Sync`. + ```rust + pub trait ItemRepository: Send + Sync { + fn create(&self, item: &Item) -> Result<(), String>; + fn get_by_id(&self, id: &str) -> Result, String>; + // ... other methods + } + ``` +3. **Implement for Redis**: Implement the trait for a generic `RedisClient`. Use `forge_shared` helpers for value conversion if needed. + ```rust + use forge_shared::{RedisClient, parse_json_value, parse_redis_value}; + + pub struct RedisItemRepository { + client: C, + } + + impl RedisItemRepository { + pub fn new(client: C) -> Self { + Self { client } + } + } + + impl ItemRepository for RedisItemRepository { + fn create(&self, item: &Item) -> Result<(), String> { + let redis_key = format!("item:{}", item.id); + // ... serialization logic ... + // Use self.client to interact with Redis + self.client.hash_mset(redis_key, fields) + } + + // ... other methods + } + ``` +4. **Register the Module**: Add your new module to `src/lib.rs` and export the trait and implementation. + ```rust + pub mod item; + pub use item::{ItemRepository, RedisItemRepository}; + ``` + +### Testing + +- **Integration Tests**: Write integration tests that use a real Redis instance (if available) or a mock. +- **Mocking**: For unit testing services, you don't need to test the repository implementation itself, but you should ensure the repository trait is easy to mock. + +### Best Practices + +- **Abstraction**: Keep the repository trait agnostic of the underlying database technology (e.g., don't expose Redis-specific types in the trait signature). +- **Serialization**: Handle serialization/deserialization within the repository implementation. The service layer should work with domain models, not raw JSON or database rows. +- **Keyspace**: Use a consistent naming convention for Redis keys (e.g., `entity:id`). +- **Atomicity**: Use Redis transactions or atomic commands where possible to ensure data consistency. diff --git a/lib/repositories/src/actor.rs b/lib/repositories/src/actor.rs new file mode 100644 index 0000000..c20fcd8 --- /dev/null +++ b/lib/repositories/src/actor.rs @@ -0,0 +1,161 @@ +//! Actor repository implementation for data persistence operations. +//! +//! This module provides the data access layer for actor (player) management, +//! implementing the repository pattern to abstract database operations. +//! +//! For full documentation and examples, see the [crate README](../README.md). + +use forge_models::Actor; +use forge_shared::{RedisClient, parse_json_value, parse_redis_value}; + +/// Repository trait defining the contract for actor data operations. +/// +/// This trait abstracts the data persistence layer, allowing different +/// implementations (Redis, PostgreSQL, etc.) while maintaining a consistent +/// interface for the service layer. All implementations must be thread-safe. +pub trait ActorRepository: Send + Sync { + /// Creates a new actor in the repository. + fn create(&self, actor: &Actor) -> Result<(), String>; + + /// Retrieves an actor by their unique identifier. + fn get_by_id(&self, id: &str) -> Result, String>; + + /// Updates an existing actor with new data. + fn update(&self, actor: &Actor) -> Result<(), String>; + + /// Permanently removes an actor from the repository. + fn delete(&self, id: &str) -> Result<(), String>; + + /// Checks if an actor exists in the repository. + fn exists(&self, id: &str) -> Result; +} + +/// Redis-based implementation of the ActorRepository trait. +/// +/// This implementation uses Redis hash maps to store actor data, providing +/// efficient field-level access and atomic operations. Each actor is stored +/// as a separate hash with the key format `actor:{uid}`. +pub struct RedisActorRepository { + /// The Redis client used for all database operations. + /// + /// This client handles the actual communication with Redis, including + /// connection management, command execution, and error handling. + client: C, +} + +impl RedisActorRepository { + /// Creates a new Redis actor repository with the provided client. + pub fn new(client: C) -> Self { + Self { client } + } +} + +impl ActorRepository for RedisActorRepository { + /// Creates a new actor in Redis using hash map storage. + /// + /// Stores each actor as a Redis hash map with the key format `actor:{uid}`. + /// Each field of the actor struct becomes a field in the Redis hash. + fn create(&self, actor: &Actor) -> Result<(), String> { + // Generate Redis key using actor UID + let redis_key = format!("actor:{}", actor.uid()); + + // Serialize actor to JSON string + let actor_json = serde_json::to_string(actor) + .map_err(|e| format!("Failed to serialize actor: {}", e))?; + + // Parse JSON string back to Value for field extraction + let json_value: serde_json::Value = serde_json::from_str(&actor_json) + .map_err(|e| format!("Failed to parse actor JSON: {}", e))?; + + // Extract fields from JSON object + if let serde_json::Value::Object(actor_map) = json_value { + // Convert each field to Redis-compatible format + let fields: Vec<(String, String)> = actor_map + .into_iter() + .map(|(field, value)| (field, parse_json_value(&value))) + .collect(); + + // Store all fields atomically using Redis HMSET + self.client.hash_mset(redis_key, fields) + } else { + Err("Failed to convert actor to object".to_string()) + } + } + + /// Retrieves an actor from Redis by their unique identifier. + /// + /// Uses Redis HGETALL to retrieve all fields of the actor hash map, + /// then reconstructs the Actor struct through JSON deserialization. + fn get_by_id(&self, id: &str) -> Result, String> { + // Generate Redis key using actor UID + let redis_key = format!("actor:{}", id); + + // Retrieve all hash fields from Redis + let actor_string = self.client.hash_get_all(redis_key)?; + + // Return None if no data found (actor doesn't exist) + if actor_string.is_empty() { + return Ok(None); + } + + // Parse comma-separated field-value pairs + let parts: Vec<&str> = actor_string.split(", ").collect(); + let mut json_map = serde_json::Map::new(); + let mut i = 0; + + // Process pairs of field names and values + while i + 1 < parts.len() { + let key = parts[i]; + let value = parts[i + 1]; + + // Convert Redis string value back to proper JSON type + let json_value = parse_redis_value(value); + json_map.insert(key.to_string(), json_value); + + i += 2; // Move to next field-value pair + } + + // Reconstruct Actor from JSON object + let json_obj = serde_json::Value::Object(json_map); + match serde_json::from_value::(json_obj) { + Ok(actor) => Ok(Some(actor)), + // Return None for any deserialization errors (corrupted data) + Err(_) => Ok(None), + } + } + + /// Updates an existing actor with the provided data. + /// + /// Uses `HMSET` to atomically update the provided fields. Fields present in Redis + /// but missing from the input are preserved. + fn update(&self, actor: &Actor) -> Result<(), String> { + // Delegate to create() which handles both creation and updates + // Redis HMSET naturally supports upsert behavior + self.create(actor) + } + + /// Permanently deletes an actor and all associated data from Redis. + /// + /// Removes the entire Redis hash containing the actor's data. + /// This operation is irreversible. + fn delete(&self, id: &str) -> Result<(), String> { + // Generate Redis key using actor UID + let redis_key = format!("actor:{}", id); + + // Delete the entire hash key from Redis + // This removes all fields and the key itself atomically + self.client.delete_key(redis_key) + } + + /// Checks if an actor exists in Redis without retrieving the data. + /// + /// Uses Redis EXISTS command for a lightweight check. + fn exists(&self, id: &str) -> Result { + // Generate Redis key using actor UID + let redis_key = format!("actor:{}", id); + + // Check if the key exists in Redis + // This is a lightweight operation that doesn't retrieve data + self.client.key_exists(redis_key) + } +} diff --git a/lib/repositories/src/bank.rs b/lib/repositories/src/bank.rs new file mode 100644 index 0000000..11940d6 --- /dev/null +++ b/lib/repositories/src/bank.rs @@ -0,0 +1,161 @@ +//! Bank repository implementation for data persistence operations. +//! +//! This module provides the data access layer for bank account management, +//! implementing the repository pattern to abstract database operations. +//! +//! For full documentation and examples, see the [crate README](../README.md). + +use forge_models::Bank; +use forge_shared::{RedisClient, parse_json_value, parse_redis_value}; + +/// Repository trait defining the contract for bank data operations. +/// +/// This trait abstracts the data persistence layer, allowing different +/// implementations (Redis, PostgreSQL, etc.) while maintaining a consistent +/// interface for the service layer. All implementations must be thread-safe. +pub trait BankRepository: Send + Sync { + /// Creates a new bank in the repository. + fn create(&self, bank: &Bank) -> Result<(), String>; + + /// Retrieves an bank by their unique identifier. + fn get_by_id(&self, id: &str) -> Result, String>; + + /// Updates an existing bank with new data. + fn update(&self, bank: &Bank) -> Result<(), String>; + + /// Permanently removes an bank from the repository. + fn delete(&self, id: &str) -> Result<(), String>; + + /// Checks if an bank exists in the repository. + fn exists(&self, id: &str) -> Result; +} + +/// Redis-based implementation of the BankRepository trait. +/// +/// This implementation uses Redis hash maps to store bank data, providing +/// efficient field-level access and atomic operations. Each bank is stored +/// as a separate hash with the key format `bank:{uid}`. +pub struct RedisBankRepository { + /// The Redis client used for all database operations. + /// + /// This client handles the actual communication with Redis, including + /// connection management, command execution, and error handling. + client: C, +} + +impl RedisBankRepository { + /// Creates a new Redis bank repository with the provided client. + pub fn new(client: C) -> Self { + Self { client } + } +} + +impl BankRepository for RedisBankRepository { + /// Creates a new bank in Redis using hash map storage. + /// + /// Stores each bank as a Redis hash map with the key format `bank:{uid}`. + /// Each field of the bank struct becomes a field in the Redis hash. + fn create(&self, bank: &Bank) -> Result<(), String> { + // Generate Redis key using bank UID + let redis_key = format!("bank:{}", bank.uid()); + + // Serialize bank to JSON string + let bank_json = + serde_json::to_string(bank).map_err(|e| format!("Failed to serialize bank: {}", e))?; + + // Parse JSON string back to Value for field extraction + let json_value: serde_json::Value = serde_json::from_str(&bank_json) + .map_err(|e| format!("Failed to parse bank JSON: {}", e))?; + + // Extract fields from JSON object + if let serde_json::Value::Object(bank_map) = json_value { + // Convert each field to Redis-compatible format + let fields: Vec<(String, String)> = bank_map + .into_iter() + .map(|(field, value)| (field, parse_json_value(&value))) + .collect(); + + // Store all fields atomically using Redis HMSET + self.client.hash_mset(redis_key, fields) + } else { + Err("Failed to convert bank to object".to_string()) + } + } + + /// Retrieves an bank from Redis by their unique identifier. + /// + /// Uses Redis HGETALL to retrieve all fields of the bank hash map, + /// then reconstructs the Bank struct through JSON deserialization. + fn get_by_id(&self, id: &str) -> Result, String> { + // Generate Redis key using bank UID + let redis_key = format!("bank:{}", id); + + // Retrieve all hash fields from Redis + let bank_string = self.client.hash_get_all(redis_key)?; + + // Return None if no data found (bank doesn't exist) + if bank_string.is_empty() { + return Ok(None); + } + + // Parse comma-separated field-value pairs + let parts: Vec<&str> = bank_string.split(", ").collect(); + let mut json_map = serde_json::Map::new(); + let mut i = 0; + + // Process pairs of field names and values + while i + 1 < parts.len() { + let key = parts[i]; + let value = parts[i + 1]; + + // Convert Redis string value back to proper JSON type + let json_value = parse_redis_value(value); + json_map.insert(key.to_string(), json_value); + + i += 2; // Move to next field-value pair + } + + // Reconstruct Bank from JSON object + let json_obj = serde_json::Value::Object(json_map); + match serde_json::from_value::(json_obj) { + Ok(bank) => Ok(Some(bank)), + // Return None for any deserialization errors (corrupted data) + Err(_) => Ok(None), + } + } + + /// Updates an existing bank with the provided data. + /// + /// Uses `HMSET` to atomically update the provided fields. Fields present in Redis + /// but missing from the input are preserved. + fn update(&self, bank: &Bank) -> Result<(), String> { + // Delegate to create() which handles both creation and updates + // Redis HMSET naturally supports upsert behavior + self.create(bank) + } + + /// Permanently deletes an bank and all associated data from Redis. + /// + /// Removes the entire Redis hash containing the bank's data. + /// This operation is irreversible. + fn delete(&self, id: &str) -> Result<(), String> { + // Generate Redis key using bank UID + let redis_key = format!("bank:{}", id); + + // Delete the entire hash key from Redis + // This removes all fields and the key itself atomically + self.client.delete_key(redis_key) + } + + /// Checks if an bank exists in Redis without retrieving the data. + /// + /// Uses Redis EXISTS command for a lightweight check. + fn exists(&self, id: &str) -> Result { + // Generate Redis key using bank UID + let redis_key = format!("bank:{}", id); + + // Check if the key exists in Redis + // This is a lightweight operation that doesn't retrieve data + self.client.key_exists(redis_key) + } +} diff --git a/lib/repositories/src/lib.rs b/lib/repositories/src/lib.rs new file mode 100644 index 0000000..3860e2b --- /dev/null +++ b/lib/repositories/src/lib.rs @@ -0,0 +1,10 @@ +pub mod actor; +pub mod bank; +pub mod org; + +pub use actor::{ActorRepository, RedisActorRepository}; +pub use bank::{BankRepository, RedisBankRepository}; +pub use org::{OrgRepository, RedisOrgRepository}; + +// Re-export RedisClient from shared library for convenience +pub use forge_shared::RedisClient; diff --git a/lib/repositories/src/org.rs b/lib/repositories/src/org.rs new file mode 100644 index 0000000..2997eea --- /dev/null +++ b/lib/repositories/src/org.rs @@ -0,0 +1,248 @@ +//! Organization repository implementation for data persistence operations. +//! +//! This module provides the data access layer for organization (guild/clan) management, +//! implementing the repository pattern to abstract database operations. +//! +//! For full documentation and examples, see the [crate README](../README.md). + +use forge_models::{MemberSummary, Org}; +use forge_shared::{RedisClient, parse_json_value, parse_redis_value}; + +/// Repository trait defining the contract for organization data operations. +/// +/// This trait abstracts the data persistence layer, allowing different +/// implementations (Redis, PostgreSQL, etc.) while maintaining a consistent +/// interface for the service layer. All implementations must be thread-safe. +pub trait OrgRepository: Send + Sync { + /// Creates a new organization in the repository. + fn create(&self, org: &Org) -> Result<(), String>; + + /// Retrieves an organization by its unique identifier. + fn get_by_id(&self, id: &str) -> Result, String>; + + /// Updates an existing organization with new data. + fn update(&self, org: &Org) -> Result<(), String>; + + /// Permanently removes an organization from the repository. + fn delete(&self, id: &str) -> Result<(), String>; + + /// Checks if an organization exists in the repository. + fn exists(&self, id: &str) -> Result; + + /// Adds a new member UID to an organization. + fn add_member(&self, org_id: &str, member_uid: &str) -> Result<(), String>; + + /// Retrieves all members of an organization as a list of MemberSummary objects. + fn get_members(&self, org_id: &str) -> Result, String>; + + /// Removes a specific member from an organization. + fn remove_member(&self, org_id: &str, member_uid: &str) -> Result<(), String>; +} + +/// Redis-based implementation of the OrgRepository trait. +/// +/// Uses Redis hash maps for organization data providing +/// efficient field-level access and atomic operations. Each organization is stored +/// as a seperate hash with the key format `org:{org_id}`. +/// Member lists are stored as sets with the key format `org:{org_id}:members`. +pub struct RedisOrgRepository { + /// The Redis client used for all database operations. + /// + /// This client handles the actual communication with Redis, including + /// connection management, command execution, and error handling for + /// both organization and member data operations. + client: C, +} + +impl RedisOrgRepository { + /// Creates a new Redis organization repository with the provided client. + pub fn new(client: C) -> Self { + Self { client } + } +} + +impl OrgRepository for RedisOrgRepository { + /// Creates a new organization in Redis using hash map storage. + /// + /// Stores each organization as a Redis hash map with the key format `{org_id}:org`. + /// Each field of the organization struct becomes a field in the Redis hash. + fn create(&self, org: &Org) -> Result<(), String> { + // Generate Redis key using organization ID + let redis_key = format!("org:{}", org.id()); + + // Serialize organization to JSON string + let org_json = + serde_json::to_string(org).map_err(|e| format!("Failed to serialize org: {}", e))?; + + // Parse JSON string back to Value for field extraction + let json_value: serde_json::Value = serde_json::from_str(&org_json) + .map_err(|e| format!("Failed to parse org JSON: {}", e))?; + + // Extract fields from JSON object + if let serde_json::Value::Object(org_map) = json_value { + // Convert each field to Redis-compatible format + let fields: Vec<(String, String)> = org_map + .into_iter() + .map(|(field, value)| (field, parse_json_value(&value))) + .collect(); + + // Store all fields atomically using Redis HMSET + self.client.hash_mset(redis_key, fields) + } else { + Err("Failed to convert org to object".to_string()) + } + } + + /// Retrieves an organization from Redis by its unique identifier. + /// + /// Uses Redis HGETALL to retrieve all fields of the organization hash map, + /// then reconstructs the Org struct through JSON deserialization. + fn get_by_id(&self, id: &str) -> Result, String> { + // Generate Redis key using organization ID + let redis_key = format!("org:{}", id); + + // Retrieve all hash fields from Redis + let org_string = self.client.hash_get_all(redis_key)?; + + // Return None if no data found (organization doesn't exist) + if org_string.is_empty() { + return Ok(None); + } + + // Parse comma-separated field-value pairs + let parts: Vec<&str> = org_string.split(", ").collect(); + let mut json_map = serde_json::Map::new(); + let mut i = 0; + + // Process pairs of field names and values + while i + 1 < parts.len() { + let key = parts[i]; + let value = parts[i + 1]; + + // Convert Redis string value back to proper JSON type + let json_value = parse_redis_value(value); + json_map.insert(key.to_string(), json_value); + + i += 2; // Move to next field-value pair + } + + // Reconstruct Org from JSON object + let json_obj = serde_json::Value::Object(json_map); + match serde_json::from_value::(json_obj) { + Ok(org) => Ok(Some(org)), + // Return None for any deserialization errors (corrupted data) + Err(_) => Ok(None), + } + } + + /// Updates an existing organization with the provided data. + /// + /// Uses `HMSET` to atomically update the provided fields. Fields present in Redis + /// but missing from the input are preserved. + fn update(&self, org: &Org) -> Result<(), String> { + // Delegate to create() which handles both creation and updates + // Redis HMSET naturally supports upsert behavior + self.create(org) + } + + /// Permanently deletes an organization and all associated data from Redis. + /// + /// Removes the organization hash and the associated members list. + /// This operation is irreversible. + fn delete(&self, id: &str) -> Result<(), String> { + // Generate Redis key using organization ID + let redis_key = format!("org:{}", id); + + // Delete the organization hash key from Redis + // Note: This does NOT delete member data stored separately + self.client.delete_key(redis_key) + } + + /// Checks if an organization exists in Redis without retrieving the data. + /// + /// Uses Redis EXISTS command for a lightweight check. + fn exists(&self, id: &str) -> Result { + // Generate Redis key using organization ID + let redis_key = format!("org:{}", id); + + // Check if the key exists in Redis + // This is a lightweight operation that doesn't retrieve data + self.client.key_exists(redis_key) + } + + /// Adds a new member to the organization. + /// + /// Stores member data in a Redis list associated with the organization. + /// Validates that the organization exists before adding the member. + fn add_member(&self, org_id: &str, member_uid: &str) -> Result<(), String> { + // Check if organization exists + if !self.exists(org_id)? { + return Err(format!("Organization {} does not exist", org_id)); + } + + // Generate Redis key for organization member set + let redis_key = format!("org:{}:members", org_id); + + // Add member UID to set using SADD + self.client.set_add(redis_key, member_uid.to_string()) + } + + /// Retrieves all members of the organization. + /// + /// Uses Redis SMEMBERS to get all member UIDs, then retrieves member details. + /// Returns a list of `MemberSummary` objects. + fn get_members(&self, org_id: &str) -> Result, String> { + // Generate Redis key for organization member set + let redis_key = format!("org:{}:members", org_id); + + // Retrieve all member UIDs from the set; fall back to empty on error + let uids: Vec = match self.client.set_members(redis_key) { + Ok(v) => v, + Err(_) => Vec::new(), + }; + + // Pre-allocate result vector + let mut result: Vec = Vec::with_capacity(uids.len()); + + for uid in uids { + if uid.trim().is_empty() { + continue; + } + + // Lookup actor name by UID; fall back to "Unknown" on error/missing + let actor_key = format!("actor:{}", uid); + let raw_name = match self.client.hash_get(actor_key, "name".to_string()) { + Ok(n) => n, + _ => String::new(), + }; + + let name = match parse_redis_value(&raw_name) { + serde_json::Value::String(s) => s, + serde_json::Value::Number(n) => n.to_string(), + serde_json::Value::Bool(b) => b.to_string(), + _ => "Unknown".to_string(), + }; + + let name = if name.trim().is_empty() { + "Unknown".to_string() + } else { + name + }; + + result.push(MemberSummary { uid, name }); + } + + Ok(result) + } + + /// Removes a specific member UID from an organization. + /// + /// Uses Redis SREM to remove the UID from the organization's member set. + fn remove_member(&self, org_id: &str, member_uid: &str) -> Result<(), String> { + // Generate Redis key for organization member set + let redis_key = format!("org:{}:members", org_id); + + // Remove the UID from the set using SREM + self.client.set_del(redis_key, member_uid.to_string()) + } +} diff --git a/lib/services/Cargo.toml b/lib/services/Cargo.toml new file mode 100644 index 0000000..9eb3cb1 --- /dev/null +++ b/lib/services/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "forge-services" +version = "0.1.0" +edition = "2024" + +[dependencies] +forge-models = { path = "../models" } +forge-repositories = { path = "../repositories" } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } diff --git a/lib/services/README.md b/lib/services/README.md new file mode 100644 index 0000000..e0b25cc --- /dev/null +++ b/lib/services/README.md @@ -0,0 +1,174 @@ +# Forge Services + +This crate implements the service layer for the Forge application, containing the core business logic and orchestration. + +## Architecture + +The service layer sits between the API/Extension layer and the Repository layer: + +```mermaid +graph TD + Extension[Extension Layer] + Services[Services Layer
#40;This Module#41;] + Repositories[Repositories Layer] + Database[Database] + + Extension --> Services + Services --> Repositories + Repositories --> Database +``` + +## Responsibilities + +- **Business Logic:** Enforces game rules and constraints. +- **Validation:** Validates input data before processing. +- **Orchestration:** Coordinates operations across multiple repositories. +- **Error Handling:** Converts technical errors into business-friendly messages. +- **Data Transformation:** Handles JSON parsing and model conversion. + +## Actor Service + +The `ActorService` manages player lifecycle and state. + +### Key Features +- **Get-or-Create:** Automatically creates new actors with default values if they don't exist. +- **JSON Integration:** Directly accepts JSON strings for updates and creation. +- **Partial Updates:** Supports updating specific fields without overwriting the entire actor. +- **UID Protection:** Prevents modification of immutable fields like UIDs. + +### Usage Example + +```rust +use forge_services::ActorService; +use forge_repositories::RedisActorRepository; + +// Initialize +let repo = RedisActorRepository::new(client); +let service = ActorService::new(repo); + +// 1. Get Actor (creates default if missing) +let actor = service.get_actor("76561198123456789".to_string())?; + +// 2. Create/Overwrite Actor +let json_data = r#"{"name": "NewPlayer", "bank": 1000.0}"#; +service.create_actor("76561198123456789".to_string(), json_data.to_string())?; + +// 3. Update Actor (Partial) +let update_json = r#"{"bank": 1500.0}"#; +service.update_actor("76561198123456789".to_string(), update_json.to_string())?; + +// 4. Check Existence +if service.actor_exists("76561198123456789".to_string())? { + println!("Actor exists"); +} + +// 5. Delete Actor +// 5. Delete Actor +service.delete_actor("76561198123456789".to_string())?; +``` + +## Organization Service + +The `OrgService` manages organization (guild/clan) lifecycle and member management. + +### Key Features +- **Get-or-Create:** Automatically creates new organizations with default values if they don't exist. +- **Member Management:** Handles adding and removing members with validation. +- **Duplicate Prevention:** Ensures unique organization IDs and member UIDs. +- **Name Validation:** Enforces non-empty organization names. + +### Usage Example + +```rust +use forge_services::OrgService; +use forge_repositories::RedisOrgRepository; + +// Initialize +let repo = RedisOrgRepository::new(client); +let service = OrgService::new(repo); + +// 1. Get Organization (creates default if missing) +let org = service.get_org("elite_squad".to_string())?; + +// 2. Create/Overwrite Organization +let json_data = r#"{"name": "Elite Squad", "description": "Best players", "leader": "76561198123456789"}"#; +service.create_org("elite_squad".to_string(), json_data.to_string())?; + +// 3. Add Member +let member_json = r#"{"uid": "76561198987654321", "rank": "member"}"#; +service.add_member("elite_squad".to_string(), member_json.to_string())?; + +// 4. Update Organization +let update_json = r#"{"description": "New description"}"#; +service.update_org("elite_squad".to_string(), update_json.to_string())?; + +// 5. Check Existence +if service.org_exists("elite_squad".to_string())? { + println!("Organization exists"); +} +``` + +## Error Handling + +The service layer returns `Result` where the error string is a descriptive message suitable for logging or displaying to administrators. It wraps lower-level repository errors with additional context. + +## Contributing + +We welcome contributions to the Forge Service Layer! This guide will help you understand how to add new services and maintain the existing codebase. + +### Adding a New Service + +To add a new service (e.g., `ItemService`), follow these steps: + +1. **Create the Module**: Create a new file in `src/` (e.g., `src/item.rs`). +2. **Define the Struct**: Define your service struct with a generic repository. + ```rust + pub struct ItemService { + repository: R, + } + ``` +3. **Implement `new`**: Provide a constructor that accepts the repository. + ```rust + impl ItemService { + pub fn new(repository: R) -> Self { + Self { repository } + } + } + ``` +4. **Implement Business Logic**: Add methods for your business logic (e.g., `create_item`, `transfer_item`). + ```rust + impl ItemService { + pub fn create_item(&self, item_id: String, data: String) -> Result { + // Validation logic... + if self.repository.exists(&item_id)? { + return Err("Item already exists".to_string()); + } + // ... logic to create item + let item = Item::new(item_id); + self.repository.create(&item)?; + Ok(item) + } + } + ``` +5. **Register the Module**: Add your new module to `src/lib.rs` and export the service struct. + ```rust + pub mod item; + pub use item::ItemService; + ``` + +### Testing + +- **Unit Tests**: Write unit tests for your business logic. +- **Mocking**: Since services use generic repositories, you can easily mock them for testing without a real database. + ```rust + // Example Mock + struct MockRepo; + impl ItemRepository for MockRepo { ... } + ``` + +### Best Practices + +- **Validation**: Always validate data at the service boundary. Do not rely on the API layer or database constraints alone. +- **Error Messages**: Return user-friendly error messages. Avoid exposing internal database errors directly. +- **Immutability**: Respect immutable fields (like UIDs). +- **Documentation**: Document public methods with doc comments (`///`) explaining their purpose, arguments, and return values. diff --git a/lib/services/src/actor.rs b/lib/services/src/actor.rs new file mode 100644 index 0000000..f095821 --- /dev/null +++ b/lib/services/src/actor.rs @@ -0,0 +1,133 @@ +//! Actor service layer providing business logic for actor management operations. +//! +//! Implements the service layer of the actor management system, handling business logic, +//! validation, and orchestration. +//! +//! For full documentation, architecture, and examples, see the [crate README](../README.md). + +use forge_models::Actor; +use forge_repositories::ActorRepository; + +/// Service layer implementation for actor business logic and operations. +/// +/// Orchestrates actor management operations, handling business logic, validation, +/// and data transformation. See [crate README](../README.md) for details. +/// +/// # Thread Safety +/// Thread-safe when used with a thread-safe repository. +pub struct ActorService { + /// The repository instance used for all data persistence operations. + /// + /// This repository handles the actual storage and retrieval of actor data, + /// abstracting away the specific database implementation details. + repository: R, +} + +impl ActorService { + /// Creates a new actor service with the provided repository. + /// + /// The repository must be initialized and ready for use. + pub fn new(repository: R) -> Self { + Self { repository } + } + + /// Creates a new actor with the provided ID and JSON data. + /// + /// Handles validation, duplicate checking, and persistence. + /// See [crate README](../README.md) for JSON format and business rules. + pub fn create_actor(&self, actor_id: String, json_data: String) -> Result { + // Create base actor with the provided UID + let new_actor = Actor::new(actor_id.clone()).map_err(|e| e.to_string())?; + + // Check if actor already exists to prevent duplicates + if self.repository.exists(&actor_id)? { + return Err(format!("Actor with UID '{}' already exists", actor_id)); + } + + // Parse and validate JSON input + let json_obj: serde_json::Value = + serde_json::from_str(&json_data).map_err(|e| format!("Invalid Actor JSON: {}", e))?; + + // Ensure JSON is an object (not array, string, etc.) + if let serde_json::Value::Object(mut map) = json_obj { + // Remove UID field to prevent conflicts (UID is immutable) + map.remove("uid"); + let update_data = serde_json::Value::Object(map); + + // Apply JSON data to the base actor + let mut final_actor = new_actor; + final_actor.update_from_json(update_data)?; + + // Store the actor in the repository + self.repository.create(&final_actor)?; + Ok(final_actor) + } else { + Err("JSON data must be an object".to_string()) + } + } + + /// Retrieves an actor by their unique identifier with automatic fallback creation. + /// + /// Implements a "get-or-create" pattern: if the actor doesn't exist, a new one + /// with default values is returned (but not persisted). + pub fn get_actor(&self, actor_id: String) -> Result { + // Attempt to retrieve actor from repository + match self.repository.get_by_id(&actor_id)? { + // Actor found - return it + Some(actor) => Ok(actor), + // Actor not found - create fallback actor with default values + None => Actor::new(actor_id).map_err(|e| e.to_string()), + } + } + + /// Updates an existing actor with new data from JSON. + /// + /// Handles partial updates, validation, and persistence. + /// See [crate README](../README.md) for JSON format and concurrency details. + pub fn update_actor(&self, actor_id: String, json_update: String) -> Result { + // Retrieve existing actor from repository + let mut actor = match self.repository.get_by_id(&actor_id)? { + Some(actor) => actor, + None => return Err(format!("Actor with UID '{}' not found", actor_id)), + }; + + // Parse and validate JSON update data + let update_data: serde_json::Value = + serde_json::from_str(&json_update).map_err(|e| format!("Invalid JSON: {}", e))?; + + // Ensure update data is a JSON object + if !update_data.is_object() { + return Err("Update data must be a JSON object".to_string()); + } + + // Apply updates to the actor (this validates the changes) + actor.update_from_json(update_data)?; + + // Persist the updated actor to repository + self.repository.update(&actor)?; + + Ok(actor) + } + + /// Permanently deletes an actor from the system. + /// + /// Irreversible operation. Delegates to repository. + pub fn delete_actor(&self, actor_id: String) -> Result<(), String> { + // Delegate deletion to repository layer + // Future enhancements could add business logic here: + // - Authorization checks + // - Audit logging + // - Cascade deletion + // - Soft deletion + self.repository.delete(&actor_id) + } + + /// Checks if an actor exists in the system. + /// + /// Lightweight check without data retrieval. + pub fn actor_exists(&self, actor_id: String) -> Result { + // Delegate existence check to repository layer + // This is a lightweight operation that doesn't retrieve data + self.repository.exists(&actor_id) + } +} diff --git a/lib/services/src/bank.rs b/lib/services/src/bank.rs new file mode 100644 index 0000000..2ad20eb --- /dev/null +++ b/lib/services/src/bank.rs @@ -0,0 +1,123 @@ +//! Bank service layer providing business logic for bank management operations. +//! +//! Implements the service layer of the bank management system, handling business logic, +//! validation, and orchestration. +//! +//! For full documentation, architecture, and examples, see the [crate README](../README.md). + +use forge_models::Bank; +use forge_repositories::BankRepository; + +/// Service layer implementation for bank business logic and operations. +/// +/// Orchestrates bank management operations, handling business logic, validation, +/// and data transformation. See [crate README](../README.md) for details. +/// +/// # Thread Safety +/// Thread-safe when used with a thread-safe repository. +pub struct BankService { + /// The repository instance used for all data persistence operations. + /// + /// This repository handles the actual storage and retrieval of bank data, + /// abstracting away the specific database implementation details. + repository: R, +} + +impl BankService { + /// Creates a new bank service with the provided repository. + /// + /// The repository must be initialized and ready for use. + pub fn new(repository: R) -> Self { + Self { repository } + } + + /// Creates a new bank with the provided ID and JSON data. + /// + /// Handles validation, duplicate checking, and persistence. + /// See [crate README](../README.md) for JSON format and business rules. + pub fn create(&self, uid: String, json_data: String) -> Result { + // Parse JSON data to Bank struct + let mut bank: Bank = + serde_json::from_str(&json_data).map_err(|e| format!("Invalid Bank JSON: {}", e))?; + + bank.uid = uid; + + // Check if bank already exists to prevent duplicates + if self.repository.exists(&bank.uid)? { + return Err(format!("Bank with uid '{}' already exists", bank.uid)); + } + + if let Err(e) = bank.validate() { + return Err(format!("Invalid Bank JSON: {}", e)); + } + + self.repository.create(&bank)?; + + Ok(bank) + } + + /// Retrieves an bank by their unique identifier with automatic fallback creation. + /// + /// Implements a "get-or-create" pattern: if the bank doesn't exist, a new one + /// with default values is returned (but not persisted). + pub fn get_bank(&self, bank_id: String) -> Result { + // Attempt to retrieve bank from repository + match self.repository.get_by_id(&bank_id)? { + // Bank found - return it + Some(bank) => Ok(bank), + // Bank not found - create fallback bank with default values + None => Err(format!("Bank with UID '{}' not found", bank_id)), + } + } + + /// Updates an existing bank with new data from JSON. + /// + /// Handles partial updates, validation, and persistence. + /// See [crate README](../README.md) for JSON format and concurrency details. + pub fn update_bank(&self, bank_id: String, json_update: String) -> Result { + // Retrieve existing bank from repository + let mut bank = match self.repository.get_by_id(&bank_id)? { + Some(bank) => bank, + None => return Err(format!("Bank with UID '{}' not found", bank_id)), + }; + + // Parse and validate JSON update data + let update_data: serde_json::Value = + serde_json::from_str(&json_update).map_err(|e| format!("Invalid JSON: {}", e))?; + + // Ensure update data is a JSON object + if !update_data.is_object() { + return Err("Update data must be a JSON object".to_string()); + } + + // Apply updates to the bank (this validates the changes) + bank.update_from_json(update_data)?; + + // Persist the updated bank to repository + self.repository.update(&bank)?; + + Ok(bank) + } + + /// Permanently deletes an bank from the system. + /// + /// Irreversible operation. Delegates to repository. + pub fn delete_bank(&self, bank_id: String) -> Result<(), String> { + // Delegate deletion to repository layer + // Future enhancements could add business logic here: + // - Authorization checks + // - Audit logging + // - Cascade deletion + // - Soft deletion + self.repository.delete(&bank_id) + } + + /// Checks if an bank exists in the system. + /// + /// Lightweight check without data retrieval. + pub fn bank_exists(&self, bank_id: String) -> Result { + // Delegate existence check to repository layer + // This is a lightweight operation that doesn't retrieve data + self.repository.exists(&bank_id) + } +} diff --git a/lib/services/src/lib.rs b/lib/services/src/lib.rs new file mode 100644 index 0000000..4eca7f5 --- /dev/null +++ b/lib/services/src/lib.rs @@ -0,0 +1,7 @@ +pub mod actor; +pub mod bank; +pub mod org; + +pub use actor::ActorService; +pub use bank::BankService; +pub use org::OrgService; diff --git a/lib/services/src/org.rs b/lib/services/src/org.rs new file mode 100644 index 0000000..6c27658 --- /dev/null +++ b/lib/services/src/org.rs @@ -0,0 +1,171 @@ +//! Organization service layer providing business logic for organization management operations. +//! +//! Implements the service layer of the organization management system, handling business logic, +//! validation, and orchestration. +//! +//! For full documentation, architecture, and examples, see the [crate README](../README.md). + +use forge_models::{MemberSummary, Org}; +use forge_repositories::OrgRepository; + +/// Service layer implementation for organization business logic and operations. +/// +/// Orchestrates organization management operations, handling business logic, validation, +/// and data transformation. See [crate README](../README.md) for details. +/// +/// # Thread Safety +/// Thread-safe when used with a thread-safe repository. +pub struct OrgService { + /// The repository instance used for all data persistence operations. + /// + /// This repository handles the actual storage and retrieval of organization + /// and member data, abstracting away the specific database implementation details. + repository: R, +} + +impl OrgService { + /// Creates a new organization service with the provided repository. + /// + /// The repository must be initialized and ready for use. + pub fn new(repository: R) -> Self { + Self { repository } + } + + /// Creates a new organization with the provided ID and JSON data. + /// + /// Handles validation, duplicate checking, and persistence. + /// See [crate README](../README.md) for JSON format and business rules. + pub fn create_org(&self, key: String, json_data: String) -> Result { + // Parse JSON data to Org struct + let mut org: Org = + serde_json::from_str(&json_data).map_err(|e| format!("Invalid Org JSON: {}", e))?; + + // Override ID with the provided parameter (ensures consistency) + org.id = key; + + // Validate organization name is not empty + if org.name.trim().is_empty() { + return Err("Organization name cannot be empty".to_string()); + } + + // Check if organization already exists to prevent duplicates + if self.repository.exists(&org.id)? { + return Err(format!("Organization with ID '{}' already exists", org.id)); + } + + // Store the organization in the repository + self.repository.create(&org)?; + + Ok(org) + } + + /// Retrieves an organization by its unique identifier with automatic fallback creation. + /// + /// Implements a "get-or-create" pattern: if the organization doesn't exist, a new one + /// with default values is returned (but not persisted). + pub fn get_org(&self, key: String) -> Result { + // Attempt to retrieve organization from repository + match self.repository.get_by_id(&key)? { + // Organization found - return it + Some(org) => Ok(org), + // Organization not found - create fallback organization with default values + None => { + // Create new organization with empty leader and name (requires setup) + Org::new( + key, + "00000000000000000".to_string(), + "Forge Dynamics".to_string(), + ) + .map_err(|e| e.to_string()) + } + } + } + + /// Updates an existing organization with new data from JSON. + /// + /// Handles partial updates, validation, and persistence. + /// See [crate README](../README.md) for JSON format and concurrency details. + pub fn update_org(&self, key: String, json_update: String) -> Result { + // Retrieve existing organization from repository + let mut org = match self.repository.get_by_id(&key)? { + Some(org) => org, + None => return Err(format!("Organization with ID '{}' not found", key)), + }; + + // Parse and validate JSON update data + let update_data: serde_json::Value = + serde_json::from_str(&json_update).map_err(|e| format!("Invalid JSON: {}", e))?; + + // Ensure update data is a JSON object + if !update_data.is_object() { + return Err("Update data must be a JSON object".to_string()); + } + + // Apply updates to the organization (this validates the changes) + org.update_from_json(update_data)?; + + // Validate organization name is not empty after update + if org.name.trim().is_empty() { + return Err("Organization name cannot be empty".to_string()); + } + + // Persist the updated organization to repository + self.repository.update(&org)?; + + Ok(org) + } + + /// Permanently deletes an organization from the system. + /// + /// Irreversible operation. Delegates to repository. + pub fn delete_org(&self, key: String) -> Result<(), String> { + let redis_key = format!("org:{}", key); + let assets_key = format!("org:{}:assets", key); + let members_key = format!("org:{}:members", key); + + // Delegate deletion to repository layer + self.repository.delete(&redis_key)?; + self.repository.delete(&assets_key)?; + self.repository.delete(&members_key)?; + + Ok(()) + } + + /// Checks if an organization exists in the system. + /// + /// Lightweight check without data retrieval. + pub fn org_exists(&self, key: String) -> Result { + // Delegate existence check to repository layer + self.repository.exists(&key) + } + + /// Adds a new member UID to an organization with validation. + pub fn add_member(&self, key: String, member_uid: String) -> Result<(), String> { + // Verify organization exists before adding member + if !self.repository.exists(&key)? { + return Err(format!("Organization with ID '{}' not found", key)); + } + + // Add member UID to organization through repository + self.repository.add_member(&key, &member_uid) + } + + /// Retrieves all members of an organization as a UID to name mapping. + pub fn get_members(&self, key: String) -> Result, String> { + // Delegate member retrieval to repository layer + self.repository.get_members(&key) + } + + /// Permanently removes a specific member from an organization. + /// + /// Irreversible operation. Delegates to repository. + pub fn remove_member(&self, key: String, member_uid: String) -> Result<(), String> { + // Verify organization exists before attempting member removal + if !self.repository.exists(&key)? { + return Err(format!("Organization with ID '{}' not found", key)); + } + + // Delegate member removal to repository layer + self.repository.remove_member(&key, &member_uid) + } +} diff --git a/lib/shared/Cargo.toml b/lib/shared/Cargo.toml new file mode 100644 index 0000000..8de94b7 --- /dev/null +++ b/lib/shared/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "forge-shared" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde_json = { workspace = true } +arma-rs = { workspace = true } diff --git a/lib/shared/src/lib.rs b/lib/shared/src/lib.rs new file mode 100644 index 0000000..7d92c7b --- /dev/null +++ b/lib/shared/src/lib.rs @@ -0,0 +1,25 @@ +pub mod redis_client; +pub mod validation; + +pub use redis_client::{RedisClient, parse_json_value, parse_redis_value}; +pub use validation::{ActorValidationError, BankValidationError, OrgValidationError}; + +/// Converts an arma_rs::Value to a serde_json::Value. +/// +/// This helper function is used to bridge the gap between Arma's SQF data types +/// and standard JSON, which is used for storage and API communication. +pub fn arma_value_to_json(arma_value: &arma_rs::Value) -> serde_json::Value { + match arma_value { + arma_rs::Value::String(s) => serde_json::Value::String(s.clone()), + arma_rs::Value::Number(n) => serde_json::Number::from_f64(*n) + .map(serde_json::Value::Number) + .unwrap_or(serde_json::Value::Null), + arma_rs::Value::Boolean(b) => serde_json::Value::Bool(*b), + arma_rs::Value::Array(arr) => { + let json_array: Vec = arr.iter().map(arma_value_to_json).collect(); + serde_json::Value::Array(json_array) + } + arma_rs::Value::Null => serde_json::Value::Null, + arma_rs::Value::Unknown(s) => serde_json::Value::String(s.clone()), + } +} diff --git a/lib/shared/src/redis_client.rs b/lib/shared/src/redis_client.rs new file mode 100644 index 0000000..3513c5b --- /dev/null +++ b/lib/shared/src/redis_client.rs @@ -0,0 +1,67 @@ +/// Redis client abstraction for dependency injection +pub trait RedisClient: Send + Sync { + // Hash operations + fn hash_mset(&self, key: String, fields: Vec<(String, String)>) -> Result<(), String>; + fn hash_get_all(&self, key: String) -> Result; + fn hash_get(&self, key: String, field: String) -> Result; + fn hash_del(&self, key: String, field: String) -> Result<(), String>; + + // List operations + fn list_rpush(&self, key: String, value: String) -> Result<(), String>; + fn list_range(&self, key: String, start: isize, end: isize) -> Result, String>; + fn list_del(&self, key: String, count: isize, value: String) -> Result<(), String>; + + // Set operations + fn set_add(&self, key: String, member: String) -> Result<(), String>; + fn set_members(&self, key: String) -> Result, String>; + fn set_del(&self, key: String, member: String) -> Result<(), String>; + + // Common operations + fn key_exists(&self, key: String) -> Result; + fn delete_key(&self, key: String) -> Result<(), String>; +} + +/// Converts a JSON value to a Redis-compatible string format. +pub fn parse_json_value(value: &serde_json::Value) -> String { + let wrapped = serde_json::Value::Array(vec![value.clone()]); + wrapped.to_string() +} + +/// Converts a Redis string value back to a JSON value with intelligent type detection. +pub fn parse_redis_value(value: &str) -> serde_json::Value { + // Handle empty values + if value.is_empty() { + return serde_json::Value::Null; + } + + // Try to parse as JSON first + if let Ok(json_val) = serde_json::from_str(value) { + // Special handling for single-element arrays (unwrap them) + if let serde_json::Value::Array(arr) = &json_val { + if arr.len() == 1 { + return arr[0].clone(); + } + } + return json_val; + } + + // Try to parse as integer + if let Ok(int_val) = value.parse::() { + return serde_json::Value::Number(serde_json::Number::from(int_val)); + } + + // Try to parse as float + if let Ok(float_val) = value.parse::() { + if let Some(num) = serde_json::Number::from_f64(float_val) { + return serde_json::Value::Number(num); + } + } + + // Try to parse as boolean (case-insensitive) + match value.to_lowercase().as_str() { + "true" => serde_json::Value::Bool(true), + "false" => serde_json::Value::Bool(false), + // Default to string if no other type matches + _ => serde_json::Value::String(value.to_string()), + } +} diff --git a/lib/shared/src/validation.rs b/lib/shared/src/validation.rs new file mode 100644 index 0000000..2abb143 --- /dev/null +++ b/lib/shared/src/validation.rs @@ -0,0 +1,148 @@ +use std::fmt; + +/// Validation errors for Actor model +#[derive(Debug, Clone)] +pub enum ActorValidationError { + EmptyUid, + InvalidName(String), + InvalidUid(String), + InvalidPosition(String), + InvalidDirection(f64), + InvalidEmail(String), + InvalidPhoneNumber(String), + InvalidState(String), + InvalidRank(String), + InvalidOrganization(String), + LoadoutError(String), + UidModificationAttempt, +} + +impl fmt::Display for ActorValidationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ActorValidationError::EmptyUid => write!(f, "UID cannot be empty"), + ActorValidationError::InvalidName(name) => write!( + f, + "Invalid name '{}' - cannot be empty or longer than 50 characters", + name + ), + ActorValidationError::InvalidUid(uid) => write!( + f, + "Invalid UID format '{}' - must be a 17-digit Steam ID", + uid + ), + ActorValidationError::InvalidPosition(msg) => write!(f, "Invalid position: {}", msg), + ActorValidationError::InvalidDirection(dir) => write!( + f, + "Invalid direction {} - must be between 0 and 360 degrees", + dir + ), + ActorValidationError::InvalidEmail(email) => write!( + f, + "Invalid email format '{}' - must contain @ and end with .mil", + email + ), + ActorValidationError::InvalidPhoneNumber(phone) => write!( + f, + "Invalid phone number '{}' - must start with 0160 and be 10 digits", + phone + ), + ActorValidationError::InvalidState(state) => write!( + f, + "Invalid state '{}' - must be HEALTHY, INJURED, INCAPACITATED, or DEAD", + state + ), + ActorValidationError::InvalidRank(rank) => write!( + f, + "Invalid rank '{}' - cannot be empty or longer than 50 characters", + rank + ), + ActorValidationError::InvalidOrganization(org) => write!( + f, + "Invalid organization '{}' - cannot be empty or longer than 100 characters", + org + ), + ActorValidationError::LoadoutError(msg) => write!(f, "Loadout error: {}", msg), + ActorValidationError::UidModificationAttempt => write!( + f, + "UID cannot be modified - it's the player's permanent Steam ID" + ), + } + } +} + +impl std::error::Error for ActorValidationError {} + +/// Validation errors for Organization model +#[derive(Debug, Clone)] +pub enum OrgValidationError { + EmptyId, + EmptyOwner, + EmptyName, + NegativeFunds, + InvalidId(String), + InvalidOwner(String), + InvalidName(String), +} + +impl fmt::Display for OrgValidationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + OrgValidationError::EmptyId => write!(f, "Organization ID cannot be empty"), + OrgValidationError::EmptyOwner => write!(f, "Organization owner cannot be empty"), + OrgValidationError::EmptyName => write!(f, "Organization name cannot be empty"), + OrgValidationError::NegativeFunds => { + write!(f, "Organization funds cannot be negative") + } + OrgValidationError::InvalidId(id) => write!( + f, + "Invalid organization ID '{}' - must contain only alphanumeric characters and underscores", + id + ), + OrgValidationError::InvalidOwner(owner) => { + write!(f, "Invalid owner '{}' - must be a 17-digit Steam ID", owner) + } + OrgValidationError::InvalidName(name) => write!( + f, + "Invalid organization name '{}' - cannot exceed 100 characters or contain control characters", + name + ), + } + } +} + +impl std::error::Error for OrgValidationError {} + +/// Validation errors for Bank model +#[derive(Debug, Clone)] +pub enum BankValidationError { + UidEmpty, + NameEmpty, + BankNegative, + CashNegative, + InvalidEmail(String), + InvalidPhoneNumber(String), +} + +impl fmt::Display for BankValidationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BankValidationError::UidEmpty => write!(f, "UID cannot be empty"), + BankValidationError::NameEmpty => write!(f, "Name cannot be empty"), + BankValidationError::BankNegative => write!(f, "Bank balance cannot be negative"), + BankValidationError::CashNegative => write!(f, "Cash cannot be negative"), + BankValidationError::InvalidEmail(email) => write!( + f, + "Invalid email format '{}' - must contain @ and end with .mil", + email + ), + BankValidationError::InvalidPhoneNumber(phone) => write!( + f, + "Invalid phone number '{}' - must start with 0160 and be 10 digits", + phone + ), + } + } +} + +impl std::error::Error for BankValidationError {}