大灾变黑暗日子:静态分析和Roguelike游戏

27 篇文章 0 订阅

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

图5
在寻找不寻常的游戏时,我偶然发现了大灾变黑暗日子,由于它的图形基于黑色背景上排列的各种颜色的ASCII字符,因此在其他游戏中脱颖而出。

令人惊讶的是,这个和其他类似游戏让你感到惊讶的是它们内置了多少功能。例如,特别是在大灾变中,由于可用的数十个参数,特征和初始场景,你甚至无法创建一个没有谷歌某些指南的冲动的角色,更不用说整个游戏中发生的事件的多种变化。

由于它是一个开源代码的游戏,而且是一个用C ++编写的游戏,我们无法使用我们的静态代码分析器PVS-Studio进行检查,我正在积极参与其中。该项目的代码令人惊讶的高质量,但它仍然有一些小缺陷,其中一些我将在本文中讨论。

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

逻辑

例1:

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

V501 “||”的左侧和右侧有相同的子表达式 operator: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 (dur))’。player_hardcoded_effects.cpp 547

在这里插入图片描述

例2:

V728可以简化过度检查。’(A && B)|| (!A &&!B)'表达式等同于’bool(A)== bool(B)'表达式。inventory_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”为名的游戏只是roguelike游戏旧类型的温和代表。这一切都始于1980年的狂热游戏Rogue,它激发了许多学生和程序员用类似元素创建自己的游戏。我想很多影响力也来自桌面游戏DnD社区及其变化。

在这里插入图片描述

微优化

例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’。input.cpp 691
  • V801性能下降。最好将第五个函数参数重新定义为引用。考虑将’const color’替换为’const …&color’。output.h 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 等等…

题外话题II

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

矮人要塞是该流派中最受欢迎的游戏之一。开发始于2002年,第一个版本于2006年发布。它的座右铭“失去乐趣”反映了这个游戏不可能获胜的事实。2007年,Dwarf Fortress通过每年在ASCII GAMES网站上举行的投票获得“年度最佳Roguelike游戏”。

图6
顺便说一句,粉丝们可能很高兴知道Dwarf Fortress将使用由两位经验丰富的模组添加的增强型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;
}

此代码也没有检查自我赋值,此外,它还有一个要填充的向量。通过赋值运算符的这种实现,将对象分配给自身将导致目标字段中的向量加倍,其中一些元素被破坏。但是,变换前面是clear,它将清除对象的向量,从而导致数据丢失。

图3

题外话题III

2008年,roguelikes甚至得到了一个正式的定义,名为“柏林解释”。据此,所有这些游戏都有以下几个要素:

  • 随机生成的世界,增加了可重玩性;
  • Permadeath:如果你的角色死了,他们会永远死去,他们所有的物品都会丢失;
  • 回合制游戏:任何改变都只与玩家的行为一起发生; 时间流程暂停,直到玩家执行某个动作;
  • 生存:资源不足。

最后,roguelikes最重要的特征主要集中在探索世界,寻找物品的新用途和地牢爬行。

这是Cataclysm DDA中常见的情况,因为你的角色最终被冷冻到骨头,饥饿,口渴,最重要的是,两条腿被六条触手取代。

图15

重要的细节

例9:

V1028可能溢出。考虑将’start + larger’运算符的操作数转换为’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)+ greater)。

例10:

V530需要使用函数’size’的返回值。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’函数来替换它。player.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必须遍历整个容器,因为它会计算所有元素的出现次数。使用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()'函数调用添加到条件表达式。action.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相关游戏结合了原始roguelikes和其他类型的元素,如平台游戏,策略等。这种游戏被称为“roguelike-like”或“roguelite”。其中包括“ 不要饿死”,“艾萨克的束缚”,“ FTL:比光更快”,“ 最黑暗的地下城”,甚至“ 暗黑破坏神”等着名作品。

然而,roguelike和roguelite之间的区别有时可能非常小,以至于你无法确定游戏属于哪个类别。有些人认为矮人堡垒不是严格意义上的roguelike,而其他人认为暗黑破坏神是经典roguelike游戏。

图片1

结论

尽管该项目通常被证明是高质量的,只有少数严重的缺陷,但并不意味着没有静态分析就能做到。静态分析的力量是经常使用的,而不是像我们为普及那样进行的一次性检查。如果经常使用,静态分析仪可以帮助您在最早的开发阶段检测错误,从而使它们更便于修复。示例计算。

图2
游戏仍然在不断发展,有一个活跃的modder社区正在努力。顺便说一句,它已被移植到多个平台,包括iOS和Android。所以,如果您有兴趣,请试一试!

作者:Victoria Khanieva
原文链接:https://www.viva64.com/en/b/0628/

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值