Add org invite workflow and portal invite controls #3
6
.gitignore
vendored
@ -23,10 +23,6 @@ target/
|
|||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
node_modules/
|
node_modules/
|
||||||
docus/.nuxt/
|
|
||||||
docus/.output/
|
|
||||||
docus/.data/
|
|
||||||
docus/.nitro/
|
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@ -34,5 +30,3 @@ Thumbs.db
|
|||||||
|
|
||||||
# Arma
|
# Arma
|
||||||
arma/ui/map-viewer/
|
arma/ui/map-viewer/
|
||||||
arma/server/surrealdb/forge.db/
|
|
||||||
promo/
|
|
||||||
|
|||||||
@ -1,49 +1,134 @@
|
|||||||
# Forge Architecture
|
# Forge Architecture & Data Flow Diagram
|
||||||
|
|
||||||
## Runtime Flow
|
## 🏗️ **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<br/>- loadout<br/>- position<br/>- stats]
|
||||||
|
end
|
||||||
|
|
||||||
|
ClientA --- OptimisticCache
|
||||||
|
ClientB --- OptimisticCache
|
||||||
|
ClientN --- OptimisticCache
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Server [ArmA 3 SERVER #40;Hot Cache#41;]
|
||||||
|
Registry["GVAR(Registry)<br/>In-Memory HashMap<br/>UID -> {loadout, position, stats...}"]
|
||||||
|
SessionMgmt[Session Management<br/>- Token Generation<br/>- UID Resolution<br/>- Player State]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Rust [EXTENSION #40;Cold Storage#41;]
|
||||||
|
ConnPool["Connection Pool<br/>(bb8-redis)<br/>2-10 connections"]
|
||||||
|
RedisOps[Redis Operations<br/>- actor_get/set/update<br/>- Async I/O]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Redis [DATABASE #40;Saved to Disc#41;]
|
||||||
|
ActorDataStore[Actor Data Store<br/>actor:UID -> JSON]
|
||||||
|
Modules[Additional Modules<br/>garage, locker, bank, org]
|
||||||
|
end
|
||||||
|
|
||||||
|
Clients -->|Event Driven<br/>#40;CBA A3 Events#41;| Server
|
||||||
|
Server -->|Extension Calls<br/>#40;Rust FFI#41;| Rust
|
||||||
|
Rust -->|Redis Protocol<br/>#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
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
Client[Arma Client Addons] --> Server[Arma Server Addons]
|
subgraph SessionMgmt [SERVER-SIDE #40;Session MGT#41;]
|
||||||
Server --> Bridge[Extension Bridge]
|
Conn[Player Connection] --> Token[Session Token Generation<br/>#40;Generated on server#41;]
|
||||||
Bridge --> Extension[Rust arma-rs Extension]
|
Token --> UID[UID Resolution<br/>#40;Steam UID mapping#41;]
|
||||||
Extension --> Services[Service Layer]
|
UID --> State[Player State Tracking<br/>#40;Tracked in Registry#41;]
|
||||||
Services --> Repositories[Repository Traits]
|
State --> Access[Data Access Authorized<br/>#40;Authorized via session#41;]
|
||||||
Repositories --> Surreal[(SurrealDB)]
|
end
|
||||||
```
|
|
||||||
|
|
||||||
## Persistence Startup
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant Arma as Arma Server
|
|
||||||
participant Ext as Forge Extension
|
|
||||||
participant Db as SurrealDB
|
|
||||||
|
|
||||||
Arma->>Ext: init
|
|
||||||
Ext->>Db: connect
|
|
||||||
Ext->>Db: apply schema modules
|
|
||||||
Db-->>Ext: ready
|
|
||||||
Arma->>Ext: status
|
|
||||||
Ext-->>Arma: connected
|
|
||||||
```
|
|
||||||
|
|
||||||
## Data Access
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant SQF as SQF Addon
|
|
||||||
participant Ext as Extension Command
|
|
||||||
participant Service as Service
|
|
||||||
participant Repo as Repository
|
|
||||||
participant Db as SurrealDB
|
|
||||||
|
|
||||||
SQF->>Ext: domain command
|
|
||||||
Ext->>Service: validate and execute
|
|
||||||
Service->>Repo: repository call
|
|
||||||
Repo->>Db: query/upsert/delete
|
|
||||||
Db-->>Repo: result
|
|
||||||
Repo-->>Service: domain model
|
|
||||||
Service-->>Ext: response
|
|
||||||
Ext-->>SQF: serialized result
|
|
||||||
```
|
```
|
||||||
|
|||||||
@ -12,6 +12,7 @@ resolver = "3"
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
arma-rs = { version = "1.11.15", features = ["chrono", "serde_json", "uuid"] }
|
arma-rs = { version = "1.11.15", features = ["chrono", "serde_json", "uuid"] }
|
||||||
chrono = "0.4.42"
|
chrono = "0.4.42"
|
||||||
|
redis = "1.0.0-rc.1"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
tokio = { version = "1.47.1", features = ["full"] }
|
tokio = { version = "1.47.1", features = ["full"] }
|
||||||
|
|||||||
353
README.md
@ -1,62 +1,313 @@
|
|||||||
# Forge
|
# Forge Framework
|
||||||
|
|
||||||
Forge is a framework for Arma 3 persistent game servers. It combines SQF
|
**Forge** is a high-performance, production-ready framework for Arma 3 persistent game servers, built with Rust and Redis for optimal performance and reliability.
|
||||||
addons, a Rust `arma-rs` extension, shared service crates, and web-based client
|
|
||||||
interfaces for player data, organizations, banking, garages, lockers, phones,
|
|
||||||
CAD, stores, and task workflows.
|
|
||||||
|
|
||||||
## Storage
|
## Overview
|
||||||
|
|
||||||
Durable persistence is backed by SurrealDB. The server extension loads schema
|
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.
|
||||||
modules at startup and routes domain repositories through the SurrealDB client.
|
|
||||||
|
### 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<br/>ArmA 3 Interface <---> Rust]
|
||||||
|
Services[Services Layer<br/>#40;Business Logic#41;]
|
||||||
|
Repositories[Repositories Layer<br/>#40;Data Persistence#41;]
|
||||||
|
Models[Models Layer<br/>#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
|
```toml
|
||||||
[surreal]
|
[redis]
|
||||||
endpoint = "127.0.0.1:8000"
|
host = "127.0.0.1"
|
||||||
namespace = "forge"
|
port = 6379
|
||||||
database = "main"
|
password = "" # Optional
|
||||||
username = "root"
|
max_connections = 10
|
||||||
password = "root"
|
min_connections = 2
|
||||||
connect_timeout_ms = 5000
|
idle_timeout = 300
|
||||||
```
|
```
|
||||||
|
|
||||||
## Workspace
|
### SQF Usage
|
||||||
|
|
||||||
```text
|
|
||||||
arma/
|
|
||||||
client/ Client-side addons and browser UIs
|
|
||||||
server/ Server-side addons and extension crate
|
|
||||||
bin/
|
|
||||||
icom/ Interprocess communication helper
|
|
||||||
lib/
|
|
||||||
models/ Shared domain models
|
|
||||||
repositories/ Repository traits and in-memory test stores
|
|
||||||
services/ Domain business logic
|
|
||||||
shared/ Cross-crate helpers
|
|
||||||
tools/ Web UI build tooling
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Commands
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cargo test
|
|
||||||
npm run build:webui
|
|
||||||
.\build-arma.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- [Framework Documentation](./docs/README.md)
|
|
||||||
- [Framework Architecture](./docs/FRAMEWORK_ARCHITECTURE.md)
|
|
||||||
- [Module Reference](./docs/MODULE_REFERENCE.md)
|
|
||||||
- [Development Guide](./docs/DEVELOPMENT_GUIDE.md)
|
|
||||||
- [Git Workflow](./docs/GIT_WORKFLOW.md)
|
|
||||||
|
|
||||||
## Extension Status
|
|
||||||
|
|
||||||
```sqf
|
```sqf
|
||||||
"forge_server" callExtension ["status", []];
|
// Create an actor
|
||||||
"forge_server" callExtension ["surreal:status", []];
|
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]];
|
||||||
```
|
```
|
||||||
|
|
||||||
Both commands report the persistence connection state.
|
## 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
|
||||||
|
|
||||||
|
View the License [here](LICENSE.md).
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- **Issues**: [Gitea Issues](https://gitea.innovativedevsolutions.org/IDSolutions/forge/issues)
|
||||||
|
- **Documentation**: See individual module READMEs
|
||||||
|
- **Architecture**: [Diagram](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**
|
||||||
|
|||||||
@ -1,46 +1,30 @@
|
|||||||
# Forge Client
|
<h1 align="center">Forge Client</h1>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://gitea.innovativedevsolutions.org/IDSolutions/forge/releases/latest"><img src="https://img.shields.io/gitea/v/release/IDSolutions/forge?gitea_url=https%3A%2F%2Fgitea.innovativedevsolutions.org&label=Version" alt="Version"></a>
|
||||||
|
<a href="https://gitea.innovativedevsolutions.org/IDSolutions/forge/issues"><img src="https://img.shields.io/gitea/issues/open/IDSolutions/forge?gitea_url=https%3A%2F%2Fgitea.innovativedevsolutions.org&label=Issues" alt="Issues"></a>
|
||||||
|
<!-- <a href="https://steamcommunity.com/sharedfiles/filedetails/?id=MOD_ID"><img src="https://img.shields.io/steam/downloads/MOD_ID.svg?&label=Downloads" alt="Downloads"></a> -->
|
||||||
|
<a href="https://gitea.innovativedevsolutions.org/IDSolutions/forge/src/branch/master/arma/server/LICENSE.md"><img src="https://img.shields.io/badge/License-APL%20SA-red?label=License" alt="License"></a>
|
||||||
|
<br>
|
||||||
|
<img src="https://img.shields.io/github/v/release/brettmayson/hemtt?label=HEMTT" alt="HEMTT">
|
||||||
|
<img src="https://img.shields.io/github/v/release/cbateam/cba_a3?label=CBA%20A3" alt="CBA A3">
|
||||||
|
</p>
|
||||||
|
|
||||||
Forge Client contains the Arma client-side addons for Forge. It owns player UI,
|
<p align="center">
|
||||||
browser bridges, client repositories, local event handling, and client-to-server
|
<b>Requires the latest version of <a href="https://github.com/CBATeam/CBA_A3/releases/latest">CBA A3</a></b>
|
||||||
CBA RPC requests.
|
</p>
|
||||||
|
|
||||||
The client mod pairs with `arma/server`: client addons collect player input and
|
**Forge Client** aims to...
|
||||||
render state, while server addons and the Rust extension own authoritative
|
|
||||||
state and persistence.
|
|
||||||
|
|
||||||
## Requirements
|
The project is entirely **open-source** and any contributions are welcome.
|
||||||
- CBA A3
|
|
||||||
- ACE3 for features that use ACE interactions, arsenal, spectator, or medical
|
|
||||||
integrations
|
|
||||||
- Forge Server running the matching server-side addons
|
|
||||||
|
|
||||||
## Addons
|
## Core Features
|
||||||
- `main`: shared client mod config and macros
|
|
||||||
- `common`: shared browser UI bridge helpers
|
|
||||||
- `actor`: player interaction menu and actor repository
|
|
||||||
- `bank`: banking UI and account request bridge
|
|
||||||
- `cad`: map/CAD UI for dispatch, groups, tasks, and support requests
|
|
||||||
- `garage`: vehicle storage and virtual garage UI
|
|
||||||
- `locker`: locker and virtual arsenal repositories
|
|
||||||
- `notifications`: notification HUD and sounds
|
|
||||||
- `org`: organization portal UI
|
|
||||||
- `phone`: phone, contacts, messages, and email UI
|
|
||||||
- `store`: storefront catalog and checkout UI
|
|
||||||
|
|
||||||
## UI Pattern
|
- Feature
|
||||||
Most feature UIs use an Arma display with a `CT_WEBBROWSER` control. JavaScript
|
|
||||||
sends JSON events through A3API, SQF handles them in `fnc_handleUIEvents.sqf`,
|
|
||||||
and response events are sent back into the browser with `ctrlWebBrowserAction
|
|
||||||
["ExecJS", ...]`.
|
|
||||||
|
|
||||||
Client repositories cache the most recent state for display only. Server addons
|
## Contributing
|
||||||
and the extension remain authoritative.
|
|
||||||
|
|
||||||
## Documentation
|
For new contributers, see the [Contributing Setup & Guidelines](./.github/CONTRIBUTING.md).
|
||||||
- [Root client usage guide](../../docs/CLIENT_USAGE_GUIDE.md)
|
|
||||||
- [Client docs](./docs/README.md)
|
|
||||||
- [Common web UI framework notes](./addons/common/WEB_UI_FRAMEWORK.md)
|
|
||||||
- [CAD map integration notes](./addons/cad/MAP_README.md)
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Forge Client is licensed under [APL-SA](./LICENSE.md).
|
Forge Client is licensed under [APL-SA](./LICENSE.md).
|
||||||
|
|||||||
@ -1,28 +1,3 @@
|
|||||||
# Forge Client Actor
|
# forge_client_actor
|
||||||
|
|
||||||
## Overview
|
Description for this addon
|
||||||
The actor addon owns the player interaction menu and client-side actor
|
|
||||||
repository. It initializes actor state from the server, tracks client-visible
|
|
||||||
actor fields, and routes menu actions to other Forge UIs.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
- `forge_client_main`
|
|
||||||
- server actor events from `forge_server_actor`
|
|
||||||
- runtime integrations with bank, CAD, garage, org, phone, store, locker, and
|
|
||||||
notifications addons
|
|
||||||
|
|
||||||
## Main Components
|
|
||||||
- `fnc_initRepository.sqf` manages client actor state and server init/save
|
|
||||||
requests.
|
|
||||||
- `fnc_openUI.sqf` opens `RscActorMenu`.
|
|
||||||
- `fnc_handleUIEvents.sqf` handles browser menu actions.
|
|
||||||
|
|
||||||
## Event Surface
|
|
||||||
The actor menu can open bank, ATM mode, CAD, garage, virtual garage, org, phone,
|
|
||||||
store, and ACE arsenal interactions. Client post-init also wires player killed
|
|
||||||
and respawn handlers into the server economy flow.
|
|
||||||
|
|
||||||
## Runtime Notes
|
|
||||||
Actor state is loaded before dependent systems initialize. When the server sends
|
|
||||||
actor sync data, the repository updates local view state and clears the loading
|
|
||||||
screen.
|
|
||||||
|
|||||||
@ -25,24 +25,6 @@ player addEventHandler ["Respawn", {
|
|||||||
|
|
||||||
if (isNil QGVAR(ActorRepository)) then { call FUNC(initRepository); };
|
if (isNil QGVAR(ActorRepository)) then { call FUNC(initRepository); };
|
||||||
|
|
||||||
GVAR(resetMedicalSpectator) = {
|
|
||||||
player switchMove "";
|
|
||||||
player playMoveNow "";
|
|
||||||
|
|
||||||
["Terminate"] call BFUNC(EGSpectator);
|
|
||||||
|
|
||||||
private _spectatorDisplay = findDisplay 60492;
|
|
||||||
if !(isNull _spectatorDisplay) then { _spectatorDisplay closeDisplay 1; };
|
|
||||||
if !(isNull player) then {
|
|
||||||
player switchCamera "INTERNAL";
|
|
||||||
player enableSimulation true;
|
|
||||||
};
|
|
||||||
|
|
||||||
cameraEffectEnableHUD true;
|
|
||||||
showCinemaBorder false;
|
|
||||||
disableUserInput false;
|
|
||||||
};
|
|
||||||
|
|
||||||
[QGVAR(initActor), {
|
[QGVAR(initActor), {
|
||||||
GVAR(ActorRepository) call ["init", []];
|
GVAR(ActorRepository) call ["init", []];
|
||||||
}] call CFUNC(addEventHandler);
|
}] call CFUNC(addEventHandler);
|
||||||
@ -58,15 +40,14 @@ GVAR(resetMedicalSpectator) = {
|
|||||||
player setDir _medSpawnDir;
|
player setDir _medSpawnDir;
|
||||||
player switchMove "Acts_LyingWounded_loop";
|
player switchMove "Acts_LyingWounded_loop";
|
||||||
|
|
||||||
[] spawn {
|
["Initialize", [player, [], false, true, true, true, true, true, false, false]] call BFUNC(EGSpectator);
|
||||||
["Initialize", [player, [], false, true, true, true, true, true, false, false]] call BFUNC(EGSpectator);
|
|
||||||
uiSleep 5;
|
[SRPC(economy,onHealed), [player]] call CFUNC(serverEvent);
|
||||||
[SRPC(economy,onHealed), [player]] call CFUNC(serverEvent);
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
}] call CFUNC(addEventHandler);
|
||||||
|
|
||||||
[QGVAR(onActorHealed), {
|
[QGVAR(onActorHealed), {
|
||||||
call GVAR(resetMedicalSpectator);
|
player switchMove "";
|
||||||
|
["Terminate"] call BFUNC(EGSpectator);
|
||||||
}] call CFUNC(addEventHandler);
|
}] call CFUNC(addEventHandler);
|
||||||
|
|
||||||
[QGVAR(responseInitActor), {
|
[QGVAR(responseInitActor), {
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* File: fnc_handleUIEvents.sqf
|
* File: fnc_handleUIEvents.sqf
|
||||||
* Author: IDSolutions
|
* Author: IDSolutions
|
||||||
* Date: 2026-01-28
|
* Date: 2026-01-28
|
||||||
* Last Update: 2026-04-06
|
* Last Update: 2026-03-28
|
||||||
* Public: No
|
* Public: No
|
||||||
*
|
*
|
||||||
* Description:
|
* Description:
|
||||||
@ -37,25 +37,11 @@ switch (_event) do {
|
|||||||
case "actor::open::bank": { [] spawn EFUNC(bank,openUI); };
|
case "actor::open::bank": { [] spawn EFUNC(bank,openUI); };
|
||||||
case "actor::open::cad": { [] spawn EFUNC(cad,openUI); };
|
case "actor::open::cad": { [] spawn EFUNC(cad,openUI); };
|
||||||
case "actor::open::device": { hint "Device interaction is not yet implemented."; };
|
case "actor::open::device": { hint "Device interaction is not yet implemented."; };
|
||||||
case "actor::open::garage": {
|
case "actor::open::garage": { [] spawn EFUNC(garage,openUI); };
|
||||||
private _garageObject = objNull;
|
case "actor::open::vgarage": { [] spawn EFUNC(garage,openVG); };
|
||||||
if (_data isEqualType createHashMap) then {
|
|
||||||
private _netId = _data getOrDefault ["netId", ""];
|
|
||||||
if (_netId isNotEqualTo "") then { _garageObject = objectFromNetId _netId; };
|
|
||||||
};
|
|
||||||
[_garageObject] spawn EFUNC(garage,openUI);
|
|
||||||
};
|
|
||||||
case "actor::open::vgarage": {
|
|
||||||
private _garageObject = objNull;
|
|
||||||
if (_data isEqualType createHashMap) then {
|
|
||||||
private _netId = _data getOrDefault ["netId", ""];
|
|
||||||
if (_netId isNotEqualTo "") then { _garageObject = objectFromNetId _netId; };
|
|
||||||
};
|
|
||||||
[_garageObject] spawn EFUNC(garage,openVG);
|
|
||||||
};
|
|
||||||
case "actor::open::org": { [] spawn EFUNC(org,openUI); };
|
case "actor::open::org": { [] spawn EFUNC(org,openUI); };
|
||||||
case "actor::open::vlocker": { [FORGE_Locker_Box, player, false] spawn AFUNC(arsenal,openBox) };
|
case "actor::open::vlocker": { [FORGE_Locker_Box, player, false] spawn AFUNC(arsenal,openBox) };
|
||||||
case "actor::open::phone": { [] spawn EFUNC(phone,openUI); };
|
case "actor::open::phone": { hint "Phone interaction is not yet implemented."; };
|
||||||
case "actor::open::iplayer": { hint "Player interaction is not yet implemented." };
|
case "actor::open::iplayer": { hint "Player interaction is not yet implemented." };
|
||||||
case "actor::open::store": { [] spawn EFUNC(store,openUI); };
|
case "actor::open::store": { [] spawn EFUNC(store,openUI); };
|
||||||
default { hint format ["Unhandled UI event: %1", _event]; };
|
default { hint format ["Unhandled UI event: %1", _event]; };
|
||||||
|
|||||||
@ -108,26 +108,21 @@ GVAR(ActorRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
params [["_control", controlNull, [controlNull]]];
|
params [["_control", controlNull, [controlNull]]];
|
||||||
private _nearbyActions = [];
|
private _nearbyActions = [];
|
||||||
{
|
{
|
||||||
|
private _storeType = _x getVariable ["storeType", ""];
|
||||||
private _isAtm = _x getVariable ["isAtm", false];
|
private _isAtm = _x getVariable ["isAtm", false];
|
||||||
private _isBank = _x getVariable ["isBank", false];
|
private _isBank = _x getVariable ["isBank", false];
|
||||||
private _isGarage = _x getVariable ["isGarage", false];
|
private _isGarage = _x getVariable ["isGarage", false];
|
||||||
private _isLocker = _x getVariable ["isLocker", false];
|
private _isLocker = _x getVariable ["isLocker", false];
|
||||||
private _isStore = _x getVariable ["isStore", false];
|
|
||||||
private _garageType = _x getVariable ["garageType", ""];
|
private _garageType = _x getVariable ["garageType", ""];
|
||||||
private _garageContext = createHashMapFromArray [
|
|
||||||
["netId", netId _x],
|
|
||||||
["name", vehicleVarName _x],
|
|
||||||
["garageType", _garageType]
|
|
||||||
];
|
|
||||||
private _deviceType = _x getVariable ["deviceType", ""];
|
private _deviceType = _x getVariable ["deviceType", ""];
|
||||||
private _isPlayer = _x isKindOf "Man" && isPlayer _x;
|
private _isPlayer = _x isKindOf "Man" && isPlayer _x;
|
||||||
|
|
||||||
if (_isStore) then { _nearbyActions pushBack ["store", true]; };
|
if (_storeType isNotEqualTo "") then { _nearbyActions pushBack ["store", _storeType]; };
|
||||||
if (_isAtm) then { _nearbyActions pushBack ["atm", true]; };
|
if (_isAtm) then { _nearbyActions pushBack ["atm", true]; };
|
||||||
if (_isBank) then { _nearbyActions pushBack ["bank", true]; };
|
if (_isBank) then { _nearbyActions pushBack ["bank", true]; };
|
||||||
if (_isLocker && GVAR(enableVA)) then { _nearbyActions pushBack ["va", true]; };
|
if (_isLocker && GVAR(enableVA)) then { _nearbyActions pushBack ["va", true]; };
|
||||||
if (_isGarage) then { _nearbyActions pushBack ["garage", _garageContext]; };
|
if (_isGarage) then { _nearbyActions pushBack ["garage", _garageType]; };
|
||||||
if (_isGarage && GVAR(enableVG)) then { _nearbyActions pushBack ["vg", _garageContext]; };
|
if (_isGarage && GVAR(enableVG)) then { _nearbyActions pushBack ["vg", true]; };
|
||||||
if (_deviceType isNotEqualTo "") then { _nearbyActions pushBack ["device", _deviceType]; };
|
if (_deviceType isNotEqualTo "") then { _nearbyActions pushBack ["device", _deviceType]; };
|
||||||
if (_isPlayer && { _x isNotEqualTo player }) then { _nearbyActions pushBack ["player", name _x]; };
|
if (_isPlayer && { _x isNotEqualTo player }) then { _nearbyActions pushBack ["player", name _x]; };
|
||||||
} forEach (player nearObjects 5);
|
} forEach (player nearObjects 5);
|
||||||
|
|||||||
@ -118,6 +118,12 @@ const baseMenuItems = [
|
|||||||
description: "View and manage your organization data",
|
description: "View and manage your organization data",
|
||||||
action: "actor::open::org",
|
action: "actor::open::org",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "store",
|
||||||
|
title: "Store",
|
||||||
|
description: "Browse and purchase items from the store",
|
||||||
|
action: "actor::open::store",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const actionDefinitions = {
|
const actionDefinitions = {
|
||||||
@ -215,21 +221,7 @@ function actorReducer(state = initialState, action) {
|
|||||||
const [type, value] = actionItem;
|
const [type, value] = actionItem;
|
||||||
const definition = state.actionDefinitions[type];
|
const definition = state.actionDefinitions[type];
|
||||||
if (definition) {
|
if (definition) {
|
||||||
const context =
|
newMenuItems.push(definition);
|
||||||
value && typeof value === "object"
|
|
||||||
? value
|
|
||||||
: { value };
|
|
||||||
const garageLabel =
|
|
||||||
context.name || context.garageType || "";
|
|
||||||
const title =
|
|
||||||
["garage", "vg"].includes(type) && garageLabel
|
|
||||||
? `${definition.title}: ${garageLabel}`
|
|
||||||
: definition.title;
|
|
||||||
newMenuItems.push({
|
|
||||||
...definition,
|
|
||||||
title,
|
|
||||||
context,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
`No definition found for: ${type} - ${value}`,
|
`No definition found for: ${type} - ${value}`,
|
||||||
@ -428,7 +420,7 @@ function RadialMenu() {
|
|||||||
console.log("Menu item clicked:", item);
|
console.log("Menu item clicked:", item);
|
||||||
const alert = {
|
const alert = {
|
||||||
event: item.action,
|
event: item.action,
|
||||||
data: item.context || {},
|
data: {},
|
||||||
};
|
};
|
||||||
if (typeof A3API !== "undefined") {
|
if (typeof A3API !== "undefined") {
|
||||||
A3API.SendAlert(JSON.stringify(alert));
|
A3API.SendAlert(JSON.stringify(alert));
|
||||||
|
|||||||
@ -1,35 +1,3 @@
|
|||||||
# Forge Client Bank
|
# forge_client_bank
|
||||||
|
|
||||||
## Overview
|
Description for this addon
|
||||||
The bank addon provides the client banking UI and browser bridge for account
|
|
||||||
hydrate, deposits, withdrawals, transfers, PIN entry, earnings deposits, and
|
|
||||||
credit-line repayment. It also exposes PIN changes from the full bank UI.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
- `forge_client_common`
|
|
||||||
- `forge_client_main`
|
|
||||||
- server bank events from `forge_server_bank`
|
|
||||||
- notifications for server-driven messages
|
|
||||||
|
|
||||||
## Main Components
|
|
||||||
- `fnc_initRepository.sqf` tracks account load state.
|
|
||||||
- `fnc_initUIBridge.sqf` translates browser requests into server RPCs and sends
|
|
||||||
server responses back to the browser.
|
|
||||||
- `fnc_handleUIEvents.sqf` handles `bank::*` browser events.
|
|
||||||
- `fnc_openUI.sqf` opens `RscBank`; ATM mode is supported by passing `true`.
|
|
||||||
|
|
||||||
## Browser Events
|
|
||||||
- `bank::ready`
|
|
||||||
- `bank::refresh`
|
|
||||||
- `bank::deposit::request`
|
|
||||||
- `bank::withdraw::request`
|
|
||||||
- `bank::transfer::request`
|
|
||||||
- `bank::depositEarnings::request`
|
|
||||||
- `bank::repayCreditLine::request`
|
|
||||||
- `bank::pin::request`
|
|
||||||
- `bank::pin::change::request`
|
|
||||||
- `bank::close`
|
|
||||||
|
|
||||||
## Runtime Notes
|
|
||||||
The client only displays and requests account changes. The server bank addon and
|
|
||||||
extension own validation, balances, authorization, and persistence.
|
|
||||||
|
|||||||
@ -3,26 +3,6 @@
|
|||||||
if (isNil QGVAR(BankRepository)) then { call FUNC(initRepository); };
|
if (isNil QGVAR(BankRepository)) then { call FUNC(initRepository); };
|
||||||
if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
|
if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
|
||||||
|
|
||||||
GVAR(sendPhoneBankEvent) = {
|
|
||||||
params [["_functionName", "", [""]], ["_arguments", [], [[]]]];
|
|
||||||
|
|
||||||
private _display = uiNamespace getVariable ["RscPhone", displayNull];
|
|
||||||
if (isNull _display || { _functionName isEqualTo "" }) exitWith { false };
|
|
||||||
|
|
||||||
private _control = _display displayCtrl 1001;
|
|
||||||
if (isNull _control) exitWith { false };
|
|
||||||
|
|
||||||
private _serializedArguments = _arguments apply { toJSON _x };
|
|
||||||
private _script = format [
|
|
||||||
"window.%1 && window.%1(%2)",
|
|
||||||
_functionName,
|
|
||||||
_serializedArguments joinString ", "
|
|
||||||
];
|
|
||||||
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", _script];
|
|
||||||
true
|
|
||||||
};
|
|
||||||
|
|
||||||
[QGVAR(initBank), {
|
[QGVAR(initBank), {
|
||||||
GVAR(BankRepository) call ["init", []];
|
GVAR(BankRepository) call ["init", []];
|
||||||
}] call CFUNC(addEventHandler);
|
}] call CFUNC(addEventHandler);
|
||||||
@ -34,7 +14,6 @@ GVAR(sendPhoneBankEvent) = {
|
|||||||
if !(isNil QGVAR(BankUIBridge)) then {
|
if !(isNil QGVAR(BankUIBridge)) then {
|
||||||
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
|
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
|
||||||
};
|
};
|
||||||
["updateMobileBankAccount", [_data]] call GVAR(sendPhoneBankEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
}] call CFUNC(addEventHandler);
|
||||||
|
|
||||||
[QGVAR(responseSyncBank), {
|
[QGVAR(responseSyncBank), {
|
||||||
@ -44,7 +23,6 @@ GVAR(sendPhoneBankEvent) = {
|
|||||||
if !(isNil QGVAR(BankUIBridge)) then {
|
if !(isNil QGVAR(BankUIBridge)) then {
|
||||||
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
|
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
|
||||||
};
|
};
|
||||||
["updateMobileBankAccount", [_data]] call GVAR(sendPhoneBankEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
}] call CFUNC(addEventHandler);
|
||||||
|
|
||||||
[QGVAR(responseHydrateBank), {
|
[QGVAR(responseHydrateBank), {
|
||||||
@ -53,7 +31,6 @@ GVAR(sendPhoneBankEvent) = {
|
|||||||
if !(isNil QGVAR(BankUIBridge)) then {
|
if !(isNil QGVAR(BankUIBridge)) then {
|
||||||
GVAR(BankUIBridge) call ["handleHydrateResponse", [_data, "bank::hydrate"]];
|
GVAR(BankUIBridge) call ["handleHydrateResponse", [_data, "bank::hydrate"]];
|
||||||
};
|
};
|
||||||
["updateMobileBank", [_data]] call GVAR(sendPhoneBankEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
}] call CFUNC(addEventHandler);
|
||||||
|
|
||||||
[QGVAR(responseBankNotice), {
|
[QGVAR(responseBankNotice), {
|
||||||
@ -62,7 +39,6 @@ GVAR(sendPhoneBankEvent) = {
|
|||||||
if !(isNil QGVAR(BankUIBridge)) then {
|
if !(isNil QGVAR(BankUIBridge)) then {
|
||||||
GVAR(BankUIBridge) call ["handleNoticeResponse", [_type, _message]];
|
GVAR(BankUIBridge) call ["handleNoticeResponse", [_type, _message]];
|
||||||
};
|
};
|
||||||
["showMobileBankNotice", [_type, _message]] call GVAR(sendPhoneBankEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
}] call CFUNC(addEventHandler);
|
||||||
|
|
||||||
[{
|
[{
|
||||||
|
|||||||
@ -78,11 +78,6 @@ switch (_event) do {
|
|||||||
GVAR(BankUIBridge) call ["handleSubmitPinRequest", [_data]];
|
GVAR(BankUIBridge) call ["handleSubmitPinRequest", [_data]];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
case "bank::pin::change::request": {
|
|
||||||
if !(isNil QGVAR(BankUIBridge)) then {
|
|
||||||
GVAR(BankUIBridge) call ["handleChangePinRequest", [_data]];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
default {
|
default {
|
||||||
hint format ["Unhandled bank UI event: %1", _event];
|
hint format ["Unhandled bank UI event: %1", _event];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -63,17 +63,6 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
[SRPC(bank,requestDeposit), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
[SRPC(bank,requestDeposit), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
||||||
true
|
true
|
||||||
}],
|
}],
|
||||||
["handleChangePinRequest", compileFinal {
|
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _currentPin = _data getOrDefault ["currentPin", ""];
|
|
||||||
private _newPin = _data getOrDefault ["newPin", ""];
|
|
||||||
if !(_currentPin isEqualType "") then { _currentPin = str _currentPin; };
|
|
||||||
if !(_newPin isEqualType "") then { _newPin = str _newPin; };
|
|
||||||
|
|
||||||
[SRPC(bank,requestChangePin), [getPlayerUID player, _currentPin, _newPin]] call CFUNC(serverEvent);
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["handleRepayCreditLineRequest", compileFinal {
|
["handleRepayCreditLineRequest", compileFinal {
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
params [["_data", createHashMap, [createHashMap]]];
|
||||||
|
|
||||||
|
|||||||
@ -49,9 +49,6 @@
|
|||||||
requestRefresh() {
|
requestRefresh() {
|
||||||
return bridge.send("bank::refresh", {});
|
return bridge.send("bank::refresh", {});
|
||||||
},
|
},
|
||||||
requestChangePin(payload) {
|
|
||||||
return bridge.send("bank::pin::change::request", payload);
|
|
||||||
},
|
|
||||||
requestSubmitPin(payload) {
|
requestSubmitPin(payload) {
|
||||||
return bridge.send("bank::pin::request", payload);
|
return bridge.send("bank::pin::request", payload);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -352,73 +352,6 @@
|
|||||||
: "Deposit Earnings",
|
: "Deposit Earnings",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
h(
|
|
||||||
"section",
|
|
||||||
{ className: "bank-page-section" },
|
|
||||||
h(
|
|
||||||
"div",
|
|
||||||
{ className: "bank-section-header" },
|
|
||||||
h(
|
|
||||||
"div",
|
|
||||||
null,
|
|
||||||
h("span", { className: "bank-eyebrow" }, "Security"),
|
|
||||||
h(
|
|
||||||
"h2",
|
|
||||||
{ className: "bank-section-title" },
|
|
||||||
"Change ATM PIN",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
h(
|
|
||||||
"div",
|
|
||||||
{ className: "bank-form-stack" },
|
|
||||||
h("input", {
|
|
||||||
id: "bank-current-pin",
|
|
||||||
className: "bank-input",
|
|
||||||
type: "password",
|
|
||||||
inputMode: "numeric",
|
|
||||||
maxLength: "4",
|
|
||||||
placeholder: "Current PIN",
|
|
||||||
}),
|
|
||||||
h("input", {
|
|
||||||
id: "bank-new-pin",
|
|
||||||
className: "bank-input",
|
|
||||||
type: "password",
|
|
||||||
inputMode: "numeric",
|
|
||||||
maxLength: "4",
|
|
||||||
placeholder: "New PIN",
|
|
||||||
}),
|
|
||||||
h("input", {
|
|
||||||
id: "bank-confirm-pin",
|
|
||||||
className: "bank-input",
|
|
||||||
type: "password",
|
|
||||||
inputMode: "numeric",
|
|
||||||
maxLength: "4",
|
|
||||||
placeholder: "Confirm new PIN",
|
|
||||||
}),
|
|
||||||
h(
|
|
||||||
"button",
|
|
||||||
{
|
|
||||||
type: "button",
|
|
||||||
className: "bank-btn bank-btn-primary",
|
|
||||||
disabled: pending("changepin"),
|
|
||||||
onClick: () => {
|
|
||||||
const sent = actions.requestChangePin(
|
|
||||||
readInputValue("bank-current-pin"),
|
|
||||||
readInputValue("bank-new-pin"),
|
|
||||||
readInputValue("bank-confirm-pin"),
|
|
||||||
);
|
|
||||||
if (sent) {
|
|
||||||
clearInputValue("bank-current-pin");
|
|
||||||
clearInputValue("bank-new-pin");
|
|
||||||
clearInputValue("bank-confirm-pin");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pending("changepin") ? "Updating PIN..." : "Update PIN",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -149,53 +149,6 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePin(value) {
|
|
||||||
return String(value || "")
|
|
||||||
.replace(/\D/g, "")
|
|
||||||
.slice(0, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestChangePin(currentPinValue, newPinValue, confirmPinValue) {
|
|
||||||
const currentPin = normalizePin(currentPinValue);
|
|
||||||
const newPin = normalizePin(newPinValue);
|
|
||||||
const confirmPin = normalizePin(confirmPinValue);
|
|
||||||
const bridge = BankApp.bridge;
|
|
||||||
|
|
||||||
if (!bridge || typeof bridge.requestChangePin !== "function") {
|
|
||||||
showNotice("error", "PIN change bridge is unavailable.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (currentPin.length !== 4) {
|
|
||||||
showNotice("error", "Enter your current four-digit PIN.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (newPin.length !== 4) {
|
|
||||||
showNotice("error", "Choose a new four-digit PIN.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (newPin !== confirmPin) {
|
|
||||||
showNotice("error", "New PIN confirmation does not match.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (currentPin === newPin) {
|
|
||||||
showNotice(
|
|
||||||
"error",
|
|
||||||
"Choose a different PIN from your current PIN.",
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.startAction("changepin");
|
|
||||||
const sent = bridge.requestChangePin({ currentPin, newPin });
|
|
||||||
if (!sent) {
|
|
||||||
store.finishAction();
|
|
||||||
showNotice("error", "PIN change bridge is unavailable.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendPinDigit(digit) {
|
function appendPinDigit(digit) {
|
||||||
const nextDigit = String(digit || "").trim();
|
const nextDigit = String(digit || "").trim();
|
||||||
if (!nextDigit) {
|
if (!nextDigit) {
|
||||||
@ -323,7 +276,6 @@
|
|||||||
closeBank,
|
closeBank,
|
||||||
refreshBank,
|
refreshBank,
|
||||||
requestAtmAmount,
|
requestAtmAmount,
|
||||||
requestChangePin,
|
|
||||||
requestDeposit,
|
requestDeposit,
|
||||||
requestDepositEarnings,
|
requestDepositEarnings,
|
||||||
requestRepayCreditLine,
|
requestRepayCreditLine,
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
# Forge Client CAD
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
The CAD addon provides the client map and dispatch interface for task
|
|
||||||
assignment, dispatch orders, support requests, group status, group roles, and
|
|
||||||
task acknowledge/decline actions.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
- `forge_client_main`
|
|
||||||
- server CAD events from `forge_server_cad`
|
|
||||||
- server task catalog data exposed through CAD hydrate payloads
|
|
||||||
|
|
||||||
## Main Components
|
|
||||||
- `fnc_initRepository.sqf` caches hydrated CAD view state.
|
|
||||||
- `fnc_initUI.sqf` wires the native map, top bar, bottom bar, side panel, and
|
|
||||||
dispatcher browser controls.
|
|
||||||
- `fnc_initUIBridge.sqf` sends browser actions to server CAD RPCs and pushes
|
|
||||||
state back to the UI.
|
|
||||||
- `fnc_handleUIEvents.sqf` handles `cad::*` browser events.
|
|
||||||
- `fnc_openUI.sqf` opens the CAD display.
|
|
||||||
|
|
||||||
## Supported Actions
|
|
||||||
- hydrate CAD state
|
|
||||||
- assign active tasks to groups
|
|
||||||
- create and close dispatch orders
|
|
||||||
- submit and close support requests
|
|
||||||
- acknowledge or decline assigned tasks
|
|
||||||
- update group status, role, and profile
|
|
||||||
- focus map requests and toggle panels
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
CAD task visibility depends on server-side task catalog entries. Tasks created
|
|
||||||
through Forge task modules or `forge_server_task_fnc_startTask` are the normal
|
|
||||||
CAD-compatible task sources.
|
|
||||||
|
|
||||||
See [MAP_README.md](./MAP_README.md) for details on the integrated native map
|
|
||||||
and browser layout.
|
|
||||||
@ -88,16 +88,6 @@ switch (_event) do {
|
|||||||
|
|
||||||
GVAR(CADUIBridge) call ["requestCreateDispatchOrder", [_assigneeGroupID, _targetGroupID, _note, _priority, _request]];
|
GVAR(CADUIBridge) call ["requestCreateDispatchOrder", [_assigneeGroupID, _targetGroupID, _note, _priority, _request]];
|
||||||
};
|
};
|
||||||
case "cad::generatedTask::request": {
|
|
||||||
private _taskType = "";
|
|
||||||
private _metadata = createHashMap;
|
|
||||||
if (_data isEqualType createHashMap) then {
|
|
||||||
_taskType = _data getOrDefault ["taskType", ""];
|
|
||||||
_metadata = _data getOrDefault ["metadata", createHashMap];
|
|
||||||
};
|
|
||||||
|
|
||||||
GVAR(CADUIBridge) call ["requestGeneratedMissionTask", [_taskType, _metadata]];
|
|
||||||
};
|
|
||||||
case "cad::supportRequest::submit": {
|
case "cad::supportRequest::submit": {
|
||||||
private _type = "";
|
private _type = "";
|
||||||
private _fields = createHashMap;
|
private _fields = createHashMap;
|
||||||
@ -182,14 +172,6 @@ switch (_event) do {
|
|||||||
|
|
||||||
GVAR(CADUIBridge) call ["focusGroup", [_groupID]];
|
GVAR(CADUIBridge) call ["focusGroup", [_groupID]];
|
||||||
};
|
};
|
||||||
case "cad::members::focus": {
|
|
||||||
private _uid = "";
|
|
||||||
if (_data isEqualType createHashMap) then {
|
|
||||||
_uid = _data getOrDefault ["uid", ""];
|
|
||||||
};
|
|
||||||
|
|
||||||
GVAR(CADUIBridge) call ["focusMember", [_uid]];
|
|
||||||
};
|
|
||||||
case "cad::tasks::focus": {
|
case "cad::tasks::focus": {
|
||||||
private _taskID = "";
|
private _taskID = "";
|
||||||
if (_data isEqualType createHashMap) then {
|
if (_data isEqualType createHashMap) then {
|
||||||
|
|||||||
@ -227,17 +227,6 @@ GVAR(CADUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
[SRPC(cad,requestCreateCadDispatchOrder), [getPlayerUID player, _assigneeGroupID, _targetGroupID, _note, _priority, _request]] call CFUNC(serverEvent);
|
[SRPC(cad,requestCreateCadDispatchOrder), [getPlayerUID player, _assigneeGroupID, _targetGroupID, _note, _priority, _request]] call CFUNC(serverEvent);
|
||||||
true
|
true
|
||||||
}],
|
}],
|
||||||
["requestGeneratedMissionTask", compileFinal {
|
|
||||||
params [
|
|
||||||
["_taskType", "", [""]],
|
|
||||||
["_metadata", createHashMap, [createHashMap]]
|
|
||||||
];
|
|
||||||
|
|
||||||
if (_taskType isEqualTo "") exitWith { false };
|
|
||||||
|
|
||||||
[SRPC(cad,requestGenerateCadMissionTask), [getPlayerUID player, _taskType, _metadata]] call CFUNC(serverEvent);
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["requestSubmitSupportRequest", compileFinal {
|
["requestSubmitSupportRequest", compileFinal {
|
||||||
params [
|
params [
|
||||||
["_type", "", [""]],
|
["_type", "", [""]],
|
||||||
@ -330,33 +319,6 @@ GVAR(CADUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
ctrlMapAnimCommit _mapCtrl;
|
ctrlMapAnimCommit _mapCtrl;
|
||||||
true
|
true
|
||||||
}],
|
}],
|
||||||
["focusMember", compileFinal {
|
|
||||||
params [["_uid", "", [""]]];
|
|
||||||
|
|
||||||
if (_uid isEqualTo "") exitWith { false };
|
|
||||||
if (isNil QGVAR(CADRepository)) exitWith { false };
|
|
||||||
|
|
||||||
private _groups = GVAR(CADRepository) getOrDefault ["groups", []];
|
|
||||||
private _position = [];
|
|
||||||
{
|
|
||||||
private _members = _x getOrDefault ["members", []];
|
|
||||||
private _memberIndex = _members findIf { (_x getOrDefault ["uid", ""]) isEqualTo _uid };
|
|
||||||
if (_memberIndex >= 0) exitWith {
|
|
||||||
_position = (_members # _memberIndex) getOrDefault ["position", []];
|
|
||||||
};
|
|
||||||
} forEach _groups;
|
|
||||||
|
|
||||||
if !(_position isEqualType []) exitWith { false };
|
|
||||||
if ((count _position) < 2) exitWith { false };
|
|
||||||
|
|
||||||
private _mapCtrl = _self call ["getMapControl", []];
|
|
||||||
if (isNull _mapCtrl) exitWith { false };
|
|
||||||
|
|
||||||
private _targetPosition = [_position # 0, _position # 1, 0];
|
|
||||||
_mapCtrl ctrlMapAnimAdd [0.35, ctrlMapScale _mapCtrl, _targetPosition];
|
|
||||||
ctrlMapAnimCommit _mapCtrl;
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["focusTask", compileFinal {
|
["focusTask", compileFinal {
|
||||||
params [["_taskID", "", [""]]];
|
params [["_taskID", "", [""]]];
|
||||||
|
|
||||||
|
|||||||
@ -49,24 +49,15 @@
|
|||||||
<section class="dispatch-panel dispatch-panel-open">
|
<section class="dispatch-panel dispatch-panel-open">
|
||||||
<div class="dispatch-panel-header">
|
<div class="dispatch-panel-header">
|
||||||
<h3>Available Contracts</h3>
|
<h3>Available Contracts</h3>
|
||||||
<div class="dispatch-panel-actions">
|
<button
|
||||||
<button
|
id="dispatcherCreateOrderBtn"
|
||||||
id="dispatcherRequestTaskBtn"
|
type="button"
|
||||||
type="button"
|
class="dispatch-icon-btn"
|
||||||
class="dispatch-btn dispatch-btn-compact"
|
aria-label="Create dispatch order"
|
||||||
>
|
title="Create dispatch order"
|
||||||
Request Task
|
>
|
||||||
</button>
|
+
|
||||||
<button
|
</button>
|
||||||
id="dispatcherCreateOrderBtn"
|
|
||||||
type="button"
|
|
||||||
class="dispatch-icon-btn"
|
|
||||||
aria-label="Create dispatch order"
|
|
||||||
title="Create dispatch order"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
id="dispatcherOpenContracts"
|
id="dispatcherOpenContracts"
|
||||||
@ -253,51 +244,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dispatcherTaskModal" class="dispatch-modal is-hidden">
|
|
||||||
<div class="dispatch-modal-backdrop"></div>
|
|
||||||
<div
|
|
||||||
class="dispatch-modal-dialog"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby="dispatcherTaskModalTitle"
|
|
||||||
>
|
|
||||||
<div class="dispatch-modal-header">
|
|
||||||
<div>
|
|
||||||
<p class="dispatch-kicker">Mission Generator</p>
|
|
||||||
<h3 id="dispatcherTaskModalTitle">Request Task</h3>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
id="dispatcherTaskModalCloseBtn"
|
|
||||||
class="dispatch-icon-btn"
|
|
||||||
type="button"
|
|
||||||
aria-label="Close task request"
|
|
||||||
>
|
|
||||||
x
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="dispatch-modal-body">
|
|
||||||
<div class="dispatch-modal-fields">
|
|
||||||
<label class="dispatch-field">
|
|
||||||
<span>Task Type</span>
|
|
||||||
<select
|
|
||||||
id="dispatcherTaskTypeSelect"
|
|
||||||
class="dispatch-select"
|
|
||||||
></select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="dispatch-modal-actions">
|
|
||||||
<button
|
|
||||||
id="dispatcherTaskModalSaveBtn"
|
|
||||||
type="button"
|
|
||||||
class="dispatch-btn"
|
|
||||||
>
|
|
||||||
Generate Task
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="dispatcherRequestModal" class="dispatch-modal is-hidden">
|
<div id="dispatcherRequestModal" class="dispatch-modal is-hidden">
|
||||||
<div class="dispatch-modal-backdrop"></div>
|
<div class="dispatch-modal-backdrop"></div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -73,15 +73,6 @@ window.cadDispatcherFormatters = {
|
|||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
},
|
},
|
||||||
buildTaskTypeOptions(selectedTaskType) {
|
|
||||||
return this.taskTypes
|
|
||||||
.map((taskType) => {
|
|
||||||
const value = taskType.value || "";
|
|
||||||
const selected = value === selectedTaskType ? "selected" : "";
|
|
||||||
return `<option value="${value}" ${selected}>${taskType.label || value}</option>`;
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
},
|
|
||||||
formatRequestFieldLabel(fieldID) {
|
formatRequestFieldLabel(fieldID) {
|
||||||
return (fieldID || "field")
|
return (fieldID || "field")
|
||||||
.replaceAll("_", " ")
|
.replaceAll("_", " ")
|
||||||
|
|||||||
@ -11,16 +11,6 @@ window.cadDispatcher = {
|
|||||||
editingGroupId: "",
|
editingGroupId: "",
|
||||||
viewingRequestId: "",
|
viewingRequestId: "",
|
||||||
convertingRequestId: "",
|
convertingRequestId: "",
|
||||||
taskTypes: [
|
|
||||||
{ value: "attack", label: "Attack" },
|
|
||||||
{ value: "defend", label: "Defend" },
|
|
||||||
{ value: "delivery", label: "Delivery" },
|
|
||||||
{ value: "destroy", label: "Destroy" },
|
|
||||||
{ value: "defuse", label: "Defuse" },
|
|
||||||
{ value: "hostage", label: "Hostage" },
|
|
||||||
{ value: "hvtkill", label: "Kill HVT" },
|
|
||||||
{ value: "hvtcapture", label: "Capture HVT" },
|
|
||||||
],
|
|
||||||
statuses: [
|
statuses: [
|
||||||
"available",
|
"available",
|
||||||
"en_route",
|
"en_route",
|
||||||
@ -34,12 +24,6 @@ window.cadDispatcher = {
|
|||||||
...dispatcherModals,
|
...dispatcherModals,
|
||||||
...dispatcherRender,
|
...dispatcherRender,
|
||||||
init() {
|
init() {
|
||||||
document
|
|
||||||
.getElementById("dispatcherRequestTaskBtn")
|
|
||||||
.addEventListener("click", () => {
|
|
||||||
this.openTaskModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
document
|
document
|
||||||
.getElementById("dispatcherCreateOrderBtn")
|
.getElementById("dispatcherCreateOrderBtn")
|
||||||
.addEventListener("click", () => {
|
.addEventListener("click", () => {
|
||||||
@ -82,24 +66,6 @@ window.cadDispatcher = {
|
|||||||
this.closeOrderModal();
|
this.closeOrderModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
document
|
|
||||||
.getElementById("dispatcherTaskModalCloseBtn")
|
|
||||||
.addEventListener("click", () => {
|
|
||||||
this.closeTaskModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
document
|
|
||||||
.getElementById("dispatcherTaskModalSaveBtn")
|
|
||||||
.addEventListener("click", () => {
|
|
||||||
this.requestGeneratedTask();
|
|
||||||
});
|
|
||||||
|
|
||||||
document
|
|
||||||
.querySelector("#dispatcherTaskModal .dispatch-modal-backdrop")
|
|
||||||
.addEventListener("click", () => {
|
|
||||||
this.closeTaskModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
document
|
document
|
||||||
.getElementById("dispatcherRequestModalCloseBtn")
|
.getElementById("dispatcherRequestModalCloseBtn")
|
||||||
.addEventListener("click", () => {
|
.addEventListener("click", () => {
|
||||||
@ -222,24 +188,6 @@ window.cadDispatcher = {
|
|||||||
|
|
||||||
this.closeOrderModal();
|
this.closeOrderModal();
|
||||||
},
|
},
|
||||||
requestGeneratedTask() {
|
|
||||||
const taskType = document.getElementById(
|
|
||||||
"dispatcherTaskTypeSelect",
|
|
||||||
).value;
|
|
||||||
if (!taskType) {
|
|
||||||
this.setStatus(
|
|
||||||
"Select a task type before requesting a task.",
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setStatus("Requesting generated task...", "info");
|
|
||||||
window.mapUI.sendEvent("cad::generatedTask::request", {
|
|
||||||
taskType: taskType,
|
|
||||||
});
|
|
||||||
this.closeTaskModal();
|
|
||||||
},
|
|
||||||
assignTask(taskID) {
|
assignTask(taskID) {
|
||||||
const selector = document.getElementById(
|
const selector = document.getElementById(
|
||||||
`dispatcher-assign-group-${taskID}`,
|
`dispatcher-assign-group-${taskID}`,
|
||||||
|
|||||||
@ -1,27 +1,4 @@
|
|||||||
window.cadDispatcherModals = {
|
window.cadDispatcherModals = {
|
||||||
openTaskModal() {
|
|
||||||
this.populateTaskModal();
|
|
||||||
document
|
|
||||||
.getElementById("dispatcherTaskModal")
|
|
||||||
.classList.remove("is-hidden");
|
|
||||||
},
|
|
||||||
closeTaskModal() {
|
|
||||||
document
|
|
||||||
.getElementById("dispatcherTaskModal")
|
|
||||||
.classList.add("is-hidden");
|
|
||||||
},
|
|
||||||
populateTaskModal() {
|
|
||||||
const taskTypeSelect = document.getElementById(
|
|
||||||
"dispatcherTaskTypeSelect",
|
|
||||||
);
|
|
||||||
if (!taskTypeSelect) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
taskTypeSelect.innerHTML = this.buildTaskTypeOptions(
|
|
||||||
taskTypeSelect.value || this.taskTypes[0]?.value || "",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
openOrderModal() {
|
openOrderModal() {
|
||||||
this.convertingRequestId = "";
|
this.convertingRequestId = "";
|
||||||
this.populateOrderModal();
|
this.populateOrderModal();
|
||||||
|
|||||||
@ -10,7 +10,6 @@ window.cadTasks = {
|
|||||||
selectedDispatchGroupId: "",
|
selectedDispatchGroupId: "",
|
||||||
selectedDispatchTaskId: "",
|
selectedDispatchTaskId: "",
|
||||||
selectedDispatchRequestId: "",
|
selectedDispatchRequestId: "",
|
||||||
selectedRosterMemberUid: "",
|
|
||||||
focusStatusTimer: null,
|
focusStatusTimer: null,
|
||||||
requestModalType: "",
|
requestModalType: "",
|
||||||
statuses: [
|
statuses: [
|
||||||
@ -432,19 +431,6 @@ window.cadTasks = {
|
|||||||
this.selectedDispatchGroupId = "";
|
this.selectedDispatchGroupId = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.selectedRosterMemberUid) {
|
|
||||||
const memberExists = this.groups.some((group) =>
|
|
||||||
this.normalizeCollection(group.members).some(
|
|
||||||
(member) =>
|
|
||||||
(member.uid || "") === this.selectedRosterMemberUid,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!memberExists) {
|
|
||||||
this.selectedRosterMemberUid = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.selectedDispatchTaskId &&
|
this.selectedDispatchTaskId &&
|
||||||
!this.contracts.some((task) => {
|
!this.contracts.some((task) => {
|
||||||
@ -760,18 +746,8 @@ window.cadTasks = {
|
|||||||
const requestActionLabel = this.isDispatchMode()
|
const requestActionLabel = this.isDispatchMode()
|
||||||
? "Close"
|
? "Close"
|
||||||
: "Cancel";
|
: "Cancel";
|
||||||
const requestID = request.requestId || "";
|
|
||||||
const isSelected =
|
|
||||||
requestID === this.selectedDispatchRequestId;
|
|
||||||
return `
|
return `
|
||||||
<div
|
<div class="task-card cad-request-card">
|
||||||
class="task-card cad-request-card dispatch-map-card ${isSelected ? "is-selected" : ""}"
|
|
||||||
data-request-id="${requestID}"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
onclick="window.cadTasks.focusRequest('${requestID}')"
|
|
||||||
onkeydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); window.cadTasks.focusRequest('${requestID}'); }"
|
|
||||||
>
|
|
||||||
<div class="task-card-header">
|
<div class="task-card-header">
|
||||||
<strong>${request.title || this.getRequestTypeLabel(request.type || "")}</strong>
|
<strong>${request.title || this.getRequestTypeLabel(request.type || "")}</strong>
|
||||||
<span class="task-type">${(request.priority || "priority").replaceAll("_", " ")}</span>
|
<span class="task-type">${(request.priority || "priority").replaceAll("_", " ")}</span>
|
||||||
@ -784,7 +760,7 @@ window.cadTasks = {
|
|||||||
${
|
${
|
||||||
canClose
|
canClose
|
||||||
? `<div class="task-action-row">
|
? `<div class="task-action-row">
|
||||||
<button type="button" class="task-secondary-btn" onclick="event.stopPropagation(); window.cadTasks.closeSupportRequest('${requestID}')">${requestActionLabel}</button>
|
<button type="button" class="task-secondary-btn" onclick="window.cadTasks.closeSupportRequest('${request.requestId || ""}')">${requestActionLabel}</button>
|
||||||
</div>`
|
</div>`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
@ -899,7 +875,6 @@ window.cadTasks = {
|
|||||||
this.selectedDispatchGroupId = groupID;
|
this.selectedDispatchGroupId = groupID;
|
||||||
this.selectedDispatchTaskId = "";
|
this.selectedDispatchTaskId = "";
|
||||||
this.selectedDispatchRequestId = "";
|
this.selectedDispatchRequestId = "";
|
||||||
this.selectedRosterMemberUid = "";
|
|
||||||
const statusMessage = `Centering map on ${group.callsign || group.groupId || "group"}...`;
|
const statusMessage = `Centering map on ${group.callsign || group.groupId || "group"}...`;
|
||||||
this.setStatus(statusMessage, "info");
|
this.setStatus(statusMessage, "info");
|
||||||
this.clearFocusStatusSoon(statusMessage);
|
this.clearFocusStatusSoon(statusMessage);
|
||||||
@ -908,51 +883,6 @@ window.cadTasks = {
|
|||||||
});
|
});
|
||||||
this.render();
|
this.render();
|
||||||
},
|
},
|
||||||
focusMember(uid) {
|
|
||||||
let selectedMember = null;
|
|
||||||
|
|
||||||
this.groups.some((group) =>
|
|
||||||
this.normalizeCollection(group.members).some((member) => {
|
|
||||||
if ((member.uid || "") !== uid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedMember = member;
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!selectedMember) {
|
|
||||||
this.setStatus(
|
|
||||||
"Selected group member is no longer available.",
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const position = Array.isArray(selectedMember.position)
|
|
||||||
? selectedMember.position
|
|
||||||
: [];
|
|
||||||
if (position.length < 2) {
|
|
||||||
this.setStatus(
|
|
||||||
"Selected group member has no map position.",
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedRosterMemberUid = uid;
|
|
||||||
this.selectedDispatchGroupId = "";
|
|
||||||
this.selectedDispatchTaskId = "";
|
|
||||||
this.selectedDispatchRequestId = "";
|
|
||||||
const statusMessage = `Centering map on ${selectedMember.name || "group member"}...`;
|
|
||||||
this.setStatus(statusMessage, "info");
|
|
||||||
this.clearFocusStatusSoon(statusMessage);
|
|
||||||
window.mapUI.sendEvent("cad::members::focus", {
|
|
||||||
uid: uid,
|
|
||||||
});
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
focusTask(taskID) {
|
focusTask(taskID) {
|
||||||
const task = this.contracts.find((entry) => {
|
const task = this.contracts.find((entry) => {
|
||||||
const entryTaskID = entry.taskId || entry.taskID || "";
|
const entryTaskID = entry.taskId || entry.taskID || "";
|
||||||
@ -969,7 +899,6 @@ window.cadTasks = {
|
|||||||
this.selectedDispatchTaskId = taskID;
|
this.selectedDispatchTaskId = taskID;
|
||||||
this.selectedDispatchGroupId = "";
|
this.selectedDispatchGroupId = "";
|
||||||
this.selectedDispatchRequestId = "";
|
this.selectedDispatchRequestId = "";
|
||||||
this.selectedRosterMemberUid = "";
|
|
||||||
const statusMessage = `Centering map on ${task.title || taskID}...`;
|
const statusMessage = `Centering map on ${task.title || taskID}...`;
|
||||||
this.setStatus(statusMessage, "info");
|
this.setStatus(statusMessage, "info");
|
||||||
this.clearFocusStatusSoon(statusMessage);
|
this.clearFocusStatusSoon(statusMessage);
|
||||||
@ -998,7 +927,6 @@ window.cadTasks = {
|
|||||||
this.selectedDispatchRequestId = requestID;
|
this.selectedDispatchRequestId = requestID;
|
||||||
this.selectedDispatchGroupId = "";
|
this.selectedDispatchGroupId = "";
|
||||||
this.selectedDispatchTaskId = "";
|
this.selectedDispatchTaskId = "";
|
||||||
this.selectedRosterMemberUid = "";
|
|
||||||
const statusMessage = `Centering map on ${request.title || requestID}...`;
|
const statusMessage = `Centering map on ${request.title || requestID}...`;
|
||||||
this.setStatus(statusMessage, "info");
|
this.setStatus(statusMessage, "info");
|
||||||
this.clearFocusStatusSoon(statusMessage);
|
this.clearFocusStatusSoon(statusMessage);
|
||||||
@ -1139,17 +1067,9 @@ window.cadTasks = {
|
|||||||
);
|
);
|
||||||
const isAssignedToLeader =
|
const isAssignedToLeader =
|
||||||
this.isLeader() && assignedGroupId === currentGroupId;
|
this.isLeader() && assignedGroupId === currentGroupId;
|
||||||
const isSelected = taskId === this.selectedDispatchTaskId;
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div
|
<div class="task-card" data-task-id="${taskId}">
|
||||||
class="task-card dispatch-map-card ${isSelected ? "is-selected" : ""}"
|
|
||||||
data-task-id="${taskId}"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
onclick="window.cadTasks.focusTask('${taskId}')"
|
|
||||||
onkeydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); window.cadTasks.focusTask('${taskId}'); }"
|
|
||||||
>
|
|
||||||
<div class="task-card-header">
|
<div class="task-card-header">
|
||||||
<strong>${task.title || taskId}</strong>
|
<strong>${task.title || taskId}</strong>
|
||||||
<span class="task-type">${this.formatTypeLabel(task)}</span>
|
<span class="task-type">${this.formatTypeLabel(task)}</span>
|
||||||
@ -1162,8 +1082,8 @@ window.cadTasks = {
|
|||||||
${
|
${
|
||||||
isAssignedToLeader && assignmentState === "assigned"
|
isAssignedToLeader && assignmentState === "assigned"
|
||||||
? `<div class="task-action-row">
|
? `<div class="task-action-row">
|
||||||
<button type="button" class="task-accept-btn" onclick="event.stopPropagation(); window.cadTasks.acknowledgeTask('${taskId}')">Acknowledge</button>
|
<button type="button" class="task-accept-btn" onclick="window.cadTasks.acknowledgeTask('${taskId}')">Acknowledge</button>
|
||||||
<button type="button" class="task-secondary-btn" onclick="event.stopPropagation(); window.cadTasks.declineTask('${taskId}')">Decline</button>
|
<button type="button" class="task-secondary-btn" onclick="window.cadTasks.declineTask('${taskId}')">Decline</button>
|
||||||
</div>`
|
</div>`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
@ -1257,19 +1177,9 @@ window.cadTasks = {
|
|||||||
const leaderBadge = member.isLeader
|
const leaderBadge = member.isLeader
|
||||||
? '<span class="roster-leader-badge">Leader</span>'
|
? '<span class="roster-leader-badge">Leader</span>'
|
||||||
: "";
|
: "";
|
||||||
const memberUid = member.uid || "";
|
|
||||||
const isSelected =
|
|
||||||
memberUid && memberUid === this.selectedRosterMemberUid;
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div
|
<div class="task-card roster-member-card" data-member-id="${member.uid || ""}">
|
||||||
class="task-card roster-member-card dispatch-map-group-card ${isSelected ? "is-selected" : ""}"
|
|
||||||
data-member-id="${memberUid}"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
onclick="window.cadTasks.focusMember('${memberUid}')"
|
|
||||||
onkeydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); window.cadTasks.focusMember('${memberUid}'); }"
|
|
||||||
>
|
|
||||||
<div class="task-card-header">
|
<div class="task-card-header">
|
||||||
<strong>${member.name || "Unknown Operator"}</strong>
|
<strong>${member.name || "Unknown Operator"}</strong>
|
||||||
<span class="task-type">${lifeState}</span>
|
<span class="task-type">${lifeState}</span>
|
||||||
|
|||||||
@ -34,12 +34,6 @@ body {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dispatch-panel-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dispatch-kicker {
|
.dispatch-kicker {
|
||||||
margin: 0 0 4px;
|
margin: 0 0 4px;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
@ -69,12 +63,6 @@ body {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dispatch-btn-compact {
|
|
||||||
padding: 8px 10px;
|
|
||||||
min-height: 32px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dispatch-btn-secondary {
|
.dispatch-btn-secondary {
|
||||||
background: rgba(53, 40, 39, 0.92);
|
background: rgba(53, 40, 39, 0.92);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
class CfgSounds {
|
|
||||||
sounds[] = {};
|
|
||||||
class FORGE_timerBeep {
|
|
||||||
name = "FORGE_timerBeep";
|
|
||||||
sound[] = {QUOTE(PATHTOF(sounds\timerClick.wav)), 1, 3};
|
|
||||||
titles[] = {};
|
|
||||||
};
|
|
||||||
class FORGE_timerBeepShort {
|
|
||||||
name = "FORGE_timerBeepShort";
|
|
||||||
sound[] = {QUOTE(PATHTOF(sounds\timerClickShort.wav)), 1, 3};
|
|
||||||
titles[] = {};
|
|
||||||
};
|
|
||||||
class FORGE_timerEnd {
|
|
||||||
name = "FORGE_timerEnd";
|
|
||||||
sound[] = {QUOTE(PATHTOF(sounds\timerEnd.wav)), 1, 3};
|
|
||||||
titles[] = {};
|
|
||||||
};
|
|
||||||
class FORGE_defused {
|
|
||||||
name = "FORGE_defused";
|
|
||||||
sound[] = {QUOTE(PATHTOF(sounds\defused.wav)), 1, 3};
|
|
||||||
titles[] = {};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,18 +1,5 @@
|
|||||||
# Forge Client Common
|
# forge_client_common
|
||||||
|
|
||||||
## Overview
|
Common functionality shared between addons.
|
||||||
The common addon contains shared client-side UI bridge helpers and common
|
|
||||||
configuration used by browser-based feature addons.
|
|
||||||
|
|
||||||
## Dependencies
|
See [WEB_UI_FRAMEWORK.md](./WEB_UI_FRAMEWORK.md) for the proposed shared `CT_WEBBROWSER` UI framework layout and API.
|
||||||
- `forge_client_main`
|
|
||||||
|
|
||||||
## Main Components
|
|
||||||
- `fnc_initWebUIBridge.sqf` provides shared bridge behavior for web browser UI
|
|
||||||
controls.
|
|
||||||
- `WEB_UI_FRAMEWORK.md` documents the proposed shared browser runtime and event
|
|
||||||
API for Forge web UIs.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
Keep feature-specific behavior in the owning addon. Common should hold reusable
|
|
||||||
browser bridge patterns, not copied application logic.
|
|
||||||
|
|||||||
@ -17,5 +17,4 @@ class CfgPatches {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#include "CfgEventHandlers.hpp"
|
#include "CfgEventHandlers.hpp"
|
||||||
#include "CfgSounds.hpp"
|
|
||||||
#include "CfgVehicles.hpp"
|
#include "CfgVehicles.hpp"
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
!function(e){const o=e.ForgeSiteLoader=e.ForgeSiteLoader||{};function t(e){return"string"==typeof e&&e.startsWith("forge\\")}function r({addonRoot:e,browserBase:o,assetPath:r}){if("undefined"!=typeof A3API&&A3API&&"function"==typeof A3API.RequestFile){const o=t(r)?r:e+String(r||"").replace(/\//g,"\\");return A3API.RequestFile(o)}const n=t(r)?r:function(e,o){return`${String(e||"./").replace(/\\/g,"/")}${String(o||"").replace(/\\/g,"/")}`}(o,r);return fetch(n).then(e=>{if(!e.ok)throw new Error(`Failed to load ${n}`);return e.text()})}function n(e){const o=document.createElement("style");o.textContent=e,document.head.appendChild(o)}function a(e){const o=document.createElement("script");o.text=e,document.head.appendChild(o)}async function i(e){const o=e&&e.addonName?e.addonName:"";if(!o)throw new Error("ForgeSiteLoader requires a config.addonName value.");const t=e.addonRoot||function(e){return`forge\\forge_client\\addons\\${e}\\ui\\_site\\`}(o),i=e.browserAddonBase||"./",s=e.browserCommonBase||"../../../common/ui/_site/",c=Array.isArray(e.styles)?e.styles:[],d=Array.isArray(e.commonScripts)?e.commonScripts:[],f=Array.isArray(e.scripts)?e.scripts:[];(await Promise.all(c.map(e=>r({addonRoot:t,browserBase:i,assetPath:e})))).forEach(n);(await Promise.all(d.map(e=>r({addonRoot:"forge\\forge_client\\addons\\common\\ui\\_site\\",browserBase:s,assetPath:e})))).forEach(a);(await Promise.all(f.map(e=>r({addonRoot:t,browserBase:i,assetPath:e})))).forEach(a)}o.boot=i,e.ForgeSiteConfig&&!1!==e.ForgeSiteConfig.autoBoot&&i(e.ForgeSiteConfig).catch(o=>{const t=e.ForgeSiteConfig.logLabel||e.ForgeSiteConfig.addonName||"Forge UI";console.error(`[${t}] Failed to load site assets.`,o)})}(window);
|
!function(e){const o=e.ForgeSiteLoader=e.ForgeSiteLoader||{};function t(e){return"string"==typeof e&&e.startsWith("forge\\")}function r({addonRoot:e,browserBase:o,assetPath:r}){if("undefined"!=typeof A3API&&A3API&&"function"==typeof A3API.RequestFile){const o=t(r)?r:e+String(r||"").replace(/\//g,"\\");return A3API.RequestFile(o)}const n=t(r)?r:function(e,o){return`${String(e||"./").replace(/\\/g,"/")}${String(o||"").replace(/\\/g,"/")}`}(o,r);return fetch(n).then(e=>{if(!e.ok)throw new Error(`Failed to load ${n}`);return e.text()})}function n(e){const o=document.createElement("style");o.textContent=e,document.head.appendChild(o)}function a(e){const o=document.createElement("script");o.text=e,document.head.appendChild(o)}async function i(e){const o=e&&e.addonName?e.addonName:"";if(!o)throw new Error("ForgeSiteLoader requires a config.addonName value.");const t=function(e){return`forge\\forge_client\\addons\\${e}\\ui\\_site\\`}(o),i=e.browserAddonBase||"./",s=e.browserCommonBase||"../../../common/ui/_site/",c=Array.isArray(e.styles)?e.styles:[],d=Array.isArray(e.commonScripts)?e.commonScripts:[],f=Array.isArray(e.scripts)?e.scripts:[];(await Promise.all(c.map(e=>r({addonRoot:t,browserBase:i,assetPath:e})))).forEach(n);(await Promise.all(d.map(e=>r({addonRoot:"forge\\forge_client\\addons\\common\\ui\\_site\\",browserBase:s,assetPath:e})))).forEach(a);(await Promise.all(f.map(e=>r({addonRoot:t,browserBase:i,assetPath:e})))).forEach(a)}o.boot=i,e.ForgeSiteConfig&&!1!==e.ForgeSiteConfig.autoBoot&&i(e.ForgeSiteConfig).catch(o=>{const t=e.ForgeSiteConfig.logLabel||e.ForgeSiteConfig.addonName||"Forge UI";console.error(`[${t}] Failed to load site assets.`,o)})}(window);
|
||||||
@ -68,7 +68,7 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const addonRoot = config.addonRoot || normalizeAddonRoot(addonName);
|
const addonRoot = normalizeAddonRoot(addonName);
|
||||||
const browserAddonBase = config.browserAddonBase || "./";
|
const browserAddonBase = config.browserAddonBase || "./";
|
||||||
const browserCommonBase =
|
const browserCommonBase =
|
||||||
config.browserCommonBase || defaultBrowserCommonBase;
|
config.browserCommonBase || defaultBrowserCommonBase;
|
||||||
|
|||||||
@ -1,48 +1,3 @@
|
|||||||
# Forge Client Garage
|
# forge_client_garage
|
||||||
|
|
||||||
## Overview
|
Description for this addon
|
||||||
The garage addon provides player vehicle storage UI, vehicle store/retrieve
|
|
||||||
actions, selected nearby vehicle service requests, and virtual garage state on
|
|
||||||
the client.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
- `forge_client_common`
|
|
||||||
- `forge_client_main`
|
|
||||||
- server garage events from `forge_server_garage`
|
|
||||||
- notifications for action feedback
|
|
||||||
|
|
||||||
## Main Components
|
|
||||||
- `fnc_initRepository.sqf` manages player garage view state.
|
|
||||||
- `fnc_initVGRepository.sqf` manages virtual garage view state.
|
|
||||||
- `fnc_initHelperService.sqf` resolves vehicle names, hit points, and payload
|
|
||||||
details.
|
|
||||||
- `fnc_initContextService.sqf` gathers nearby/current vehicle context.
|
|
||||||
- `fnc_initPayloadService.sqf` builds browser hydrate payloads.
|
|
||||||
- `fnc_initActionService.sqf` sends store/retrieve requests, forwards selected
|
|
||||||
nearby vehicle refuel/repair service requests, and handles action responses.
|
|
||||||
- `fnc_initUIBridge.sqf` pushes hydrate/sync events to the browser.
|
|
||||||
- `fnc_openUI.sqf` opens `RscGarage`.
|
|
||||||
- `fnc_openVG.sqf` opens the Arma garage-style virtual garage view.
|
|
||||||
|
|
||||||
## Browser Events
|
|
||||||
- `garage::ready`
|
|
||||||
- `garage::refresh`
|
|
||||||
- `garage::vehicle::retrieve::request`
|
|
||||||
- `garage::vehicle::store::request`
|
|
||||||
- `garage::vehicle::refuel::request`
|
|
||||||
- `garage::vehicle::repair::request`
|
|
||||||
- `garage::close`
|
|
||||||
|
|
||||||
## Runtime Notes
|
|
||||||
The client builds vehicle context and sends requests. The server garage addon
|
|
||||||
and extension own stored vehicle state.
|
|
||||||
|
|
||||||
Virtual garage spawning resolves the active garage context and category lane,
|
|
||||||
then finalizes only the vehicle selected in that BIS garage session. Nearby
|
|
||||||
world vehicles are ignored as spawn candidates and are only used for the spawn
|
|
||||||
blocking check at the resolved lane.
|
|
||||||
|
|
||||||
Refuel and repair buttons are available from the selected vehicle detail panel
|
|
||||||
for nearby world vehicles. Stored records must be retrieved before they can be
|
|
||||||
serviced because fuel and repair operate on live vehicle objects. Service
|
|
||||||
billing is handled by the server economy addon and charges organization funds.
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* File: fnc_handleUIEvents.sqf
|
* File: fnc_handleUIEvents.sqf
|
||||||
* Author: IDSolutions
|
* Author: IDSolutions
|
||||||
* Date: 2025-12-16
|
* Date: 2025-12-16
|
||||||
* Last Update: 2026-04-18
|
* Last Update: 2026-01-30
|
||||||
* Public: No
|
* Public: No
|
||||||
*
|
*
|
||||||
* Description:
|
* Description:
|
||||||
@ -53,21 +53,6 @@ switch (_event) do {
|
|||||||
GVAR(GarageActionService) call ["handleStoreRequest", [_data]];
|
GVAR(GarageActionService) call ["handleStoreRequest", [_data]];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
case "garage::vehicle::refuel::request": {
|
|
||||||
if !(isNil QGVAR(GarageActionService)) then {
|
|
||||||
GVAR(GarageActionService) call ["handleRefuelRequest", [_data]];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "garage::vehicle::repair::request": {
|
|
||||||
if !(isNil QGVAR(GarageActionService)) then {
|
|
||||||
GVAR(GarageActionService) call ["handleRepairRequest", [_data]];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "garage::vehicle::rearm::request": {
|
|
||||||
if !(isNil QGVAR(GarageActionService)) then {
|
|
||||||
GVAR(GarageActionService) call ["handleRearmRequest", [_data]];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "garage::refresh": {
|
case "garage::refresh": {
|
||||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
if !(isNil QGVAR(GarageUIBridge)) then {
|
||||||
GVAR(GarageUIBridge) call ["refreshGarage", []];
|
GVAR(GarageUIBridge) call ["refreshGarage", []];
|
||||||
|
|||||||
@ -4,12 +4,10 @@
|
|||||||
* File: fnc_initActionService.sqf
|
* File: fnc_initActionService.sqf
|
||||||
* Author: IDSolutions
|
* Author: IDSolutions
|
||||||
* Date: 2026-03-27
|
* Date: 2026-03-27
|
||||||
* Last Update: 2026-04-18
|
|
||||||
* Public: No
|
* Public: No
|
||||||
*
|
*
|
||||||
* Description:
|
* Description:
|
||||||
* Initializes the garage action service for retrieve, store, refuel, rearm,
|
* Initializes the garage action service for retrieve and store world actions.
|
||||||
* and repair world actions.
|
|
||||||
*
|
*
|
||||||
* Arguments:
|
* Arguments:
|
||||||
* None
|
* None
|
||||||
@ -28,52 +26,6 @@ GVAR(GarageActionServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
_self set ["pendingStoreVehicle", objNull];
|
_self set ["pendingStoreVehicle", objNull];
|
||||||
_self set ["pendingRetrieve", createHashMap];
|
_self set ["pendingRetrieve", createHashMap];
|
||||||
}],
|
}],
|
||||||
["sendServiceResult", compileFinal {
|
|
||||||
params [["_action", "", [""]], ["_success", false, [false]], ["_message", "", [""]]];
|
|
||||||
|
|
||||||
private _event = ["garage::service::failure", "garage::service::success"] select _success;
|
|
||||||
GVAR(GarageUIBridge) call ["sendEvent", [_event, createHashMapFromArray [["action", _action], ["message", _message]]]];
|
|
||||||
}],
|
|
||||||
["refreshAfterService", compileFinal {
|
|
||||||
[] spawn {
|
|
||||||
sleep 0.75;
|
|
||||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
|
||||||
GVAR(GarageUIBridge) call ["refreshGarage", []];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}],
|
|
||||||
["resolveServiceVehicle", compileFinal {
|
|
||||||
params [["_data", createHashMap, [createHashMap]], ["_action", "service", [""]]];
|
|
||||||
|
|
||||||
private _netId = _data getOrDefault ["netId", ""];
|
|
||||||
if (_netId isEqualTo "") exitWith {
|
|
||||||
_self call ["sendServiceResult", [_action, false, "Select a nearby vehicle first."]];
|
|
||||||
objNull
|
|
||||||
};
|
|
||||||
|
|
||||||
private _vehicle = objectFromNetId _netId;
|
|
||||||
if (isNull _vehicle) exitWith {
|
|
||||||
_self call ["sendServiceResult", [_action, false, "The selected vehicle is no longer available."]];
|
|
||||||
objNull
|
|
||||||
};
|
|
||||||
|
|
||||||
if !(_vehicle isKindOf "Car" || { _vehicle isKindOf "Tank" } || { _vehicle isKindOf "Air" } || { _vehicle isKindOf "Ship" }) exitWith {
|
|
||||||
_self call ["sendServiceResult", [_action, false, "Selected object is not a serviceable vehicle."]];
|
|
||||||
objNull
|
|
||||||
};
|
|
||||||
|
|
||||||
_vehicle
|
|
||||||
}],
|
|
||||||
["vehicleNeedsRepair", compileFinal {
|
|
||||||
params [["_vehicle", objNull, [objNull]]];
|
|
||||||
|
|
||||||
if (isNull _vehicle) exitWith { false };
|
|
||||||
if ((damage _vehicle) > 0.001) exitWith { true };
|
|
||||||
|
|
||||||
private _rawHitPoints = getAllHitPointsDamage _vehicle;
|
|
||||||
private _hitPointValues = if (_rawHitPoints isEqualType [] && { count _rawHitPoints >= 3 }) then { _rawHitPoints param [2, []] } else { [] };
|
|
||||||
({ _x > 0.001 } count _hitPointValues) > 0
|
|
||||||
}],
|
|
||||||
["handleRetrieveRequest", compileFinal {
|
["handleRetrieveRequest", compileFinal {
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
params [["_data", createHashMap, [createHashMap]]];
|
||||||
|
|
||||||
@ -88,21 +40,9 @@ GVAR(GarageActionServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "Stored vehicle record could not be found."]]]];
|
GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "Stored vehicle record could not be found."]]]];
|
||||||
};
|
};
|
||||||
|
|
||||||
private _className = _vehicleData getOrDefault ["classname", ""];
|
|
||||||
if (_className isEqualTo "") exitWith {
|
|
||||||
GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "Stored vehicle record is missing a classname."]]]];
|
|
||||||
};
|
|
||||||
|
|
||||||
private _context = GVAR(GarageContextService) call ["getContext", []];
|
private _context = GVAR(GarageContextService) call ["getContext", []];
|
||||||
private _vehicleCategory = GVAR(GarageHelperService) call ["resolveVGCategory", [_className]];
|
private _spawnPosition = _context getOrDefault ["spawnPosition", getPosATL player];
|
||||||
private _spawnLane = GVAR(GarageContextService) call ["getExactSpawnLane", [_vehicleCategory, _context]];
|
private _spawnHeading = _context getOrDefault ["spawnHeading", getDir player];
|
||||||
if (_spawnLane isEqualTo createHashMap) exitWith {
|
|
||||||
private _categoryLabel = GVAR(GarageHelperService) call ["resolveGarageCategoryLabel", [_vehicleCategory]];
|
|
||||||
GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", format ["This garage does not support spawning %1.", _categoryLabel]]]]];
|
|
||||||
};
|
|
||||||
|
|
||||||
private _spawnPosition = _spawnLane getOrDefault ["spawnPosition", _context getOrDefault ["spawnPosition", getPosATL player]];
|
|
||||||
private _spawnHeading = _spawnLane getOrDefault ["spawnHeading", _context getOrDefault ["spawnHeading", getDir player]];
|
|
||||||
private _spawnRadius = _context getOrDefault ["spawnRadius", 6];
|
private _spawnRadius = _context getOrDefault ["spawnRadius", 6];
|
||||||
private _blockingVehicles = [];
|
private _blockingVehicles = [];
|
||||||
{ _blockingVehicles pushBackUnique _x; } forEach (_spawnPosition nearEntities [["Car", "Tank", "Air", "Ship"], _spawnRadius]);
|
{ _blockingVehicles pushBackUnique _x; } forEach (_spawnPosition nearEntities [["Car", "Tank", "Air", "Ship"], _spawnRadius]);
|
||||||
@ -111,6 +51,11 @@ GVAR(GarageActionServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "The garage spawn area is blocked."]]]];
|
GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "The garage spawn area is blocked."]]]];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private _className = _vehicleData getOrDefault ["classname", ""];
|
||||||
|
if (_className isEqualTo "") exitWith {
|
||||||
|
GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "Stored vehicle record is missing a classname."]]]];
|
||||||
|
};
|
||||||
|
|
||||||
private _vehicle = createVehicle [_className, _spawnPosition, [], 0, "CAN_COLLIDE"];
|
private _vehicle = createVehicle [_className, _spawnPosition, [], 0, "CAN_COLLIDE"];
|
||||||
_vehicle setDir _spawnHeading;
|
_vehicle setDir _spawnHeading;
|
||||||
_vehicle setFuel (_vehicleData getOrDefault ["fuel", 0]);
|
_vehicle setFuel (_vehicleData getOrDefault ["fuel", 0]);
|
||||||
@ -150,50 +95,7 @@ GVAR(GarageActionServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
private _hitPointsJson = toJSON (createHashMapFromArray [["names", _rawHitPoints param [0, []]], ["selections", _rawHitPoints param [1, []]], ["values", _rawHitPoints param [2, []]]]);
|
private _hitPointsJson = toJSON (createHashMapFromArray [["names", _rawHitPoints param [0, []]], ["selections", _rawHitPoints param [1, []]], ["values", _rawHitPoints param [2, []]]]);
|
||||||
|
|
||||||
_self set ["pendingStoreVehicle", _vehicle];
|
_self set ["pendingStoreVehicle", _vehicle];
|
||||||
[SRPC(garage,requestStoreVehicle), [getPlayerUID player, netId _vehicle, typeOf _vehicle, fuel _vehicle, damage _vehicle, _hitPointsJson]] call CFUNC(serverEvent);
|
[SRPC(garage,requestStoreVehicle), [getPlayerUID player, typeOf _vehicle, fuel _vehicle, damage _vehicle, _hitPointsJson]] call CFUNC(serverEvent);
|
||||||
}],
|
|
||||||
["handleRefuelRequest", compileFinal {
|
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _vehicle = _self call ["resolveServiceVehicle", [_data, "refuel"]];
|
|
||||||
if (isNull _vehicle) exitWith { false };
|
|
||||||
|
|
||||||
if ((fuel _vehicle) >= 0.999) exitWith {
|
|
||||||
_self call ["sendServiceResult", ["refuel", false, "Vehicle fuel tank is already full."]];
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
[SRPC(economy,RefuelService), [_vehicle, player]] call CFUNC(serverEvent);
|
|
||||||
_self call ["sendServiceResult", ["refuel", true, "Refuel request sent. Billing result will appear as a notification."]];
|
|
||||||
_self call ["refreshAfterService", []];
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["handleRepairRequest", compileFinal {
|
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _vehicle = _self call ["resolveServiceVehicle", [_data, "repair"]];
|
|
||||||
if (isNull _vehicle) exitWith { false };
|
|
||||||
|
|
||||||
if !(_self call ["vehicleNeedsRepair", [_vehicle]]) exitWith {
|
|
||||||
_self call ["sendServiceResult", ["repair", false, "Vehicle has no reported damage."]];
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
[SRPC(economy,RepairService), [_vehicle, player, -1]] call CFUNC(serverEvent);
|
|
||||||
_self call ["sendServiceResult", ["repair", true, "Repair request sent. Billing result will appear as a notification."]];
|
|
||||||
_self call ["refreshAfterService", []];
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["handleRearmRequest", compileFinal {
|
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _vehicle = _self call ["resolveServiceVehicle", [_data, "rearm"]];
|
|
||||||
if (isNull _vehicle) exitWith { false };
|
|
||||||
|
|
||||||
[SRPC(economy,RearmService), [_vehicle, player, -1]] call CFUNC(serverEvent);
|
|
||||||
_self call ["sendServiceResult", ["rearm", true, "Rearm request sent. Billing result will appear as a notification."]];
|
|
||||||
_self call ["refreshAfterService", []];
|
|
||||||
true
|
|
||||||
}],
|
}],
|
||||||
["handleActionResponse", compileFinal {
|
["handleActionResponse", compileFinal {
|
||||||
params [["_payload", createHashMap, [createHashMap]]];
|
params [["_payload", createHashMap, [createHashMap]]];
|
||||||
|
|||||||
@ -22,244 +22,92 @@
|
|||||||
#pragma hemtt ignore_variables ["_self"]
|
#pragma hemtt ignore_variables ["_self"]
|
||||||
GVAR(GarageContextServiceBaseClass) = compileFinal createHashMapFromArray [
|
GVAR(GarageContextServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||||
["#type", "GarageContextServiceBaseClass"],
|
["#type", "GarageContextServiceBaseClass"],
|
||||||
["#create", compileFinal {
|
["#create", compileFinal { _self set ["lastContext", createHashMap]; }],
|
||||||
_self set ["lastContext", createHashMap];
|
["#delete", compileFinal { _self set ["lastContext", createHashMap]; }],
|
||||||
_self set ["activeGarageObject", objNull];
|
|
||||||
}],
|
|
||||||
["#delete", compileFinal {
|
|
||||||
_self set ["lastContext", createHashMap];
|
|
||||||
_self set ["activeGarageObject", objNull];
|
|
||||||
}],
|
|
||||||
["setActiveGarageObject", compileFinal {
|
|
||||||
params [["_garageObject", objNull, [objNull]]];
|
|
||||||
|
|
||||||
if (isNull _garageObject || { !(_garageObject getVariable ["isGarage", false]) }) exitWith {
|
|
||||||
_self set ["activeGarageObject", objNull];
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
_self set ["activeGarageObject", _garageObject];
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["getActiveGarageObject", compileFinal {
|
|
||||||
private _garageObject = _self getOrDefault ["activeGarageObject", objNull];
|
|
||||||
if (isNull _garageObject || { !(_garageObject getVariable ["isGarage", false]) }) exitWith { objNull };
|
|
||||||
if ((player distance2D _garageObject) > 12) exitWith {
|
|
||||||
_self set ["activeGarageObject", objNull];
|
|
||||||
objNull
|
|
||||||
};
|
|
||||||
|
|
||||||
_garageObject
|
|
||||||
}],
|
|
||||||
["createDefaultContext", compileFinal {
|
["createDefaultContext", compileFinal {
|
||||||
createHashMapFromArray [
|
createHashMapFromArray [
|
||||||
["name", "Vehicle Garage"],
|
["name", "Vehicle Garage"],
|
||||||
["anchorPosition", getPosATL player],
|
["anchorPosition", getPosATL player],
|
||||||
["sourceObject", objNull],
|
["sourceObject", objNull],
|
||||||
["garageType", ""],
|
|
||||||
["spawnHeading", getDir player],
|
["spawnHeading", getDir player],
|
||||||
["spawnPosition", player getPos [8, getDir player]],
|
["spawnPosition", player getPos [8, getDir player]],
|
||||||
["spawnLanes", createHashMap],
|
|
||||||
["spawnRadius", 6],
|
["spawnRadius", 6],
|
||||||
["nearbyRadius", 30],
|
["nearbyRadius", 30]
|
||||||
["laneRadius", 25]
|
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
["findNearbyGarageObject", compileFinal {
|
["scanEntryValues", compileFinal {
|
||||||
private _nearestGarage = objNull;
|
params [["_values", [], [[]]], ["_state", createHashMap, [createHashMap]]];
|
||||||
private _nearestDistance = 1e10;
|
|
||||||
|
|
||||||
{
|
{
|
||||||
if (isNull _x || { !(_x getVariable ["isGarage", false]) }) then { continue; };
|
if (_x isEqualType "" && { (_state getOrDefault ["name", "Vehicle Garage"]) isEqualTo "Vehicle Garage" }) then { _state set ["name", _x]; };
|
||||||
private _distance = player distance2D _x;
|
if (_x isEqualType "") then {
|
||||||
if (_distance < _nearestDistance) then {
|
private _resolvedObject = _state getOrDefault ["sourceObject", objNull];
|
||||||
_nearestDistance = _distance;
|
if (isNull _resolvedObject) then {
|
||||||
_nearestGarage = _x;
|
private _namedObject = missionNamespace getVariable [_x, objNull];
|
||||||
|
if (!isNull _namedObject) then { _state set ["sourceObject", _namedObject]; };
|
||||||
|
};
|
||||||
|
if ((_state getOrDefault ["anchorPosition", []]) isEqualTo [] && { _x in allMapMarkers }) then { _state set ["anchorPosition", markerPos _x]; };
|
||||||
|
continue;
|
||||||
};
|
};
|
||||||
} forEach (player nearObjects 12);
|
if (_x isEqualType objNull && { isNull (_state getOrDefault ["sourceObject", objNull]) }) then {
|
||||||
|
_state set ["sourceObject", _x];
|
||||||
_nearestGarage
|
if ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) then { _state set ["anchorPosition", getPosATL _x]; };
|
||||||
}],
|
continue;
|
||||||
["resolveGarageName", compileFinal {
|
|
||||||
params [["_garageObject", objNull, [objNull]]];
|
|
||||||
|
|
||||||
if (isNull _garageObject) exitWith { "Vehicle Garage" };
|
|
||||||
|
|
||||||
private _displayName = _garageObject getVariable ["garageName", ""];
|
|
||||||
if (_displayName isNotEqualTo "") exitWith { _displayName };
|
|
||||||
|
|
||||||
private _varName = vehicleVarName _garageObject;
|
|
||||||
if (_varName isEqualTo "") exitWith { "Vehicle Garage" };
|
|
||||||
|
|
||||||
_varName
|
|
||||||
}],
|
|
||||||
["buildMarkerLane", compileFinal {
|
|
||||||
params [["_markerName", "", [""]], ["_garageObject", objNull, [objNull]]];
|
|
||||||
|
|
||||||
if (_markerName isEqualTo "" || { markerShape _markerName isEqualTo "" }) exitWith { createHashMap };
|
|
||||||
|
|
||||||
private _spawnCategory = GVAR(GarageHelperService) call ["inferGarageCategory", [_markerName]];
|
|
||||||
if (_spawnCategory isEqualTo "") exitWith { createHashMap };
|
|
||||||
|
|
||||||
private _spawnPosition = markerPos _markerName;
|
|
||||||
private _interactionPosition = if (isNull _garageObject) then { _spawnPosition } else { getPosATL _garageObject };
|
|
||||||
private _markerDistance = if (isNull _garageObject) then { player distance2D _spawnPosition } else { _garageObject distance2D _spawnPosition };
|
|
||||||
private _garageVarName = if (isNull _garageObject) then { "" } else { toLowerANSI (vehicleVarName _garageObject) };
|
|
||||||
private _markerKey = toLowerANSI _markerName;
|
|
||||||
private _isExplicitMatch = _garageVarName isNotEqualTo "" && { (_markerKey find _garageVarName) >= 0 };
|
|
||||||
|
|
||||||
createHashMapFromArray [
|
|
||||||
["name", _markerName],
|
|
||||||
["isExplicitMatch", _isExplicitMatch],
|
|
||||||
["interactionPosition", _interactionPosition],
|
|
||||||
["sourceObject", _garageObject],
|
|
||||||
["spawnCategory", _spawnCategory],
|
|
||||||
["spawnHeading", markerDir _markerName],
|
|
||||||
["spawnPosition", _spawnPosition],
|
|
||||||
["score", _markerDistance]
|
|
||||||
]
|
|
||||||
}],
|
|
||||||
["discoverSpawnLanes", compileFinal {
|
|
||||||
params [["_garageObject", objNull, [objNull]]];
|
|
||||||
|
|
||||||
private _laneRadius = (_self call ["createDefaultContext", []]) getOrDefault ["laneRadius", 25];
|
|
||||||
private _explicitLanes = createHashMap;
|
|
||||||
private _fallbackLanes = createHashMap;
|
|
||||||
|
|
||||||
{
|
|
||||||
private _markerName = _x;
|
|
||||||
if ((toLowerANSI _markerName find "garage") < 0) then { continue; };
|
|
||||||
|
|
||||||
private _entry = _self call ["buildMarkerLane", [_markerName, _garageObject]];
|
|
||||||
if (_entry isEqualTo createHashMap) then { continue; };
|
|
||||||
|
|
||||||
private _spawnPosition = _entry getOrDefault ["spawnPosition", []];
|
|
||||||
if (_spawnPosition isEqualTo []) then { continue; };
|
|
||||||
|
|
||||||
private _distance = if (isNull _garageObject) then { player distance2D _spawnPosition } else { _garageObject distance2D _spawnPosition };
|
|
||||||
if (_distance > _laneRadius) then { continue; };
|
|
||||||
|
|
||||||
private _spawnCategory = _entry getOrDefault ["spawnCategory", ""];
|
|
||||||
private _laneSet = _fallbackLanes;
|
|
||||||
if (_entry getOrDefault ["isExplicitMatch", false]) then {
|
|
||||||
_laneSet = _explicitLanes;
|
|
||||||
};
|
};
|
||||||
private _currentEntry = _laneSet getOrDefault [_spawnCategory, createHashMap];
|
if (_x isEqualType 0 && { (_state getOrDefault ["spawnHeading", -1]) < 0 }) then { _state set ["spawnHeading", _x]; continue; };
|
||||||
|
if (_x isEqualType [] && { count _x > 0 }) then {
|
||||||
if (_currentEntry isEqualTo createHashMap || { (_entry getOrDefault ["score", 1e10]) < (_currentEntry getOrDefault ["score", 1e10]) }) then {
|
if ({ _x isEqualType 0 } count _x >= 2 && { ((_state getOrDefault ["offset", []]) isEqualTo []) || ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) }) then {
|
||||||
_laneSet set [_spawnCategory, _entry];
|
if ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) then { _state set ["anchorPosition", _x]; } else { _state set ["offset", _x]; };
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
_self call ["scanEntryValues", [_x, _state]];
|
||||||
};
|
};
|
||||||
} forEach allMapMarkers;
|
} forEach _values;
|
||||||
|
_state
|
||||||
private _lanes = createHashMap;
|
|
||||||
{ _lanes set [_x, _y]; } forEach _fallbackLanes;
|
|
||||||
{ _lanes set [_x, _y]; } forEach _explicitLanes;
|
|
||||||
|
|
||||||
_lanes
|
|
||||||
}],
|
}],
|
||||||
["selectSpawnLane", compileFinal {
|
["resolveEntry", compileFinal {
|
||||||
params [
|
params [["_entry", [], [[]]]];
|
||||||
["_lanes", createHashMap, [createHashMap]],
|
private _state = createHashMapFromArray [["name", "Vehicle Garage"], ["anchorPosition", []], ["sourceObject", objNull], ["offset", []], ["spawnHeading", -1]];
|
||||||
["_preferredCategory", "", [""]],
|
_self call ["scanEntryValues", [_entry, _state]];
|
||||||
["_defaultPosition", [], [[]]],
|
private _anchorPosition = _state getOrDefault ["anchorPosition", []];
|
||||||
["_defaultHeading", 0, [0]]
|
private _offset = _state getOrDefault ["offset", []];
|
||||||
];
|
private _spawnPosition = if (_anchorPosition isEqualTo []) then { [] } else { if (_offset isEqualTo []) then { _anchorPosition } else { _anchorPosition vectorAdd _offset } };
|
||||||
|
createHashMapFromArray [["name", _state getOrDefault ["name", "Vehicle Garage"]], ["anchorPosition", _anchorPosition], ["sourceObject", _state getOrDefault ["sourceObject", objNull]], ["spawnHeading", _state getOrDefault ["spawnHeading", -1]], ["spawnPosition", _spawnPosition]]
|
||||||
private _normalizedCategory = GVAR(GarageHelperService) call ["normalizeGarageCategory", [_preferredCategory]];
|
|
||||||
private _lane = createHashMap;
|
|
||||||
|
|
||||||
if (_normalizedCategory isNotEqualTo "") then {
|
|
||||||
_lane = _lanes getOrDefault [_normalizedCategory, createHashMap];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (_lane isEqualTo createHashMap) then {
|
|
||||||
{
|
|
||||||
private _candidate = _lanes getOrDefault [_x, createHashMap];
|
|
||||||
if (_candidate isNotEqualTo createHashMap) exitWith { _lane = _candidate; };
|
|
||||||
} forEach ["cars", "armor", "helis", "planes", "naval", "other"];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (_lane isEqualTo createHashMap) then {
|
|
||||||
_lane = createHashMapFromArray [
|
|
||||||
["spawnCategory", _normalizedCategory],
|
|
||||||
["spawnHeading", _defaultHeading],
|
|
||||||
["spawnPosition", _defaultPosition]
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
_lane
|
|
||||||
}],
|
|
||||||
["getSpawnLane", compileFinal {
|
|
||||||
params [["_category", "", [""]], ["_context", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _resolvedContext = _context;
|
|
||||||
if (_resolvedContext isEqualTo createHashMap) then {
|
|
||||||
_resolvedContext = _self call ["getContext", []];
|
|
||||||
};
|
|
||||||
|
|
||||||
private _spawnLanes = _resolvedContext getOrDefault ["spawnLanes", createHashMap];
|
|
||||||
private _defaultPosition = _resolvedContext getOrDefault ["spawnPosition", getPosATL player];
|
|
||||||
private _defaultHeading = _resolvedContext getOrDefault ["spawnHeading", getDir player];
|
|
||||||
_self call ["selectSpawnLane", [_spawnLanes, _category, _defaultPosition, _defaultHeading]]
|
|
||||||
}],
|
|
||||||
["getExactSpawnLane", compileFinal {
|
|
||||||
params [["_category", "", [""]], ["_context", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _resolvedContext = _context;
|
|
||||||
if (_resolvedContext isEqualTo createHashMap) then {
|
|
||||||
_resolvedContext = _self call ["getContext", []];
|
|
||||||
};
|
|
||||||
|
|
||||||
private _normalizedCategory = GVAR(GarageHelperService) call ["normalizeGarageCategory", [_category]];
|
|
||||||
if (_normalizedCategory isEqualTo "") exitWith { createHashMap };
|
|
||||||
|
|
||||||
private _spawnLanes = _resolvedContext getOrDefault ["spawnLanes", createHashMap];
|
|
||||||
_spawnLanes getOrDefault [_normalizedCategory, createHashMap]
|
|
||||||
}],
|
}],
|
||||||
["resolveContext", compileFinal {
|
["resolveContext", compileFinal {
|
||||||
params [["_preferredGarageObject", objNull, [objNull]]];
|
|
||||||
|
|
||||||
private _context = _self call ["createDefaultContext", []];
|
private _context = _self call ["createDefaultContext", []];
|
||||||
private _garageObject = _preferredGarageObject;
|
private _locations = (missionConfigFile >> "FORGE_CfgGarages" >> "locations") call BFUNC(getCfgData);
|
||||||
if (isNull _garageObject || { !(_garageObject getVariable ["isGarage", false]) }) then {
|
if !(_locations isEqualType []) exitWith { _self set ["lastContext", _context]; _context };
|
||||||
_garageObject = _self call ["getActiveGarageObject", []];
|
|
||||||
};
|
|
||||||
if (isNull _garageObject) then {
|
|
||||||
_garageObject = _self call ["findNearbyGarageObject", []];
|
|
||||||
};
|
|
||||||
private _garageName = _self call ["resolveGarageName", [_garageObject]];
|
|
||||||
private _garageType = "";
|
|
||||||
private _anchorPosition = getPosATL player;
|
|
||||||
private _spawnHeading = getDir player;
|
|
||||||
private _spawnPosition = player getPos [8, _spawnHeading];
|
|
||||||
private _spawnLanes = createHashMap;
|
|
||||||
|
|
||||||
if (!isNull _garageObject) then {
|
private _nearestEntry = [];
|
||||||
_garageType = GVAR(GarageHelperService) call ["normalizeGarageCategory", [_garageObject getVariable ["garageType", ""]]];
|
private _nearestDistance = 1e10;
|
||||||
_anchorPosition = getPosATL _garageObject;
|
{
|
||||||
_spawnHeading = getDir _garageObject;
|
private _entry = _self call ["resolveEntry", [_x]];
|
||||||
_spawnPosition = _garageObject getPos [8, _spawnHeading];
|
private _anchorPosition = _entry getOrDefault ["anchorPosition", []];
|
||||||
_spawnLanes = _self call ["discoverSpawnLanes", [_garageObject]];
|
if (_anchorPosition isEqualTo []) then { continue; };
|
||||||
};
|
private _distance = player distance2D _anchorPosition;
|
||||||
|
if (_distance < _nearestDistance) then { _nearestDistance = _distance; _nearestEntry = _entry; };
|
||||||
|
} forEach _locations;
|
||||||
|
|
||||||
private _selectedLane = _self call ["selectSpawnLane", [_spawnLanes, _garageType, _spawnPosition, _spawnHeading]];
|
if (_nearestEntry isEqualTo []) exitWith { _self set ["lastContext", _context]; _context };
|
||||||
_spawnHeading = _selectedLane getOrDefault ["spawnHeading", _spawnHeading];
|
|
||||||
_spawnPosition = _selectedLane getOrDefault ["spawnPosition", _spawnPosition];
|
private _anchorPosition = _nearestEntry getOrDefault ["anchorPosition", []];
|
||||||
|
private _garageObject = _nearestEntry getOrDefault ["sourceObject", objNull];
|
||||||
|
private _garageName = _nearestEntry getOrDefault ["name", "Vehicle Garage"];
|
||||||
|
private _spawnHeading = _nearestEntry getOrDefault ["spawnHeading", getDir player];
|
||||||
|
if (_spawnHeading < 0) then { _spawnHeading = if (!isNull _garageObject) then { getDir _garageObject } else { getDir player }; };
|
||||||
|
|
||||||
|
private _spawnPosition = _nearestEntry getOrDefault ["spawnPosition", []];
|
||||||
|
if (_spawnPosition isEqualTo []) then { _spawnPosition = if (_anchorPosition isEqualTo []) then { player getPos [8, _spawnHeading] } else { _anchorPosition }; };
|
||||||
|
|
||||||
_context set ["name", _garageName];
|
_context set ["name", _garageName];
|
||||||
_context set ["anchorPosition", _anchorPosition];
|
_context set ["anchorPosition", _anchorPosition];
|
||||||
_context set ["sourceObject", _garageObject];
|
_context set ["sourceObject", _garageObject];
|
||||||
_context set ["garageType", _garageType];
|
|
||||||
_context set ["spawnHeading", _spawnHeading];
|
_context set ["spawnHeading", _spawnHeading];
|
||||||
_context set ["spawnPosition", _spawnPosition];
|
_context set ["spawnPosition", _spawnPosition];
|
||||||
_context set ["spawnLanes", _spawnLanes];
|
|
||||||
_self set ["lastContext", _context];
|
_self set ["lastContext", _context];
|
||||||
_context
|
_context
|
||||||
}],
|
}],
|
||||||
["getContext", compileFinal {
|
["getContext", compileFinal { _self call ["resolveContext", []] }],
|
||||||
params [["_preferredGarageObject", objNull, [objNull]]];
|
|
||||||
_self call ["resolveContext", [_preferredGarageObject]]
|
|
||||||
}],
|
|
||||||
["buildNearbyState", compileFinal {
|
["buildNearbyState", compileFinal {
|
||||||
private _context = _self call ["getContext", []];
|
private _context = _self call ["getContext", []];
|
||||||
private _anchorPosition = _context getOrDefault ["anchorPosition", []];
|
private _anchorPosition = _context getOrDefault ["anchorPosition", []];
|
||||||
|
|||||||
@ -22,33 +22,6 @@
|
|||||||
#pragma hemtt ignore_variables ["_self"]
|
#pragma hemtt ignore_variables ["_self"]
|
||||||
GVAR(GarageHelperServiceBaseClass) = compileFinal createHashMapFromArray [
|
GVAR(GarageHelperServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||||
["#type", "GarageHelperServiceBaseClass"],
|
["#type", "GarageHelperServiceBaseClass"],
|
||||||
["normalizeGarageCategory", compileFinal {
|
|
||||||
params [["_value", "", [""]]];
|
|
||||||
|
|
||||||
private _normalized = toLowerANSI (trim _value);
|
|
||||||
if (_normalized isEqualTo "") exitWith { "" };
|
|
||||||
if (_normalized in ["cars", "armor", "helis", "planes", "naval", "other"]) exitWith { _normalized };
|
|
||||||
""
|
|
||||||
}],
|
|
||||||
["inferGarageCategory", compileFinal {
|
|
||||||
params [["_value", "", [""]]];
|
|
||||||
|
|
||||||
private _normalized = toLowerANSI (trim _value);
|
|
||||||
if (_normalized isEqualTo "") exitWith { "" };
|
|
||||||
|
|
||||||
private _resolvedCategory = _self call ["normalizeGarageCategory", [_normalized]];
|
|
||||||
if (_resolvedCategory isNotEqualTo "") exitWith { _resolvedCategory };
|
|
||||||
|
|
||||||
switch (true) do {
|
|
||||||
case ((_normalized find "cars") >= 0): { "cars" };
|
|
||||||
case ((_normalized find "armor") >= 0): { "armor" };
|
|
||||||
case ((_normalized find "helis") >= 0): { "helis" };
|
|
||||||
case ((_normalized find "planes") >= 0): { "planes" };
|
|
||||||
case ((_normalized find "naval") >= 0): { "naval" };
|
|
||||||
case ((_normalized find "other") >= 0): { "other" };
|
|
||||||
default { "" };
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
["resolveCategory", compileFinal {
|
["resolveCategory", compileFinal {
|
||||||
params [["_className", "", [""]]];
|
params [["_className", "", [""]]];
|
||||||
|
|
||||||
@ -63,33 +36,6 @@ GVAR(GarageHelperServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
default { "other" };
|
default { "other" };
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
["resolveVGCategory", compileFinal {
|
|
||||||
params [["_className", "", [""]]];
|
|
||||||
|
|
||||||
if (_className isEqualTo "") exitWith { "other" };
|
|
||||||
|
|
||||||
switch (true) do {
|
|
||||||
case (_className isKindOf ["Car", configFile >> "CfgVehicles"]): { "cars" };
|
|
||||||
case (_className isKindOf ["Tank", configFile >> "CfgVehicles"]): { "armor" };
|
|
||||||
case (_className isKindOf ["Helicopter", configFile >> "CfgVehicles"]): { "helis" };
|
|
||||||
case (_className isKindOf ["Plane", configFile >> "CfgVehicles"]): { "planes" };
|
|
||||||
case (_className isKindOf ["Ship", configFile >> "CfgVehicles"]): { "naval" };
|
|
||||||
default { "other" };
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
["resolveGarageCategoryLabel", compileFinal {
|
|
||||||
params [["_category", "", [""]]];
|
|
||||||
|
|
||||||
switch (_category) do {
|
|
||||||
case "cars": { "cars" };
|
|
||||||
case "armor": { "armored vehicles" };
|
|
||||||
case "helis": { "helicopters" };
|
|
||||||
case "planes": { "planes" };
|
|
||||||
case "naval": { "naval vehicles" };
|
|
||||||
case "other": { "other vehicles" };
|
|
||||||
default { "this vehicle type" };
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
["resolveDisplayName", compileFinal {
|
["resolveDisplayName", compileFinal {
|
||||||
params [["_className", "", [""]]];
|
params [["_className", "", [""]]];
|
||||||
|
|
||||||
|
|||||||
@ -20,12 +20,6 @@
|
|||||||
* call forge_client_garage_fnc_openUI;
|
* call forge_client_garage_fnc_openUI;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
params [["_garageObject", objNull, [objNull]]];
|
|
||||||
|
|
||||||
if (!isNull _garageObject) then {
|
|
||||||
GVAR(GarageContextService) call ["setActiveGarageObject", [_garageObject]];
|
|
||||||
};
|
|
||||||
|
|
||||||
private _display = createDialog ["RscGarage", true];
|
private _display = createDialog ["RscGarage", true];
|
||||||
private _ctrl = _display displayCtrl 1006;
|
private _ctrl = _display displayCtrl 1006;
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* File: fnc_openVG.sqf
|
* File: fnc_openVG.sqf
|
||||||
* Author: IDSolutions
|
* Author: IDSolutions
|
||||||
* Date: 2025-12-16
|
* Date: 2025-12-16
|
||||||
* Last Update: 2026-04-22
|
* Last Update: 2026-01-30
|
||||||
* Public: No
|
* Public: No
|
||||||
*
|
*
|
||||||
* Description:
|
* Description:
|
||||||
@ -20,18 +20,11 @@
|
|||||||
* call forge_client_garage_fnc_openVG
|
* call forge_client_garage_fnc_openVG
|
||||||
*/
|
*/
|
||||||
|
|
||||||
params [["_garageObject", objNull, [objNull]]];
|
private _locations = (missionConfigFile >> "FORGE_CfgGarages" >> "locations") call BFUNC(getCfgData);
|
||||||
|
{
|
||||||
if (!isNull _garageObject) then {
|
FORGE_VehSpawnPos = (_x select 1) getPos [5, (_x select 2)];
|
||||||
GVAR(GarageContextService) call ["setActiveGarageObject", [_garageObject]];
|
true;
|
||||||
};
|
} count _locations;
|
||||||
|
|
||||||
private _context = GVAR(GarageContextService) call ["getContext", [_garageObject]];
|
|
||||||
private _spawnLane = GVAR(GarageContextService) call ["getSpawnLane", [_context getOrDefault ["garageType", ""], _context]];
|
|
||||||
|
|
||||||
FORGE_VehSpawnPos = _spawnLane getOrDefault ["spawnPosition", player getPos [8, getDir player]];
|
|
||||||
missionNamespace setVariable [QGVAR(activeVGContext), _context];
|
|
||||||
missionNamespace setVariable [QGVAR(activeVGNearbyVehicles), + (FORGE_VehSpawnPos nearEntities [["Car", "Tank", "Air", "Ship"], 15])];
|
|
||||||
|
|
||||||
BIS_fnc_garage_center = createVehicle ["Land_HelipadEmpty_F", FORGE_VehSpawnPos, [], 0, "NONE"];
|
BIS_fnc_garage_center = createVehicle ["Land_HelipadEmpty_F", FORGE_VehSpawnPos, [], 0, "NONE"];
|
||||||
BIS_fnc_garage_centerType = getText (configFile >> "CfgVehicles" >> "B_Quadbike_01_F" >> "model");
|
BIS_fnc_garage_centerType = getText (configFile >> "CfgVehicles" >> "B_Quadbike_01_F" >> "model");
|
||||||
@ -60,41 +53,16 @@ if !(GVAR(isPreLoaded)) then {
|
|||||||
}] call BFUNC(addScriptedEventHandler);
|
}] call BFUNC(addScriptedEventHandler);
|
||||||
|
|
||||||
[missionNamespace, "garageClosed", {
|
[missionNamespace, "garageClosed", {
|
||||||
private _nearbyVehicles = BIS_fnc_garage_center nearEntities [["Car", "Tank", "Air", "Ship"], 15];
|
private _nearestObjects = BIS_fnc_garage_center nearEntities [["Car","Tank","Air","Ship"], 15];
|
||||||
private _preExistingVehicles = missionNamespace getVariable [QGVAR(activeVGNearbyVehicles), []];
|
|
||||||
private _spawnedVehicles = _nearbyVehicles select { !(_x in _preExistingVehicles) };
|
|
||||||
|
|
||||||
if (_spawnedVehicles isNotEqualTo []) then {
|
|
||||||
private _spawnedVehiclePairs = _spawnedVehicles apply { [_x distance2D BIS_fnc_garage_center, _x] };
|
|
||||||
_spawnedVehiclePairs sort true;
|
|
||||||
|
|
||||||
private _obj = (_spawnedVehiclePairs select 0) param [1, objNull];
|
|
||||||
if (isNull _obj) exitWith {
|
|
||||||
missionNamespace setVariable [QGVAR(activeVGNearbyVehicles), nil];
|
|
||||||
missionNamespace setVariable [QGVAR(activeVGContext), nil];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
if (!isNil "_nearestObjects") then {
|
||||||
|
private _obj = _nearestObjects select 0;
|
||||||
private _veh = typeOf _obj;
|
private _veh = typeOf _obj;
|
||||||
private _textures = getObjectTextures _obj;
|
private _textures = getObjectTextures _obj;
|
||||||
private _animationNames = animationNames _obj;
|
private _animationNames = animationNames _obj;
|
||||||
private _context = missionNamespace getVariable [QGVAR(activeVGContext), createHashMap];
|
|
||||||
private _spawnCategory = GVAR(GarageHelperService) call ["resolveVGCategory", [_veh]];
|
|
||||||
private _spawnLane = GVAR(GarageContextService) call ["getExactSpawnLane", [_spawnCategory, _context]];
|
|
||||||
private _spawnLabel = GVAR(GarageHelperService) call ["resolveGarageCategoryLabel", [_spawnCategory]];
|
|
||||||
|
|
||||||
{ deleteVehicle _x } forEach _spawnedVehicles;
|
{ deleteVehicle _x } forEach _nearestObjects;
|
||||||
|
private _createVehicle = createVehicle [_veh, FORGE_VehSpawnPos, [], 0, "CAN_COLLIDE"];
|
||||||
if (_spawnLane isEqualTo createHashMap) exitWith {
|
|
||||||
missionNamespace setVariable [QGVAR(activeVGNearbyVehicles), nil];
|
|
||||||
missionNamespace setVariable [QGVAR(activeVGContext), nil];
|
|
||||||
private _params = ["warning", "Virtual Garage", format ["This garage does not support spawning %1.", _spawnLabel], 4000];
|
|
||||||
EGVAR(notifications,NotificationService) call ["create", _params];
|
|
||||||
};
|
|
||||||
|
|
||||||
private _spawnPosition = _spawnLane getOrDefault ["spawnPosition", FORGE_VehSpawnPos];
|
|
||||||
private _spawnHeading = _spawnLane getOrDefault ["spawnHeading", getDir _obj];
|
|
||||||
private _createVehicle = createVehicle [_veh, _spawnPosition, [], 0, "CAN_COLLIDE"];
|
|
||||||
_createVehicle setDir _spawnHeading;
|
|
||||||
|
|
||||||
if (_textures isNotEqualTo []) then {
|
if (_textures isNotEqualTo []) then {
|
||||||
private _count = 0;
|
private _count = 0;
|
||||||
@ -113,9 +81,6 @@ if !(GVAR(isPreLoaded)) then {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
missionNamespace setVariable [QGVAR(activeVGNearbyVehicles), nil];
|
|
||||||
missionNamespace setVariable [QGVAR(activeVGContext), nil];
|
|
||||||
}] call BFUNC(addScriptedEventHandler);
|
}] call BFUNC(addScriptedEventHandler);
|
||||||
|
|
||||||
GVAR(isPreLoaded) = true;
|
GVAR(isPreLoaded) = true;
|
||||||
|
|||||||
@ -23,18 +23,6 @@
|
|||||||
return bridge.send("garage::vehicle::store::request", payload);
|
return bridge.send("garage::vehicle::store::request", payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestRefuel(payload) {
|
|
||||||
return bridge.send("garage::vehicle::refuel::request", payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestRepair(payload) {
|
|
||||||
return bridge.send("garage::vehicle::repair::request", payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestRearm(payload) {
|
|
||||||
return bridge.send("garage::vehicle::rearm::request", payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
function notifyReady() {
|
function notifyReady() {
|
||||||
return bridge.ready({ loaded: true });
|
return bridge.ready({ loaded: true });
|
||||||
}
|
}
|
||||||
@ -87,34 +75,11 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
bridge.on("garage::service::success", (payloadData) => {
|
|
||||||
store.finishAction();
|
|
||||||
if (GarageApp.actions) {
|
|
||||||
GarageApp.actions.showNotice(
|
|
||||||
"success",
|
|
||||||
payloadData.message || "Service request sent.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
bridge.on("garage::service::failure", (payloadData) => {
|
|
||||||
store.finishAction();
|
|
||||||
if (GarageApp.actions) {
|
|
||||||
GarageApp.actions.showNotice(
|
|
||||||
"error",
|
|
||||||
payloadData.message || "Unable to service vehicle.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
GarageApp.bridge = {
|
GarageApp.bridge = {
|
||||||
notifyReady,
|
notifyReady,
|
||||||
receive: bridge.receive,
|
receive: bridge.receive,
|
||||||
requestClose,
|
requestClose,
|
||||||
requestRefresh,
|
requestRefresh,
|
||||||
requestRearm,
|
|
||||||
requestRefuel,
|
|
||||||
requestRepair,
|
|
||||||
requestRetrieve,
|
requestRetrieve,
|
||||||
requestStore,
|
requestStore,
|
||||||
sendEvent: bridge.send,
|
sendEvent: bridge.send,
|
||||||
|
|||||||
@ -343,17 +343,11 @@
|
|||||||
|
|
||||||
const isStored = currentSelection.entryKind === "stored";
|
const isStored = currentSelection.entryKind === "stored";
|
||||||
const pendingAction = String(state.pendingAction || "");
|
const pendingAction = String(state.pendingAction || "");
|
||||||
const isBusy = Boolean(pendingAction);
|
const isBusy =
|
||||||
|
pendingAction === "retrieve" || pendingAction === "store";
|
||||||
const canRetrieve = isStored && !session.spawnBlocked && !isBusy;
|
const canRetrieve = isStored && !session.spawnBlocked && !isBusy;
|
||||||
const canStore =
|
const canStore =
|
||||||
!isStored && currentSelection.isEmpty !== false && !isBusy;
|
!isStored && currentSelection.isEmpty !== false && !isBusy;
|
||||||
const canRefuel =
|
|
||||||
!isStored && Number(currentSelection.fuel || 0) < 0.999 && !isBusy;
|
|
||||||
const canRepair =
|
|
||||||
!isStored &&
|
|
||||||
Number(currentSelection.health || 0) < 0.999 &&
|
|
||||||
!isBusy;
|
|
||||||
const canRearm = !isStored && !isBusy;
|
|
||||||
|
|
||||||
return h(
|
return h(
|
||||||
"section",
|
"section",
|
||||||
@ -473,48 +467,6 @@
|
|||||||
type: "button",
|
type: "button",
|
||||||
className:
|
className:
|
||||||
"garage-btn garage-btn-secondary",
|
"garage-btn garage-btn-secondary",
|
||||||
disabled: !canRefuel,
|
|
||||||
onClick: () =>
|
|
||||||
actions.requestRefuelSelected(),
|
|
||||||
},
|
|
||||||
pendingAction === "refuel"
|
|
||||||
? "Refueling..."
|
|
||||||
: "Refuel",
|
|
||||||
),
|
|
||||||
h(
|
|
||||||
"button",
|
|
||||||
{
|
|
||||||
type: "button",
|
|
||||||
className:
|
|
||||||
"garage-btn garage-btn-secondary",
|
|
||||||
disabled: !canRepair,
|
|
||||||
onClick: () =>
|
|
||||||
actions.requestRepairSelected(),
|
|
||||||
},
|
|
||||||
pendingAction === "repair"
|
|
||||||
? "Repairing..."
|
|
||||||
: "Repair",
|
|
||||||
),
|
|
||||||
h(
|
|
||||||
"button",
|
|
||||||
{
|
|
||||||
type: "button",
|
|
||||||
className:
|
|
||||||
"garage-btn garage-btn-secondary",
|
|
||||||
disabled: !canRearm,
|
|
||||||
onClick: () =>
|
|
||||||
actions.requestRearmSelected(),
|
|
||||||
},
|
|
||||||
pendingAction === "rearm"
|
|
||||||
? "Rearming..."
|
|
||||||
: "Rearm",
|
|
||||||
),
|
|
||||||
h(
|
|
||||||
"button",
|
|
||||||
{
|
|
||||||
type: "button",
|
|
||||||
className:
|
|
||||||
"garage-btn garage-btn-secondary garage-action-refresh",
|
|
||||||
disabled: isBusy,
|
disabled: isBusy,
|
||||||
onClick: () => actions.refreshGarage(),
|
onClick: () => actions.refreshGarage(),
|
||||||
},
|
},
|
||||||
@ -527,10 +479,10 @@
|
|||||||
isStored
|
isStored
|
||||||
? session.spawnBlocked
|
? session.spawnBlocked
|
||||||
? "The garage spawn lane is currently blocked."
|
? "The garage spawn lane is currently blocked."
|
||||||
: "Retrieve this stored vehicle into the active spawn lane before refuel, rearm, or repair service."
|
: "Retrieve this stored vehicle into the active spawn lane."
|
||||||
: currentSelection.isEmpty === false
|
: currentSelection.isEmpty === false
|
||||||
? "Only empty nearby vehicles can be stored."
|
? "Only empty nearby vehicles can be stored."
|
||||||
: "Store this nearby vehicle or request organization-billed refuel, rearm, and repair service.",
|
: "Store this nearby vehicle back into persistent garage storage.",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
h(
|
h(
|
||||||
|
|||||||
@ -159,97 +159,6 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestRefuelSelected() {
|
|
||||||
const selectedEntry = getSelectedEntry();
|
|
||||||
if (!selectedEntry || selectedEntry.entryKind !== "nearby") {
|
|
||||||
showNotice("error", "Select a nearby vehicle to refuel.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Number(selectedEntry.fuel || 0) >= 0.999) {
|
|
||||||
showNotice("error", "Vehicle fuel tank is already full.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bridge = GarageApp.bridge;
|
|
||||||
if (!bridge || typeof bridge.requestRefuel !== "function") {
|
|
||||||
showNotice("error", "Garage refuel bridge is unavailable.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.startAction("refuel");
|
|
||||||
const sent = bridge.requestRefuel({
|
|
||||||
netId: selectedEntry.netId || "",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!sent) {
|
|
||||||
store.finishAction();
|
|
||||||
showNotice("error", "Garage refuel bridge is unavailable.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestRepairSelected() {
|
|
||||||
const selectedEntry = getSelectedEntry();
|
|
||||||
if (!selectedEntry || selectedEntry.entryKind !== "nearby") {
|
|
||||||
showNotice("error", "Select a nearby vehicle to repair.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Number(selectedEntry.health || 0) >= 0.999) {
|
|
||||||
showNotice("error", "Vehicle has no reported damage.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bridge = GarageApp.bridge;
|
|
||||||
if (!bridge || typeof bridge.requestRepair !== "function") {
|
|
||||||
showNotice("error", "Garage repair bridge is unavailable.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.startAction("repair");
|
|
||||||
const sent = bridge.requestRepair({
|
|
||||||
netId: selectedEntry.netId || "",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!sent) {
|
|
||||||
store.finishAction();
|
|
||||||
showNotice("error", "Garage repair bridge is unavailable.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestRearmSelected() {
|
|
||||||
const selectedEntry = getSelectedEntry();
|
|
||||||
if (!selectedEntry || selectedEntry.entryKind !== "nearby") {
|
|
||||||
showNotice("error", "Select a nearby vehicle to rearm.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bridge = GarageApp.bridge;
|
|
||||||
if (!bridge || typeof bridge.requestRearm !== "function") {
|
|
||||||
showNotice("error", "Garage rearm bridge is unavailable.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.startAction("rearm");
|
|
||||||
const sent = bridge.requestRearm({
|
|
||||||
netId: selectedEntry.netId || "",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!sent) {
|
|
||||||
store.finishAction();
|
|
||||||
showNotice("error", "Garage rearm bridge is unavailable.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
GarageApp.actions = {
|
GarageApp.actions = {
|
||||||
showNotice,
|
showNotice,
|
||||||
closeGarage,
|
closeGarage,
|
||||||
@ -259,9 +168,6 @@
|
|||||||
selectCategory,
|
selectCategory,
|
||||||
selectEntry,
|
selectEntry,
|
||||||
getSelectedEntry,
|
getSelectedEntry,
|
||||||
requestRearmSelected,
|
|
||||||
requestRefuelSelected,
|
|
||||||
requestRepairSelected,
|
|
||||||
requestRetrieveSelected,
|
requestRetrieveSelected,
|
||||||
requestStoreSelected,
|
requestStoreSelected,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -163,8 +163,8 @@ button:disabled {
|
|||||||
|
|
||||||
.garage-scroll-body {
|
.garage-scroll-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: clamp(10rem, 20vh, 16rem);
|
min-height: 20rem;
|
||||||
max-height: clamp(12rem, 25vh, 19rem);
|
max-height: 24rem;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
@ -172,7 +172,7 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.garage-detail-body {
|
.garage-detail-body {
|
||||||
padding-top: 0.75rem;
|
padding-top: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.garage-detail-grid {
|
.garage-detail-grid {
|
||||||
@ -195,7 +195,7 @@ button:disabled {
|
|||||||
|
|
||||||
.garage-detail-meta {
|
.garage-detail-meta {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
margin-bottom: 0.7rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.garage-summary-grid {
|
.garage-summary-grid {
|
||||||
@ -209,7 +209,7 @@ button:disabled {
|
|||||||
.garage-search-actions,
|
.garage-search-actions,
|
||||||
.garage-action-row {
|
.garage-action-row {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 0.55rem;
|
gap: 0.65rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.garage-category-grid {
|
.garage-category-grid {
|
||||||
@ -217,10 +217,6 @@ button:disabled {
|
|||||||
gap: 0.65rem;
|
gap: 0.65rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.garage-action-refresh {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.garage-footer-bar {
|
.garage-footer-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-top: 1px solid rgb(18 54 93 / 0.1);
|
border-top: 1px solid rgb(18 54 93 / 0.1);
|
||||||
@ -235,8 +231,8 @@ button:disabled {
|
|||||||
|
|
||||||
.garage-meter-stack {
|
.garage-meter-stack {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.55rem;
|
gap: 0.75rem;
|
||||||
margin-bottom: 0.7rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.garage-eyebrow,
|
.garage-eyebrow,
|
||||||
@ -451,8 +447,8 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.garage-btn {
|
.garage-btn {
|
||||||
min-height: 2.5rem;
|
min-height: 2.75rem;
|
||||||
padding: 0.62rem 1rem;
|
padding: 0.72rem 1rem;
|
||||||
border-radius: 0.8rem;
|
border-radius: 0.8rem;
|
||||||
border: 1px solid var(--garage-border-strong);
|
border: 1px solid var(--garage-border-strong);
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
|
|||||||
@ -1,27 +1,3 @@
|
|||||||
# Forge Client Locker
|
# forge_client_locker
|
||||||
|
|
||||||
## Overview
|
Description for this addon
|
||||||
The locker addon manages client repositories for personal locker state and
|
|
||||||
virtual arsenal unlock state. It also integrates with ACE Arsenal display
|
|
||||||
behavior.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
- `forge_client_main`
|
|
||||||
- ACE Arsenal
|
|
||||||
- server locker events from `forge_server_locker`
|
|
||||||
|
|
||||||
## Main Components
|
|
||||||
- `fnc_initRepository.sqf` manages locker state, container open/close behavior,
|
|
||||||
and server sync requests.
|
|
||||||
- `fnc_initVARepository.sqf` manages virtual arsenal state.
|
|
||||||
|
|
||||||
## Runtime Behavior
|
|
||||||
- Requests locker and virtual arsenal state after actor load.
|
|
||||||
- Syncs server responses into client repositories.
|
|
||||||
- Sends locker override data to the server when a managed locker container is
|
|
||||||
closed.
|
|
||||||
- Hides selected ACE Arsenal controls when the arsenal display opens.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
The client repository is display/input state. The server locker addon and
|
|
||||||
extension own saved locker and virtual arsenal data.
|
|
||||||
|
|||||||
@ -260,12 +260,8 @@ GVAR(LockerRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
private _pos = getPosASL _globalLocker;
|
private _pos = getPosASL _globalLocker;
|
||||||
private _vDir = vectorDir _globalLocker;
|
private _vDir = vectorDir _globalLocker;
|
||||||
private _vUp = vectorUp _globalLocker;
|
private _vUp = vectorUp _globalLocker;
|
||||||
private _lockerClass = typeOf _globalLocker;
|
|
||||||
if (_lockerClass isEqualTo "") then {
|
|
||||||
_lockerClass = "Box_NATO_Equip_F";
|
|
||||||
};
|
|
||||||
|
|
||||||
private _localLocker = createVehicleLocal [_lockerClass, [0, 0, 0]];
|
private _localLocker = createVehicleLocal ["Box_NATO_Equip_F", [0, 0, 0]];
|
||||||
_localLocker setPosASL _pos;
|
_localLocker setPosASL _pos;
|
||||||
_localLocker setVectorDirAndUp [_vDir, _vUp];
|
_localLocker setVectorDirAndUp [_vDir, _vUp];
|
||||||
_localLocker allowDamage false;
|
_localLocker allowDamage false;
|
||||||
|
|||||||
@ -1,18 +1,3 @@
|
|||||||
# Forge Client Main
|
# forge_client_main
|
||||||
|
|
||||||
## Overview
|
Main Addon for forge-client
|
||||||
The main addon provides shared mod metadata, macros, settings, and compile
|
|
||||||
infrastructure for Forge client addons.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
- `cba_main`
|
|
||||||
|
|
||||||
## Main Components
|
|
||||||
- `script_macros.hpp` defines shared function, RPC, path, variable, and compile
|
|
||||||
macros.
|
|
||||||
- `script_mod.hpp` and `script_version.hpp` define mod identity and version.
|
|
||||||
- `CfgSettings.hpp` contains client-side CBA settings.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
Feature logic should live in the owning addon. Main is the shared foundation for
|
|
||||||
configuration, macros, and mod-level metadata.
|
|
||||||
|
|||||||
@ -1,27 +1,3 @@
|
|||||||
# Forge Client Notifications
|
# forge_client_notifications
|
||||||
|
|
||||||
## Overview
|
Description for this addon
|
||||||
The notifications addon owns the client notification HUD, notification sound,
|
|
||||||
and local notification service used by other Forge client and server modules.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
- `forge_client_main`
|
|
||||||
|
|
||||||
## Main Components
|
|
||||||
- `fnc_initService.sqf` manages queued and visible notifications.
|
|
||||||
- `fnc_openUI.sqf` opens the notification HUD display.
|
|
||||||
- `fnc_handleUIEvents.sqf` handles browser/HUD events.
|
|
||||||
- `CfgSounds.hpp` defines the notification sound.
|
|
||||||
|
|
||||||
## Event Surface
|
|
||||||
`forge_client_notifications_recieveNotification` accepts:
|
|
||||||
|
|
||||||
```sqf
|
|
||||||
[_type, _title, _content, _duration]
|
|
||||||
```
|
|
||||||
|
|
||||||
The event plays the configured sound and adds the notification to the HUD.
|
|
||||||
|
|
||||||
## Runtime Notes
|
|
||||||
The HUD opens after the virtual arsenal repository is loaded. Other addons
|
|
||||||
should use this notification event instead of creating their own transient UI.
|
|
||||||
|
|||||||
@ -1,34 +1,85 @@
|
|||||||
# Forge Client Organization
|
# forge_client_org
|
||||||
|
|
||||||
## Overview
|
Player organization UI and client integration.
|
||||||
The organization addon provides the client organization portal UI and bridge for
|
|
||||||
organization hydrate, registration, membership, invitations, credit lines,
|
|
||||||
leave/disband actions, assets, fleet, and treasury display. Registration shows
|
|
||||||
the $50,000 personal funds requirement enforced by the server org addon.
|
|
||||||
|
|
||||||
## Dependencies
|
## UI Login Contract
|
||||||
- `forge_client_common`
|
|
||||||
- `forge_client_main`
|
|
||||||
- server organization events from `forge_server_org`
|
|
||||||
- notifications for user feedback
|
|
||||||
|
|
||||||
## Main Components
|
The web UI sends the following request through `A3API.SendAlert`:
|
||||||
- `fnc_initRepository.sqf` caches organization portal state.
|
|
||||||
- `fnc_initUIBridge.sqf` sends browser requests to server org RPCs and pushes
|
|
||||||
hydrate/sync events back to the browser.
|
|
||||||
- `fnc_handleUIEvents.sqf` handles `org::*` browser events.
|
|
||||||
- `fnc_openUI.sqf` opens `RscOrg`.
|
|
||||||
|
|
||||||
## Browser Events
|
```json
|
||||||
- `org::login::request`
|
{
|
||||||
- `org::create::request`
|
"event": "org::login::request",
|
||||||
- `org::disband::request`
|
"data": {
|
||||||
- `org::leave::request`
|
"email": "admin@spearnet.mil",
|
||||||
- `org::credit::request`
|
"password": "secret"
|
||||||
- `org::invite::request`
|
}
|
||||||
- `org::invite::accept`
|
}
|
||||||
- `org::invite::decline`
|
```
|
||||||
|
|
||||||
## Runtime Notes
|
On success, SQF should call the browser bridge with:
|
||||||
The client portal is a view/controller. Organization state, funds, reputation,
|
|
||||||
credit lines, assets, fleet, and membership are authoritative on the server.
|
```sqf
|
||||||
|
private _payload = createHashMapFromArray [
|
||||||
|
["session", createHashMapFromArray [
|
||||||
|
["actorName", name player],
|
||||||
|
["role", "Leader"]
|
||||||
|
]],
|
||||||
|
["portalData", createHashMapFromArray [
|
||||||
|
["org", createHashMapFromArray [
|
||||||
|
["name", "Black Rifle Company"],
|
||||||
|
["tag", "BRC-0160566824"],
|
||||||
|
["type", "Private Military Company"],
|
||||||
|
["status", "Operational"],
|
||||||
|
["headquarters", "Georgetown Command Annex"],
|
||||||
|
["owner", "Jacob Schmidt"]
|
||||||
|
]],
|
||||||
|
["funds", 482750],
|
||||||
|
["reputation", 72],
|
||||||
|
["members", [
|
||||||
|
createHashMapFromArray [["name", "Jacob Schmidt"]],
|
||||||
|
createHashMapFromArray [["name", "Mara Velez"]]
|
||||||
|
]],
|
||||||
|
["fleet", [
|
||||||
|
createHashMapFromArray [
|
||||||
|
["name", "UH-80 Ghost Hawk"],
|
||||||
|
["type", "helicopter"],
|
||||||
|
["status", "Ready"],
|
||||||
|
["damage", "16%"]
|
||||||
|
]
|
||||||
|
]],
|
||||||
|
["assets", [
|
||||||
|
createHashMapFromArray [
|
||||||
|
["name", "First Aid Kits"],
|
||||||
|
["type", "items"],
|
||||||
|
["quantity", "36"]
|
||||||
|
]
|
||||||
|
]],
|
||||||
|
["activity", []],
|
||||||
|
["roadmap", []]
|
||||||
|
]]
|
||||||
|
];
|
||||||
|
|
||||||
|
_control ctrlWebBrowserAction [
|
||||||
|
"ExecJS",
|
||||||
|
format ["OrgUIBridge.receiveLoginSuccess(%1)", toJSON _payload]
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
On failure:
|
||||||
|
|
||||||
|
```sqf
|
||||||
|
private _payload = createHashMapFromArray [
|
||||||
|
["message", "Invalid credentials."]
|
||||||
|
];
|
||||||
|
|
||||||
|
_control ctrlWebBrowserAction [
|
||||||
|
"ExecJS",
|
||||||
|
format ["OrgUIBridge.receiveLoginFailure(%1)", toJSON _payload]
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
Current implementation:
|
||||||
|
|
||||||
|
- `fnc_handleUIEvents.sqf` now handles `org::login::request`
|
||||||
|
- success hydrates the portal with `session` + `portalData`
|
||||||
|
- failure returns a single `message` string for inline UI feedback
|
||||||
|
|||||||
@ -50,12 +50,6 @@ if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initUIBridge); };
|
|||||||
GVAR(OrgUIBridge) call ["handleCreditLineResponse", [_payload]];
|
GVAR(OrgUIBridge) call ["handleCreditLineResponse", [_payload]];
|
||||||
}] call CFUNC(addEventHandler);
|
}] call CFUNC(addEventHandler);
|
||||||
|
|
||||||
[QGVAR(responseTreasuryAction), {
|
|
||||||
params [["_payload", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
GVAR(OrgUIBridge) call ["handleTreasuryResponse", [_payload]];
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseInviteOrg), {
|
[QGVAR(responseInviteOrg), {
|
||||||
params [["_payload", createHashMap, [createHashMap]]];
|
params [["_payload", createHashMap, [createHashMap]]];
|
||||||
|
|
||||||
|
|||||||
@ -46,12 +46,6 @@ switch (_event) do {
|
|||||||
case "org::credit::request": {
|
case "org::credit::request": {
|
||||||
GVAR(OrgUIBridge) call ["requestCreditLine", [_data]];
|
GVAR(OrgUIBridge) call ["requestCreditLine", [_data]];
|
||||||
};
|
};
|
||||||
case "org::payroll::request": {
|
|
||||||
GVAR(OrgUIBridge) call ["requestPayroll", [_data]];
|
|
||||||
};
|
|
||||||
case "org::transfer::request": {
|
|
||||||
GVAR(OrgUIBridge) call ["requestTransferFunds", [_data]];
|
|
||||||
};
|
|
||||||
case "org::invite::request": {
|
case "org::invite::request": {
|
||||||
GVAR(OrgUIBridge) call ["requestInvite", [_data]];
|
GVAR(OrgUIBridge) call ["requestInvite", [_data]];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -161,16 +161,6 @@ GVAR(OrgUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}],
|
}],
|
||||||
["handleTreasuryResponse", compileFinal {
|
|
||||||
params [["_payload", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _eventName = [
|
|
||||||
"org::treasury::failure",
|
|
||||||
"org::treasury::success"
|
|
||||||
] select (_payload getOrDefault ["success", false]);
|
|
||||||
|
|
||||||
_self call ["sendEvent", [_eventName, _payload]];
|
|
||||||
}],
|
|
||||||
["handleInviteResponse", compileFinal {
|
["handleInviteResponse", compileFinal {
|
||||||
params [["_payload", createHashMap, [createHashMap]]];
|
params [["_payload", createHashMap, [createHashMap]]];
|
||||||
|
|
||||||
@ -206,20 +196,6 @@ GVAR(OrgUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
|
|
||||||
[SRPC(org,requestAssignCreditLine), [getPlayerUID player, _memberUid, _memberName, _amount]] call CFUNC(serverEvent);
|
[SRPC(org,requestAssignCreditLine), [getPlayerUID player, _memberUid, _memberName, _amount]] call CFUNC(serverEvent);
|
||||||
}],
|
}],
|
||||||
["requestPayroll", compileFinal {
|
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _amount = _data getOrDefault ["amount", 0];
|
|
||||||
[SRPC(org,requestPayroll), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
|
||||||
}],
|
|
||||||
["requestTransferFunds", compileFinal {
|
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _memberUid = _data getOrDefault ["memberUid", ""];
|
|
||||||
private _memberName = _data getOrDefault ["memberName", ""];
|
|
||||||
private _amount = _data getOrDefault ["amount", 0];
|
|
||||||
[SRPC(org,requestTreasuryTransfer), [getPlayerUID player, _memberUid, _memberName, _amount]] call CFUNC(serverEvent);
|
|
||||||
}],
|
|
||||||
["requestInvite", compileFinal {
|
["requestInvite", compileFinal {
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
params [["_data", createHashMap, [createHashMap]]];
|
||||||
|
|
||||||
|
|||||||
@ -80,40 +80,6 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestPayroll(payload) {
|
|
||||||
const sent = sendEvent("org::payroll::request", payload);
|
|
||||||
if (sent) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const OrgPortal = window.OrgPortal;
|
|
||||||
if (OrgPortal && OrgPortal.actions) {
|
|
||||||
OrgPortal.actions.showTreasuryNotice(
|
|
||||||
"error",
|
|
||||||
"Arma payroll bridge is unavailable.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestTreasuryTransfer(payload) {
|
|
||||||
const sent = sendEvent("org::transfer::request", payload);
|
|
||||||
if (sent) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const OrgPortal = window.OrgPortal;
|
|
||||||
if (OrgPortal && OrgPortal.actions) {
|
|
||||||
OrgPortal.actions.showTreasuryNotice(
|
|
||||||
"error",
|
|
||||||
"Arma treasury transfer bridge is unavailable.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestInvitePlayer(payload) {
|
function requestInvitePlayer(payload) {
|
||||||
const sent = sendEvent("org::invite::request", payload);
|
const sent = sendEvent("org::invite::request", payload);
|
||||||
if (sent) {
|
if (sent) {
|
||||||
@ -213,30 +179,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
bridge.on("org::treasury::success", (payloadData) => {
|
|
||||||
const OrgPortal = window.OrgPortal;
|
|
||||||
if (OrgPortal && OrgPortal.store) {
|
|
||||||
OrgPortal.store.setModal(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OrgPortal && OrgPortal.actions) {
|
|
||||||
OrgPortal.actions.showTreasuryNotice(
|
|
||||||
"success",
|
|
||||||
payloadData.message || "Treasury action completed.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
bridge.on("org::treasury::failure", (payloadData) => {
|
|
||||||
const OrgPortal = window.OrgPortal;
|
|
||||||
if (OrgPortal && OrgPortal.actions) {
|
|
||||||
OrgPortal.actions.showTreasuryNotice(
|
|
||||||
"error",
|
|
||||||
payloadData.message || "Treasury action failed.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
bridge.on("org::invite::success", (payloadData) => {
|
bridge.on("org::invite::success", (payloadData) => {
|
||||||
const OrgPortal = window.OrgPortal;
|
const OrgPortal = window.OrgPortal;
|
||||||
if (OrgPortal && OrgPortal.store) {
|
if (OrgPortal && OrgPortal.store) {
|
||||||
@ -387,8 +329,6 @@
|
|||||||
requestDisbandOrg,
|
requestDisbandOrg,
|
||||||
requestLeaveOrg,
|
requestLeaveOrg,
|
||||||
requestCreditLine,
|
requestCreditLine,
|
||||||
requestPayroll,
|
|
||||||
requestTreasuryTransfer,
|
|
||||||
requestInvitePlayer,
|
requestInvitePlayer,
|
||||||
requestAcceptInvite,
|
requestAcceptInvite,
|
||||||
requestDeclineInvite,
|
requestDeclineInvite,
|
||||||
|
|||||||
@ -91,7 +91,7 @@
|
|||||||
...memberSelectProps,
|
...memberSelectProps,
|
||||||
},
|
},
|
||||||
...members.map((member) =>
|
...members.map((member) =>
|
||||||
h("option", { value: member.uid }, member.name),
|
h("option", { value: member.name }, member.name),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -203,24 +203,15 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bridge = window.RegistryApp
|
store.setFunds(funds - total);
|
||||||
? window.RegistryApp.bridge
|
this.showTreasuryNotice(
|
||||||
: null;
|
"success",
|
||||||
|
`Payroll sent to ${members.length} members for ${getters.formatCurrency(total)}.`,
|
||||||
if (!bridge || typeof bridge.requestPayroll !== "function") {
|
);
|
||||||
this.showTreasuryNotice(
|
return true;
|
||||||
"error",
|
|
||||||
"Payroll bridge is unavailable.",
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bridge.requestPayroll({
|
|
||||||
amount: amountPerMember,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sendFundsToMember(memberUid, amount) {
|
sendFundsToMember(memberName, amount) {
|
||||||
if (!getters.canManageTreasury()) {
|
if (!getters.canManageTreasury()) {
|
||||||
this.showTreasuryNotice(
|
this.showTreasuryNotice(
|
||||||
"error",
|
"error",
|
||||||
@ -231,7 +222,7 @@
|
|||||||
|
|
||||||
const funds = store.getFunds();
|
const funds = store.getFunds();
|
||||||
|
|
||||||
if (!memberUid) {
|
if (!memberName) {
|
||||||
this.showTreasuryNotice(
|
this.showTreasuryNotice(
|
||||||
"error",
|
"error",
|
||||||
"Select a member to receive funds.",
|
"Select a member to receive funds.",
|
||||||
@ -255,38 +246,12 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const member = store
|
store.setFunds(funds - amount);
|
||||||
.getMembers()
|
this.showTreasuryNotice(
|
||||||
.find((entry) => getters.getMemberUid(entry) === memberUid);
|
"success",
|
||||||
const memberName = member ? getters.getMemberName(member) : "";
|
`${getters.formatCurrency(amount)} sent to ${memberName}.`,
|
||||||
if (!memberName) {
|
);
|
||||||
this.showTreasuryNotice(
|
return true;
|
||||||
"error",
|
|
||||||
"Selected member was not found in the organization roster.",
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bridge = window.RegistryApp
|
|
||||||
? window.RegistryApp.bridge
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!bridge ||
|
|
||||||
typeof bridge.requestTreasuryTransfer !== "function"
|
|
||||||
) {
|
|
||||||
this.showTreasuryNotice(
|
|
||||||
"error",
|
|
||||||
"Treasury transfer bridge is unavailable.",
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bridge.requestTreasuryTransfer({
|
|
||||||
memberUid,
|
|
||||||
memberName,
|
|
||||||
amount,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
grantCreditLine(memberUid, amount) {
|
grantCreditLine(memberUid, amount) {
|
||||||
|
|||||||
@ -46,7 +46,7 @@ ${scopeSelector} .home-feedback {
|
|||||||
h(
|
h(
|
||||||
"p",
|
"p",
|
||||||
null,
|
null,
|
||||||
"Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Registration requires $50,000 in personal funds.",
|
"Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly.",
|
||||||
),
|
),
|
||||||
h(
|
h(
|
||||||
"button",
|
"button",
|
||||||
|
|||||||
@ -177,7 +177,7 @@ ${scopeSelector} .form-feedback.is-error {
|
|||||||
h(
|
h(
|
||||||
"p",
|
"p",
|
||||||
null,
|
null,
|
||||||
"Complete the form to add your organization to the Global Organization Registry. Registration requires at least $50,000 in personal funds.",
|
"Complete the form to add your organization to the Global Organization Registry.",
|
||||||
),
|
),
|
||||||
h(
|
h(
|
||||||
"ul",
|
"ul",
|
||||||
@ -258,11 +258,7 @@ ${scopeSelector} .form-feedback.is-error {
|
|||||||
h(
|
h(
|
||||||
"div",
|
"div",
|
||||||
{ className: "price-tag" },
|
{ className: "price-tag" },
|
||||||
h(
|
h("span", { className: "price-label" }, "Registration Fee"),
|
||||||
"span",
|
|
||||||
{ className: "price-label" },
|
|
||||||
"Required Registration Fee",
|
|
||||||
),
|
|
||||||
h("span", { className: "price-value" }, "$50,000"),
|
h("span", { className: "price-value" }, "$50,000"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
forge\forge_client\addons\phone
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
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));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
# Forge Client Phone
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
The phone addon provides the in-game phone UI for contacts, SMS messages, and
|
|
||||||
email. It keeps a local `PhoneRepository` facade for view state and sends all
|
|
||||||
authoritative operations to the server phone addon.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
- `forge_client_main`
|
|
||||||
- server phone events from `forge_server_phone`
|
|
||||||
- notifications for contact/message/email feedback
|
|
||||||
|
|
||||||
## Main Components
|
|
||||||
- `fnc_initRepository.sqf` initializes the local phone repository.
|
|
||||||
- `fnc_handleUIEvents.sqf` translates browser events into server phone RPCs.
|
|
||||||
- `fnc_openUI.sqf` opens `RscPhone`.
|
|
||||||
- `ui/_site` contains the browser phone UI source.
|
|
||||||
|
|
||||||
## Supported Operations
|
|
||||||
- initialize and sync phone state
|
|
||||||
- refresh contacts
|
|
||||||
- add/remove contacts by UID, phone number, or email
|
|
||||||
- send, read, and delete SMS messages
|
|
||||||
- send, read, and delete email
|
|
||||||
- push incoming message/email updates into the browser UI
|
|
||||||
|
|
||||||
## Runtime Notes
|
|
||||||
Phone data is owned by the server extension. Client state is only used to render
|
|
||||||
the phone UI and provide immediate feedback.
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
PREP(handleUIEvents);
|
|
||||||
PREP(initRepository);
|
|
||||||
PREP(openUI);
|
|
||||||
@ -1 +0,0 @@
|
|||||||
#include "script_component.hpp"
|
|
||||||
@ -1,340 +0,0 @@
|
|||||||
#include "script_component.hpp"
|
|
||||||
|
|
||||||
[{
|
|
||||||
GETVAR(player,FORGE_isLoaded,false)
|
|
||||||
}, {
|
|
||||||
[QGVAR(initPhone), []] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(waitUntilAndExecute);
|
|
||||||
|
|
||||||
if (isNil QGVAR(PhoneRepository)) then { [] call FUNC(initRepository); };
|
|
||||||
|
|
||||||
[QGVAR(initPhone), {
|
|
||||||
GVAR(PhoneRepository) call ["init", []];
|
|
||||||
|
|
||||||
["forge_server_phone_requestInitPhone", [getPlayerUID player, createHashMap]] call CFUNC(serverEvent);
|
|
||||||
["forge_server_phone_requestRefreshContacts", [getPlayerUID player, player]] call CFUNC(serverEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseSyncPhone), {
|
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
GVAR(PhoneRepository) call ["sync", [_data]];
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
// Contact Management Response Events
|
|
||||||
[QGVAR(responseAddContact), {
|
|
||||||
params [["_success", false, [false]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
EGVAR(notifications,NotificationService) call ["create", ["success", "Contact Added", "Contact added successfully", 3000]];
|
|
||||||
[QGVAR(refreshUI), []] call CFUNC(localEvent);
|
|
||||||
} else {
|
|
||||||
EGVAR(notifications,NotificationService) call ["create", ["danger", "Contact Error", "Failed to add contact", 4000]];
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseAddContactByPhone), {
|
|
||||||
params [["_success", false, [false]], ["_phoneNumber", "", [""]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
EGVAR(notifications,NotificationService) call ["create", ["success", "Contact Added", format ["Contact with phone %1 added successfully", _phoneNumber], 3000]];
|
|
||||||
[QGVAR(refreshUI), []] call CFUNC(localEvent);
|
|
||||||
} else {
|
|
||||||
EGVAR(notifications,NotificationService) call ["create", ["warning", "Contact Not Found", format ["Player with phone %1 not found", _phoneNumber], 4000]];
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseAddContactByEmail), {
|
|
||||||
params [["_success", false, [false]], ["_email", "", [""]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
EGVAR(notifications,NotificationService) call ["create", ["success", "Contact Added", format ["Contact with email %1 added successfully", _email], 3000]];
|
|
||||||
[QGVAR(refreshUI), []] call CFUNC(localEvent);
|
|
||||||
} else {
|
|
||||||
EGVAR(notifications,NotificationService) call ["create", ["warning", "Contact Not Found", format ["Player with email %1 not found", _email], 4000]];
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseRemoveContact), {
|
|
||||||
params [["_success", false, [false]], ["_contactUid", "", [""]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
EGVAR(notifications,NotificationService) call ["create", ["success", "Contact Removed", "Contact removed successfully", 3000]];
|
|
||||||
[QGVAR(refreshUI), []] call CFUNC(localEvent);
|
|
||||||
} else {
|
|
||||||
EGVAR(notifications,NotificationService) call ["create", ["danger", "Contact Error", "Failed to remove contact", 4000]];
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseRefreshContacts), {
|
|
||||||
params [["_contacts", [], [[]]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Contacts refreshed: %1 contacts", count _contacts];
|
|
||||||
|
|
||||||
[QGVAR(updateContacts), [_contacts]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseGetContacts), {
|
|
||||||
params [["_contactUids", [], [[]]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Got contact UIDs: %1", _contactUids];
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
// Messaging Response Events
|
|
||||||
[QGVAR(responseMessageSent), {
|
|
||||||
params [["_messageObj", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Message sent: %1", _messageObj];
|
|
||||||
|
|
||||||
[QGVAR(updateMessageSent), [_messageObj]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseMessageReceived), {
|
|
||||||
params [["_messageObj", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _fromUid = _messageObj get "from";
|
|
||||||
private _message = _messageObj get "message";
|
|
||||||
private _contacts = player getVariable ["FORGE_Contacts", []];
|
|
||||||
private _senderName = "Unknown";
|
|
||||||
|
|
||||||
{
|
|
||||||
if ((_x get "uid") isEqualTo _fromUid) exitWith {
|
|
||||||
_senderName = _x get "name";
|
|
||||||
};
|
|
||||||
} forEach _contacts;
|
|
||||||
|
|
||||||
EGVAR(notifications,NotificationService) call ["create", ["info", "New Message", format ["From %1", _senderName], 4000]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Message received from %1: %2", _fromUid, _message];
|
|
||||||
|
|
||||||
[QGVAR(updateMessageReceived), [_messageObj]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseSendMessage), {
|
|
||||||
params [["_success", false, [false]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
EGVAR(notifications,NotificationService) call ["create", ["success", "Message Sent", "Message sent successfully", 2000]];
|
|
||||||
} else {
|
|
||||||
EGVAR(notifications,NotificationService) call ["create", ["danger", "Message Failed", "Failed to send message", 4000]];
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseGetMessages), {
|
|
||||||
params [["_messages", [], [[]]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Got %1 messages", count _messages];
|
|
||||||
|
|
||||||
[QGVAR(updateMessages), [_messages]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseGetMessageThread), {
|
|
||||||
params [["_messages", [], [[]]], ["_otherUid", "", [""]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Got message thread with %1: %2 messages", _otherUid, count _messages];
|
|
||||||
|
|
||||||
[QGVAR(updateMessageThread), [_messages, _otherUid]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseMarkMessageRead), {
|
|
||||||
params [["_success", false, [false]], ["_messageId", "", [""]]];
|
|
||||||
|
|
||||||
if (_success) then { diag_log format ["[FORGE:Client:Phone] Message %1 marked as read", _messageId]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseMessageRead), {
|
|
||||||
params [["_messageId", "", [""]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Message %1 marked as read", _messageId];
|
|
||||||
|
|
||||||
[QGVAR(updateMessageRead), [_messageId]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseDeleteMessage), {
|
|
||||||
params [["_success", false, [false]], ["_messageId", "", [""]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Message %1 deleted", _messageId];
|
|
||||||
[QGVAR(updateMessageDeleted), [_messageId]] call CFUNC(localEvent);
|
|
||||||
} else {
|
|
||||||
EGVAR(notifications,NotificationService) call ["create", ["danger", "Message Delete Failed", "Failed to delete message", 4000]];
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
// Email Response Events
|
|
||||||
[QGVAR(responseEmailSent), {
|
|
||||||
params [["_emailObj", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Email sent: %1", _emailObj];
|
|
||||||
|
|
||||||
[QGVAR(updateEmailSent), [_emailObj]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseEmailReceived), {
|
|
||||||
params [["_emailObj", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _fromUid = _emailObj get "from";
|
|
||||||
private _subject = _emailObj get "subject";
|
|
||||||
private _contacts = player getVariable ["FORGE_Contacts", []];
|
|
||||||
private _senderName = "Unknown";
|
|
||||||
|
|
||||||
{
|
|
||||||
if ((_x get "uid") isEqualTo _fromUid) exitWith {
|
|
||||||
_senderName = _x get "name";
|
|
||||||
};
|
|
||||||
} forEach _contacts;
|
|
||||||
|
|
||||||
EGVAR(notifications,NotificationService) call ["create", ["info", "New Email", format ["From %1: %2", _senderName, _subject], 4000]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Email received from %1: %2", _fromUid, _subject];
|
|
||||||
|
|
||||||
[QGVAR(updateEmailReceived), [_emailObj]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseSendEmail), {
|
|
||||||
params [["_success", false, [false]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
EGVAR(notifications,NotificationService) call ["create", ["success", "Email Sent", "Email sent successfully", 2000]];
|
|
||||||
} else {
|
|
||||||
EGVAR(notifications,NotificationService) call ["create", ["danger", "Email Failed", "Failed to send email", 4000]];
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseGetEmails), {
|
|
||||||
params [["_emails", [], [[]]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Got %1 emails", count _emails];
|
|
||||||
|
|
||||||
[QGVAR(updateEmails), [_emails]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseMarkEmailRead), {
|
|
||||||
params [["_success", false, [false]], ["_emailId", "", [""]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Email %1 marked as read", _emailId];
|
|
||||||
[QGVAR(updateEmailRead), [_emailId]] call CFUNC(localEvent);
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseEmailRead), {
|
|
||||||
params [["_emailId", "", [""]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Email %1 marked as read", _emailId];
|
|
||||||
|
|
||||||
[QGVAR(updateEmailRead), [_emailId]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseDeleteEmail), {
|
|
||||||
params [["_success", false, [false]], ["_emailId", "", [""]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Email %1 deleted", _emailId];
|
|
||||||
[QGVAR(updateEmailDeleted), [_emailId]] call CFUNC(localEvent);
|
|
||||||
} else {
|
|
||||||
EGVAR(notifications,NotificationService) call ["create", ["danger", "Email Delete Failed", "Failed to delete email", 4000]];
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
// Cleanup Response Events
|
|
||||||
[QGVAR(responseRemovePhone), {
|
|
||||||
params [["_success", false, [false]]];
|
|
||||||
|
|
||||||
if (_success) then { diag_log "[FORGE:Client:Phone] Phone data removed successfully"; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
// UI Update Events (for internal use)
|
|
||||||
[QGVAR(refreshUI), {
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", "refreshContacts()"]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateContacts), {
|
|
||||||
params [["_contacts", [], [[]]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateContacts(%1)", (toJSON _contacts)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateMessageSent), {
|
|
||||||
params [["_messageObj", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateMessageSent(%1)", (toJSON _messageObj)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateMessageReceived), {
|
|
||||||
params [["_messageObj", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateMessageReceived(%1)", (toJSON _messageObj)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateMessages), {
|
|
||||||
params [["_messages", [], [[]]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateMessages(%1)", (toJSON _messages)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateMessageThread), {
|
|
||||||
params [["_messages", [], [[]]], ["_otherUid", "", [""]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateMessageThread(%1, %2)", (toJSON _messages), (toJSON _otherUid)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateMessageDeleted), {
|
|
||||||
params [["_messageId", "", [""]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateMessageDeleted(%1)", (toJSON _messageId)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateEmailSent), {
|
|
||||||
params [["_emailObj", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateEmailSent(%1)", (toJSON _emailObj)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateEmailReceived), {
|
|
||||||
params [["_emailObj", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateEmailReceived(%1)", (toJSON _emailObj)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateEmails), {
|
|
||||||
params [["_emails", [], [[]]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateEmails(%1)", (toJSON _emails)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateEmailRead), {
|
|
||||||
params [["_emailId", "", [""]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateEmailRead(%1)", (toJSON _emailId)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateEmailDeleted), {
|
|
||||||
params [["_emailId", "", [""]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateEmailDeleted(%1)", (toJSON _emailId)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
#include "script_component.hpp"
|
|
||||||
|
|
||||||
PREP_RECOMPILE_START;
|
|
||||||
#include "XEH_PREP.hpp"
|
|
||||||
PREP_RECOMPILE_END;
|
|
||||||
|
|
||||||
private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
|
|
||||||
|
|
||||||
#include "initKeybinds.inc.sqf"
|
|
||||||
@ -1 +0,0 @@
|
|||||||
#include "script_component.hpp"
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
#include "script_component.hpp"
|
|
||||||
#include "XEH_PREP.hpp"
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
#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\RscPhone.hpp"
|
|
||||||
@ -1,401 +0,0 @@
|
|||||||
#include "..\script_component.hpp"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Author: IDSolutions
|
|
||||||
* Handles UI events.
|
|
||||||
*
|
|
||||||
* Arguments:
|
|
||||||
* None
|
|
||||||
*
|
|
||||||
* Return Value:
|
|
||||||
* None
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* [] call forge_client_phone_fnc_handleUIEvents;
|
|
||||||
*
|
|
||||||
* Public: No
|
|
||||||
*/
|
|
||||||
|
|
||||||
params ["_control", "_isConfirmDialog", "_message"];
|
|
||||||
|
|
||||||
private _alert = fromJSON _message;
|
|
||||||
private _event = _alert get "event";
|
|
||||||
private _data = _alert get "data";
|
|
||||||
|
|
||||||
// diag_log format ["[FORGE:Client:Phone] Handling UI event: %1 with data: %2", _event, _data];
|
|
||||||
|
|
||||||
switch (_event) do {
|
|
||||||
case "phone::get::player": {
|
|
||||||
private _uid = getPlayerUID player;
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", format ["setPlayerUid(%1)", (toJSON _uid)]];
|
|
||||||
};
|
|
||||||
case "phone::get::theme": {
|
|
||||||
private _isDark = profileNamespace getVariable ["FORGE_Phone_isDark", true];
|
|
||||||
private _theme = ["light", "dark"] select (_isDark);
|
|
||||||
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", format ["setTheme(%1)", (toJSON _theme)]];
|
|
||||||
};
|
|
||||||
case "phone::get::contacts": {
|
|
||||||
private _contacts = player getVariable ["FORGE_Contacts", []];
|
|
||||||
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", format ["loadContacts(%1)", (toJSON _contacts)]];
|
|
||||||
["forge_server_phone_requestRefreshContacts", [getPlayerUID player, player]] call CFUNC(serverEvent);
|
|
||||||
};
|
|
||||||
case "phone::set::theme": {
|
|
||||||
private _isDark = _data get "isDark";
|
|
||||||
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_isDark", _isDark];
|
|
||||||
};
|
|
||||||
case "phone::add::contact": {
|
|
||||||
private _contactPhone = _data get "phone";
|
|
||||||
|
|
||||||
if (_contactPhone isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestAddContactByPhone", [getPlayerUID player, _contactPhone, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No phone number provided for contact addition";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::add::contact::by::phone": {
|
|
||||||
private _phoneNumber = _data get "phone";
|
|
||||||
|
|
||||||
if (_phoneNumber isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestAddContactByPhone", [getPlayerUID player, _phoneNumber, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No phone number provided";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::add::contact::by::email": {
|
|
||||||
private _email = _data get "email";
|
|
||||||
|
|
||||||
if (_email isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestAddContactByEmail", [getPlayerUID player, _email, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No email provided";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::remove::contact": {
|
|
||||||
private _contactUid = _data get "uid";
|
|
||||||
|
|
||||||
if (_contactUid isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestRemoveContact", [getPlayerUID player, _contactUid, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No contact UID provided for removal";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::refresh::contacts": {
|
|
||||||
["forge_server_phone_requestRefreshContacts", [getPlayerUID player, player]] call CFUNC(serverEvent);
|
|
||||||
};
|
|
||||||
case "phone::send::message": {
|
|
||||||
private _contactName = _data get "contactName";
|
|
||||||
private _messageData = _data get "message";
|
|
||||||
private _messageText = _messageData get "text";
|
|
||||||
private _toUid = _data get "toUid";
|
|
||||||
|
|
||||||
if (_toUid isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestSendMessage", [getPlayerUID player, _toUid, _messageText, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log format ["[FORGE:Client:Phone] No recipient UID provided for message to %1", _contactName];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::get::messages": {
|
|
||||||
["forge_server_phone_requestGetMessages", [getPlayerUID player, player]] call CFUNC(serverEvent);
|
|
||||||
};
|
|
||||||
case "phone::get::message::thread": {
|
|
||||||
private _otherUid = _data get "otherUid";
|
|
||||||
|
|
||||||
if (_otherUid isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestGetMessageThread", [getPlayerUID player, _otherUid, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No other UID provided for message thread";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::mark::message::read": {
|
|
||||||
private _messageId = _data get "messageId";
|
|
||||||
|
|
||||||
if (_messageId isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestMarkMessageRead", [getPlayerUID player, _messageId, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No message ID provided for mark read";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::delete::message": {
|
|
||||||
private _messageId = _data get "messageId";
|
|
||||||
|
|
||||||
if (_messageId isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestDeleteMessage", [getPlayerUID player, _messageId, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No message ID provided for delete";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::send::email": {
|
|
||||||
private _toUid = _data get "toUid";
|
|
||||||
private _subject = _data get "subject";
|
|
||||||
private _body = _data get "body";
|
|
||||||
if (_subject isEqualTo "") then { _subject = "No subject"; };
|
|
||||||
|
|
||||||
if (_toUid isNotEqualTo "" && _body isNotEqualTo "") then {
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Sending email to %1 subject length %2 body length %3", _toUid, count _subject, count _body];
|
|
||||||
["forge_server_phone_requestSendEmail", [getPlayerUID player, _toUid, _subject, _body, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] Missing required email parameters";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::get::emails": {
|
|
||||||
["forge_server_phone_requestGetEmails", [getPlayerUID player, player]] call CFUNC(serverEvent);
|
|
||||||
};
|
|
||||||
case "phone::mark::email::read": {
|
|
||||||
private _emailId = _data get "emailId";
|
|
||||||
|
|
||||||
if (_emailId isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestMarkEmailRead", [getPlayerUID player, _emailId, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No email ID provided for mark read";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::delete::email": {
|
|
||||||
private _emailId = _data get "emailId";
|
|
||||||
|
|
||||||
if (_emailId isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestDeleteEmail", [getPlayerUID player, _emailId, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No email ID provided for delete";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::get::notes": {
|
|
||||||
private _notes = GVAR(PhoneRepository) call ["getAllNotes", []];
|
|
||||||
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", format ["loadNotes(%1)", (toJSON _notes)]];
|
|
||||||
};
|
|
||||||
case "phone::save::note": {
|
|
||||||
private _success = GVAR(PhoneRepository) call ["addNote", [_data]];
|
|
||||||
_success
|
|
||||||
};
|
|
||||||
case "phone::delete::note": {
|
|
||||||
private _noteId = _data get "id";
|
|
||||||
|
|
||||||
private _success = GVAR(PhoneRepository) call ["deleteNote", [_noteId]];
|
|
||||||
_success
|
|
||||||
};
|
|
||||||
case "phone::get::events": {
|
|
||||||
private _events = profileNamespace getVariable ["FORGE_Phone_Events", []];
|
|
||||||
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", format ["loadCalendarEvents(%1)", (toJSON _events)]];
|
|
||||||
};
|
|
||||||
case "phone::save::event": {
|
|
||||||
private _eventId = _data get "id";
|
|
||||||
private _eventTitle = _data get "title";
|
|
||||||
|
|
||||||
private _events = profileNamespace getVariable ["FORGE_Phone_Events", []];
|
|
||||||
private _existingIndex = -1;
|
|
||||||
{
|
|
||||||
private _existingId = _x get "id";
|
|
||||||
if (_existingId isEqualTo _eventId) then {
|
|
||||||
_existingIndex = _forEachIndex;
|
|
||||||
};
|
|
||||||
} forEach _events;
|
|
||||||
|
|
||||||
if (_existingIndex >= 0) then {
|
|
||||||
_events set [_existingIndex, _data];
|
|
||||||
diag_log format ["[PHONE] Updated event: %1 [ID: %2]", _eventTitle, _eventId];
|
|
||||||
} else {
|
|
||||||
_events pushBack _data;
|
|
||||||
diag_log format ["[PHONE] Added new event: %1 [ID: %2]", _eventTitle, _eventId];
|
|
||||||
};
|
|
||||||
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_Events", _events];
|
|
||||||
diag_log format ["[PHONE] Saved events to profile. Total events: %1", count _events];
|
|
||||||
};
|
|
||||||
case "phone::delete::event": {
|
|
||||||
private _eventId = _data get "id";
|
|
||||||
private _events = profileNamespace getVariable ["FORGE_Phone_Events", []];
|
|
||||||
|
|
||||||
private _newEvents = [];
|
|
||||||
private _deleted = false;
|
|
||||||
{
|
|
||||||
private _existingId = _x get "id";
|
|
||||||
if (_existingId isEqualTo _eventId) then {
|
|
||||||
_deleted = true;
|
|
||||||
} else {
|
|
||||||
_newEvents pushBack _x;
|
|
||||||
};
|
|
||||||
} forEach _events;
|
|
||||||
|
|
||||||
if (_deleted) then {
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_Events", _newEvents];
|
|
||||||
diag_log format ["[PHONE] Deleted calendar event [ID: %1]. Remaining events: %2", _eventId, count _newEvents];
|
|
||||||
} else {
|
|
||||||
diag_log format ["[PHONE] Calendar event not found for deletion [ID: %1]", _eventId];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::get::clocks": {
|
|
||||||
private _worldClocks = profileNamespace getVariable ["FORGE_Phone_WorldClocks", []];
|
|
||||||
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", format ["loadWorldClocks(%1)", (toJSON _worldClocks)]];
|
|
||||||
};
|
|
||||||
case "phone::save::clock": {
|
|
||||||
private _clockId = _data get "id";
|
|
||||||
private _timezone = _data get "timezone";
|
|
||||||
private _city = _data get "city";
|
|
||||||
|
|
||||||
private _worldClocks = profileNamespace getVariable ["FORGE_Phone_WorldClocks", []];
|
|
||||||
private _clockExists = false;
|
|
||||||
{
|
|
||||||
private _existingId = _x get "id";
|
|
||||||
private _existingTimezone = _x get "timezone";
|
|
||||||
if (_existingId isEqualTo _clockId || _existingTimezone isEqualTo _timezone) then {
|
|
||||||
_clockExists = true;
|
|
||||||
};
|
|
||||||
} forEach _worldClocks;
|
|
||||||
|
|
||||||
if (!_clockExists) then {
|
|
||||||
_worldClocks pushBack _data;
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_WorldClocks", _worldClocks];
|
|
||||||
|
|
||||||
diag_log format ["[PHONE] Added world clock: %1 (%2) [ID: %3]. Total clocks: %4", _city, _timezone, _clockId, count _worldClocks];
|
|
||||||
} else {
|
|
||||||
diag_log format ["[PHONE] World clock already exists: %1 (%2) [ID: %3]. Skipping duplicate.", _city, _timezone, _clockId];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::delete::clock": {
|
|
||||||
private _clockId = _data get "id";
|
|
||||||
|
|
||||||
private _worldClocks = profileNamespace getVariable ["FORGE_Phone_WorldClocks", []];
|
|
||||||
private _newClocks = [];
|
|
||||||
private _deleted = false;
|
|
||||||
{
|
|
||||||
private _existingId = _x get "id";
|
|
||||||
if (_existingId isEqualTo _clockId) then {
|
|
||||||
_deleted = true;
|
|
||||||
} else {
|
|
||||||
_newClocks pushBack _x;
|
|
||||||
};
|
|
||||||
} forEach _worldClocks;
|
|
||||||
|
|
||||||
if (_deleted) then {
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_WorldClocks", _newClocks];
|
|
||||||
diag_log format ["[PHONE] Deleted world clock [ID: %1]. Remaining clocks: %2", _clockId, count _newClocks];
|
|
||||||
} else {
|
|
||||||
diag_log format ["[PHONE] World clock not found for deletion [ID: %1]", _clockId];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::get::alarms": {
|
|
||||||
private _alarms = profileNamespace getVariable ["FORGE_Phone_Alarms", []];
|
|
||||||
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", format ["loadAlarms(%1)", (toJSON _alarms)]];
|
|
||||||
};
|
|
||||||
case "phone::save::alarm": {
|
|
||||||
private _alarmId = _data get "id";
|
|
||||||
private _alarmTime = _data get "time";
|
|
||||||
private _alarmLabel = _data get "label";
|
|
||||||
|
|
||||||
private _alarms = profileNamespace getVariable ["FORGE_Phone_Alarms", []];
|
|
||||||
private _existingIndex = -1;
|
|
||||||
{
|
|
||||||
private _existingId = _x get "id";
|
|
||||||
if (_existingId isEqualTo _alarmId) then {
|
|
||||||
_existingIndex = _forEachIndex;
|
|
||||||
};
|
|
||||||
} forEach _alarms;
|
|
||||||
|
|
||||||
if (_existingIndex >= 0) then {
|
|
||||||
_alarms set [_existingIndex, _data];
|
|
||||||
diag_log format ["[PHONE] Updated alarm: %1 at %2 [ID: %3]", _alarmLabel, _alarmTime, _alarmId];
|
|
||||||
} else {
|
|
||||||
_alarms pushBack _data;
|
|
||||||
diag_log format ["[PHONE] Added new alarm: %1 at %2 [ID: %3]", _alarmLabel, _alarmTime, _alarmId];
|
|
||||||
};
|
|
||||||
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_Alarms", _alarms];
|
|
||||||
diag_log format ["[PHONE] Saved alarms to profile. Total alarms: %1", count _alarms];
|
|
||||||
};
|
|
||||||
case "phone::delete::alarm": {
|
|
||||||
private _alarmId = _data get "id";
|
|
||||||
|
|
||||||
private _alarms = profileNamespace getVariable ["FORGE_Phone_Alarms", []];
|
|
||||||
private _newAlarms = [];
|
|
||||||
private _deleted = false;
|
|
||||||
{
|
|
||||||
private _existingId = _x get "id";
|
|
||||||
if (_existingId isEqualTo _alarmId) then {
|
|
||||||
_deleted = true;
|
|
||||||
} else {
|
|
||||||
_newAlarms pushBack _x;
|
|
||||||
};
|
|
||||||
} forEach _alarms;
|
|
||||||
|
|
||||||
if (_deleted) then {
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_Alarms", _newAlarms];
|
|
||||||
diag_log format ["[PHONE] Deleted alarm [ID: %1]. Remaining alarms: %2", _alarmId, count _newAlarms];
|
|
||||||
} else {
|
|
||||||
diag_log format ["[PHONE] Alarm not found for deletion [ID: %1]", _alarmId];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::toggle::alarm": {
|
|
||||||
private _alarmId = _data get "id";
|
|
||||||
|
|
||||||
private _alarms = profileNamespace getVariable ["FORGE_Phone_Alarms", []];
|
|
||||||
{
|
|
||||||
private _existingId = _x get "id";
|
|
||||||
if (_existingId isEqualTo _alarmId) then {
|
|
||||||
private _currentEnabled = _x get "enabled";
|
|
||||||
_x set ["enabled", !_currentEnabled];
|
|
||||||
diag_log format ["[PHONE] Toggled alarm [ID: %1] to %2", _alarmId, !_currentEnabled];
|
|
||||||
};
|
|
||||||
} forEach _alarms;
|
|
||||||
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_Alarms", _alarms];
|
|
||||||
};
|
|
||||||
case "phone::bank::refresh": {
|
|
||||||
["forge_server_bank_requestHydrateBank", [getPlayerUID player, "bank", false]] call CFUNC(serverEvent);
|
|
||||||
};
|
|
||||||
case "phone::bank::transfer::request": {
|
|
||||||
private _amount = floor (_data getOrDefault ["amount", 0]);
|
|
||||||
private _target = _data getOrDefault ["target", ""];
|
|
||||||
private _from = toLowerANSI (_data getOrDefault ["from", "bank"]);
|
|
||||||
|
|
||||||
if (_target isNotEqualTo "" && { _amount > 0 }) then {
|
|
||||||
["forge_server_bank_requestTransfer", [getPlayerUID player, _target, _from, _amount]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
private _display = uiNamespace getVariable ["RscPhone", displayNull];
|
|
||||||
if !(isNull _display) then {
|
|
||||||
private _control = _display displayCtrl 1001;
|
|
||||||
if !(isNull _control) then {
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", "window.showMobileBankNotice && window.showMobileBankNotice('error', 'Choose a recipient and valid amount.')"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::bank::depositEarnings::request": {
|
|
||||||
private _amount = floor (_data getOrDefault ["amount", 0]);
|
|
||||||
|
|
||||||
if (_amount > 0) then {
|
|
||||||
["forge_server_bank_requestDepositEarnings", [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
private _display = uiNamespace getVariable ["RscPhone", displayNull];
|
|
||||||
if !(isNull _display) then {
|
|
||||||
private _control = _display displayCtrl 1001;
|
|
||||||
if !(isNull _control) then {
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", "window.showMobileBankNotice && window.showMobileBankNotice('error', 'Enter a valid earnings amount.')"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::bank::repayCreditLine::request": {
|
|
||||||
private _amount = floor (_data getOrDefault ["amount", 0]);
|
|
||||||
|
|
||||||
if (_amount > 0) then {
|
|
||||||
["forge_server_bank_requestRepayCreditLine", [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
private _display = uiNamespace getVariable ["RscPhone", displayNull];
|
|
||||||
if !(isNull _display) then {
|
|
||||||
private _control = _display displayCtrl 1001;
|
|
||||||
if !(isNull _control) then {
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", "window.showMobileBankNotice && window.showMobileBankNotice('error', 'Enter a valid payment amount.')"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
default { hint format ["Unhandled phone event: %1", _event]; };
|
|
||||||
};
|
|
||||||
|
|
||||||
true;
|
|
||||||
@ -1,256 +0,0 @@
|
|||||||
#include "..\script_component.hpp"
|
|
||||||
|
|
||||||
#pragma hemtt ignore_variables ["_self"]
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Author: IDSolutions
|
|
||||||
* Initialize phone repository
|
|
||||||
*
|
|
||||||
* Arguments:
|
|
||||||
* N/A
|
|
||||||
*
|
|
||||||
* Return Value:
|
|
||||||
* Phone repository object
|
|
||||||
*
|
|
||||||
* Examples:
|
|
||||||
* [] call forge_client_phone_fnc_initRepository
|
|
||||||
*
|
|
||||||
* Public: Yes
|
|
||||||
*/
|
|
||||||
|
|
||||||
GVAR(PhoneRepository) = createHashMapObject [[
|
|
||||||
["#type", "IPhoneRepository"],
|
|
||||||
["#create", {
|
|
||||||
_self set ["uid", getPlayerUID player];
|
|
||||||
_self set ["notes", createHashMap];
|
|
||||||
_self set ["events", []];
|
|
||||||
_self set ["settings", createHashMap];
|
|
||||||
_self set ["isLoaded", false];
|
|
||||||
_self set ["lastSave", time];
|
|
||||||
|
|
||||||
private _settings = createHashMap;
|
|
||||||
_settings set ["theme", "light"];
|
|
||||||
_settings set ["notifications", true];
|
|
||||||
_settings set ["sound", true];
|
|
||||||
_settings set ["vibration", true];
|
|
||||||
_self set ["settings", _settings];
|
|
||||||
}],
|
|
||||||
["init", {
|
|
||||||
private _savedNotes = profileNamespace getVariable ["FORGE_Phone_Notes", createHashMap];
|
|
||||||
private _savedEvents = profileNamespace getVariable ["FORGE_Phone_Events", []];
|
|
||||||
private _savedSettings = profileNamespace getVariable ["FORGE_Phone_Settings", createHashMap];
|
|
||||||
|
|
||||||
_self set ["notes", _savedNotes];
|
|
||||||
_self set ["events", _savedEvents];
|
|
||||||
|
|
||||||
private _defaultSettings = _self get "settings";
|
|
||||||
{
|
|
||||||
_defaultSettings set [_x, _y];
|
|
||||||
} forEach _savedSettings;
|
|
||||||
|
|
||||||
_self set ["settings", _defaultSettings];
|
|
||||||
_self set ["isLoaded", true];
|
|
||||||
|
|
||||||
systemChat format ["Phone loaded for %1", name player];
|
|
||||||
diag_log "[FORGE:Client:Phone] Phone Repository Initialized!";
|
|
||||||
}],
|
|
||||||
["_padString", {
|
|
||||||
params [["_number", 0, [0]], ["_length", 0, [0]]];
|
|
||||||
|
|
||||||
private _str = str _number;
|
|
||||||
while { (_str select [(_length - 1), 1]) == "" } do { _str = "0" + _str };
|
|
||||||
_str
|
|
||||||
}],
|
|
||||||
["save", {
|
|
||||||
params [["_sync", false, [false]]];
|
|
||||||
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_Notes", _self get "notes"];
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_Events", _self get "events"];
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_Settings", _self get "settings"];
|
|
||||||
|
|
||||||
if (_sync) then { saveProfileNamespace; };
|
|
||||||
_self set ["lastSave", time];
|
|
||||||
}],
|
|
||||||
["sync", {
|
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
|
||||||
if (_data isEqualTo createHashMap) exitWith { diag_log "[FORGE:Client:Phone] Empty data received for sync, skipping."; };
|
|
||||||
}],
|
|
||||||
["get", {
|
|
||||||
params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
|
|
||||||
|
|
||||||
private _settings = _self get "settings";
|
|
||||||
_settings getOrDefault [_key, _default];
|
|
||||||
}],
|
|
||||||
["addNote", {
|
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
|
||||||
if (_data isEqualTo createHashMap) exitWith { false };
|
|
||||||
|
|
||||||
private _noteId = _data get "id";
|
|
||||||
private _notes = _self get "notes";
|
|
||||||
_notes set [_noteId, _data];
|
|
||||||
_self call ["save", [true]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Added note [ID: %1]", _noteId];
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["updateNote", {
|
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _noteId = _data get "id";
|
|
||||||
if (isNil "_noteId" || _noteId == "") exitWith { false };
|
|
||||||
|
|
||||||
private _notes = _self get "notes";
|
|
||||||
if !(_noteId in _notes) exitWith { false };
|
|
||||||
|
|
||||||
_notes set [_noteId, _data];
|
|
||||||
_self set ["notes", _notes];
|
|
||||||
_self call ["save", [true]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Updated note [ID: %1]", _noteId];
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["deleteNote", {
|
|
||||||
params [["_noteId", "", [""]]];
|
|
||||||
if (_noteId == "") exitWith { false };
|
|
||||||
|
|
||||||
private _notes = _self get "notes";
|
|
||||||
if !(_noteId in _notes) exitWith { false };
|
|
||||||
|
|
||||||
_notes deleteAt _noteId;
|
|
||||||
_self set ["notes", _notes];
|
|
||||||
_self call ["save", [true]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Deleted note [ID: %1]", _noteId];
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["getNote", {
|
|
||||||
params [["_noteId", "", [""]], ["_default", nil]];
|
|
||||||
|
|
||||||
private _notes = _self get "notes";
|
|
||||||
_notes getOrDefault [_noteId, _default];
|
|
||||||
}],
|
|
||||||
["getAllNotes", {
|
|
||||||
private _notes = _self get "notes";
|
|
||||||
private _notesArray = [];
|
|
||||||
|
|
||||||
{
|
|
||||||
_notesArray pushBack _y;
|
|
||||||
} forEach _notes;
|
|
||||||
|
|
||||||
_notesArray
|
|
||||||
}],
|
|
||||||
["setSetting", {
|
|
||||||
params [["_key", "", [""]], ["_value", nil]];
|
|
||||||
if (_key == "") exitWith { false };
|
|
||||||
|
|
||||||
private _settings = _self get "settings";
|
|
||||||
_settings set [_key, _value];
|
|
||||||
_self set ["settings", _settings];
|
|
||||||
_self call ["save", [true]];
|
|
||||||
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["getSetting", {
|
|
||||||
params [["_key", "", [""]], ["_default", nil]];
|
|
||||||
|
|
||||||
private _settings = _self get "settings";
|
|
||||||
_settings getOrDefault [_key, _default];
|
|
||||||
}],
|
|
||||||
["getAllSettings", {
|
|
||||||
_self get "settings";
|
|
||||||
}],
|
|
||||||
["addEvent", {
|
|
||||||
params [["_eventData", createHashMap, [createHashMap]]];
|
|
||||||
if (_eventData isEqualTo createHashMap) exitWith { false };
|
|
||||||
|
|
||||||
private _eventId = _eventData get "id";
|
|
||||||
if (isNil "_eventId" || _eventId == "") exitWith { false };
|
|
||||||
|
|
||||||
private _events = _self get "events";
|
|
||||||
private _existingIndex = _events findIf { (_x get "id") isEqualTo _eventId };
|
|
||||||
|
|
||||||
if (_existingIndex >= 0) then {
|
|
||||||
_events set [_existingIndex, _eventData];
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Updated event [ID: %1]", _eventId];
|
|
||||||
} else {
|
|
||||||
_events pushBack _eventData;
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Added event [ID: %1]", _eventId];
|
|
||||||
};
|
|
||||||
|
|
||||||
_self set ["events", _events];
|
|
||||||
_self call ["save", [true]];
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["updateEvent", {
|
|
||||||
params [["_eventData", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _eventId = _eventData get "id";
|
|
||||||
if (isNil "_eventId" || _eventId == "") exitWith { false };
|
|
||||||
|
|
||||||
private _events = _self get "events";
|
|
||||||
private _existingIndex = _events findIf { (_x get "id") isEqualTo _eventId };
|
|
||||||
if (_existingIndex < 0) exitWith { false };
|
|
||||||
|
|
||||||
_events set [_existingIndex, _eventData];
|
|
||||||
_self set ["events", _events];
|
|
||||||
_self call ["save", [true]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Updated event [ID: %1]", _eventId];
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["deleteEvent", {
|
|
||||||
params [["_eventId", "", [""]]];
|
|
||||||
if (_eventId == "") exitWith { false };
|
|
||||||
|
|
||||||
private _events = _self get "events";
|
|
||||||
private _existingIndex = _events findIf { (_x get "id") isEqualTo _eventId };
|
|
||||||
if (_existingIndex < 0) exitWith { false };
|
|
||||||
|
|
||||||
_events deleteAt _existingIndex;
|
|
||||||
_self set ["events", _events];
|
|
||||||
_self call ["save", [true]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Deleted event [ID: %1]", _eventId];
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["getEvent", {
|
|
||||||
params [["_eventId", "", [""]], ["_default", nil]];
|
|
||||||
|
|
||||||
private _events = _self get "events";
|
|
||||||
private _event = _events select { (_x get "id") isEqualTo _eventId };
|
|
||||||
if (_event isNotEqualTo []) then { _event select 0 } else { _default };
|
|
||||||
}],
|
|
||||||
["getAllEvents", {
|
|
||||||
_self get "events";
|
|
||||||
}],
|
|
||||||
["getEventsByDate", {
|
|
||||||
params [["_date", "", [""]]];
|
|
||||||
|
|
||||||
private _events = _self get "events";
|
|
||||||
_events select {
|
|
||||||
private _eventStartTime = _x get "startTime";
|
|
||||||
if (isNil "_eventStartTime") then { false } else {
|
|
||||||
private _eventDate = (_eventStartTime splitString "T") select 0;
|
|
||||||
_eventDate isEqualTo _date
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
["clearAllEvents", {
|
|
||||||
_self set ["events", []];
|
|
||||||
_self call ["save", [true]];
|
|
||||||
diag_log "[FORGE:Client:Phone] Cleared all events";
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["getEventsForToday", {
|
|
||||||
private _currentTime = systemTimeUTC;
|
|
||||||
private _todayDate = format ["%1-%2-%3",
|
|
||||||
_currentTime select 0,
|
|
||||||
_self call ["_padString", [(_currentTime select 1), 2]],
|
|
||||||
_self call ["_padString", [(_currentTime select 2), 2]]
|
|
||||||
];
|
|
||||||
|
|
||||||
_self call ["getEventsByDate", [_todayDate]]
|
|
||||||
}]
|
|
||||||
]];
|
|
||||||
|
|
||||||
GVAR(PhoneRepository)
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
#include "..\script_component.hpp"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Author: IDSolutions
|
|
||||||
* Open phone interface.
|
|
||||||
*
|
|
||||||
* Arguments:
|
|
||||||
* None
|
|
||||||
*
|
|
||||||
* Return Value:
|
|
||||||
* None
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* [] call forge_client_phone_fnc_openUI;
|
|
||||||
*
|
|
||||||
* Public: No
|
|
||||||
*/
|
|
||||||
|
|
||||||
private _display = (findDisplay 46) createDisplay "RscPhone";
|
|
||||||
private _ctrl = (_display displayCtrl 1001);
|
|
||||||
|
|
||||||
_ctrl ctrlAddEventHandler ["JSDialog", {
|
|
||||||
params ["_control", "_isConfirmDialog", "_message"];
|
|
||||||
|
|
||||||
[_control, _isConfirmDialog, _message] call FUNC(handleUIEvents);
|
|
||||||
}];
|
|
||||||
|
|
||||||
_ctrl ctrlWebBrowserAction ["LoadFile", QUOTE(PATHTOF(ui\_site\index.html))];
|
|
||||||
// _ctrl ctrlWebBrowserAction ["OpenDevConsole"];
|
|
||||||
|
|
||||||
true;
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
#include "\forge\forge_client\addons\main\data\hpp\defineDIKCodes.hpp"
|
|
||||||
|
|
||||||
[
|
|
||||||
_category, QGVAR(ForgePhone),
|
|
||||||
[LSTRING(phone), LSTRING(phoneTooltip)], {
|
|
||||||
[] call FUNC(openUI)
|
|
||||||
}, {}, [DIK_P, [false, false, false]]
|
|
||||||
] call CFUNC(addKeybind);
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
#define COMPONENT phone
|
|
||||||
#define COMPONENT_BEAUTIFIED Phone
|
|
||||||
#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"
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project name="FFE">
|
|
||||||
<Package name="Phone">
|
|
||||||
<Key ID="STR_forge_client_phone_displayName">
|
|
||||||
<English>Phone</English>
|
|
||||||
</Key>
|
|
||||||
<Key ID="STR_forge_client_phone_phone">
|
|
||||||
<English>Phone</English>
|
|
||||||
</Key>
|
|
||||||
<Key ID="STR_forge_client_phone_phoneTooltip">
|
|
||||||
<English>Open your phone</English>
|
|
||||||
</Key>
|
|
||||||
</Package>
|
|
||||||
</Project>
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
// Control types
|
|
||||||
#define CT_STATIC 0
|
|
||||||
#define CT_BUTTON 1
|
|
||||||
#define CT_EDIT 2
|
|
||||||
#define CT_SLIDER 3
|
|
||||||
#define CT_COMBO 4
|
|
||||||
#define CT_LISTBOX 5
|
|
||||||
#define CT_TOOLBOX 6
|
|
||||||
#define CT_CHECKBOXES 7
|
|
||||||
#define CT_PROGRESS 8
|
|
||||||
#define CT_HTML 9
|
|
||||||
#define CT_STATIC_SKEW 10
|
|
||||||
#define CT_ACTIVETEXT 11
|
|
||||||
#define CT_TREE 12
|
|
||||||
#define CT_STRUCTURED_TEXT 13
|
|
||||||
#define CT_CONTEXT_MENU 14
|
|
||||||
#define CT_CONTROLS_GROUP 15
|
|
||||||
#define CT_SHORTCUTBUTTON 16
|
|
||||||
#define CT_HITZONES 17
|
|
||||||
#define CT_XKEYDESC 40
|
|
||||||
#define CT_XBUTTON 41
|
|
||||||
#define CT_XLISTBOX 42
|
|
||||||
#define CT_XSLIDER 43
|
|
||||||
#define CT_XCOMBO 44
|
|
||||||
#define CT_ANIMATED_TEXTURE 45
|
|
||||||
#define CT_OBJECT 80
|
|
||||||
#define CT_OBJECT_ZOOM 81
|
|
||||||
#define CT_OBJECT_CONTAINER 82
|
|
||||||
#define CT_OBJECT_CONT_ANIM 83
|
|
||||||
#define CT_LINEBREAK 98
|
|
||||||
#define CT_USER 99
|
|
||||||
#define CT_MAP 100
|
|
||||||
#define CT_MAP_MAIN 101
|
|
||||||
#define CT_LISTNBOX 102
|
|
||||||
#define CT_ITEMSLOT 103
|
|
||||||
#define CT_CHECKBOX 77
|
|
||||||
|
|
||||||
// Static styles
|
|
||||||
#define ST_POS 0x0F
|
|
||||||
#define ST_HPOS 0x03
|
|
||||||
#define ST_VPOS 0x0C
|
|
||||||
#define ST_LEFT 0x00
|
|
||||||
#define ST_RIGHT 0x01
|
|
||||||
#define ST_CENTER 0x02
|
|
||||||
#define ST_DOWN 0x04
|
|
||||||
#define ST_UP 0x08
|
|
||||||
#define ST_VCENTER 0x0C
|
|
||||||
|
|
||||||
#define ST_TYPE 0xF0
|
|
||||||
#define ST_SINGLE 0x00
|
|
||||||
#define ST_MULTI 0x10
|
|
||||||
#define ST_TITLE_BAR 0x20
|
|
||||||
#define ST_PICTURE 0x30
|
|
||||||
#define ST_FRAME 0x40
|
|
||||||
#define ST_BACKGROUND 0x50
|
|
||||||
#define ST_GROUP_BOX 0x60
|
|
||||||
#define ST_GROUP_BOX2 0x70
|
|
||||||
#define ST_HUD_BACKGROUND 0x80
|
|
||||||
#define ST_TILE_PICTURE 0x90
|
|
||||||
#define ST_WITH_RECT 0xA0
|
|
||||||
#define ST_LINE 0xB0
|
|
||||||
#define ST_UPPERCASE 0xC0
|
|
||||||
#define ST_LOWERCASE 0xD0
|
|
||||||
|
|
||||||
#define ST_SHADOW 0x100
|
|
||||||
#define ST_NO_RECT 0x200
|
|
||||||
#define ST_KEEP_ASPECT_RATIO 0x800
|
|
||||||
|
|
||||||
// Slider styles
|
|
||||||
#define SL_DIR 0x400
|
|
||||||
#define SL_VERT 0
|
|
||||||
#define SL_HORZ 0x400
|
|
||||||
|
|
||||||
#define SL_TEXTURES 0x10
|
|
||||||
|
|
||||||
// progress bar
|
|
||||||
#define ST_VERTICAL 0x01
|
|
||||||
#define ST_HORIZONTAL 0
|
|
||||||
|
|
||||||
// Listbox styles
|
|
||||||
#define LB_TEXTURES 0x10
|
|
||||||
#define LB_MULTI 0x20
|
|
||||||
|
|
||||||
// Tree styles
|
|
||||||
#define TR_SHOWROOT 1
|
|
||||||
#define TR_AUTOCOLLAPSE 2
|
|
||||||
|
|
||||||
// Default text sizes
|
|
||||||
#define GUI_TEXT_SIZE_SMALL (GUI_GRID_H * 0.8)
|
|
||||||
#define GUI_TEXT_SIZE_MEDIUM (GUI_GRID_H * 1)
|
|
||||||
#define GUI_TEXT_SIZE_LARGE (GUI_GRID_H * 1.2)
|
|
||||||
|
|
||||||
// Pixel grid
|
|
||||||
#define pixelScale 0.50
|
|
||||||
#define GRID_W (pixelW * pixelGrid * pixelScale)
|
|
||||||
#define GRID_H (pixelH * pixelGrid * pixelScale)
|
|
||||||
|
|
||||||
class RscText;
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
class RscPhone {
|
|
||||||
idd = 1000;
|
|
||||||
movingEnable = 1;
|
|
||||||
enableSimulation = 1;
|
|
||||||
duration = 1e011;
|
|
||||||
fadeIn = 0;
|
|
||||||
fadeOut = 0;
|
|
||||||
onLoad = "uiNamespace setVariable ['RscPhone', _this select 0]";
|
|
||||||
|
|
||||||
class controlsBackground {};
|
|
||||||
class controls {
|
|
||||||
class Background: RscText {
|
|
||||||
type = 106;
|
|
||||||
idc = 1001;
|
|
||||||
x = "safezoneX + (safezoneW * 0.4125)";
|
|
||||||
y = "safezoneY + (safezoneH * 0.1)";
|
|
||||||
w = "safezoneW * 1";
|
|
||||||
h = "safezoneH * 1";
|
|
||||||
colorBackground[] = {0, 0, 0, 0};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,156 +0,0 @@
|
|||||||
# Phone UI Framework
|
|
||||||
|
|
||||||
A lightweight, component-based framework for building phone-like user interfaces in the browser. This framework provides a React-like development experience without external dependencies, making it perfect for creating mobile-first web applications.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Component-based architecture (React-like API)
|
|
||||||
- Virtual DOM-like rendering system
|
|
||||||
- Built-in global and local state management
|
|
||||||
- Modular, maintainable CSS structure
|
|
||||||
- Mobile-first, accessible design (ARIA roles/labels)
|
|
||||||
- No external dependencies
|
|
||||||
- Easy production bundling (JS & CSS)
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
1. Clone the repository
|
|
||||||
2. **On Windows, run the provided script to build and start the local server:**
|
|
||||||
```powershell
|
|
||||||
./start.ps1
|
|
||||||
```
|
|
||||||
This will automatically build the JS and CSS bundles and open the app in your browser at [http://localhost:8000](http://localhost:8000).
|
|
||||||
|
|
||||||
3. **On Linux/macOS, run the provided shell script:**
|
|
||||||
```sh
|
|
||||||
chmod +x start.sh
|
|
||||||
./start.sh
|
|
||||||
```
|
|
||||||
This will automatically build the JS and CSS bundles and open the app in your browser at [http://localhost:8000](http://localhost:8000).
|
|
||||||
|
|
||||||
4. If you prefer, you can run the build manually with `node tools/concat-all.js` and start a local server (e.g., `python3 -m http.server`).
|
|
||||||
|
|
||||||
> **Note:** The app will not work unless you run the build script. Always re-run the build script if you add, remove, or change any JS or CSS files.
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
├── index.html # Main entry point
|
|
||||||
├── dist/ # Production bundles (auto-generated)
|
|
||||||
│ ├── app.bundle.js
|
|
||||||
│ └── app.bundle.css
|
|
||||||
├── styles/ # CSS files
|
|
||||||
│ ├── base.css
|
|
||||||
│ ├── main.css
|
|
||||||
│ └── components/ # Component-specific styles
|
|
||||||
├── js/ # JavaScript files
|
|
||||||
│ ├── core/ # Core framework (Component, StateManager)
|
|
||||||
│ ├── components/ # Shared UI components
|
|
||||||
│ ├── apps/ # App modules (phone, messages, contacts, settings)
|
|
||||||
│ ├── utils/ # Utility functions (scriptLoader, helpers)
|
|
||||||
│ ├── app.js # Main app integration/root
|
|
||||||
│ └── main.js # App initialization
|
|
||||||
├── tools/ # Build and utility scripts
|
|
||||||
│ ├── concat-js.js
|
|
||||||
│ ├── concat-css.js
|
|
||||||
│ └── concat-all.js
|
|
||||||
├── start.ps1 # Windows script to build and start local server
|
|
||||||
├── start.sh # Linux/macOS script to build and start local server
|
|
||||||
└── images/ # Image assets
|
|
||||||
```
|
|
||||||
|
|
||||||
## App Structure
|
|
||||||
|
|
||||||
- **Main App (`App` class in `js/app.js`)**: Handles app switching, global modals, and integration.
|
|
||||||
- **Apps (`js/apps/`)**: Each app (Phone, Messages, Contacts, Settings) has its own entry point (`index.js`) and components.
|
|
||||||
- **Components (`js/components/` and app subfolders)**: Reusable UI elements (NavigationBar, Modal, StatusBar, etc.).
|
|
||||||
- **State Management (`js/core/StateManager.js`)**: Global state via `globalState`, plus local state in components.
|
|
||||||
- **Utilities (`js/utils/`)**: Script loader, helpers, etc.
|
|
||||||
|
|
||||||
## Creating Components
|
|
||||||
|
|
||||||
Components are created by extending the base `Component` class:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
class MyComponent extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = { /* ... */ };
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
return this.createElement('div', { className: 'my-component' }, 'Hello World');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Component Lifecycle
|
|
||||||
- `constructor(props)`: Initialize component
|
|
||||||
- `render()`: Define component structure
|
|
||||||
- `componentDidMount()`: Called after mount
|
|
||||||
- `componentWillUnmount()`: Called before unmount
|
|
||||||
- `onStateChange(prevState, newState)`: On state change
|
|
||||||
|
|
||||||
### State Management
|
|
||||||
- Local: `this.setState({ ... })`
|
|
||||||
- Global: `globalState.setState({ ... })`, `globalState.subscribe(cb)`
|
|
||||||
|
|
||||||
## Creating Elements
|
|
||||||
|
|
||||||
Use `createElement` to create DOM elements:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
this.createElement('div', { className: 'container', onClick: ... }, 'Content');
|
|
||||||
```
|
|
||||||
|
|
||||||
## Styling
|
|
||||||
|
|
||||||
- Base styles: `base.css`, `main.css`
|
|
||||||
- Component styles: `styles/components/`
|
|
||||||
- For all environments, use the bundled `dist/app.bundle.css`
|
|
||||||
|
|
||||||
## Available Components
|
|
||||||
|
|
||||||
- `StatusBar`, `NavigationBar`, `Modal`, `HomeScreen`, `HomeIndicator`, `Header`, `SearchBar`
|
|
||||||
- App-specific: `ContactList`, `ContactItem`, `AddContactForm`, `MessagesList`, `MessageItem`, `ConversationView`, `Dialpad`, `Settings`
|
|
||||||
|
|
||||||
## Scripts
|
|
||||||
|
|
||||||
- `tools/concat-js.js`: Bundles all JS files into `dist/app.bundle.js`
|
|
||||||
- `tools/concat-css.js`: Bundles all CSS files into `dist/app.bundle.css`
|
|
||||||
- `tools/concat-all.js`: Bundles both JS and CSS (**required for all environments**)
|
|
||||||
- `start.ps1`: Builds and starts a local server on Windows
|
|
||||||
- `start.sh`: Builds and starts a local server on Linux/macOS
|
|
||||||
|
|
||||||
## How to Add a New App
|
|
||||||
|
|
||||||
1. Create a new folder in `js/apps/yourapp/` with an `index.js` and any components.
|
|
||||||
2. Add your app's entry point to the bundler scripts and (if needed) to the app switch logic in `js/app.js`.
|
|
||||||
3. Add styles in `styles/components/yourapp.css` and include in the CSS bundle list.
|
|
||||||
4. **Re-run the build script after any changes.**
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. Keep components small and focused
|
|
||||||
2. Use state management for global data
|
|
||||||
3. Follow the component lifecycle
|
|
||||||
4. Use modular CSS for styling
|
|
||||||
5. Handle cleanup in `componentWillUnmount`
|
|
||||||
6. Use ARIA roles/labels for accessibility
|
|
||||||
|
|
||||||
## Development & Production
|
|
||||||
|
|
||||||
- **Always run the build script (`node tools/concat-all.js`, `./start.ps1`, or `./start.sh`) before starting or deploying the app.**
|
|
||||||
- The app will not work unless all JS and CSS are bundled.
|
|
||||||
- If you encounter issues, re-run the build script to ensure all files are up to date.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
1. Fork the repository
|
|
||||||
2. Create your feature branch
|
|
||||||
3. Commit your changes
|
|
||||||
4. Push to the branch
|
|
||||||
5. Create a Pull Request
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the MIT License.
|
|
||||||
3303
arma/client/addons/phone/ui/_site/dist/app.bundle.css
vendored
8389
arma/client/addons/phone/ui/_site/dist/app.bundle.js
vendored
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |