using System.Runtime.InteropServices; using System.Text; using System.Xml.Linq; #pragma warning disable IDE0130 // Namespace does not match folder structure namespace ArmaRAMDb #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 ARDB_VERSION = "1.0.0"; public const int ARDB_BUFFERSIZE = 1024; public static string ARDB_LOGFOLDER { get; private set; } = "\\@ramdb\\logs"; public static bool ARDB_DEBUG {get; private set; } = false; public static bool ARDB_INITCHECK {get; private set; } = false; public static string STEAMID {get; private set; } = ""; public static bool ARDB_CONTEXTLOG {get; private set; } = false; public static IntPtr ARDB_CONTEXT { get; private set; } private static int numCalls = 0; public static bool ARDB_ISLOADED { get; private set; } = false; 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 ARDB_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("-ardb", StringComparison.CurrentCultureIgnoreCase)) { str = strArray[1]; break; } } if (str == "") str = "@ramdb\\config.xml"; if (File.Exists(Environment.CurrentDirectory + "\\" + str)) { List strList = []; List list = XElement.Load(Environment.CurrentDirectory + "\\" + str).Elements().Select(eintrag => (string)eintrag).ToList(); if (bool.TryParse(list[0], out bool res0)) ARDB_CONTEXTLOG = res0; if (bool.TryParse(list[1], out bool res1)) ARDB_DEBUG = res1; Log($"Config file found! Context Mode: {ARDB_CONTEXTLOG}! Debug Mode: {ARDB_DEBUG}!", "action"); } else { Log("Config file not found! Default Settings Loaded.", "action"); } ARDB_INITCHECK = true; } public static void Log(string msg, string type) { if (!ARDB_DEBUG) return; string logFileName = type + ".log"; string path = Environment.CurrentDirectory + ARDB_LOGFOLDER; if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } using StreamWriter sw = new(Path.Combine(path, logFileName), true); sw.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, ARDB_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) { ARDB_CONTEXT = (IntPtr)argv; var context = new CallContext { SteamId = ulong.Parse(Marshal.PtrToStringAnsi((IntPtr)argv[0])), FileSource = Marshal.PtrToStringAnsi((IntPtr)argv[1]), MissionName = Marshal.PtrToStringAnsi((IntPtr)argv[2]), ServerName = Marshal.PtrToStringAnsi((IntPtr)argv[3]), RemoteExecutedOwner = short.Parse(Marshal.PtrToStringAnsi((IntPtr)argv[4])) }; STEAMID = context.SteamId.ToString(); if (ARDB_CONTEXTLOG) { Log($"Context: SteamID={context.SteamId}, FileSource={context.FileSource}, MissionName={context.MissionName}, ServerName={context.ServerName}, RemoteExecutedOwner={context.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 (!ARDB_INITCHECK) ARDB_Init(); numCalls++; var func = GetString(function); switch (func.ToLower()) { case "time": DateTime timeNow = DateTime.Now; string timeNowString = timeNow.ToString("dd:MM:yyyy HH:mm:ss"); WriteOutput(output, timeNowString); 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 (!ARDB_INITCHECK) ARDB_Init(); numCalls++; var _id = DateTimeOffset.Now.ToUnixTimeMilliseconds(); var func = GetString(function); List args = Enumerable.Range(0, argc).Select(i => GetString(argv[i])).ToList(); switch (func.ToLower()) { case "del": HandleDeleteOperation(args, argc); WriteOutput(output, "Async"); return 200; case "get": HandleGetOperation(_id, args, argc); WriteOutput(output, $"[\"{_id}_get\",{args[0]}]"); return 100; case "hdel": HandleHDelOperation(args, argc); WriteOutput(output, "Async"); return 200; case "hdelid": HandleHDelIdOperation(args, argc); WriteOutput(output, "Async"); return 200; case "hget": HandleHGetOperation(_id, args, argc); WriteOutput(output, $"[\"{_id}_hget\",{args[1]}]"); return 100; case "hgetid": HandleHGetIdOperation(_id, args, argc); WriteOutput(output, $"[\"{_id}_hgetid\",{args[0]}]"); return 100; case "hgetall": HandleHGetAllOperation(_id, args, argc); WriteOutput(output, $"[\"{_id}_hgetall\",{args[0]}]"); return 100; case "hgetallid": HandleHGetAllIdOperation(_id, args, argc); WriteOutput(output, $"[\"{_id}_hgetallid\",{args[0]}]"); return 100; case "hrem": HandleHRemOperation(args, argc); WriteOutput(output, "Async"); return 200; case "hremid": HandleHRemIdOperation(args, argc); WriteOutput(output, "Async"); return 200; case "hset": HandleHSetOperation(args, argc); WriteOutput(output, "Async"); return 200; case "hsetid": HandleHSetIdOperation(args, argc); WriteOutput(output, "Async"); return 200; case "isloaded": WriteOutput(output, ARDB_ISLOADED.ToString().ToLower()); return 100; case "listadd": HandleListAddOperation(args, argc); WriteOutput(output, "Async"); return 200; case "listidx": HandleListIdxOperation(_id, args, argc); WriteOutput(output, $"[\"{_id}_listindex\",{args[0]}]"); return 100; case "listrem": HandleListRemOperation(args, argc); WriteOutput(output, "Async"); return 200; case "listrng": HandleListRngOperation(_id, args, argc); WriteOutput(output, $"[\"{_id}_listrange\",{args[0]}]"); return 100; case "listset": HandleListSetOperation(args, argc); WriteOutput(output, "Async"); return 200; case "load": LoadData(); WriteOutput(output, "Data loaded"); return 100; case "save": SaveData(); WriteOutput(output, "Data saved"); return 100; case "set": HandleSetOperation(args, argc); WriteOutput(output, "Async"); return 200; case "version": WriteOutput(output, ARDB_VERSION); return 100; default: WriteOutput(output, "Available functions: del, get, hdel, hdelid, hget, hgetid, hgetall, hgetallid, hrem, hremid, hset, hsetid, isloaded, listadd, listidx, listrem, listrng, listset, load, save, set, 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 SaveData() { var db = new RAMDb(); db.ExportToXml(); Log("Data saved to XML", "action"); } private static void LoadData() { var db = new RAMDb(); db.ImportFromXml(); ARDB_ISLOADED = true; Log("Data loaded from XML", "action"); } #pragma warning disable IDE0060 // Remove unused parameter private static void HandleHDelOperation(List args, int argc) #pragma warning restore IDE0060 // Remove unused parameter { Task.Run(async () => { await HashStore.HashDelete(STEAMID); }); } #pragma warning disable IDE0060 // Remove unused parameter private static void HandleHDelIdOperation(List args, int argc) #pragma warning restore IDE0060 // Remove unused parameter { Task.Run(async () => { await HashStore.HashDelete(args[0].Trim('"')); }); } private static void HandleHGetOperation(long _id, List args, int argc) { Task.Run(async () => { string _uniqueID = $"{_id}_hget"; if (argc == 2) await HashStore.HashGet(STEAMID, args[0].Trim('"'), args[1].Trim('"'), _uniqueID); if (argc == 3) await HashStore.HashGet(STEAMID, args[0].Trim('"'), args[1].Trim('"'), _uniqueID, args[2].Trim('"')); if (argc == 4) await HashStore.HashGet(STEAMID, args[0].Trim('"'), args[1].Trim('"'), _uniqueID, args[2].Trim('"'), args[3].Trim('"')); }); } private static void HandleHGetIdOperation(long _id, List args, int argc) { Task.Run(async () => { string _uniqueID = $"{_id}_hgetid"; if (argc == 3) await HashStore.HashGet(args[0].Trim('"'), args[1].Trim('"'), args[2].Trim('"'), _uniqueID); if (argc == 4) await HashStore.HashGet(args[0].Trim('"'), args[1].Trim('"'), args[2].Trim('"'), _uniqueID, args[3].Trim('"')); if (argc == 5) await HashStore.HashGet(args[0].Trim('"'), args[1].Trim('"'), args[2].Trim('"'), _uniqueID, args[3].Trim('"'), args[4].Trim('"')); }); } private static void HandleHGetAllOperation(long _id, List args, int argc) { Task.Run(async () => { string _uniqueID = $"{_id}_hgetall"; if (argc == 1) await HashStore.HashGetAllAsync(STEAMID, args[0].Trim('"'), _uniqueID); if (argc == 2) await HashStore.HashGetAllAsync(STEAMID, args[0].Trim('"'), _uniqueID, args[1].Trim('"')); if (argc == 3) await HashStore.HashGetAllAsync(STEAMID, args[0].Trim('"'), _uniqueID, args[1].Trim('"'), args[2].Trim('"')); }); } private static void HandleHGetAllIdOperation(long _id, List args, int argc) { Task.Run(async () => { string _uniqueID = $"{_id}_hgetallid"; if (argc == 2) await HashStore.HashGetAllAsync(args[0].Trim('"'), args[1].Trim('"'), _uniqueID); if (argc == 3) await HashStore.HashGetAllAsync(args[0].Trim('"'), args[1].Trim('"'), _uniqueID, args[2].Trim('"')); if (argc == 4) await HashStore.HashGetAllAsync(args[0].Trim('"'), args[1].Trim('"'), _uniqueID, args[2], args[3].Trim('"')); }); } #pragma warning disable IDE0060 // Remove unused parameter private static void HandleHRemOperation(List args, int argc) #pragma warning restore IDE0060 // Remove unused parameter { Task.Run(async () => { await HashStore.HashRemoveAsync(STEAMID, args[0].Trim('"')); }); } #pragma warning disable IDE0060 // Remove unused parameter private static void HandleHRemIdOperation(List args, int argc) #pragma warning restore IDE0060 // Remove unused parameter { Task.Run(async () => { await HashStore.HashRemoveAsync(args[0].Trim('"'), args[1].Trim('"')); }); } private static void HandleHSetOperation(List args, int argc) { Task.Run(async () => { if (argc > 2) { var bulkValues = new Dictionary(); for (var i = 0; i < argc; i += 2) { bulkValues[args[i].Trim('"')] = args[i + 1]; } await HashStore.HashSetBulkAsync(STEAMID, bulkValues); } else { await HashStore.HashSetAsync(STEAMID, args[0].Trim('"'), args[1].Trim('"')); } }); } private static void HandleHSetIdOperation(List args, int argc) { Task.Run(async () => { if (argc > 4) { var bulkValues = new Dictionary(); for (var i = 1; i < argc; i += 2) { bulkValues[args[i].Trim('"')] = args[i + 1]; } await HashStore.HashSetBulkAsync(args[0].Trim('"'), bulkValues); } else { await HashStore.HashSetAsync(args[0].Trim('"'), args[1].Trim('"'), args[2].Trim('"')); } }); } private static void HandleListAddOperation(List args, int argc) { Task.Run(async () => { switch (argc) { case > 2: for (var i = 0; i < argc; i++) { await ListStore.ListAddAsync(args[0].Trim('"'), args[i]); } break; default: await ListStore.ListAddAsync(args[0].Trim('"'), args[1].Trim('"')); break; } }); } #pragma warning disable IDE0060 // Remove unused parameter private static void HandleListRemOperation(List args, int argc) #pragma warning restore IDE0060 // Remove unused parameter { Task.Run(async () => { await ListStore.ListSetAsync(args[0].Trim('"'), args[1].Trim('"'), "RAMDBREMOVE"); await ListStore.ListRemoveAsync(args[0].Trim('"'), "RAMDBREMOVE"); }); } private static void HandleListIdxOperation(long _id, List args, int argc) { Task.Run(async () => { string _uniqueID = $"{_id}_listidx"; if (argc == 3) await ListStore.ListIndexAsync(args[0].Trim('"'), args[1].Trim('"'), args[2].Trim('"'), _uniqueID); if (argc == 4) await ListStore.ListIndexAsync(args[0].Trim('"'), args[1].Trim('"'), args[2].Trim('"'), _uniqueID, args[3].Trim('"')); if (argc == 5) await ListStore.ListIndexAsync(args[0].Trim('"'), args[1].Trim('"'), args[2].Trim('"'), _uniqueID, args[3].Trim('"'), args[4].Trim('"')); }); } private static void HandleListRngOperation(long _id, List args, int argc) { Task.Run(async () => { string _uniqueID = $"{_id}_listrng"; if (argc == 4) await ListStore.ListRangeAsync(args[0].Trim('"'), args[1].Trim('"'), args[2].Trim('"'), args[3].Trim('"'), _uniqueID); if (argc == 5) await ListStore.ListRangeAsync(args[0].Trim('"'), args[1].Trim('"'), args[2].Trim('"'), args[3].Trim('"'), _uniqueID, args[4].Trim('"')); if (argc == 6) await ListStore.ListRangeAsync(args[0].Trim('"'), args[1].Trim('"'), args[2].Trim('"'), args[3].Trim('"'), _uniqueID, args[4].Trim('"'), args[5].Trim('"')); }); } #pragma warning disable IDE0060 // Remove unused parameter private static void HandleListSetOperation(List args, int argc) #pragma warning restore IDE0060 // Remove unused parameter { Task.Run(async () => { await ListStore.ListSetAsync(args[0].Trim('"'), args[1].Trim('"'), args[2]); }); } #pragma warning disable IDE0060 // Remove unused parameter private static void HandleSetOperation(List args, int argc) #pragma warning restore IDE0060 // Remove unused parameter { Task.Run(async () => { await KeyValueStore.SetAsync(args[0].Trim('"'), args[1]); }); } private static void HandleGetOperation(long _id, List args, int argc) { Task.Run(async () => { string _uniqueID = $"{_id}_get"; if (argc == 2) await KeyValueStore.GetAsync(args[0].Trim('"'), args[1].Trim('"'), _uniqueID); if (argc == 3) await KeyValueStore.GetAsync(args[0].Trim('"'), args[1].Trim('"'), _uniqueID, args[2].Trim('"')); if (argc == 4) await KeyValueStore.GetAsync(args[0].Trim('"'), args[1].Trim('"'), _uniqueID, args[2].Trim('"'), args[3].Trim('"')); }); } #pragma warning disable IDE0060 // Remove unused parameter private static void HandleDeleteOperation(List args, int argc) #pragma warning restore IDE0060 // Remove unused parameter { Task.Run(async () => { await KeyValueStore.DeleteAsync(args[0].Trim('"')); }); } } }