From 1b5c1bf3985976f17d5f7436ab9d973a6203d69c Mon Sep 17 00:00:00 2001 From: Procyonae <45432782+Procyonae@users.noreply.github.com> Date: Tue, 9 Apr 2024 23:54:46 +0100 Subject: [PATCH] Basecamp recipes can specify parameters and om_terrain_match_type (#72642) * Pointers and defaults are confuse * Overkill overloads * Less stoopid * multimap -> map of value sets * ANY handling and astyle * Underp only being allowed one om_terrain * om_terrain_match_type usage * Documentation + starting parameter docs I forgot to add --- .../json/recipes/basecamps/recipe_groups.json | 150 ++++-------------- doc/BASECAMP.md | 52 +++++- doc/JSON_INFO.md | 34 +++- src/condition.cpp | 6 +- src/faction_camp.cpp | 13 +- src/npctalk.cpp | 2 +- src/recipe_groups.cpp | 88 ++++++++-- src/recipe_groups.h | 6 +- 8 files changed, 198 insertions(+), 153 deletions(-) diff --git a/data/json/recipes/basecamps/recipe_groups.json b/data/json/recipes/basecamps/recipe_groups.json index 41f302dbb9962..7c30f7cc3fecd 100644 --- a/data/json/recipes/basecamps/recipe_groups.json +++ b/data/json/recipes/basecamps/recipe_groups.json @@ -11,14 +11,8 @@ "field", "rural_road", "dirt_road_farm_parking", - "farmland_straight", - "farmland_turn", - "farmland_turn_inside", - "farmland_U", - "hayfield_straight", - "hayfield_turnL", - "hayfield_turnR", - "hayfield_end" + { "om_terrain": "farmland", "om_terrain_match_type": "PREFIX" }, + { "om_terrain": "hayfield", "om_terrain_match_type": "PREFIX" } ] }, { @@ -28,14 +22,8 @@ "field", "rural_road", "dirt_road_farm_parking", - "farmland_straight", - "farmland_turn", - "farmland_turn_inside", - "farmland_U", - "hayfield_straight", - "hayfield_turnL", - "hayfield_turnR", - "hayfield_end" + { "om_terrain": "farmland", "om_terrain_match_type": "PREFIX" }, + { "om_terrain": "hayfield", "om_terrain_match_type": "PREFIX" } ] }, { "id": "faction_base_firestation_0", "description": "Firestation Base", "om_terrains": [ "fire_station" ] }, @@ -140,14 +128,8 @@ "field", "rural_road", "dirt_road_farm_parking", - "farmland_straight", - "farmland_turn", - "farmland_turn_inside", - "farmland_U", - "hayfield_straight", - "hayfield_turnL", - "hayfield_turnR", - "hayfield_end" + { "om_terrain": "farmland", "om_terrain_match_type": "PREFIX" }, + { "om_terrain": "hayfield", "om_terrain_match_type": "PREFIX" } ] }, { @@ -157,14 +139,8 @@ "field", "rural_road", "dirt_road_farm_parking", - "farmland_straight", - "farmland_turn", - "farmland_turn_inside", - "farmland_U", - "hayfield_straight", - "hayfield_turnL", - "hayfield_turnR", - "hayfield_end" + { "om_terrain": "farmland", "om_terrain_match_type": "PREFIX" }, + { "om_terrain": "hayfield", "om_terrain_match_type": "PREFIX" } ] }, { @@ -174,14 +150,8 @@ "field", "rural_road", "dirt_road_farm_parking", - "farmland_straight", - "farmland_turn", - "farmland_turn_inside", - "farmland_U", - "hayfield_straight", - "hayfield_turnL", - "hayfield_turnR", - "hayfield_end" + { "om_terrain": "farmland", "om_terrain_match_type": "PREFIX" }, + { "om_terrain": "hayfield", "om_terrain_match_type": "PREFIX" } ] }, { @@ -191,14 +161,8 @@ "field", "rural_road", "dirt_road_farm_parking", - "farmland_straight", - "farmland_turn", - "farmland_turn_inside", - "farmland_U", - "hayfield_straight", - "hayfield_turnL", - "hayfield_turnR", - "hayfield_end" + { "om_terrain": "farmland", "om_terrain_match_type": "PREFIX" }, + { "om_terrain": "hayfield", "om_terrain_match_type": "PREFIX" } ] }, { @@ -208,14 +172,8 @@ "field", "rural_road", "dirt_road_farm_parking", - "farmland_straight", - "farmland_turn", - "farmland_turn_inside", - "farmland_U", - "hayfield_straight", - "hayfield_turnL", - "hayfield_turnR", - "hayfield_end" + { "om_terrain": "farmland", "om_terrain_match_type": "PREFIX" }, + { "om_terrain": "hayfield", "om_terrain_match_type": "PREFIX" } ] }, { @@ -225,14 +183,8 @@ "field", "rural_road", "dirt_road_farm_parking", - "farmland_straight", - "farmland_turn", - "farmland_turn_inside", - "farmland_U", - "hayfield_straight", - "hayfield_turnL", - "hayfield_turnR", - "hayfield_end" + { "om_terrain": "farmland", "om_terrain_match_type": "PREFIX" }, + { "om_terrain": "hayfield", "om_terrain_match_type": "PREFIX" } ] }, { @@ -242,14 +194,8 @@ "field", "rural_road", "dirt_road_farm_parking", - "farmland_straight", - "farmland_turn", - "farmland_turn_inside", - "farmland_U", - "hayfield_straight", - "hayfield_turnL", - "hayfield_turnR", - "hayfield_end" + { "om_terrain": "farmland", "om_terrain_match_type": "PREFIX" }, + { "om_terrain": "hayfield", "om_terrain_match_type": "PREFIX" } ] }, { @@ -259,14 +205,8 @@ "field", "rural_road", "dirt_road_farm_parking", - "farmland_straight", - "farmland_turn", - "farmland_turn_inside", - "farmland_U", - "hayfield_straight", - "hayfield_turnL", - "hayfield_turnR", - "hayfield_end" + { "om_terrain": "farmland", "om_terrain_match_type": "PREFIX" }, + { "om_terrain": "hayfield", "om_terrain_match_type": "PREFIX" } ] }, { @@ -276,14 +216,8 @@ "field", "rural_road", "dirt_road_farm_parking", - "farmland_straight", - "farmland_turn", - "farmland_turn_inside", - "farmland_U", - "hayfield_straight", - "hayfield_turnL", - "hayfield_turnR", - "hayfield_end" + { "om_terrain": "farmland", "om_terrain_match_type": "PREFIX" }, + { "om_terrain": "hayfield", "om_terrain_match_type": "PREFIX" } ] }, { @@ -293,14 +227,8 @@ "field", "rural_road", "dirt_road_farm_parking", - "farmland_straight", - "farmland_turn", - "farmland_turn_inside", - "farmland_U", - "hayfield_straight", - "hayfield_turnL", - "hayfield_turnR", - "hayfield_end" + { "om_terrain": "farmland", "om_terrain_match_type": "PREFIX" }, + { "om_terrain": "hayfield", "om_terrain_match_type": "PREFIX" } ] }, { @@ -310,14 +238,8 @@ "field", "rural_road", "dirt_road_farm_parking", - "farmland_straight", - "farmland_turn", - "farmland_turn_inside", - "farmland_U", - "hayfield_straight", - "hayfield_turnL", - "hayfield_turnR", - "hayfield_end" + { "om_terrain": "farmland", "om_terrain_match_type": "PREFIX" }, + { "om_terrain": "hayfield", "om_terrain_match_type": "PREFIX" } ] }, { @@ -327,14 +249,8 @@ "field", "rural_road", "dirt_road_farm_parking", - "farmland_straight", - "farmland_turn", - "farmland_turn_inside", - "farmland_U", - "hayfield_straight", - "hayfield_turnL", - "hayfield_turnR", - "hayfield_end" + { "om_terrain": "farmland", "om_terrain_match_type": "PREFIX" }, + { "om_terrain": "hayfield", "om_terrain_match_type": "PREFIX" } ] }, { @@ -344,14 +260,8 @@ "field", "rural_road", "dirt_road_farm_parking", - "farmland_straight", - "farmland_turn", - "farmland_turn_inside", - "farmland_U", - "hayfield_straight", - "hayfield_turnL", - "hayfield_turnR", - "hayfield_end" + { "om_terrain": "farmland", "om_terrain_match_type": "PREFIX" }, + { "om_terrain": "hayfield", "om_terrain_match_type": "PREFIX" } ] }, { diff --git a/doc/BASECAMP.md b/doc/BASECAMP.md index e62a13715f50f..da40ed158b719 100644 --- a/doc/BASECAMP.md +++ b/doc/BASECAMP.md @@ -216,7 +216,6 @@ the needs must be autocalculated. ## Recipe groups Recipe groups serve two purposes: they indicate what recipes can produced by the camp after an upgrade mission is completed, and they indicate what upgrade paths are available and where camps can be placed. -### Upgrade Paths and Expansions There are two special recipe groups, `"all_faction_base_types"` and `"all_faction_base_expansions"`. They both look like this: ```json { @@ -232,11 +231,56 @@ There are two special recipe groups, `"all_faction_base_types"` and `"all_factio }, ``` -Each entry in the `"recipes"` array must be a dictionary with the `"id"`, `"description"`, and `"om_terrains"` fields. `"id"` is the recipe `"id"` of the recipe that starts that basecamp or basecamp expansion upgrade path, and has to conform to the pattern `"faction_base_X_0"`, where X distinguishes the entry from the others, with the prefix and suffix required by the code. `"description"` is a short name of the basecamp or basecamp expansion. `"om_terrains"` is a list of overmap terrain ids which can be used as the basis for the basecamp or basecamp expansion. +### Upgrade Paths and Expansions + +All recipes that start an upgrade path or expansion should have a blueprint requirement that can never be met, such as `"not_an_upgrade"`, to prevent them from showing up as available upgrades. +Additionally, if you want to add an expansion, you must create an OMT with the same `id` as the expansion's `id`. +If the player attempts to start a basecamp on an overmap terrain that has two or more valid basecamp expansion paths, they will allowed to choose which path to start. + +Each entry in the `"recipes"` array must be a dictionary with the `"id"`, `"description"`, and `"om_terrains"` fields. + +---- Field ---- | --------------------------------------------------------------------------------------------------------- Description --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +`"id"` | Id of the recipe that starts that basecamp or basecamp expansion upgrade path, and has to conform to the pattern `"faction_base_X_0"`, where X distinguishes the entry from the others, with the prefix and suffix required by the code. | +`"description"` | The name shown in-game for the basecamp or basecamp expansion. | +`"om_terrains"` | An array of overmap terrain ids which can be used as the basis for the basecamp or basecamp expansion. Individual entries can either be a string or an object containing `"om_terrain"` for the overmap terrain id, along with at least one of `"parameters"` and `"om_terrain_match_type"` (see [JSON_INFO.md](JSON_INFO.md#Starting-locations)) | + + +#### Examples + +Camp that can be made anywhere + +```json + { + "id": "faction_base_barebones_0", + "description": "Barebones Camp", + "om_terrains": [ "ANY" ] + }, +``` + +Camp that can be placed on any overmap terrain starting `farmland_` or `hayfield_` or that match `"farmland"` or `"hayfield"` exactly + +```json + { + "id": "faction_base_farmer_0", + "description": "Farmer Camp", + "om_terrains": [ + { "om_terrain": "farmland", "om_terrain_match_type": "PREFIX" }, + { "om_terrain": "hayfield", "om_terrain_match_type": "PREFIX" } + ] + }, +``` -All recipes that start an upgrade path or expansion should have a blueprint requirement that can never be met, such as `"not_an_upgrade"`, to prevent them from showing up as available upgrades. Additionally, if you want to add an expansion, you must create an OMT with the same `id` as the expansion's `id`. +Camp that can only be placed on `"omt_that_varies_in_shape"` if the parameter `"shape"` is set to either `"circle"` or `"circle_alt"` (but not say `"rectangle"` that would want different map updates) -If the player attempts to start a basecamp on an overmap terrain that has two or more valid basecamp expansion paths, she will allowed to choose which path to start. +```json + { + "id": "faction_base_circle_0", + "description": "Farmer Camp", + "om_terrains": [ + { "om_terrain": "omt_that_varies_in_shape", "parameters": { "shape": [ "circle", "circle_alt" ] } + ] + }, +``` ## Sample basecamp upgrade path diff --git a/doc/JSON_INFO.md b/doc/JSON_INFO.md index 2474b3c2f6a18..298a8177be462 100644 --- a/doc/JSON_INFO.md +++ b/doc/JSON_INFO.md @@ -5846,10 +5846,11 @@ String here contains the id of an overmap terrain type (see overmap_terrain.json If it is an object - it has following attributes: - Identifier | Description ---- | --- -`om_terrain` | ID of overmap terrain which will be selected as the target. Mandatory. -`om_terrain_match_type`| Matching rule to use with `om_terrain`. Defaults to TYPE. Details are below. + Identifier | Description | +---------------------- | --------------------------------------------------------------------------------------- | +`om_terrain` | ID of overmap terrain which will be selected as the target. Mandatory. | +`om_terrain_match_type`| Optional. Matching rule to use with `om_terrain`. Defaults to TYPE. Details are below. | +`parameters` | Optional. Parameter key/value pairs to set. Details are below. | `om_terrain_match_type` defaults to TYPE if unspecified, and has the following possible values: @@ -5874,6 +5875,31 @@ If it is an object - it has following attributes: id, but may occur at the beginning, end, or middle and does not have any rules about underscore delimiting. +`parameters` is an object containing one or more keys to set to a specific value provided, say to pick a safe variant of a map. +The keys and values must be valid for the overmap terrains in question. +This can also be used with 0 weight values to provide unique starting map variations that don't spawn normally. + +### Examples + +Any overmap terrain that is either `"shelter"` or begins with `shelter_` + +```json +{ + "om_terrain": "shelter", + "om_terrain_match_type": "PREFIX" +} +``` + +Any overmap terrain that is either `"mansion"` or begins with `mansion_`, and forces the parameter `mansion_variant` to be set to `haunted_scenario_only` + +```json +{ + "om_terrain": "mansion", + "om_terrain_match_type": "PREFIX", + "parameters": { "mansion_variant": "haunted_scenario_only" } +} +``` + ## `city_sizes` (array of two integers) diff --git a/src/condition.cpp b/src/condition.cpp index b4358fb4a6d48..25812e6df4b67 100644 --- a/src/condition.cpp +++ b/src/condition.cpp @@ -974,7 +974,8 @@ conditional_t::func f_at_om_location( const JsonObject &jo, std::string_view mem // TODO: legacy check to be removed once primitive field camp OMTs have been purged return omt_str.find( "faction_base_camp" ) != std::string::npos; } else if( location_value == "FACTION_CAMP_START" ) { - return !recipe_group::get_recipes_by_id( "all_faction_base_types", omt_str ).empty(); + const std::optional *maybe_args = overmap_buffer.mapgen_args( omt_pos ); + return !recipe_group::get_recipes_by_id( "all_faction_base_types", omt_ter, maybe_args ).empty(); } else { return oter_no_dir( omt_ter ) == location_value; } @@ -991,6 +992,7 @@ conditional_t::func f_near_om_location( const JsonObject &jo, std::string_view m for( const tripoint_abs_omt &curr_pos : points_in_radius( omt_pos, range.evaluate( d ) ) ) { const oter_id &omt_ter = overmap_buffer.ter( curr_pos ); + const std::optional *maybe_args = overmap_buffer.mapgen_args( omt_pos ); const std::string &omt_str = omt_ter.id().str(); std::string location_value = location.evaluate( d ); @@ -1004,7 +1006,7 @@ conditional_t::func f_near_om_location( const JsonObject &jo, std::string_view m return true; } } else if( location_value == "FACTION_CAMP_START" && - !recipe_group::get_recipes_by_id( "all_faction_base_types", omt_str ).empty() ) { + !recipe_group::get_recipes_by_id( "all_faction_base_types", omt_ter, maybe_args ).empty() ) { return true; } else { if( oter_no_dir( omt_ter ) == location_value ) { diff --git a/src/faction_camp.cpp b/src/faction_camp.cpp index 3df5de1ae8c79..9fd4639c232d5 100644 --- a/src/faction_camp.cpp +++ b/src/faction_camp.cpp @@ -704,9 +704,9 @@ void talk_function::start_camp( npc &p ) { const tripoint_abs_omt omt_pos = p.global_omt_location(); const oter_id &omt_ref = overmap_buffer.ter( omt_pos ); - - const auto &pos_camps = recipe_group::get_recipes_by_id( "all_faction_base_types", - omt_ref.id().c_str() ); + const std::optional *maybe_args = overmap_buffer.mapgen_args( omt_pos ); + const auto &pos_camps = recipe_group::get_recipes_by_id( "all_faction_base_types", omt_ref, + maybe_args ); if( pos_camps.empty() ) { popup( _( "You cannot build a camp here." ) ); return; @@ -1450,8 +1450,10 @@ void basecamp::get_available_missions( mission_data &mission_key, map &here ) for( const auto &dir : base_camps::all_directions ) { if( dir.first != base_camps::base_dir && expansions.find( dir.first ) == expansions.end() ) { const oter_id &omt_ref = overmap_buffer.ter( omt_pos + dir.first ); + const std::optional *maybe_args = overmap_buffer.mapgen_args( + omt_pos + dir.first ); const auto &pos_expansions = recipe_group::get_recipes_by_id( "all_faction_base_expansions", - omt_ref.id().c_str() ); + omt_ref, maybe_args ); if( !pos_expansions.empty() ) { possible_expansion_found = true; break; @@ -4533,8 +4535,9 @@ bool basecamp::survey_return( const mission_id &miss_id ) } const oter_id &omt_ref = overmap_buffer.ter( where ); + const std::optional *maybe_args = overmap_buffer.mapgen_args( where ); const auto &pos_expansions = recipe_group::get_recipes_by_id( "all_faction_base_expansions", - omt_ref.id().c_str() ); + omt_ref, maybe_args ); if( pos_expansions.empty() ) { popup( _( "You can't build any expansions in a %s." ), omt_ref.id().c_str() ); if( query_yn( diff --git a/src/npctalk.cpp b/src/npctalk.cpp index d7ca41245c45b..042e379fb582d 100644 --- a/src/npctalk.cpp +++ b/src/npctalk.cpp @@ -6878,7 +6878,7 @@ dynamic_line_t::dynamic_line_t( const JsonObject &jo ) }; } else if( jo.get_bool( "list_faction_camp_sites", false ) ) { function = [&]( const dialogue & ) { - const auto &sites = recipe_group::get_recipes_by_id( "all_faction_base_types", "ANY" ); + const auto &sites = recipe_group::get_recipes_by_id( "all_faction_base_types" ); if( sites.empty() ) { return std::string( _( "I can't think of a single place I can build a camp." ) ); } diff --git a/src/recipe_groups.cpp b/src/recipe_groups.cpp index 6e64715de54ab..8789bfbf88eee 100644 --- a/src/recipe_groups.cpp +++ b/src/recipe_groups.cpp @@ -21,12 +21,18 @@ struct recipe_group_data; using group_id = string_id; +struct omt_types_parameters { + std::string omt; + ot_match_type omt_type; + std::unordered_map> parameters; +}; + struct recipe_group_data { group_id id; std::vector> src; std::string building_type = "NONE"; std::map recipes; - std::map> om_terrains; + std::map> om_terrains; bool was_loaded = false; void load( const JsonObject &jo, std::string_view src ); @@ -46,9 +52,23 @@ void recipe_group_data::load( const JsonObject &jo, const std::string_view ) translation desc; ordering.read( "description", desc ); recipes.emplace( name_id, desc ); - om_terrains[name_id] = std::set(); - for( const std::string ter_type : ordering.get_array( "om_terrains" ) ) { - om_terrains[name_id].insert( ter_type ); + for( const JsonValue jv : ordering.get_array( "om_terrains" ) ) { + std::string ter; + ot_match_type ter_match_type = ot_match_type::type; + std::unordered_map> parameter_map; + if( jv.test_string() ) { + ter = jv.get_string(); + } else { + JsonObject jo = jv.get_object(); + ter = jo.get_string( "om_terrain" ); + if( jo.has_string( "om_terrain_match_type" ) ) { + ter_match_type = jo.get_enum_value( "om_terrain_match_type", ter_match_type ); + } + if( jo.has_object( "parameters" ) ) { + jo.read( "parameters", parameter_map ); + } + } + om_terrains[name_id].emplace_back( omt_types_parameters{ ter, ter_match_type, parameter_map } ); } } } @@ -81,30 +101,68 @@ std::map recipe_group::get_recipes_by_bldg( const std::s } } -std::map recipe_group::get_recipes_by_id( const std::string &id, - const std::string &om_terrain_id ) +std::map recipe_group::get_recipes_by_id( const std::string &id ) { std::map all_rec; if( !recipe_groups_data.is_valid( group_id( id ) ) ) { return all_rec; } const recipe_group_data &group = recipe_groups_data.obj( group_id( id ) ); - if( om_terrain_id != "ANY" ) { - std::string base_om_ter_id{ oter_no_dir( oter_id( om_terrain_id ) ) }; + return group.recipes; +} - for( const auto &recp : group.recipes ) { - const auto &recp_terrain = group.om_terrains.find( recp.first ); - if( recp_terrain == group.om_terrains.end() ) { +std::map recipe_group::get_recipes_by_id( const std::string &id, + const oter_id &omt_ter, const std::optional *maybe_args ) +{ + std::map all_rec; + if( !recipe_groups_data.is_valid( group_id( id ) ) ) { + return all_rec; + } + const recipe_group_data &group = recipe_groups_data.obj( group_id( id ) ); + for( const auto &recp : group.recipes ) { + const auto &recp_terrain_it = group.om_terrains.find( recp.first ); + if( recp_terrain_it == group.om_terrains.end() ) { + debugmsg( "Recipe %s doesn't specify 'om_terrains', use ANY instead if intended to work anywhere", + recp.second ); + continue; + } + for( const omt_types_parameters &om_terrain : recp_terrain_it->second ) { + if( om_terrain.omt == "ANY" ) { + all_rec.emplace( recp ); + break; + } + if( !is_ot_match( om_terrain.omt, omt_ter, om_terrain.omt_type ) ) { continue; } - if( recp_terrain->second.find( base_om_ter_id ) != recp_terrain->second.end() || - recp_terrain->second.find( "ANY" ) != recp_terrain->second.end() ) { + if( om_terrain.parameters.empty() ) { all_rec.emplace( recp ); + break; + } + if( !!maybe_args ) { + bool parameters_matched = true; + for( const auto &key_value_set_pair : om_terrain.parameters ) { + auto map_key_it = maybe_args->value().map.find( key_value_set_pair.first ); + if( map_key_it == maybe_args->value().map.end() ) { + debugmsg( "Parameter key %s in recipe %s not found for omt %s", key_value_set_pair.first, id, + omt_ter.id().str() ); + continue; + } + if( key_value_set_pair.second.find( map_key_it->second.get_string() ) == + key_value_set_pair.second.end() ) { + parameters_matched = false; + break; + } + } + if( parameters_matched ) { + all_rec.emplace( recp ); + break; + } + } else { + debugmsg( "Parameter(s) expected for recipe %s but none found for omt %s", id, omt_ter.id().str() ); } } - return all_rec; } - return group.recipes; + return all_rec; } std::string recipe_group::get_building_of_recipe( const std::string &recipe ) diff --git a/src/recipe_groups.h b/src/recipe_groups.h index 4421de7f1ac0d..663b17c8c2dbb 100644 --- a/src/recipe_groups.h +++ b/src/recipe_groups.h @@ -6,6 +6,7 @@ #include #include "type_id.h" +#include "mapgendata.h" class JsonObject; class translation; @@ -18,8 +19,9 @@ void check(); void reset(); std::map get_recipes_by_bldg( const std::string &bldg ); -std::map get_recipes_by_id( const std::string &id, - const std::string &om_terrain_id = "ANY" ); +std::map get_recipes_by_id( const std::string &id ); +std::map get_recipes_by_id( const std::string &id, const oter_id &omt_ter, + const std::optional *maybe_args ); std::string get_building_of_recipe( const std::string &recipe ); } // namespace recipe_group