diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f74c5d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ + +*.log + +\@17thAttendanceTracker/config.json + +*.bak diff --git a/@17thAttendanceTracker/addons/AttendanceTracker.pbo b/@17thAttendanceTracker/addons/AttendanceTracker.pbo new file mode 100644 index 0000000..d860fd1 Binary files /dev/null and b/@17thAttendanceTracker/addons/AttendanceTracker.pbo differ diff --git a/@17thAttendanceTracker/addons/AttendanceTracker/config.cpp b/@17thAttendanceTracker/addons/AttendanceTracker/config.cpp new file mode 100644 index 0000000..3b93ee5 --- /dev/null +++ b/@17thAttendanceTracker/addons/AttendanceTracker/config.cpp @@ -0,0 +1,25 @@ +class CfgPatches { + class AttendanceTracker { + units[] = {}; + weapons[] = {}; + requiredVersion = 2.10; + requiredAddons[] = {}; + author[] = {"IndigoFox"}; + authorUrl = "http://example.com"; + }; +}; + +class CfgFunctions { + class attendanceTracker { + class functions { + file = "\AttendanceTracker\functions"; + class postInit {postInit = 1;}; + class eventHandlers {}; + class callbackHandler {postInit = 1;}; + class log {}; + class logMissionEvent {}; + class logServerEvent {}; + class timestamp {}; + }; + }; +}; \ No newline at end of file diff --git a/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_callbackHandler.sqf b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_callbackHandler.sqf new file mode 100644 index 0000000..bc8321c --- /dev/null +++ b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_callbackHandler.sqf @@ -0,0 +1,48 @@ +addMissionEventHandler ["ExtensionCallback", { + params ["_name", "_function", "_data"]; + if !(_name == "AttendanceTracker") exitWith {}; + + // Validate data param + if (isNil "_data") then {_data = ""}; + + if (_data isEqualTo "") exitWith { + [ + format ["Callback empty data: %1", _function], + "WARN" + ] call attendanceTracker_fnc_log; + false; + }; + + if (typeName _data != "STRING") exitWith { + [ + format ["Callback invalid data: %1: %2", _function, _data], + "WARN" + ] call attendanceTracker_fnc_log; + false; + }; + + // Parse response from string array + private "_response"; + try { + // diag_log format ["Raw callback: %1: %2", _function, _data]; + _response = parseSimpleArray _data; + } catch { + [ + format ["Callback invalid data: %1: %2: %3", _function, _data, _exception], + "WARN" + ] call attendanceTracker_fnc_log; + }; + + if (isNil "_response") exitWith {false}; + + switch (_function) do { + case "connectDB": { + systemChat format ["AttendanceTracker: %1", _response#0]; + [_response#0, _response#1, _function] call attendanceTracker_fnc_log; + }; + default { + _response call attendanceTracker_fnc_log; + }; + }; + true; +}]; \ No newline at end of file diff --git a/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_eventHandlers.sqf b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_eventHandlers.sqf new file mode 100644 index 0000000..7ac520e --- /dev/null +++ b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_eventHandlers.sqf @@ -0,0 +1,78 @@ +[ + ["OnUserConnected", { + params ["_networkId", "_clientStateNumber", "_clientState"]; + private _userInfo = (getUserInfo _networkId); + _userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"]; + if (_isHC) exitWith {}; + + [ + "ConnectedServer", + _playerUID, + _profileName, + _steamName + ] call attendanceTracker_fnc_logServerEvent; + + (AttendanceTracker getVariable ["allUsers", createHashMap]) set [_networkId, _userInfo]; + + [format ["(EventHandler) OnUserConnected fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log; + }], + ["OnUserDisconnected", { + params ["_networkId", "_clientStateNumber", "_clientState"]; + private _userInfo = (AttendanceTracker getVariable ["allUsers", createHashMap]) get [_networkId, nil]; + if (isNil "_userInfo") exitWith {}; + + _userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"]; + if (_isHC) exitWith {}; + + [ + "DisconnectedServer", + _playerUID, + _profileName, + _steamName + ] call attendanceTracker_fnc_logServerEvent; + + [format ["(EventHandler) OnUserDisconnected fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log; + }], + ["PlayerConnected", { + params ["_id", "_uid", "_name", "_jip", "_owner", "_idstr"]; + 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", + _playerUID, + _profileName, + _steamName, + _jip, + roleDescription _unit + ] call attendanceTracker_fnc_logMissionEvent; + + [format ["(EventHandler) PlayerConnected fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log; + }], + ["HandleDisconnect", { + params ["_unit", "_id", "_uid", "_name"]; + private _userInfo = (AttendanceTracker getVariable ["allUsers", createHashMap]) get [_id toFixed 0, nil]; + if (isNil "_userInfo") exitWith {}; + + _userInfo params ["_playerID", "_ownerId", "_playerUID", "_profileName", "_displayName", "_steamName", "_clientState", "_isHC", "_adminState", "_networkInfo", "_unit"]; + + if (_isHC) exitWith {}; + + [ + "DisconnectedMission", + _playerUID, + _profileName, + _steamName, + _jip + ] call attendanceTracker_fnc_logMissionEvent; + + [format ["(EventHandler) HandleDisconnect fired: %1", _this], "DEBUG"] call attendanceTracker_fnc_log; + false; + }] +]; \ No newline at end of file diff --git a/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_log.sqf b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_log.sqf new file mode 100644 index 0000000..307b019 --- /dev/null +++ b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_log.sqf @@ -0,0 +1,17 @@ +params [ + ["_message", "", [""]], + ["_level", "INFO", [""]], + "_function" +]; + +if (isNil "_message") exitWith {false}; + +"AttendanceTracker" callExtension ["log", [_level, _message]]; + +if (!isNil "_function") then { + diag_log formatText["[AttendanceTracker] (%1): <%2> %3", _level, _function, _message]; +} else { + diag_log formatText["[AttendanceTracker] (%1): %2", _level, _message]; +}; + +true; \ No newline at end of file diff --git a/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_logMissionEvent.sqf b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_logMissionEvent.sqf new file mode 100644 index 0000000..4967873 --- /dev/null +++ b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_logMissionEvent.sqf @@ -0,0 +1,20 @@ +params [ + ["_eventType", ""], + ["_playerUID", ""], + ["_profileName", ""], + ["_steamName", ""], + ["_isJIP", false, [true, false]], + ["_roleDescription", ""] +]; + +private _hash = + (AttendanceTracker getVariable ["missionContext", createHashMap]); +_hash set ["eventType", _eventType]; +_hash set ["playerUID", _playerUID]; +_hash set ["profileName", _profileName]; +_hash set ["steamName", _steamName]; +_hash set ["isJIP", _isJIP]; +_hash set ["roleDescription", _roleDescription]; + +"AttendanceTracker" callExtension ["logAttendance", [[_hash] call CBA_fnc_encodeJSON]]; + +true; \ No newline at end of file diff --git a/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_logServerEvent.sqf b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_logServerEvent.sqf new file mode 100644 index 0000000..cbadacb --- /dev/null +++ b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_logServerEvent.sqf @@ -0,0 +1,19 @@ +params [ + ["_eventType", ""], + ["_playerUID", ""], + ["_profileName", ""], + ["_steamName", ""] +]; + + +private _hash = + (AttendanceTracker getVariable ["missionContext", createHashMap]); +_hash set ["eventType", _eventType]; +_hash set ["playerUID", _playerUID]; +_hash set ["profileName", _profileName]; +_hash set ["steamName", _steamName]; +_hash set ["isJIP", ""]; +_hash set ["roleDescription", ""]; + +"AttendanceTracker" callExtension ["logAttendance", [[_hash] call CBA_fnc_encodeJSON]]; + +true; \ No newline at end of file diff --git a/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_postInit.sqf b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_postInit.sqf new file mode 100644 index 0000000..baca6da --- /dev/null +++ b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_postInit.sqf @@ -0,0 +1,37 @@ + +AttendanceTracker = false call CBA_fnc_createNamespace; + +AttendanceTracker setVariable ["missionContext", createHashMapFromArray [ + ["missionName", missionName], + ["briefingName", briefingName], + ["missionNameSource", missionNameSource], + ["onLoadName", getMissionConfigValue ["onLoadName", ""]], + ["author", getMissionConfigValue ["author", ""]], + ["serverName", serverName], + ["serverProfile", profileName], + ["missionStart", call attendanceTracker_fnc_timestamp] +]]; + +// store all user details in a hash when they connect so we can reference it in disconnect events +AttendanceTracker setVariable ["allUsers", createHashMap]; + +private _database = "AttendanceTracker" callExtension "connectDB"; +systemChat "AttendanceTracker: Connecting to database..."; +["Connecting to database...", "INFO"] call attendanceTracker_fnc_log; + +{ + if (!isServer) exitWith {}; + _x params ["_ehName", "_code"]; + + _handle = (addMissionEventHandler [_ehName, _code]); + if (isNil "_handle") then { + [format["Failed to add Mission event handler: %1", _x], "ERROR"] call attendanceTracker_fnc_log; + false; + } else { + missionNamespace setVariable [ + ("AttendanceTracker" + "_MEH_" + _ehName), + _handle + ]; + true; + }; +} forEach (call attendanceTracker_fnc_eventHandlers); \ No newline at end of file diff --git a/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_timestamp.sqf b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_timestamp.sqf new file mode 100644 index 0000000..19f8fb3 --- /dev/null +++ b/@17thAttendanceTracker/addons/AttendanceTracker/functions/fn_timestamp.sqf @@ -0,0 +1 @@ +(parseSimpleArray ("AttendanceTracker" callExtension "getTimestamp")) select 0; \ No newline at end of file diff --git a/@17thAttendanceTracker/config.example.json b/@17thAttendanceTracker/config.example.json new file mode 100644 index 0000000..a9b32eb --- /dev/null +++ b/@17thAttendanceTracker/config.example.json @@ -0,0 +1,7 @@ +{ + "mysqlHost": "127.0.0.1", + "mysqlPort": 3306, + "mysqlUser": "root", + "mysqlPassword": "root", + "mysqlDatabase": "db" +} \ No newline at end of file diff --git a/@17thAttendanceTracker/mod.cpp b/@17thAttendanceTracker/mod.cpp new file mode 100644 index 0000000..e69de29 diff --git a/extension/@17thAttendanceTracker/config.example.json b/extension/@17thAttendanceTracker/config.example.json new file mode 100644 index 0000000..a9b32eb --- /dev/null +++ b/extension/@17thAttendanceTracker/config.example.json @@ -0,0 +1,7 @@ +{ + "mysqlHost": "127.0.0.1", + "mysqlPort": 3306, + "mysqlUser": "root", + "mysqlPassword": "root", + "mysqlDatabase": "db" +} \ No newline at end of file diff --git a/extension/AttendanceTracker_x64.dll b/extension/AttendanceTracker_x64.dll new file mode 100644 index 0000000..4620634 Binary files /dev/null and b/extension/AttendanceTracker_x64.dll differ diff --git a/extension/AttendanceTracker_x64.h b/extension/AttendanceTracker_x64.h new file mode 100644 index 0000000..0282d20 --- /dev/null +++ b/extension/AttendanceTracker_x64.h @@ -0,0 +1,86 @@ +/* Code generated by cmd/cgo; DO NOT EDIT. */ + +/* package main.go */ + + +#line 1 "cgo-builtin-export-prolog" + +#include /* for ptrdiff_t below */ + +#ifndef GO_CGO_EXPORT_PROLOGUE_H +#define GO_CGO_EXPORT_PROLOGUE_H + +#ifndef GO_CGO_GOSTRING_TYPEDEF +typedef struct { const char *p; ptrdiff_t n; } _GoString_; +#endif + +#endif + +/* Start of preamble from import "C" comments. */ + + +#line 3 "main.go" + +#include +#include +#include +#include "extensionCallback.h" + +#line 1 "cgo-generated-wrapper" + + +/* End of preamble from import "C" comments. */ + + +/* Start of boilerplate cgo prologue. */ +#line 1 "cgo-gcc-export-header-prolog" + +#ifndef GO_CGO_PROLOGUE_H +#define GO_CGO_PROLOGUE_H + +typedef signed char GoInt8; +typedef unsigned char GoUint8; +typedef short GoInt16; +typedef unsigned short GoUint16; +typedef int GoInt32; +typedef unsigned int GoUint32; +typedef long long GoInt64; +typedef unsigned long long GoUint64; +typedef GoInt64 GoInt; +typedef GoUint64 GoUint; +typedef __SIZE_TYPE__ GoUintptr; +typedef float GoFloat32; +typedef double GoFloat64; +typedef float _Complex GoComplex64; +typedef double _Complex GoComplex128; + +/* + static assertion to make sure the file is being used on architecture + at least with matching size of GoInt. +*/ +typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1]; + +#ifndef GO_CGO_GOSTRING_TYPEDEF +typedef _GoString_ GoString; +#endif +typedef void *GoMap; +typedef void *GoChan; +typedef struct { void *t; void *v; } GoInterface; +typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; + +#endif + +/* End of boilerplate cgo prologue. */ + +#ifdef __cplusplus +extern "C" { +#endif + +extern __declspec(dllexport) void goRVExtensionVersion(char* output, size_t outputsize); +extern __declspec(dllexport) void goRVExtensionArgs(char* output, size_t outputsize, char* input, char** argv, int argc); +extern __declspec(dllexport) void goRVExtension(char* output, size_t outputsize, char* input); +extern __declspec(dllexport) void goRVExtensionRegisterCallback(extensionCallback fnc); + +#ifdef __cplusplus +} +#endif diff --git a/extension/RVExtension.c b/extension/RVExtension.c new file mode 100644 index 0000000..70fb2a6 --- /dev/null +++ b/extension/RVExtension.c @@ -0,0 +1,61 @@ +#include + +#include "extensionCallback.h" + +extern void goRVExtension(char *output, size_t outputSize, char *input); +extern void goRVExtensionVersion(char *output, size_t outputSize); +extern void goRVExtensionArgs(char *output, size_t outputSize, char *input, char **argv, int argc); +extern void goRVExtensionRegisterCallback(extensionCallback fnc); + +#ifdef WIN64 +__declspec(dllexport) void RVExtension(char *output, size_t outputSize, char *input) +{ + goRVExtension(output, outputSize, input); +} + +__declspec(dllexport) void RVExtensionVersion(char *output, size_t outputSize) +{ + goRVExtensionVersion(output, outputSize); +} + +__declspec(dllexport) void RVExtensionArgs(char *output, size_t outputSize, char *input, char **argv, int argc) +{ + goRVExtensionArgs(output, outputSize, input, argv, argc); +} + +__declspec(dllexport) void RVExtensionRegisterCallback(extensionCallback fnc) +{ + goRVExtensionRegisterCallback(fnc); +} +#else +__declspec(dllexport) void __stdcall _RVExtension(char *output, size_t outputSize, char *input) +{ + goRVExtension(output, outputSize, input); +} + +__declspec(dllexport) void __stdcall _RVExtensionVersion(char *output, size_t outputSize) +{ + goRVExtensionVersion(output, outputSize); +} + +__declspec(dllexport) void __stdcall _RVExtensionArgs(char *output, size_t outputSize, char *input, char **argv, int argc) +{ + goRVExtensionArgs(output, outputSize, input, argv, argc); +} + +__declspec(dllexport) void __stdcall _RVExtensionRegisterCallback(extensionCallback fnc) +{ + goRVExtensionRegisterCallback(fnc); +} +#endif +// do this for all the other exported functions + +// dll entrypoint +// Path: RVExtension.c + +#include + +BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) +{ + return TRUE; +} diff --git a/extension/build.txt b/extension/build.txt new file mode 100644 index 0000000..0be8802 --- /dev/null +++ b/extension/build.txt @@ -0,0 +1,3 @@ +$ENV:GOARCH = "amd64" +$ENV:CGO_ENABLED = 1 +go1.16.4 build -o AttendanceTracker_x64.dll -buildmode=c-shared . \ No newline at end of file diff --git a/extension/callExtension.exe b/extension/callExtension.exe new file mode 100644 index 0000000..a4b9e74 Binary files /dev/null and b/extension/callExtension.exe differ diff --git a/extension/callExtension_x64.exe b/extension/callExtension_x64.exe new file mode 100644 index 0000000..f2c652c Binary files /dev/null and b/extension/callExtension_x64.exe differ diff --git a/extension/extensionCallback.h b/extension/extensionCallback.h new file mode 100644 index 0000000..1381732 --- /dev/null +++ b/extension/extensionCallback.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +typedef int (*extensionCallback)(char const *name, char const *function, char const *data); + +/* https://golang.org/cmd/cgo/#hdr-C_references_to_Go */ +static inline int runExtensionCallback(extensionCallback fnc, char const *name, char const *function, char const *data) +{ + return fnc(name, function, data); +} \ No newline at end of file diff --git a/extension/go.mod b/extension/go.mod new file mode 100644 index 0000000..db66596 --- /dev/null +++ b/extension/go.mod @@ -0,0 +1,5 @@ +module main.go + +go 1.16 + +require github.com/go-sql-driver/mysql v1.6.0 diff --git a/extension/go.sum b/extension/go.sum new file mode 100644 index 0000000..20c16d6 --- /dev/null +++ b/extension/go.sum @@ -0,0 +1,2 @@ +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= diff --git a/extension/main.go b/extension/main.go new file mode 100644 index 0000000..c5b3da6 --- /dev/null +++ b/extension/main.go @@ -0,0 +1,341 @@ +package main + +/* +#include +#include +#include +#include "extensionCallback.h" +*/ +import "C" // This is required to import the C code + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "log" + "os" + "strconv" + "strings" + "time" + "unsafe" + + _ "github.com/go-sql-driver/mysql" +) + +var EXTENSION_VERSION string = "0.0.1" +var extensionCallbackFnc C.extensionCallback + +// file paths +var ADDON_FOLDER string = getDir() + "\\@17thAttendanceTracker" +var LOG_FILE string = ADDON_FOLDER + "\\attendanceTracker.log" +var CONFIG_FILE string = ADDON_FOLDER + "\\config.json" + +var ATConfig AttendanceTrackerConfig + +type AttendanceTrackerConfig struct { + MySQLHost string `json:"mysqlHost"` + MySQLPort int `json:"mysqlPort"` + MySQLUser string `json:"mysqlUser"` + MySQLPassword string `json:"mysqlPassword"` + MySQLDatabase string `json:"mysqlDatabase"` +} + +// database connection +var db *sql.DB + +// configure log output +func init() { + log.SetFlags(log.LstdFlags | log.Lshortfile) + // log to file + f, err := os.OpenFile(LOG_FILE, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + log.Fatalf("error opening file: %v", err) + } + // log to console as well + // log.SetOutput(io.MultiWriter(f, os.Stdout)) + // log only to file + log.SetOutput(f) +} + +func version() { + functionName := "version" + writeLog(functionName, fmt.Sprintf(`["AttendanceTracker Extension Version:%s", "INFO"]`, EXTENSION_VERSION)) +} + +func getDir() string { + dir, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + return dir +} + +func loadConfig() { + // load config from file as JSON + functionName := "loadConfig" + file, err := os.Open(CONFIG_FILE) + if err != nil { + writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) + return + } + defer file.Close() + + decoder := json.NewDecoder(file) + err = decoder.Decode(&ATConfig) + if err != nil { + writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) + return + } + writeLog(functionName, `["Config loaded", "INFO"]`) +} + +func connectDB() string { + functionName := "connectDB" + var err error + + // load config + loadConfig() + + // connect to database + connectionString := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", ATConfig.MySQLUser, ATConfig.MySQLPassword, ATConfig.MySQLHost, ATConfig.MySQLPort, ATConfig.MySQLDatabase) + + db, err = sql.Open("mysql", connectionString) + if err != nil { + writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) + return "ERROR" + } + if db == nil { + writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, "db is nil")) + return "ERROR" + } + // defer db.Close() + + db.SetConnMaxLifetime(time.Minute * 3) + db.SetMaxOpenConns(10) + db.SetMaxIdleConns(10) + + pingErr := db.Ping() + if pingErr != nil { + writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, pingErr)) + return "ERROR" + } + + // Connect and check the server version + var version string + err = db.QueryRow("SELECT VERSION()").Scan(&version) + if err != nil { + writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) + return "ERROR" + } + writeLog(functionName, fmt.Sprintf(`["Connected to MySQL/MariaDB version %s", "INFO"]`, version)) + return version +} + +type AttendanceLogItem struct { + MissionName string `json:"missionName"` + BriefingName string `json:"briefingName"` + MissionNameSource string `json:"missionNameSource"` + OnLoadName string `json:"onLoadName"` + Author string `json:"author"` + ServerName string `json:"serverName"` + ServerProfile string `json:"serverProfile"` + MissionStart string `json:"missionStart"` + // situational + EventType string `json:"eventType"` + PlayerUID string `json:"playerUID"` + ProfileName string `json:"profileName"` + SteamName string `json:"steamName"` + IsJIP bool `json:"isJIP"` + RoleDescription string `json:"roleDescription"` +} + +func writeAttendance(data string) { + functionName := "writeAttendance" + // data is json, parse it + stringjson := fixEscapeQuotes(trimQuotes(data)) + var event AttendanceLogItem + err := json.Unmarshal([]byte(stringjson), &event) + if err != nil { + writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) + return + } + + // get MySQL friendly datetime + // first, convert string to int + missionStartTime, err := strconv.ParseInt(event.MissionStart, 10, 64) + if err != nil { + writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) + return + } + t := time.Unix(0, missionStartTime).Format("2006-01-02 15:04:05") + // get MySQL friendly NOW + now := time.Now().Format("2006-01-02 15:04:05") + + // prevent crash + if db == nil { + writeLog(functionName, `["db is nil", "ERROR"]`) + return + } + + // send to DB + + result, err := db.ExecContext(context.Background(), `INSERT INTO AttendanceLog (timestamp, mission_name, briefing_name, mission_name_source, on_load_name, author, server_name, server_profile, mission_start, event_type, player_uid, profile_name, steam_name, is_jip, role_description) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, + now, + event.MissionName, + event.BriefingName, + event.MissionNameSource, + event.OnLoadName, + event.Author, + event.ServerName, + event.ServerProfile, + t, + event.EventType, + event.PlayerUID, + event.ProfileName, + event.SteamName, + event.IsJIP, + event.RoleDescription, + ) + + if err != nil { + writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) + return + } + + id, err := result.LastInsertId() + if err != nil { + writeLog(functionName, fmt.Sprintf(`["%s", "ERROR"]`, err)) + return + } + + writeLog(functionName, fmt.Sprintf(`["Saved attendance for %s to row id %d", "INFO"]`, event.ProfileName, id)) + +} + +func runExtensionCallback(name *C.char, function *C.char, data *C.char) C.int { + return C.runExtensionCallback(extensionCallbackFnc, name, function, data) +} + +//export goRVExtensionVersion +func goRVExtensionVersion(output *C.char, outputsize C.size_t) { + result := C.CString(EXTENSION_VERSION) + defer C.free(unsafe.Pointer(result)) + var size = C.strlen(result) + 1 + if size > outputsize { + size = outputsize + } + C.memmove(unsafe.Pointer(output), unsafe.Pointer(result), size) +} + +//export goRVExtensionArgs +func goRVExtensionArgs(output *C.char, outputsize C.size_t, input *C.char, argv **C.char, argc C.int) { + var offset = unsafe.Sizeof(uintptr(0)) + var out []string + for index := C.int(0); index < argc; index++ { + out = append(out, C.GoString(*argv)) + argv = (**C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(argv)) + offset)) + } + + // temp := fmt.Sprintf("Function: %s nb params: %d params: %s!", C.GoString(input), argc, out) + temp := fmt.Sprintf("Function: %s nb params: %d", C.GoString(input), argc) + + switch C.GoString(input) { + case "logAttendance": // callExtension ["serverEvent", [_hash] call CBA_fnc_encodeJSON]; + if argc == 1 { + go writeAttendance(out[0]) + } + } + + // Return a result to Arma + result := C.CString(temp) + defer C.free(unsafe.Pointer(result)) + var size = C.strlen(result) + 1 + if size > outputsize { + size = outputsize + } + + C.memmove(unsafe.Pointer(output), unsafe.Pointer(result), size) +} + +func callBackExample() { + name := C.CString("arma") + defer C.free(unsafe.Pointer(name)) + function := C.CString("funcToExecute") + defer C.free(unsafe.Pointer(function)) + // Make a callback to Arma + for i := 0; i < 3; i++ { + time.Sleep(2 * time.Second) + param := C.CString(fmt.Sprintf("Loop: %d", i)) + defer C.free(unsafe.Pointer(param)) + runExtensionCallback(name, function, param) + } +} + +func getTimestamp() int64 { + // get the current unix timestamp in nanoseconds + return time.Now().UnixNano() +} + +func trimQuotes(s string) string { + // trim the start and end quotes from a string + return strings.Trim(s, `"`) +} + +func fixEscapeQuotes(s string) string { + // fix the escape quotes in a string + return strings.Replace(s, `""`, `"`, -1) +} + +func writeLog(functionName string, data string) { + statusName := C.CString("AttendanceTracker") + defer C.free(unsafe.Pointer(statusName)) + statusFunction := C.CString(functionName) + defer C.free(unsafe.Pointer(statusFunction)) + statusParam := C.CString(data) + defer C.free(unsafe.Pointer(statusParam)) + runExtensionCallback(statusName, statusFunction, statusParam) + + log.Printf(`%s: %s`, functionName, data) +} + +//export goRVExtension +func goRVExtension(output *C.char, outputsize C.size_t, input *C.char) { + + var temp string + + // logLine("goRVExtension", fmt.Sprintf(`["Input: %s", "DEBUG"]`, C.GoString(input)), true) + + switch C.GoString(input) { + case "version": + temp = EXTENSION_VERSION + case "getDir": + temp = getDir() + case "getTimestamp": + time := getTimestamp() + temp = fmt.Sprintf(`["%s"]`, strconv.FormatInt(time, 10)) + case "connectDB": + go connectDB() + temp = fmt.Sprintf(`["%s"]`, "Connecting to DB") + + default: + temp = fmt.Sprintf(`["%s"]`, "Unknown Function") + } + + result := C.CString(temp) + defer C.free(unsafe.Pointer(result)) + var size = C.strlen(result) + 1 + if size > outputsize { + size = outputsize + } + + C.memmove(unsafe.Pointer(output), unsafe.Pointer(result), size) + // return +} + +//export goRVExtensionRegisterCallback +func goRVExtensionRegisterCallback(fnc C.extensionCallback) { + extensionCallbackFnc = fnc +} + +func main() {} diff --git a/extension/test.sqf b/extension/test.sqf new file mode 100644 index 0000000..3d218d3 --- /dev/null +++ b/extension/test.sqf @@ -0,0 +1,7 @@ +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; \ No newline at end of file