9 Commits

Author SHA1 Message Date
737416965e add null disconnect handling for query 2023-07-05 11:43:23 -07:00
05946c0ebf update readme & cleanup 2023-07-05 11:39:31 -07:00
79fb39b154 Merge branch 'feature/track-sessions' into main 2023-06-28 21:55:38 -07:00
fc53ecb770 bug fixes
- fixes world/mission log race condition w/ new foreign key constraint in DB
- fixes missing world_id sent with mission rows
- adds worldName to missionContext hash in sqf
2023-06-28 21:54:57 -07:00
dbd3d68537 WORKING: final bugfixes & add stored procedure for prev mission cleanup 2023-06-22 10:27:09 -07:00
3bb8c358fa bug fixing 2023-06-20 23:10:58 -07:00
8f971d9887 bug fixes/debugging 2023-06-17 22:34:52 -07:00
f6ff42e467 bug fixes, 3 table split, update readme 2023-06-14 09:59:45 -07:00
7608df9e53 change to 1 row per session (networkId) 2023-06-13 17:59:04 -07:00
27 changed files with 567 additions and 218 deletions

5
.gitignore vendored
View File

@@ -1,6 +1,5 @@
*.log *.log
\@17thAttendanceTracker/config.json
*.bak *.bak
\@AttendanceTracker/config.json

View File

@@ -1,91 +0,0 @@
[
["OnUserConnected", {
params ["_networkId", "_clientStateNumber", "_clientState"];
[format ["(EventHandler) OnUserConnected fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
private _userInfo = (getUserInfo _networkId);
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"];
if (_isHC) exitWith {};
[
"ConnectedServer",
_playerID,
_playerUID,
_profileName,
_steamName
] call attendanceTracker_fnc_logServerEvent;
(AttendanceTracker getVariable ["allUsers", createHashMap]) set [_networkId, _userInfo];
}],
["OnUserDisconnected", {
params ["_networkId", "_clientStateNumber", "_clientState"];
[format ["(EventHandler) OnUserDisconnected fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
private _userInfo = (AttendanceTracker getVariable ["allUsers", createHashMap]) get _networkId;
if (isNil "_userInfo") exitWith {};
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"];
if (_isHC) exitWith {};
[
"DisconnectedServer",
_playerID,
_playerUID,
_profileName,
_steamName
] call attendanceTracker_fnc_logServerEvent;
}],
["PlayerConnected", {
params ["_id", "_uid", "_name", "_jip", "_owner", "_idstr"];
[format ["(EventHandler) PlayerConnected fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
private _userInfo = (getUserInfo _idstr);
if (isNil "_userInfo") exitWith {};
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"];
(AttendanceTracker getVariable ["allUsers", createHashMap]) set [_playerID, _userInfo];
if (_isHC) exitWith {};
[
"ConnectedMission",
_playerID,
_playerUID,
_profileName,
_steamName,
_jip,
roleDescription _unit
] call attendanceTracker_fnc_logMissionEvent;
}],
["PlayerDisconnected", {
// NOTE: HandleDisconnect returns a DIFFERENT _id than PlayerDisconnected and above handlers, so we can't use it here
params ["_id", "_uid", "_name", "_jip", "_owner", "_idstr"];
[format ["(EventHandler) HandleDisconnect fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
private _userInfo = (AttendanceTracker getVariable ["allUsers", createHashMap]) get _idstr;
if (isNil "_userInfo") exitWith {
[format ["(EventHandler) HandleDisconnect: No user info found for %1", _idstr], "DEBUG"] call attendanceTracker_fnc_log;
};
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"];
if (_isHC) exitWith {};
[
"DisconnectedMission",
_playerID,
_playerUID,
_profileName,
_steamName,
_jip
] call attendanceTracker_fnc_logMissionEvent;
false;
}]
];

View File

@@ -1,2 +0,0 @@
params [["_value", "", [""]]];
("AttendanceTracker" callExtension ["getMissionHash", _value]) select 0;

View File

@@ -1 +0,0 @@
(parseSimpleArray ("AttendanceTracker" callExtension "getTimestamp")) select 0;

View File

@@ -18,10 +18,12 @@ class CfgFunctions {
class eventHandlers {}; class eventHandlers {};
class callbackHandler {postInit = 1;}; class callbackHandler {postInit = 1;};
class log {}; class log {};
class logMissionEvent {}; class writeConnect {};
class logServerEvent {}; class writeDisconnect {};
class timestamp {}; class timestamp {};
class getHash {}; class getMissionHash {};
class getWorldInfo {};
class missionLoaded {};
}; };
}; };
}; };

View File

@@ -21,11 +21,16 @@ addMissionEventHandler ["ExtensionCallback", {
false; false;
}; };
diag_log format ["Raw callback: %1: %2", _function, _data];
// Parse response from string array // Parse response from string array
private "_response"; private "_response";
try { try {
// diag_log format ["Raw callback: %1: %2", _function, _data]; // diag_log format ["Raw callback: %1: %2", _function, _data];
_response = parseSimpleArray _data; _response = parseSimpleArray _data;
if (_response isEqualTo []) then {
throw "Failed to parse response as array";
};
} catch { } catch {
[ [
format ["Callback invalid data: %1: %2: %3", _function, _data, _exception], format ["Callback invalid data: %1: %2: %3", _function, _data, _exception],
@@ -41,18 +46,40 @@ addMissionEventHandler ["ExtensionCallback", {
[_response#0, _response#1, _function] call attendanceTracker_fnc_log; [_response#0, _response#1, _function] call attendanceTracker_fnc_log;
if (_response#0 == "SUCCESS") then { if (_response#0 == "SUCCESS") then {
missionNamespace setVariable ["AttendanceTracker_DBConnected", true]; missionNamespace setVariable ["AttendanceTracker_DBConnected", true];
// close any null disconnect values from previous mission
"AttendanceTracker" callExtension ["fillLastMissionNull", []];
// log world info
private _response = "AttendanceTracker" callExtension [
"logWorld",
[
[(call attendanceTracker_fnc_getWorldInfo)] call CBA_fnc_encodeJSON
]
];
// log mission info and get back the row Id to send with future messages // log mission info and get back the row Id to send with future messages
private _response = "AttendanceTracker" callExtension ["logMission", [ private _response = "AttendanceTracker" callExtension [
[AttendanceTracker getVariable ["missionContext", createHashMap]] call CBA_fnc_encodeJSON "logMission",
]]; [
AttendanceTracker_missionId = parseNumber _response; [AttendanceTracker getVariable ["missionContext", createHashMap]] call CBA_fnc_encodeJSON
]
];
// log world info };
private _response = "AttendanceTracker" callExtension ["logWorld", [ };
[call attendanceTracker_fnc_getWorldInfo] call CBA_fnc_encodeJSON case "writeMissionInfo": {
]]; if (_response#0 == "MISSION_ID") then {
AttendanceTracker_missionId = parseNumber (_response#1);
};
};
case "writeAttendance": {
if (_response#0 == "ATT_LOG") then {
(_response#1) params ["_eventType", "_netId", "_rowId"];
private _storeIndex = ["SERVER", "MISSION"] find _eventType;
((AttendanceTracker getVariable ["rowIds", createHashMap]) getOrDefault [
_netId,
[nil, nil]
]) set [_storeIndex, _rowId];
}; };
}; };
default { default {

View File

@@ -0,0 +1,160 @@
[
["OnUserConnected", {
params ["_networkId", "_clientStateNumber", "_clientState"];
[format ["(EventHandler) OnUserConnected fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
private _userInfo = (getUserInfo _networkId);
if (isNil "_userInfo") exitWith {
[format ["(EventHandler) OnUserConnected: No user info found for %1", _networkId], "DEBUG"] call attendanceTracker_fnc_log;
};
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"];
if (_isHC) exitWith {
[format ["(EventHandler) OnUserConnected: %1 is HC, skipping", _playerID], "DEBUG"] call attendanceTracker_fnc_log;
};
(AttendanceTracker getVariable ["allUsers", createHashMap]) set [_networkId, _userInfo];
[
"Server",
_playerID,
_playerUID,
_profileName,
_steamName,
nil,
nil
] call attendanceTracker_fnc_writeConnect;
}],
["OnUserDisconnected", {
params ["_networkId", "_clientStateNumber", "_clientState"];
[format ["(EventHandler) OnUserDisconnected fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
if !(call attendanceTracker_fnc_missionLoaded) exitWith {
[format ["(EventHandler) OnUserDisconnected: Server is in Mission Asked, likely mission selection state. Skipping.."], "DEBUG"] call attendanceTracker_fnc_log;
};
private _userInfo = (AttendanceTracker getVariable ["allUsers", createHashMap]) get _networkId;
if (isNil "_userInfo") exitWith {
[format ["(EventHandler) OnUserDisconnected: No user info found for %1", _networkId], "DEBUG"] call attendanceTracker_fnc_log;
};
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"];
if (_isHC) exitWith {
[format ["(EventHandler) OnUserDisconnected: %1 is HC, skipping", _playerID], "DEBUG"] call attendanceTracker_fnc_log;
};
[
"Server",
_playerID,
_playerUID,
_profileName,
_steamName
] call attendanceTracker_fnc_writeDisconnect;
}],
["PlayerConnected", {
params ["_id", "_uid", "_name", "_jip", "_owner", "_idstr"];
[format ["(EventHandler) PlayerConnected fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
if !(call attendanceTracker_fnc_missionLoaded) exitWith {
[format ["(EventHandler) PlayerConnected: Server is in Mission Asked, likely mission selection state. Skipping.."], "DEBUG"] call attendanceTracker_fnc_log;
};
private _userInfo = (getUserInfo _idstr);
if (isNil "_userInfo") exitWith {
[format ["(EventHandler) PlayerConnected: No user info found for %1", _idstr], "DEBUG"] call attendanceTracker_fnc_log;
};
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"];
if (_isHC) exitWith {
[format ["(EventHandler) PlayerConnected: %1 is HC, skipping", _playerID], "DEBUG"] call attendanceTracker_fnc_log;
};
(AttendanceTracker getVariable ["allUsers", createHashMap]) set [_playerID, _userInfo];
[
"Mission",
_playerID,
_playerUID,
_profileName,
_steamName,
_jip,
roleDescription _unit
] call attendanceTracker_fnc_writeConnect;
}],
["PlayerDisconnected", {
// NOTE: HandleDisconnect returns a DIFFERENT _id than PlayerDisconnected and above handlers, so we can't use it here
params ["_id", "_uid", "_name", "_jip", "_owner", "_idstr"];
[format ["(EventHandler) HandleDisconnect fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
if !(call attendanceTracker_fnc_missionLoaded) exitWith {
[format ["(EventHandler) HandleDisconnect: Server is in Mission Asked, likely mission selection state. Skipping.."], "DEBUG"] call attendanceTracker_fnc_log;
};
private _userInfo = (AttendanceTracker getVariable ["allUsers", createHashMap]) get _idstr;
if (isNil "_userInfo") exitWith {
[format ["(EventHandler) HandleDisconnect: No user info found for %1", _idstr], "DEBUG"] call attendanceTracker_fnc_log;
};
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit", "_rowId"];
if (_isHC) exitWith {
[format ["(EventHandler) HandleDisconnect: %1 is HC, skipping", _playerID], "DEBUG"] call attendanceTracker_fnc_log;
};
[
"Mission",
_playerID,
_playerUID,
_profileName,
_steamName,
_jip,
nil
] call attendanceTracker_fnc_writeDisconnect;
false;
}],
["OnUserKicked", {
params ["_networkId", "_kickTypeNumber", "_kickType", "_kickReason", "_kickMessageIncReason"];
[format ["(EventHandler) OnUserKicked fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log;
if !(call attendanceTracker_fnc_missionLoaded) exitWith {
[format ["(EventHandler) OnUserKicked: Server is in Mission Asked, likely mission selection state. Skipping.."], "DEBUG"] call attendanceTracker_fnc_log;
};
private _userInfo = (AttendanceTracker getVariable ["allUsers", createHashMap]) get _networkId;
if (isNil "_userInfo") exitWith {
[format ["(EventHandler) OnUserKicked: No user info found for %1", _networkId], "DEBUG"] call attendanceTracker_fnc_log;
};
_userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"];
if (_isHC) exitWith {
[format ["(EventHandler) OnUserKicked: %1 is HC, skipping", _playerID], "DEBUG"] call attendanceTracker_fnc_log;
};
[
"Server",
_playerID,
_playerUID,
_profileName,
_steamName,
nil,
nil
] call attendanceTracker_fnc_writeDisconnect;
[
"Mission",
_playerID,
_playerUID,
_profileName,
_steamName,
nil,
nil
] call attendanceTracker_fnc_writeDisconnect;
}]
];

View File

@@ -0,0 +1 @@
(parseSimpleArray ("AttendanceTracker" callExtension "getMissionHash")) select 0;

View File

@@ -14,7 +14,7 @@ _workshopID = '';
} foreach getLoadedModsInfo; } foreach getLoadedModsInfo;
// [_name, _author, _workshopID]; // [_name, _author, _workshopID];
[ _return = createHashMapFromArray [
["author", _author], ["author", _author],
["workshopID", _workshopID], ["workshopID", _workshopID],
["displayName", _name], ["displayName", _name],
@@ -23,4 +23,6 @@ _workshopID = '';
["worldSize", worldSize], ["worldSize", worldSize],
["latitude", getNumber( _world >> "latitude" )], ["latitude", getNumber( _world >> "latitude" )],
["longitude", getNumber( _world >> "longitude" )] ["longitude", getNumber( _world >> "longitude" )]
]; ];
diag_log format ["Attendance Tracker: WorldInfo is: %1", _return];
_return

View File

@@ -0,0 +1 @@
!(getClientStateNumber <= 5 || getClientStateNumber isEqualTo 11);

View File

@@ -2,7 +2,9 @@
AttendanceTracker = false call CBA_fnc_createNamespace; AttendanceTracker = false call CBA_fnc_createNamespace;
AttendanceTracker_missionStartTimestamp = call attendanceTracker_fnc_timestamp; AttendanceTracker_missionStartTimestamp = call attendanceTracker_fnc_timestamp;
AttendanceTracker_missionHash = [AttendanceTracker_missionStartTimestamp] call attendanceTracker_fnc_getMissionHash; diag_log format ["AttendanceTracker: Mission started at %1", AttendanceTracker_missionStartTimestamp];
AttendanceTracker_missionHash = call attendanceTracker_fnc_getMissionHash;
diag_log format ["AttendanceTracker: Mission hash is %1", AttendanceTracker_missionHash];
AttendanceTracker setVariable ["missionContext", createHashMapFromArray [ AttendanceTracker setVariable ["missionContext", createHashMapFromArray [
["missionName", missionName], ["missionName", missionName],
@@ -13,13 +15,15 @@ AttendanceTracker setVariable ["missionContext", createHashMapFromArray [
["serverName", serverName], ["serverName", serverName],
["serverProfile", profileName], ["serverProfile", profileName],
["missionStart", AttendanceTracker_missionStartTimestamp], ["missionStart", AttendanceTracker_missionStartTimestamp],
["missionHash", AttendanceTracker_missionHash] ["missionHash", AttendanceTracker_missionHash],
["worldName", toLower worldName]
]]; ]];
// store all user details in a hash when they connect so we can reference it in disconnect events // store all user details in a hash when they connect so we can reference it in disconnect events
AttendanceTracker setVariable ["allUsers", createHashMap]; AttendanceTracker setVariable ["allUsers", createHashMap];
AttendanceTracker setVariable ["rowIds", createHashMap];
missionNamespace setVariable ["AttendanceTracker_debug", false]; missionNamespace setVariable ["AttendanceTracker_debug", false];
call attendanceTracker_fnc_connectDB; call attendanceTracker_fnc_connectDB;

View File

@@ -0,0 +1,24 @@
// (parseSimpleArray ("AttendanceTracker" callExtension "getTimestamp")) select 0;
// need date for MySQL in format 2006-01-02 15:04:05
systemTimeUTC params [
"_year",
"_month",
"_day",
"_hour",
"_minute",
"_second",
"_millisecond"
];
format[
"%1-%2-%3 %4:%5:%6",
_year,
_month,
_day,
_hour,
_minute,
_second
];

View File

@@ -9,6 +9,7 @@ params [
]; ];
private _hash = + (AttendanceTracker getVariable ["missionContext", createHashMap]); private _hash = + (AttendanceTracker getVariable ["missionContext", createHashMap]);
_hash set ["eventType", _eventType]; _hash set ["eventType", _eventType];
_hash set ["playerId", _playerId]; _hash set ["playerId", _playerId];
_hash set ["playerUID", _playerUID]; _hash set ["playerUID", _playerUID];
@@ -18,6 +19,6 @@ _hash set ["isJIP", _isJIP];
_hash set ["roleDescription", _roleDescription]; _hash set ["roleDescription", _roleDescription];
_hash set ["missionHash", missionNamespace getVariable ["AttendanceTracker_missionHash", ""]]; _hash set ["missionHash", missionNamespace getVariable ["AttendanceTracker_missionHash", ""]];
"AttendanceTracker" callExtension ["logAttendance", [[_hash] call CBA_fnc_encodeJSON]]; "AttendanceTracker" callExtension ["writeAttendance", [[_hash] call CBA_fnc_encodeJSON]];
true; true;

View File

@@ -3,20 +3,22 @@ params [
["_playerId", ""], ["_playerId", ""],
["_playerUID", ""], ["_playerUID", ""],
["_profileName", ""], ["_profileName", ""],
["_steamName", ""] ["_steamName", ""],
["_isJIP", false, [true, false]],
["_roleDescription", ""]
]; ];
private _hash = + (AttendanceTracker getVariable ["missionContext", createHashMap]); private _hash = + (AttendanceTracker getVariable ["missionContext", createHashMap]);
_hash set ["eventType", _eventType]; _hash set ["eventType", _eventType];
_hash set ["playerId", _playerId]; _hash set ["playerId", _playerId];
_hash set ["playerUID", _playerUID]; _hash set ["playerUID", _playerUID];
_hash set ["profileName", _profileName]; _hash set ["profileName", _profileName];
_hash set ["steamName", _steamName]; _hash set ["steamName", _steamName];
_hash set ["isJIP", false]; _hash set ["isJIP", _isJIP];
_hash set ["roleDescription", ""]; _hash set ["roleDescription", _roleDescription];
_hash set ["missionHash", missionNamespace getVariable ["AttendanceTracker_missionHash", ""]]; _hash set ["missionHash", missionNamespace getVariable ["AttendanceTracker_missionHash", ""]];
"AttendanceTracker" callExtension ["logAttendance", [[_hash] call CBA_fnc_encodeJSON]]; "AttendanceTracker" callExtension ["writeDisconnectEvent", [[_hash] call CBA_fnc_encodeJSON]];
true; true;

View File

@@ -3,5 +3,5 @@
"mysqlPort": 3306, "mysqlPort": 3306,
"mysqlUser": "root", "mysqlUser": "root",
"mysqlPassword": "root", "mysqlPassword": "root",
"mysqlDatabase": "db" "mysqlDatabase": "arma3_attendance"
} }

105
README.md
View File

@@ -1,40 +1,23 @@
# 17th-attendanceTracker # Arma 3 Attendance Tracker
## Setup ## Setup
**You will need a running MySQL or MariaDB instance.** **You will need a running MySQL or MariaDB instance.**
Create a database with a name of your choosing. Then, run the following SQL command against it to create a table. The following SQL commands will set up the necessary tables for the application. You can run them from the MySQL command line or from a tool like phpMyAdmin.
*In future, an ORM will be used to set this up automatically.*
```sql ```sql
-- a3server.attendancelog definition CREATE DATABASE `arma3_attendance` /*!40100 DEFAULT CHARACTER SET utf8mb3 */;
CREATE TABLE `attendancelog` ( USE `arma3_attendance`;
`id` int(11) NOT NULL AUTO_INCREMENT,
`timestamp` datetime NOT NULL,
`event_hash` varchar(100) NOT NULL,
`event_type` varchar(100) NOT NULL,
`player_id` varchar(30) NOT NULL,
`player_uid` varchar(100) NOT NULL,
`profile_name` varchar(100) NOT NULL,
`steam_name` varchar(100) DEFAULT NULL,
`is_jip` tinyint(4) DEFAULT NULL,
`role_description` varchar(100) DEFAULT NULL,
`mission_start` datetime NOT NULL,
`mission_name` varchar(100) DEFAULT NULL,
`briefing_name` varchar(100) DEFAULT NULL,
`mission_name_source` varchar(100) DEFAULT NULL,
`on_load_name` varchar(100) DEFAULT NULL,
`author` varchar(100) DEFAULT NULL,
`server_name` varchar(100) NOT NULL,
`server_profile` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2713 DEFAULT CHARSET=utf8mb3;
-- a3server.`missions` definition
-- a3server.missions definition
CREATE TABLE `missions` ( CREATE TABLE `missions` (
`id` int(11) NOT NULL AUTO_INCREMENT, `id` int(11) NOT NULL AUTO_INCREMENT,
`world_id` int(11) DEFAULT NULL,
`mission_hash` varchar(100) NOT NULL DEFAULT '',
`mission_name` varchar(100) NOT NULL, `mission_name` varchar(100) NOT NULL,
`mission_name_source` varchar(100) DEFAULT NULL, `mission_name_source` varchar(100) DEFAULT NULL,
`briefing_name` varchar(100) DEFAULT NULL, `briefing_name` varchar(100) DEFAULT NULL,
@@ -42,14 +25,31 @@ CREATE TABLE `missions` (
`author` varchar(100) DEFAULT NULL, `author` varchar(100) DEFAULT NULL,
`server_name` varchar(100) DEFAULT NULL, `server_name` varchar(100) DEFAULT NULL,
`server_profile` varchar(100) DEFAULT NULL, `server_profile` varchar(100) DEFAULT NULL,
`mission_start` datetime DEFAULT NULL, `mission_start` datetime DEFAULT NULL COMMENT 'In UTC',
PRIMARY KEY (`id`),
KEY `mission_hash` (`mission_hash`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3;
-- arma3_attendance.attendance definition
CREATE TABLE `attendance` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`join_time` datetime DEFAULT NULL COMMENT 'Stored in UTC',
`disconnect_time` datetime DEFAULT NULL COMMENT 'Stored in UTC',
`mission_hash` varchar(100) DEFAULT NULL, `mission_hash` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`) `event_type` varchar(100) NOT NULL,
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb3; `player_id` varchar(30) NOT NULL,
`player_uid` varchar(100) NOT NULL,
`profile_name` varchar(100) NOT NULL,
`steam_name` varchar(100) DEFAULT NULL,
`is_jip` tinyint(4) DEFAULT NULL,
`role_description` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `mission_hash` (`mission_hash`),
CONSTRAINT `attendance_ibfk_1` FOREIGN KEY (`mission_hash`) REFERENCES `missions` (`mission_hash`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3;
-- a3server.`worlds` definition -- a3server.worlds definition
CREATE TABLE `worlds` ( CREATE TABLE `worlds` (
`id` int(11) NOT NULL AUTO_INCREMENT, `id` int(11) NOT NULL AUTO_INCREMENT,
`author` varchar(100) DEFAULT NULL, `author` varchar(100) DEFAULT NULL,
@@ -59,9 +59,44 @@ CREATE TABLE `worlds` (
`world_size` int(11) DEFAULT NULL, `world_size` int(11) DEFAULT NULL,
`latitude` float DEFAULT NULL, `latitude` float DEFAULT NULL,
`longitude` float DEFAULT NULL, `longitude` float DEFAULT NULL,
PRIMARY KEY (`id`) `workshop_id` varchar(50) DEFAULT NULL,
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb3; PRIMARY KEY (`id`),
UNIQUE KEY `world_name` (`world_name`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb3;
``` ```
Finally, copy `config.example.json` to `config.json` and update it with your database credentials. Finally, copy `config.example.json` to `config.json` and update it with your database credentials and path.
## QUERIES
### Show missions with attendance
This will retrieve a view showing all missions with attendance data, sorted by the most recent mission joins first. Mission events without a mission disconnect_time (due to server crash or in-progress mission) will be ignored.
```sql
select
a.server_profile as Server,
a.briefing_name as "Mission Name",
a.mission_start as "Start Time",
b.display_name as "World",
c.profile_name as "Player Name",
c.player_uid as "Player UID",
TIMESTAMPDIFF(
MINUTE,
c.join_time,
c.disconnect_time
) as "Play Time (m)",
c.join_time as "Join Time",
c.disconnect_time as "Leave Time"
from missions a
LEFT JOIN worlds b ON a.world_id = b.id
LEFT JOIN attendance c ON a.mission_hash = c.mission_hash
where
c.event_type = 'Mission'
AND c.disconnect_time IS NOT NULL
AND TIMESTAMPDIFF(
MINUTE,
c.join_time,
c.disconnect_time
) > 0
```

View File

@@ -3,5 +3,5 @@
"mysqlPort": 3306, "mysqlPort": 3306,
"mysqlUser": "root", "mysqlUser": "root",
"mysqlPassword": "root", "mysqlPassword": "root",
"mysqlDatabase": "db" "mysqlDatabase": "arma3_attendance"
} }

Binary file not shown.

View File

@@ -1,3 +1,3 @@
$ENV:GOARCH = "amd64" $ENV:GOARCH = "amd64"
$ENV:CGO_ENABLED = 1 $ENV:CGO_ENABLED = 1
go1.16.4 build -o AttendanceTracker_x64.dll -buildmode=c-shared . go1.16.4 build -o ../@AttendanceTracker/AttendanceTracker_x64.dll -buildmode=c-shared .

Binary file not shown.

Binary file not shown.

View File

@@ -16,7 +16,8 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"strconv" "path"
"runtime"
"strings" "strings"
"time" "time"
"unsafe" "unsafe"
@@ -32,6 +33,12 @@ var ADDON_FOLDER string = getDir() + "\\@AttendanceTracker"
var LOG_FILE string = ADDON_FOLDER + "\\attendanceTracker.log" var LOG_FILE string = ADDON_FOLDER + "\\attendanceTracker.log"
var CONFIG_FILE string = ADDON_FOLDER + "\\config.json" var CONFIG_FILE string = ADDON_FOLDER + "\\config.json"
var ATTENDANCE_TABLE string = "attendance"
var MISSIONS_TABLE string = "missions"
var WORLDS_TABLE string = "worlds"
// ! TODO make a hash to save key:netId from A3 value:rowId from join event
var ATConfig AttendanceTrackerConfig var ATConfig AttendanceTrackerConfig
type AttendanceTrackerConfig struct { type AttendanceTrackerConfig struct {
@@ -104,14 +111,14 @@ func loadConfig() {
writeLog(functionName, `["Config loaded", "INFO"]`) writeLog(functionName, `["Config loaded", "INFO"]`)
} }
func getMissionHash(time string) string { func getMissionHash() string {
functionName := "getMissionHash" functionName := "getMissionHash"
// get md5 hash of string // get md5 hash of string
// https://stackoverflow.com/questions/2377881/how-to-get-a-md5-hash-from-a-string-in-golang // https://stackoverflow.com/questions/2377881/how-to-get-a-md5-hash-from-a-string-in-golang
hash := md5.Sum([]byte(time)) hash := md5.Sum([]byte(time.Now().UTC().Format("2006-01-02 15:04:05")))
// convert to string // convert to string
hashString := fmt.Sprintf("%x", hash) hashString := fmt.Sprintf(`%x`, hash)
writeLog(functionName, fmt.Sprintf(`["Mission hash: %s", "INFO"]`, hashString)) writeLog(functionName, fmt.Sprintf(`["Mission hash: %s", "INFO"]`, hashString))
return hashString return hashString
} }
@@ -147,7 +154,7 @@ func connectDB() string {
return "ERROR" return "ERROR"
} }
// Connect and check the server version // Check the server version
var version string var version string
err = db.QueryRow("SELECT VERSION()").Scan(&version) err = db.QueryRow("SELECT VERSION()").Scan(&version)
if err != nil { if err != nil {
@@ -172,24 +179,29 @@ type WorldInfo struct {
func writeWorldInfo(worldInfo string) { func writeWorldInfo(worldInfo string) {
functionName := "writeWorldInfo" functionName := "writeWorldInfo"
// writeLog(functionName, fmt.Sprintf(`["%s", "DEBUG"]`, worldInfo))
// worldInfo is json, parse it // worldInfo is json, parse it
var wi WorldInfo var wi WorldInfo
err := json.Unmarshal([]byte(worldInfo), &wi) fixedString := fixEscapeQuotes(trimQuotes(worldInfo))
err := json.Unmarshal([]byte(fixedString), &wi)
if err != nil { if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return return
} }
// write to log // write to log as json
writeLog(functionName, fmt.Sprintf(`["Author:%s WorkshopID:%s DisplayName:%s WorldName:%s WorldNameOriginal:%s WorldSize:%d Latitude:%f Longitude:%f", "INFO"]`, wi.Author, wi.WorkshopID, wi.DisplayName, wi.WorldName, wi.WorldNameOriginal, wi.WorldSize, wi.Latitude, wi.Longitude)) // writeLog(functionName, fmt.Sprintf(`["%s", "DEBUG"]`, json.Marshal(wi)))
// write to database // write to database
// check if world exists // check if world exists
var worldID int var worldID int
err = db.QueryRow("SELECT id FROM worlds WHERE workshop_id = ?", wi.WorkshopID).Scan(&worldID) err = db.QueryRow("SELECT id FROM worlds WHERE world_name = ?", wi.WorldName).Scan(&worldID)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
// world does not exist, insert it // world does not exist, insert it
stmt, err := db.Prepare("INSERT INTO worlds (author, workshop_id, display_name, world_name, world_name_original, world_size, latitude, longitude) VALUES (?, ?, ?, ?, ?, ?, ?, ?)") stmt, err := db.Prepare(fmt.Sprintf(
"INSERT INTO %s (author, workshop_id, display_name, world_name, world_name_original, world_size, latitude, longitude) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
WORLDS_TABLE,
))
if err != nil { if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return return
@@ -212,7 +224,10 @@ func writeWorldInfo(worldInfo string) {
} }
} else { } else {
// world exists, update it // world exists, update it
stmt, err := db.Prepare("UPDATE worlds SET author = ?, workshop_id = ?, display_name = ?, world_name = ?, world_name_original = ?, world_size = ?, latitude = ?, longitude = ? WHERE id = ?") stmt, err := db.Prepare(fmt.Sprintf(
"UPDATE %s SET author = ?, workshop_id = ?, display_name = ?, world_name = ?, world_name_original = ?, world_size = ?, latitude = ?, longitude = ? WHERE id = ?",
WORLDS_TABLE,
))
if err != nil { if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return return
@@ -242,48 +257,74 @@ type MissionInfo struct {
ServerProfile string `json:"serverProfile"` ServerProfile string `json:"serverProfile"`
MissionStart string `json:"missionStart"` MissionStart string `json:"missionStart"`
MissionHash string `json:"missionHash"` MissionHash string `json:"missionHash"`
WorldName string `json:"worldName"`
} }
func writeMissionInfo(missionInfo string) { func writeMissionInfo(missionInfo string) {
functionName := "writeMissionInfo" functionName := "writeMissionInfo"
var err error
// writeLog(functionName, fmt.Sprintf(`["%s", "DEBUG"]`, missionInfo))
// missionInfo is json, parse it // missionInfo is json, parse it
var mi MissionInfo var mi MissionInfo
err := json.Unmarshal([]byte(missionInfo), &mi) fixedString := fixEscapeQuotes(trimQuotes(missionInfo))
err = json.Unmarshal([]byte(fixedString), &mi)
if err != nil { if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return return
} }
// get MySQL friendly datetime // check if mission exists based on hash
// first, convert string to int var worldID int
missionStartTime, err := strconv.ParseInt(mi.MissionStart, 10, 64) err = db.QueryRow("SELECT id FROM worlds WHERE world_name = ?", mi.WorldName).Scan(&worldID)
if err != nil { if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return return
} }
t := time.Unix(0, missionStartTime).Format("2006-01-02 15:04:05")
// write to log
writeLog(functionName, fmt.Sprintf(`["MissionName:%s BriefingName:%s MissionNameSource:%s OnLoadName:%s Author:%s ServerName:%s ServerProfile:%s MissionStart:%s MissionHash:%s", "INFO"]`, mi.MissionName, mi.BriefingName, mi.MissionNameSource, mi.OnLoadName, mi.Author, mi.ServerName, mi.ServerProfile, t, mi.MissionHash))
// write to database var stmt *sql.Stmt
// every mission is unique, so insert it var res sql.Result
stmt, err := db.Prepare("INSERT INTO missions (mission_name, briefing_name, mission_name_source, on_load_name, author, server_name, server_profile, mission_start, mission_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
if err != nil { if worldID != 0 {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) sqlWorld := fmt.Sprintf(
return "INSERT INTO %s (mission_name, briefing_name, mission_name_source, on_load_name, author, server_name, server_profile, mission_start, mission_hash, world_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
} MISSIONS_TABLE,
defer stmt.Close() )
res, err := stmt.Exec(mi.MissionName, mi.BriefingName, mi.MissionNameSource, mi.OnLoadName, mi.Author, mi.ServerName, mi.ServerProfile, t, mi.MissionStart, mi.MissionHash) stmt, err = db.Prepare(sqlWorld)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
defer stmt.Close()
res, err = stmt.Exec(mi.MissionName, mi.BriefingName, mi.MissionNameSource, mi.OnLoadName, mi.Author, mi.ServerName, mi.ServerProfile, mi.MissionStart, mi.MissionHash, worldID)
} else {
// if no world was found, write without it
sqlNoWorld := fmt.Sprintf(
"INSERT INTO %s (mission_name, briefing_name, mission_name_source, on_load_name, author, server_name, server_profile, mission_start, mission_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
MISSIONS_TABLE,
)
stmt, err = db.Prepare(sqlNoWorld)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
defer stmt.Close()
res, err = stmt.Exec(mi.MissionName, mi.BriefingName, mi.MissionNameSource, mi.OnLoadName, mi.Author, mi.ServerName, mi.ServerProfile, mi.MissionStart, mi.MissionHash)
}
if err != nil { if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return return
} }
lastID, err := res.LastInsertId() lastID, err := res.LastInsertId()
if err != nil { if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return return
} }
writeLog(functionName, fmt.Sprintf(`["Mission inserted with ID %d", "INFO"]`, lastID)) writeLog(functionName, fmt.Sprintf(`["Mission inserted with ID %d", "INFO"]`, lastID))
writeLog(functionName, fmt.Sprintf(`["MISSION_ID", "%d"]`, lastID))
} }
type AttendanceLogItem struct { type AttendanceLogItem struct {
@@ -299,17 +340,18 @@ type AttendanceLogItem struct {
func writeAttendance(data string) { func writeAttendance(data string) {
functionName := "writeAttendance" functionName := "writeAttendance"
var err error
// data is json, parse it // data is json, parse it
stringjson := fixEscapeQuotes(trimQuotes(data)) stringjson := fixEscapeQuotes(trimQuotes(data))
var event AttendanceLogItem var event AttendanceLogItem
err := json.Unmarshal([]byte(stringjson), &event) err = json.Unmarshal([]byte(stringjson), &event)
if err != nil { if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return return
} }
// get MySQL friendly NOW // get MySQL friendly NOW
now := time.Now().Format("2006-01-02 15:04:05") now := time.Now().UTC().Format("2006-01-02 15:04:05")
// prevent crash // prevent crash
if db == nil { if db == nil {
@@ -318,18 +360,44 @@ func writeAttendance(data string) {
} }
// send to DB // send to DB
var result sql.Result
result, err := db.ExecContext(context.Background(), `INSERT INTO AttendanceLog (event_time, event_type, player_id, player_uid, profile_name, steam_name, is_jip, role_description, mission_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, if event.EventType == "Server" {
now, sql := fmt.Sprintf(
event.EventType, `INSERT INTO %s (join_time, event_type, player_id, player_uid, profile_name, steam_name, is_jip, role_description) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
event.PlayerId, ATTENDANCE_TABLE,
event.PlayerUID, )
event.ProfileName, result, err = db.ExecContext(
event.SteamName, context.Background(),
event.IsJIP, sql,
event.RoleDescription, now,
event.MissionHash, event.EventType,
) event.PlayerId,
event.PlayerUID,
event.ProfileName,
event.SteamName,
event.IsJIP,
event.RoleDescription,
)
} else if event.EventType == "Mission" {
sql := fmt.Sprintf(
`INSERT INTO %s (join_time, event_type, player_id, player_uid, profile_name, steam_name, is_jip, role_description, mission_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
ATTENDANCE_TABLE,
)
result, err = db.ExecContext(
context.Background(),
sql,
now,
event.EventType,
event.PlayerId,
event.PlayerUID,
event.ProfileName,
event.SteamName,
event.IsJIP,
event.RoleDescription,
event.MissionHash,
)
}
if err != nil { if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
@@ -343,9 +411,124 @@ func writeAttendance(data string) {
} }
writeLog(functionName, fmt.Sprintf(`["Saved attendance for %s to row id %d", "INFO"]`, event.ProfileName, id)) writeLog(functionName, fmt.Sprintf(`["Saved attendance for %s to row id %d", "INFO"]`, event.ProfileName, id))
if event.EventType == "Server" {
writeLog(functionName, fmt.Sprintf(`["ATT_LOG", ["SERVER", "%s", "%d"]]`, event.PlayerId, id))
} else if event.EventType == "Mission" {
writeLog(functionName, fmt.Sprintf(`["ATT_LOG", ["MISSION", "%s", "%d"]]`, event.PlayerId, id))
}
} }
type DisconnectItem struct {
EventType string `json:"eventType"`
PlayerId string `json:"playerId"`
MissionHash string `json:"missionHash"`
}
func writeDisconnectEvent(data string) {
functionName := "writeDisconnectEvent"
// data is json, parse it
stringjson := fixEscapeQuotes(trimQuotes(data))
var event DisconnectItem
err := json.Unmarshal([]byte(stringjson), &event)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
// get MySQL friendly NOW
now := time.Now().UTC().Format("2006-01-02 15:04:05")
// prevent crash
if db == nil {
writeLog(functionName, `["db is nil", "ERROR"]`)
return
}
// first, check if a row exists for this player
var sql string
if event.EventType == "Mission" {
sql = fmt.Sprintf(
`
SELECT id FROM attendance
WHERE player_id = '%s' and event_type = '%s' and mission_hash = '%s' and disconnect_time IS NULL and join_time >= (NOW() - INTERVAL 24 hour)
ORDER BY join_time DESC
`,
event.PlayerId,
event.EventType,
event.MissionHash,
)
} else if event.EventType == "Server" {
sql = fmt.Sprintf(
`
SELECT id FROM attendance
WHERE player_id = '%s' and event_type = '%s' and disconnect_time IS NULL and join_time >= (NOW() - INTERVAL 24 hour)
ORDER BY join_time DESC
`,
event.PlayerId,
event.EventType,
)
} else {
writeLog(functionName, fmt.Sprintf(`["Unknown event type %s", "ERROR"]`, event.EventType))
return
}
rows, err := db.QueryContext(context.Background(), sql)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
defer rows.Close()
// if there is a row, update it
if rows.Next() {
// create interface to hold values
var rowId int64
err = rows.Scan(&rowId)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
// update the row
sql = fmt.Sprintf(
`UPDATE attendance SET disconnect_time = '%s' WHERE id = %d`,
now,
rowId,
)
_, err := db.ExecContext(context.Background(), sql)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
writeLog(functionName, fmt.Sprintf(`["Saved disconnect event for %s to row id %d", "INFO"]`, event.PlayerId, rowId))
} else {
// otherwise, log an error
writeLog(functionName, fmt.Sprintf(`["No row found for %s, %s", "ERROR"]`, event.PlayerId, event.EventType))
}
}
func fillLastMissionNull() {
functionName := "fillLastMissionNull"
// prevent crash
if db == nil {
writeLog(functionName, `["db is nil", "ERROR"]`)
return
}
sql := `call proc_filllastmissionnull`
_, err := db.ExecContext(context.Background(), sql)
if err != nil {
writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err))
return
}
writeLog(functionName, `["Filled mission event NULLs", "INFO"]`)
}
func runExtensionCallback(name *C.char, function *C.char, data *C.char) C.int { func runExtensionCallback(name *C.char, function *C.char, data *C.char) C.int {
return C.runExtensionCallback(extensionCallbackFnc, name, function, data) return C.runExtensionCallback(extensionCallbackFnc, name, function, data)
} }
@@ -374,12 +557,23 @@ func goRVExtensionArgs(output *C.char, outputsize C.size_t, input *C.char, argv
temp := fmt.Sprintf("Function: %s nb params: %d", C.GoString(input), argc) temp := fmt.Sprintf("Function: %s nb params: %d", C.GoString(input), argc)
switch C.GoString(input) { switch C.GoString(input) {
case "logAttendance": case "fillLastMissionNull":
{ // callExtension ["serverEvent", [_hash] call CBA_fnc_encodeJSON]; {
go fillLastMissionNull()
}
case "writeAttendance":
{ // callExtension ["logAttendance", [_hash] call CBA_fnc_encodeJSON]];
if argc == 1 { if argc == 1 {
go writeAttendance(out[0]) go writeAttendance(out[0])
} }
} }
case "writeDisconnectEvent":
{ // callExtension ["writeDisconnectEvent", [[_hash] call CBA_fnc_encodeJSON]];
if argc == 1 {
go writeDisconnectEvent(out[0])
}
}
case "logMission": case "logMission":
if argc == 1 { if argc == 1 {
go writeMissionInfo(out[0]) go writeMissionInfo(out[0])
@@ -388,12 +582,6 @@ func goRVExtensionArgs(output *C.char, outputsize C.size_t, input *C.char, argv
if argc == 1 { if argc == 1 {
go writeWorldInfo(out[0]) go writeWorldInfo(out[0])
} }
case "getMissionHash":
{
if argc == 1 {
go getMissionHash(out[0])
}
}
} }
// Return a result to Arma // Return a result to Arma
@@ -421,9 +609,10 @@ func callBackExample() {
} }
} }
func getTimestamp() int64 { func getTimestamp() string {
// get the current unix timestamp in nanoseconds // get the current unix timestamp in nanoseconds
return time.Now().UnixNano() // return time.Now().Local().Unix()
return time.Now().UTC().Format("2006-01-02 15:04:05")
} }
func trimQuotes(s string) string { func trimQuotes(s string) string {
@@ -445,6 +634,9 @@ func writeLog(functionName string, data string) {
defer C.free(unsafe.Pointer(statusParam)) defer C.free(unsafe.Pointer(statusParam))
runExtensionCallback(statusName, statusFunction, statusParam) runExtensionCallback(statusName, statusFunction, statusParam)
// get calling function & line
_, file, line, _ := runtime.Caller(1)
log.Printf(`%s:%d: %s`, path.Base(file), line, data)
log.Printf(`%s: %s`, functionName, data) log.Printf(`%s: %s`, functionName, data)
} }
@@ -461,12 +653,12 @@ func goRVExtension(output *C.char, outputsize C.size_t, input *C.char) {
case "getDir": case "getDir":
temp = getDir() temp = getDir()
case "getTimestamp": case "getTimestamp":
time := getTimestamp() temp = fmt.Sprintf(`["%s"]`, getTimestamp())
temp = fmt.Sprintf(`["%s"]`, strconv.FormatInt(time, 10))
case "connectDB": case "connectDB":
go connectDB() go connectDB()
temp = fmt.Sprintf(`["%s"]`, "Connecting to DB") temp = fmt.Sprintf(`["%s"]`, "Connecting to DB")
case "getMissionHash":
temp = fmt.Sprintf(`["%s"]`, getMissionHash())
default: default:
temp = fmt.Sprintf(`["%s"]`, "Unknown Function") temp = fmt.Sprintf(`["%s"]`, "Unknown Function")
} }

View File

@@ -1,7 +0,0 @@
freeExtension "AttendanceTracker";
"AttendanceTracker" callExtension "connectDB";
sleep 2;
"attendanceTracker" callExtension ["logAttendance", ["{""playerUID"": ""76561197991996737"", ""roleDescription"": ""NULL"", ""missionNameSource"": ""aaaltisaiatk"", ""eventType"": ""ConnectedMission"", ""briefingName"": ""aaaltisaiatk"", ""profileName"": ""IndigoFox"", ""serverName"": ""IndigoFox on DESKTOP-6B2U0AT"", ""steamName"": ""IndigoFox"", ""onLoadName"": ""NULL"", ""missionName"": ""aaaltisaiatk"", ""isJIP"": false, ""author"": ""IndigoFox"", ""missionStart"": ""1682549469590908300""}"]]
"attendanceTracker" callExtension ["logAttendance", ["{""playerUID"": ""76561197991996737"", ""roleDescription"": ""NULL"", ""missionNameSource"": ""aaaltisaiatk"", ""eventType"": ""ConnectedServer"", ""briefingName"": ""aaaltisaiatk"", ""profileName"": ""IndigoFox"", ""serverName"": ""IndigoFox on DESKTOP-6B2U0AT"", ""steamName"": ""IndigoFox"", ""onLoadName"": ""NULL"", ""missionName"": ""aaaltisaiatk"", ""isJIP"": false, ""author"": ""IndigoFox"", ""missionStart"": ""1682549469590908300""}"]]
sleep 15;
exit;