|
@@ -493,6 +493,113 @@ std::string CraftDefinitionShapeless::getName() const
|
|
|
return "shapeless";
|
|
|
}
|
|
|
|
|
|
+constexpr u16 SHAPELESS_GROUPS_MAX = 30000;
|
|
|
+
|
|
|
+// Checks if there's a matching that matches all nodes in a given bipartite graph.
|
|
|
+// bip_graph has graph_size nodes on each side. It is stored as list of lists of
|
|
|
+// neighbors from one side.
|
|
|
+// See https://en.wikipedia.org/w/index.php?title=Hopcroft-Karp_algorithm for
|
|
|
+// details.
|
|
|
+static bool hopcroft_karp_can_match_all(const std::vector<std::vector<u16>> &bip_graph)
|
|
|
+{
|
|
|
+ assert(bip_graph.size() <= SHAPELESS_GROUPS_MAX);
|
|
|
+ u16 graph_size = bip_graph.size();
|
|
|
+ const u16 nil = graph_size; // nil / dummy index
|
|
|
+ constexpr u16 inf = UINT16_MAX; // bigger than any path length (> SHAPELESS_GROUPS_MAX * 2)
|
|
|
+
|
|
|
+ auto pair_u = std::make_unique<u16[]>(graph_size + 1); // for each u (or nil) the matched v (or nil)
|
|
|
+ auto pair_v = std::make_unique<u16[]>(graph_size + 1); // for each v (or nil) the matched u (or nil)
|
|
|
+ auto dist = std::make_unique<u16[]>(graph_size + 1); // for each u (or nil) the bfs distance
|
|
|
+ u16 num_matched;
|
|
|
+ std::queue<u16> queue{};
|
|
|
+
|
|
|
+ // calculates distances from unmatched nodes for augmentation paths until
|
|
|
+ // dummy is reached
|
|
|
+ // returns false if dummy can't be reached (and hence there are no further
|
|
|
+ // augmentation paths)
|
|
|
+ auto do_bfs = [&]() {
|
|
|
+ assert(queue.empty());
|
|
|
+
|
|
|
+ // enqueue all unmatched, give inf dist to the rest
|
|
|
+ for (u16 u = 0; u < graph_size; ++u) {
|
|
|
+ if (pair_u[u] == nil) {
|
|
|
+ dist[u] = 0;
|
|
|
+ queue.push(u);
|
|
|
+ } else {
|
|
|
+ dist[u] = inf;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ dist[nil] = inf;
|
|
|
+
|
|
|
+ while (!queue.empty()) {
|
|
|
+ u16 u = queue.front();
|
|
|
+ queue.pop();
|
|
|
+
|
|
|
+ if (dist[u] < dist[nil]) { // if dummy not yet reached
|
|
|
+ for (u16 v : bip_graph[u]) { // for all adjanced of u
|
|
|
+ u16 u_back = pair_v[v];
|
|
|
+ // if u_back unvisited, go there
|
|
|
+ if (dist[u_back] == inf) {
|
|
|
+ dist[u_back] = dist[u] + 1;
|
|
|
+ queue.push(u_back);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return dist[nil] != inf;
|
|
|
+ };
|
|
|
+
|
|
|
+ // tries to find an augmenting path from u to the dummy
|
|
|
+ // if successful, swaps all edges along path and returns true
|
|
|
+ // otherwise returns false
|
|
|
+ auto do_dfs_raw = [&](u16 u, auto &&recurse) -> bool {
|
|
|
+ if (u == nil) // dummy => dest reached
|
|
|
+ return true;
|
|
|
+
|
|
|
+ for (u16 v : bip_graph[u]) { // for all adjanced of u
|
|
|
+ u16 u_back = pair_v[v];
|
|
|
+ // only walk according to bfs dists
|
|
|
+ if (dist[u_back] != dist[u] + 1)
|
|
|
+ continue;
|
|
|
+
|
|
|
+ // if walk along u_back reached dummy, swap edges and backtrack
|
|
|
+ if (recurse(u_back, recurse)) {
|
|
|
+ pair_v[v] = u;
|
|
|
+ pair_u[u] = v;
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // unsuccessful path, don't walk here again
|
|
|
+ dist[u] = inf;
|
|
|
+ return false;
|
|
|
+ };
|
|
|
+
|
|
|
+ auto do_dfs = [&](u16 u) {
|
|
|
+ return do_dfs_raw(u, do_dfs_raw);
|
|
|
+ };
|
|
|
+
|
|
|
+ // everyone starts as matched to dummy
|
|
|
+ std::fill_n(&pair_u[0], graph_size + 1, nil);
|
|
|
+ std::fill_n(&pair_v[0], graph_size + 1, nil);
|
|
|
+
|
|
|
+ num_matched = 0;
|
|
|
+
|
|
|
+ while (do_bfs()) {
|
|
|
+ // try to match unmatched u nodes
|
|
|
+ for (u16 u = 0; u < graph_size; ++u) {
|
|
|
+ if (pair_u[u] == nil) {
|
|
|
+ if (do_dfs(u)) {
|
|
|
+ num_matched += 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return num_matched == graph_size;
|
|
|
+}
|
|
|
+
|
|
|
bool CraftDefinitionShapeless::check(const CraftInput &input, IGameDef *gamedef) const
|
|
|
{
|
|
|
if (input.method != CRAFT_METHOD_NORMAL)
|
|
@@ -513,35 +620,61 @@ bool CraftDefinitionShapeless::check(const CraftInput &input, IGameDef *gamedef)
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
+ // Sort input and recipe
|
|
|
+ std::sort(input_filtered.begin(), input_filtered.end());
|
|
|
+
|
|
|
std::vector<std::string> recipe_copy;
|
|
|
- if (hash_inited)
|
|
|
+ if (hash_inited) {
|
|
|
recipe_copy = recipe_names;
|
|
|
- else {
|
|
|
+ } else {
|
|
|
recipe_copy = craftGetItemNames(recipe, gamedef);
|
|
|
std::sort(recipe_copy.begin(), recipe_copy.end());
|
|
|
}
|
|
|
|
|
|
- // Try with all permutations of the recipe,
|
|
|
- // start from the lexicographically first permutation (=sorted),
|
|
|
- // recipe_names is pre-sorted
|
|
|
- do {
|
|
|
- // If all items match, the recipe matches
|
|
|
- bool all_match = true;
|
|
|
- //dstream<<"Testing recipe (output="<<output<<"):";
|
|
|
- for (size_t i=0; i<recipe.size(); i++) {
|
|
|
- //dstream<<" ("<<input_filtered[i]<<" == "<<recipe_copy[i]<<")";
|
|
|
- if (!inputItemMatchesRecipe(input_filtered[i], recipe_copy[i],
|
|
|
- gamedef->idef())) {
|
|
|
- all_match = false;
|
|
|
- break;
|
|
|
- }
|
|
|
+ // Split recipe in group and non-group
|
|
|
+ std::vector<std::string> recipe_nogroup;
|
|
|
+ std::vector<std::string> recipe_onlygroup;
|
|
|
+ std::partition_copy(recipe_copy.begin(), recipe_copy.end(),
|
|
|
+ std::back_inserter(recipe_onlygroup),
|
|
|
+ std::back_inserter(recipe_nogroup),
|
|
|
+ [](const std::string &name) { return str_starts_with(name, "group:"); });
|
|
|
+
|
|
|
+ // Filter out non-group recipe slots, using sorted merge.
|
|
|
+ // (This prefiltering is only a performance optimization and not strictly
|
|
|
+ // necessary.)
|
|
|
+ std::vector<std::string> input_for_group;
|
|
|
+ std::set_difference(input_filtered.begin(), input_filtered.end(),
|
|
|
+ recipe_nogroup.begin(), recipe_nogroup.end(),
|
|
|
+ std::back_inserter(input_for_group));
|
|
|
+
|
|
|
+ // All non-group slots must be satisfied
|
|
|
+ if (input_filtered.size() - input_for_group.size() != recipe_nogroup.size())
|
|
|
+ return false;
|
|
|
+
|
|
|
+ // Find out which recipe slots each input item satisfies. This creates a
|
|
|
+ // bipartite graph
|
|
|
+ assert(recipe_onlygroup.size() == input_for_group.size());
|
|
|
+ if (recipe_onlygroup.size() > SHAPELESS_GROUPS_MAX) {
|
|
|
+ // SHAPELESS_GROUPS_MAX is large enough that this should never happen by
|
|
|
+ // accident
|
|
|
+ errorstream << "Too many groups in shapless craft." << std::endl;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ u16 graph_size = recipe_onlygroup.size();
|
|
|
+ // bip_graph[i] are the group-slots that item i can satisfy
|
|
|
+ std::vector<std::vector<u16>> bip_graph;
|
|
|
+ bip_graph.resize(graph_size);
|
|
|
+ for (u16 i = 0; i < graph_size; ++i) {
|
|
|
+ std::vector<u16> &neighbors_i = bip_graph[i];
|
|
|
+ for (u16 j = 0; j < graph_size; ++j) {
|
|
|
+ if (inputItemMatchesRecipe(input_for_group[i], recipe_onlygroup[j],
|
|
|
+ gamedef->idef()))
|
|
|
+ neighbors_i.push_back(j);
|
|
|
}
|
|
|
- //dstream<<" -> match="<<all_match<<std::endl;
|
|
|
- if (all_match)
|
|
|
- return true;
|
|
|
- } while (std::next_permutation(recipe_copy.begin(), recipe_copy.end()));
|
|
|
+ }
|
|
|
|
|
|
- return false;
|
|
|
+ // Check if the maximum cardinality matching of bip_graph matches all items
|
|
|
+ return hopcroft_karp_can_match_all(bip_graph);
|
|
|
}
|
|
|
|
|
|
CraftOutput CraftDefinitionShapeless::getOutput(const CraftInput &input, IGameDef *gamedef) const
|