package main /* #include #include #include #include "extensionCallback.h" */ import "C" // This is required to import the C code import ( "bytes" "context" "encoding/json" "errors" "fmt" "io/ioutil" "log" "os" "reflect" "strconv" "strings" "time" "unsafe" influxdb2 "github.com/influxdata/influxdb-client-go/v2" "github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4/pgxpool" ) // declare list of functions available for call var AVAILABLE_FUNCTIONS = map[string]interface{}{ "initExtension": initExtension, "deinitExtension": deinitExtension, "loadSettings": loadSettings, "connectToInflux": connectToInflux, "writeToInflux": writeToInflux, "connectToTimescale": connectToTimescale, "initTimescale": initTimescale, "writeToTimescale": writeToTimescale, "getDir": getDir, "sanitizeLineProtocol": sanitizeLineProtocol, "version": version, "getUnixTimeNano": getUnixTimeNano, } var EXTENSION_VERSION string = "0.0.2" var extensionCallbackFnc C.extensionCallback type ServerPollSetting struct { Name string `json:"name"` Enabled bool `json:"enabled"` ServerOnly bool `json:"serverOnly"` IntervalMs int `json:"intervalMs"` Bucket string `json:"bucket"` Measurement string `json:"measurement"` Description string `json:"description"` } var ServerPollSettingProperties []string = []string{ "Name", "Enabled", "ServerOnly", "IntervalMs", "Bucket", "Measurement", "Description", } type CBAEventHandler struct { Name string `json:"name"` Enabled bool `json:"enabled"` Description string `json:"description"` } var CBAEventHandlerProperties []string = []string{ "Name", "Enabled", "Description", } type settingsJson struct { Influx struct { Enabled bool `json:"enabled"` Host string `json:"host"` Token string `json:"token"` Org string `json:"org"` } `json:"influxdb"` Timescale struct { Enabled bool `json:"enabled"` ConnectionUrl string `json:"connectionUrl"` DatabaseName string `json:"databaseName"` } `json:"timescaledb"` Arma3 struct { RefreshRateMs int `json:"refreshRateMs"` Debug bool `json:"debug"` } `json:"arma3"` RecordingSettings map[string]interface{} `json:"recordingSettings"` } var activeSettings settingsJson // InfluxDB variables var influxClient influxdb2.Client // TimescaleDB variables var timescaleDbPool *pgxpool.Pool // file paths var ADDON_FOLDER string = getDir() + "\\@RangerMetrics" var LOG_FILE string = ADDON_FOLDER + "\\rangermetrics.log" var SETTINGS_FILE string = ADDON_FOLDER + "\\settings.json" var SETTINGS_FILE_EXAMPLE string = ADDON_FOLDER + "\\settings.example.json" // var BACKUP_FILE_PATH string = ADDON_FOLDER + "/local_backup.log.gzip" // var BACKUP_WRITER *gzip.Writer // 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 initExtension() { functionName := "initExtension" logLine(functionName, `["Initializing RangerMetrics extension", "INFO"]`, false) logLine(functionName, fmt.Sprintf(`["RangerMetrics Extension Version: %v", "INFO"]`, EXTENSION_VERSION), false) var err error activeSettings, err = loadSettings() if err != nil { logLine(functionName, fmt.Sprintf(`["Error loading settings: %v", "ERROR"]`, err), false) return } if activeSettings.Influx.Enabled { influxClient, err = connectToInflux() if err != nil { logLine(functionName, fmt.Sprintf(`["Error connecting to InfluxDB: %v", "ERROR"]`, err), false) return } } if activeSettings.Timescale.Enabled { timescaleDbPool, err = connectToTimescale() if err != nil { logLine(functionName, fmt.Sprintf(`["Error connecting to TimescaleDB: %v", "ERROR"]`, err), false) return } initTimescale() } logLine("extensionReady", `["Extension ready", "INFO"]`, false) } func deinitExtension() { functionName := "deinitExtension" logLine(functionName, `["Deinitializing RangerMetrics extension", "INFO"]`, false) if timescaleDbPool != nil { logLine(functionName, `["Closing TimescaleDB connection", "INFO"]`, false) timescaleDbPool.Close() } else { logLine(functionName, `["TimescaleDB connection not open", "INFO"]`, false) } logLine(functionName, `[true]`, false) } // func RVExtensionContext(output *C.char, argc *C.int) { // } func version() { functionName := "version" logLine(functionName, fmt.Sprintf(`["RangerMetrics Extension Version:%s", "INFO"]`, EXTENSION_VERSION), false) } // return db client and error func connectToInflux() (influxdb2.Client, error) { if activeSettings.Influx.Host == "" || activeSettings.Influx.Host == "http://host:8086" { return nil, errors.New("influxConnectionSettings.Host is empty") // logLine("connectToInflux", `["Creating backup file", "INFO"]`) // file, err := os.Open(BACKUP_FILE_PATH) // if err != nil { // log.Fatal(err) // logLine("connectToInflux", `["Error opening backup file", "ERROR"]`) // } // BACKUP_WRITER = gzip.NewWriter(file) // if err != nil { // log.Fatal(err) // logLine("connectToInflux", `["Error creating gzip writer", "ERROR"]`) // } // return "Error connecting to Influx. Using local backup" } influxClient := influxdb2.NewClientWithOptions(activeSettings.Influx.Host, activeSettings.Influx.Token, influxdb2.DefaultOptions().SetBatchSize(1000).SetFlushInterval(1000)) return influxClient, nil } ////////////////////////////////// // TIMESCALE ////////////////////////////////// func connectToTimescale() (*pgxpool.Pool, error) { functionName := "connectToTimescale" var err error // urlExample := "postgres://username:password@localhost:5432/database_name" // logLine("connectToTimescale", fmt.Sprintf(`["timescaleConnectionSettings.ConnectionUrl: %s", "INFO"]`, timescaleConnectionSettings.ConnectionUrl)) conn, err := pgx.Connect(context.Background(), activeSettings.Timescale.ConnectionUrl+"/postgres") if err != nil { logLine( functionName, fmt.Sprintf(`["Error connecting to Timescale DB: %v", "ERROR"]`, err.Error()), false, ) return nil, err } // ensure database exists logLine( functionName, fmt.Sprintf(`["TimescaleDB: 'CREATE DATABASE %s'", "INFO"]`, activeSettings.Timescale.DatabaseName), false, ) conn.Query(context.Background(), fmt.Sprintf(`CREATE DATABASE %s`, activeSettings.Timescale.DatabaseName)) // close connection conn.Close(context.Background()) // create connection pool var dbPool *pgxpool.Pool dbPool, err = pgxpool.Connect( context.Background(), fmt.Sprintf(`%s/%s`, activeSettings.Timescale.ConnectionUrl, activeSettings.Timescale.DatabaseName), ) if err != nil { logLine( functionName, fmt.Sprintf(`["Error connecting to Timescale DB: %v", "ERROR"]`, err.Error()), false, ) return nil, err } logLine("connectToTimescale", `["Connected to Timescale successfully", "INFO"]`, false) return dbPool, nil } func initTimescale() { functionName := "initTimescale" var err error // schema init sql var tableCreationSql string = ` CREATE TABLE IF NOT EXISTS units."State" ( "timestamp" timestamp with time zone NOT NULL, mission_id integer NOT NULL, net_id text COLLATE pg_catalog."default" NOT NULL, player_uid text COLLATE pg_catalog."default" NOT NULL DEFAULT '-1'::integer, is_alive boolean NOT NULL, is_afk boolean, is_speaking smallint, unit_name text COLLATE pg_catalog."default" NOT NULL, side text COLLATE pg_catalog."default" NOT NULL, "position" point NOT NULL, direction numeric(2, 0) NOT NULL, health numeric(2, 0), traits text[] COLLATE pg_catalog."default", CONSTRAINT "UnitStates_pkey" PRIMARY KEY (net_id, "timestamp", player_uid, mission_id) ); COMMENT ON TABLE units."State" IS 'Reflects a unit''s (soldier''s) state during a mission. Can be AI (player_uid = -1) or a player.'; CREATE TABLE IF NOT EXISTS units."Identity" ( "timestamp" timestamp with time zone NOT NULL, mission_id integer NOT NULL, net_id text COLLATE pg_catalog."default" NOT NULL, player_uid text COLLATE pg_catalog."default" NOT NULL DEFAULT '-1'::integer, class_name text COLLATE pg_catalog."default" NOT NULL, display_name text, side text, role_description text COLLATE pg_catalog."default", traits text[] COLLATE pg_catalog."default", CONSTRAINT "Units_pkey" PRIMARY KEY (mission_id, net_id, "timestamp", player_uid) ); COMMENT ON TABLE units."Identity" IS 'This is polled periodically during the mission to gather static and less-dynamic traits of units (soldiers).'; CREATE TABLE IF NOT EXISTS events."Chat" ( id bigserial NOT NULL, "timestamp" timestamp with time zone NOT NULL, mission_id integer NOT NULL, player_uid text COLLATE pg_catalog."default" NOT NULL DEFAULT -1, channel integer, owner integer, name text COLLATE pg_catalog."default", "from" text COLLATE pg_catalog."default", text text COLLATE pg_catalog."default", "forcedDisplay" boolean, "isPlayerMessage" boolean, "sentenceType" integer, "chatMessageType" integer, CONSTRAINT "Chat_pkey" PRIMARY KEY ("timestamp", mission_id, player_uid, id), UNIQUE (id) ); CREATE TABLE IF NOT EXISTS missions."Played" ( id serial NOT NULL, start_time_utc timestamp with time zone NOT NULL, start_time_game timestamp with time zone NOT NULL, world_name text COLLATE pg_catalog."default" NOT NULL, briefing_name text COLLATE pg_catalog."default" NOT NULL, mission_name text COLLATE pg_catalog."default" NOT NULL, mission_name_source text COLLATE pg_catalog."default" NOT NULL, on_load_name text NOT NULL, author text COLLATE pg_catalog."default" NOT NULL, server_name text COLLATE pg_catalog."default" NOT NULL, server_mods json, ace_medical boolean, radio_tfar boolean, radio_acre boolean, duration interval, version_addon integer[] NOT NULL, version_extension integer[] NOT NULL, tags text[] COLLATE pg_catalog."default", CONSTRAINT "MissionsRun_pkey" PRIMARY KEY (id, world_name, start_time_utc), CONSTRAINT "MissionsRun_id_start_time_utc_key" UNIQUE (id, start_time_utc), UNIQUE (id) ); COMMENT ON TABLE missions."Played" IS 'Contains core mission data, recorded at mission init. The IDs here are referenced by many other areas to "place" them.'; CREATE TABLE IF NOT EXISTS players."ConnectionsMission" ( id bigserial NOT NULL, "timestamp" timestamp with time zone NOT NULL, mission_id integer NOT NULL, player_uid text COLLATE pg_catalog."default" NOT NULL, event_type text COLLATE pg_catalog."default" NOT NULL, profile_name text COLLATE pg_catalog."default" NOT NULL, display_name text COLLATE pg_catalog."default" NOT NULL, steam_name text COLLATE pg_catalog."default" NOT NULL, is_jip boolean, CONSTRAINT "ConnectionsMission_pkey" PRIMARY KEY (player_uid, "timestamp", profile_name, mission_id, id), UNIQUE ("timestamp", mission_id, player_uid, id) ); CREATE TABLE IF NOT EXISTS units."Inventory" ( "timestamp" timestamp with time zone NOT NULL, mission_id integer NOT NULL, net_id text NOT NULL, player_uid text NOT NULL DEFAULT -1, inventory jsonb NOT NULL, PRIMARY KEY ("timestamp", net_id, mission_id, player_uid) ); COMMENT ON TABLE units."Inventory" IS 'This is used to track the inventory state of units.'; CREATE TABLE IF NOT EXISTS players."ConnectionsServer" ( "timestamp" timestamp with time zone NOT NULL, player_uid text COLLATE pg_catalog."default" NOT NULL, event_type text COLLATE pg_catalog."default" NOT NULL, profile_name text COLLATE pg_catalog."default" NOT NULL, display_name text COLLATE pg_catalog."default" NOT NULL, steam_name text COLLATE pg_catalog."default" NOT NULL, is_jip boolean, CONSTRAINT "PlayersObserved_pkey" PRIMARY KEY (player_uid, "timestamp", profile_name) ); CREATE TABLE IF NOT EXISTS missions."Environment" ( "timestamp" timestamp with time zone NOT NULL, mission_id integer NOT NULL, fog numeric(2), overcast numeric(2), rain numeric(2), humidity numeric(2), waves numeric(2), "windDir" numeric(2), "windStr" numeric(2), gusts numeric(2), lightnings numeric(2), "moonIntensity" numeric(2), "moonPhase" numeric(2), "sunOrMoon" numeric(2), PRIMARY KEY ("timestamp", mission_id) ); COMMENT ON TABLE missions."Environment" IS 'Contains periodically collected environmental information during missions.'; CREATE TABLE IF NOT EXISTS vehicles."Identity" ( "timestamp" timestamp with time zone NOT NULL, mission_id integer NOT NULL, net_id text NOT NULL, class_name text NOT NULL, display_name text, customization jsonb, weapons jsonb, PRIMARY KEY ("timestamp", mission_id, net_id) ); COMMENT ON TABLE vehicles."Identity" IS 'Periodically polled identity information about a vehicle.'; CREATE TABLE IF NOT EXISTS vehicles."State" ( "timestamp" timestamp with time zone NOT NULL, mission_id integer NOT NULL, net_id text NOT NULL, is_alive boolean, side text, "position" point NOT NULL, direction numeric(2), health numeric(2), crew text[], PRIMARY KEY ("timestamp", mission_id, net_id) ); COMMENT ON TABLE vehicles."State" IS 'State information about vehicles. Crew is an array of net_id to be referenced against the Unit tables.'; CREATE TABLE IF NOT EXISTS players."AllObserved" ( id serial NOT NULL, player_uid text NOT NULL, PRIMARY KEY (id, player_uid), UNIQUE (player_uid) ); ` relationCreationSql := []string{ `ALTER TABLE IF EXISTS units."State" ADD FOREIGN KEY (mission_id) REFERENCES missions."Played" (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION NOT VALID; `, `ALTER TABLE IF EXISTS units."State" ADD FOREIGN KEY (player_uid) REFERENCES players."AllObserved" (player_uid) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION NOT VALID; `, `ALTER TABLE IF EXISTS units."Identity" ADD FOREIGN KEY (mission_id) REFERENCES missions."Played" (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION NOT VALID; `, `ALTER TABLE IF EXISTS units."Identity" ADD FOREIGN KEY (player_uid) REFERENCES players."AllObserved" (player_uid) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION NOT VALID; `, `ALTER TABLE IF EXISTS events."Chat" ADD FOREIGN KEY (mission_id) REFERENCES missions."Played" (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION NOT VALID; `, `ALTER TABLE IF EXISTS events."Chat" ADD FOREIGN KEY (player_uid) REFERENCES players."AllObserved" (player_uid) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION NOT VALID; `, `ALTER TABLE IF EXISTS players."ConnectionsMission" ADD FOREIGN KEY (mission_id) REFERENCES missions."Played" (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION NOT VALID; `, `ALTER TABLE IF EXISTS players."ConnectionsMission" ADD FOREIGN KEY (player_uid) REFERENCES players."AllObserved" (player_uid) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION NOT VALID; `, `ALTER TABLE IF EXISTS units."Inventory" ADD FOREIGN KEY (mission_id) REFERENCES missions."Played" (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION NOT VALID; `, `ALTER TABLE IF EXISTS units."Inventory" ADD FOREIGN KEY (player_uid) REFERENCES players."AllObserved" (player_uid) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION NOT VALID; `, `ALTER TABLE IF EXISTS missions."Environment" ADD FOREIGN KEY (mission_id) REFERENCES missions."Played" (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION NOT VALID; `, `ALTER TABLE IF EXISTS vehicles."Identity" ADD FOREIGN KEY (mission_id) REFERENCES missions."Played" (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION NOT VALID; `, `ALTER TABLE IF EXISTS vehicles."State" ADD FOREIGN KEY (mission_id) REFERENCES missions."Played" (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION NOT VALID; `, } var tx pgx.Tx tx, err = timescaleDbPool.Begin(context.Background()) if err != nil { logLine(functionName, fmt.Sprintf(`["Error creating transaction: %v", "ERROR"]`, err.Error()), false) return } _, err = tx.Exec(context.Background(), tableCreationSql) if err != nil { logLine(functionName, fmt.Sprintf(`["Error creating tables: %v", "ERROR"]`, err.Error()), false) tx.Rollback(context.Background()) tx.Commit(context.Background()) return } err = tx.Commit(context.Background()) if err != nil { logLine(functionName, fmt.Sprintf(`["Error committing table creation transaction: %v", "ERROR"]`, err.Error()), false) return } // run each relation creation sql statement for _, sql := range relationCreationSql { _, err = tx.Exec(context.Background(), sql) if err != nil { logLine(functionName, fmt.Sprintf(`["Error creating relation: %v", "ERROR"]`, err.Error()), false) tx.Rollback(context.Background()) tx.Commit(context.Background()) return } } err = tx.Commit(context.Background()) if err != nil { logLine(functionName, fmt.Sprintf(`["Error committing relation creation transaction: %v", "ERROR"]`, err.Error()), false) return } logLine(functionName, `["Timescale schema initialized", "INFO"]`, false) } func writeToTimescale(table string, line string) { // logLine("writeToTimescale", fmt.Sprintf(`["line: %s", "INFO"]`, line)) functionName := "writeToTimescale" _, err := timescaleDbPool.Exec(context.Background(), "INSERT INTO %1 (time, line) VALUES (NOW(), $2);", table, line) if err != nil { logLine(functionName, fmt.Sprintf(`["Error writing to timescale: %v", "ERROR"]`, err.Error()), false) } } func writeToInflux(a3DataRaw *[]string) string { // convert to string array a3Data := *a3DataRaw logLine("writeToInflux", fmt.Sprintf(`["Received %d params", "DEBUG"]`, len(a3Data)), true) MIN_PARAMS_COUNT := 1 var logData string functionName := "writeToInflux" if len(a3Data) < MIN_PARAMS_COUNT { logData = fmt.Sprintf(`["Not all parameters present (got %d, expected at least %d)", "ERROR"]`, len(a3Data), MIN_PARAMS_COUNT) logLine(functionName, logData, false) return logData } // use custom bucket or default var bucket string = fixEscapeQuotes(trimQuotes(string(a3Data[0]))) // Get non-blocking write client WRITE_API := influxClient.WriteAPI(activeSettings.Influx.Org, bucket) if WRITE_API == nil { logData = `["Error creating write API", "ERROR"]` logLine(functionName, logData, false) return logData } // Get errors channel errorsCh := WRITE_API.Errors() go func() { for writeErr := range errorsCh { logData = fmt.Sprintf(`["Error parsing line protocol: %s", "ERROR"]`, strings.Replace(writeErr.Error(), `"`, `'`, -1)) logLine(functionName, logData, false) } }() // now we have our write client, we'll go through the rest of the receive array items in line protocol format and write them to influx for i := 1; i < len(a3Data); i++ { var p string = fixEscapeQuotes(trimQuotes(string(a3Data[i]))) // write the line to influx WRITE_API.WriteRecord(p) // TODO: Add backup writer // // append backup line to file if BACKUP_WRITER is set // // // if BACKUP_WRITER != nil { // _, err = BACKUP_WRITER.Write([]byte(p + "\n")) // } } // schedule cleanup WRITE_API.Flush() logData = fmt.Sprintf(`["Wrote %d lines to influx", "DEBUG"]`, len(a3Data)-1) logLine(functionName, logData, true) return "Success" } // sanitize line protocol for influx func sanitizeLineProtocol(line string) string { // replace all spaces with underscores // line = strings.ReplaceAll(line, ` `, `\ `) // replace all commas with underscores // line = strings.ReplaceAll(line, `,`, `\,`) // replace all equals with underscores // line = strings.ReplaceAll(line, "=", "_") // replace all quotes with underscores // line = strings.ReplaceAll(line, "\"", "_") return line } func getDir() string { dir, err := os.Getwd() if err != nil { log.Fatal(err) } return dir } // return true if the program should continue func loadSettings() (settingsJson, error) { functionName := "loadSettings" logLine(functionName, fmt.Sprintf(`["ADDON_FOLDER: %s", "INFO"]`, ADDON_FOLDER), false) logLine(functionName, fmt.Sprintf(`["LOG_FILE: %s", "INFO"]`, LOG_FILE), false) logLine(functionName, fmt.Sprintf(`["SETTINGS_FILE: %s", "INFO"]`, SETTINGS_FILE), false) settings := settingsJson{} // print the current working directory var file *os.File var err error // read settings from file // settings.json should be in the same directory as the .dll // see if the file exists if _, err = os.Stat(SETTINGS_FILE); os.IsNotExist(err) { // file does not exist logLine( functionName, fmt.Sprintf(`["%s does not exist", "ERROR"]`, SETTINGS_FILE), false, ) // copy settings.json.example to settings.json // load contents fileContents, err := ioutil.ReadFile(SETTINGS_FILE_EXAMPLE) if err != nil { return settings, err } // write contents to settings.json err = ioutil.WriteFile(SETTINGS_FILE, fileContents, 0644) if err != nil { return settings, err } // Exit false to discontinue initialization since settings are defaults logLine(functionName, `["CREATED SETTINGS"]`, false) // return a new error return settings, errors.New("settings.json does not exist") } else { // file exists logLine(functionName, `["settings.json found", "DEBUG"]`, true) // read the file file, err = os.Open(SETTINGS_FILE) if err != nil { return settings, err } defer file.Close() decoder := json.NewDecoder(file) err = decoder.Decode(&settings) if err != nil { return settings, err } // send contents of settings file // get the file contents fileContents, err := ioutil.ReadFile(SETTINGS_FILE) if err != nil { return settings, err } // compact the json var jsonStr bytes.Buffer err = json.Compact(&jsonStr, fileContents) if err != nil { return settings, err } logLine( "loadSettingsJSON", jsonStr.String(), false, ) } return settings, nil } 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)) } var temp string temp = fmt.Sprintf("Function: %s nb params: %d params: %s!", C.GoString(input), argc, out) if C.GoString(input) == "sendToInflux" { // start a goroutine to send the data to influx // param string is argv[0] which is the data to send to influx go writeToInflux(&out) temp = fmt.Sprintf("Function: %s nb params: %d", C.GoString(input), argc) } if C.GoString(input) == "setAddonDir" { // set the addon directory ADDON_FOLDER = out[0] LOG_FILE = ADDON_FOLDER + "\\rangermetrics.log" SETTINGS_FILE = ADDON_FOLDER + "\\settings.json" SETTINGS_FILE_EXAMPLE = ADDON_FOLDER + "\\settings.example.json" } // 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 getUnixTimeNano() 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 logLine(functionName string, data string, isDebug bool) { statusName := C.CString("RangerMetrics") 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) if activeSettings.Arma3.Debug && isDebug { log.Println(data) } else if !isDebug { log.Println(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 "getUnixTimeNano": time := getUnixTimeNano() temp = fmt.Sprintf(`["%s"]`, strconv.FormatInt(time, 10)) default: // check if input is in AVAILABLE_FUNCTIONS // if not, return error // if yes, continue if _, ok := AVAILABLE_FUNCTIONS[C.GoString(input)]; !ok { temp = fmt.Sprintf(`["Function: %s not found!", "ERROR"]`, C.GoString(input)) } else { // call the function by name go reflect.ValueOf(AVAILABLE_FUNCTIONS[C.GoString(input)]).Call([]reflect.Value{}) temp = fmt.Sprintf(`["Function: %s called successfully", "DEBUG"]`, C.GoString(input)) } } // switch C.GoString(input) { // case "version": // logLine("goRVExtension", fmt.Sprintf(`["Input: %s", "INFO"]`, C.GoString(input))) // temp = EXTENSION_VERSION // case "getDir": // logLine("goRVExtension", fmt.Sprintf(`["Input: %s", "INFO"]`, C.GoString(input))) // temp = getDir() // case "loadSettings": // logLine("goRVExtension", fmt.Sprintf(`["Input: %s", "INFO"]`, C.GoString(input))) // cwd, result, influxHost, timescaleUrl := loadSettings() // log.Println("CWD:", cwd) // log.Println("RESULT:", result) // log.Println("INFLUX HOST:", influxHost) // log.Println("TIMESCALE URL:", timescaleUrl) // if result != "" { // logLine("goRVExtension", result) // temp = fmt.Sprintf( // `["%s", "%s", "%s", "%d"]`, // EXTENSION_VERSION, // influxConnectionSettings.Host, // influxConnectionSettings.Org, // a3Settings.RefreshRateMs, // ) // } // case "connectToInflux": // // logLine("goRVExtension", fmt.Sprintf(`["Input: %s", "INFO"]`, C.GoString(input))) // go connectToInflux() // temp = `["Connecting to InfluxDB", "INFO"]` // case "connectToTimescale": // // logLine("goRVExtension", fmt.Sprintf(`["Input: %s", "INFO"]`, C.GoString(input))) // go connectToTimescale() // temp = `["Connecting to TimescaleDB", "INFO"]` // case "getUnixTimeNano": // temp = fmt.Sprintf(`["%d", "INFO"]`, getUnixTimeNano()) // case "deinitialize": // logLine("goRVExtension", fmt.Sprintf(`["Input: %s", "INFO"]`, C.GoString(input))) // deinitialize() // temp = `["Deinitializing", "INFO"]` // default: // temp = fmt.Sprintf(`["Unknown command: %s", "ERROR"]`, C.GoString(input)) // } 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() {}