Skip to content

Commit 3cce0d2

Browse files
committed
Lua: Improve function/property handling
1. Follows advice from https://sol2.readthedocs.io/en/latest/functions.html to use `set_function` when binding functions. 2. Adds autocomplete support for userdata methods.
1 parent 4681167 commit 3cce0d2

23 files changed

+315
-187
lines changed

Source/lua/autocomplete.cpp

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include <algorithm>
55
#include <cstddef>
6+
#include <optional>
67
#include <string>
78
#include <string_view>
89
#include <vector>
@@ -121,8 +122,36 @@ ValueInfo GetValueInfo(const sol::table &table, std::string_view key, const sol:
121122
return info;
122123
}
123124

125+
ValueInfo GetValueInfoForUserdata(const sol::userdata &obj, std::string_view key, const sol::object &value, std::optional<LuaUserdataMemberType> memberType)
126+
{
127+
ValueInfo info;
128+
if (value.get_type() == sol::type::userdata) {
129+
info.callable = false;
130+
return info;
131+
}
132+
133+
if (std::optional<std::string> signature = GetLuaUserdataSignature(obj, key); signature.has_value()) {
134+
info.signature = *std::move(signature);
135+
}
136+
if (std::optional<std::string> docstring = GetLuaUserdataDocstring(obj, key); docstring.has_value()) {
137+
info.docstring = *std::move(docstring);
138+
}
139+
if (memberType.has_value()) {
140+
info.callable = *memberType == LuaUserdataMemberType::MemberFunction;
141+
} else {
142+
info.callable = value.get_type() == sol::type::function;
143+
}
144+
return info;
145+
}
146+
147+
struct UserdataQuery {
148+
const sol::userdata *obj;
149+
bool colonAccess;
150+
};
151+
124152
void SuggestionsFromTable(const sol::table &table, std::string_view prefix,
125-
size_t maxSuggestions, ankerl::unordered_dense::set<LuaAutocompleteSuggestion> &out)
153+
size_t maxSuggestions, ankerl::unordered_dense::set<LuaAutocompleteSuggestion> &out,
154+
std::optional<UserdataQuery> userdataQuery = std::nullopt)
126155
{
127156
for (const auto &[key, value] : table) {
128157
if (key.get_type() == sol::type::string) {
@@ -136,14 +165,30 @@ void SuggestionsFromTable(const sol::table &table, std::string_view prefix,
136165
|| keyStr.find("") != std::string::npos
137166
|| keyStr.find("🔩") != std::string::npos)
138167
continue;
139-
ValueInfo info = GetValueInfo(table, keyStr, value);
168+
ValueInfo info;
169+
std::optional<LuaUserdataMemberType> memberType;
170+
if (userdataQuery.has_value()) {
171+
memberType = GetLuaUserdataMemberType(*userdataQuery->obj, keyStr, value);
172+
const bool requiresColonAccess = memberType.has_value()
173+
? *memberType == LuaUserdataMemberType::MemberFunction
174+
: value.get_type() == sol::type::function;
175+
if (userdataQuery->colonAccess != requiresColonAccess) {
176+
continue;
177+
}
178+
info = GetValueInfoForUserdata(*userdataQuery->obj, keyStr, value, memberType);
179+
} else {
180+
info = GetValueInfo(table, keyStr, value);
181+
}
140182
std::string completionText = keyStr.substr(prefix.size());
141183
LuaAutocompleteSuggestion suggestion { std::move(keyStr), std::move(completionText) };
142184
if (info.callable) {
143185
suggestion.completionText.append("()");
144186
suggestion.cursorAdjust = -1;
145187
}
146188
if (!info.signature.empty()) {
189+
if (memberType.has_value() && memberType != LuaUserdataMemberType::MemberFunction) {
190+
StrAppend(suggestion.displayText, ": ");
191+
}
147192
StrAppend(suggestion.displayText, info.signature);
148193
}
149194
if (!info.docstring.empty()) {
@@ -164,6 +209,15 @@ void SuggestionsFromTable(const sol::table &table, std::string_view prefix,
164209
}
165210
}
166211

212+
void SuggestionsFromUserdata(UserdataQuery query, std::string_view prefix,
213+
size_t maxSuggestions, ankerl::unordered_dense::set<LuaAutocompleteSuggestion> &out)
214+
{
215+
const auto &meta = query.obj->get<std::optional<sol::object>>(sol::metatable_key);
216+
if (meta.has_value() && meta->get_type() == sol::type::table) {
217+
SuggestionsFromTable(meta->as<sol::table>(), prefix, maxSuggestions, out, query);
218+
}
219+
}
220+
167221
} // namespace
168222

169223
void GetLuaAutocompleteSuggestions(std::string_view text, const sol::environment &lua,
@@ -174,8 +228,9 @@ void GetLuaAutocompleteSuggestions(std::string_view text, const sol::environment
174228
std::string_view token = GetLastToken(text);
175229
const char prevChar = token.data() == text.data() ? '\0' : *(token.data() - 1);
176230
if (prevChar == '(' || prevChar == ',') return;
177-
const size_t dotPos = token.rfind('.');
231+
const size_t dotPos = token.find_last_of(".:");
178232
const std::string_view prefix = token.substr(dotPos + 1);
233+
const char completionChar = dotPos != std::string_view::npos ? token[dotPos] : '\0';
179234
token.remove_suffix(token.size() - (dotPos == std::string_view::npos ? 0 : dotPos));
180235

181236
ankerl::unordered_dense::set<LuaAutocompleteSuggestion> suggestions;
@@ -192,13 +247,20 @@ void GetLuaAutocompleteSuggestions(std::string_view text, const sol::environment
192247
}
193248
} else {
194249
std::optional<sol::object> obj = lua;
195-
for (const std::string_view part : SplitByChar(token, '.')) {
196-
obj = obj->as<sol::table>().get<std::optional<sol::object>>(part);
197-
if (!obj.has_value() || obj->get_type() != sol::type::table)
198-
return;
250+
for (const std::string_view partDot : SplitByChar(token, '.')) {
251+
for (const std::string_view part : SplitByChar(partDot, ':')) {
252+
obj = obj->as<sol::table>().get<std::optional<sol::object>>(part);
253+
if (!obj.has_value() || !(obj->get_type() == sol::type::table || obj->get_type() == sol::type::userdata)) {
254+
return;
255+
}
256+
}
199257
}
200258
if (obj->get_type() == sol::type::table) {
201259
addSuggestions(obj->as<sol::table>());
260+
} else if (obj->get_type() == sol::type::userdata) {
261+
const sol::userdata &data = obj->as<sol::userdata>();
262+
SuggestionsFromUserdata(UserdataQuery { &data, completionChar == ':' },
263+
prefix, maxSuggestions, suggestions);
202264
}
203265
}
204266

Source/lua/metadoc.hpp

Lines changed: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
#pragma once
22

3-
#include <sol/sol.hpp>
4-
3+
#include <cstdint>
4+
#include <optional>
5+
#include <string>
56
#include <string_view>
67

8+
#include <sol/sol.hpp>
9+
710
#include "utils/str_cat.hpp"
811

912
namespace devilution {
1013

14+
enum class LuaUserdataMemberType : uint8_t {
15+
ReadonlyProperty,
16+
Property,
17+
MemberFunction,
18+
Constructor,
19+
};
20+
1121
inline std::string LuaSignatureKey(std::string_view key)
1222
{
1323
return StrCat("__sig_", key);
@@ -18,40 +28,75 @@ inline std::string LuaDocstringKey(std::string_view key)
1828
return StrCat("__doc_", key);
1929
}
2030

31+
inline std::string LuaUserdataMemberTypeKey(std::string_view key)
32+
{
33+
return StrCat("__udt_", key);
34+
}
35+
2136
template <typename U, typename T>
22-
void SetDocumented(sol::usertype<U> &table, std::string_view key, std::string_view signature, std::string_view doc, T &&value)
37+
void LuaSetDoc(sol::usertype<U> &table, std::string_view key, const char *signature, const char *doc, T &&value)
2338
{
2439
table.set(key, std::forward<T>(value));
25-
// TODO: figure out a way to set signature and docstring.
40+
table.set(LuaSignatureKey(key), sol::var(signature));
41+
table.set(LuaDocstringKey(key), sol::var(doc));
2642
}
2743

28-
// This overload works around compiler issues on clang-15 with member field references.
29-
template <typename U, typename F>
30-
void SetDocumented(sol::usertype<U> &table, std::string_view key, std::string_view signature, std::string_view doc, F U::*&&value)
44+
template <typename U, typename T>
45+
void LuaSetDocFn(sol::usertype<U> &table, std::string_view key, const char *signature, const char *doc, T &&value)
46+
{
47+
table.set_function(key, std::forward<T>(value));
48+
table.set(LuaSignatureKey(key), sol::var(signature));
49+
table.set(LuaDocstringKey(key), sol::var(doc));
50+
table.set(LuaUserdataMemberTypeKey(key), sol::var(static_cast<uint8_t>(LuaUserdataMemberType::MemberFunction)));
51+
}
52+
53+
template <typename U, typename G>
54+
void LuaSetDocReadonlyProperty(sol::usertype<U> &table, std::string_view key, const char *type, const char *doc, G &&getter)
3155
{
32-
table.set(key, std::forward<F U::*>(value));
33-
// TODO: figure out a way to set signature and docstring.
56+
table.set(key, sol::readonly_property(std::forward<G>(getter)));
57+
table.set(LuaSignatureKey(key), sol::var(type));
58+
table.set(LuaDocstringKey(key), sol::var(doc));
59+
table.set(LuaUserdataMemberTypeKey(key), sol::var(static_cast<uint8_t>(LuaUserdataMemberType::ReadonlyProperty)));
3460
}
3561

3662
template <typename U, typename G, typename S>
37-
void SetDocumented(sol::usertype<U> &table, std::string_view key, std::string_view signature, std::string_view doc, G &&getter, S &&setter)
63+
void LuaSetDocProperty(sol::usertype<U> &table, std::string_view key, const char *type, const char *doc, G &&getter, S &&setter)
3864
{
3965
table.set(key, sol::property(std::forward<G>(getter), std::forward<S>(setter)));
40-
// TODO: figure out a way to set signature and docstring.
66+
table.set(LuaSignatureKey(key), sol::var(type));
67+
table.set(LuaDocstringKey(key), sol::var(doc));
68+
table.set(LuaUserdataMemberTypeKey(key), sol::var(static_cast<uint8_t>(LuaUserdataMemberType::Property)));
69+
}
70+
71+
template <typename U, typename F>
72+
void LuaSetDocProperty(sol::usertype<U> &table, std::string_view key, const char *type, const char *doc, F U::*&&value)
73+
{
74+
table.set(key, value);
75+
table.set(LuaSignatureKey(key), sol::var(type));
76+
table.set(LuaDocstringKey(key), sol::var(doc));
77+
table.set(LuaUserdataMemberTypeKey(key), sol::var(static_cast<uint8_t>(LuaUserdataMemberType::Property)));
4178
}
4279

4380
template <typename T>
44-
void SetDocumented(sol::table &table, std::string_view key, std::string_view signature, std::string_view doc, T &&value)
81+
void LuaSetDoc(sol::table &table, std::string_view key, std::string_view signature, std::string_view doc, T &&value)
4582
{
4683
table.set(key, std::forward<T>(value));
4784
table.set(LuaSignatureKey(key), signature);
4885
table.set(LuaDocstringKey(key), doc);
4986
}
5087

5188
template <typename T>
52-
void SetWithSignature(sol::table &table, std::string_view key, std::string_view signature, T &&value)
89+
void LuaSetDocFn(sol::table &table, std::string_view key, std::string_view signature, std::string_view doc, T &&value)
5390
{
54-
table.set(key, std::forward<T>(value));
91+
table.set_function(key, std::forward<T>(value));
92+
table.set(LuaSignatureKey(key), signature);
93+
table.set(LuaDocstringKey(key), doc);
94+
}
95+
96+
template <typename T>
97+
void LuaSetDocFn(sol::table &table, std::string_view key, std::string_view signature, T &&value)
98+
{
99+
table.set_function(key, std::forward<T>(value));
55100
table.set(LuaSignatureKey(key), signature);
56101
}
57102

@@ -65,4 +110,25 @@ inline std::optional<std::string> GetDocstring(const sol::table &table, std::str
65110
return table.get<std::optional<std::string>>(LuaDocstringKey(key));
66111
}
67112

113+
inline std::optional<std::string> GetLuaUserdataSignature(const sol::userdata &obj, std::string_view key)
114+
{
115+
return obj.get<std::optional<std::string>>(LuaSignatureKey(key));
116+
}
117+
118+
inline std::optional<std::string> GetLuaUserdataDocstring(const sol::userdata &obj, std::string_view key)
119+
{
120+
return obj.get<std::optional<std::string>>(LuaDocstringKey(key));
121+
}
122+
123+
inline std::optional<LuaUserdataMemberType> GetLuaUserdataMemberType(const sol::userdata &obj, std::string_view key, const sol::object &value)
124+
{
125+
std::optional<uint8_t> result = obj.get<std::optional<uint8_t>>(LuaUserdataMemberTypeKey(key));
126+
if (!result.has_value()) {
127+
if (value.get_type() == sol::type::userdata) return LuaUserdataMemberType::Property;
128+
if (value.get_type() == sol::type::function && key == "new") return LuaUserdataMemberType::Constructor;
129+
return std::nullopt;
130+
}
131+
return static_cast<LuaUserdataMemberType>(*result);
132+
}
133+
68134
} // namespace devilution

Source/lua/modules/audio.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ bool IsValidSfx(int16_t psfx)
1919
sol::table LuaAudioModule(sol::state_view &lua)
2020
{
2121
sol::table table = lua.create_table();
22-
SetWithSignature(table,
22+
LuaSetDocFn(table,
2323
"playSfx", "(id: number)",
2424
[](int16_t psfx) { if (IsValidSfx(psfx)) PlaySFX(static_cast<SfxID>(psfx)); });
25-
SetWithSignature(table,
25+
LuaSetDocFn(table,
2626
"playSfxLoc", "(id: number, x: number, y: number)",
2727
[](int16_t psfx, int x, int y) { if (IsValidSfx(psfx)) PlaySfxLoc(static_cast<SfxID>(psfx), { x, y }); });
2828
return table;

Source/lua/modules/dev.cpp

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ namespace devilution {
1818
sol::table LuaDevModule(sol::state_view &lua)
1919
{
2020
sol::table table = lua.create_table();
21-
SetDocumented(table, "display", "", "Debugging HUD and rendering commands.", LuaDevDisplayModule(lua));
22-
SetDocumented(table, "items", "", "Item-related commands.", LuaDevItemsModule(lua));
23-
SetDocumented(table, "level", "", "Level-related commands.", LuaDevLevelModule(lua));
24-
SetDocumented(table, "monsters", "", "Monster-related commands.", LuaDevMonstersModule(lua));
25-
SetDocumented(table, "player", "", "Player-related commands.", LuaDevPlayerModule(lua));
26-
SetDocumented(table, "quests", "", "Quest-related commands.", LuaDevQuestsModule(lua));
27-
SetDocumented(table, "search", "", "Search the map for monsters / items / objects.", LuaDevSearchModule(lua));
28-
SetDocumented(table, "towners", "", "Town NPC commands.", LuaDevTownersModule(lua));
21+
LuaSetDoc(table, "display", "", "Debugging HUD and rendering commands.", LuaDevDisplayModule(lua));
22+
LuaSetDoc(table, "items", "", "Item-related commands.", LuaDevItemsModule(lua));
23+
LuaSetDoc(table, "level", "", "Level-related commands.", LuaDevLevelModule(lua));
24+
LuaSetDoc(table, "monsters", "", "Monster-related commands.", LuaDevMonstersModule(lua));
25+
LuaSetDoc(table, "player", "", "Player-related commands.", LuaDevPlayerModule(lua));
26+
LuaSetDoc(table, "quests", "", "Quest-related commands.", LuaDevQuestsModule(lua));
27+
LuaSetDoc(table, "search", "", "Search the map for monsters / items / objects.", LuaDevSearchModule(lua));
28+
LuaSetDoc(table, "towners", "", "Town NPC commands.", LuaDevTownersModule(lua));
2929
return table;
3030
}
3131

Source/lua/modules/dev/display.cpp

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,13 @@ std::string DebugCmdToggleFPS(std::optional<bool> on)
121121
sol::table LuaDevDisplayModule(sol::state_view &lua)
122122
{
123123
sol::table table = lua.create_table();
124-
SetDocumented(table, "fps", "(name: string = nil)", "Toggle FPS display.", &DebugCmdToggleFPS);
125-
SetDocumented(table, "fullbright", "(on: boolean = nil)", "Toggle light shading.", &DebugCmdFullbright);
126-
SetDocumented(table, "grid", "(on: boolean = nil)", "Toggle showing the grid.", &DebugCmdShowGrid);
127-
SetDocumented(table, "path", "(on: boolean = nil)", "Toggle path debug rendering.", &DebugCmdPath);
128-
SetDocumented(table, "scrollView", "(on: boolean = nil)", "Toggle view scrolling via Shift+Mouse.", &DebugCmdScrollView);
129-
SetDocumented(table, "tileData", "(name: string = nil)", "Toggle showing tile data.", &DebugCmdShowTileData);
130-
SetDocumented(table, "vision", "(on: boolean = nil)", "Toggle vision debug rendering.", &DebugCmdVision);
124+
LuaSetDocFn(table, "fps", "(name: string = nil)", "Toggle FPS display.", &DebugCmdToggleFPS);
125+
LuaSetDocFn(table, "fullbright", "(on: boolean = nil)", "Toggle light shading.", &DebugCmdFullbright);
126+
LuaSetDocFn(table, "grid", "(on: boolean = nil)", "Toggle showing the grid.", &DebugCmdShowGrid);
127+
LuaSetDocFn(table, "path", "(on: boolean = nil)", "Toggle path debug rendering.", &DebugCmdPath);
128+
LuaSetDocFn(table, "scrollView", "(on: boolean = nil)", "Toggle view scrolling via Shift+Mouse.", &DebugCmdScrollView);
129+
LuaSetDocFn(table, "tileData", "(name: string = nil)", "Toggle showing tile data.", &DebugCmdShowTileData);
130+
LuaSetDocFn(table, "vision", "(on: boolean = nil)", "Toggle vision debug rendering.", &DebugCmdVision);
131131
return table;
132132
}
133133

Source/lua/modules/dev/items.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,9 @@ std::string DebugSpawnUniqueItem(std::string itemName)
194194
sol::table LuaDevItemsModule(sol::state_view &lua)
195195
{
196196
sol::table table = lua.create_table();
197-
SetDocumented(table, "info", "()", "Show info of currently selected item.", &DebugCmdItemInfo);
198-
SetDocumented(table, "spawn", "(name: string)", "Attempt to generate an item.", &DebugSpawnItem);
199-
SetDocumented(table, "spawnUnique", "(name: string)", "Attempt to generate a unique item.", &DebugSpawnUniqueItem);
197+
LuaSetDocFn(table, "info", "()", "Show info of currently selected item.", &DebugCmdItemInfo);
198+
LuaSetDocFn(table, "spawn", "(name: string)", "Attempt to generate an item.", &DebugSpawnItem);
199+
LuaSetDocFn(table, "spawnUnique", "(name: string)", "Attempt to generate a unique item.", &DebugSpawnUniqueItem);
200200
return table;
201201
}
202202

Source/lua/modules/dev/level.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,11 @@ std::string DebugCmdLevelSeed(std::optional<uint8_t> level)
122122
sol::table LuaDevLevelModule(sol::state_view &lua)
123123
{
124124
sol::table table = lua.create_table();
125-
SetDocumented(table, "exportDun", "()", "Save the current level as a dun-file.", &ExportDun);
126-
SetDocumented(table, "map", "", "Automap-related commands.", LuaDevLevelMapModule(lua));
127-
SetDocumented(table, "reset", "(n: number, seed: number = nil)", "Resets specified level.", &DebugCmdResetLevel);
128-
SetDocumented(table, "seed", "(level: number = nil)", "Get the seed of the current or given level.", &DebugCmdLevelSeed);
129-
SetDocumented(table, "warp", "", "Warp to a level or a custom map.", LuaDevLevelWarpModule(lua));
125+
LuaSetDocFn(table, "exportDun", "()", "Save the current level as a dun-file.", &ExportDun);
126+
LuaSetDocFn(table, "map", "", "Automap-related commands.", LuaDevLevelMapModule(lua));
127+
LuaSetDocFn(table, "reset", "(n: number, seed: number = nil)", "Resets specified level.", &DebugCmdResetLevel);
128+
LuaSetDocFn(table, "seed", "(level: number = nil)", "Get the seed of the current or given level.", &DebugCmdLevelSeed);
129+
LuaSetDocFn(table, "warp", "", "Warp to a level or a custom map.", LuaDevLevelWarpModule(lua));
130130
return table;
131131
}
132132

Source/lua/modules/dev/level/map.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ std::string DebugCmdMapHide()
3232
sol::table LuaDevLevelMapModule(sol::state_view &lua)
3333
{
3434
sol::table table = lua.create_table();
35-
SetDocumented(table, "hide", "()", "Hide the map.", &DebugCmdMapHide);
36-
SetDocumented(table, "reveal", "()", "Reveal the map.", &DebugCmdMapReveal);
35+
LuaSetDocFn(table, "hide", "()", "Hide the map.", &DebugCmdMapHide);
36+
LuaSetDocFn(table, "reveal", "()", "Reveal the map.", &DebugCmdMapReveal);
3737
return table;
3838
}
3939

Source/lua/modules/dev/level/warp.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ std::string DebugCmdWarpToCustomMap(std::string_view path, int dunType, int x, i
7272
sol::table LuaDevLevelWarpModule(sol::state_view &lua)
7373
{
7474
sol::table table = lua.create_table();
75-
SetDocumented(table, "dungeon", "(n: number)", "Go to dungeon level (0 for town).", &DebugCmdWarpToDungeonLevel);
76-
SetDocumented(table, "map", "(path: string, dunType: number, x: number, y: number)", "Go to custom {path}.dun level", &DebugCmdWarpToCustomMap);
77-
SetDocumented(table, "quest", "(n: number)", "Go to quest level.", &DebugCmdWarpToQuestLevel);
75+
LuaSetDocFn(table, "dungeon", "(n: number)", "Go to dungeon level (0 for town).", &DebugCmdWarpToDungeonLevel);
76+
LuaSetDocFn(table, "map", "(path: string, dunType: number, x: number, y: number)", "Go to custom {path}.dun level", &DebugCmdWarpToCustomMap);
77+
LuaSetDocFn(table, "quest", "(n: number)", "Go to quest level.", &DebugCmdWarpToQuestLevel);
7878
return table;
7979
}
8080

Source/lua/modules/dev/monsters.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,8 @@ std::string DebugCmdSpawnMonster(std::string name, std::optional<unsigned> count
172172
sol::table LuaDevMonstersModule(sol::state_view &lua)
173173
{
174174
sol::table table = lua.create_table();
175-
SetDocumented(table, "spawn", "(name: string, count: number = 1)", "Spawn monster(s)", &DebugCmdSpawnMonster);
176-
SetDocumented(table, "spawnUnique", "(name: string, count: number = 1)", "Spawn unique monster(s)", &DebugCmdSpawnUniqueMonster);
175+
LuaSetDocFn(table, "spawn", "(name: string, count: number = 1)", "Spawn monster(s)", &DebugCmdSpawnMonster);
176+
LuaSetDocFn(table, "spawnUnique", "(name: string, count: number = 1)", "Spawn unique monster(s)", &DebugCmdSpawnUniqueMonster);
177177
return table;
178178
}
179179

0 commit comments

Comments
 (0)