Add org invite workflow and portal invite controls #3

Closed
J.Schmidt92 wants to merge 0 commits from development into master
968 changed files with 15065 additions and 103399 deletions

6
.gitignore vendored
View File

@ -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/

View File

@ -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
``` ```

View File

@ -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
View File

@ -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**

View File

@ -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).

View File

@ -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.

View File

@ -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), {

View File

@ -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]; };

View File

@ -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);

View File

@ -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));

View File

@ -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.

View File

@ -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);
[{ [{

View File

@ -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];
}; };

View File

@ -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]]];

File diff suppressed because one or more lines are too long

View File

@ -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);
}, },

View File

@ -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",
),
),
),
); );
} }

View File

@ -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,

View File

@ -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.

View File

@ -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 {

View File

@ -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", "", [""]]];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -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("_", " ")

View File

@ -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}`,

View File

@ -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();

View File

@ -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>

View File

@ -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);
} }

View File

@ -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[] = {};
};
};

View File

@ -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.

View File

@ -17,5 +17,4 @@ class CfgPatches {
}; };
#include "CfgEventHandlers.hpp" #include "CfgEventHandlers.hpp"
#include "CfgSounds.hpp"
#include "CfgVehicles.hpp" #include "CfgVehicles.hpp"

View File

@ -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);

View File

@ -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;

View File

@ -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.

View File

@ -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", []];

View File

@ -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]]];

View File

@ -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", []];

View File

@ -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", "", [""]]];

View File

@ -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;

View File

@ -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;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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,

View File

@ -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(

View File

@ -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,
}; };

View File

@ -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;

View File

@ -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.

View File

@ -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;

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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]]];

View File

@ -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]];
}; };

View File

@ -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]]];

File diff suppressed because one or more lines are too long

View File

@ -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,

View File

@ -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),
), ),
), ),
), ),

View File

@ -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) {

View File

@ -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",

View File

@ -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"),
), ),
), ),

View File

@ -1 +0,0 @@
forge\forge_client\addons\phone

View File

@ -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));
};
};

View File

@ -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.

View File

@ -1,3 +0,0 @@
PREP(handleUIEvents);
PREP(initRepository);
PREP(openUI);

View File

@ -1 +0,0 @@
#include "script_component.hpp"

View File

@ -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);

View File

@ -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"

View File

@ -1 +0,0 @@
#include "script_component.hpp"

View File

@ -1,2 +0,0 @@
#include "script_component.hpp"
#include "XEH_PREP.hpp"

View File

@ -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"

View File

@ -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;

View File

@ -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)

View File

@ -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;

View File

@ -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);

View File

@ -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"

View File

@ -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>

View File

@ -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;

View File

@ -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};
};
};
};

View File

@ -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.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because one or more lines are too long

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