diff --git a/dDashboardServer/routes/Scenery.cpp b/dDashboardServer/routes/Scenery.cpp index 5308c3f5a..926e8ec35 100644 --- a/dDashboardServer/routes/Scenery.cpp +++ b/dDashboardServer/routes/Scenery.cpp @@ -144,8 +144,32 @@ namespace { std::string asset; // RenderComponent.render_asset as stored std::string type; // Objects.type int32_t shader{ -1 }; // mapShaders.gameValue of RenderComponent.shader_id (-1 fixed function or unknown) + bool hidesOnLoad{}; // its client script hides it as soon as it loads (WorldScene::ClientScriptHidesOnLoad) }; + // A client script's text, by its ScriptComponent path (backslashes, any case); empty when the client has none + std::string ReadClientScript(const std::string& stored) { + static const auto index = [] { + std::unordered_map files; + const auto res = ClientAssets::ResFolder(); + if (res.empty()) return files; + std::error_code ec; + for (const auto& top : std::filesystem::directory_iterator(res, ec)) { + if (Lower(top.path().filename().string()) != "scripts" || !top.is_directory(ec)) continue; + for (auto it = std::filesystem::recursive_directory_iterator(top.path(), ec); !ec && it != std::filesystem::recursive_directory_iterator(); it.increment(ec)) { + if (it->is_regular_file(ec)) files.emplace(Lower(std::filesystem::relative(it->path(), res, ec).generic_string()), it->path()); + } + } + return files; + }(); + auto key = Lower(stored); + std::replace(key.begin(), key.end(), '\\', '/'); + const auto it = index.find(key); + if (it == index.end()) return {}; + std::ifstream in(it->second, std::ios::binary); + return std::string(std::istreambuf_iterator(in), std::istreambuf_iterator()); + } + // mapShaders: id (what RenderComponent.shader_id and multishader tags name) -> gameValue (the shader drawn) std::map g_ShaderValues; @@ -161,6 +185,19 @@ namespace { } auto shaders = CDClientDatabase::ExecuteQuery("SELECT id, gameValue FROM mapShaders;"); for (; !shaders.eof(); shaders.nextRow()) g_ShaderValues.try_emplace(shaders.getIntField(0), shaders.getIntField(1)); + // Objects whose client script hides them as soon as they load + auto scripts = CDClientDatabase::ExecuteQuery( + "SELECT cr.id, sc.client_script_name FROM ComponentsRegistry cr JOIN ScriptComponent sc ON sc.id = cr.component_id " + "WHERE cr.component_type = " + std::to_string(static_cast(eReplicaComponentType::SCRIPT)) + " AND sc.client_script_name IS NOT NULL AND sc.client_script_name != '';"); + std::unordered_map hides; // by script, read once + for (; !scripts.eof(); scripts.nextRow()) { + const auto it = infos.find(static_cast(scripts.getIntField(0))); + if (it == infos.end()) continue; + const std::string script = scripts.getStringField(1, ""); + auto [known, added] = hides.try_emplace(script, false); + if (added) known->second = WorldScene::ClientScriptHidesOnLoad(ReadClientScript(script)); + it->second.hidesOnLoad = it->second.hidesOnLoad || known->second; + } auto types = CDClientDatabase::ExecuteQuery("SELECT id, type FROM Objects;"); for (; !types.eof(); types.nextRow()) { const auto it = infos.find(static_cast(types.getIntField(0))); @@ -192,7 +229,8 @@ namespace { const auto& infos = RenderInfos(); const auto info = infos.find(lot); if (info == infos.end()) return {}; - const auto draw = WorldScene::ClientDraws(object, info->second.type); + auto draw = WorldScene::ClientDraws(object, info->second.type); + if (draw == WorldScene::eClientDraw::DRAWN && info->second.hidesOnLoad) draw = WorldScene::eClientDraw::HIDDEN; if (draw == WorldScene::eClientDraw::NO_MODEL) return {}; static std::mutex mutex; static std::unordered_map resolved; diff --git a/dDashboardServer/routes/WorldScene.h b/dDashboardServer/routes/WorldScene.h index 90af84707..5b26f9419 100644 --- a/dDashboardServer/routes/WorldScene.h +++ b/dDashboardServer/routes/WorldScene.h @@ -82,6 +82,38 @@ namespace WorldScene { DRAWN }; + /** + * Whether a client script hides its object as soon as it loads: a self:SetVisible{visible = false ...} statement + * directly in the body of onStartup or onRenderComponentReady, not inside a condition (as + * scripts/02_client/map/general/l_set_invisible.lua on "Clear threat list Trigger Wall", or l_bootydig_client.lua, + * which shows its object again once it's dug). The game runs the script; the viewers read it. + */ + inline bool ClientScriptHidesOnLoad(const std::string& script) { + std::istringstream lines(script); + std::string line; + bool inLoadHandler = false; + while (std::getline(lines, line)) { + if (!line.empty() && line.back() == '\r') line.pop_back(); + if (line.starts_with("function ")) { + const auto name = line.substr(9, line.find('(') == std::string::npos ? std::string::npos : line.find('(') - 9); + inLoadHandler = name == "onStartup" || name == "onRenderComponentReady"; + continue; + } + if (line.starts_with("end")) { + inLoadHandler = false; + continue; + } + if (!inLoadHandler) continue; + // One level in: a tab, or up to four spaces + const auto indent = line.find_first_not_of(" \t"); + if (indent == std::string::npos || !(line.compare(0, indent, "\t") == 0 || (indent <= 4 && line.find('\t') >= indent))) continue; + std::string statement; + for (const char c : line.substr(indent)) if (c != ' ') statement += c; + if (statement.starts_with("self:SetVisible{visible=false")) return true; + } + return false; + } + /** * Whether the client draws an object's model, given its LOT's Objects.type. As the client's * ObjectLoader2::LoadRenderComponent (0x01053ca5 in client 1.10.64): nothing for renderDisabled or CreateNULLRender, diff --git a/tests/dWebTests/WorldViewTests.cpp b/tests/dWebTests/WorldViewTests.cpp index f797bc961..912c9b2fc 100644 --- a/tests/dWebTests/WorldViewTests.cpp +++ b/tests/dWebTests/WorldViewTests.cpp @@ -142,3 +142,16 @@ TEST(WorldSceneTests, ClientDrawsWhatTheGameDraws) { EXPECT_EQ(WorldScene::ClientDraws(read[4], "Smashables"), eClientDraw::NO_MODEL); EXPECT_EQ(WorldScene::ClientDraws(read[5], "Environmental"), eClientDraw::HIDDEN); } + +// A client script that hides its object as soon as it loads (a top-level SetVisible false in onStartup or +// onRenderComponentReady) makes the viewers treat the object as hidden; conditional ones don't +TEST(WorldSceneTests, ClientScriptsThatHideOnLoad) { + // scripts/02_client/map/general/l_set_invisible.lua + EXPECT_TRUE(WorldScene::ClientScriptHidesOnLoad("-- comment\nfunction onRenderComponentReady(self,msg)\n\tself:SetVisible{visible = false, fadeTime = 0}\nend")); + EXPECT_TRUE(WorldScene::ClientScriptHidesOnLoad("function onStartup(self)\r\n self:SetVisible{ visible = false }\r\nend\r\n")); + // Only when a condition holds, in another handler, or shown + EXPECT_FALSE(WorldScene::ClientScriptHidesOnLoad("function onStartup(self)\n\tif x then\n\t\tself:SetVisible{visible = false}\n\tend\nend")); + EXPECT_FALSE(WorldScene::ClientScriptHidesOnLoad("function onTimerDone(self,msg)\n\tself:SetVisible{visible = false}\nend")); + EXPECT_FALSE(WorldScene::ClientScriptHidesOnLoad("function onStartup(self)\n\tself:SetVisible{visible = true}\nend")); + EXPECT_FALSE(WorldScene::ClientScriptHidesOnLoad("")); +}