大灾变:黑暗的未来:静态分析和类Rogue游戏

图片5

您一定已经从标题中猜测到,今天的文章将重点关注软件源代码中的错误。 但不仅如此。 如果您不仅对C ++感兴趣,并且对阅读其他开发人员代码中的错误感兴趣,还对不寻常的视频游戏感兴趣,并且想知道什么是“ roguelikes”以及如何玩这些游戏,那么欢迎您继续阅读!

在寻找不寻常的游戏时,我偶然发现了《大灾变:黑暗的未来》 ,该游戏在其他游戏中脱颖而出,这要归功于其图形基于黑色背景上排列的各种颜色的ASCII字符。

令这款游戏和其他类似游戏惊讶的一件事是它们内置了多少功能。 例如,特别是在《 大地的裂变》中 ,由于有数十种参数,特征和初始场景可供使用,因此您甚至无法创建一个不带Google指导的冲动的角色,更不用说整个游戏中发生的事件的多种变化。

由于这是一款带有开源代码的游戏,并且是用C ++编写的,因此,如果没有积极参与开发的静态代码分析器PVS-Studio的检查,我们将无可奈何。 该项目的代码出奇的高品质,但是它仍然存在一些小缺陷,我将在本文中讨论其中一些缺陷。

PVS-Studio已经检查了很多游戏。 您可以在我们的文章“ 视频游戏开发中的静态分析:十大软件错误 ”中找到一些示例。

逻辑学


范例1:

本示例显示了经典的复制粘贴错误。

V501在'||'的左侧和右侧有相同的子表达式 运算符:rng(2,7)<abs(z)|| rng(2,7)<abs(z)overmap.cpp 1503

bool overmap::generate_sub( const int z ) { .... if( rng( 2, 7 ) < abs( z ) || rng( 2, 7 ) < abs( z ) ) { .... } .... } 

两次检查相同的条件。 程序员复制了表达式,但忘记修改副本。 我不确定这是否是一个严重的错误,但事实是该检查无法按预期进行。

另一个类似的错误:

  • V501在'&&'运算符的左侧和右侧有相同的子表达式'one_in(100000 / to_turns <int>(dur))。 player_hardcoded_effects.cpp 547

图片11

范例2:

V728过度检查可以简化。 '(A && B)|| (!A &&!B)'表达式等同于'bool(A)== bool(B)'表达式。 stock_ui.cpp 199

 bool inventory_selector_preset::sort_compare( .... ) const { .... const bool left_fav = g->u.inv.assigned.count( lhs.location->invlet ); const bool right_fav = g->u.inv.assigned.count( rhs.location->invlet ); if( ( left_fav && right_fav ) || ( !left_fav && !right_fav ) ) { return .... } .... } 

从逻辑上讲,这种情况是正确的,但它过于复杂。 编写此代码的人都应该怜悯将要维护它的程序员。 可以用更简单的形式重写它: if(left_fav == right_fav)

另一个类似的错误:

  • V728过度检查可以简化。 '((A &&!B)|| (!A && B)'表达式等同于'bool(A)!= Bool(B)'表达式。 iuse_actor.cpp 2653

离题我


我很惊讶地发现,如今以“ roguelikes”之名进行的游戏只是老派流氓游戏中较为温和的代表。 一切始于1980年的Rogue游戏,该游戏启发了许多学生和程序员来创建自己的具有相似元素的游戏。 我想很多影响也来自桌面游戏DnD及其变体的社区。

图片8

微观优化


范例3:

该组的警告指向可能被优化而不是错误的斑点。

V801性能下降 。 最好将第二个函数参数重新定义为引用。 考虑用“ const ...&type”替换“ const ... type”。 map.cpp 4644

 template <typename Stack> std::list<item> use_amount_stack( Stack stack, const itype_id type ) { std::list<item> ret; for( auto a = stack.begin(); a != stack.end() && quantity > 0; ) { if( a->use_amount( type, ret ) ) { a = stack.erase( a ); } else { ++a; } } return ret; } 

在此代码中, itype_id实际上是伪装的std :: string 。 由于参数无论如何都是作为常量传递的,这意味着它是不可变的,因此简单地传递对变量的引用将有助于避免复制操作,从而提高性能并节省计算资源。 即使该字符串不太可能是一个长字符串,但每次都没有充分理由就将其复制是一个坏主意-尤其如此,因为此函数由各种调用程序调用,而这些调用程序又从外部获取类型并具有复制它。

类似问题:

  • V801性能下降。 最好重新定义第三个函数参数作为参考。 考虑用“ const ...&evt_filter”替换“ const ... evt_filter”。 输入.cpp 691
  • V801性能下降。 最好将第五个函数参数重新定义为参考。 考虑用“ const ...&color”替换“ const ... color”。 输出207
  • 分析仪总共发出32种此类警告。

范例4:

V813性能下降 。 'str'参数可能应该呈现为常量引用。 catacharset.cpp 256

 std::string base64_encode( std::string str ) { if( str.length() > 0 && str[0] == '#' ) { return str; } int input_length = str.length(); std::string encoded_data( output_length, '\0' ); .... for( int i = 0, j = 0; i < input_length; ) { .... } for( int i = 0; i < mod_table[input_length % 3]; i++ ) { encoded_data[output_length - 1 - i] = '='; } return "#" + encoded_data; } 

尽管参数是非常量的,但它在函数体内没有任何变化。 因此,为了优化起见,更好的解决方案是通过常量引用传递它,而不是强制编译器创建本地副本。

这个警告也不是一个人来的。 此类型的警告总数为26。

图片7

类似问题:

  • V813性能下降。 'message'参数可能应该呈现为常量引用。 json.cpp 1452
  • V813性能下降。 “ s”自变量可能应呈现为常量引用。 catacharset.cpp 218
  • 依此类推...

离题二


一些经典的类似roguelike的游戏仍在积极开发中。 如果您查看Cataclysm DDANetHack的GitHub存储库,则会看到每天都提交更改。 NetHack实际上是仍在开发中的最古老的游戏:它于1987年7月发布,最新版本可追溯到2018年。

矮人要塞Dwarf Fortress)是最流行(尽管较年轻)的游戏类型之一。 开发工作始于2002年,第一个版本于2006年发布。其座右铭“输得很开心”反映了这一游戏不可能取胜的事实。 在2007年, 矮人要塞Dwarf Fortress )每年在ASCII GAMES网站上举行的投票中被评为“年度最佳Roguelike游戏”。

图片6

顺便说一下,粉丝们可能会很高兴知道矮人要塞即将由两个有经验的改装人员添加的增强型32位图形加入Steam。 高级版本还将获得其他音乐曲目和Steam Workshop支持。 付费副本的所有者可以根据需要切换到旧的ASCII图形。 更多

覆盖赋值运算符


示例5、6:

这里有几个有趣的警告。

V690'JsonObject'类实现了副本构造函数,但缺少'='运算符。 使用这样的类是危险的。 json.h 647

 class JsonObject { private: .... JsonIn *jsin; .... public: JsonObject( JsonIn &jsin ); JsonObject( const JsonObject &jsobj ); JsonObject() : positions(), start( 0 ), end( 0 ), jsin( NULL ) {} ~JsonObject() { finish(); } void finish(); // moves the stream to the end of the object .... void JsonObject::finish() { .... } .... } 

此类具有复制构造函数和析构函数,但不覆盖赋值运算符。 问题在于,自动生成的赋值运算符只能将指针分配给JsonIn 。 结果,类JsonObject的两个对象都将指向相同的JsonIn 。 我不能肯定地说这种情况在当前版本中是否会发生,但是肯定有一天会有人陷入这种陷阱。

下一个类也有类似的问题。

V690'JsonArray'类实现了复制构造函数,但缺少'='运算符。 使用这样的类是危险的。 json.h 820

 class JsonArray { private: .... JsonIn *jsin; .... public: JsonArray( JsonIn &jsin ); JsonArray( const JsonArray &jsarr ); JsonArray() : positions(), ...., jsin( NULL ) {}; ~JsonArray() { finish(); } void finish(); // move the stream position to the end of the array void JsonArray::finish() { .... } } 

在文章“ 两大法则 ”中详细说明了不覆盖复杂类中的赋值运算符的危险。

示例7、8:

这两个还处理赋值运算符重载,但是这次是它的特定实现。

V794应该保护赋值运算符免受'this ==&other'的影响。 mattack_common.h 49

 class StringRef { public: .... private: friend struct StringRefTestAccess; char const* m_start; size_type m_size; char* m_data = nullptr; .... auto operator = ( StringRef const &other ) noexcept -> StringRef& { delete[] m_data; m_data = nullptr; m_start = other.m_start; m_size = other.m_size; return *this; } 

此实现无法防止潜在的自我分配,这是不安全的做法。 也就是说,将*此引用传递给此运算符可能会导致内存泄漏。

这是一个具有异常副作用的不正确覆盖的赋值运算符的类似示例:

V794应该保护赋值运算符免受'this ==&rhs'的影响。 player_activity.cpp 38

 player_activity &player_activity::operator=( const player_activity &rhs ) { type = rhs.type; .... targets.clear(); targets.reserve( rhs.targets.size() ); std::transform( rhs.targets.begin(), rhs.targets.end(), std::back_inserter( targets ), []( const item_location & e ) { return e.clone(); } ); return *this; } 

该代码也没有检查自我分配,此外,它还有一个要填充的向量。 使用赋值运算符的这种实现,向其自身分配对象将导致目标字段中的向量加倍,并且其中某些元素会损坏。 但是,在transform之前先加上clear ,这将清除对象的向量,从而导致数据丢失。

图片3

第三题


2008年,流氓分子甚至以史诗般的标题“柏林解释”获得了正式的定义。 据此,所有此类游戏都具有以下要素:

  • 随机生成的世界,提高了可重玩性;
  • Permadeath:如果你的角色去世,他们就永远死了,所有的物品都会丢失;
  • 回合制游戏:任何变化仅与玩家的动作一起发生; 时间流暂停,直到玩家执行动作为止;
  • 生存:资源匮乏。

最后,roguelike的最重要特征主要集中在探索世界,寻找物品的新用途以及地下城爬行。

在《 大地裂变》 DDA中 ,您的角色经常结冰,挨饿,口渴,最重要的是,用两只触手代替两只脚,这是很常见的情况。

图片15

细节很重要


范例9:

V1028可能的溢出。 考虑将“开始+较大”运算符的操作数强制转换为“ size_t”类型,而不是结果。 worldfactory.cpp 638

 void worldfactory::draw_mod_list( int &start, .... ) { .... int larger = ....; unsigned int iNum = ....; .... for( .... ) { if( iNum >= static_cast<size_t>( start ) && iNum < static_cast<size_t>( start + larger ) ) { .... } .... } .... } 

程序员似乎想采取预防措施以防溢出。 但是,提高总和的类型不会有任何区别,因为在此之前,溢出会在加值的步骤中发生,并且升值将在毫无意义的值上进行。 为避免这种情况,应仅将其中一个参数强制转换为更广泛的类型: (static_cast <size_t>(start)+ large)

范例10:

V530需要使用功能“尺寸”的返回值。 worldfactory.cpp 1340

 bool worldfactory::world_need_lua_build( std::string world_name ) { #ifndef LUA .... #endif // Prevent unused var error when LUA and RELEASE enabled. world_name.size(); return false; } 

对于这种情况,有一个技巧。 如果最后得到一个未使用的变量,并且想取消编译器警告,则只需编写(void)world_name,而不是对该变量调用方法。

示例11:

V812性能下降 。 无效使用“计数”功能。 可以通过调用“ find”函数代替它。 播放器.cpp 9600

 bool player::read( int inventory_position, const bool continuous ) { .... player_activity activity; if( !continuous || !std::all_of( learners.begin(), learners.end(), [&]( std::pair<npc *, std::string> elem ) { return std::count( activity.values.begin(), activity.values.end(), elem.first->getID() ) != 0; } ) { .... } .... } 

count与零进行比较的事实表明,程序员希望找出活动是否包含至少一个必需的元素。 但是count必须遍历整个容器,因为它对元素的所有出现进行计数。 使用find可以更快地完成工作,一旦找到第一个事件,它将停止。

示例12:

如果您知道有关char类型的一个棘手的细节,就很容易找到此错误。

V739 EOF不应与“ char”类型的值进行比较。 “ ch”应为“ int”类型。 json.cpp 762

 void JsonIn::skip_separator() { signed char ch; .... if (ch == ',') { if( ate_separator ) { .... } .... } else if (ch == EOF) { .... } 

图片13

除非您知道EOF定义为-1,否则这是您不容易发现的错误之一。 因此,将其与signed char类型的变量进行比较时,在几乎每种情况下,条件的评估结果均为false 。 唯一的例外是代码为0xFF(255)的字符。 在比较中使用时,它将变为-1,从而使条件成立。

示例13:

这个小错误可能有一天会变得至关重要。 毕竟,有充分的理由在CWE列表中找到CWE-834 。 请注意,该项目已触发此警告五次。

V663可能出现无限循环。 'cin.eof()'条件不足以使它脱离循环。 考虑将'cin.fail()'函数调用添加到条件表达式中。 动作.cpp 46

 void parse_keymap( std::istream &keymap_txt, .... ) { while( !keymap_txt.eof() ) { .... } } 

如警告所述,仅从文件读取时检查EOF是不够的-您还必须通过调用cin.fail()来检查输入失败。 让我们修复代码以使其更安全:

 while( !keymap_txt.eof() ) { if(keymap_txt.fail()) { keymap_txt.clear(); keymap_txt.ignore(numeric_limits<streamsize>::max(),'\n'); break; } .... } 

keymap_txt.clear()的目的是在发生读取错误后清除流上的错误状态(标志),以便您可以读取其余文本。 使用参数numeric_limits < streamsize > :: max()和换行符调用keymap_txt.ignore可以跳过字符串的其余部分。

有一种更简单的方法来停止读取:

 while( !keymap_txt ) { .... } 

当置于逻辑上下文中时,流将自身转换为等于true的值,直到达到EOF

第四题


我们时代最流行的与roguelike相关的游戏结合了原始roguelike和其他类型的元素,例如平台游戏,策略等。 这样的游戏已被称为“类rogue”或“ roguelite”。 其中包括著名的标题,如《 不要挨饿》《以撒的结合》 ,《 FTL:比光明快》 ,《 黑暗地牢》甚至《 暗黑破坏神》

但是,roguelike和roguelite之间的区别有时可能很小,以至于您无法确定游戏所属的类别。 有些人认为矮人要塞从严格意义上讲不是类盗贼,而另一些人则认为暗黑破坏神是一款经典的类盗贼游戏。

图片1

结论


即使该项目被证明总体上是高质量的,只有很少的严重缺陷,但这并不意味着它不能进行静态分析。 静态分析的功能是常规使用,而不是像我们为普及所做的一次性检查。 如果定期使用静态分析器,则可以在开发的最早阶段帮助您检测错误,从而使它们的修复成本更低。 计算示例

图片2

该游戏仍在不断开发中,活跃的modder社区正在为此进行开发。 顺便说一句,它已被移植到多个平台,包括iOS和Android。 因此,如果您有兴趣,请尝试一下!

Source: https://habr.com/ru/post/zh-CN449488/


All Articles