diff --git a/rctctl/src/commands/research_marketing_commands.cpp b/rctctl/src/commands/research_marketing_commands.cpp index 77d31c2c4f4a..0827ffe14139 100644 --- a/rctctl/src/commands/research_marketing_commands.cpp +++ b/rctctl/src/commands/research_marketing_commands.cpp @@ -21,30 +21,10 @@ void AppendResearchMarketingCommands(std::vector& specs) "research", { "status" }, "Show research status.", - "Displays funding level, progress, upcoming discoveries, and category priorities.", - { CommandArgSpec{ "queue-limit", "Limit items shown from the queue.", false, "INT" }, - CommandArgSpec{ "queue-category", "Comma list of queue categories to include.", false, "LIST" }, - CommandArgSpec{ "queue-order", "Order queue by scenario, name, or category.", false, "FIELD" }, - CommandArgSpec{ "queue-direction", "Sort order asc/desc (ignored for scenario order).", false, "DIR" } }, - [](const ParsedArgs& args) { - json params = json::object(); - if (auto limit = cli::GetIntOption(args, { "queue-limit", "queueLimit" })) - { - params["queueLimit"] = *limit; - } - if (auto categories = cli::GetStringOption(args, { "queue-category", "queueCategories" })) - { - params["queueCategories"] = cli::SplitCommaSeparated(*categories); - } - if (auto order = cli::GetStringOption(args, { "queue-order", "queueOrder" })) - { - params["queueOrder"] = *order; - } - if (auto direction = cli::GetStringOption(args, { "queue-direction", "queueDirection" })) - { - params["queueDirection"] = *direction; - } - return CommandPlan{ "research.status", params }; + "Displays funding level, current research progress, and category priorities. Shows what a player would see in the research window - current item visibility depends on progress stage.", + {}, + [](const ParsedArgs&) { + return CommandPlan{ "research.status", json::object() }; }, renderers::RenderResearchStatus }); diff --git a/rctctl/src/commands/shop_commands.cpp b/rctctl/src/commands/shop_commands.cpp index 5f5d6b0eb20b..b0ee9e8cda61 100644 --- a/rctctl/src/commands/shop_commands.cpp +++ b/rctctl/src/commands/shop_commands.cpp @@ -25,10 +25,16 @@ void AppendShopCommands(std::vector& specs) "shops", { "catalog" }, "List available shop/stall blueprints.", - "Shows every loaded shop/stall object identifier, ride type, stocked items, and build cost. Once placed, use 'rides get' to inspect shop status and pricing.", - {}, - [](const ParsedArgs&) { - return CommandPlan{ "shops.catalog", json::object() }; + "Enumerates invented shop/stall objects (or every loaded entry with --all) with ride type, stocked items, and build cost. Once placed, use 'rides get' to inspect shop status and pricing.", + { CommandArgSpec{ "all", "Include locked/uninvented entries as well.", false, "BOOL" }, + CommandArgSpec{ "include-locked", "Alias for --all.", false, "BOOL" } }, + [](const ParsedArgs& args) { + json params = json::object(); + if (auto includeLocked = cli::GetBoolOption(args, { "all", "include-locked" })) + { + params["includeLocked"] = *includeLocked; + } + return CommandPlan{ "shops.catalog", params }; }, renderers::RenderShopCatalog }); diff --git a/rctctl/src/renderers/research_marketing.cpp b/rctctl/src/renderers/research_marketing.cpp index 14d099251c35..59d25a26c411 100644 --- a/rctctl/src/renderers/research_marketing.cpp +++ b/rctctl/src/renderers/research_marketing.cpp @@ -10,59 +10,35 @@ namespace rctctl::renderers { namespace { using json = nlohmann::json; -std::string FormatPercent(double value) +std::string DescribeStage(const json& stageValue) { - std::ostringstream oss; - oss << std::fixed << std::setprecision(1) << value << "%"; - return oss.str(); -} - -double ExtractProgressPercent(const json& result) -{ - if (auto it = result.find("progressPercent"); it != result.end() && it->is_number()) - { - return it->get(); - } - double raw = result.value("progress", 0.0); - int stage = result.value("progressStage", 0); - if (stage == 4) - { - return 100.0; - } - if (stage == 0) - { - return 0.0; - } - constexpr double kProgressUnits = 65535.0; - double percent = (raw / kProgressUnits) * 100.0; - if (percent < 0.0) - { - percent = 0.0; - } - if (percent > 100.0) + // Handle both string format (new) and numeric format (old/compatibility) + if (stageValue.is_string()) { - percent = 100.0; - } - return percent; -} - -const char* DescribeStage(int stage) -{ - switch (stage) - { - case 0: + std::string stage = stageValue.get(); + if (stage == "initialResearch") return "Initial research"; - case 1: + if (stage == "designing") return "Designing"; - case 2: + if (stage == "completingDesign") return "Completing design"; - case 3: - return "Unknown"; - case 4: - return "Finished"; - default: - return "Unknown"; + if (stage == "allComplete") + return "All complete"; + return "Unknown"; } + if (stageValue.is_number()) + { + int stage = stageValue.get(); + switch (stage) + { + case 0: return "Initial research"; + case 1: return "Designing"; + case 2: return "Completing design"; + case 255: return "All complete"; + default: return "Unknown"; + } + } + return "Unknown"; } } @@ -71,8 +47,12 @@ void RenderResearchStatus(const json& result) TextCanvas canvas(std::cout); canvas.Section("Research"); canvas.KeyValue("Funding", result.value("fundingLevel", std::string(""))); - canvas.KeyValue("Stage", DescribeStage(result.value("progressStage", 0))); - canvas.KeyValue("Progress", FormatPercent(ExtractProgressPercent(result))); + + auto stageIt = result.find("progressStage"); + if (stageIt != result.end()) + { + canvas.KeyValue("Stage", DescribeStage(*stageIt)); + } const auto& priorities = result.value("priorities", json::object()); if (!priorities.empty()) @@ -89,10 +69,46 @@ void RenderResearchStatus(const json& result) } } + // Current research - what we show depends on visibility stage if (auto nextIt = result.find("next"); nextIt != result.end()) { - canvas.KeyValue("Next discovery", nextIt->value("name", std::string(""))); + const auto& next = *nextIt; + std::string status = next.value("status", std::string("")); + + if (status == "unknown") + { + canvas.KeyValue("Current research", "Unknown"); + } + else if (status == "designing") + { + // Only category is visible + canvas.KeyValue("Current research", next.value("category", std::string("Unknown category"))); + } + else if (status == "completingDesign") + { + // Full name is visible + std::string name = next.value("name", std::string("")); + std::string category = next.value("category", std::string("")); + if (!name.empty()) + { + canvas.KeyValue("Current research", name + " (" + category + ")"); + } + else + { + canvas.KeyValue("Current research", category); + } + } + } + + // Expected completion + if (result.contains("expectedMonth") && result.contains("expectedDay")) + { + std::ostringstream expected; + expected << "Day " << result.value("expectedDay", 0) << ", Month " << (result.value("expectedMonth", 0) + 1); + canvas.KeyValue("Expected", expected.str()); } + + // Last completed research if (auto lastIt = result.find("last"); lastIt != result.end()) { canvas.KeyValue("Last discovery", lastIt->value("name", std::string(""))); @@ -102,23 +118,12 @@ void RenderResearchStatus(const json& result) { canvas.Paragraph("All research complete."); } - - if (auto queueIt = result.find("queue"); queueIt != result.end() && queueIt->is_array()) + else { - const auto& queue = *queueIt; - if (!queue.empty()) + int remaining = result.value("uninventedCount", 0); + if (remaining > 0) { - canvas.Section("Queue"); - canvas.KeyValue("Entries", static_cast(queue.size())); - TableView table; - table.headers = { "Name", "Category", "Type" }; - for (const auto& entry : queue) - { - table.rows.push_back({ entry.value("name", std::string("")), - entry.value("category", entry.value("categoryKey", std::string(""))), - entry.value("type", std::string("")) }); - } - canvas.Table(table); + canvas.KeyValue("Items remaining", remaining); } } } diff --git a/rctctl/src/renderers/shops.cpp b/rctctl/src/renderers/shops.cpp index f1598cc9bd64..0f43f8d66176 100644 --- a/rctctl/src/renderers/shops.cpp +++ b/rctctl/src/renderers/shops.cpp @@ -99,7 +99,7 @@ void RenderShopCatalog(const json& result) canvas.KeyValue("Entries", static_cast(entries.size())); TableView table; - table.headers = { "Name", "Type", "Items", "Build", "Use with" }; + table.headers = { "Name", "Type", "Status", "Items", "Build", "Use with" }; for (const auto& entry : entries) { auto name = entry.value("name", entry.value("identifier", std::string())); @@ -107,6 +107,7 @@ void RenderShopCatalog(const json& result) auto classification = entry.value("classification", std::string()); auto buildCost = entry.value("buildCost", 0.0); auto labels = ExtractItemLabels(entry.value("items", json::array())); + std::string status = entry.value("invented", false) ? "Available" : "Locked"; std::string typeLabel = rideType; if (!classification.empty()) @@ -118,7 +119,7 @@ void RenderShopCatalog(const json& result) typeLabel += classification; } - table.rows.push_back({ name, typeLabel, JoinItemList(labels), util::FormatCurrency(buildCost), + table.rows.push_back({ name, typeLabel, status, JoinItemList(labels), util::FormatCurrency(buildCost), BuildSelectorLabel(entry) }); } canvas.Table(table); diff --git a/src/openrct2/scripting/rpc/handlers/ResearchHandlers.cpp b/src/openrct2/scripting/rpc/handlers/ResearchHandlers.cpp index c1e45d1160b1..28e7e09ff7a5 100644 --- a/src/openrct2/scripting/rpc/handlers/ResearchHandlers.cpp +++ b/src/openrct2/scripting/rpc/handlers/ResearchHandlers.cpp @@ -397,17 +397,35 @@ namespace OpenRCT2::Scripting::Rpc::Handlers return true; } - json_t BuildResearchStatusPayload(const ResearchStatusQuery& query) + json_t BuildResearchStatusPayload(const ResearchStatusQuery& /*query*/) { const auto& gameState = getGameState(); json_t payload = json_t::object(); payload["fundingLevel"] = ResearchFundingLevelToString(gameState.researchFundingLevel); payload["fundingIndex"] = gameState.researchFundingLevel; - payload["progress"] = gameState.researchProgress; - payload["progressStage"] = gameState.researchProgressStage; - payload["progressPercent"] = CalculateResearchProgressPercent(gameState.researchProgress, gameState.researchProgressStage); - payload["expectedMonth"] = gameState.researchExpectedMonth; - payload["expectedDay"] = gameState.researchExpectedDay; + + // Progress stage info - but not raw progress value (that's internal) + const auto stage = gameState.researchProgressStage; + std::string stageLabel; + switch (stage) + { + case RESEARCH_STAGE_INITIAL_RESEARCH: + stageLabel = "initialResearch"; + break; + case RESEARCH_STAGE_DESIGNING: + stageLabel = "designing"; + break; + case RESEARCH_STAGE_COMPLETING_DESIGN: + stageLabel = "completingDesign"; + break; + case RESEARCH_STAGE_FINISHED_ALL: + stageLabel = "allComplete"; + break; + default: + stageLabel = "unknown"; + break; + } + payload["progressStage"] = stageLabel; json_t priorities = json_t::object(); auto addPriority = [&](ResearchCategory category, std::string_view name) { @@ -423,90 +441,58 @@ namespace OpenRCT2::Scripting::Rpc::Handlers addPriority(ResearchCategory::sceneryGroup, "scenery"); payload["priorities"] = priorities; - if (gameState.researchNextItem) - { - payload["next"] = BuildResearchItemPayload(gameState.researchNextItem.value()); - } - if (gameState.researchLastItem) + // Current research - visibility depends on stage (like the in-game UI) + // RESEARCH_STAGE_INITIAL_RESEARCH: Nothing visible ("Unknown") + // RESEARCH_STAGE_DESIGNING: Category only visible + // RESEARCH_STAGE_COMPLETING_DESIGN: Full item name visible + if (gameState.researchNextItem && stage != RESEARCH_STAGE_FINISHED_ALL) { - payload["last"] = BuildResearchItemPayload(gameState.researchLastItem.value()); - } + json_t next = json_t::object(); + const auto& item = gameState.researchNextItem.value(); - std::vector queueEntries; - size_t scenarioIndex = 0; - for (const auto& item : gameState.researchItemsUninvented) - { - if (!query.categories.empty()) + if (stage == RESEARCH_STAGE_INITIAL_RESEARCH) { - bool match = false; - for (auto category : query.categories) - { - if (category == item.category) - { - match = true; - break; - } - } - if (!match) - { - continue; - } + // Player sees "Unknown" - we show nothing specific + next["status"] = "unknown"; } - json_t entry = BuildResearchItemPayload(item); - entry["queueIndex"] = scenarioIndex++; - queueEntries.push_back(entry); - } - - if (query.queueOrder != ResearchQueueOrder::Scenario) - { - std::sort(queueEntries.begin(), queueEntries.end(), [&](const json_t& lhs, const json_t& rhs) { - switch (query.queueOrder) - { - case ResearchQueueOrder::Name: - { - int cmp = CompareCaseInsensitive(lhs.value("name", std::string("")), rhs.value("name", std::string(""))); - if (cmp != 0) - { - return query.descending ? cmp > 0 : cmp < 0; - } - break; - } - case ResearchQueueOrder::Category: - { - int cmp = CompareCaseInsensitive(lhs.value("categoryKey", std::string("")), rhs.value("categoryKey", std::string(""))); - if (cmp != 0) - { - return query.descending ? cmp > 0 : cmp < 0; - } - break; - } - case ResearchQueueOrder::Scenario: - break; - } - auto leftIndex = lhs.value("queueIndex", 0); - auto rightIndex = rhs.value("queueIndex", 0); - return leftIndex < rightIndex; - }); - } + else if (stage == RESEARCH_STAGE_DESIGNING) + { + // Player sees category only (e.g., "Roller Coaster") + next["status"] = "designing"; + next["category"] = ResolveStringId(item.GetCategoryName()); + next["categoryKey"] = ResearchCategoryToKey(item.category); + } + else if (stage == RESEARCH_STAGE_COMPLETING_DESIGN) + { + // Player sees full item name + next["status"] = "completingDesign"; + next["category"] = ResolveStringId(item.GetCategoryName()); + next["categoryKey"] = ResearchCategoryToKey(item.category); + next["name"] = ResolveStringId(item.GetName()); + next["type"] = item.type == OpenRCT2::Research::EntryType::ride ? "ride" : "scenery"; + } + payload["next"] = next; - json_t queue = json_t::array(); - size_t emitted = 0; - for (const auto& entry : queueEntries) - { - queue.push_back(entry); - emitted++; - if (query.limitEnabled && emitted >= query.queueLimit) + // Expected completion date - only visible after initial research + if (stage != RESEARCH_STAGE_INITIAL_RESEARCH && gameState.researchExpectedDay != 255) { - break; + payload["expectedMonth"] = gameState.researchExpectedMonth; + payload["expectedDay"] = gameState.researchExpectedDay; } } - payload["queue"] = queue; - payload["queueCount"] = queueEntries.size(); - if (query.limitEnabled) + + // Last completed research - player can always see this + if (gameState.researchLastItem) { - payload["queueLimit"] = query.queueLimit; + payload["last"] = BuildResearchItemPayload(gameState.researchLastItem.value()); } - payload["allComplete"] = gameState.researchProgressStage == RESEARCH_STAGE_FINISHED_ALL; + + // NOTE: We intentionally do NOT expose the research queue. + // Players cannot see what will be researched next or in what order. + // This would give an unfair advantage. + + payload["allComplete"] = stage == RESEARCH_STAGE_FINISHED_ALL; + payload["uninventedCount"] = static_cast(gameState.researchItemsUninvented.size()); return payload; } diff --git a/src/openrct2/scripting/rpc/handlers/ShopHandlers.cpp b/src/openrct2/scripting/rpc/handlers/ShopHandlers.cpp index 587145c7f7fc..c54b3f8b9e82 100644 --- a/src/openrct2/scripting/rpc/handlers/ShopHandlers.cpp +++ b/src/openrct2/scripting/rpc/handlers/ShopHandlers.cpp @@ -26,6 +26,7 @@ #include "../../../core/Numerics.hpp" #include "../../../interface/WindowBase.h" #include "../../../localisation/Formatter.h" +#include "../../../management/Research.h" #include "../../../localisation/Formatting.h" #include "../../../localisation/StringIds.h" #include "../../../object/ObjectList.h" @@ -495,11 +496,12 @@ namespace OpenRCT2::Scripting::Rpc::Handlers return items; } - json_t BuildShopCatalogPayload(OpenRCT2::IContext& context) + json_t BuildShopCatalogPayload(OpenRCT2::IContext& context, bool includeLocked = false) { auto& manager = context.GetObjectManager(); auto maxEntries = static_cast(getObjectEntryGroupCount(ObjectType::ride)); json_t entries = json_t::array(); + size_t loadedEntries = 0; for (ObjectEntryIndex i = 0; i < maxEntries; ++i) { auto* rideObject = manager.GetLoadedObject(i); @@ -513,6 +515,14 @@ namespace OpenRCT2::Scripting::Rpc::Handlers { continue; } + loadedEntries++; + + // Research/availability status + const bool invented = ResearchIsInvented(ObjectType::ride, i) || RideEntryIsInvented(i); + if (!includeLocked && !invented) + { + continue; + } json_t node = json_t::object(); node["identifier"] = shopInfo->legacyIdentifier; @@ -531,6 +541,10 @@ namespace OpenRCT2::Scripting::Rpc::Handlers node["classification"] = std::string(RideClassificationToString(shopInfo->descriptor->Classification)); node["category"] = std::string(RideCategoryToString(shopInfo->descriptor->Category)); + + node["invented"] = invented; + node["available"] = invented; // Alias for clarity + node["startPiece"] = static_cast(shopInfo->descriptor->StartTrackPiece); node["defaultPrices"] = json_t::array({ MoneyToDouble(shopInfo->descriptor->DefaultPrices[0]), @@ -544,6 +558,8 @@ namespace OpenRCT2::Scripting::Rpc::Handlers json_t payload = json_t::object(); payload["entries"] = entries; payload["count"] = entries.size(); + payload["includeLocked"] = includeLocked; + payload["loadedEntries"] = loadedEntries; return payload; } @@ -957,16 +973,18 @@ namespace OpenRCT2::Scripting::Rpc::Handlers } // Handler implementations - RpcResult HandleShopsCatalog(const json_t& /*params*/) + RpcResult HandleShopsCatalog(const json_t& params) { auto* context = GetContext(); if (context == nullptr) { return RpcResult::Error(-32603, "Game context is not available"); } - auto payload = BuildShopCatalogPayload(*context); + const bool includeLocked = params.is_object() ? GetBoolParam(params, "includeLocked").value_or(false) : false; + auto payload = BuildShopCatalogPayload(*context, includeLocked); + std::string contextLabel = includeLocked ? "Browsed all shop blueprints" : "Browsed invented shop blueprints"; auto hint = MakeConstructRideHint( - "shops.catalog", "Browsed shop catalog", Telemetry::AIAgentConstructRideTab::Shop); + "shops.catalog", std::move(contextLabel), Telemetry::AIAgentConstructRideTab::Shop); return RpcResult::Ok(payload, std::move(hint)); }