diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 6e5e05145..c6c0ee65f 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1345,6 +1345,11 @@ function UTILS.VecSubstract(a, b) return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z} end +--- Substract is not a word, don't want to rename the original function because it's been around since forever +function UTILS.VecSubtract(a, b) + return UTILS.VecSubstract(a, b) +end + --- Calculate the difference between two 2D vectors by substracting the x,y components from each other. -- @param DCS#Vec2 a Vector in 2D with x, y components. -- @param DCS#Vec2 b Vector in 2D with x, y components. @@ -1353,6 +1358,11 @@ function UTILS.Vec2Substract(a, b) return {x=a.x-b.x, y=a.y-b.y} end +--- Substract is not a word, don't want to rename the original function because it's been around since forever +function UTILS.Vec2Subtract(a, b) + return UTILS.Vec2Substract(a, b) +end + --- Calculate the total vector of two 3D vectors by adding the x,y,z components of each other. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param DCS#Vec3 b Vector in 3D with x, y, z components. @@ -3101,3 +3111,491 @@ function UTILS.PlotRacetrack(Coordinate, Altitude, Speed, Heading, Leg, Coalitio circle_center_two_three:CircleToAll(UTILS.NMToMeters(turn_radius), coalition, color, alpha, nil, 0, lineType)--, ReadOnly, Text) end + +--- Get the current time in a "nice" format like 21:01:15 +-- @return #string Returns string with the current time +function UTILS.TimeNow() + return UTILS.SecondsToClock(timer.getAbsTime(), false, false) +end + + +--- Given 2 "nice" time string, returns the difference between the two in seconds +-- @param #string start_time Time string like "07:15:22" +-- @param #string end_time Time string like "08:11:27" +-- @return #number Seconds between start_time and end_time +function UTILS.TimeDifferenceInSeconds(start_time, end_time) + return UTILS.ClockToSeconds(end_time) - UTILS.ClockToSeconds(start_time) +end + +--- Given check if the current time is later than time_string. +-- @param #string start_time Time string like "07:15:22" +-- @return #boolean True if later, False if before +function UTILS.TimeLaterThan(time_string) + if timer.getAbsTime() > UTILS.ClockToSeconds(time_string) then + return true + end + return false +end + +--- Given check if the current time is before time_string. +-- @param #string start_time Time string like "07:15:22" +-- @return #boolean False if later, True if before +function UTILS.TimeBefore(time_string) + if timer.getAbsTime() < UTILS.ClockToSeconds(time_string) then + return true + end + return false +end + + +--- Combines two time strings to give you a new time. For example "15:16:32" and "02:06:24" would return "17:22:56" +-- @param #string time_string_01 Time string like "07:15:22" +-- @param #string time_string_02 Time string like "08:11:27" +-- @return #string Result of the two time string combined +function UTILS.CombineTimeStrings(time_string_01, time_string_02) + local hours1, minutes1, seconds1 = time_string_01:match("(%d+):(%d+):(%d+)") + local hours2, minutes2, seconds2 = time_string_02:match("(%d+):(%d+):(%d+)") + local total_seconds = tonumber(seconds1) + tonumber(seconds2) + tonumber(minutes1) * 60 + tonumber(minutes2) * 60 + tonumber(hours1) * 3600 + tonumber(hours2) * 3600 + + total_seconds = total_seconds % (24 * 3600) + if total_seconds < 0 then + total_seconds = total_seconds + 24 * 3600 + end + + local hours = math.floor(total_seconds / 3600) + total_seconds = total_seconds - hours * 3600 + local minutes = math.floor(total_seconds / 60) + local seconds = total_seconds % 60 + + return string.format("%02d:%02d:%02d", hours, minutes, seconds) +end + + +--- Subtracts two time string to give you a new time. For example "15:16:32" and "02:06:24" would return "13:10:08" +-- @param #string time_string_01 Time string like "07:15:22" +-- @param #string time_string_02 Time string like "08:11:27" +-- @return #string Result of the two time string subtracted +function UTILS.SubtractTimeStrings(time_string_01, time_string_02) + local hours1, minutes1, seconds1 = time_string_01:match("(%d+):(%d+):(%d+)") + local hours2, minutes2, seconds2 = time_string_02:match("(%d+):(%d+):(%d+)") + local total_seconds = tonumber(seconds1) - tonumber(seconds2) + tonumber(minutes1) * 60 - tonumber(minutes2) * 60 + tonumber(hours1) * 3600 - tonumber(hours2) * 3600 + + total_seconds = total_seconds % (24 * 3600) + if total_seconds < 0 then + total_seconds = total_seconds + 24 * 3600 + end + + local hours = math.floor(total_seconds / 3600) + total_seconds = total_seconds - hours * 3600 + local minutes = math.floor(total_seconds / 60) + local seconds = total_seconds % 60 + + return string.format("%02d:%02d:%02d", hours, minutes, seconds) +end + +--- Checks if the current time is in between start_time and end_time +-- @param #string time_string_01 Time string like "07:15:22" +-- @param #string time_string_02 Time string like "08:11:27" +-- @return #bool True if it is, False if it's not +function UTILS.TimeBetween(start_time, end_time) + return UTILS.TimeLaterThan(start_time) and UTILS.TimeBefore(end_time) +end + +--- Easy to read one line to roll the dice on something. 1% is very unlikely to happen, 99% is very likely to happen +-- @param #number chance (optional) Percentage chance you want something to happen. Defaults to a random number if not given +-- @return #bool True if the dice roll was within the given percentage chance of happening +function UTILS.PercentageChance(chance) + chance = chance or math.random(0, 100) + chance = UTILS.Clamp(chance, 0, 100) + local percentage = math.random(0, 100) + if percentage < chance then + return true + end + return false +end + +--- Easy to read one liner to clamp a value +-- @param #number value Input value +-- @param #number min Minimal value that should be respected +-- @param #number max Maximal value that should be respected +-- @return #number Clamped value +function UTILS.Clamp(value, min, max) + if value < min then value = min end + if value > max then value = max end + + return value +end + +--- Clamp an angle so that it's always between 0 and 360 while still being correct +-- @param #number value Input value +-- @return #number Clamped value +function UTILS.ClampAngle(value) + if value > 360 then return value - 360 end + if value < 0 then return value + 360 end + return value +end + +--- Remap an input to a new value in a given range. For example: +--- UTILS.RemapValue(20, 10, 30, 0, 200) would return 50 +--- 20 is 50% between 10 and 30 +--- 50% between 0 and 200 is 100 +-- @param #number value Input value +-- @param #number old_min Min value to remap from +-- @param #number old_max Max value to remap from +-- @param #number new_min Min value to remap to +-- @param #number new_max Max value to remap to +-- @return #number Remapped value +function UTILS.RemapValue(value, old_min, old_max, new_min, new_max) + new_min = new_min or 0 + new_max = new_max or 100 + + local old_range = old_max - old_min + local new_range = new_max - new_min + local percentage = (value - old_min) / old_range + return (new_range * percentage) + new_min +end + +--- Given a triangle made out of 3 vector 2s, return a vec2 that is a random number in this triangle +-- @param #Vec2 pt1 Min value to remap from +-- @param #Vec2 pt2 Max value to remap from +-- @param #Vec2 pt3 Max value to remap from +-- @return #Vec2 Random point in triangle +function UTILS.RandomPointInTriangle(pt1, pt2, pt3) + local pt = {math.random(), math.random()} + table.sort(pt) + local s = pt[1] + local t = pt[2] - pt[1] + local u = 1 - pt[2] + + return {x = s * pt1.x + t * pt2.x + u * pt3.x, + y = s * pt1.y + t * pt2.y + u * pt3.y} +end + +--- Checks if a given angle (heading) is between 2 other angles. Min and max have to be given in clockwise order For example: +--- UTILS.AngleBetween(350, 270, 15) would return True +--- UTILS.AngleBetween(22, 95, 20) would return False +-- @param #number angle Min value to remap from +-- @param #number min Max value to remap from +-- @param #number max Max value to remap from +-- @return #bool +function UTILS.AngleBetween(angle, min, max) + angle = (360 + (angle % 360)) % 360 + min = (360 + min % 360) % 360 + max = (360 + max % 360) % 360 + + if min < max then return min <= angle and angle <= max end + return min <= angle or angle <= max +end + +--- Easy to read one liner to write a JSON file. Everything in @data should be serializable +--- json.lua exists in the DCS install Scripts folder +-- @param #table data table to write +-- @param #string file_path File path +function UTILS.WriteJSON(data, file_path) + package.path = package.path .. ";.\\Scripts\\?.lua" + local JSON = require("json") + local pretty_json_text = JSON:encode_pretty(data) + local write_file = io.open(file_path, "w") + write_file:write(pretty_json_text) + write_file:close() +end + +--- Easy to read one liner to read a JSON file. +--- json.lua exists in the DCS install Scripts folder +-- @param #string file_path File path +-- @return #table +function UTILS.ReadJSON(file_path) + package.path = package.path .. ";.\\Scripts\\?.lua" + local JSON = require("json") + local read_file = io.open(file_path, "r") + local contents = read_file:read( "*a" ) + io.close(read_file) + return JSON:decode(contents) +end + +--- Get the properties names and values of properties set up on a Zone in the Mission Editor. +--- This doesn't work for any zones created in MOOSE +-- @param #string zone_name Name of the zone as set up in the Mission Editor +-- @return #table with all the properties on a zone +function UTILS.GetZoneProperties(zone_name) + local return_table = {} + for _, zone in pairs(env.mission.triggers.zones) do + if zone["name"] == zone_name then + if table.length(zone["properties"]) > 0 then + for _, property in pairs(zone["properties"]) do + return_table[property["key"]] = property["value"] + end + return return_table + else + BASE:I(string.format("%s doesn't have any properties", zone_name)) + return {} + end + end + end +end + +--- Rotates a point around another point with a given angle. Useful if you're loading in groups or +--- statics but you want to rotate them all as a collection. You can get the center point of everything +--- and then rotate all the positions of every object around this center point. +-- @param #Vec2 point Point that you want to rotate +-- @param #Vec2 pivot Pivot point of the rotation +-- @param #number angle How many degrees the point should be rotated +-- @return #Vec Rotated point +function UTILS.RotatePointAroundPivot(point, pivot, angle) + local radians = math.rad(angle) + + local x = point.x - pivot.x + local y = point.y - pivot.y + + local rotated_x = x * math.cos(radians) - y * math.sin(radians) + local rotatex_y = x * math.sin(radians) + y * math.cos(radians) + + local original_x = rotated_x + pivot.x + local original_y = rotatex_y + pivot.y + + return { x = original_x, y = original_y } +end + +--- Makes a string semi-unique by attaching a random number between 0 and 1 million to it +-- @param #string base String you want to unique-fy +-- @return #string Unique string +function UTILS.UniqueName(base) + base = base or "" + local ran = tostring(math.random(0, 1000000)) + + if base == "" then + return ran + end + return base .. "_" .. ran +end + +--- Check if a string starts with something +-- @param #string str String to check +-- @param #string value +-- @return #bool True if str starts with value +function string.startswith(str, value) + return string.sub(str,1,string.len(value)) == value +end + + +--- Check if a string ends with something +-- @param #string str String to check +-- @param #string value +-- @return #bool True if str ends with value +function string.endswith(str, value) + return value == "" or str:sub(-#value) == value +end + +--- Splits a string on a separator. For example: +--- string.split("hello_dcs_world", "-") would return {"hello", "dcs", "world"} +-- @param #string input String to split +-- @param #string separator What to split on +-- @return #table individual strings +function string.split(input, separator) + local parts = {} + for part in input:gmatch("[^" .. separator .. "]+") do + table.insert(parts, part) + end + return parts +end + + +--- Checks if a string contains a substring. Easier to remember for Python people :) +--- string.split("hello_dcs_world", "-") would return {"hello", "dcs", "world"} +-- @param #string str +-- @param #string value +-- @return #bool True if str contains value +function string.contains(str, value) + return string.match(str, value) +end + +--- Given tbl is a indexed table ({"hello", "dcs", "world"}), checks if element exists in the table. +--- The table can be made up out of complex tables or values as well +-- @param #table tbl +-- @param #string element +-- @return #bool True if tbl contains element +function table.contains(tbl, element) + if element == nil or tbl == nil then return false end + + local index = 1 + while tbl[index] do + if tbl[index] == element then + return true + end + index = index + 1 + end + return false +end + +--- Checks if a table contains a specific key. +-- @param #table tbl Table to check +-- @param #string key Key to look for +-- @return #bool True if tbl contains key +function table.contains_key(tbl, key) + if tbl[key] ~= nil then return true else return false end +end + +--- Inserts a unique element into a table. +-- @param #table tbl Table to insert into +-- @param #string element Element to insert +function table.insert_unique(tbl, element) + if element == nil or tbl == nil then return end + + if not table.contains(tbl, element) then + table.insert(tbl, element) + end +end + +--- Removes an element from a table by its value. +-- @param #table tbl Table to remove from +-- @param #string element Element to remove +function table.remove_by_value(tbl, element) + local indices_to_remove = {} + local index = 1 + for _, value in pairs(tbl) do + if value == element then + table.insert(indices_to_remove, index) + end + index = index + 1 + end + + for _, idx in pairs(indices_to_remove) do + table.remove(tbl, idx) + end +end + +--- Removes an element from a table by its key. +-- @param #table table Table to remove from +-- @param #string key Key of the element to remove +-- @return #string Removed element +function table.remove_key(table, key) + local element = table[key] + table[key] = nil + return element +end + +--- Finds the index of an element in a table. +-- @param #table table Table to search +-- @param #string element Element to find +-- @return #int Index of the element, or nil if not found +function table.index_of(table, element) + for i, v in ipairs(table) do + if v == element then + return i + end + end + return nil +end + +--- Counts the number of elements in a table. +-- @param #table T Table to count +-- @return #int Number of elements in the table +function table.length(T) + local count = 0 + for _ in pairs(T) do count = count + 1 end + return count +end + +--- Slices a table between two indices, much like Python's my_list[2:-1] +-- @param #table tbl Table to slice +-- @param #int first Starting index +-- @param #int last Ending index +-- @return #table Sliced table +function table.slice(tbl, first, last) + local sliced = {} + local start = first or 1 + local stop = last or table.length(tbl) + local count = 1 + + for key, value in pairs(tbl) do + if count >= start and count <= stop then + sliced[key] = value + end + count = count + 1 + end + + return sliced +end + +--- Counts the number of occurrences of a value in a table. +-- @param #table tbl Table to search +-- @param #string value Value to count +-- @return #int Number of occurrences of the value +function table.count_value(tbl, value) + local count = 0 + for _, item in pairs(tbl) do + if item == value then count = count + 1 end + end + return count +end + +--- Add 2 table together, t2 gets added to t1 +-- @param #table t1 First table +-- @param #table t2 Second table +-- @return #table Combined table +function table.combine(t1, t2) + if t1 == nil and t2 == nil then + BASE:E("Both tables were empty!") + end + + if t1 == nil then return t2 end + if t2 == nil then return t1 end + for i=1,#t2 do + t1[#t1+1] = t2[i] + end + return t1 +end + +--- Merges two tables into one. If a key exists in both t1 and t2, the value of t1 with be overwritten by the value of t2 +-- @param #table t1 First table +-- @param #table t2 Second table +-- @return #table Merged table +function table.merge(t1, t2) + for k, v in pairs(t2) do + if (type(v) == "table") and (type(t1[k] or false) == "table") then + table.merge(t1[k], t2[k]) + else + t1[k] = v + end + end + return t1 +end + +--- Adds an item to the end of a table. +-- @param #table tbl Table to add to +-- @param #string item Item to add +function table.add(tbl, item) + tbl[#tbl + 1] = item +end + +--- Shuffles the elements of a table. +-- @param #table tbl Table to shuffle +-- @return #table Shuffled table +function table.shuffle(tbl) + local new_table = {} + for _, value in ipairs(tbl) do + local pos = math.random(1, #new_table +1) + table.insert(new_table, pos, value) + end + return new_table +end + +--- Finds a key-value pair in a table. +-- @param #table tbl Table to search +-- @param #string key Key to find +-- @param #string value Value to find +-- @return #table Table containing the key-value pair, or nil if not found +function table.find_key_value_pair(tbl, key, value) + for k, v in pairs(tbl) do + if type(v) == "table" then + local result = table.find_key_value_pair(v, key, value) + if result ~= nil then + return result + end + elseif k == key and v == value then + return tbl + end + end + return nil +end +