summaryrefslogtreecommitdiff
path: root/indra
diff options
context:
space:
mode:
Diffstat (limited to 'indra')
-rw-r--r--indra/llui/CMakeLists.txt2
-rw-r--r--indra/llui/llfloaterreglistener.cpp14
-rw-r--r--indra/llui/llfloaterreglistener.h2
-rw-r--r--indra/llui/llluafloater.cpp269
-rw-r--r--indra/llui/llluafloater.h54
-rw-r--r--indra/newview/scripts/lua/luafloater_demo.xml82
-rw-r--r--indra/newview/scripts/lua/luafloater_gesture_list.xml21
-rw-r--r--indra/newview/scripts/lua/test_luafloater_demo.lua78
-rw-r--r--indra/newview/scripts/lua/test_luafloater_gesture_list.lua74
-rw-r--r--indra/newview/scripts/lua/util.lua10
10 files changed, 606 insertions, 0 deletions
diff --git a/indra/llui/CMakeLists.txt b/indra/llui/CMakeLists.txt
index a0314cb5f2..69e1b57245 100644
--- a/indra/llui/CMakeLists.txt
+++ b/indra/llui/CMakeLists.txt
@@ -49,6 +49,7 @@ set(llui_SOURCE_FILES
lllineeditor.cpp
llloadingindicator.cpp
lllocalcliprect.cpp
+ llluafloater.cpp
llmenubutton.cpp
llmenugl.cpp
llmodaldialog.cpp
@@ -164,6 +165,7 @@ set(llui_HEADER_FILES
lllineeditor.h
llloadingindicator.h
lllocalcliprect.h
+ llluafloater.h
llmenubutton.h
llmenugl.h
llmodaldialog.h
diff --git a/indra/llui/llfloaterreglistener.cpp b/indra/llui/llfloaterreglistener.cpp
index 7525b8cab3..8316101264 100644
--- a/indra/llui/llfloaterreglistener.cpp
+++ b/indra/llui/llfloaterreglistener.cpp
@@ -37,6 +37,7 @@
#include "llfloaterreg.h"
#include "llfloater.h"
#include "llbutton.h"
+#include "llluafloater.h"
LLFloaterRegListener::LLFloaterRegListener():
LLEventAPI("LLFloaterReg",
@@ -72,6 +73,13 @@ LLFloaterRegListener::LLFloaterRegListener():
"Simulate clicking the named [\"button\"] in the visible floater named in [\"name\"]",
&LLFloaterRegListener::clickButton,
requiredNameButton);
+
+ add("showLuaFloater",
+ "Open the new floater using XML file specified in [\"xml_path\"] with ID in [\"reqid\"]",
+ &LLLuaFloater::showLuaFloater, {llsd::map("xml_path", LLSD(), "reqid", LLSD())});
+ add("getFloaterEvents",
+ "Return the table of Lua Floater events which are send to the script",
+ &LLFloaterRegListener::getLuaFloaterEvents);
}
void LLFloaterRegListener::getBuildMap(const LLSD& event) const
@@ -154,3 +162,9 @@ void LLFloaterRegListener::clickButton(const LLSD& event) const
LLEventPumps::instance().obtain(replyPump).post(reply);
}
}
+
+void LLFloaterRegListener::getLuaFloaterEvents(const LLSD &event) const
+{
+ Response response(llsd::map("events", LLLuaFloater::getEventsData()), event);
+}
+
diff --git a/indra/llui/llfloaterreglistener.h b/indra/llui/llfloaterreglistener.h
index 24311a2dfa..9cb0af2de5 100644
--- a/indra/llui/llfloaterreglistener.h
+++ b/indra/llui/llfloaterreglistener.h
@@ -49,6 +49,8 @@ private:
void toggleInstance(const LLSD& event) const;
void instanceVisible(const LLSD& event) const;
void clickButton(const LLSD& event) const;
+
+ void getLuaFloaterEvents(const LLSD &event) const;
};
#endif /* ! defined(LL_LLFLOATERREGLISTENER_H) */
diff --git a/indra/llui/llluafloater.cpp b/indra/llui/llluafloater.cpp
new file mode 100644
index 0000000000..afc287a864
--- /dev/null
+++ b/indra/llui/llluafloater.cpp
@@ -0,0 +1,269 @@
+/**
+ * @file llluafloater.cpp
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2024, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ *
+ */
+
+#include "llluafloater.h"
+
+#include <filesystem>
+#include "llevents.h"
+
+#include "llcheckboxctrl.h"
+#include "llcombobox.h"
+#include "llscrolllistctrl.h"
+
+const std::string LISTENER_NAME("LLLuaFloater");
+
+std::set<std::string> EVENT_LIST = {
+ "commit",
+ "double_click",
+ "mouse_enter",
+ "mouse_leave",
+ "mouse_down",
+ "mouse_up",
+ "right_mouse_down",
+ "right_mouse_up",
+ "post_build",
+ "floater_close"
+};
+
+LLLuaFloater::LLLuaFloater(const LLSD &key) :
+ LLFloater(key),
+ mDispatchListener(LLUUID::generateNewID().asString(), "action"),
+ mReplyPumpName(key["reply"].asString()),
+ mReqID(key)
+{
+ auto ctrl_lookup = [this](const LLSD &event, std::function<LLSD(LLUICtrl*,const LLSD&)> cb)
+ {
+ LLUICtrl *ctrl = getChild<LLUICtrl>(event["ctrl_name"].asString());
+ if (!ctrl)
+ {
+ LL_WARNS("LuaFloater") << "Control not found: " << event["ctrl_name"] << LL_ENDL;
+ return LLSD();
+ }
+ return cb(ctrl, event);
+ };
+
+ LLSD requiredParams = llsd::map("ctrl_name", LLSD(), "value", LLSD());
+ mDispatchListener.add("set_enabled", "", [ctrl_lookup](const LLSD &event)
+ {
+ return ctrl_lookup(event, [](LLUICtrl *ctrl, const LLSD &event) { ctrl->setEnabled(event["value"].asBoolean()); return LLSD(); });
+ }, requiredParams);
+ mDispatchListener.add("set_visible", "", [ctrl_lookup](const LLSD &event)
+ {
+ return ctrl_lookup(event, [](LLUICtrl *ctrl, const LLSD &event) { ctrl->setVisible(event["value"].asBoolean()); return LLSD(); });
+ }, requiredParams);
+ mDispatchListener.add("set_value", "", [ctrl_lookup](const LLSD &event)
+ {
+ return ctrl_lookup(event, [](LLUICtrl *ctrl, const LLSD &event) { ctrl->setValue(event["value"]); return LLSD(); });
+ }, requiredParams);
+
+ mDispatchListener.add("add_list_element", "", [this](const LLSD &event)
+ {
+ LLScrollListCtrl *ctrl = getChild<LLScrollListCtrl>(event["ctrl_name"].asString());
+ if(ctrl)
+ {
+ LLSD element_data = event["value"];
+ if (element_data.isArray())
+ {
+ for (const auto &row : llsd::inArray(element_data))
+ {
+ ctrl->addElement(row);
+ }
+ }
+ else
+ {
+ ctrl->addElement(element_data);
+ }
+ }
+ }, requiredParams);
+
+ mDispatchListener.add("set_title", "", [this](const LLSD &event)
+ {
+ setTitle(event["value"].asString());
+ }, llsd::map("value", LLSD()));
+
+ mDispatchListener.add("get_value", "", [ctrl_lookup](const LLSD &event)
+ {
+ return ctrl_lookup(event, [](LLUICtrl *ctrl, const LLSD &event) { return llsd::map("value", ctrl->getValue()); });
+ }, llsd::map("ctrl_name", LLSD(), "reqid", LLSD()));
+}
+
+LLLuaFloater::~LLLuaFloater()
+{
+ //post empty LLSD() to indicate done, in case it wasn't handled by the script after CLOSE_EVENT
+ post(LLSD());
+}
+
+BOOL LLLuaFloater::postBuild()
+{
+ for (LLView *view : *getChildList())
+ {
+ LLUICtrl *ctrl = dynamic_cast<LLUICtrl*>(view);
+ if (ctrl)
+ {
+ LLSD data;
+ data["ctrl_name"] = view->getName();
+
+ ctrl->setCommitCallback([this, data](LLUICtrl *ctrl, const LLSD &param)
+ {
+ LLSD event(data);
+ event["value"] = ctrl->getValue();
+ postEvent(event, "commit");
+ });
+ }
+ }
+
+ //optional field to send additional specified events to the script
+ if (mKey.has("extra_events"))
+ {
+ //the first value is ctrl name, the second contains array of events to send
+ for (const auto &[name, data] : llsd::inMap(mKey["extra_events"]))
+ {
+ for (const auto &event : llsd::inArray(data))
+ {
+ registerCallback(name, event);
+ }
+ }
+ }
+
+ //send pump name to the script after the floater is built
+ postEvent(llsd::map("command_name", mDispatchListener.getPumpName()), "post_build");
+
+ return true;
+}
+
+void LLLuaFloater::onClose(bool app_quitting)
+{
+ postEvent(llsd::map("app_quitting", app_quitting), "floater_close");
+}
+
+bool event_is(const std::string &event_name, const std::string &list_event)
+{
+ llassert(EVENT_LIST.find(list_event) != EVENT_LIST.end());
+ return (event_name == list_event);
+}
+
+void LLLuaFloater::registerCallback(const std::string &ctrl_name, const std::string &event)
+{
+ LLUICtrl *ctrl = getChild<LLUICtrl>(ctrl_name);
+ if (!ctrl) return;
+
+ LLSD data;
+ data["ctrl_name"] = ctrl_name;
+ data["event"] = event;
+
+ auto mouse_event_cb = [this, data](LLUICtrl *ctrl, const LLSD &param) { post(data); };
+
+ auto mouse_event_coords_cb = [this, data](LLUICtrl *ctrl, S32 x, S32 y, MASK mask)
+ {
+ LLSD event(data);
+ post(event.with("x", x).with("y", y));
+ };
+
+ if (event_is(event, "mouse_enter"))
+ {
+ ctrl->setMouseEnterCallback(mouse_event_cb);
+ }
+ else if (event_is(event, "mouse_leave"))
+ {
+ ctrl->setMouseLeaveCallback(mouse_event_cb);
+ }
+ else if (event_is(event, "mouse_down"))
+ {
+ ctrl->setMouseDownCallback(mouse_event_coords_cb);
+ }
+ else if (event_is(event, "mouse_up"))
+ {
+ ctrl->setMouseUpCallback(mouse_event_coords_cb);
+ }
+ else if (event_is(event, "right_mouse_down"))
+ {
+ ctrl->setRightMouseDownCallback(mouse_event_coords_cb);
+ }
+ else if (event_is(event, "right_mouse_up"))
+ {
+ ctrl->setRightMouseUpCallback(mouse_event_coords_cb);
+ }
+ else if (event_is(event, "double_click"))
+ {
+ LLScrollListCtrl *list = dynamic_cast<LLScrollListCtrl *>(ctrl);
+ if (list)
+ {
+ list->setDoubleClickCallback(
+ [this, data, list]()
+ {
+ LLSD event(data);
+ post(event.with("value", list->getCurrentID()));
+ });
+ }
+ else
+ {
+ ctrl->setDoubleClickCallback(mouse_event_coords_cb);
+ }
+ }
+ else
+ {
+ LL_WARNS("LuaFloater") << "Can't register callback for unknown event: " << event << " , control: " << ctrl_name << LL_ENDL;
+ }
+}
+
+void LLLuaFloater::post(const LLSD &data)
+{
+ // send event data to the script signed with ["reqid"] key
+ LLSD stamped_data(data);
+ mReqID.stamp(stamped_data);
+ LLEventPumps::instance().obtain(mReplyPumpName).post(stamped_data);
+}
+
+void LLLuaFloater::postEvent(LLSD data, const std::string &event_name)
+{
+ llassert(EVENT_LIST.find(event_name) != EVENT_LIST.end());
+ post(data.with("event", event_name));
+}
+
+void LLLuaFloater::showLuaFloater(const LLSD &data)
+{
+ std::filesystem::path fs_path(data["xml_path"].asString());
+ std::string path = fs_path.lexically_normal().string();
+ if (!fs_path.is_absolute())
+ {
+ std::string lib_path = gDirUtilp->getExpandedFilename(LL_PATH_SCRIPTS, "lua");
+ path = (std::filesystem::path(lib_path) / path).u8string();
+ }
+
+ LLLuaFloater *floater = new LLLuaFloater(data);
+ floater->buildFromFile(path);
+ floater->openFloater(floater->getKey());
+}
+
+LLSD LLLuaFloater::getEventsData()
+{
+ LLSD event_data;
+ for (auto &it : EVENT_LIST)
+ {
+ event_data.append(it);
+ }
+ return event_data;
+}
diff --git a/indra/llui/llluafloater.h b/indra/llui/llluafloater.h
new file mode 100644
index 0000000000..ccc3ccb39b
--- /dev/null
+++ b/indra/llui/llluafloater.h
@@ -0,0 +1,54 @@
+/**
+ * @file llluafloater.h
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2024, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#ifndef LL_LLLUAFLOATER_H
+#define LL_LLLUAFLOATER_H
+
+#include "llfloater.h"
+#include "lleventdispatcher.h"
+#include "llevents.h"
+
+class LLLuaFloater : public LLFloater
+{
+public:
+ LLLuaFloater(const LLSD &key);
+ BOOL postBuild();
+ virtual ~LLLuaFloater();
+
+ void registerCallback(const std::string &ctrl_name, const std::string &event);
+ void onClose(bool app_quitting);
+
+ void post(const LLSD &data);
+ void postEvent(LLSD data, const std::string &event);
+ static void showLuaFloater(const LLSD &data);
+ static LLSD getEventsData();
+
+private:
+ LLReqID mReqID;
+ LLDispatchListener mDispatchListener;
+
+ std::string mReplyPumpName;
+};
+#endif
diff --git a/indra/newview/scripts/lua/luafloater_demo.xml b/indra/newview/scripts/lua/luafloater_demo.xml
new file mode 100644
index 0000000000..069f229128
--- /dev/null
+++ b/indra/newview/scripts/lua/luafloater_demo.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<floater
+ legacy_header_height="18"
+ height="150"
+ layout="topleft"
+ name="lua_demo"
+ title="LUA"
+ width="320">
+ <check_box
+ height="16"
+ label="Disable button"
+ layout="topleft"
+ top_pad="35"
+ left="5"
+ name="disable_ctrl"
+ width="146" />
+ <line_editor
+ border_style="line"
+ border_thickness="1"
+ follows="left|bottom"
+ font="SansSerif"
+ height="20"
+ layout="topleft"
+ max_length_bytes="50"
+ name="openfloater_cmd"
+ top_delta="25"
+ width="100" />
+ <button
+ follows="left|bottom"
+ height="23"
+ label="Open floater"
+ layout="topleft"
+ name="open_btn"
+ top_delta="25"
+ width="100" >
+ </button>
+ <text
+ type="string"
+ follows="left|top"
+ height="10"
+ layout="topleft"
+ top="30"
+ left_delta="170"
+ name="title_lbl">
+ Select title
+ </text>
+ <combo_box
+ follows="top|left"
+ height="23"
+ name="title_cmb"
+ top_pad="5"
+ width="100">
+ <combo_box.item
+ label="LUA"
+ value="LUA" />
+ <combo_box.item
+ label="LUA floater"
+ value="LUA floater" />
+ <combo_box.item
+ label="LUA-U title"
+ value="LUA-U title" />
+ </combo_box>
+ <text
+ type="string"
+ follows="left|bottom"
+ height="10"
+ layout="topleft"
+ top_delta="50"
+ name="show_time_lbl">
+ Double click me
+ </text>
+ <text
+ type="string"
+ follows="left|top"
+ height="10"
+ layout="topleft"
+ top_pad="15"
+ font="SansSerif"
+ text_color="white"
+ left_delta="15"
+ name="time_lbl"/>
+</floater>
diff --git a/indra/newview/scripts/lua/luafloater_gesture_list.xml b/indra/newview/scripts/lua/luafloater_gesture_list.xml
new file mode 100644
index 0000000000..a38a04eed0
--- /dev/null
+++ b/indra/newview/scripts/lua/luafloater_gesture_list.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<floater
+ legacy_header_height="18"
+ height="150"
+ layout="topleft"
+ name="lua_gestures"
+ title="Gestures"
+ width="320">
+ <scroll_list
+ draw_heading="false"
+ left="5"
+ width="310"
+ height="115"
+ top_pad ="25"
+ follows="all"
+ name="gesture_list">
+ <scroll_list.columns
+ name="gesture_name"
+ label="Name"/>
+ </scroll_list>
+</floater>
diff --git a/indra/newview/scripts/lua/test_luafloater_demo.lua b/indra/newview/scripts/lua/test_luafloater_demo.lua
new file mode 100644
index 0000000000..b81259c060
--- /dev/null
+++ b/indra/newview/scripts/lua/test_luafloater_demo.lua
@@ -0,0 +1,78 @@
+XML_FILE_PATH = "luafloater_demo.xml"
+
+leap = require 'leap'
+coro = require 'coro'
+util = require 'util'
+
+--event pump for sending actions to the floater
+COMMAND_PUMP_NAME = ""
+--table of floater UI events
+event_list={}
+coro.launch(function ()
+ event_list = leap.request("LLFloaterReg", {op="getFloaterEvents"})["events"]
+ leap.done()
+end)
+leap.process()
+
+local function _event(event_name)
+ if not util.contains(event_list, event_name) then
+ print_warning("Incorrect event name: " .. event_name)
+ end
+ return event_name
+end
+
+function post(action)
+ leap.send(COMMAND_PUMP_NAME, action)
+end
+
+function getCurrentTime()
+ local currentTime = os.date("*t")
+ return string.format("%02d:%02d:%02d", currentTime.hour, currentTime.min, currentTime.sec)
+end
+
+function handleEvents(event_data)
+ if event_data.event == _event("commit") then
+ if event_data.ctrl_name == "disable_ctrl" then
+ post({action="set_enabled", ctrl_name="open_btn", value = (1 - event_data.value)})
+ elseif event_data.ctrl_name == "title_cmb" then
+ post({action="set_title", value= event_data.value})
+ elseif event_data.ctrl_name == "open_btn" then
+ floater_name = leap.request(COMMAND_PUMP_NAME, {action="get_value", ctrl_name='openfloater_cmd'})['value']
+ leap.send("LLFloaterReg", {name = floater_name, op = "showInstance"})
+ end
+ elseif event_data.event == _event("double_click") then
+ if event_data.ctrl_name == "show_time_lbl" then
+ post({action="set_value", ctrl_name="time_lbl", value= getCurrentTime()})
+ end
+ elseif event_data.event == _event("floater_close") then
+ print_warning("Floater was closed")
+ leap.done()
+ end
+end
+
+local key = {xml_path = XML_FILE_PATH, op = "showLuaFloater"}
+--sign for additional events for defined control {<control_name>= {action1, action2, ...}}
+key.extra_events={show_time_lbl = {_event("right_mouse_down"), _event("double_click")}}
+coro.launch(function ()
+ --script received event pump name, after floater was built
+ COMMAND_PUMP_NAME = leap.request("LLFloaterReg", key)["command_name"]
+ leap.done()
+end)
+leap.process()
+
+catch_events = leap.WaitFor:new(-1, "all_events")
+function catch_events:filter(pump, data)
+ return data
+end
+
+function process_events(waitfor)
+ event_data = waitfor:wait()
+ while event_data do
+ handleEvents(event_data)
+ event_data = waitfor:wait()
+ end
+end
+
+coro.launch(process_events, catch_events)
+leap.process()
+print_warning("End of the script")
diff --git a/indra/newview/scripts/lua/test_luafloater_gesture_list.lua b/indra/newview/scripts/lua/test_luafloater_gesture_list.lua
new file mode 100644
index 0000000000..b46e36b4d9
--- /dev/null
+++ b/indra/newview/scripts/lua/test_luafloater_gesture_list.lua
@@ -0,0 +1,74 @@
+XML_FILE_PATH = "luafloater_gesture_list.xml"
+
+leap = require 'leap'
+coro = require 'coro'
+util = require 'util'
+LLGesture = require 'LLGesture'
+
+--event pump for sending actions to the floater
+COMMAND_PUMP_NAME = ""
+--table of floater UI events
+event_list={}
+coro.launch(function ()
+ event_list = leap.request("LLFloaterReg", {op="getFloaterEvents"})["events"]
+ leap.done()
+end)
+leap.process()
+
+local function _event(event_name)
+ if not util.contains(event_list, event_name) then
+ print_warning("Incorrect event name: " .. event_name)
+ end
+ return event_name
+end
+
+function post(action)
+ leap.send(COMMAND_PUMP_NAME, action)
+end
+
+function handleEvents(event_data)
+ if event_data.event == _event("floater_close") then
+ leap.done()
+ elseif event_data.event == _event("post_build") then
+ COMMAND_PUMP_NAME = event_data.command_name
+ gestures_uuid = LLGesture.getActiveGestures()
+ local action_data = {}
+ action_data.action = "add_list_element"
+ action_data.ctrl_name = "gesture_list"
+ gestures = {}
+ for uuid, info in pairs(gestures_uuid) do
+ table.insert(gestures, {value = uuid, columns ={column = "gesture_name", value = info.name}})
+ end
+ action_data.value = gestures
+ post(action_data)
+ elseif event_data.event == _event("double_click") then
+ if event_data.ctrl_name == "gesture_list" then
+ LLGesture.startGesture(event_data.value)
+ end
+ end
+end
+
+local key = {xml_path = XML_FILE_PATH, op = "showLuaFloater"}
+--receive additional events for defined control {<control_name>= {action1, action2, ...}}
+key.extra_events={gesture_list = {_event("double_click")}}
+coro.launch(function ()
+ handleEvents(leap.request("LLFloaterReg", key))
+ leap.done()
+end)
+leap.process()
+
+catch_events = leap.WaitFor:new(-1, "all_events")
+function catch_events:filter(pump, data)
+ return data
+end
+
+function process_events(waitfor)
+ event_data = waitfor:wait()
+ while event_data do
+ handleEvents(event_data)
+ event_data = waitfor:wait()
+ end
+end
+
+coro.launch(process_events, catch_events)
+leap.process()
diff --git a/indra/newview/scripts/lua/util.lua b/indra/newview/scripts/lua/util.lua
index e3af633ea7..5d6042dfe5 100644
--- a/indra/newview/scripts/lua/util.lua
+++ b/indra/newview/scripts/lua/util.lua
@@ -36,4 +36,14 @@ function util.equal(t1, t2)
return util.empty(temp)
end
+-- check if array-like table contains certain value
+function util.contains(t, v)
+ for _, value in ipairs(t) do
+ if value == v then
+ return true
+ end
+ end
+ return false
+end
+
return util