Refactoring movetype and terrain_costs (#2486)

Simplify the public interface for movement, vision and jamming costs to only
the movetype class and a read-only interface called "terrain_costs".  Changes
to terrain_costs now need to be done through the movetype instance, so logic
about how movement, vision and jamming interact with each other can be handled
in movetype.  This fixes a possible dangling-pointer issue: the default
copy/move implementation in movetype::terrain_info would have copied raw
pointers, without ensuring that those pointers remained valid.

This feels too complex, but also feels like time to merge it and do any further
cleanup as a separate commit.  For example, there's only one place that calls
make_data_unique, and one place that calls make_data_shareable. Merging the
functions should make it clearer.

The terrain_info class is now a private class implementing the terrain_costs
interface, previously it was a public class with raw pointers to other
instances of itself, and a copy constructor which copied the raw-pointers
as-is. One of the raw pointers is still there, but it's only non-null when both
instances involved are owned by one instance of movetype.

=== To cherry-pick back to 1.14 ===

Cherry-pick the tests from 358f564301.

* units.cpp: needs a manual merge. Remove the `set_attr_changed(UA_MOVEMENT_TYPE)`
  call from the master version.
* movetype.cpp: take the changes in this commit, in each case the conflict is
  the change of std::shared_ptr::unique() to std::shared_ptr::count(), which
  has been replaced with make_data_shareable() and make_data_writable().
* movetype.hpp: take the changes in this commit, deleting the old terrain_costs
  class and adding functions to movetype.
This commit is contained in:
Steve Cotton 2019-07-24 17:44:16 +02:00
parent ef47599d96
commit 86f430c2dd
6 changed files with 435 additions and 280 deletions

View file

@ -103,7 +103,7 @@ clearer_info::clearer_info(const unit & viewer) :
underlying_id(viewer.underlying_id()),
sight_range(viewer.vision()),
slowed(viewer.get_state(unit::STATE_SLOWED)),
costs(viewer.movement_type().get_vision())
costs(viewer.movement_type().get_vision().make_standalone())
{
}
@ -114,7 +114,7 @@ clearer_info::clearer_info(const config & cfg) :
underlying_id(cfg["underlying_id"].to_size_t()),
sight_range(cfg["vision"].to_int()),
slowed(cfg.child_or_empty("status")["slowed"].to_bool()),
costs(cfg.child_or_empty("vision_costs"))
costs(movetype::read_terrain_costs(cfg.child_or_empty("vision_costs")))
{
}
@ -129,7 +129,7 @@ void clearer_info::write(config & cfg) const
cfg["vision"] = sight_range;
if ( slowed )
cfg.add_child("status")["slowed"] = true;
costs.write(cfg, "vision_costs");
costs->write(cfg, "vision_costs");
}
@ -437,7 +437,7 @@ bool shroud_clearer::clear_unit(const map_location &view_loc, team &view_team,
find_it->get_location();
return clear_unit(view_loc, view_team, viewer.underlying_id,
viewer.sight_range, viewer.slowed, viewer.costs,
viewer.sight_range, viewer.slowed, *viewer.costs,
real_loc, nullptr, nullptr, nullptr, nullptr, instant);
}

View file

@ -42,7 +42,8 @@ struct clearer_info {
std::size_t underlying_id;
int sight_range;
bool slowed;
movetype::terrain_costs costs;
/// costs is always non-null, all of the constructors initialize it
std::unique_ptr<movetype::terrain_costs> costs;
clearer_info(const unit & viewer);
clearer_info(const config & cfg);

View file

@ -70,8 +70,9 @@ struct movetype::terrain_info::parameters
};
/// Limits for movement, vision and jamming
const movetype::terrain_info::parameters
movetype::terrain_costs::params_(1, movetype::UNREACHABLE);
movetype::mvj_params_{1, movetype::UNREACHABLE};
const movetype::terrain_info::parameters
movetype::terrain_defense::params_min_(0, 100, config_to_min, false, true);
@ -103,14 +104,13 @@ public:
{}
/// Clears the cached data (presumably our fallback has changed).
void clear_cache(const terrain_info * cascade) const;
void clear_cache() const;
/// Tests if merging @a new_values would result in changes.
bool config_has_changes(const config & new_values, bool overwrite) const;
/// Tests for no data in this object.
bool empty() const { return cfg_.empty(); }
/// Merges the given config over the existing costs.
void merge(const config & new_values, bool overwrite,
const terrain_info * cascade);
void merge(const config & new_values, bool overwrite);
/// Read-only access to our parameters.
const parameters & params() const { return params_; }
/// Returns the value associated with the given terrain.
@ -145,14 +145,10 @@ private:
/**
* Clears the cached data (presumably our fallback has changed).
* @param[in] cascade Cache clearing will be cascaded into this terrain_info.
*/
void movetype::terrain_info::data::clear_cache(const terrain_info * cascade) const
void movetype::terrain_info::data::clear_cache() const
{
cache_.clear();
// Cascade the clear to whichever terrain_info falls back on us.
if ( cascade )
cascade->clear_cache();
}
@ -183,13 +179,15 @@ bool movetype::terrain_info::data::config_has_changes(const config & new_values,
/**
* Merges the given config over the existing costs.
*
* After calling this function, the caller must call clear_cache on any
* terrain_info that uses this one as a fallback.
*
* @param[in] new_values The new values.
* @param[in] overwrite If true, the new values overwrite the old.
* If false, the new values are added to the old.
* @param[in] cascade Cache clearing will be cascaded into this terrain_info.
*/
void movetype::terrain_info::data::merge(const config & new_values, bool overwrite,
const terrain_info * cascade)
void movetype::terrain_info::data::merge(const config & new_values, bool overwrite)
{
if ( overwrite )
// We do not support child tags here, so do not copy any that might
@ -215,7 +213,7 @@ void movetype::terrain_info::data::merge(const config & new_values, bool overwri
}
// The new data has invalidated the cache.
clear_cache(cascade);
clear_cache();
}
@ -394,17 +392,11 @@ int movetype::terrain_info::data::value(
* @param[in] params The parameters to use when calculating values.
* This is stored as a reference, so it must be long-lived (typically a static variable).
* @param[in] fallback Used as a backup in case we are asked for data we do not have (think vision costs falling back to movement costs).
* @param[in] cascade A terrain_info that uses us as a fallback. (Needed to sync cache clearing.)
* @note The fallback/cascade mechanism is a bit fragile and really should only
* be used by movetype.
*/
movetype::terrain_info::terrain_info(const parameters & params,
const terrain_info * fallback,
const terrain_info * cascade) :
data_(new data(params)),
merged_data_(),
fallback_(fallback),
cascade_(cascade)
const terrain_info * fallback) :
unique_data_(new data(params)),
fallback_(fallback)
{
}
@ -415,86 +407,134 @@ movetype::terrain_info::terrain_info(const parameters & params,
* @param[in] params The parameters to use when calculating values.
* This is stored as a reference, so it must be long-lived (typically a static variable).
* @param[in] fallback Used as a backup in case we are asked for data we do not have (think vision costs falling back to movement costs).
* @param[in] cascade A terrain_info that uses us as a fallback. (Needed to sync cache clearing.)
* @note The fallback/cascade mechanism is a bit fragile and really should only
* be used by movetype.
*/
movetype::terrain_info::terrain_info(const config & cfg, const parameters & params,
const terrain_info * fallback,
const terrain_info * cascade) :
data_(new data(cfg, params)),
merged_data_(),
fallback_(fallback),
cascade_(cascade)
const terrain_info * fallback) :
unique_data_(new data(cfg, params)),
fallback_(fallback)
{
}
/**
* Copy constructor.
* @param[in] that The terran_info to copy.
* @param[in] fallback Used as a backup in case we are asked for data we do not have (think vision costs falling back to movement costs).
* @param[in] cascade A terrain_info that uses us as a fallback. (Needed to sync cache clearing.)
* @note The fallback/cascade mechanism is a bit fragile and really should only
* be used by movetype.
* Reverse of terrain_costs::write. Never returns nullptr.
* @param[in] cfg An initial data set
*/
movetype::terrain_info::terrain_info(const terrain_info & that,
const terrain_info * fallback,
const terrain_info * cascade) :
// If we do not have a fallback, we need to incorporate that's fallback.
// (See also the assignment operator.)
data_(fallback ? that.data_ : that.get_merged()),
merged_data_(that.merged_data_),
fallback_(fallback),
cascade_(cascade)
std::unique_ptr<movetype::terrain_costs> movetype::read_terrain_costs(const config & cfg)
{
// todoc++14: use std::make_unique
std::unique_ptr<terrain_costs> ret;
ret.reset(new terrain_info(cfg, mvj_params_, nullptr));
return ret;
}
/**
* Copy constructor for callers that handle the fallback and cascade. This is
* intended for terrain_defense or movetype's copy constructors, where a
* similar set of terrain_infos will be created, complete with the same
* relationships between parts of the set.
*
* @param[in] that The terrain_info to copy.
* @param[in] fallback Used as a backup in case we are asked for data we do not have (think vision costs falling back to movement costs).
*/
movetype::terrain_info::terrain_info(const terrain_info & that,
const terrain_info * fallback) :
fallback_(fallback)
{
assert(fallback ? !! that.fallback_ : ! that.fallback_);
copy_data(that);
}
movetype::terrain_info::terrain_info(terrain_info && that,
const terrain_info * fallback) :
fallback_(fallback)
{
assert(fallback ? !! that.fallback_ : ! that.fallback_);
swap_data(that);
}
/**
* Destructor
*
* While this is simply the default destructor, it needs
* to be defined in this file so that it knows about ~data(), which
* is called from the smart pointers' destructor.
*/
movetype::terrain_info::~terrain_info()
{
// While this appears to be simply the default destructor, it needs
// to be defined in this file so that it knows about ~data(), which
// is called from the smart pointers' destructor.
}
movetype::terrain_info::~terrain_info() = default;
/**
* Assignment operator.
* This is only expected to be called either when
* 1) both this and @a that have no siblings, as happens when terrain_defense is copied, or
* 2) all of the siblings are being copied, as happens when movetype is copied.
*/
movetype::terrain_info & movetype::terrain_info::operator=(const terrain_info & that)
void movetype::terrain_info::copy_data(const movetype::terrain_info & that)
{
if ( this != &that ) {
// If we do not have a fallback, we need to incorporate that's fallback.
// (See also the copy constructor.)
data_ = fallback_ ? that.data_ : that.get_merged();
merged_data_ = that.merged_data_;
// We do not change our fallback nor our cascade.
}
that.make_data_shareable();
this->unique_data_.reset();
this->shared_data_ = that.shared_data_;
}
/**
* Swap function for the terrain_info class
*
* This is only expected to be called either when
* 1) both this and @a that have no siblings, as happens when swapping two terrain_defenses, or
* 2) all of the siblings are being swapped, as happens when two movetypes are swapped.
*/
void movetype::terrain_info::swap_data(movetype::terrain_info & that)
{
// It doesn't matter whether they're both unique, both shared, or
// one unique with the other shared.
std::swap(this->unique_data_, that.unique_data_);
std::swap(this->shared_data_, that.shared_data_);
}
/**
* Swap function for the terrain_defense class
*
* This relies on all of the terrain_infos having no fallback and no cascade,
* an assumption which is provided by terrain_defense's constructors.
*/
void swap(movetype::terrain_defense & a, movetype::terrain_defense & b)
{
a.min_.swap_data(b.min_);
a.max_.swap_data(b.max_);
}
/**
* Swap function for the movetype class, including its terrain_info members
*
* This relies on the two sets of the terrain_infos having their movement,
* vision and jamming cascaded in the same way. This assumption is provided by
* movetype's constructors.
*/
void swap(movetype & a, movetype & b)
{
a.movement_.swap_data(b.movement_);
a.vision_.swap_data(b.vision_);
a.jamming_.swap_data(b.jamming_);
swap(a.defense_, b.defense_);
std::swap(a.resist_, b.resist_);
std::swap(a.flying_, b.flying_);
}
movetype & movetype::operator=(const movetype & that)
{
movetype m(that);
swap(*this, m);
return *this;
}
/**
* Clears the cache of values.
*/
void movetype::terrain_info::clear_cache() const
movetype & movetype::operator=(movetype && that)
{
merged_data_.reset();
data_->clear_cache(cascade_);
swap(*this, that);
return *this;
}
/**
* Returns whether or not our data is empty.
*/
bool movetype::terrain_info::empty() const
{
return data_->empty();
return get_data().empty();
}
@ -503,35 +543,28 @@ bool movetype::terrain_info::empty() const
* @param[in] new_values The new values.
* @param[in] overwrite If true, the new values overwrite the old.
* If false, the new values are added to the old.
* @param[in] dependants Other instances that use this as a fallback.
*/
void movetype::terrain_info::merge(const config & new_values, bool overwrite)
void movetype::terrain_info::merge(const config & new_values, bool overwrite,
const std::vector<movetype::terrain_info * > & dependants)
{
if ( !data_->config_has_changes(new_values, overwrite) )
if ( !get_data().config_has_changes(new_values, overwrite) )
// Nothing will change, so skip the copy-on-write.
return;
// Reset merged_data_ before seeing if data_ is unique, since the two might
// point to the same thing.
merged_data_.reset();
// Copy-on-write.
if(data_.use_count() != 1) {
data_.reset(new data(*data_));
// We also need to make copies of our fallback and cascade.
// This is to keep the caching manageable, as this means each
// individual movetype will either share *all* of its cost data
// or not share *all* of its cost data. In particular, we avoid:
// 1) many sets of (unshared) vision costs whose cache would need
// to be cleared when a shared set of movement costs changes;
// 2) a caching nightmare when shared vision costs fallback to
// unshared movement costs.
if ( fallback_ )
fallback_->make_unique_fallback();
if ( cascade_ )
cascade_->make_unique_cascade();
//
// We also need to make our cascade writeable, because changes to this
// instance will change data that they receive when using this as their
// fallback. However, it's no problem for a writable instance to have a
// shareable instance as its fallback.
make_data_writable();
for (auto & dependant : dependants) {
// This will automatically clear the dependant's cache
dependant->make_data_writable();
}
data_->merge(new_values, overwrite, cascade_);
unique_data_->merge(new_values, overwrite);
}
@ -540,10 +573,9 @@ void movetype::terrain_info::merge(const config & new_values, bool overwrite)
*/
int movetype::terrain_info::value(const t_translation::terrain_code & terrain) const
{
return data_->value(terrain, fallback_);
return get_data().value(terrain, fallback_);
}
/**
* Writes our data to a config.
* @param[out] cfg The config that will receive the data.
@ -555,67 +587,131 @@ void movetype::terrain_info::write(config & cfg, const std::string & child_name,
bool merged) const
{
if ( !merged )
data_->write(cfg, child_name);
get_data().write(cfg, child_name);
else
data_->write(cfg, child_name, fallback_);
get_data().write(cfg, child_name, fallback_);
}
/**
* Returns a pointer to data the incorporates our fallback.
* Does a sufficiently deep copy so that the returned object's lifespan
* is independent of other objects' lifespan. Never returns nullptr.
*
* This implements terrain_costs's virtual method for getting an instance that
* doesn't depend on the lifespan of a terrain_defense or movetype object.
* This will do a deep copy of the data (with fallback_ already merged) if
* needed.
*/
const std::shared_ptr<movetype::terrain_info::data> &
movetype::terrain_info::get_merged() const
std::unique_ptr<movetype::terrain_costs> movetype::terrain_info::make_standalone() const
{
// Create-on-demand.
if ( !merged_data_ )
{
if ( !fallback_ )
// Nothing to incorporate.
merged_data_ = data_;
else if ( data_->empty() )
// Pure fallback.
merged_data_ = fallback_->get_merged();
else {
// Need to merge data.
config merged;
write(merged, "", true);
merged_data_ = std::make_shared<data>(merged, data_->params());
}
// todoc++14: use std::make_unique
std::unique_ptr<terrain_costs> t;
if(!fallback_) {
// Call the copy constructor, which will make_data_shareable().
t.reset(new terrain_info(*this, nullptr));
}
return merged_data_;
else if(get_data().empty()) {
// Pure fallback.
t = fallback_->make_standalone();
}
else {
// Need to merge data.
config merged;
write(merged, "", true);
t.reset(new terrain_info(merged, get_data().params(), nullptr));
}
return t;
}
const movetype::terrain_info::data & movetype::terrain_info::get_data() const
{
assert(unique_data_ || shared_data_);
assert(! (unique_data_ && shared_data_));
if(unique_data_)
return *unique_data_;
return *shared_data_;
}
/**
* Ensures our data is not shared, and propagates to our cascade.
* Copy the immutable data back to unique_data_, no-op if the data
* is already in unique_data_.
*
* Ensures our data is not shared, and therefore that changes only
* affect this instance of terrain_info (and any instances using it
* as a fallback).
*
* This does not need to affect the fallback - it's no problem if a
* writable instance has a fallback to a shareable instance, although
* a shareable instance must not fallback to a writable instance.
*/
void movetype::terrain_info::make_unique_cascade() const
void movetype::terrain_info::make_data_writable() const
{
if(data_.use_count() != 1)
if(!unique_data_)
{
// Const hack because this is not really changing the data.
const_cast<terrain_info *>(this)->data_.reset(new data(*data_));
auto t = const_cast<terrain_info *>(this);
t->unique_data_.reset(new data(*shared_data_));
t->shared_data_.reset();
}
if ( cascade_ )
cascade_->make_unique_cascade();
// As we're about to write data, invalidate the cache
unique_data_->clear_cache();
}
/**
* Ensures our data is not shared, and propagates to our fallback.
* Move data to an immutable copy in shared_data_, no-op if the data
* is already in shared_data_.
*
* This is recursive on the fallback chain, because if the data shouldn't be
* writable then the data shouldn't be writable via the fallback either.
*/
void movetype::terrain_info::make_unique_fallback() const
void movetype::terrain_info::make_data_shareable() const
{
if(data_.use_count() != 1)
// Const hack because this is not really changing the data.
const_cast<terrain_info *>(this)->data_.reset(new data(*data_));
if(!unique_data_)
return;
if ( fallback_ )
fallback_->make_unique_fallback();
if(fallback_)
fallback_->make_data_shareable();
// Const hack because this is not really changing the data.
auto t = const_cast<terrain_info *>(this);
t->shared_data_ = std::move(t->unique_data_);
}
/* *** terrain_defense *** */
movetype::terrain_defense::terrain_defense(const terrain_defense & that) :
min_(that.min_, nullptr),
max_(that.max_, nullptr)
{
}
movetype::terrain_defense::terrain_defense(terrain_defense && that) :
min_(std::move(that.min_), nullptr),
max_(std::move(that.max_), nullptr)
{
}
movetype::terrain_defense & movetype::terrain_defense::operator=(const terrain_defense & that)
{
min_.copy_data(that.min_);
max_.copy_data(that.max_);
return *this;
}
movetype::terrain_defense & movetype::terrain_defense::operator=(terrain_defense && that)
{
min_.swap_data(that.min_);
max_.swap_data(that.max_);
return *this;
}
/// Merges the given config over the existing costs.
/// (Not overwriting implies adding.)
void movetype::terrain_defense::merge(const config & new_data, bool overwrite)
{
min_.merge(new_data, overwrite, {});
max_.merge(new_data, overwrite, {});
}
/* *** resistances *** */
@ -695,9 +791,9 @@ void movetype::resistances::write(config & out_cfg, const std::string & child_na
* Default constructor
*/
movetype::movetype() :
movement_(nullptr, &vision_), // This is not access before initialization; the address is merely stored at this point.
vision_(&movement_, &jamming_), // This is not access before initialization; the address is merely stored at this point.
jamming_(&vision_, nullptr),
movement_(mvj_params_, nullptr),
vision_(mvj_params_, &movement_),
jamming_(mvj_params_, &vision_),
defense_(),
resist_(),
flying_(false)
@ -709,12 +805,13 @@ movetype::movetype() :
* Constructor from a config
*/
movetype::movetype(const config & cfg) :
movement_(cfg.child_or_empty("movement_costs"), nullptr, &vision_), // This is not access before initialization; the address is merely stored at this point.
vision_(cfg.child_or_empty("vision_costs"), &movement_, &jamming_), // This is not access before initialization; the address is merely stored at this point.
jamming_(cfg.child_or_empty("jamming_costs"), &vision_, nullptr),
movement_(cfg.child_or_empty("movement_costs"), mvj_params_, nullptr),
vision_(cfg.child_or_empty("vision_costs"), mvj_params_, &movement_),
jamming_(cfg.child_or_empty("jamming_costs"), mvj_params_, &vision_),
defense_(cfg.child_or_empty("defense")),
resist_(cfg.child_or_empty("resistance")),
flying_(cfg["flies"].to_bool(false))
// \todo standardize on "flying" instead of "flies"
{
}
@ -723,15 +820,28 @@ movetype::movetype(const config & cfg) :
* Copy constructor
*/
movetype::movetype(const movetype & that) :
movement_(that.movement_, nullptr, &vision_), // This is not access before initialization; the address is merely stored at this point.
vision_(that.vision_, &movement_, &jamming_), // This is not access before initialization; the address is merely stored at this point.
jamming_(that.jamming_, &vision_, nullptr),
movement_(that.movement_, nullptr),
vision_(that.vision_, &movement_),
jamming_(that.jamming_, &vision_),
defense_(that.defense_),
resist_(that.resist_),
flying_(that.flying_)
{
}
/**
* Move constructor.
*/
movetype::movetype(movetype && that) :
movement_(std::move(that.movement_), nullptr),
vision_(std::move(that.vision_), &movement_),
jamming_(std::move(that.jamming_), &vision_),
defense_(std::move(that.defense_)),
resist_(std::move(that.resist_)),
flying_(std::move(that.flying_))
{
}
/**
* Checks if we have a defense cap (nontrivial min value) for any of the given terrain types.
*/
@ -749,24 +859,10 @@ bool movetype::has_terrain_defense_caps(const std::set<t_translation::terrain_co
*/
void movetype::merge(const config & new_cfg, bool overwrite)
{
for (const config & child : new_cfg.child_range("movement_costs")) {
movement_.merge(child, overwrite);
}
for (const config & child : new_cfg.child_range("vision_costs")) {
vision_.merge(child, overwrite);
}
for (const config & child : new_cfg.child_range("jamming_costs")) {
jamming_.merge(child, overwrite);
}
for (const config & child : new_cfg.child_range("defense")) {
defense_.merge(child, overwrite);
}
for (const config & child : new_cfg.child_range("resistance")) {
resist_.merge(child, overwrite);
for (const auto & applies_to : movetype::effects) {
for (const config & child : new_cfg.child_range(applies_to)) {
merge(child, applies_to, overwrite);
}
}
// "flies" is used when WML defines a movetype.
@ -776,6 +872,25 @@ void movetype::merge(const config & new_cfg, bool overwrite)
flying_ = new_cfg["flying"].to_bool(flying_);
}
void movetype::merge(const config & new_cfg, const std::string & applies_to, bool overwrite)
{
if(applies_to == "movement_costs") {
movement_.merge(new_cfg, overwrite, {&vision_, &jamming_});
}
else if(applies_to == "vision_costs") {
vision_.merge(new_cfg, overwrite, {&jamming_});
}
else if(applies_to == "jamming_costs") {
jamming_.merge(new_cfg, overwrite, {});
}
else if(applies_to == "defense") {
defense_.merge(new_cfg, overwrite);
}
else if(applies_to == "resistance") {
resist_.merge(new_cfg, overwrite);
}
}
/**
* The set of strings defining effects which apply to movetypes.
*/
@ -784,6 +899,9 @@ const std::set<std::string> movetype::effects {"movement_costs",
/**
* Writes the movement type data to the provided config.
*
* Note that this writes the movement part of SingleUnitWML, not the WML for a
* [movetype] tag, because it uses the key "flying" instead of "flies".
*/
void movetype::write(config & cfg) const
{

View file

@ -23,10 +23,72 @@ namespace t_translation { struct terrain_code; }
/// The basic "size" of the unit - flying, small land, large land, etc.
/// This encompasses terrain costs, defenses, and resistances.
///
/// This class is used for both [movetype] and [unit] configs, which use the
/// same data in their configs for [movement_costs], [defense], etc. However,
/// the data for whether the unit flies is held in [movetype]'s "flies" vs
/// [unit]'s "flying".
///
/// Existing behavior of 1.14:
/// * movetype::movetype(const config & cfg) will read only the "flies" key
/// * movetype::merge(const config & cfg, bool overwrite) will read both keys,
/// with "flying" taking priority if both are supplied
/// * movetype::write() will write only the "flying" key
///
/// \todo make this more logical. Ideas:
/// * for 1.15, support both "flying" and "flies" in [movetype]
/// * for 1.17 or later, drop the "flies"
class movetype
{
/// Stores a set of data based on terrain.
class terrain_info
public:
/// A const-only interface for how many (movement, vision, or "jamming") points a
/// unit needs for each hex. Functions to modify the costs are exposed by the
/// movetype instance owning this terrain_costs, so that changes to movement will
/// cascade to the vision, etc.
class terrain_costs
{
public:
virtual ~terrain_costs() = default;
/// Returns the value associated with the given terrain.
///
/// Calculated values are cached for later queries.
virtual int value(const t_translation::terrain_code & terrain) const = 0;
/// Returns the cost associated with the given terrain.
/// Costs are doubled when @a slowed is true.
int cost(const t_translation::terrain_code & terrain, bool slowed=false) const
{
int result = value(terrain);
return slowed && result != movetype::UNREACHABLE ? 2 * result : result;
}
/// Does a sufficiently deep copy so that the returned object's lifespan
/// is independent of other objects' lifespan. Never returns nullptr.
virtual std::unique_ptr<terrain_costs> make_standalone() const = 0;
/// Writes our data to a config.
virtual void write(config & cfg, const std::string & child_name="", bool merged=true) const = 0;
};
/// Reverse of terrain_costs::write. Never returns nullptr.
static std::unique_ptr<terrain_costs> read_terrain_costs(const config & cfg);
// Forward declaration so that terrain_info can friend the
// swap(terrain_defense, terrain_defense) function
class terrain_defense;
private:
/// Stores a set of data based on terrain, in some cases with raw pointers to
/// other instances of terrain_info (the fallback_).
///
/// The data can either be a single instance (in which case it's
/// writable and stored in unique_data_) or may have already been shared
/// (via make_data_shareable()), in which case it's stored in shared_data_.
/// There will always be exactly one of those two that's non-null,
/// get_data() returns it from where-ever it is.
class terrain_info : public terrain_costs
{
/// The terrain-based data.
class data;
@ -37,43 +99,53 @@ class movetype
struct parameters;
explicit terrain_info(const parameters & params,
const terrain_info * fallback=nullptr,
const terrain_info * cascade=nullptr);
const terrain_info * fallback);
terrain_info(const config & cfg, const parameters & params,
const terrain_info * fallback=nullptr,
const terrain_info * cascade=nullptr);
const terrain_info * fallback);
~terrain_info() override;
// Instead of the standard copy and move constructors, there are ones
// that copy the data but require the caller to specify the fallback.
terrain_info(const terrain_info & that) = delete;
terrain_info(terrain_info && that) = delete;
explicit terrain_info(terrain_info && that,
const terrain_info * fallback);
terrain_info(const terrain_info & that,
const terrain_info * fallback=nullptr,
const terrain_info * cascade=nullptr);
~terrain_info();
const terrain_info * fallback);
terrain_info & operator=(const terrain_info & that);
// Similarly to the copy and move constructors, the default assignments
// are deleted, because the caller needs to know about the siblings.
terrain_info & operator=(const terrain_info & that) = delete;
terrain_info & operator=(terrain_info && that) = delete;
void copy_data(const movetype::terrain_info & that);
void swap_data(movetype::terrain_info & that);
/// Clears the cache of values.
void clear_cache() const;
/// Returns whether or not our data is empty.
bool empty() const;
/// Merges the given config over the existing values.
void merge(const config & new_values, bool overwrite);
/// Returns the value associated with the given terrain.
int value(const t_translation::terrain_code & terrain) const;
/// Writes our data to a config.
void write(config & cfg, const std::string & child_name="", bool merged=true) const;
void merge(const config & new_values, bool overwrite,
const std::vector<movetype::terrain_info *> & dependants);
// Implementation of terrain_costs
int value(const t_translation::terrain_code & terrain) const override;
void write(config & cfg, const std::string & child_name="", bool merged=true) const override;
std::unique_ptr<terrain_costs> make_standalone() const override;
private:
// Returns a pointer to data the incorporates our fallback.
const std::shared_ptr<data> & get_merged() const;
// Ensures our data is not shared, and propagates to our cascade.
void make_unique_cascade() const;
// Ensures our data is not shared, and propagates to our fallback.
void make_unique_fallback() const;
/// Move data to an immutable copy in shared_data_, no-op if the data
/// is already in shared_data_.
void make_data_shareable() const;
/// Copy the immutable data back to unique_data_, no-op if the data
/// is already in unique_data_.
void make_data_writable() const;
/// Returns either *unique_data_ or *shared_data_, choosing the one that
/// currently holds the data.
const data & get_data() const;
private:
std::shared_ptr<data> data_; /// Never nullptr
mutable std::shared_ptr<data> merged_data_; /// Created as needed.
std::unique_ptr<data> unique_data_;
std::shared_ptr<const data> shared_data_;
const terrain_info * const fallback_;
const terrain_info * const cascade_;
};
@ -82,38 +154,6 @@ public:
/// The UNREACHABLE macro in the data tree should match this value.
static const int UNREACHABLE = 99;
/// Stores a set of terrain costs (for movement, vision, or "jamming").
class terrain_costs : public terrain_info
{
static const parameters params_;
public:
explicit terrain_costs(const terrain_costs * fallback=nullptr,
const terrain_costs * cascade=nullptr) :
terrain_info(params_, fallback, cascade)
{}
explicit terrain_costs(const config & cfg,
const terrain_costs * fallback=nullptr,
const terrain_costs * cascade=nullptr) :
terrain_info(cfg, params_, fallback, cascade)
{}
terrain_costs(const terrain_costs & that,
const terrain_costs * fallback=nullptr,
const terrain_costs * cascade=nullptr) :
terrain_info(that, fallback, cascade)
{}
terrain_costs& operator=(const terrain_costs&) = default;
/// Returns the cost associated with the given terrain.
/// Costs are doubled when @a slowed is true.
int cost(const t_translation::terrain_code & terrain, bool slowed=false) const
{ int result = value(terrain);
return slowed && result != movetype::UNREACHABLE ? 2 * result : result; }
// Inherited from terrain_info:
//void merge(const config & new_values, bool overwrite);
//void write(config & cfg, const std::string & child_name="", bool merged=true) const;
};
/// Stores a set of defense levels.
class terrain_defense
{
@ -121,10 +161,14 @@ public:
static const terrain_info::parameters params_max_;
public:
terrain_defense() : min_(params_min_), max_(params_max_) {}
terrain_defense() : min_(params_min_, nullptr), max_(params_max_, nullptr) {}
explicit terrain_defense(const config & cfg) :
min_(cfg, params_min_), max_(cfg, params_max_)
min_(cfg, params_min_, nullptr), max_(cfg, params_max_, nullptr)
{}
terrain_defense(const terrain_defense & that);
terrain_defense(terrain_defense && that);
terrain_defense & operator=(const terrain_defense & that);
terrain_defense & operator=(terrain_defense && that);
/// Returns the defense associated with the given terrain.
int defense(const t_translation::terrain_code & terrain) const
@ -134,13 +178,14 @@ public:
{ return min_.value(terrain) != 0; }
/// Merges the given config over the existing costs.
/// (Not overwriting implies adding.)
void merge(const config & new_data, bool overwrite)
{ min_.merge(new_data, overwrite); max_.merge(new_data, overwrite); }
void merge(const config & new_data, bool overwrite);
/// Writes our data to a config, as a child if @a child_name is specified.
/// (No child is created if there is no data.)
void write(config & cfg, const std::string & child_name="") const
{ max_.write(cfg, child_name, false); }
friend void swap(movetype::terrain_defense & a, movetype::terrain_defense & b);
private:
// There will be duplication of the config here, but it is a small
// config, and the duplication allows greater code sharing.
@ -170,17 +215,30 @@ public:
config cfg_;
};
private:
static const terrain_info::parameters mvj_params_;
public:
movetype();
explicit movetype(const config & cfg);
movetype(const movetype & that);
movetype& operator=(const movetype& that) = default;
movetype(movetype && that);
movetype &operator=(const movetype & that);
movetype &operator=(movetype && that);
// The default destructor is sufficient, despite the Rule of Five.
// The copy and assignment functions handle the pointers between
// terrain_cost_impl instances, but all of these instances are owned
// by this instance of movetype.
~movetype() = default;
friend void swap(movetype & a, movetype & b);
friend void swap(movetype::terrain_info & a, movetype::terrain_info & b);
// This class is basically just a holder for its various pieces, so
// provide access to those pieces on demand.
terrain_costs & get_movement() { return movement_; }
terrain_costs & get_vision() { return vision_; }
terrain_costs & get_jamming() { return jamming_; }
// provide access to those pieces on demand. There's no non-const
// getters for terrain_costs, as that's now an interface with only
// const functions in it, and because the logic for how the cascade and
// fallback mechanism works would be easier to handle in movetype itself.
terrain_defense & get_defense() { return defense_; }
resistances & get_resistances() { return resist_; }
// And const access:
@ -226,19 +284,26 @@ public:
/// Returns whether or not there are any jamming-specific costs.
bool has_jamming_data() const { return !jamming_.empty(); }
/// Merges the given config over the existing data.
/// Merges the given config over the existing data, the config should have zero or more
/// children named "movement_costs", "defense", etc.
void merge(const config & new_cfg, bool overwrite=true);
/// Merges the given config over the existing data.
/// @a applies_to which type of movement to change ("movement_costs", etc)
/// @a new_cfg data which could be one of the children of the config for the two-argument form of this function.
void merge(const config & new_cfg, const std::string & applies_to, bool overwrite=true);
/// The set of applicable effects for movement types
static const std::set<std::string> effects;
/// Writes the movement type data to the provided config.
/// Writes the movement type data to the provided config. This uses
/// [unit]'s keynames, with "flying" instead of "flies".
void write(config & cfg) const;
private:
terrain_costs movement_;
terrain_costs vision_;
terrain_costs jamming_;
terrain_info movement_;
terrain_info vision_;
terrain_info jamming_;
terrain_defense defense_;
resistances resist_;

View file

@ -1079,7 +1079,7 @@ const boost::regex fai_identifier("[a-zA-Z_]+");
template<typename MoveT>
void patch_movetype(
MoveT& mt, const std::string& new_key, const std::string& formula_str, int default_val, bool replace)
MoveT& mt, const std::string& type, const std::string& new_key, const std::string& formula_str, int default_val, bool replace)
{
config temp_cfg, original_cfg;
mt.write(original_cfg);
@ -1101,7 +1101,7 @@ void patch_movetype(
}
temp_cfg[new_key] = formula(original);
mt.merge(temp_cfg, true);
mt.merge(temp_cfg, type, true);
}
} // unnamed namespace
@ -1142,7 +1142,7 @@ void unit_type_data::set_config(config& cfg)
continue;
}
patch_movetype(movement_types_[mt].get_resistances(), dmg_type, attr.second, 100, true);
patch_movetype(movement_types_[mt], "resistances", dmg_type, attr.second, 100, true);
}
if(r.has_attribute("default")) {
@ -1152,7 +1152,7 @@ void unit_type_data::set_config(config& cfg)
continue;
}
patch_movetype(mt.second.get_resistances(), dmg_type, r["default"], 100, false);
patch_movetype(mt.second, "resistances", dmg_type, r["default"], 100, false);
}
}
}
@ -1179,13 +1179,9 @@ void unit_type_data::set_config(config& cfg)
}
if(tag == "defense") {
patch_movetype(movement_types_[mt].get_defense(), ter_type, attr.second, 100, true);
} else if(tag == "vision") {
patch_movetype(movement_types_[mt].get_vision(), ter_type, attr.second, 99, true);
} else if(tag == "movement") {
patch_movetype(movement_types_[mt].get_movement(), ter_type, attr.second, 99, true);
} else if(tag == "jamming") {
patch_movetype(movement_types_[mt].get_jamming(), ter_type, attr.second, 99, true);
patch_movetype(movement_types_[mt], tag, ter_type, attr.second, 100, true);
} else {
patch_movetype(movement_types_[mt], tag, ter_type, attr.second, 99, true);
}
}
@ -1197,13 +1193,9 @@ void unit_type_data::set_config(config& cfg)
}
if(tag == "defense") {
patch_movetype(mt.second.get_defense(), ter_type, info["default"], 100, false);
} else if(tag == "vision") {
patch_movetype(mt.second.get_vision(), ter_type, info["default"], 99, false);
} else if(tag == "movement") {
patch_movetype(mt.second.get_movement(), ter_type, info["default"], 99, false);
} else if(tag == "jamming") {
patch_movetype(mt.second.get_jamming(), ter_type, info["default"], 99, false);
patch_movetype(mt.second, tag, ter_type, info["default"], 100, false);
} else {
patch_movetype(mt.second, tag, ter_type, info["default"], 99, false);
}
}
}

View file

@ -2042,32 +2042,11 @@ void unit::apply_builtin_effect(std::string apply_to, const config& effect)
{
set_state(to_remove, false);
}
// Note: It would not be hard to define a new "applies_to=" that
// combines the next five options (the movetype effects).
} else if(apply_to == "movement_costs") {
if(const config& ap = effect.child("movement_costs")) {
} else if(std::find(movetype::effects.cbegin(), movetype::effects.cend(), apply_to) != movetype::effects.cend()) {
// "movement_costs", "vision_costs", "jamming_costs", "defense", "resistance"
if(const config& ap = effect.child(apply_to)) {
set_attr_changed(UA_MOVEMENT_TYPE);
movement_type_.get_movement().merge(ap, effect["replace"].to_bool());
}
} else if(apply_to == "vision_costs") {
if(const config& ap = effect.child("vision_costs")) {
set_attr_changed(UA_MOVEMENT_TYPE);
movement_type_.get_vision().merge(ap, effect["replace"].to_bool());
}
} else if(apply_to == "jamming_costs") {
if(const config& ap = effect.child("jamming_costs")) {
set_attr_changed(UA_MOVEMENT_TYPE);
movement_type_.get_jamming().merge(ap, effect["replace"].to_bool());
}
} else if(apply_to == "defense") {
if(const config& ap = effect.child("defense")) {
set_attr_changed(UA_MOVEMENT_TYPE);
movement_type_.get_defense().merge(ap, effect["replace"].to_bool());
}
} else if(apply_to == "resistance") {
if(const config& ap = effect.child("resistance")) {
set_attr_changed(UA_MOVEMENT_TYPE);
movement_type_.get_resistances().merge(ap, effect["replace"].to_bool());
movement_type_.merge(ap, apply_to, effect["replace"].to_bool());
}
} else if(apply_to == "zoc") {
if(const config::attribute_value* v = effect.get("value")) {