- Rework org and store UI state modules (rename/move store/getter files, add runtime and bridge wiring) - Update store UI components and page structure (navbar/cart split, new StoreView flow) - Apply broad markdown/YAML/HTML/CSS/JS formatting cleanup across docs, templates, and workflows
438 lines
14 KiB
Markdown
438 lines
14 KiB
Markdown
# Redis Client Usage Examples
|
||
|
||
Practical examples of using the **raw Redis client module** as a foundation for higher-level game modules. These examples show low-level Redis operations that would typically be wrapped by game-specific modules (actor, garage, locker, bank).
|
||
|
||
> **Note**: These examples show raw Redis responses. In practice, your game modules would wrap these calls and return structured JSON to SQF scripts.
|
||
|
||
## 🚀 Function Behavior
|
||
|
||
All Redis functions are **synchronous from SQF's perspective**:
|
||
|
||
- Functions **block** until Redis operation completes
|
||
- **No callbacks** or async handling needed in SQF
|
||
- **Direct return values** – either data or error strings
|
||
- **Thread-safe** – multiple scripts can call simultaneously
|
||
|
||
The extension handles all async complexity internally using a macro-based architecture.
|
||
|
||
## 🎮 Player Management
|
||
|
||
### Player Join/Leave Tracking
|
||
|
||
```sqf
|
||
// When player joins
|
||
_playerUID = getPlayerUID player;
|
||
_playerName = name player;
|
||
|
||
// Store player info in hash
|
||
"forge_server" callExtension ["redis:hash:set", [format ["player:%1", _playerUID], "name", _playerName]];
|
||
"forge_server" callExtension ["redis:hash:set", [format ["player:%1", _playerUID], "join_time", str time]];
|
||
|
||
// Add to online players set
|
||
"forge_server" callExtension ["redis:set:add", ["online_players", _playerUID]];
|
||
|
||
// When player leaves
|
||
"forge_server" callExtension ["redis:set:del", ["online_players", _playerUID]];
|
||
"forge_server" callExtension ["redis:hash:set", [format ["player:%1", _playerUID], "leave_time", str time]];
|
||
```
|
||
|
||
### Player Statistics System
|
||
|
||
```sqf
|
||
// Initialize player stats
|
||
fnc_initPlayerStats = {
|
||
params ["_playerUID"];
|
||
|
||
_playerKey = format ["stats:%1", _playerUID];
|
||
"forge_server" callExtension ["redis:hash:mset", [_playerKey, [
|
||
["kills", "0"],
|
||
["deaths", "0"],
|
||
["score", "0"],
|
||
["playtime", "0"]
|
||
]]];
|
||
};
|
||
|
||
// Update player kill
|
||
fnc_addPlayerKill = {
|
||
params ["_playerUID"];
|
||
|
||
_playerKey = format ["stats:%1", _playerUID];
|
||
"forge_server" callExtension ["redis:hash:incr", [_playerKey, "kills", 1]];
|
||
"forge_server" callExtension ["redis:hash:incr", [_playerKey, "score", 10]];
|
||
};
|
||
|
||
// Get player stats (raw response)
|
||
fnc_getPlayerStats = {
|
||
params ["_playerUID"];
|
||
|
||
_playerKey = format ["stats:%1", _playerUID];
|
||
_rawResult = "forge_server" callExtension ["redis:hash:getall", [_playerKey]];
|
||
// _rawResult is now "kills,15,deaths,3,score,150,playtime,7200"
|
||
|
||
// Game modules would parse this into structured data
|
||
// For now, return raw comma-separated response
|
||
_rawResult select 0;
|
||
};
|
||
```
|
||
|
||
## 🏆 Leaderboards and Rankings
|
||
|
||
### Global Kill Leaderboard
|
||
|
||
```sqf
|
||
// Add score to sorted leaderboard (using list for simplicity)
|
||
fnc_updateLeaderboard = {
|
||
params ["_playerName", "_kills"];
|
||
|
||
// Store individual score
|
||
"forge_server" callExtension ["redis:common:set", [format ["kills:%1", _playerName], str _kills]];
|
||
|
||
// Add to leaderboard tracking
|
||
"forge_server" callExtension ["redis:set:add", ["leaderboard_players", _playerName]];
|
||
};
|
||
|
||
// Get top 10 players (raw response handling)
|
||
fnc_getTopPlayers = {
|
||
// Get all leaderboard players - returns comma-separated list
|
||
_playersResult = "forge_server" callExtension ["redis:set:members", ["leaderboard_players"]];
|
||
_rawPlayers = _playersResult select 0;
|
||
|
||
// Check for error
|
||
if (_rawPlayers find "Error:" == 0) exitWith { [] };
|
||
|
||
// Split comma-separated player list
|
||
_players = _rawPlayers splitString ",";
|
||
_scoreArray = [];
|
||
|
||
// Get scores for all players
|
||
{
|
||
_killsResult = "forge_server" callExtension ["redis:common:get", [format ["kills:%1", _x]]];
|
||
_rawKills = _killsResult select 0;
|
||
|
||
// Check for valid response (not an error)
|
||
if (_rawKills find "Error:" != 0) then {
|
||
_scoreArray pushBack [_x, parseNumber _rawKills];
|
||
};
|
||
} forEach _players;
|
||
|
||
// Sort by score (highest first)
|
||
_scoreArray sort false;
|
||
_scoreArray resize (10 min (count _scoreArray)); // Top 10
|
||
|
||
_scoreArray;
|
||
};
|
||
```
|
||
|
||
## 🎯 Mission State Management
|
||
|
||
### Objective System
|
||
|
||
```sqf
|
||
// Set mission objectives
|
||
fnc_initMissionObjectives = {
|
||
"forge_server" callExtension ["redis:list:rpush", ["objectives", "Secure Alpha Base"]];
|
||
"forge_server" callExtension ["redis:list:rpush", ["objectives", "Extract Intel"]];
|
||
"forge_server" callExtension ["redis:list:rpush", ["objectives", "Eliminate HVT"]];
|
||
|
||
// Set current objective pointer
|
||
"forge_server" callExtension ["redis:common:set", ["current_objective", "0"]];
|
||
};
|
||
|
||
// Complete current objective
|
||
fnc_completeObjective = {
|
||
// Get current objective index - returns raw string
|
||
_indexResult = "forge_server" callExtension ["redis:common:get", ["current_objective"]];
|
||
_rawIndex = _indexResult select 0;
|
||
|
||
// Check for error
|
||
if (_rawIndex find "Error:" == 0) exitWith {};
|
||
|
||
_currentIndex = parseNumber _rawIndex;
|
||
|
||
// Get objective name - returns raw string
|
||
_objResult = "forge_server" callExtension ["redis:list:get", ["objectives", _currentIndex]];
|
||
_objectiveName = _objResult select 0;
|
||
|
||
// Check for valid response
|
||
if (_objectiveName find "Error:" != 0) then {
|
||
// Move to completed objectives - returns new list length
|
||
"forge_server" callExtension ["redis:list:rpush", ["completed_objectives", _objectiveName]];
|
||
|
||
// Move to next objective - returns "OK"
|
||
"forge_server" callExtension ["redis:common:set", ["current_objective", str (_currentIndex + 1)]];
|
||
|
||
// Broadcast completion
|
||
[format ["Objective Complete: %1", _objectiveName]] remoteExec ["hint"];
|
||
};
|
||
};
|
||
|
||
// Get mission progress - raw responses
|
||
fnc_getMissionProgress = {
|
||
_totalResult = "forge_server" callExtension ["redis:list:len", ["objectives"]];
|
||
_completedResult = "forge_server" callExtension ["redis:list:len", ["completed_objectives"]];
|
||
|
||
_rawTotal = _totalResult select 0;
|
||
_rawCompleted = _completedResult select 0;
|
||
|
||
// Check for errors
|
||
if (_rawTotal find "Error:" == 0 || _rawCompleted find "Error:" == 0) exitWith {
|
||
"Mission Progress: Unknown";
|
||
};
|
||
|
||
_total = parseNumber _rawTotal;
|
||
_completed = parseNumber _rawCompleted;
|
||
|
||
format ["Mission Progress: %1/%2 objectives completed", _completed, _total];
|
||
};
|
||
```
|
||
|
||
## 🚁 Vehicle and Equipment Tracking
|
||
|
||
### Vehicle Pool System
|
||
|
||
```sqf
|
||
// Initialize vehicle pool
|
||
fnc_initVehiclePool = {
|
||
params ["_vehicleClass", "_count"];
|
||
|
||
for "_i" from 1 to _count do {
|
||
_vehicleId = format ["%1_%2", _vehicleClass, _i];
|
||
"forge_server" callExtension ["redis:set:add", ["available_vehicles", _vehicleId]];
|
||
"forge_server" callExtension ["redis:hash:mset", [format ["vehicle:%1", _vehicleId], [
|
||
["class", _vehicleClass],
|
||
["status", "available"],
|
||
["condition", "100"]
|
||
]]];
|
||
};
|
||
};
|
||
|
||
// Request vehicle
|
||
fnc_requestVehicle = {
|
||
params ["_playerUID"];
|
||
|
||
// Get random available vehicle
|
||
_result = "forge_server" callExtension ["redis:set:pop", ["available_vehicles"]];
|
||
_data = fromJSON (_result select 0);
|
||
|
||
if ((_data select "status") == "success") then {
|
||
_vehicleId = _data select "data";
|
||
|
||
// Mark as in use
|
||
"forge_server" callExtension ["redis:hash:set", [format ["vehicle:%1", _vehicleId], "status", "in_use"]];
|
||
"forge_server" callExtension ["redis:hash:set", [format ["vehicle:%1", _vehicleId], "user", _playerUID]];
|
||
"forge_server" callExtension ["redis:set:add", ["used_vehicles", _vehicleId]];
|
||
|
||
_vehicleId;
|
||
} else {
|
||
""; // No vehicles available
|
||
};
|
||
};
|
||
|
||
// Return vehicle
|
||
fnc_returnVehicle = {
|
||
params ["_vehicleId", "_condition"];
|
||
|
||
// Update condition
|
||
"forge_server" callExtension ["redis:hash:set", [format ["vehicle:%1", _vehicleId], "condition", str _condition]];
|
||
|
||
// Return to pool if condition is good
|
||
if (_condition > 50) then {
|
||
"forge_server" callExtension ["redis:hash:set", [format ["vehicle:%1", _vehicleId], "status", "available"]];
|
||
"forge_server" callExtension ["redis:hash:del", [format ["vehicle:%1", _vehicleId], "user"]];
|
||
"forge_server" callExtension ["redis:set:del", ["used_vehicles", _vehicleId]];
|
||
"forge_server" callExtension ["redis:set:add", ["available_vehicles", _vehicleId]];
|
||
} else {
|
||
"forge_server" callExtension ["redis:hash:set", [format ["vehicle:%1", _vehicleId], "status", "maintenance"]];
|
||
"forge_server" callExtension ["redis:set:add", ["maintenance_vehicles", _vehicleId]];
|
||
};
|
||
};
|
||
```
|
||
|
||
## 📊 Server Analytics
|
||
|
||
### Player Session Tracking
|
||
|
||
```sqf
|
||
// Track player session start
|
||
fnc_startPlayerSession = {
|
||
params ["_playerUID"];
|
||
|
||
_sessionId = format ["%1_%2", _playerUID, floor time];
|
||
_sessionKey = format ["session:%1", _sessionId];
|
||
|
||
"forge_server" callExtension ["redis:hash:mset", [_sessionKey, [
|
||
["player_uid", _playerUID],
|
||
["start_time", str time],
|
||
["server_id", serverName],
|
||
["player_count", str (count allPlayers)]
|
||
]]];
|
||
|
||
// Store current session for player
|
||
"forge_server" callExtension ["redis:common:set", [format ["current_session:%1", _playerUID], _sessionId]];
|
||
|
||
_sessionId;
|
||
};
|
||
|
||
// End player session
|
||
fnc_endPlayerSession = {
|
||
params ["_playerUID", "_sessionStats"];
|
||
|
||
// Get current session
|
||
_result = "forge_server" callExtension ["redis:common:get", [format ["current_session:%1", _playerUID]]];
|
||
_data = fromJSON (_result select 0);
|
||
|
||
if ((_data select "status") == "success") then {
|
||
_sessionId = _data select "data";
|
||
_sessionKey = format ["session:%1", _sessionId];
|
||
|
||
// Update session with end data
|
||
"forge_server" callExtension ["redis:hash:mset", [_sessionKey, [
|
||
["end_time", str time],
|
||
["duration", str (_sessionStats select "duration")],
|
||
["kills", str (_sessionStats select "kills")],
|
||
["deaths", str (_sessionStats select "deaths")]
|
||
]]];
|
||
|
||
// Clean up current session tracking
|
||
"forge_server" callExtension ["redis:common:del", [format ["current_session:%1", _playerUID]]];
|
||
};
|
||
};
|
||
```
|
||
|
||
## 🔄 Cross-Server Communication
|
||
|
||
### Message Queue System
|
||
|
||
```sqf
|
||
// Send message to other servers
|
||
fnc_sendCrossServerMessage = {
|
||
params ["_targetServer", "_messageType", "_messageData"];
|
||
|
||
_message = createHashMap;
|
||
_message set ["from_server", serverName];
|
||
_message set ["type", _messageType];
|
||
_message set ["data", _messageData];
|
||
_message set ["timestamp", str time];
|
||
|
||
_queueKey = format ["messages:%1", _targetServer];
|
||
"forge_server" callExtension ["redis:list:rpush", [_queueKey, str _message]];
|
||
};
|
||
|
||
// Check for incoming messages
|
||
fnc_checkMessages = {
|
||
_queueKey = format ["messages:%1", serverName];
|
||
|
||
// Get next message
|
||
_result = "forge_server" callExtension ["redis:list:lpop", [_queueKey, 1]];
|
||
_data = fromJSON (_result select 0);
|
||
|
||
if ((_data select "status") == "success") then {
|
||
_messages = _data select "data";
|
||
if (count _messages > 0) then {
|
||
_messageStr = _messages select 0;
|
||
_message = fromJSON _messageStr;
|
||
|
||
// Process message based on type
|
||
_type = _message select "type";
|
||
_messageData = _message select "data";
|
||
|
||
switch (_type) do {
|
||
case "player_transfer": {
|
||
[_messageData] call fnc_handlePlayerTransfer;
|
||
};
|
||
case "server_status": {
|
||
[_messageData] call fnc_handleServerStatus;
|
||
};
|
||
case "admin_broadcast": {
|
||
[_messageData select "message"] remoteExec ["hint"];
|
||
};
|
||
};
|
||
};
|
||
};
|
||
};
|
||
|
||
// Run message checker periodically
|
||
[] spawn {
|
||
while {true} do {
|
||
call fnc_checkMessages;
|
||
sleep 5; // Check every 5 seconds
|
||
};
|
||
};
|
||
```
|
||
|
||
## 🛠️ Utility Functions
|
||
|
||
### Redis Helper Functions
|
||
|
||
```sqf
|
||
// Parse Redis response safely
|
||
fnc_parseRedisResponse = {
|
||
params ["_response"];
|
||
|
||
try {
|
||
_data = fromJSON (_response select 0);
|
||
if ((_data select "status") == "success") then {
|
||
_data select "data";
|
||
} else {
|
||
diag_log format ["Redis Error: %1", _data select "error"];
|
||
nil;
|
||
};
|
||
} catch {
|
||
diag_log format ["JSON Parse Error: %1", _exception];
|
||
nil;
|
||
};
|
||
};
|
||
|
||
// Batch Redis operations
|
||
fnc_redisBatch = {
|
||
params ["_operations"];
|
||
|
||
_results = [];
|
||
{
|
||
_op = _x;
|
||
_result = "forge_server" callExtension [_op select 0, _op select 1];
|
||
_results pushBack (fromJSON (_result select 0));
|
||
} forEach _operations;
|
||
|
||
_results;
|
||
};
|
||
|
||
// Example batch usage:
|
||
_batchOps = [
|
||
["redis:common:set", ["key1", "value1"]],
|
||
["redis:common:set", ["key2", "value2"]],
|
||
["redis:common:get", ["key1"]]
|
||
];
|
||
_results = [_batchOps] call fnc_redisBatch;
|
||
```
|
||
|
||
## 🎯 Best Practices
|
||
|
||
### Error Handling Pattern
|
||
|
||
```sqf
|
||
fnc_safeRedisCall = {
|
||
params ["_command", "_params", ["_defaultValue", nil]];
|
||
|
||
try {
|
||
_result = "forge_server" callExtension [_command, _params];
|
||
_data = fromJSON (_result select 0);
|
||
|
||
if ((_data select "status") == "success") then {
|
||
_data select "data";
|
||
} else {
|
||
diag_log format ["Redis operation failed: %1 - %2", _command, _data select "error"];
|
||
_defaultValue;
|
||
};
|
||
} catch {
|
||
diag_log format ["Redis call exception: %1 - %2", _command, _exception];
|
||
_defaultValue;
|
||
};
|
||
};
|
||
|
||
// Usage:
|
||
_playerName = ["redis:common:get", ["player_name"], "Unknown"] call fnc_safeRedisCall;
|
||
```
|
||
|
||
These examples demonstrate real-world usage patterns for the Redis extension in Arma 3 environments, covering player management, mission state, analytics, and cross-server communication.
|