summaryrefslogtreecommitdiff
path: root/indra
diff options
context:
space:
mode:
Diffstat (limited to 'indra')
-rw-r--r--indra/llcommon/llassettype.cpp17
-rw-r--r--indra/llcommon/llassettype.h2
-rw-r--r--indra/llcommon/lua_function.cpp2
-rw-r--r--indra/llinventory/llfoldertype.cpp19
-rw-r--r--indra/llinventory/llfoldertype.h2
-rw-r--r--indra/newview/CMakeLists.txt2
-rw-r--r--indra/newview/llinventoryfunctions.h2
-rw-r--r--indra/newview/llinventorylistener.cpp298
-rw-r--r--indra/newview/llinventorylistener.h48
-rw-r--r--indra/newview/llinventorymodel.cpp8
-rw-r--r--indra/newview/llviewerinventory.cpp3
-rw-r--r--indra/newview/scripts/lua/require/LLInventory.lua48
-rw-r--r--indra/newview/scripts/lua/test_LLInventory.lua21
13 files changed, 471 insertions, 1 deletions
diff --git a/indra/llcommon/llassettype.cpp b/indra/llcommon/llassettype.cpp
index 3e46bde954..00c61c6e0a 100644
--- a/indra/llcommon/llassettype.cpp
+++ b/indra/llcommon/llassettype.cpp
@@ -30,6 +30,7 @@
#include "lldictionary.h"
#include "llmemory.h"
#include "llsingleton.h"
+#include "llsd.h"
///----------------------------------------------------------------------------
/// Class LLAssetType
@@ -244,3 +245,19 @@ bool LLAssetType::lookupIsAssetIDKnowable(EType asset_type)
}
return false;
}
+
+LLSD LLAssetType::getTypeNames()
+{
+ LLSD type_names;
+ const LLAssetDictionary *dict = LLAssetDictionary::getInstance();
+ for (S32 type = 0; type < AT_COUNT; ++type)
+ {
+ const AssetEntry *entry = dict->lookup(LLAssetType::EType(type));
+ // skip llassettype_bad_lookup
+ if (entry)
+ {
+ type_names.append(entry->mTypeName);
+ }
+ }
+ return type_names;
+}
diff --git a/indra/llcommon/llassettype.h b/indra/llcommon/llassettype.h
index 1989155550..4185063ae5 100644
--- a/indra/llcommon/llassettype.h
+++ b/indra/llcommon/llassettype.h
@@ -163,6 +163,8 @@ public:
static bool lookupIsAssetFetchByIDAllowed(EType asset_type); // the asset allows direct download
static bool lookupIsAssetIDKnowable(EType asset_type); // asset data can be known by the viewer
+ static LLSD getTypeNames();
+
static const std::string BADLOOKUP;
protected:
diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp
index 452bc24b16..880bc209f6 100644
--- a/indra/llcommon/lua_function.cpp
+++ b/indra/llcommon/lua_function.cpp
@@ -35,7 +35,7 @@
using namespace std::literals; // e.g. std::string_view literals: "this"sv
-const S32 INTERRUPTS_MAX_LIMIT = 20000;
+const S32 INTERRUPTS_MAX_LIMIT = 100000;
const S32 INTERRUPTS_SUSPEND_LIMIT = 100;
#define lua_register(L, n, f) (lua_pushcfunction(L, (f), n), lua_setglobal(L, (n)))
diff --git a/indra/llinventory/llfoldertype.cpp b/indra/llinventory/llfoldertype.cpp
index 8f968ae2fd..95deb46431 100644
--- a/indra/llinventory/llfoldertype.cpp
+++ b/indra/llinventory/llfoldertype.cpp
@@ -29,6 +29,7 @@
#include "llfoldertype.h"
#include "lldictionary.h"
#include "llmemory.h"
+#include "llsd.h"
#include "llsingleton.h"
///----------------------------------------------------------------------------
@@ -220,3 +221,21 @@ const std::string &LLFolderType::badLookup()
static const std::string sBadLookup = "llfoldertype_bad_lookup";
return sBadLookup;
}
+
+LLSD LLFolderType::getTypeNames()
+{
+ LLSD type_names;
+ const LLFolderDictionary *dict = LLFolderDictionary::getInstance();
+ for (S32 type = 0; type < FT_COUNT; ++type)
+ {
+ if (lookupIsEnsembleType(LLFolderType::EType(type))) continue;
+
+ const FolderEntry *entry = dict->lookup(LLFolderType::EType(type));
+ //skip llfoldertype_bad_lookup
+ if (entry)
+ {
+ type_names.append(entry->mName);
+ }
+ }
+ return type_names;
+}
diff --git a/indra/llinventory/llfoldertype.h b/indra/llinventory/llfoldertype.h
index 46a1b92a96..dd12693f66 100644
--- a/indra/llinventory/llfoldertype.h
+++ b/indra/llinventory/llfoldertype.h
@@ -115,6 +115,8 @@ public:
static const std::string& badLookup(); // error string when a lookup fails
+ static LLSD getTypeNames();
+
protected:
LLFolderType() {}
~LLFolderType() {}
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index 4e7e072289..04eb2c1d6d 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -360,6 +360,7 @@ set(viewer_SOURCE_FILES
llinventorygallerymenu.cpp
llinventoryicon.cpp
llinventoryitemslist.cpp
+ llinventorylistener.cpp
llinventorylistitem.cpp
llinventorymodel.cpp
llinventorymodelbackgroundfetch.cpp
@@ -1027,6 +1028,7 @@ set(viewer_HEADER_FILES
llinventorygallerymenu.h
llinventoryicon.h
llinventoryitemslist.h
+ llinventorylistener.h
llinventorylistitem.h
llinventorymodel.h
llinventorymodelbackgroundfetch.h
diff --git a/indra/newview/llinventoryfunctions.h b/indra/newview/llinventoryfunctions.h
index 78077d2007..f6d910820d 100644
--- a/indra/newview/llinventoryfunctions.h
+++ b/indra/newview/llinventoryfunctions.h
@@ -189,6 +189,8 @@ public:
virtual ~LLInventoryCollectFunctor(){};
virtual bool operator()(LLInventoryCategory* cat, LLInventoryItem* item) = 0;
+ virtual bool exceedsLimit() { return false; }
+
static bool itemTransferCommonlyAllowed(const LLInventoryItem* item);
};
diff --git a/indra/newview/llinventorylistener.cpp b/indra/newview/llinventorylistener.cpp
new file mode 100644
index 0000000000..157e04dce3
--- /dev/null
+++ b/indra/newview/llinventorylistener.cpp
@@ -0,0 +1,298 @@
+/**
+ * @file llinventorylistener.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 "llviewerprecompiledheaders.h"
+
+#include "llinventorylistener.h"
+
+#include "llappearancemgr.h"
+#include "llinventoryfunctions.h"
+#include "lltransutil.h"
+#include "llwearableitemslist.h"
+#include "stringize.h"
+
+static const F32 MAX_ITEM_LIMIT = 100;
+
+LLInventoryListener::LLInventoryListener()
+ : LLEventAPI("LLInventory",
+ "API for interactions with viewer Inventory items")
+{
+ add("getItemsInfo",
+ "Return information about items or folders defined in [\"item_ids\"]:\n"
+ "reply will contain [\"items\"] and [\"categories\"] tables accordingly",
+ &LLInventoryListener::getItemsInfo,
+ llsd::map("item_ids", LLSD(), "reply", LLSD()));
+
+ add("getFolderTypeNames",
+ "Return the table of folder type names, contained in [\"names\"]\n",
+ &LLInventoryListener::getFolderTypeNames,
+ llsd::map("reply", LLSD()));
+
+ add("getAssetTypeNames",
+ "Return the table of asset type names, contained in [\"names\"]\n",
+ &LLInventoryListener::getAssetTypeNames,
+ llsd::map("reply", LLSD()));
+
+ add("getBasicFolderID",
+ "Return the UUID of the folder by specified folder type name, for example:\n"
+ "\"Textures\", \"My outfits\", \"Sounds\" and other basic folders which have associated type",
+ &LLInventoryListener::getBasicFolderID,
+ llsd::map("ft_name", LLSD(), "reply", LLSD()));
+
+ add("getDirectDescendents",
+ "Return the direct descendents(both items and folders) of the [\"folder_id\"]",
+ &LLInventoryListener::getDirectDescendents,
+ llsd::map("folder_id", LLSD(), "reply", LLSD()));
+
+ add("collectDescendentsIf",
+ "Return the descendents(both items and folders) of the [\"folder_id\"], if it passes specified filters:\n"
+ "[\"name\"] is a substring of object's name,\n"
+ "[\"desc\"] is a substring of object's description,\n"
+ "asset [\"type\"] corresponds to the string name of the object's asset type\n"
+ "[\"limit\"] sets item count limit in reply, maximum and default is 100\n"
+ "[\"filter_links\"]: EXCLUDE_LINKS - don't show links, ONLY_LINKS - only show links, INCLUDE_LINKS - show links too (default)",
+ &LLInventoryListener::collectDescendentsIf,
+ llsd::map("folder_id", LLSD(), "reply", LLSD()));
+ }
+
+
+void add_item_info(LLEventAPI::Response& response, LLViewerInventoryItem* item)
+{
+ response["items"].insert(item->getUUID().asString(),
+ llsd::map("name", item->getName(),
+ "parent_id", item->getParentUUID(),
+ "desc", item->getDescription(),
+ "inv_type", LLInventoryType::lookup(item->getInventoryType()),
+ "asset_type", LLAssetType::lookup(item->getType()),
+ "creation_date", (S32) item->getCreationDate(),
+ "asset_id", item->getAssetUUID(),
+ "is_link", item->getIsLinkType(),
+ "linked_id", item->getLinkedUUID()));
+}
+
+void add_cat_info(LLEventAPI::Response &response, LLViewerInventoryCategory *cat)
+{
+ response["categories"].insert(cat->getUUID().asString(),
+ llsd::map("name", cat->getName(),
+ "parent_id", cat->getParentUUID(),
+ "type", LLFolderType::lookup(cat->getPreferredType())));
+}
+
+void add_objects_info(LLEventAPI::Response& response, LLInventoryModel::cat_array_t cat_array, LLInventoryModel::item_array_t item_array)
+{
+ for (auto &p : item_array)
+ {
+ add_item_info(response, p);
+ }
+ for (auto &p : cat_array)
+ {
+ add_cat_info(response, p);
+ }
+}
+
+void LLInventoryListener::getItemsInfo(LLSD const &data)
+{
+ Response response(LLSD(), data);
+
+ uuid_vec_t ids = LLSDParam<uuid_vec_t>(data["item_ids"]);
+ for (auto &it : ids)
+ {
+ LLViewerInventoryItem* item = gInventory.getItem(it);
+ if (item)
+ {
+ add_item_info(response, item);
+ }
+ else
+ {
+ LLViewerInventoryCategory *cat = gInventory.getCategory(it);
+ if (cat)
+ {
+ add_cat_info(response, cat);
+ }
+ }
+ }
+}
+
+void LLInventoryListener::getFolderTypeNames(LLSD const &data)
+{
+ Response response(llsd::map("names", LLFolderType::getTypeNames()), data);
+}
+
+void LLInventoryListener::getAssetTypeNames(LLSD const &data)
+{
+ Response response(llsd::map("names", LLAssetType::getTypeNames()), data);
+}
+
+void LLInventoryListener::getBasicFolderID(LLSD const &data)
+{
+ Response response(llsd::map("id", gInventory.findCategoryUUIDForType(LLFolderType::lookup(data["ft_name"].asString()))), data);
+}
+
+
+void LLInventoryListener::getDirectDescendents(LLSD const &data)
+{
+ Response response(LLSD(), data);
+ LLInventoryModel::cat_array_t* cats;
+ LLInventoryModel::item_array_t* items;
+ gInventory.getDirectDescendentsOf(data["folder_id"], cats, items);
+
+ add_objects_info(response, *cats, *items);
+}
+
+struct LLFilteredCollector : public LLInventoryCollectFunctor
+{
+ enum EFilterLink
+ {
+ INCLUDE_LINKS, // show links too
+ EXCLUDE_LINKS, // don't show links
+ ONLY_LINKS // only show links
+ };
+
+ LLFilteredCollector(LLSD const &data);
+ virtual ~LLFilteredCollector() {}
+ virtual bool operator()(LLInventoryCategory *cat, LLInventoryItem *item) override;
+ virtual bool exceedsLimit() override { return (mItemLimit <= mItemCount); };
+
+ protected:
+ bool checkagainstType(LLInventoryCategory *cat, LLInventoryItem *item);
+ bool checkagainstNameDesc(LLInventoryCategory *cat, LLInventoryItem *item);
+ bool checkagainstLinks(LLInventoryCategory *cat, LLInventoryItem *item);
+
+ LLAssetType::EType mType;
+ std::string mName;
+ std::string mDesc;
+ EFilterLink mLinkFilter;
+
+ S32 mItemLimit;
+ S32 mItemCount;
+};
+
+void LLInventoryListener::collectDescendentsIf(LLSD const &data)
+{
+ Response response(LLSD(), data);
+ LLUUID folder_id(data["folder_id"].asUUID());
+ LLViewerInventoryCategory *cat = gInventory.getCategory(folder_id);
+ if (!cat)
+ {
+ return response.error(stringize("Folder ", std::quoted(data["folder_id"].asString()), " was not found"));
+ }
+ LLInventoryModel::cat_array_t cat_array;
+ LLInventoryModel::item_array_t item_array;
+
+ LLFilteredCollector collector = LLFilteredCollector(data);
+
+ gInventory.collectDescendentsIf(folder_id, cat_array, item_array, LLInventoryModel::EXCLUDE_TRASH, collector);
+
+ add_objects_info(response, cat_array, item_array);
+}
+
+LLFilteredCollector::LLFilteredCollector(LLSD const &data) :
+ mType(LLAssetType::EType::AT_UNKNOWN),
+ mLinkFilter(INCLUDE_LINKS),
+ mItemLimit(MAX_ITEM_LIMIT),
+ mItemCount(0)
+{
+
+ mName = data["name"].asString();
+ mDesc = data["desc"].asString();
+
+ if (data.has("type"))
+ {
+ mType = LLAssetType::lookup(data["type"]);
+ }
+ if (data.has("filter_links"))
+ {
+ if (data["filter_links"] == "EXCLUDE_LINKS")
+ {
+ mLinkFilter = EXCLUDE_LINKS;
+ }
+ else if (data["filter_links"] == "ONLY_LINKS")
+ {
+ mLinkFilter = ONLY_LINKS;
+ }
+ }
+ if (data["limit"].isInteger())
+ {
+ mItemLimit = llclamp(data["limit"].asInteger(), 1, MAX_ITEM_LIMIT);
+ }
+}
+
+bool LLFilteredCollector::operator()(LLInventoryCategory *cat, LLInventoryItem *item)
+{
+ bool passed = checkagainstType(cat, item);
+ passed = passed && checkagainstNameDesc(cat, item);
+ passed = passed && checkagainstLinks(cat, item);
+
+ if (passed)
+ {
+ ++mItemCount;
+ }
+ return passed;
+}
+
+bool LLFilteredCollector::checkagainstNameDesc(LLInventoryCategory *cat, LLInventoryItem *item)
+{
+ std::string name, desc;
+ bool passed(true);
+ if (cat)
+ {
+ if (!mDesc.empty()) return false;
+ name = cat->getName();
+ }
+ if (item)
+ {
+ name = item->getName();
+ passed = (mDesc.empty() || (item->getDescription().find(mDesc) != std::string::npos));
+ }
+
+ return passed && (mName.empty() || name.find(mName) != std::string::npos);
+}
+
+bool LLFilteredCollector::checkagainstType(LLInventoryCategory *cat, LLInventoryItem *item)
+{
+ if (mType == LLAssetType::AT_UNKNOWN)
+ {
+ return true;
+ }
+ if (cat && (mType == LLAssetType::AT_CATEGORY))
+ {
+ return true;
+ }
+ if (item && item->getType() == mType)
+ {
+ return true;
+ }
+ return false;
+}
+
+bool LLFilteredCollector::checkagainstLinks(LLInventoryCategory *cat, LLInventoryItem *item)
+{
+ bool is_link = cat ? cat->getIsLinkType() : item->getIsLinkType();
+ if (is_link && (mLinkFilter == EXCLUDE_LINKS))
+ return false;
+ if (!is_link && (mLinkFilter == ONLY_LINKS))
+ return false;
+ return true;
+}
diff --git a/indra/newview/llinventorylistener.h b/indra/newview/llinventorylistener.h
new file mode 100644
index 0000000000..5cbac2ca32
--- /dev/null
+++ b/indra/newview/llinventorylistener.h
@@ -0,0 +1,48 @@
+/**
+ * @file llinventorylistener.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_LLINVENTORYLISTENER_H
+#define LL_LLINVENTORYLISTENER_H
+
+#include "lleventapi.h"
+#include "llinventoryfunctions.h"
+
+class LLInventoryListener : public LLEventAPI
+{
+public:
+ LLInventoryListener();
+
+private:
+ void getItemsInfo(LLSD const &data);
+ void getFolderTypeNames(LLSD const &data);
+ void getAssetTypeNames(LLSD const &data);
+ void getBasicFolderID(LLSD const &data);
+ void getDirectDescendents(LLSD const &data);
+ void collectDescendentsIf(LLSD const &data);
+};
+
+#endif // LL_LLINVENTORYLISTENER_H
+
diff --git a/indra/newview/llinventorymodel.cpp b/indra/newview/llinventorymodel.cpp
index 5325c28abf..c98c3482d0 100644
--- a/indra/newview/llinventorymodel.cpp
+++ b/indra/newview/llinventorymodel.cpp
@@ -1289,6 +1289,10 @@ void LLInventoryModel::collectDescendentsIf(const LLUUID& id,
S32 count = cat_array->size();
for(S32 i = 0; i < count; ++i)
{
+ if (add.exceedsLimit())
+ {
+ break;
+ }
LLViewerInventoryCategory* cat = cat_array->at(i);
if(add(cat,NULL))
{
@@ -1307,6 +1311,10 @@ void LLInventoryModel::collectDescendentsIf(const LLUUID& id,
S32 count = item_array->size();
for(S32 i = 0; i < count; ++i)
{
+ if (add.exceedsLimit())
+ {
+ break;
+ }
item = item_array->at(i);
if(add(NULL, item))
{
diff --git a/indra/newview/llviewerinventory.cpp b/indra/newview/llviewerinventory.cpp
index 16810efa01..7030426e21 100644
--- a/indra/newview/llviewerinventory.cpp
+++ b/indra/newview/llviewerinventory.cpp
@@ -71,6 +71,9 @@
#include "llclipboard.h"
#include "llhttpretrypolicy.h"
#include "llsettingsvo.h"
+#include "llinventorylistener.h"
+
+LLInventoryListener sInventoryListener;
// do-nothing ops for use in callbacks.
void no_op_inventory_func(const LLUUID&) {}
diff --git a/indra/newview/scripts/lua/require/LLInventory.lua b/indra/newview/scripts/lua/require/LLInventory.lua
new file mode 100644
index 0000000000..dd1b910250
--- /dev/null
+++ b/indra/newview/scripts/lua/require/LLInventory.lua
@@ -0,0 +1,48 @@
+local leap = require 'leap'
+local mapargs = require 'mapargs'
+
+local LLInventory = {}
+
+-- Get the items/folders info by provided IDs,
+-- reply will contain "items" and "categories" tables accordingly
+function LLInventory.getItemsInfo(item_ids)
+ return leap.request('LLInventory', {op = 'getItemsInfo', item_ids=item_ids})
+end
+
+-- Get the table of folder type names, which can be later used to get the ID of the basic folders
+function LLInventory.getFolderTypeNames()
+ return leap.request('LLInventory', {op = 'getFolderTypeNames'}).names
+end
+
+-- Get the UUID of the basic folder("Textures", "My outfits", "Sounds" etc.) by specified folder type name
+function LLInventory.getBasicFolderID(ft_name)
+ return leap.request('LLInventory', {op = 'getBasicFolderID', ft_name=ft_name}).id
+end
+
+-- Get the table of asset type names, which can be later used to get the specific items via LLInventory.collectDescendentsIf(...)
+function LLInventory.getAssetTypeNames()
+ return leap.request('LLInventory', {op = 'getAssetTypeNames'}).names
+end
+
+-- Get the direct descendents of the 'folder_id' provided,
+-- reply will contain "items" and "categories" tables accordingly
+function LLInventory.getDirectDescendents(folder_id)
+ return leap.request('LLInventory', {op = 'getDirectDescendents', folder_id=folder_id})
+end
+
+-- Get the descendents of the 'folder_id' provided, which pass specified filters
+-- reply will contain "items" and "categories" tables accordingly
+-- LLInventory.collectDescendentsIf{ folder_id -- parent folder ID
+-- [, name] -- name (substring)
+-- [, desc] -- description (substring)
+-- [, type] -- asset type
+-- [, limit] -- item count limit in reply, maximum and default is 100
+-- [, filter_links]} -- EXCLUDE_LINKS - don't show links, ONLY_LINKS - only show links, INCLUDE_LINKS - show links too (default)
+function LLInventory.collectDescendentsIf(...)
+ local args = mapargs('folder_id,name,desc,type,filter_links,limit', ...)
+ args.op = 'collectDescendentsIf'
+ return leap.request('LLInventory', args)
+end
+
+
+return LLInventory
diff --git a/indra/newview/scripts/lua/test_LLInventory.lua b/indra/newview/scripts/lua/test_LLInventory.lua
new file mode 100644
index 0000000000..107b0791d4
--- /dev/null
+++ b/indra/newview/scripts/lua/test_LLInventory.lua
@@ -0,0 +1,21 @@
+inspect = require 'inspect'
+LLInventory = require 'LLInventory'
+
+-- Get 'My Landmarks' folder id (you can see all folder types via LLInventory.getFolderTypeNames())
+my_landmarks_id = LLInventory.getBasicFolderID('landmark')
+-- Get 3 landmarks from the 'My Landmarks' folder (you can see all folder types via LLInventory.getAssetTypeNames())
+landmarks = LLInventory.collectDescendentsIf{folder_id=my_landmarks_id, type="landmark", limit=3}
+print(inspect(landmarks))
+
+-- Get 'Calling Cards' folder id
+calling_cards_id = LLInventory.getBasicFolderID('callcard')
+-- Get all items located directly in 'Calling Cards' folder
+calling_cards = LLInventory.getDirectDescendents(calling_cards_id).items
+
+-- Print a random calling card name from 'Calling Cards' folder
+local card_names = {}
+for _, value in pairs(calling_cards) do
+ table.insert(card_names, value.name)
+end
+math.randomseed(os.time())
+print("Random calling card: " .. inspect(card_names[math.random(#card_names)]))