From 1dc71b038483557a29f0b97a927c65bcf79ec95e Mon Sep 17 00:00:00 2001 From: Louis Moureaux Date: Mon, 30 Aug 2021 03:07:14 +0200 Subject: [PATCH] Migrate LAYER_TERRAIN* to a class This is a very large one with heavy refactoring. The code was split in smaller functions, boosting readability. Documentation was also improved. No new functionality was added. The unused is_reversed flag was dropped; it was not used in any known tileset and layer_order provides a better alternative. The factorization makes it very straightforward to add more than 3 terrain layers, limited only by the tileset reading code. All shipped tilesets load correctly. See #430. --- client/CMakeLists.txt | 1 + client/layer.h | 9 + client/layer_terrain.cpp | 843 ++++++++++++++++++++++++++++++++++ client/layer_terrain.h | 141 ++++++ client/tilespec.cpp | 953 +++++++-------------------------------- client/tilespec.h | 7 + doc/README.graphics | 9 +- 7 files changed, 1160 insertions(+), 803 deletions(-) create mode 100644 client/layer_terrain.cpp create mode 100644 client/layer_terrain.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 55d282ee74..0013daa4e8 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -60,6 +60,7 @@ add_library( layer_base_flags.cpp layer_darkness.cpp layer_special.cpp + layer_terrain.cpp luaconsole_common.cpp mapctrl_common.cpp mapview_common.cpp diff --git a/client/layer.h b/client/layer.h index 72d03a3cfa..79d3a67ab7 100644 --- a/client/layer.h +++ b/client/layer.h @@ -17,6 +17,7 @@ class QPixmap; struct city; struct player; +struct terrain; struct tileset; struct unit; struct unit_type; @@ -156,6 +157,14 @@ class layer { */ virtual void free_player(int player_id) { Q_UNUSED(player_id); } + /** + * Initializes terrain-specific data. + */ + virtual void initialize_terrain(const terrain *terrain) + { + Q_UNUSED(terrain); + } + mapview_layer type() const { return m_layer; } protected: diff --git a/client/layer_terrain.cpp b/client/layer_terrain.cpp new file mode 100644 index 0000000000..6262a38f2a --- /dev/null +++ b/client/layer_terrain.cpp @@ -0,0 +1,843 @@ +/*__ ___ *************************************** +/ \ / \ Copyright (c) 2021 Freeciv21 contributors. +\_ \ / __/ This file is part of Freeciv21. + _\ \ / /__ Freeciv21 is free software: you can redistribute it + \___ \____/ __/ and/or modify it under the terms of the GNU General + \_ _/ Public License as published by the Free Software + | \ \ \_ Foundation, either version 3 of the License, + | or (at your option) any later version. + _/ /\ You should have received a copy of the GNU + /o) (o/\ \_ General Public License along with Freeciv21. + \_____/ / If not, see https://www.gnu.org/licenses/. + \____/ ********************************************************/ + +#include "layer_terrain.h" + +#include "climap.h" +#include "extras.h" +#include "game.h" // For extra_type_iterate +#include "map.h" +#include "rand.h" +#include "sprite_g.h" +#include "tilespec.h" + +/** + * \class freeciv::layer_terrain + * \brief Draws terrain sprites on the map. + * + * Terrain layers are a core feature of the map. They are used to draw + * sprites representing terrain. Up to three layers can be used. Within each + * layer, one can choose how each terrain will be drawn. + * + * The configuration is based on *tags* that are used when constructing + * sprite identifiers. Tags represent sets of related sprites that, drawn in + * a specific way, construct the visual representation of a tile. The tag + * used to represent a terrain is taken from its `graphic_str` or + * `graphic_alt` properties. + * + * Each tag has a set of properties that influence how sprites are drawn. The + * offsets at which the sprites are drawn is set using \ref set_tag_offsets. + * The tiles can be drawn in two ways: either a single sprite is used for the + * whole tile (\ref CELL_WHOLE), or one sprite is used to draw each corner of + * the tile + * (\ref CELL_CORNER). This is set with \ref set_tag_sprite_type. "Matching" + * with adjacent terrain types provides a powerful mechanism for + * sophisticated effects. For isometric tilesets, it is also possible to use + * a mask to blend adjacent tiles together with \ref enable_blending. + * + * Matching + * -------- + * + * The sprites used to draw a tile can depend on adjacent terrains, allowing + * the representation of continuous coasts, ridges and other edges. + * + * To use tag matching, one first defines a number of terrain groups ("match + * type" in `spec` files). Groups are created with \ref + * create_matching_group. Every tag must be in a group, set with \ref + * set_tag_matching_group. The first letter of group names must be unique + * within a layer. Each tag can then be matched against any number of groups. + * There will be one sprite for each combination of groups next to the tile + * of interest. + * + * The simplest matching is no matching, in which case the sprite used + * doesn't depend on adjacent terrain. This is available for both \ref + * CELL_WHOLE and \ref CELL_CORNER, although there is little use for the + * second. The sprite names for \ref CELL_WHOLE are formed like + * `t.l0.grassland1`, where 0 is the layer number, `grassland` appears in the + * name of the `tilespec` section, and 1 is an index (when there are several + * sprites with indices 1, 2, 3, ..., one is picked at random). For + * \ref CELL_CORNER, the names are like `t.l0.grassland_cell_u`, where `u` + * ("up") indicates the direction of the corner. It can be `u` ("up"), `d` + * ("down"), `r` ("right"), or `l` ("left"). + * + * When a tag is matched against one group, there are two possibilities: + * + * * The matched group is the same as the tag group. For \ref CELL_WHOLE, + * this requires sprites with names like `t.l0.grassland_n1e0s0w0`, where the + * `n1` indicates that the terrain in the north direction is in the same + * group as the tile that is being drawn, and the 0's indicate that other + * terrains are different. Sprites must be provided for all 16 combinations + * of 0's and 1's. Amplio2 forests and hills are drawn this way. + * + * For \ref CELL_CORNER, this requires 24 sprites with names like + * `t.l0.grassland_cell_u010`. `t.l0.grassland_cell_u` is like in the no + * match case, and `010` indicates which sides of the corner match the + * terrain being drawn. Amplio2 ice uses this method. + * + * * The matched group is different from the tag group (only supported for + * \ref CELL_CORNER). There are again 24 sprites, this time with names + * like `t.l0.grassland_cell_u_a_a_b`. The first letter, in this example `u`, + * is the direction of the corner. The other three indicate which terrains + * are found on the three external sides of the corner. They are the first + * letter of the name of a matching group: the group being matched against if + * the adjacent terrain is of that group, and otherwise the group of the + * sprite being drawn. The coasts of Amplio2 lakes use this method. + * + * When more than one group is used, which is only supported for + * \ref CELL_CORNER, the sprites are named like `t.l0.cellgroup_a_b_c_d`. + * Each sprite represents the junction of four tiles with the group names + * first letters `a`, `b`, `c`, and `d`. Each sprite is split in four to + * provide four corner sprites. Amplio2 coasts are drawn this way. + */ + +namespace { +static const char direction4letters[5] = "udrl"; +} + +namespace freeciv { + +layer_terrain::layer_terrain(struct tileset *ts, int number) + : freeciv::layer(ts, LAYER_TERRAIN1), m_number(number) +{ +} + +/** + * \brief Creates a matching group with the given name. + * \returns Whether the operation succeeded. + */ +bool layer_terrain::create_matching_group(const QString &name) +{ + if (name.isEmpty()) { + return false; + } + + if (m_matching_groups.count(name.at(0)) != 0) { + tileset_error( + LOG_FATAL, + _("[layer%d] match_types: \"%s\" initial ('%c') is not unique."), + m_number, qUtf8Printable(name), qUtf8Printable(name.at(0))); + return false; + } + + m_matching_groups[name.at(0)] = + matching_group{static_cast(m_matching_groups.size()), name}; + + return true; +} + +/** + * \brief Makes a terrain tag available for use by this layer. + * + * This function only makes the tag available. Its properties must be set + * using the `set_tag_*` functions. + * + * \returns True if the tag did not exist. + */ +bool layer_terrain::add_tag(const QString &tag, const QString &sprite_name) +{ + bool ok = true; + if (m_terrain_tag_info.count(tag) > 0) { + qWarning("Multiple tile sections containing terrain tag \"%s\".", + qUtf8Printable(tag)); + ok = false; + } + + m_terrain_tag_info[tag].sprite_name = sprite_name; // Creates the data + return ok; +} + +/** + * \brief Sets the type of sprite used to draw the specified tag. + * + * The tag must have been created using \ref add_tag. + */ +bool layer_terrain::set_tag_sprite_type(const QString &tag, sprite_type type) +{ + // FIXME The ancient code did not handle CELL_CORNER for "tall" terrain or + // sprite offsets. Does the new code work support that? + m_terrain_tag_info.at(tag).type = type; + return true; +} + +/** + * \brief Sets the offsets used to draw the specified tag. + * + * The tag must have been created using \ref add_tag. + */ +bool layer_terrain::set_tag_offsets(const QString &tag, int offset_x, + int offset_y) +{ + m_terrain_tag_info.at(tag).offset_x = offset_x; + m_terrain_tag_info.at(tag).offset_y = offset_y; + return true; +} + +/** + * \brief Sets the matching group for the specified tag. + * + * The tag must have been created using \ref add_tag. + */ +bool layer_terrain::set_tag_matching_group(const QString &tag, + const QString &group_name) +{ + if (auto g = group(group_name); g != nullptr) { + m_terrain_tag_info.at(tag).group = g; + // Tags always match with themselves + m_terrain_tag_info.at(tag).matches_with.push_back(g); + return true; + } else { + return false; + } +} + +/** + * \brief Sets the specified tag to be matched against a group. + * + * The tag must have been created using \ref add_tag. + */ +bool layer_terrain::set_tag_matches_with(const QString &tag, + const QString &group_name) +{ + if (auto g = group(group_name); g != nullptr) { + m_terrain_tag_info.at(tag).matches_with.push_back(g); + return true; + } else { + return false; + } +} + +/** + * \brief Enable blending on this layer for the given terrain tag. + */ +void layer_terrain::enable_blending(const QString &tag) +{ + if (!tileset_is_isometric(tileset())) { + qCritical() << "Blending is only supported for isometric tilesets"; + } + // Create the entry + m_terrain_tag_info.at(tag).blend = true; +} + +/** + * \brief Sets up the structure to draw the specified terrain. + */ +void layer_terrain::initialize_terrain(const terrain *terrain) +{ + // Find the good terrain_info + terrain_info info; + if (m_terrain_tag_info.count(terrain->graphic_str) > 0) { + info = m_terrain_tag_info[terrain->graphic_str]; + } else if (m_terrain_tag_info.count(terrain->graphic_alt) > 0) { + info = m_terrain_tag_info[terrain->graphic_alt]; + } else if (m_number == 0) { + // All terrains should be present in layer 0... + tileset_error(LOG_WARN, + _("Terrain \"%s\": no graphic tile \"%s\" or \"%s\"."), + terrain_rule_name(terrain), terrain->graphic_str, + terrain->graphic_alt); + return; + } else { + // Terrain is not drawn by this layer + return; + } + + // Determine the match style + switch (info.matches_with.size()) { + case 0: + case 1: + info.style = MATCH_NONE; + break; + case 2: + if (info.matches_with.front() == info.matches_with.back()) { + info.style = MATCH_SAME; + } else { + info.style = MATCH_PAIR; + } + break; + default: + info.style = MATCH_FULL; + break; + } + + // Build graphics data + // TODO Use inheritance? + switch (info.type) { + case CELL_WHOLE: + switch (info.style) { + case MATCH_NONE: + initialize_cell_whole_match_none(terrain, info); + break; + case MATCH_SAME: + initialize_cell_whole_match_same(terrain, info); + break; + case MATCH_PAIR: + case MATCH_FULL: + qWarning() << "Not implemented CELL_WHOLE + MATCH_FULL/MATCH_PAIR."; + return; + } + break; + case CELL_CORNER: + switch (info.style) { + case MATCH_NONE: + initialize_cell_corner_match_none(terrain, info); + break; + case MATCH_SAME: + initialize_cell_corner_match_same(terrain, info); + break; + case MATCH_PAIR: + initialize_cell_corner_match_pair(terrain, info); + break; + case MATCH_FULL: + initialize_cell_corner_match_full(terrain, info); + break; + } + break; + } + + // Blending + initialize_blending(terrain, info); + + // Copy it to the terrain index + m_terrain_info[terrain_index(terrain)] = info; +} + +/** + * \brief Sets up terrain information for \ref CELL_WHOLE and `MATCH_SAME`. + */ +void layer_terrain::initialize_cell_whole_match_none(const terrain *terrain, + terrain_info &info) +{ + QString buffer; + + // Load whole sprites for this tile. + for (int i = 0;; i++) { + buffer = QStringLiteral("t.l%1.%2%3") + .arg(m_number) + .arg(info.sprite_name) + .arg(i + 1); + auto sprite = load_sprite(tileset(), buffer, true, false); + if (!sprite) { + break; + } + info.sprites.push_back(sprite); + } + + // check for base sprite, allowing missing sprites above base + if (m_number == 0 && info.sprites.empty()) { + // TRANS: 'base' means 'base of terrain gfx', not 'military base' + tileset_error(LOG_FATAL, _("Missing base sprite for tag \"%s\"."), + qUtf8Printable(buffer)); + } +} + +/** + * \brief Sets up terrain information for \ref CELL_WHOLE and `MATCH_SAME`. + */ +void layer_terrain::initialize_cell_whole_match_same(const terrain *terrain, + terrain_info &info) +{ + // Load 16 cardinally-matched sprites. + for (int i = 0; i < tileset_num_index_cardinals(tileset()); i++) { + auto buffer = QStringLiteral("t.l%1.%2_%3") + .arg(m_number) + .arg(info.sprite_name) + .arg(cardinal_index_str(tileset(), i)); + info.sprites.push_back(tiles_lookup_sprite_tag_alt( + tileset(), LOG_FATAL, qUtf8Printable(buffer), "", "matched terrain", + terrain_rule_name(terrain), true)); + } +} + +/** + * \brief Sets up terrain information for \ref CELL_CORNER and `MATCH_NONE`. + */ +void layer_terrain::initialize_cell_corner_match_none(const terrain *terrain, + terrain_info &info) +{ + // Determine how many sprites we need + int number = NUM_CORNER_DIRS; + + // Load the sprites + for (int i = 0; i < number; i++) { + enum direction4 dir = static_cast(i % NUM_CORNER_DIRS); + auto buffer = QStringLiteral("t.l%1.%2_cell_%3") + .arg(m_number) + .arg(info.sprite_name) + .arg(direction4letters[dir]); + info.sprites.push_back(tiles_lookup_sprite_tag_alt( + tileset(), LOG_FATAL, qUtf8Printable(buffer), "", "cell terrain", + terrain_rule_name(terrain), true)); + } +} + +/** + * \brief Sets up terrain information for \ref CELL_CORNER and `MATCH_SAME`. + */ +void layer_terrain::initialize_cell_corner_match_same(const terrain *terrain, + terrain_info &info) +{ + // Determine how many sprites we need + int number = NUM_CORNER_DIRS * 2 * 2 * 2; + + // Load the sprites + for (int i = 0; i < number; i++) { + enum direction4 dir = static_cast(i % NUM_CORNER_DIRS); + int value = i / NUM_CORNER_DIRS; + + auto buffer = QStringLiteral("t.l%1.%2_cell_%3%4%5%6") + .arg(m_number) + .arg(info.sprite_name) + .arg(direction4letters[dir]) + .arg(value & 1) + .arg((value >> 1) & 1) + .arg((value >> 2) & 1); + info.sprites.push_back(tiles_lookup_sprite_tag_alt( + tileset(), LOG_FATAL, qUtf8Printable(buffer), "", + "same cell terrain", terrain_rule_name(terrain), true)); + } +} + +/** + * \brief Sets up terrain information for \ref CELL_CORNER and `MATCH_PAIR`. + */ +void layer_terrain::initialize_cell_corner_match_pair(const terrain *terrain, + terrain_info &info) +{ + // Determine how many sprites we need + int number = NUM_CORNER_DIRS * 2 * 2 * 2; + + // Load the sprites + for (int i = 0; i < number; i++) { + enum direction4 dir = static_cast(i % NUM_CORNER_DIRS); + int value = i / NUM_CORNER_DIRS; + + QChar letters[2] = {info.group->name[0], + info.matches_with.front()->name[0]}; + auto buffer = QStringLiteral("t.l%1.%2_cell_%3_%4_%5_%6") + .arg(m_number) + .arg(info.sprite_name, QChar(direction4letters[dir]), + letters[value & 1], letters[(value >> 1) & 1], + letters[(value >> 2) & 1]); + + info.sprites.push_back(tiles_lookup_sprite_tag_alt( + tileset(), LOG_FATAL, qUtf8Printable(buffer), "", + "cell pair terrain", terrain_rule_name(terrain), true)); + } +} + +/** + * \brief Sets up terrain information for \ref CELL_CORNER and `MATCH_FULL`. + */ +void layer_terrain::initialize_cell_corner_match_full(const terrain *terrain, + terrain_info &info) +{ + // Determine how many sprites we need + // N directions (NSEW) * 3 dimensions of matching + // could use exp() or expi() here? + const int count = info.matches_with.size(); + const int number = NUM_CORNER_DIRS * count * count * count; + + // Load the sprites + for (int i = 0; i < number; i++) { + enum direction4 dir = static_cast(i % NUM_CORNER_DIRS); + int value = i / NUM_CORNER_DIRS; + + const auto g1 = info.matches_with[value % count]; + value /= count; + const auto g2 = info.matches_with[value % count]; + value /= count; + const auto g3 = info.matches_with[value % count]; + + const matching_group *n, *s, *e, *w; + // Assume merged cells. This should be a separate option. + switch (dir) { + case DIR4_NORTH: + s = info.group; + w = g1; + n = g2; + e = g3; + break; + case DIR4_EAST: + w = info.group; + n = g1; + e = g2; + s = g3; + break; + case DIR4_SOUTH: + n = info.group; + e = g1; + s = g2; + w = g3; + break; + case DIR4_WEST: + default: // avoid warnings + e = info.group; + s = g1; + w = g2; + n = g3; + break; + }; + + // Use first character of match_types, already checked for uniqueness. + auto buffer = QStringLiteral("t.l%1.cellgroup_%2_%3_%4_%5") + .arg(QString::number(m_number), n->name[0], e->name[0], + s->name[0], w->name[0]); + auto sprite = load_sprite(tileset(), buffer, true, false); + + if (sprite) { + // Crop the sprite to separate this cell. + const int W = sprite->width(); + const int H = sprite->height(); + int x[4] = {W / 4, W / 4, 0, W / 2}; + int y[4] = {H / 2, 0, H / 4, H / 4}; + int xo[4] = {0, 0, -W / 2, W / 2}; + int yo[4] = {H / 2, -H / 2, 0, 0}; + + sprite = crop_sprite(sprite, x[dir], y[dir], W / 2, H / 2, + get_mask_sprite(tileset()), xo[dir], yo[dir], + 1.0f, false); + // We allocated new sprite with crop_sprite. Store its address so we + // can free it. + m_allocated.push_back(sprite); + } else { + qCritical("Terrain graphics sprite for tag \"%s\" missing.", + qUtf8Printable(buffer)); + } + + info.sprites.push_back(sprite); + } +} + +/** + * \brief Initializes blending sprites. + */ +void layer_terrain::initialize_blending(const terrain *terrain, + terrain_info &info) +{ + // Get the blending info + if (!info.blend) { + // No blending + return; + } + + // try an optional special name + auto buffer = QStringLiteral("t.blend.%1").arg(info.sprite_name); + auto blender = tiles_lookup_sprite_tag_alt( + tileset(), LOG_VERBOSE, qUtf8Printable(buffer), "", "blend terrain", + terrain_rule_name(terrain), true); + + if (blender == nullptr) { + // try an unloaded base name + // Need to pass "1" as an argument because %21 is interpreted as argument + // 21 + buffer = QStringLiteral("t.l%1.%2%3") + .arg(m_number) + .arg(info.sprite_name) + .arg(1); + blender = tiles_lookup_sprite_tag_alt( + tileset(), LOG_ERROR, qUtf8Printable(buffer), "", + "base (blend) terrain", terrain_rule_name(terrain), true); + } + + if (blender == nullptr) { + qCritical( + "Cannot find sprite for blending terrain with tag %s on layer %d", + qUtf8Printable(info.sprite_name), m_number); + return; + } + + // Set up blending sprites. This only works in iso-view! + const int W = tileset_tile_width(tileset()); + const int H = tileset_tile_height(tileset()); + const int offsets[4][2] = {{W / 2, 0}, {0, H / 2}, {W / 2, H / 2}, {0, 0}}; + int dir = 0; + + for (; dir < 4; dir++) { + info.blend_sprites[dir] = + crop_sprite(blender, offsets[dir][0], offsets[dir][1], W / 2, H / 2, + get_dither_sprite(tileset()), 0, 0, 1.0f, false); + } +} + +/** + * \implements layer::fill_sprite_array + */ +std::vector layer_terrain::fill_sprite_array( + const tile *ptile, const tile_edge *pedge, const tile_corner *pcorner, + const unit *punit, const city *pcity, const unit_type *putype) const +{ + if (ptile == nullptr) { + return {}; + } + + const auto terrain = ptile->terrain; + if (terrain == nullptr) { + return {}; + } + + // Don't draw terrain when the solid background is used + if (solid_background(ptile, punit, pcity)) { + return {}; + } + + auto sprites = std::vector(); + + // Handle scenario-defined sprites: scenarios can instruct the client to + // draw a specific sprite at some location. + // FIXME: this should avoid calling load_sprite since it's slow and + // increases the refcount without limit. + if (QPixmap * sprite; + ptile->spec_sprite + && (sprite = + load_sprite(tileset(), ptile->spec_sprite, true, false))) { + if (m_number == 0) { + sprites.emplace_back(tileset(), sprite); + } + // Skip the normal drawing process. + return sprites; + } + + struct terrain *terrain_near[8] = {nullptr}; + bv_extras extras_near[8]; // dummy + build_tile_data(ptile, terrain, terrain_near, extras_near); + + fill_terrain_sprite_array(sprites, ptile, terrain, terrain_near); + fill_blending_sprite_array(sprites, ptile, terrain, terrain_near); + + return sprites; +} + +/** + * Retrieves the group structure of the provided name. + * \param name The name to look for. + * \returns The terrain group structure, or `nullptr` on failure. + */ +layer_terrain::matching_group *layer_terrain::group(const QString &name) +{ + if (name.isEmpty() || m_matching_groups.count(name[0]) == 0) { + qCritical() << "No matching group called" << name; + return nullptr; + } + + auto candidate = &m_matching_groups.at(name[0]); + if (candidate->name == name) { + return candidate; + } else { + // Should not happen + return nullptr; + } +} + +/** + * Retrieves the group number for a given terrain. + * \param pterrain The terrain to look for. Can be null. + * \returns The terrain group number, or -1 if not drawn in this layer. + */ +int layer_terrain::terrain_group(const terrain *pterrain) const +{ + if (!pterrain || m_terrain_info.count(terrain_index(pterrain)) == 0) { + return -1; + } + + return m_terrain_info.at(terrain_index(pterrain)).group->number; +} + +/** + * Helper function for fill_sprite_array. + */ +void layer_terrain::fill_terrain_sprite_array( + std::vector &sprs, const tile *ptile, + const terrain *pterrain, terrain **tterrain_near) const +{ + if (m_terrain_info.find(terrain_index(pterrain)) == m_terrain_info.end()) { + // Not drawn in this layer + return; + } + + const auto info = m_terrain_info.at(terrain_index(pterrain)); + +#define MATCH(dir) terrain_group(tterrain_near[(dir)]) + + switch (info.type) { + case CELL_WHOLE: { + switch (info.style) { + case MATCH_NONE: { + if (!info.sprites.empty()) { + /* Pseudo-random reproducable algorithm to pick a sprite. Use + * modulo to limit the number to a handleable size [0..32000). */ + const int i = + fc_randomly(tile_index(ptile) % 32000, info.sprites.size()); + sprs.emplace_back(tileset(), info.sprites[i], true, info.offset_x, + info.offset_y); + } + break; + } + case MATCH_SAME: { + fc_assert_ret(info.matches_with.size() == 2); + fc_assert_ret(info.sprites.size() + == tileset_num_index_cardinals(tileset())); + int tileno = 0; + + for (int i = 0; i < tileset_num_cardinal_dirs(tileset()); i++) { + enum direction8 dir = tileset_cardinal_dirs(tileset())[i]; + + if (MATCH(dir) == info.matches_with.back()->number) { + tileno |= 1 << i; + } + } + sprs.emplace_back(tileset(), info.sprites[tileno], true, info.offset_x, + info.offset_y); + break; + } + case MATCH_PAIR: + case MATCH_FULL: + fc_assert(false); // not yet defined + break; + }; + break; + } + case CELL_CORNER: { + /* Divide the tile up into four rectangular cells. Each of these + * cells covers one corner, and each is adjacent to 3 different + * tiles. For each cell we pick a sprite based upon the adjacent + * terrains at each of those tiles. Thus, we have 8 different sprites + * for each of the 4 cells (32 sprites total). + * + * These arrays correspond to the direction4 ordering. */ + const int W = tileset_tile_width(tileset()); + const int H = tileset_tile_height(tileset()); + const int iso_offsets[4][2] = { + {W / 4, 0}, {W / 4, H / 2}, {W / 2, H / 4}, {0, H / 4}}; + const int noniso_offsets[4][2] = { + {0, 0}, {W / 2, H / 2}, {W / 2, 0}, {0, H / 2}}; + + // put corner cells + for (int i = 0; i < NUM_CORNER_DIRS; i++) { + const int count = info.matches_with.size(); + enum direction8 dir = dir_ccw(DIR4_TO_DIR8[i]); + int x = (tileset_is_isometric(tileset()) ? iso_offsets[i][0] + : noniso_offsets[i][0]); + int y = (tileset_is_isometric(tileset()) ? iso_offsets[i][1] + : noniso_offsets[i][1]); + int m[3] = {MATCH(dir_ccw(dir)), MATCH(dir), MATCH(dir_cw(dir))}; + + int array_index = 0; + switch (info.style) { + case MATCH_NONE: + // We have no need for matching, just plug the piece in place. + break; + case MATCH_SAME: + fc_assert_ret(info.matches_with.size() == 2); + array_index = array_index * 2 + (m[2] != info.group->number); + array_index = array_index * 2 + (m[1] != info.group->number); + array_index = array_index * 2 + (m[0] != info.group->number); + break; + case MATCH_PAIR: { + fc_assert_ret(info.matches_with.size() == 2); + const auto that = info.matches_with.back()->number; + array_index = array_index * 2 + (m[2] == that); + array_index = array_index * 2 + (m[1] == that); + array_index = array_index * 2 + (m[0] == that); + } break; + case MATCH_FULL: + default: { + int n[3]; + for (int j = 0; j < 3; j++) { + for (int k = 0; k < count; k++) { + n[j] = k; // default to last entry + if (m[j] == info.matches_with[k]->number) { + break; + } + } + } + array_index = array_index * count + n[2]; + array_index = array_index * count + n[1]; + array_index = array_index * count + n[0]; + } break; + }; + array_index = array_index * NUM_CORNER_DIRS + i; + + const auto sprite = info.sprites[array_index]; + if (sprite) { + sprs.emplace_back(tileset(), sprite, true, x, y); + } + } + break; + } + }; +#undef MATCH +} + +/** + * Helper function for fill_sprite_array. + * Fill in the sprite array for blended terrain. + * + * This function assumes that m_blending contains a value for pterrain. + */ +void layer_terrain::fill_blending_sprite_array( + std::vector &sprs, const tile *ptile, + const terrain *pterrain, terrain **tterrain_near) const +{ + // By how much the blending sprites need to be offset + const int W = tileset_tile_width(tileset()); + const int H = tileset_tile_height(tileset()); + const int offsets[4][2] = {{W / 2, 0}, {0, H / 2}, {W / 2, H / 2}, {0, 0}}; + + // Not drawn + if (m_terrain_info.count(terrain_index(pterrain)) == 0) { + return; + } + + // Not blended + auto info = m_terrain_info.at(terrain_index(pterrain)); + if (!info.blend) { + return; + } + + for (int dir = 0; dir < 4; dir++) { + struct tile *neighbor = mapstep(&(wld.map), ptile, DIR4_TO_DIR8[dir]); + struct terrain *other; + + // No other tile, don't "blend" at the edge of the map + if (!neighbor) { + continue; + } + + // Other tile is unknown + if (client_tile_get_known(neighbor) == TILE_UNKNOWN) { + continue; + } + + // No blending between identical terrains + if (pterrain == (other = tterrain_near[DIR4_TO_DIR8[dir]])) { + continue; + } + + // Other terrain is not drawn + if (m_terrain_info.count(terrain_index(other)) == 0) { + continue; + } + + // Other terrain is not blended + auto other_info = m_terrain_info.at(terrain_index(other)); + if (!other_info.blend) { + continue; + } + + // Pick the blending sprite and add it + sprs.emplace_back(tileset(), other_info.blend_sprites.at(dir), true, + offsets[dir][0], offsets[dir][1]); + } +} + +} // namespace freeciv diff --git a/client/layer_terrain.h b/client/layer_terrain.h new file mode 100644 index 0000000000..ddcf5adeda --- /dev/null +++ b/client/layer_terrain.h @@ -0,0 +1,141 @@ +/*__ ___ *************************************** +/ \ / \ Copyright (c) 2021 Freeciv21 contributors. +\_ \ / __/ This file is part of Freeciv21. + _\ \ / /__ Freeciv21 is free software: you can redistribute it + \___ \____/ __/ and/or modify it under the terms of the GNU General + \_ _/ Public License as published by the Free Software + | @ @ \_ Foundation, either version 3 of the License, + | or (at your option) any later version. + _/ /\ You should have received a copy of the GNU + /o) (o/\ \_ General Public License along with Freeciv21. + \_____/ / If not, see https://www.gnu.org/licenses/. + \____/ ********************************************************/ +#pragma once + +#include "fc_types.h" +#include "layer.h" +#include "tilespec.h" + +#include + +namespace freeciv { + +class layer_terrain : public layer { +public: + /// Indicates how many sprites are used to draw a tile. + enum sprite_type { + CELL_WHOLE, ///< One sprite for the entire tile. + CELL_CORNER ///< One sprite for each corner of the tile. + }; + +private: + struct matching_group { + int number; + QString name; + }; + + enum match_style { + MATCH_NONE, + MATCH_SAME, // "boolean" match + MATCH_PAIR, + MATCH_FULL + }; + + struct terrain_info { + sprite_type type = CELL_WHOLE; + QString sprite_name; + int offset_x = 0, offset_y = 0; + + match_style style = MATCH_NONE; + matching_group *group = nullptr; + std::vector matches_with; + + std::vector sprites = {}; + + bool blend = false; + std::array blend_sprites = {nullptr}; + }; + +public: + constexpr static auto MAX_NUM_MATCH_WITH = 8; + + explicit layer_terrain(struct tileset *ts, int number); + + bool create_matching_group(const QString &name); + + bool add_tag(const QString &tag, const QString &sprite_name); + bool set_tag_sprite_type(const QString &tag, sprite_type type); + bool set_tag_offsets(const QString &tag, int offset_x, int offset_y); + bool set_tag_matching_group(const QString &tag, const QString &group_name); + bool set_tag_matches_with(const QString &tag, const QString &group_name); + void enable_blending(const QString &tag); + + void initialize_terrain(const terrain *terrain) override; + + std::vector + fill_sprite_array(const tile *ptile, const tile_edge *pedge, + const tile_corner *pcorner, const unit *punit, + const city *pcity, + const unit_type *putype) const override; + +private: + matching_group *group(const QString &name); + + void initialize_cell_whole_match_none(const terrain *terrain, + terrain_info &info); + void initialize_cell_whole_match_same(const terrain *terrain, + terrain_info &info); + void initialize_cell_corner_match_none(const terrain *terrain, + terrain_info &info); + void initialize_cell_corner_match_same(const terrain *terrain, + terrain_info &info); + void initialize_cell_corner_match_pair(const terrain *terrain, + terrain_info &info); + void initialize_cell_corner_match_full(const terrain *terrain, + terrain_info &info); + void initialize_blending(const terrain *terrain, terrain_info &info); + + int terrain_group(const terrain *pterrain) const; + void fill_terrain_sprite_array(std::vector &sprs, + const tile *ptile, const terrain *pterrain, + terrain **tterrain_near) const; + void fill_blending_sprite_array(std::vector &sprs, + const tile *ptile, const terrain *pterrain, + terrain **tterrain_near) const; + + int m_number = 0; + + /* List of those sprites in 'cells' that are allocated by some other + * means than load_sprite() and thus are not freed by unload_all_sprites(). + */ + std::vector m_allocated; + + std::map m_matching_groups; + + /** + * Before terrains are loaded, this contains the list of available terrain + * tags. + */ + std::map m_terrain_tag_info; + + /// Every terrain drawn in this layer appears here + std::map m_terrain_info; + + // CELL_WHOLE + // len(match_with) == 0 or 1 => MATCH_NONE => 1 sprite (or random) 1 + // ex: amplio2 base layer + // len(match_with) == 2, identical => MATCH_SAME => n0e1s0w1 16 + // ex: amplio2 mountains/hills, forest/jungle + // len(match_with) == 2, different => MATCH_PAIR => doesn't exist + // len(match_with) > 2 => MATCH_FULL => doesn't exist + + // CELL_CORNER + // len(match_with) == 0 or 1 => MATCH_NONE => x_cell_[urdl] 4 ex: useless?? + // unused at least len(match_with) == 2, identical => MATCH_SAME => + // x_cell_[urdl][01]{3} 24 = 4*2^3 ex: trident lake l0 len(match_with) == + // 2, different => MATCH_PAIR => x_cell_[urdl]_a_b_c 24 = 4*2^3 ex: + // amplio2 lake l0 len(match_with) > 2 => MATCH_FULL => cellgroup_a_b_c_d + // (4*)n^4 ex: amplio2 coast l0 +}; + +} // namespace freeciv diff --git a/client/tilespec.cpp b/client/tilespec.cpp index a6b9bc9881..6fbe4ab0bb 100644 --- a/client/tilespec.cpp +++ b/client/tilespec.cpp @@ -82,6 +82,7 @@ #include "layer_base_flags.h" #include "layer_darkness.h" #include "layer_special.h" +#include "layer_terrain.h" #include "options.h" // for fill_xxx #include "themes_common.h" @@ -122,60 +123,11 @@ #define FULL_TILE_X_OFFSET ((t->normal_tile_width - t->full_tile_width) / 2) #define FULL_TILE_Y_OFFSET (t->normal_tile_height - t->full_tile_height) -static const char direction4letters[5] = "udrl"; // This must correspond to enum edge_type. static const char edge_name[EDGE_COUNT][3] = {"ns", "we", "ud", "lr"}; -enum match_style { - MATCH_NONE, - MATCH_SAME, // "boolean" match - MATCH_PAIR, - MATCH_FULL -}; - -enum sprite_type { - CELL_WHOLE, // entire tile - CELL_CORNER // corner of tile -}; - -struct drawing_layer { - bool is_tall; - int offset_x, offset_y; - -#define MAX_NUM_MATCH_WITH 8 - enum match_style match_style; - int match_index[1 + MAX_NUM_MATCH_WITH]; - int match_indices; // 0 = no match_type, 1 = no match_with - - enum sprite_type sprite_type; - - struct sprite_vector base; - QPixmap *match[MAX_INDEX_CARDINAL]; - QPixmap **cells; - - /* List of those sprites in 'cells' that are allocated by some other - * means than load_sprite() and thus are not freed by unload_all_sprites(). - */ - struct sprite_vector allocated; -}; - -struct drawing_data { - bool init; - - QString name; - - int num_layers; // 1 thru MAX_NUM_LAYERS. #define MAX_NUM_LAYERS 3 - struct drawing_layer layer[MAX_NUM_LAYERS]; - - bool is_reversed; - - int blending; // layer, 0 = none - QPixmap *blender; - QPixmap *blend[4]; // indexed by a direction4 -}; - struct city_style_threshold { QPixmap *sprite; }; @@ -317,8 +269,6 @@ struct named_sprites { QPixmap *grid_borders[EDGE_COUNT][2]; QPixmap *color; } player[MAX_NUM_PLAYER_SLOTS]; - - struct drawing_data *drawing[MAX_NUM_ITEMS]; }; struct specfile { @@ -347,13 +297,6 @@ struct small_sprite { QPixmap *sprite; }; -static void drawing_data_destroy(struct drawing_data *draw); - -struct tileset_layer { - char **match_types; - size_t match_count; -}; - struct tileset { char name[512]; char given_name[MAX_LEN_NAME]; @@ -370,6 +313,7 @@ struct tileset { struct { freeciv::layer_special *background, *middleground, *foreground; } special_layers; + std::array terrain_layers; freeciv::layer_darkness *darkness_layer; enum ts_type type; @@ -406,17 +350,13 @@ struct tileset { int unit_upkeep_offset_y; int unit_upkeep_small_offset_y; -#define NUM_CORNER_DIRS 4 int num_valid_tileset_dirs, num_cardinal_tileset_dirs; int num_index_valid, num_index_cardinal; std::array valid_tileset_dirs, cardinal_tileset_dirs; - std::array terrain_layers; QSet *specfiles; QSet *small_sprites; // This hash table maps tilespec tags to struct small_sprites. QHash *sprite_hash; - // This hash table maps terrain graphic strings to drawing data. - QHash *tile_hash; QHash *estyle_hash; struct named_sprites sprites; struct color_system *color_system; @@ -481,45 +421,6 @@ void tileset_error(QtMsgType level, const char *format, ...) } } -/** - Create a new drawing data. - */ -static struct drawing_data *drawing_data_new() -{ - struct drawing_data *draw = new drawing_data[1](); - - return draw; -} - -/** - Free a drawing data. - */ -static void drawing_data_destroy(struct drawing_data *draw) -{ - int i; - - fc_assert_ret(NULL != draw); - - for (i = 0; i < 4; i++) { - if (draw->blend[i]) { - free_sprite(draw->blend[i]); - } - } - for (i = 0; i < draw->num_layers; i++) { - int vec_size = sprite_vector_size(&draw->layer[i].allocated); - int j; - - for (j = 0; j < vec_size; j++) { - free_sprite(draw->layer[i].allocated.p[j]); - } - - sprite_vector_free(&draw->layer[i].base); - sprite_vector_free(&draw->layer[i].allocated); - delete[] draw->layer[i].cells; - } - delete[] draw; -} - /** Return unscaled tileset if it exists, or default otherwise */ @@ -802,6 +703,16 @@ int tileset_num_cardinal_dirs(const struct tileset *t) return t->num_cardinal_tileset_dirs; } +/** + * @brief Returns the number of cardinal indices used by the tileset. + * + * This is `2^tileset_num_cardinal_dirs(t)`. + */ +int tileset_num_index_cardinals(const struct tileset *t) +{ + return t->num_index_cardinal; +} + /** * @brief Returns the cardinal directions used by the tileset. * @@ -1027,7 +938,7 @@ static bool check_tilespec_capabilities(struct section_file *file, */ static void tileset_free_toplevel(struct tileset *t) { - int i, j; + int i; if (t->main_intro_filename) { FCPP_FREE(t->main_intro_filename); @@ -1042,12 +953,6 @@ static void tileset_free_toplevel(struct tileset *t) } t->num_preferred_themes = 0; - if (t->tile_hash) { - for (auto *a : qAsConst(*t->tile_hash)) { - drawing_data_destroy(a); - } - FC_FREE(t->tile_hash); - } if (t->estyle_hash) { delete t->estyle_hash; t->estyle_hash = NULL; @@ -1059,17 +964,6 @@ static void tileset_free_toplevel(struct tileset *t) } } - for (i = 0; i < MAX_NUM_LAYERS; i++) { - struct tileset_layer *tslp = &t->terrain_layers[i]; - - if (tslp->match_types) { - for (j = 0; j < tslp->match_count; j++) { - delete[] tslp->match_types[j]; - } - FCPP_FREE(tslp->match_types); - } - } - if (t->color_system) { color_system_free(t->color_system); t->color_system = NULL; @@ -1626,20 +1520,20 @@ static char *tilespec_gfx_filename(const char *gfx_filename) /** Determine the sprite_type string. */ -static int check_sprite_type(const char *sprite_type, - const char *tile_section) +static freeciv::layer_terrain::sprite_type +check_sprite_type(const char *sprite_type, const char *tile_section) { if (fc_strcasecmp(sprite_type, "corner") == 0) { - return CELL_CORNER; + return freeciv::layer_terrain::CELL_CORNER; } if (fc_strcasecmp(sprite_type, "single") == 0) { - return CELL_WHOLE; + return freeciv::layer_terrain::CELL_WHOLE; } if (fc_strcasecmp(sprite_type, "whole") == 0) { - return CELL_WHOLE; + return freeciv::layer_terrain::CELL_WHOLE; } qCritical("[%s] unknown sprite_type \"%s\".", tile_section, sprite_type); - return CELL_WHOLE; + return freeciv::layer_terrain::CELL_WHOLE; } static bool tileset_invalid_offsets(struct tileset *t, @@ -1745,6 +1639,21 @@ static void tileset_add_layer(struct tileset *t, mapview_layer layer) t->darkness_layer = l.get(); t->layers.emplace_back(std::move(l)); } break; + case LAYER_TERRAIN1: { + auto l = std::make_unique(t, 0); + t->terrain_layers[0] = l.get(); + t->layers.emplace_back(std::move(l)); + } break; + case LAYER_TERRAIN2: { + auto l = std::make_unique(t, 1); + t->terrain_layers[1] = l.get(); + t->layers.emplace_back(std::move(l)); + } break; + case LAYER_TERRAIN3: { + auto l = std::make_unique(t, 2); + t->terrain_layers[2] = l.get(); + t->layers.emplace_back(std::move(l)); + } break; case LAYER_SPECIAL1: { auto l = std::make_unique(t, layer); t->special_layers.background = l.get(); @@ -2157,22 +2066,13 @@ static struct tileset *tileset_read_toplevel(const char *tileset_name, // Terrain layer info. for (i = 0; i < MAX_NUM_LAYERS; i++) { - struct tileset_layer *tslp = &t->terrain_layers[i]; - int j, k; - - tslp->match_types = const_cast(secfile_lookup_str_vec( - file, &tslp->match_count, "layer%d.match_types", i)); - for (j = 0; j < tslp->match_count; j++) { - tslp->match_types[j] = fc_strdup(tslp->match_types[j]); - - for (k = 0; k < j; k++) { - if (tslp->match_types[k][0] == tslp->match_types[j][0]) { - tileset_error(LOG_FATAL, - _("[layer%d] match_types: \"%s\" initial " - "('%c') is not unique."), - i, tslp->match_types[j], tslp->match_types[j][0]); - // FIXME: Returns NULL. - } + std::size_t count = 0; + auto match_types = + secfile_lookup_str_vec(file, &count, "layer%d.match_types", i); + for (int j = 0; j < count; j++) { + if (!t->terrain_layers[i]->create_matching_group(match_types[j])) { + tileset_stop_read(t, file, fname, sections, layer_order); + return nullptr; } } } @@ -2187,170 +2087,107 @@ static struct tileset *tileset_read_toplevel(const char *tileset_name, return nullptr; } - fc_assert(t->tile_hash == NULL); - t->tile_hash = new QHash; - section_list_iterate(sections, psection) { - const char *sec_name = section_name(psection); - struct drawing_data *draw = drawing_data_new(); - const char *sprite_type; - int l; - const char *terrain_name; + auto sec_name = section_name(psection); - terrain_name = secfile_lookup_str(file, "%s.tag", sec_name); - - if (terrain_name != NULL) { - draw->name = fc_strdup(terrain_name); - } else { - tileset_error(LOG_ERROR, _("No terrain tag given in section [%s]."), - sec_name); - drawing_data_destroy(draw); - tileset_stop_read(t, file, fname, sections, layer_order); - return nullptr; + QString tag; + { + auto c_tag = secfile_lookup_str(file, "%s.tag", sec_name); + if (c_tag != NULL) { + tag = c_tag; + } else { + tileset_error(LOG_ERROR, _("No terrain tag given in section [%s]."), + sec_name); + tileset_stop_read(t, file, fname, sections, layer_order); + return nullptr; + } } - draw->blending = - secfile_lookup_int_default(file, 0, "%s.blend_layer", sec_name); - draw->blending = CLIP(0, draw->blending, MAX_NUM_LAYERS); - - draw->is_reversed = - secfile_lookup_bool_default(file, false, "%s.is_reversed", sec_name); - draw->num_layers = - secfile_lookup_int_default(file, 0, "%s.num_layers", sec_name); - draw->num_layers = CLIP(1, draw->num_layers, MAX_NUM_LAYERS); - - for (l = 0; l < draw->num_layers; l++) { - struct drawing_layer *dlp = &draw->layer[l]; - struct tileset_layer *tslp = &t->terrain_layers[l]; - const char *match_type; - const char **match_with; - size_t count; - - dlp->is_tall = secfile_lookup_bool_default( - file, false, "%s.layer%d_is_tall", sec_name, l); - dlp->offset_x = secfile_lookup_int_default( - file, 0, "%s.layer%d_offset_x", sec_name, l); - dlp->offset_y = secfile_lookup_int_default( - file, 0, "%s.layer%d_offset_y", sec_name, l); - dlp->offset_x = ceil(t->scale * dlp->offset_x); - dlp->offset_y = ceil(t->scale * dlp->offset_y); - - match_type = secfile_lookup_str_default( - file, NULL, "%s.layer%d_match_type", sec_name, l); - if (match_type) { - int j; - - // Determine our match_type. - for (j = 0; j < tslp->match_count; j++) { - if (fc_strcasecmp(tslp->match_types[j], match_type) == 0) { - break; - } - } - if (j >= tslp->match_count) { - qCritical("[%s] invalid match_type \"%s\".", sec_name, match_type); - } else { - dlp->match_index[dlp->match_indices++] = j; + { + auto num_layers = + secfile_lookup_int_default(file, 0, "%s.num_layers", sec_name); + num_layers = CLIP(1, num_layers, MAX_NUM_LAYERS); + + for (int l = 0; l < num_layers; l++) { + if (!t->terrain_layers[l]->add_tag( + tag, QString(sec_name).mid(strlen(TILE_SECTION_PREFIX)))) { + tileset_stop_read(t, file, fname, sections, layer_order); + return nullptr; } - } - match_with = secfile_lookup_str_vec( - file, &count, "%s.layer%d_match_with", sec_name, l); - if (match_with) { - int j, k; + // Offsets + { + auto is_tall = secfile_lookup_bool_default( + file, false, "%s.layer%d_is_tall", sec_name, l); + + auto offset_x = secfile_lookup_int_default( + file, 0, "%s.layer%d_offset_x", sec_name, l); + offset_x = ceil(t->scale * offset_x); + if (is_tall) { + offset_x += FULL_TILE_X_OFFSET; + } + + auto offset_y = secfile_lookup_int_default( + file, 0, "%s.layer%d_offset_y", sec_name, l); + offset_y = ceil(t->scale * offset_y); + if (is_tall) { + offset_y += FULL_TILE_Y_OFFSET; + } - if (count > MAX_NUM_MATCH_WITH) { - qCritical("[%s] match_with has too many types (%d, max %d)", - sec_name, static_cast(count), MAX_NUM_MATCH_WITH); - count = MAX_NUM_MATCH_WITH; + if (!t->terrain_layers[l]->set_tag_offsets(tag, offset_x, + offset_y)) { + tileset_stop_read(t, file, fname, sections, layer_order); + return nullptr; + } } - if (1 < dlp->match_indices) { - qCritical("[%s] previous match_with ignored.", sec_name); - dlp->match_indices = 1; - } else if (1 > dlp->match_indices) { - qCritical("[%s] missing match_type, using \"%s\".", sec_name, - tslp->match_types[0]); - dlp->match_index[0] = 0; - dlp->match_indices = 1; + // Sprite type + { + auto type_str = secfile_lookup_str_default( + file, "whole", "%s.layer%d_sprite_type", sec_name, l); + if (!t->terrain_layers[l]->set_tag_sprite_type( + tag, check_sprite_type(type_str, sec_name))) { + tileset_stop_read(t, file, fname, sections, layer_order); + return nullptr; + } } - for (k = 0; k < count; k++) { - for (j = 0; j < tslp->match_count; j++) { - if (fc_strcasecmp(tslp->match_types[j], match_with[k]) == 0) { - break; + // Matching + { + auto matching_group = secfile_lookup_str_default( + file, NULL, "%s.layer%d_match_type", sec_name, l); + + if (matching_group) { + if (!t->terrain_layers[l]->set_tag_matching_group( + tag, matching_group)) { + tileset_stop_read(t, file, fname, sections, layer_order); + return nullptr; } } - if (j >= tslp->match_count) { - qCritical("[%s] layer%d_match_with: invalid \"%s\".", sec_name, - l, match_with[k]); - } else if (1 < count) { - int m; - - for (m = 0; m < dlp->match_indices; m++) { - if (dlp->match_index[m] == j) { - qCritical("[%s] layer%d_match_with: duplicate \"%s\".", - sec_name, l, match_with[k]); - break; + + std::size_t count = 0; + auto match_with = secfile_lookup_str_vec( + file, &count, "%s.layer%d_match_with", sec_name, l); + if (match_with) { + for (std::size_t j = 0; j < count; ++j) { + if (!t->terrain_layers[l]->set_tag_matches_with( + tag, match_with[j])) { + tileset_stop_read(t, file, fname, sections, layer_order); + return nullptr; } } - if (m >= dlp->match_indices) { - dlp->match_index[dlp->match_indices++] = j; - } - } else { - dlp->match_index[dlp->match_indices++] = j; } } - FCPP_FREE(match_with); } - - // Check match_indices - switch (dlp->match_indices) { - case 0: - case 1: - dlp->match_style = MATCH_NONE; - break; - case 2: - if (dlp->match_index[0] == dlp->match_index[1]) { - dlp->match_style = MATCH_SAME; - } else { - dlp->match_style = MATCH_PAIR; - } - break; - default: - dlp->match_style = MATCH_FULL; - break; - }; - - sprite_type = secfile_lookup_str_default( - file, "whole", "%s.layer%d_sprite_type", sec_name, l); - dlp->sprite_type = static_cast( - check_sprite_type(sprite_type, sec_name)); - - switch (dlp->sprite_type) { - case CELL_WHOLE: - // OK, no problem - break; - case CELL_CORNER: - if (dlp->is_tall || dlp->offset_x > 0 || dlp->offset_y > 0) { - qCritical("[%s] layer %d: you cannot have tall terrain or\n" - "a sprite offset with a cell-based drawing method.", - sec_name, l); - dlp->is_tall = false; - dlp->offset_x = dlp->offset_y = 0; + { + auto blending = + secfile_lookup_int_default(file, 0, "%s.blend_layer", sec_name); + if (blending > 0) { + t->terrain_layers[CLIP(0, blending - 1, MAX_NUM_LAYERS - 1)] + ->enable_blending(tag); } - break; - }; - } - - bool hc = t->tile_hash->contains(draw->name); - t->tile_hash->insert(draw->name, draw); - if (hc) { - qCritical( - "warning: multiple tile sections containing terrain tag \"%s\".", - qUtf8Printable(draw->name)); - tileset_stop_read(t, file, fname, sections, layer_order); - return nullptr; + } } } section_list_iterate_end; @@ -2470,13 +2307,10 @@ static const char *citizen_rule_name(enum citizen_category citizen) binary value 1000 will be converted into "n1e0s0w0". This is in a clockwise ordering. */ -static QString &cardinal_index_str(const struct tileset *t, int idx) +QString cardinal_index_str(const struct tileset *t, int idx) { - static QString c; - int i; - - c = QString(); - for (i = 0; i < t->num_cardinal_tileset_dirs; i++) { + auto c = QString(); + for (int i = 0; i < t->num_cardinal_tileset_dirs; i++) { int value = (idx >> i) & 1; c += QStringLiteral("%1%2").arg( @@ -3832,268 +3666,9 @@ static void tileset_setup_base(struct tileset *t, void tileset_setup_tile_type(struct tileset *t, const struct terrain *pterrain) { - struct drawing_data *draw; - QPixmap *sprite; - QString buffer; - int i, l; - - if (!t->tile_hash->contains(pterrain->graphic_str) - && !t->tile_hash->contains(pterrain->graphic_alt)) { - tileset_error(LOG_FATAL, - _("Terrain \"%s\": no graphic tile \"%s\" or \"%s\"."), - terrain_rule_name(pterrain), pterrain->graphic_str, - pterrain->graphic_alt); - } - draw = t->tile_hash->value(pterrain->graphic_str); - if (!draw) { - draw = t->tile_hash->value(pterrain->graphic_alt); + for (auto &layer : t->layers) { + layer->initialize_terrain(pterrain); } - if (draw->init) { - t->sprites.drawing[terrain_index(pterrain)] = draw; - return; - } - - // Set up each layer of the drawing. - for (l = 0; l < draw->num_layers; l++) { - struct drawing_layer *dlp = &draw->layer[l]; - struct tileset_layer *tslp = &t->terrain_layers[l]; - sprite_vector_init(&dlp->base); - sprite_vector_init(&dlp->allocated); - - switch (dlp->sprite_type) { - case CELL_WHOLE: - switch (dlp->match_style) { - case MATCH_NONE: - // Load whole sprites for this tile. - for (i = 0;; i++) { - buffer = QStringLiteral("t.l%1.%2%3") - .arg(QString::number(l), draw->name, - QString::number(i + 1)); - sprite = load_sprite(t, buffer, true, false); - if (!sprite) { - break; - } - sprite_vector_reserve(&dlp->base, i + 1); - dlp->base.p[i] = sprite; - } - // check for base sprite, allowing missing sprites above base - if (0 == i && 0 == l) { - /* TRANS: 'base' means 'base of terrain gfx', not 'military base' - */ - tileset_error(LOG_FATAL, _("Missing base sprite for tag \"%s\"."), - qUtf8Printable(buffer)); - } - break; - case MATCH_SAME: - // Load 16 cardinally-matched sprites. - for (i = 0; i < t->num_index_cardinal; i++) { - buffer = QStringLiteral("t.l%1.%2_%3") - .arg(QString::number(l), draw->name, - cardinal_index_str(t, i)); - dlp->match[i] = tiles_lookup_sprite_tag_alt( - t, LOG_FATAL, qUtf8Printable(buffer), "", "matched terrain", - terrain_rule_name(pterrain), true); - } - break; - case MATCH_PAIR: - case MATCH_FULL: - fc_assert(false); // not yet defined - break; - }; - break; - case CELL_CORNER: { - const int count = dlp->match_indices; - int number = NUM_CORNER_DIRS; - - switch (dlp->match_style) { - case MATCH_NONE: - // do nothing - break; - case MATCH_PAIR: - case MATCH_SAME: - // N directions (NSEW) * 3 dimensions of matching - fc_assert(count == 2); - number = NUM_CORNER_DIRS * 2 * 2 * 2; - break; - case MATCH_FULL: - default: - // N directions (NSEW) * 3 dimensions of matching - // could use exp() or expi() here? - number = NUM_CORNER_DIRS * count * count * count; - break; - }; - - dlp->cells = new QPixmap *[number](); - - for (i = 0; i < number; i++) { - enum direction4 dir = static_cast(i % NUM_CORNER_DIRS); - int value = i / NUM_CORNER_DIRS; - - switch (dlp->match_style) { - case MATCH_NONE: - buffer = QStringLiteral("t.l%1.%2_cell_%3") - .arg(QString::number(l), draw->name, - QString(direction4letters[dir])); - dlp->cells[i] = tiles_lookup_sprite_tag_alt( - t, LOG_FATAL, qUtf8Printable(buffer), "", "cell terrain", - terrain_rule_name(pterrain), true); - break; - case MATCH_SAME: - buffer = QStringLiteral("t.l%1.%2_cell_%3%4%5%6") - .arg(QString::number(l), draw->name, - QString(direction4letters[dir]), - QString::number((value) &1), - QString::number((value >> 1) & 1), - QString::number((value >> 2) & 1)); - dlp->cells[i] = tiles_lookup_sprite_tag_alt( - t, LOG_FATAL, qUtf8Printable(buffer), "", "same cell terrain", - terrain_rule_name(pterrain), true); - break; - case MATCH_PAIR: - buffer = - QStringLiteral("t.l%1.%2_cell_%3_%4_%5_%6") - .arg(QString::number(l), draw->name, - QChar(direction4letters[dir]), - QChar(tslp->match_types[dlp->match_index[(value) &1]] - [0]), - QChar(tslp->match_types[dlp->match_index[(value >> 1) - & 1]][0]), - QChar(tslp->match_types[dlp->match_index[(value >> 2) - & 1]][0])); - - dlp->cells[i] = tiles_lookup_sprite_tag_alt( - t, LOG_FATAL, qUtf8Printable(buffer), "", "cell pair terrain", - terrain_rule_name(pterrain), true); - break; - case MATCH_FULL: { - int tthis = dlp->match_index[0]; - int n, s, e, w; - int v1, v2, v3; - - v1 = dlp->match_index[value % count]; - value /= count; - v2 = dlp->match_index[value % count]; - value /= count; - v3 = dlp->match_index[value % count]; - - fc_assert(v1 < count && v2 < count && v3 < count); - - // Assume merged cells. This should be a separate option. - switch (dir) { - case DIR4_NORTH: - s = tthis; - w = v1; - n = v2; - e = v3; - break; - case DIR4_EAST: - w = tthis; - n = v1; - e = v2; - s = v3; - break; - case DIR4_SOUTH: - n = tthis; - e = v1; - s = v2; - w = v3; - break; - case DIR4_WEST: - default: // avoid warnings - e = tthis; - s = v1; - w = v2; - n = v3; - break; - }; - - /* Use first character of match_types, - * already checked for uniqueness. */ - buffer = - QStringLiteral("t.l%1.cellgroup_%2_%3_%4_%5") - .arg(QString::number(l), QChar(tslp->match_types[n][0]), - QChar(tslp->match_types[e][0]), - QChar(tslp->match_types[s][0]), - QChar(tslp->match_types[w][0])); - sprite = load_sprite(t, buffer, true, false); - - if (sprite) { - // Crop the sprite to separate this cell. - int vec_size = sprite_vector_size(&dlp->allocated); - - const int W = t->normal_tile_width; - const int H = t->normal_tile_height; - int x[4] = {W / 4, W / 4, 0, W / 2}; - int y[4] = {H / 2, 0, H / 4, H / 4}; - int xo[4] = {0, 0, -W / 2, W / 2}; - int yo[4] = {H / 2, -H / 2, 0, 0}; - - sprite = crop_sprite(sprite, x[dir], y[dir], W / 2, H / 2, - t->sprites.mask.tile, xo[dir], yo[dir], - 1.0f, false); - /* We allocated new sprite with crop_sprite. Store its - * address so we can free it. */ - sprite_vector_reserve(&dlp->allocated, vec_size + 1); - dlp->allocated.p[vec_size] = sprite; - } else { - qCritical("Terrain graphics sprite for tag \"%s\" missing.", - qUtf8Printable(buffer)); - } - - dlp->cells[i] = sprite; - } break; - }; - } - } break; - }; - } - - // try an optional special name - buffer = QStringLiteral("t.blend.%1").arg(draw->name); - draw->blender = tiles_lookup_sprite_tag_alt( - t, LOG_VERBOSE, qUtf8Printable(buffer), "", "blend terrain", - terrain_rule_name(pterrain), true); - - if (draw->blending > 0) { - const int bl = draw->blending - 1; - - if (NULL == draw->blender) { - int li = 0; - - // try an already loaded base - while (NULL == draw->blender && li < draw->blending - && 0 < draw->layer[li].base.size) { - draw->blender = draw->layer[li++].base.p[0]; - } - } - - if (NULL == draw->blender) { - // try an unloaded base name - buffer = - QStringLiteral("t.l%1.%21").arg(QString::number(bl), draw->name); - draw->blender = tiles_lookup_sprite_tag_alt( - t, LOG_FATAL, qUtf8Printable(buffer), "", "base (blend) terrain", - terrain_rule_name(pterrain), true); - } - } - - if (NULL != draw->blender) { - // Set up blending sprites. This only works in iso-view! - const int W = t->normal_tile_width; - const int H = t->normal_tile_height; - const int offsets[4][2] = { - {W / 2, 0}, {0, H / 2}, {W / 2, H / 2}, {0, 0}}; - int dir = 0; - - for (; dir < 4; dir++) { - draw->blend[dir] = - crop_sprite(draw->blender, offsets[dir][0], offsets[dir][1], W / 2, - H / 2, t->sprites.dither_tile, 0, 0, 1.0f, false); - } - } - - draw->init = true; - t->sprites.drawing[terrain_index(pterrain)] = draw; } /** @@ -4176,10 +3751,8 @@ static QPixmap *get_unit_nation_flag_sprite(const struct tileset *t, tterrain_near : terrain types of all adjacent terrain tspecial_near : specials of all adjacent terrain */ -static void build_tile_data(const struct tile *ptile, - struct terrain *pterrain, - struct terrain **tterrain_near, - bv_extras *textras_near) +void build_tile_data(const struct tile *ptile, struct terrain *pterrain, + struct terrain **tterrain_near, bv_extras *textras_near) { int dir; @@ -4800,41 +4373,6 @@ static void fill_city_overlays_sprite_array(const struct tileset *t, } } -/** - Helper function for fill_terrain_sprite_layer. - Fill in the sprite array for blended terrain. - */ -static void fill_terrain_sprite_blending(const struct tileset *t, - std::vector &sprs, - const struct tile *ptile, - const struct terrain *pterrain, - struct terrain **tterrain_near) -{ - const int W = t->normal_tile_width, H = t->normal_tile_height; - const int offsets[4][2] = {{W / 2, 0}, {0, H / 2}, {W / 2, H / 2}, {0, 0}}; - int dir = 0; - - /* - * We want to mark unknown tiles so that an unreal tile will be - * given the same marking as our current tile - that way we won't - * get the "unknown" dither along the edge of the map. - */ - for (; dir < 4; dir++) { - struct tile *tile1 = mapstep(&(wld.map), ptile, DIR4_TO_DIR8[dir]); - struct terrain *other; - - if (!tile1 || client_tile_get_known(tile1) == TILE_UNKNOWN - || pterrain == (other = tterrain_near[DIR4_TO_DIR8[dir]]) - || (0 == t->sprites.drawing[terrain_index(other)]->blending - && NULL == t->sprites.drawing[terrain_index(other)]->blender)) { - continue; - } - - sprs.emplace_back(t, t->sprites.drawing[terrain_index(other)]->blend[dir], true, - offsets[dir][0], offsets[dir][1]); - } -} - /** Add sprites for fog (and some forms of darkness). */ @@ -4885,185 +4423,6 @@ static void fill_fog_sprite_array(const struct tileset *t, } } -/** - Helper function for fill_terrain_sprite_layer. - */ -static void fill_terrain_sprite_array( - struct tileset *t, std::vector &sprs, int l, // layer_num - const struct tile *ptile, const struct terrain *pterrain, - struct terrain **tterrain_near, struct drawing_data *draw) -{ - struct drawing_layer *dlp = &draw->layer[l]; - int tthis = dlp->match_index[0]; - int that = dlp->match_index[1]; - int ox = dlp->offset_x; - int oy = dlp->offset_y; - int i; - -#define MATCH(dir) \ - (t->sprites.drawing[terrain_index(tterrain_near[(dir)])]->num_layers > l \ - ? t->sprites.drawing[terrain_index(tterrain_near[(dir)])] \ - ->layer[l] \ - .match_index[0] \ - : -1) - - switch (dlp->sprite_type) { - case CELL_WHOLE: { - switch (dlp->match_style) { - case MATCH_NONE: { - int count = sprite_vector_size(&dlp->base); - - if (count > 0) { - /* Pseudo-random reproducable algorithm to pick a sprite. Use - * modulo to limit the number to a handleable size [0..32000). */ - count = fc_randomly(tile_index(ptile) % 32000, count); - - if (dlp->is_tall) { - ox += FULL_TILE_X_OFFSET; - oy += FULL_TILE_Y_OFFSET; - } - sprs.emplace_back(t, dlp->base.p[count], true, ox, oy); - } - break; - } - case MATCH_SAME: { - int tileno = 0; - - for (i = 0; i < t->num_cardinal_tileset_dirs; i++) { - enum direction8 dir = - t->cardinal_tileset_dirs[static_cast(i)]; - - if (MATCH(dir) == tthis) { - tileno |= 1 << i; - } - } - - if (dlp->is_tall) { - ox += FULL_TILE_X_OFFSET; - oy += FULL_TILE_Y_OFFSET; - } - sprs.emplace_back(t, dlp->match[tileno], true, ox, oy); - break; - } - case MATCH_PAIR: - case MATCH_FULL: - fc_assert(false); // not yet defined - break; - }; - break; - } - case CELL_CORNER: { - /* Divide the tile up into four rectangular cells. Each of these - * cells covers one corner, and each is adjacent to 3 different - * tiles. For each cell we pick a sprite based upon the adjacent - * terrains at each of those tiles. Thus, we have 8 different sprites - * for each of the 4 cells (32 sprites total). - * - * These arrays correspond to the direction4 ordering. */ - const int W = t->normal_tile_width; - const int H = t->normal_tile_height; - const int iso_offsets[4][2] = { - {W / 4, 0}, {W / 4, H / 2}, {W / 2, H / 4}, {0, H / 4}}; - const int noniso_offsets[4][2] = { - {0, 0}, {W / 2, H / 2}, {W / 2, 0}, {0, H / 2}}; - - // put corner cells - for (i = 0; i < NUM_CORNER_DIRS; i++) { - const int count = dlp->match_indices; - int array_index = 0; - enum direction8 dir = dir_ccw(DIR4_TO_DIR8[i]); - int x = (t->type == TS_ISOMETRIC ? iso_offsets[i][0] - : noniso_offsets[i][0]); - int y = (t->type == TS_ISOMETRIC ? iso_offsets[i][1] - : noniso_offsets[i][1]); - int m[3] = {MATCH(dir_ccw(dir)), MATCH(dir), MATCH(dir_cw(dir))}; - QPixmap *s; - - // synthesize 4 dimensional array? - switch (dlp->match_style) { - case MATCH_NONE: - // We have no need for matching, just plug the piece in place. - break; - case MATCH_SAME: - array_index = array_index * 2 + (m[2] != tthis); - array_index = array_index * 2 + (m[1] != tthis); - array_index = array_index * 2 + (m[0] != tthis); - break; - case MATCH_PAIR: - array_index = array_index * 2 + (m[2] == that); - array_index = array_index * 2 + (m[1] == that); - array_index = array_index * 2 + (m[0] == that); - break; - case MATCH_FULL: - default: { - int n[3]; - int j = 0; - for (; j < 3; j++) { - int k = 0; - for (; k < count; k++) { - n[j] = k; // default to last entry - if (m[j] == dlp->match_index[k]) { - break; - } - } - } - array_index = array_index * count + n[2]; - array_index = array_index * count + n[1]; - array_index = array_index * count + n[0]; - } break; - }; - array_index = array_index * NUM_CORNER_DIRS + i; - - s = dlp->cells[array_index]; - if (s) { - sprs.emplace_back(t, s, true, x, y); - } - } - break; - } - }; -#undef MATCH -} - -/** - Add sprites for the base tile to the sprite list. This doesn't - include specials or rivers. - */ -static void fill_terrain_sprite_layer(struct tileset *t, - std::vector &sprs, - int layer_num, - const struct tile *ptile, - const struct terrain *pterrain, - struct terrain **tterrain_near) -{ - QPixmap *sprite; - struct drawing_data *draw = t->sprites.drawing[terrain_index(pterrain)]; - const int l = - (draw->is_reversed ? (draw->num_layers - layer_num - 1) : layer_num); - - fc_assert(layer_num < TERRAIN_LAYER_COUNT); - - // Skip the normal drawing process. - /* FIXME: this should avoid calling load_sprite since it's slow and - * increases the refcount without limit. */ - if (ptile->spec_sprite - && (sprite = load_sprite(t, ptile->spec_sprite, true, false))) { - if (l == 0) { - sprs.emplace_back(t, sprite); - } - return; - } - - if (l < draw->num_layers) { - fill_terrain_sprite_array(t, sprs, l, ptile, pterrain, tterrain_near, - draw); - - if ((l + 1) == draw->blending) { - fill_terrain_sprite_blending(t, sprs, ptile, pterrain, tterrain_near); - } - } -} - /** Indicate whether a unit is to be drawn with a surrounding city outline under current conditions. @@ -5404,9 +4763,7 @@ fill_sprite_array(struct tileset *t, enum mapview_layer layer, break; case LAYER_TERRAIN1: - if (NULL != pterrain && !solid_bg) { - fill_terrain_sprite_layer(t, sprs, 0, ptile, pterrain, tterrain_near); - } + fc_assert_ret_val(false, {}); break; case LAYER_DARKNESS: @@ -5414,16 +4771,11 @@ fill_sprite_array(struct tileset *t, enum mapview_layer layer, break; case LAYER_TERRAIN2: - if (NULL != pterrain && !solid_bg) { - fill_terrain_sprite_layer(t, sprs, 1, ptile, pterrain, tterrain_near); - } + fc_assert_ret_val(false, {}); break; case LAYER_TERRAIN3: - if (NULL != pterrain && !solid_bg) { - fc_assert(MAX_NUM_LAYERS == 3); - fill_terrain_sprite_layer(t, sprs, 2, ptile, pterrain, tterrain_near); - } + fc_assert_ret_val(false, {}); break; case LAYER_WATER: @@ -6155,6 +5507,14 @@ QPixmap *get_event_sprite(const struct tileset *t, enum event_type event) return t->sprites.events[event]; } +/** + * Return dither sprite + */ +QPixmap *get_dither_sprite(const struct tileset *t) +{ + return t->sprites.dither_tile; +} + /** * Return tile mask sprite */ @@ -6365,27 +5725,24 @@ std::vector fill_basic_terrain_layer_sprite_array(struct tileset *t, int layer, struct terrain *pterrain) { - auto sprs = std::vector(); - struct drawing_data *draw = t->sprites.drawing[terrain_index(pterrain)]; - - struct terrain *tterrain_near[8]; - bv_special tspecial_near[8]; - - struct tile dummy_tile; // :( - - int i; - - memset(&dummy_tile, 0, sizeof(struct tile)); + // We create a virtual tile with only the requested terrain, then collect + // sprites from every layer. + auto tile = tile_virtual_new(nullptr); + BV_CLR_ALL(tile->extras); + tile->terrain = pterrain; - for (i = 0; i < 8; i++) { - tterrain_near[i] = pterrain; - BV_CLR_ALL(tspecial_near[i]); + auto sprs = std::vector(); + for (const auto &layer : t->layers) { + const auto lsprs = layer->fill_sprite_array(tile, nullptr, nullptr, + nullptr, nullptr, nullptr); + // Merge by hand because drawn_sprite isn't copyable (but it is + // copy-constructible) + for (const auto &sprite : lsprs) { + sprs.emplace_back(sprite); + } } - i = draw->is_reversed ? draw->num_layers - layer - 1 : layer; - fill_terrain_sprite_array(t, sprs, i, &dummy_tile, pterrain, tterrain_near, - draw); - + tile_virtual_destroy(tile); return sprs; } diff --git a/client/tilespec.h b/client/tilespec.h index 3485179d7f..179c6b9c69 100644 --- a/client/tilespec.h +++ b/client/tilespec.h @@ -78,6 +78,7 @@ struct resource_type; #define MAX_INDEX_VALID 256 #define NUM_TILES_PROGRESS 8 +#define NUM_CORNER_DIRS 4 #define MAX_NUM_CITIZEN_SPRITES 6 @@ -215,6 +216,9 @@ QPixmap *get_citizen_sprite(const struct tileset *t, const struct city *pcity); QPixmap *get_city_flag_sprite(const struct tileset *t, const struct city *pcity); +void build_tile_data(const struct tile *ptile, struct terrain *pterrain, + struct terrain **tterrain_near, + bv_extras *textras_near); QPixmap *get_nation_flag_sprite(const struct tileset *t, const struct nation_type *nation); QPixmap *get_nation_shield_sprite(const struct tileset *t, @@ -253,6 +257,7 @@ fill_basic_extra_sprite_array(const struct tileset *t, const struct extra_type *pextra); bool is_extra_drawing_enabled(struct extra_type *pextra); QPixmap *get_event_sprite(const struct tileset *t, enum event_type event); +QPixmap *get_dither_sprite(const struct tileset *t); QPixmap *get_mask_sprite(const struct tileset *t); QPixmap *tiles_lookup_sprite_tag_alt(struct tileset *t, QtMsgType level, @@ -289,7 +294,9 @@ int tileset_num_city_colors(const struct tileset *t); bool tileset_use_hard_coded_fog(const struct tileset *t); int tileset_num_cardinal_dirs(const struct tileset *t); +int tileset_num_index_cardinals(const struct tileset *t); std::array tileset_cardinal_dirs(const struct tileset *t); +QString cardinal_index_str(const struct tileset *t, int idx); /* These are used as array index -> can't be changed freely to values bigger than size of those arrays. */ diff --git a/doc/README.graphics b/doc/README.graphics index fa70ffd28d..41380adb35 100644 --- a/doc/README.graphics +++ b/doc/README.graphics @@ -8,7 +8,7 @@ Using Graphics: To use different graphics with Freeciv, use the '--tiles' argument to the Freeciv client. Eg, to use the 'engels' graphics, start the client as: - + freeciv-gtk3 --tiles engels What Freeciv actually does in this case is look for a file called @@ -199,7 +199,6 @@ This section contains information on how to draw this terrain type. currently supports blending. Only the base graphic will be blended. The blending mask has sprite t.dither_tile. - is_reversed : Draw layers in reverse order. num_layers : The number of layers in the terrain. This value must be 1, 2 or 3. Each layer is drawn separately. The layerN options below control the @@ -208,7 +207,7 @@ This section contains information on how to draw this terrain type. on normal_tile_width and normal_tile_height, but to corner of the full tile. layerN_offset_x : Offset for terrain sprites - layerN_offset_y + layerN_offset_y layerN_match_type : If 0 or unset, no terrain matching will be done and the base sprite will be drawn for the terrain. If non-zero, then terrain matching will be done. A @@ -330,7 +329,7 @@ having to modify earlier files in the list. Tag prefixes: ------------- -To help keep the tags organised, there is a rough prefix system used +To help keep the tags organised, there is a rough prefix system used for standard tags: f. national flags @@ -396,7 +395,7 @@ sprites. progress indicators: There are three types of progress indicator. "science_bulb" indicates - progress toward the current research target. "warming_sun" indicates + progress toward the current research target. "warming_sun" indicates progress toward global warming. "cooling_flake" indicates progress toward nuclear winter. Each indicator should have 8 states, numbered 0 (least) through 7 (most). The sprite names are "s._".