ramdb/extension/src/Main.cs
2025-01-01 13:37:36 -06:00

583 lines
24 KiB
C#

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<string> strList = [];
List<string> 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);
}
/// <summary>
/// Called only once when Arma 3 loads the extension.
/// </summary>
/// <param name="func">Pointer to Arma 3's callback function</param>
[UnmanagedCallersOnly(EntryPoint = "RVExtensionRegisterCallback")]
public unsafe static void RVExtensionRegisterCallback(nint func)
{
Callback = Marshal.GetDelegateForFunctionPointer<ExtensionCallback>(func);
}
/// <summary>
/// Called only once when Arma 3 loads the extension.
/// The output will be written in the RPT logs.
/// </summary>
/// <param name="output">A pointer to the output buffer</param>
/// <param name="outputSize">The maximum length of the buffer (always 32 for this particular method)</param>
[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);
}
/// <summary>
/// Called before every RVExtension/RVExtensionArgs execution, providing context about where the call came from.
/// </summary>
/// <param name="argv">
/// Array of pointers to null-terminated strings containing:
/// [0] - Steam 64bit UserID
/// [1] - FileSource
/// [2] - MissionName
/// [3] - ServerName
/// [4] - Machine Network ID
/// </param>
/// <param name="argc">Number of arguments in the argv array (always 5).</param>
[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;
}
/// <summary>
/// The entry point for the default "callExtension" command.
/// </summary>
/// <param name="output">A pointer to the output buffer</param>
/// <param name="outputSize">The maximum size of the buffer (20480 bytes)</param>
/// <param name="function">The function identifier passed in "callExtension"</param>
[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;
}
}
/// <summary>
/// The entry point for the "callExtension" command with function arguments.
/// </summary>
/// <param name="output">A pointer to the output buffer</param>
/// <param name="outputSize">The maximum size of the buffer (20480 bytes)</param>
/// <param name="function">The function identifier passed in "callExtension"</param>
/// <param name="argv">The args passed to "callExtension" as a string array</param>
/// <param name="argc">Number of elements in "argv"</param>
/// <returns>The return code</returns>
[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<string> 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;
}
}
/// <summary>
/// Reads a string from the given pointer.
/// If the pointer is null, a default value will be returned instead.
/// </summary>
/// <param name="pointer">The pointer to read</param>
/// <param name="defaultValue">The string's default value</param>
/// <returns>The marshalled string</returns>
private unsafe static string GetString(char* pointer, string defaultValue = "")
{
return Marshal.PtrToStringAnsi((nint)pointer) ?? defaultValue;
}
/// <summary>
/// Serializes a list of strings into a string representing a valid Arma 3 array.
/// </summary>
/// <param name="list">The list of strings to serialize</param>
/// <returns>A string representing an Arma 3 array</returns>
private static string SerializeList(List<string> list)
{
var content = string.Join(",", [.. list]);
return string.Format("[{0}]", content);
}
/// <summary>
/// Writes the given payload to the output buffer using the provided pointer.
/// Ensures that the payload string is always null terminated.
/// </summary>
/// <param name="output">A pointer to the output buffer</param>
/// <param name="payload">The payload to write</param>
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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> args, int argc)
{
Task.Run(async () =>
{
if (argc > 2)
{
var bulkValues = new Dictionary<string, string>();
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<string> args, int argc)
{
Task.Run(async () =>
{
if (argc > 4)
{
var bulkValues = new Dictionary<string, string>();
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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> args, int argc)
#pragma warning restore IDE0060 // Remove unused parameter
{
Task.Run(async () =>
{
await KeyValueStore.DeleteAsync(args[0].Trim('"'));
});
}
}
}