From b4fa732047ade17afac099bea85cdebe93a9fc77 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 14:44:23 -0500 Subject: [PATCH] fix(ugc): answer the client's built-in UGC path, and document how it downloads In the 1.10.64 client tested, 3D services ignore boot.cfg's UGC and patch server lines and ask http://127.0.0.1:80/lwoclient/UserBrickModels/...; the UGC server now answers that path too (useful when it runs on port 80), and the docs say to keep UGCUSE3DSERVICES=7:0 (the client builds the models from the world's LXFML; verified that property models load) until it can. Co-Authored-By: Claude Opus 5.5 --- dUgcServer/UgcServer.cpp | 41 ++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/dUgcServer/UgcServer.cpp b/dUgcServer/UgcServer.cpp index 3d7293a0a..82a7e3ebd 100644 --- a/dUgcServer/UgcServer.cpp +++ b/dUgcServer/UgcServer.cpp @@ -228,23 +228,29 @@ namespace { } void RegisterRoutes() { - const auto prefix = Segments(Lower(Game::config->GetValue("client_path").empty() ? "/ugc" : Game::config->GetValue("client_path"))); - std::string base; - for (const auto& segment : prefix) base += "/" + segment; - - const auto clientRoute = [prefixSize = prefix.size()](HTTPReply& reply, const HTTPContext& context) { - auto segments = Segments(context.originalPath); - if (segments.size() <= prefixSize) { - NotFound(reply); - return; - } - segments.erase(segments.begin(), segments.begin() + static_cast(prefixSize)); - ServeClientDownload(reply, segments); - // What the client asked for and what it got, to see how it loads models - LOG("Client download %s -> %i%s", context.originalPath.c_str(), static_cast(reply.status), reply.file.empty() ? "" : " (file)"); - }; - Game::web.RegisterHTTPRoute({ .path = base + "/:folder/:file", .method = eHTTPMethod::GET, .middleware = {}, .handle = clientRoute }); - Game::web.RegisterHTTPRoute({ .path = base + "/:folder/:type/:file", .method = eHTTPMethod::GET, .middleware = {}, .handle = clientRoute }); + // The configured path, and the one the 1.10.64 client uses whatever its boot.cfg says (its built-in patch server + // folder, lwoclient/UserBrickModels; see docs/UgcServer.md) + std::set bases; + for (const auto& path : { Game::config->GetValue("client_path").empty() ? std::string("/ugc") : Game::config->GetValue("client_path"), std::string("/lwoclient/UserBrickModels") }) { + const auto prefix = Segments(Lower(path)); + std::string base; + for (const auto& segment : prefix) base += "/" + segment; + if (!bases.insert(base).second) continue; + const auto clientRoute = [prefixSize = prefix.size()](HTTPReply& reply, const HTTPContext& context) { + auto segments = Segments(context.originalPath); + if (segments.size() <= prefixSize) { + NotFound(reply); + return; + } + segments.erase(segments.begin(), segments.begin() + static_cast(prefixSize)); + ServeClientDownload(reply, segments); + // What the client asked for and what it got, to see how it loads models + LOG("Client download %s -> %i%s", context.originalPath.c_str(), static_cast(reply.status), reply.file.empty() ? "" : " (file)"); + }; + Game::web.RegisterHTTPRoute({ .path = base + "/:folder/:file", .method = eHTTPMethod::GET, .middleware = {}, .handle = clientRoute }); + Game::web.RegisterHTTPRoute({ .path = base + "/:folder/:type/:file", .method = eHTTPMethod::GET, .middleware = {}, .handle = clientRoute }); + LOG("Serving the client's downloads under %s/UGCC/", base.c_str()); + } // Previews for the dashboard Game::web.RegisterHTTPRoute({ .path = "/files/:kind/:id/:file", .method = eHTTPMethod::GET, .middleware = {}, @@ -361,7 +367,6 @@ namespace { reply.headers.push_back("Access-Control-Allow-Origin: *"); reply.headers.push_back("Cache-Control: no-store"); } }); - LOG("Serving the client's downloads under %s/UGCC/", base.c_str()); } std::filesystem::path ResPath() {