using System.Runtime.InteropServices; using System.Text; using System.Xml.Linq; #pragma warning disable IDE0130 // Namespace does not match folder structure namespace ArmaDragonflyClient #pragma warning restore IDE0130 // Namespace does not match folder structure { public class CallContext { public ulong SteamId { get; set; } public string FileSource { get; set; } public string MissionName { get; set; } public string ServerName { get; set; } public short RemoteExecutedOwner { get; set; } } public class Main { private const string ADC_VERSION = "1.0.0"; public const int ADC_BUFFERSIZE = 10240; public static string ADC_HOST {get; private set; } = "127.0.0.1"; public static int ADC_PORT {get; private set; } = 6379; public static string ADC_PASSWORD {get; private set; } = "xyz123"; public static string ADC_LOGFOLDER {get; private set; } = $"{Path.DirectorySeparatorChar}@dragonfly{Path.DirectorySeparatorChar}logs"; public static bool ADC_DEBUG {get; private set; } = false; public static bool ADC_INITCHECK {get; private set; } = false; public static string STEAMID {get; private set; } = ""; public static bool ADC_CONTEXTLOG {get; private set; } = false; public static IntPtr ADC_CONTEXT { get; private set; } private static int numCalls = 0; public static ExtensionCallback Callback { get; private set; } public delegate int ExtensionCallback([MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string function, [MarshalAs(UnmanagedType.LPStr)] string data); public static void ADC_Init() { char[] separator = ['=']; string[] commandLineArgs = Environment.GetCommandLineArgs(); string str = ""; for (int index = 0; index < commandLineArgs.Length; index++) { string[] strArray = commandLineArgs[index].Split(separator, StringSplitOptions.RemoveEmptyEntries); if (strArray[0].Equals("-adc", StringComparison.CurrentCultureIgnoreCase)) { str = strArray[1]; break; } } if (str == "") str = $"{Path.DirectorySeparatorChar}@dragonfly{Path.DirectorySeparatorChar}config.xml"; if (File.Exists(Environment.CurrentDirectory + $"{Path.DirectorySeparatorChar}{str}")) { List strList = []; List list = [.. XElement.Load(Environment.CurrentDirectory + $"{Path.DirectorySeparatorChar}{str}").Elements().Select(eintrag => (string)eintrag)]; ADC_HOST = list[0]; ADC_PORT = Convert.ToInt32(list[1]); ADC_PASSWORD = list[2]; if (bool.TryParse(list[3], out bool res1)) ADC_CONTEXTLOG = res1; if (bool.TryParse(list[4], out bool res2)) ADC_DEBUG = res2; Log($"Config file found! Context Mode: {ADC_CONTEXTLOG}! Debug Mode: {ADC_DEBUG}! Changed Server Settings to: {ADC_HOST}:{ADC_PORT}:{ADC_PASSWORD}!", "action"); } else { Log("Config file not found! Default Settings Loaded.", "action"); } ADC_INITCHECK = true; } public static void Log(string msg, string logType) { if (!ADC_DEBUG) return; string logFileName = logType + ".log"; string path = Environment.CurrentDirectory + ADC_LOGFOLDER; if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } using (StreamWriter streamWriter = new(Path.Combine(path, logFileName), true)) { streamWriter.WriteLine(DateTime.Now.ToString() + " >> " + msg); }; } /// /// Called only once when Arma 3 loads the extension. /// /// Pointer to Arma 3's callback function [UnmanagedCallersOnly(EntryPoint = "RVExtensionRegisterCallback")] public unsafe static void RVExtensionRegisterCallback(nint func) { Callback = (Marshal.GetDelegateForFunctionPointer(func)); } /// /// Called only once when Arma 3 loads the extension. /// The output will be written in the RPT logs. /// /// A pointer to the output buffer /// The maximum length of the buffer (always 32 for this particular method) [UnmanagedCallersOnly(EntryPoint = "RVExtensionVersion")] #pragma warning disable IDE0060 // Remove unused parameter public unsafe static void RVExtensionVersion(char* output, int outputSize) #pragma warning restore IDE0060 // Remove unused parameter { WriteOutput(output, ADC_VERSION); } /// /// Called before every RVExtension/RVExtensionArgs execution, providing context about where the call came from. /// /// /// Array of pointers to null-terminated strings containing: /// [0] - Steam 64bit UserID /// [1] - FileSource /// [2] - MissionName /// [3] - ServerName /// [4] - Machine Network ID /// /// Number of arguments in the argv array (always 5). [UnmanagedCallersOnly(EntryPoint = "RVExtensionContext")] public unsafe static int RVExtensionContext(char** argv, int argc) { ADC_CONTEXT = (IntPtr)argv; STEAMID = Marshal.PtrToStringAnsi((IntPtr)argv[0]); string steamID = Marshal.PtrToStringAnsi((IntPtr)argv[0]); string fileSource = Marshal.PtrToStringAnsi((IntPtr)argv[1]); string missionName = Marshal.PtrToStringAnsi((IntPtr)argv[2]); string serverName = Marshal.PtrToStringAnsi((IntPtr)argv[3]); short remoteExecutedOwner = short.Parse(Marshal.PtrToStringAnsi((IntPtr)argv[4])); if (ADC_CONTEXTLOG) { Log($"SteamID: {steamID}", "context"); Log($"FileSource: {fileSource}", "context"); Log($"MissionName: {missionName}", "context"); Log($"ServerName: {serverName}", "context"); Log($"RemoteExecutedOwner: {remoteExecutedOwner}", "context"); } return 100; } /// /// The entry point for the default "callExtension" command. /// /// A pointer to the output buffer /// The maximum size of the buffer (20480 bytes) /// The function identifier passed in "callExtension" [UnmanagedCallersOnly(EntryPoint = "RVExtension")] #pragma warning disable IDE0060 // Remove unused parameter public unsafe static void RVExtension(char* output, int outputSize, char* function) #pragma warning restore IDE0060 // Remove unused parameter { if (!ADC_INITCHECK) ADC_Init(); numCalls++; var func = GetString(function); switch (func.ToLower()) { case "time": DateTime timeNow = DateTime.Now; string formattedTime = timeNow.ToString("dd:MM:yyyy HH:mm:ss"); WriteOutput(output, formattedTime); break; default: WriteOutput(output, func); break; } } /// /// The entry point for the "callExtension" command with function arguments. /// /// A pointer to the output buffer /// The maximum size of the buffer (20480 bytes) /// The function identifier passed in "callExtension" /// The args passed to "callExtension" as a string array /// Number of elements in "argv" /// The return code [UnmanagedCallersOnly(EntryPoint = "RVExtensionArgs")] public unsafe static int RVExtensionArgs(char* output, int outputSize, char* function, char** argv, int argc) { if (!ADC_INITCHECK) ADC_Init(); numCalls++; var func = GetString(function); long _id = Utils.GetUniqueId(); List argsArr = []; for (int i = 0; i < argc; i++) { argsArr.Add(GetString(argv[i])); } switch (func.ToLower()) { case "get": HandleGetOperation(_id, argsArr, argc); WriteOutput(output, $"[\"{_id}_get\",{argsArr[1]}]"); return 100; case "set": HandleSetOperation(argsArr); WriteOutput(output, "Async"); return 200; case "del": HandleDelOperation(argsArr); WriteOutput(output, "Async"); return 200; case "listadd": HandleListAddOperation(argsArr, argc); WriteOutput(output, "Async"); return 200; case "listidx": HandleListIdxOperation(_id, argsArr, argc); WriteOutput(output, $"[\"{_id}_listidx\",{argsArr[2]}]"); return 100; case "listlen": HandleListLenOperation(_id, argsArr); WriteOutput(output, $"[\"{_id}_listlen\"]"); return 100; case "listrem": HandleListRemOperation(argsArr); WriteOutput(output, "Async"); return 200; case "listrng": HandleListRngOperation(_id, argsArr, argc); WriteOutput(output, $"[\"{_id}_listrng\",{argsArr[3]}]"); return 100; case "listset": HandleListSetOperation(argsArr); WriteOutput(output, "Async"); return 200; case "hset": HandleHSetOperation(argsArr, argc); WriteOutput(output, "Async"); return 200; case "hsetid": HandleHSetIdOperation(argsArr, argc); WriteOutput(output, "Async"); return 200; case "hget": HandleHGetOperation(_id, argsArr, argc); WriteOutput(output, $"[\"{_id}_hget\",{argsArr[1]}]"); return 100; case "hgetid": HandleHGetIdOperation(_id, argsArr, argc); WriteOutput(output, $"[\"{_id}_hgetid\",{argsArr[2]}]"); return 100; case "hgetall": HandleHGetAllOperation(_id, argsArr, argc); WriteOutput(output, $"[\"{_id}_hgetall\",{argsArr[0]}]"); return 100; case "hgetallid": HandleHGetAllIdOperation(_id, argsArr, argc); WriteOutput(output, $"[\"{_id}_hgetallid\",{argsArr[1]}]"); return 100; case "hdel": HandleHDelOperation(argsArr); WriteOutput(output, "Async"); return 200; case "hdelid": HandleHDelIdOperation(argsArr); WriteOutput(output, "Async"); return 200; case "publish": HandlePublishOperation(_id, argsArr); WriteOutput(output, $"[\"{_id}_publish\"]"); return 100; case "subscribe": HandleSubscribeOperation(_id, argsArr, argc); WriteOutput(output, $"[\"{_id}_subscribe\"]"); return 100; case "savedb": HandleSaveDBOperation(); WriteOutput(output, "Async"); return 200; case "setup": HandleSetupOperation(argsArr, argc); WriteOutput(output, "Connection settings updated"); return 100; case "version": WriteOutput(output, ADC_VERSION); return 100; default: WriteOutput(output, "Available Functions: get, set, del, listadd, listidx, listlen, listrem, listrng, listset, hset, hsetid, hget, hgetid, hgetall, hgetallid, hdel, hdelid, publish, subscribe, savedb, setup, version"); return -1; } } /// /// Reads a string from the given pointer. /// If the pointer is null, a default value will be returned instead. /// /// The pointer to read /// The string's default value /// The marshalled string private unsafe static string GetString(char* pointer, string defaultValue = "") { return Marshal.PtrToStringAnsi((nint)pointer) ?? defaultValue; } /// /// Serializes a list of strings into a string representing a valid Arma 3 array. /// /// The list of strings to serialize /// A string representing an Arma 3 array private static string SerializeList(List list) { var content = string.Join(",", [.. list]); return string.Format("[{0}]", content); } /// /// Writes the given payload to the output buffer using the provided pointer. /// Ensures that the payload string is always null terminated. /// /// A pointer to the output buffer /// The payload to write private unsafe static void WriteOutput(char* output, string payload) { byte[] bytes = Encoding.ASCII.GetBytes(payload + '\0'); Marshal.Copy(bytes, 0, (nint)output, bytes.Length); } private static void HandleGetOperation(long _id, List argsArr, int argsCnt) { Task.Run(async () => { string _uniqueID = $"{_id}_get"; switch (argsCnt) { case 2: await DragonflyDB.DragonflyGetAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), _uniqueID); break; case 3: await DragonflyDB.DragonflyGetAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), _uniqueID, argsArr[2].Trim('"')); break; case 4: await DragonflyDB.DragonflyGetAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), _uniqueID, argsArr[2].Trim('"'), argsArr[3].Trim('"')); break; } }); } private static void HandleSetOperation(List argsArr) { Task.Run(async () => { await DragonflyDB.DragonflySetAsync(argsArr[0].Trim('"'), argsArr[1]); }); } private static void HandleDelOperation(List argsArr) { Task.Run(async () => { await DragonflyDB.DragonflyDeleteAsync(argsArr[0].Trim('"')); }); } private static void HandleListAddOperation(List argsArr, int argsCnt) { Task.Run(async () => { switch (argsCnt) { case > 2: for (var i = 0; i < argsCnt; i++) { await DragonflyDB.DragonflyListAddAsync(argsArr[0].Trim('"'), argsArr[i]); } break; default: await DragonflyDB.DragonflyListAddAsync(argsArr[0].Trim('"'), argsArr[1]); break; } }); } private static void HandleListIdxOperation(long _id, List argsArr, int argsCnt) { Task.Run(async () => { string _uniqueID = $"{_id}_listidx"; switch (argsCnt) { case 3: await DragonflyDB.DragonflyListIndexAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), argsArr[2].Trim('"'), _uniqueID); break; case 4: await DragonflyDB.DragonflyListIndexAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), argsArr[2].Trim('"'), _uniqueID, argsArr[3].Trim('"')); break; case 5: await DragonflyDB.DragonflyListIndexAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), argsArr[2].Trim('"'), _uniqueID, argsArr[3].Trim('"'), argsArr[4].Trim('"')); break; } }); } private static void HandleListLenOperation(long _id, List argsArr) { Task.Run(async () => { string _uniqueID = $"{_id}_listlen"; await DragonflyDB.DragonflyListLengthAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), _uniqueID); }); } private static void HandleListRngOperation(long _id, List argsArr, int argsCnt) { Task.Run(async () => { string _uniqueID = $"{_id}_listrng"; switch (argsCnt) { case 4: await DragonflyDB.DragonflyListRangeAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), argsArr[2].Trim('"'), argsArr[3].Trim('"'), _uniqueID); break; case 5: await DragonflyDB.DragonflyListRangeAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), argsArr[2].Trim('"'), argsArr[3].Trim('"'), _uniqueID, argsArr[4].Trim('"')); break; case 6: await DragonflyDB.DragonflyListRangeAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), argsArr[2].Trim('"'), argsArr[3].Trim('"'), _uniqueID, argsArr[4].Trim('"'), argsArr[5].Trim('"')); break; } }); } private static void HandleListRemOperation(List argsArr) { Task.Run(async () => { await DragonflyDB.DragonflyListRemoveAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"')); }); } private static void HandleListSetOperation(List argsArr) { Task.Run(async () => { await DragonflyDB.DragonflyListSetAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), argsArr[2]); }); } private static void HandleHSetOperation(List argsArr, int argsCnt) { Task.Run(async () => { switch (argsCnt) { case > 2: for (var i = 0; i < argsCnt; i += 2) { await DragonflyDB.DragonflyHashSetAsync(STEAMID, argsArr[i].Trim('"'), argsArr[i + 1]); } break; default: await DragonflyDB.DragonflyHashSetAsync(STEAMID, argsArr[0].Trim('"'), argsArr[1]); break; } }); } private static void HandleHSetIdOperation(List argsArr, int argsCnt) { Task.Run(async () => { switch (argsCnt) { case > 4: for (var i = 1; i < argsCnt; i += 2) { await DragonflyDB.DragonflyHashSetAsync(argsArr[0].Trim('"'), argsArr[i].Trim('"'), argsArr[i + 1]); } break; default: await DragonflyDB.DragonflyHashSetAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), argsArr[2]); break; } }); } private static void HandleHGetOperation(long _id, List argsArr, int argsCnt) { Task.Run(async () => { string _uniqueID = $"{_id}_hget"; switch (argsCnt) { case 2: await DragonflyDB.DragonflyHashGetAsync(STEAMID, argsArr[0].Trim('"'), argsArr[1].Trim('"'), _uniqueID); break; case 3: await DragonflyDB.DragonflyHashGetAsync(STEAMID, argsArr[0].Trim('"'), argsArr[1].Trim('"'), _uniqueID, argsArr[2].Trim('"')); break; case 4: await DragonflyDB.DragonflyHashGetAsync(STEAMID, argsArr[0].Trim('"'), argsArr[1].Trim('"'), _uniqueID, argsArr[2].Trim('"'), argsArr[3].Trim('"')); break; } }); } private static void HandleHGetIdOperation(long _id, List argsArr, int argsCnt) { Task.Run(async () => { string _uniqueID = $"{_id}_hgetid"; switch (argsCnt) { case 3: await DragonflyDB.DragonflyHashGetAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), argsArr[2].Trim('"'), _uniqueID); break; case 4: await DragonflyDB.DragonflyHashGetAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), argsArr[2].Trim('"'), _uniqueID, argsArr[3].Trim('"')); break; case 5: await DragonflyDB.DragonflyHashGetAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), argsArr[2].Trim('"'), _uniqueID, argsArr[3].Trim('"'), argsArr[4].Trim('"')); break; } }); } private static void HandleHGetAllOperation(long _id, List argsArr, int argsCnt) { Task.Run(async () => { string _uniqueID = $"{_id}_hgetall"; switch (argsCnt) { case 1: await DragonflyDB.DragonflyHashGetAllAsync(STEAMID, argsArr[0].Trim('"'), _uniqueID); break; case 2: await DragonflyDB.DragonflyHashGetAllAsync(STEAMID, argsArr[0].Trim('"'), _uniqueID, argsArr[1].Trim('"')); break; case 3: await DragonflyDB.DragonflyHashGetAllAsync(STEAMID, argsArr[0].Trim('"'), _uniqueID, argsArr[1].Trim('"'), argsArr[2].Trim('"')); break; } }); } private static void HandleHGetAllIdOperation(long _id, List argsArr, int argsCnt) { Task.Run(async () => { string _uniqueID = $"{_id}_hgetallid"; switch (argsCnt) { case 2: await DragonflyDB.DragonflyHashGetAllAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), _uniqueID); break; case 3: await DragonflyDB.DragonflyHashGetAllAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), _uniqueID, argsArr[2].Trim('"')); break; case 4: await DragonflyDB.DragonflyHashGetAllAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), _uniqueID, argsArr[2].Trim('"'), argsArr[3].Trim('"')); break; } }); } private static void HandleHDelOperation(List argsArr) { Task.Run(async () => { await DragonflyDB.DragonflyHashDeleteAsync(STEAMID, argsArr[0].Trim('"')); }); } private static void HandleHDelIdOperation(List argsArr) { Task.Run(async () => { await DragonflyDB.DragonflyHashDeleteAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"')); }); } private static void HandleSaveDBOperation() { Task.Run(DragonflyDB.DragonflySaveDBAsync); } private static void HandleSetupOperation(List argsArr, int argsCnt) { if (argsCnt != 3) { Log("Invalid number of arguments for 'setup' function", "error"); return; } string host = argsArr[0].Trim('"'); if (!int.TryParse(argsArr[1].Trim('"'), out int port)) { Log("Invalid port number", "error"); return; } string password = argsArr[2].Trim('"'); if (string.IsNullOrEmpty(password)) { Log("Warning: Setting up DragonflyDB without authentication is not recommended", "action"); password = ""; } ADC_HOST = host; ADC_PORT = port; ADC_PASSWORD = password; Log($"DragonflyDB connection settings updated - Host: '{host}', Port: '{port}', Password: '{password}'", "action"); } private static void HandlePublishOperation(long _id, List argsArr) { Task.Run(async () => { string _uniqueID = $"{_id}_publish"; await DragonflyDB.DragonflyPublishAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), _uniqueID); }); } private static void HandleSubscribeOperation(long _id, List argsArr, int argsCnt) { Task.Run(async () => { string _uniqueID = $"{_id}_subscribe"; switch (argsCnt) { case 3: await DragonflyDB.DragonflySubscribeAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), argsArr[2].Trim('"'), _uniqueID); break; case 4: await DragonflyDB.DragonflySubscribeAsync(argsArr[0].Trim('"'), argsArr[1].Trim('"'), argsArr[2].Trim('"'), _uniqueID, argsArr[3].Trim('"')); break; } }); } } }