| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| set(common_SCRIPT_LUA_API_SRCS | ||
| ${CMAKE_CURRENT_SOURCE_DIR}/l_areastore.cpp | ||
| ${CMAKE_CURRENT_SOURCE_DIR}/l_auth.cpp | ||
|
There was a problem hiding this comment. Also add to https://github.com/minetest/minetest/blob/master/build/android/jni/Android.mk There was a problem hiding this comment. Done, but I have not been able to successfully build on Android yet, so I can't test this. I see that not all unit test files are listed. Is this on purpose? There was a problem hiding this comment. Probably it compiled fine on Android so nobody noticed that they were forgotten - or it's on purpose and the documentation is missing. 🤷♂️ There was a problem hiding this comment. afaik the missing unit test files are on purpose |
||
| ${CMAKE_CURRENT_SOURCE_DIR}/l_base.cpp | ||
| ${CMAKE_CURRENT_SOURCE_DIR}/l_craft.cpp | ||
| ${CMAKE_CURRENT_SOURCE_DIR}/l_env.cpp | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,216 @@ | ||
| /* | ||
| Minetest | ||
| Copyright (C) 2018 bendeutsch, Ben Deutsch <ben@bendeutsch.de> | ||
| This program 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; either version 2.1 of the License, or | ||
| (at your option) any later version. | ||
| This program 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 program; if not, write to the Free Software Foundation, Inc., | ||
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||
| */ | ||
|
|
||
| #include "lua_api/l_auth.h" | ||
| #include "lua_api/l_internal.h" | ||
| #include "common/c_converter.h" | ||
| #include "common/c_content.h" | ||
| #include "cpp_api/s_base.h" | ||
| #include "server.h" | ||
| #include "environment.h" | ||
| #include "database/database.h" | ||
| #include <algorithm> | ||
|
|
||
| // common start: ensure auth db | ||
| AuthDatabase *ModApiAuth::getAuthDb(lua_State *L) | ||
| { | ||
| ServerEnvironment *server_environment = | ||
| dynamic_cast<ServerEnvironment *>(getEnv(L)); | ||
| if (!server_environment) | ||
| return nullptr; | ||
| return server_environment->getAuthDatabase(); | ||
| } | ||
|
|
||
| void ModApiAuth::pushAuthEntry(lua_State *L, const AuthEntry &authEntry) | ||
| { | ||
| lua_newtable(L); | ||
| int table = lua_gettop(L); | ||
| // id | ||
| lua_pushnumber(L, authEntry.id); | ||
| lua_setfield(L, table, "id"); | ||
| // name | ||
| lua_pushstring(L, authEntry.name.c_str()); | ||
| lua_setfield(L, table, "name"); | ||
| // password | ||
| lua_pushstring(L, authEntry.password.c_str()); | ||
| lua_setfield(L, table, "password"); | ||
| // privileges | ||
| lua_newtable(L); | ||
| int privtable = lua_gettop(L); | ||
| for (const std::string &privs : authEntry.privileges) { | ||
|
There was a problem hiding this comment. I would argue that in this case, writing out the actual type aids clarity (the '.c_str' method is used two lines below). |
||
| lua_pushboolean(L, true); | ||
| lua_setfield(L, privtable, privs.c_str()); | ||
| } | ||
| lua_setfield(L, table, "privileges"); | ||
| // last_login | ||
| lua_pushnumber(L, authEntry.last_login); | ||
| lua_setfield(L, table, "last_login"); | ||
|
|
||
| lua_pushvalue(L, table); | ||
| } | ||
|
|
||
| // auth_read(name) | ||
| int ModApiAuth::l_auth_read(lua_State *L) | ||
|
There was a problem hiding this comment. These functions need to be documented. There was a problem hiding this comment. In this case, they're exposed via a Maybe it does make sense for mods to access this? Even though it exposes (hashed) passwords? Then documentation is in order. Otherwise, how do I let this be used by There was a problem hiding this comment. If you want to restrict it to builtin only, then you can copy the functions to a new table and delete them afterwards (i.e. There was a problem hiding this comment. Ok, I'll do that. Where should the documentation of the (then private) object+methods go? Is Update: Done, it's There was a problem hiding this comment. @bendeutsch The function arguments are already mentioned there in the comments. When the functions are unavailable to modders, this does not seem to need more documentation. |
||
| { | ||
| NO_MAP_LOCK_REQUIRED; | ||
| AuthDatabase *auth_db = getAuthDb(L); | ||
| if (!auth_db) | ||
| return 0; | ||
| AuthEntry authEntry; | ||
| const char *name = luaL_checkstring(L, 1); | ||
| bool success = auth_db->getAuth(std::string(name), authEntry); | ||
| if (!success) | ||
| return 0; | ||
|
|
||
| pushAuthEntry(L, authEntry); | ||
| return 1; | ||
| } | ||
|
|
||
| // auth_save(table) | ||
| int ModApiAuth::l_auth_save(lua_State *L) | ||
| { | ||
| NO_MAP_LOCK_REQUIRED; | ||
| AuthDatabase *auth_db = getAuthDb(L); | ||
| if (!auth_db) | ||
| return 0; | ||
| luaL_checktype(L, 1, LUA_TTABLE); | ||
| int table = 1; | ||
| AuthEntry authEntry; | ||
| bool success; | ||
| success = getintfield(L, table, "id", authEntry.id); | ||
| success = success && getstringfield(L, table, "name", authEntry.name); | ||
| success = success && getstringfield(L, table, "password", authEntry.password); | ||
| lua_getfield(L, table, "privileges"); | ||
| if (lua_istable(L, -1)) { | ||
| lua_pushnil(L); | ||
| while (lua_next(L, -2)) { | ||
| authEntry.privileges.emplace_back( | ||
| lua_tostring(L, -2)); // the key, not the value | ||
| lua_pop(L, 1); | ||
| } | ||
| } else { | ||
| success = false; | ||
| } | ||
| lua_pop(L, 1); // the table | ||
| success = success && getintfield(L, table, "last_login", authEntry.last_login); | ||
|
|
||
| if (!success) { | ||
| lua_pushboolean(L, false); | ||
| return 1; | ||
| } | ||
|
|
||
| lua_pushboolean(L, auth_db->saveAuth(authEntry)); | ||
| return 1; | ||
| } | ||
|
|
||
| // auth_create(table) | ||
| int ModApiAuth::l_auth_create(lua_State *L) | ||
| { | ||
| NO_MAP_LOCK_REQUIRED; | ||
| AuthDatabase *auth_db = getAuthDb(L); | ||
| if (!auth_db) | ||
| return 0; | ||
| luaL_checktype(L, 1, LUA_TTABLE); | ||
| int table = 1; | ||
| AuthEntry authEntry; | ||
| bool success; | ||
| // no meaningful id field, we assume | ||
| success = getstringfield(L, table, "name", authEntry.name); | ||
| success = success && getstringfield(L, table, "password", authEntry.password); | ||
| lua_getfield(L, table, "privileges"); | ||
| if (lua_istable(L, -1)) { | ||
| lua_pushnil(L); | ||
| while (lua_next(L, -2)) { | ||
| authEntry.privileges.emplace_back( | ||
| lua_tostring(L, -2)); // the key, not the value | ||
| lua_pop(L, 1); | ||
| } | ||
| } else { | ||
| success = false; | ||
| } | ||
| lua_pop(L, 1); // the table | ||
| success = success && getintfield(L, table, "last_login", authEntry.last_login); | ||
|
|
||
| if (!success) | ||
| return 0; | ||
|
|
||
| if (auth_db->createAuth(authEntry)) { | ||
| pushAuthEntry(L, authEntry); | ||
| return 1; | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| // auth_delete(name) | ||
| int ModApiAuth::l_auth_delete(lua_State *L) | ||
| { | ||
| NO_MAP_LOCK_REQUIRED; | ||
| AuthDatabase *auth_db = getAuthDb(L); | ||
| if (!auth_db) | ||
| return 0; | ||
| std::string name(luaL_checkstring(L, 1)); | ||
| lua_pushboolean(L, auth_db->deleteAuth(name)); | ||
| return 1; | ||
| } | ||
|
|
||
| // auth_list_names() | ||
| int ModApiAuth::l_auth_list_names(lua_State *L) | ||
| { | ||
| NO_MAP_LOCK_REQUIRED; | ||
| AuthDatabase *auth_db = getAuthDb(L); | ||
| if (!auth_db) | ||
| return 0; | ||
| std::vector<std::string> names; | ||
| auth_db->listNames(names); | ||
| lua_createtable(L, names.size(), 0); | ||
| int table = lua_gettop(L); | ||
| int i = 1; | ||
| for (const std::string &name : names) { | ||
| lua_pushstring(L, name.c_str()); | ||
| lua_rawseti(L, table, i++); | ||
| } | ||
| return 1; | ||
| } | ||
|
|
||
| // auth_reload() | ||
| int ModApiAuth::l_auth_reload(lua_State *L) | ||
| { | ||
| NO_MAP_LOCK_REQUIRED; | ||
| AuthDatabase *auth_db = getAuthDb(L); | ||
| if (auth_db) | ||
| auth_db->reload(); | ||
| return 0; | ||
| } | ||
|
|
||
| void ModApiAuth::Initialize(lua_State *L, int top) | ||
| { | ||
|
|
||
| lua_newtable(L); | ||
| int auth_top = lua_gettop(L); | ||
|
|
||
| registerFunction(L, "read", l_auth_read, auth_top); | ||
| registerFunction(L, "save", l_auth_save, auth_top); | ||
| registerFunction(L, "create", l_auth_create, auth_top); | ||
| registerFunction(L, "delete", l_auth_delete, auth_top); | ||
| registerFunction(L, "list_names", l_auth_list_names, auth_top); | ||
| registerFunction(L, "reload", l_auth_reload, auth_top); | ||
|
|
||
| lua_setfield(L, top, "auth"); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| /* | ||
| Minetest | ||
| Copyright (C) 2018 bendeutsch, Ben Deutsch <ben@bendeutsch.de> | ||
| This program 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; either version 2.1 of the License, or | ||
| (at your option) any later version. | ||
| This program 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 program; if not, write to the Free Software Foundation, Inc., | ||
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include "lua_api/l_base.h" | ||
|
|
||
| class AuthDatabase; | ||
| struct AuthEntry; | ||
|
|
||
| class ModApiAuth : public ModApiBase | ||
| { | ||
| private: | ||
| // auth_read(name) | ||
| static int l_auth_read(lua_State *L); | ||
|
|
||
| // auth_save(table) | ||
| static int l_auth_save(lua_State *L); | ||
|
|
||
| // auth_create(table) | ||
| static int l_auth_create(lua_State *L); | ||
|
|
||
| // auth_delete(name) | ||
| static int l_auth_delete(lua_State *L); | ||
|
|
||
| // auth_list_names() | ||
| static int l_auth_list_names(lua_State *L); | ||
|
|
||
| // auth_reload() | ||
| static int l_auth_reload(lua_State *L); | ||
|
|
||
| // helper for auth* methods | ||
| static AuthDatabase *getAuthDb(lua_State *L); | ||
| static void pushAuthEntry(lua_State *L, const AuthEntry &authEntry); | ||
|
|
||
| public: | ||
| static void Initialize(lua_State *L, int top); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,299 @@ | ||
| /* | ||
| Minetest | ||
| Copyright (C) 2018 bendeutsch, Ben Deutsch <ben@bendeutsch.de> | ||
| This program 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; either version 2.1 of the License, or | ||
| (at your option) any later version. | ||
| This program 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 program; if not, write to the Free Software Foundation, Inc., | ||
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||
| */ | ||
|
|
||
| #include "test.h" | ||
|
|
||
| #include <algorithm> | ||
| #include "database/database-files.h" | ||
| #include "database/database-sqlite3.h" | ||
| #include "util/string.h" | ||
| #include "filesys.h" | ||
|
|
||
| namespace | ||
| { | ||
| // Anonymous namespace to create classes that are only | ||
| // visible to this file | ||
| // | ||
| // These are helpers that return a *AuthDatabase and | ||
| // allow us to run the same tests on different databases and | ||
| // database acquisition strategies. | ||
|
|
||
| class AuthDatabaseProvider | ||
| { | ||
| public: | ||
| virtual ~AuthDatabaseProvider() = default; | ||
| virtual AuthDatabase *getAuthDatabase() = 0; | ||
| }; | ||
|
|
||
| class FixedProvider : public AuthDatabaseProvider | ||
| { | ||
| public: | ||
| FixedProvider(AuthDatabase *auth_db) : auth_db(auth_db){}; | ||
| virtual ~FixedProvider(){}; | ||
| virtual AuthDatabase *getAuthDatabase() { return auth_db; }; | ||
|
|
||
| private: | ||
| AuthDatabase *auth_db; | ||
| }; | ||
|
|
||
| class FilesProvider : public AuthDatabaseProvider | ||
| { | ||
| public: | ||
| FilesProvider(const std::string &dir) : dir(dir){}; | ||
| virtual ~FilesProvider() { delete auth_db; }; | ||
| virtual AuthDatabase *getAuthDatabase() | ||
| { | ||
| delete auth_db; | ||
| auth_db = new AuthDatabaseFiles(dir); | ||
| return auth_db; | ||
| }; | ||
|
|
||
| private: | ||
| std::string dir; | ||
| AuthDatabase *auth_db = nullptr; | ||
| }; | ||
|
|
||
| class SQLite3Provider : public AuthDatabaseProvider | ||
| { | ||
| public: | ||
| SQLite3Provider(const std::string &dir) : dir(dir){}; | ||
| virtual ~SQLite3Provider() { delete auth_db; }; | ||
| virtual AuthDatabase *getAuthDatabase() | ||
| { | ||
| delete auth_db; | ||
| auth_db = new AuthDatabaseSQLite3(dir); | ||
| return auth_db; | ||
| }; | ||
|
|
||
| private: | ||
| std::string dir; | ||
| AuthDatabase *auth_db = nullptr; | ||
| }; | ||
| } | ||
|
|
||
| class TestAuthDatabase : public TestBase | ||
| { | ||
| public: | ||
| TestAuthDatabase() | ||
| { | ||
| TestManager::registerTestModule(this); | ||
| // fixed directory, for persistence | ||
| test_dir = getTestTempDirectory(); | ||
| } | ||
| const char *getName() { return "TestAuthDatabase"; } | ||
|
|
||
| void runTests(IGameDef *gamedef); | ||
| void runTestsForCurrentDB(); | ||
|
|
||
| void testRecallFail(); | ||
| void testCreate(); | ||
| void testRecall(); | ||
| void testChange(); | ||
| void testRecallChanged(); | ||
| void testChangePrivileges(); | ||
| void testRecallChangedPrivileges(); | ||
| void testListNames(); | ||
| void testDelete(); | ||
|
|
||
| private: | ||
| std::string test_dir; | ||
| AuthDatabaseProvider *auth_provider; | ||
| }; | ||
|
|
||
| static TestAuthDatabase g_test_instance; | ||
|
|
||
| void TestAuthDatabase::runTests(IGameDef *gamedef) | ||
| { | ||
| // Each set of tests is run twice for each database type: | ||
| // one where we reuse the same AuthDatabase object (to test local caching), | ||
| // and one where we create a new AuthDatabase object for each call | ||
| // (to test actual persistence). | ||
|
|
||
| rawstream << "-------- Files database (same object)" << std::endl; | ||
|
|
||
| AuthDatabase *auth_db = new AuthDatabaseFiles(test_dir); | ||
| auth_provider = new FixedProvider(auth_db); | ||
|
|
||
| runTestsForCurrentDB(); | ||
|
|
||
| delete auth_db; | ||
| delete auth_provider; | ||
|
|
||
| // reset database | ||
| fs::DeleteSingleFileOrEmptyDirectory(test_dir + DIR_DELIM + "auth.txt"); | ||
|
|
||
| rawstream << "-------- Files database (new objects)" << std::endl; | ||
|
|
||
| auth_provider = new FilesProvider(test_dir); | ||
|
|
||
| runTestsForCurrentDB(); | ||
|
|
||
| delete auth_provider; | ||
|
|
||
| rawstream << "-------- SQLite3 database (same object)" << std::endl; | ||
|
|
||
| auth_db = new AuthDatabaseSQLite3(test_dir); | ||
| auth_provider = new FixedProvider(auth_db); | ||
|
|
||
| runTestsForCurrentDB(); | ||
|
|
||
| delete auth_db; | ||
| delete auth_provider; | ||
|
|
||
| // reset database | ||
| fs::DeleteSingleFileOrEmptyDirectory(test_dir + DIR_DELIM + "auth.sqlite"); | ||
|
|
||
| rawstream << "-------- SQLite3 database (new objects)" << std::endl; | ||
|
|
||
| auth_provider = new SQLite3Provider(test_dir); | ||
|
|
||
| runTestsForCurrentDB(); | ||
|
|
||
| delete auth_provider; | ||
| } | ||
|
|
||
| //////////////////////////////////////////////////////////////////////////////// | ||
|
|
||
| void TestAuthDatabase::runTestsForCurrentDB() | ||
| { | ||
| TEST(testRecallFail); | ||
| TEST(testCreate); | ||
| TEST(testRecall); | ||
| TEST(testChange); | ||
| TEST(testRecallChanged); | ||
| TEST(testChangePrivileges); | ||
| TEST(testRecallChangedPrivileges); | ||
| TEST(testListNames); | ||
| TEST(testDelete); | ||
| TEST(testRecallFail); | ||
| } | ||
|
|
||
| void TestAuthDatabase::testRecallFail() | ||
| { | ||
| AuthDatabase *auth_db = auth_provider->getAuthDatabase(); | ||
| AuthEntry authEntry; | ||
|
|
||
| // no such user yet | ||
| UASSERT(!auth_db->getAuth("TestName", authEntry)); | ||
| } | ||
|
|
||
| void TestAuthDatabase::testCreate() | ||
| { | ||
| AuthDatabase *auth_db = auth_provider->getAuthDatabase(); | ||
| AuthEntry authEntry; | ||
|
|
||
| authEntry.name = "TestName"; | ||
| authEntry.password = "TestPassword"; | ||
| authEntry.privileges.emplace_back("shout"); | ||
| authEntry.privileges.emplace_back("interact"); | ||
| authEntry.last_login = 1000; | ||
| UASSERT(auth_db->createAuth(authEntry)); | ||
| } | ||
|
|
||
| void TestAuthDatabase::testRecall() | ||
| { | ||
| AuthDatabase *auth_db = auth_provider->getAuthDatabase(); | ||
| AuthEntry authEntry; | ||
|
|
||
| UASSERT(auth_db->getAuth("TestName", authEntry)); | ||
| UASSERTEQ(std::string, authEntry.name, "TestName"); | ||
| UASSERTEQ(std::string, authEntry.password, "TestPassword"); | ||
| // the order of privileges is unimportant | ||
| std::sort(authEntry.privileges.begin(), authEntry.privileges.end()); | ||
| UASSERTEQ(std::string, str_join(authEntry.privileges, ","), "interact,shout"); | ||
| } | ||
|
|
||
| void TestAuthDatabase::testChange() | ||
| { | ||
| AuthDatabase *auth_db = auth_provider->getAuthDatabase(); | ||
| AuthEntry authEntry; | ||
|
|
||
| UASSERT(auth_db->getAuth("TestName", authEntry)); | ||
| authEntry.password = "NewPassword"; | ||
| authEntry.last_login = 1002; | ||
| UASSERT(auth_db->saveAuth(authEntry)); | ||
| } | ||
|
|
||
| void TestAuthDatabase::testRecallChanged() | ||
| { | ||
| AuthDatabase *auth_db = auth_provider->getAuthDatabase(); | ||
| AuthEntry authEntry; | ||
|
|
||
| UASSERT(auth_db->getAuth("TestName", authEntry)); | ||
| UASSERTEQ(std::string, authEntry.password, "NewPassword"); | ||
| // the order of privileges is unimportant | ||
| std::sort(authEntry.privileges.begin(), authEntry.privileges.end()); | ||
| UASSERTEQ(std::string, str_join(authEntry.privileges, ","), "interact,shout"); | ||
| UASSERTEQ(u64, authEntry.last_login, 1002); | ||
| } | ||
|
|
||
| void TestAuthDatabase::testChangePrivileges() | ||
| { | ||
| AuthDatabase *auth_db = auth_provider->getAuthDatabase(); | ||
| AuthEntry authEntry; | ||
|
|
||
| UASSERT(auth_db->getAuth("TestName", authEntry)); | ||
| authEntry.privileges.clear(); | ||
| authEntry.privileges.emplace_back("interact"); | ||
| authEntry.privileges.emplace_back("fly"); | ||
| authEntry.privileges.emplace_back("dig"); | ||
| UASSERT(auth_db->saveAuth(authEntry)); | ||
| } | ||
|
|
||
| void TestAuthDatabase::testRecallChangedPrivileges() | ||
| { | ||
| AuthDatabase *auth_db = auth_provider->getAuthDatabase(); | ||
| AuthEntry authEntry; | ||
|
|
||
| UASSERT(auth_db->getAuth("TestName", authEntry)); | ||
| // the order of privileges is unimportant | ||
| std::sort(authEntry.privileges.begin(), authEntry.privileges.end()); | ||
| UASSERTEQ(std::string, str_join(authEntry.privileges, ","), "dig,fly,interact"); | ||
| } | ||
|
|
||
| void TestAuthDatabase::testListNames() | ||
| { | ||
|
|
||
| AuthDatabase *auth_db = auth_provider->getAuthDatabase(); | ||
| std::vector<std::string> list; | ||
|
|
||
| AuthEntry authEntry; | ||
|
|
||
| authEntry.name = "SecondName"; | ||
| authEntry.password = "SecondPassword"; | ||
| authEntry.privileges.emplace_back("shout"); | ||
| authEntry.privileges.emplace_back("interact"); | ||
| authEntry.last_login = 1003; | ||
| auth_db->createAuth(authEntry); | ||
|
|
||
| auth_db->listNames(list); | ||
| // not necessarily sorted, so sort before comparing | ||
| std::sort(list.begin(), list.end()); | ||
| UASSERTEQ(std::string, str_join(list, ","), "SecondName,TestName"); | ||
| } | ||
|
|
||
| void TestAuthDatabase::testDelete() | ||
| { | ||
| AuthDatabase *auth_db = auth_provider->getAuthDatabase(); | ||
|
|
||
| UASSERT(!auth_db->deleteAuth("NoSuchName")); | ||
| UASSERT(auth_db->deleteAuth("TestName")); | ||
| // second try, expect failure | ||
| UASSERT(!auth_db->deleteAuth("TestName")); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can be a const method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried, and it's tricky. Any call will also call
verifyDatabase, which will potentially create the database, flag everything asm_initializedetc.Yes, I could mark any such things (the statements, too, are changed during reads until a
resetis called) asvolatile, but then I'd have to back-patch this into the map and player databases too; none of which, by the way, haveconstmethods. Is this really that critical?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not critical at all, just something I noticed.