أيام مأساوية مأساوية: التحليل الثابت وألعاب Roguelike

الصورة 5

يجب أن تكون قد خمنت بالفعل من العنوان الذي يركز مقال اليوم على الأخطاء في شفرة مصدر البرنامج. لكن ليس هذا فقط. إذا لم تكن مهتمًا فقط بـ C ++ وبالقراءة عن الأخطاء في كود المطورين الآخرين ولكن أيضًا حفر ألعاب فيديو غير عادية وتساءل عن ماهية "roguelikes" وكيف تلعبها ، فمرحبا بك في القراءة!

أثناء البحث عن ألعاب غير عادية ، تعثرت على Cataclysm Dark Days Ahead ، والتي تبرز من بين ألعاب أخرى بفضل رسوماتها المستندة إلى أحرف ASCII ذات الألوان المختلفة مرتبة على خلفية سوداء.

أحد الأشياء التي تذهلك حول هذه اللعبة وغيرها من الألعاب المشابهة هو مقدار الوظائف المدمجة فيها. على سبيل المثال في Cataclysm ، على سبيل المثال ، لا يمكنك حتى إنشاء شخصية دون الشعور بالحاجة إلى google لبعض الأدلة بسبب العشرات من المعلمات والسمات والسيناريوهات الأولية المتاحة ، ناهيك عن الاختلافات المتعددة للأحداث التي تحدث في جميع أنحاء اللعبة.

نظرًا لأنها لعبة ذات شفرة مفتوحة المصدر ، وكتبت في C ++ ، لم نتمكن من السير دون التحقق من ذلك باستخدام محلل الكود الثابت PVS-Studio ، الذي اشتركت فيه بنشاط. رمز المشروع عالي الجودة بشكل مدهش ، لكنه لا يزال يحتوي على بعض العيوب الطفيفة ، والتي سأتحدث عنها في هذا المقال.

تم التحقق من الكثير من الألعاب باستخدام PVS-Studio بالفعل. يمكنك العثور على بعض الأمثلة في مقالتنا " التحليل الثابت في تطوير ألعاب الفيديو: أهم 10 أخطاء برمجية ."

منطق


مثال 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 يمكن تبسيط عملية التحقق المفرطة. The '(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 .... } .... } 

هذا الشرط صحيح منطقيا ، لكنه معقد للغاية. كل من كتب هذا الكود كان يجب أن يشفق على زملائه من المبرمجين الذين سيحتفظون به. يمكن إعادة كتابته بشكل أبسط: إذا (left_fav == right_fav) .

خطأ مشابه آخر:

  • V728 يمكن تبسيط عملية التحقق المفرطة. The '(A &&! B) || تعبير (! A && B) 'يعادل التعبير' bool (A)! = Bool (B) '. iuse_actor.cpp 2653

استطرادا


لقد فوجئت باكتشاف أن الألعاب التي تدور باسم "roguelikes" اليوم ليست سوى ممثلين أكثر اعتدالًا من النوع القديم من ألعاب roguelike. بدأ كل شيء مع لعبة عبادة Rogue لعام 1980 ، والتي ألهمت العديد من الطلاب والمبرمجين لإنشاء ألعابهم الخاصة مع عناصر مماثلة. أعتقد أن الكثير من التأثير جاء أيضا من مجتمع لعبة الطاولة 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". 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 انخفاض الأداء. ربما يجب تقديم حجة "الرسالة" كمرجع ثابت. json.cpp 1452
  • V813 انخفاض الأداء. ربما يجب تقديم حجة '' كمرجع ثابت. catacharset.cpp 218
  • وهلم جرا ...

الاستطراد الثاني


بعض ألعاب roguelike الكلاسيكية لا تزال قيد التطوير النشط. إذا قمت بفحص مستودعات GitHub في Cataclysm DDA أو NetHack ، فسترى أن التغييرات يتم تقديمها يوميًا. NetHack هي أقدم لعبة لا تزال قيد التطوير: تم إصدارها في يوليو 1987 ، ويعود الإصدار الأخير إلى عام 2018.

تعتبر Dwarf Fortress واحدة من أكثر الألعاب شعبية - وإن كانت أصغر سنا. بدأ التطوير في عام 2002 وتم إصدار الإصدار الأول في عام 2006. ويعكس شعاره "الخسارة متعة" حقيقة أنه من المستحيل الفوز في هذه اللعبة. في عام 2007 ، حصل Dwarf Fortress على جائزة "أفضل لعبة روغويليك للعام" من خلال التصويت الذي يُجرى سنويًا على موقع ASCII GAMES.

صورة 6

بالمناسبة ، قد يكون من دواعي سرور المشجعين معرفة أن Dwarf Fortress سيصل إلى Steam مع رسومات محسنة 32 بت يضيفها موحدان من ذوي الخبرة. سيحصل الإصدار المتميز أيضًا على مقاطع موسيقية إضافية ودعم 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 يجب حماية مشغل المهمة من حالة "هذا == & غيرها". 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; } 

لا يحتوي هذا الرمز على فحص مقابل التخصيص الذاتي أيضًا ، بالإضافة إلى أنه يحتوي على ناقل يجب ملؤه. مع هذا التطبيق لمشغل المهمة ، فإن تعيين كائن لنفسه سيؤدي إلى مضاعفة المتجه في حقل الأهداف ، مع تلف بعض العناصر. ومع ذلك ، يسبق التحويل مسح ، مما يؤدي إلى مسح ناقل الكائن ، مما يؤدي إلى فقدان البيانات.

الصورة 3

الاستطراد الثالث


في عام 2008 ، حصل roguelikes على تعريف رسمي معروف تحت العنوان الملحمي "Berlin Interpretation". وفقًا لذلك ، تشترك جميع هذه الألعاب في العناصر التالية:

  • العالم ولدت بشكل عشوائي ، مما يزيد من إمكانية إعادة التشغيل ؛
  • دائم: إذا مات شخصيتك ، يموتون للأبد ، وتضيع كل عناصرهم ؛
  • طريقة اللعب على أساس الدوران: تحدث أي تغييرات فقط جنبًا إلى جنب مع تصرفات اللاعب ؛ يتم تعليق تدفق الوقت حتى يقوم اللاعب بعمل ما ؛
  • البقاء على قيد الحياة: الموارد ضئيلة.

أخيرًا ، تركز ميزة roguelikes الأكثر أهمية على استكشاف العالم وإيجاد استخدامات جديدة للعناصر وزحف الأبراج المحصنة.

إنه وضع شائع في Cataclysm DDA ، بحيث ينتهي بك الأمر إلى أن تتجمد حتى العظم ، وتتضور جوعًا وعطشًا ، وفي المقام الأول ، تستبدل ساقيها بستة مخالب.

صورة 15

التفاصيل المهمة


مثال 9:

V1028 تجاوز السعة المحتملة. ضع في الاعتبار صب معاملات عامل التشغيل "start + أكبر" للنوع "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) + أكبر) .

مثال 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; } 

هناك خدعة واحدة لحالات مثل هذا. إذا انتهى بك الأمر إلى متغير غير مستخدم وترغب في منع تحذير المحول البرمجي ، ما عليك سوى كتابة (باطلة) 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; } ) { .... } .... } 

تشير حقيقة المقارنة بين صفر إلى أن المبرمج أراد معرفة ما إذا كان النشاط يحتوي على عنصر مطلوب واحد على الأقل. لكن العد يجب أن يمشي عبر الحاوية بأكملها لأنه يحسب كل تكرارات العنصر. يمكن إنجاز المهمة بشكل أسرع باستخدام 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. لذلك ، عند مقارنتها مع متغير من النوع 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 () هو مسح حالة الخطأ (علامة) على الدفق بعد حدوث خطأ في القراءة حتى تتمكن من قراءة بقية النص. استدعاء keymap_txt.ignore باستخدام المعلمات numeric_limits <streamsize> :: max () وحرف السطر الجديد يتيح لك تخطي الجزء المتبقي من السلسلة.

هناك طريقة أكثر بساطة لإيقاف القراءة:

 while( !keymap_txt ) { .... } 

عند وضعه في سياق منطقي ، سيحول الدفق نفسه إلى قيمة مكافئة للقيمة true حتى يتم الوصول إلى EOF .

الاستطراد الرابع


تجمع ألعاب roguelike الأكثر شيوعًا في عصرنا بين عناصر roguelikes الأصلية والأنواع الأخرى مثل المنصات والاستراتيجيات وما إلى ذلك. أصبحت هذه الألعاب تُعرف باسم "لعبة تشبه لعبة roguelike" أو "لعبة roguelite". من بين هذه الألقاب الشهيرة مثل " لا تجوع" ، " The Binding of Isaac" ، و " FTL: Faster Than Light" ، و " Darkest Dungeon" ، وحتى Diablo .

ومع ذلك ، قد يكون التمييز بين roguelike و roguelite ضئيلًا في بعض الأحيان بحيث لا يمكنك تحديد الفئة التي تنتمي إليها اللعبة. يجادل البعض بأن Dwarf Fortress ليست لعبة روغويلية بالمعنى الدقيق للكلمة ، بينما يعتقد البعض الآخر أن لعبة Diablo هي لعبة كلاسيكية من لعبة roguelike.

الصورة 1

استنتاج


على الرغم من أن المشروع أثبت أنه عالي الجودة بشكل عام ، مع وجود عيوب خطيرة قليلة فقط ، فإن هذا لا يعني أنه يمكن القيام به دون تحليل ثابت. قوة التحليل الثابت في الاستخدام المنتظم وليس الشيكات لمرة واحدة كتلك التي نقوم بها للترويج. عند استخدامها بانتظام ، تساعدك أجهزة التحليل الثابتة على اكتشاف الأخطاء في مرحلة التطوير الأولى ، وبالتالي تجعلها أرخص لإصلاحها. مثال الحسابات .

الصورة 2

لا تزال اللعبة قيد التطوير بشكل مكثف ، مع مجتمع نشط نشط يعمل عليها. بالمناسبة ، تم نقله إلى منصات متعددة ، بما في ذلك iOS و Android. لذا ، إذا كنت مهتمًا ، فجرّبها!

Source: https://habr.com/ru/post/ar449488/


All Articles