В «Осу!» Играй, Не Забывай Об Ошибках



В «осу!» играй, не забывай об ошибках

Приветствуем всех любителей экзотических и не очень ошибок кода.

Сегодня у нас на тестовом стенде PVS-Studio довольно редкий гость — игра на языке C#.

А именно – «осу!» Как обычно: ищем ошибки, думаем, играем.



Игра

Осу! — это музыкальная ритм-игра с открытым исходным кодом.

Судя по информации из игровой сайт – довольно популярен, так как зарегистрировано более 15 миллионов игроков.

Проект отличается свободным геймплеем, красочным оформлением с возможностью настройки карт, расширенными возможностями составления онлайн-рейтингов игроков, наличием мультиплеера и большим выбором музыкальных композиций.

Подробно описывать игру не буду; желающие легко найдут всю информацию в Интернете.

Например, здесь .

Меня больше интересует исходный код проекта, который доступен для скачивания с сайта GitHub .

Значительное количество коммитов (более 24 тысяч) в репозиторий сразу обращает на себя внимание, что говорит об активном развитии проекта, которое продолжается и по сей день (игра вышла в 2007 году, но работа, вероятно, началась раньше).

При этом исходного кода немного — 1813 файлов .

cs, содержащих 135 тысяч строк кода, не считая пустых.

В этом коде есть тесты, которые я обычно не рассматриваю в проверках.

Код теста содержится в 306 файлах .

cs и соответственно 25 тысячах строк кода, не считая пустых.

Это небольшой проект: для сравнения, C#-ядро анализатора PVS-Studio содержит около 300 тысяч строк кода.

Всего для проверки игры на ошибки я использовал нетестовые проекты, содержащие 1507 файлов исходного кода и 110 тысяч строк.

Однако результат меня несколько порадовал, так как было несколько интересных ошибок, о которых спешу вам рассказать.



Ошибки

В3001 Слева и справа от '||' имеются идентичные подвыражения 'result == HitResult.Perfect'.

оператор.

DrawableHoldNote.cs 266

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
   

protected override void CheckForResult(.

) { .

ApplyResult(r => { if (holdNote.hasBroken && (result == HitResult.Perfect || result == HitResult.Perfect)) result = HitResult.Good; .

}); }

Хороший пример программирования, ориентированного на копирование и вставку.

Юмористический термин, который недавно использовал (ввел) мой коллега Валерий Комаров в своей статье " Топ-10 ошибок в Java-проектах за 2019 год ".

Итак, две одинаковые проверки следуют одна за другой.

Одна из проверок, скорее всего, должна содержать другую константу перечисления.

Результат попадания :

public enum HitResult { None, Miss, Meh, Ok, Good, Great, Perfect, }

Какую константу надо было использовать или вторая проверка вообще не нужна? Вопросы, на которые может ответить только разработчик.

В любом случае была допущена ошибка, искажающая логику работы программы.

В3001 Слева и справа от оператора «&&» находятся идентичные подвыражения «family != GetFamilyString(TournamentTypeface.Aquatico)».

Турнирный шрифт.cs 64

public static string GetWeightString(string family, FontWeight weight) { .

if (weight == FontWeight.Regular && family != GetFamilyString(TournamentTypeface.Aquatico) && family != GetFamilyString(TournamentTypeface.Aquatico)) weightString = string.Empty; .

}

И снова копипаст. Я отформатировал код так, чтобы ошибку было легко увидеть.

В оригинальной версии все условие записывалось в одну строку.

Также сложно сказать, как можно исправить код. Передача ТурнирШрифт содержит одну константу:

public enum TournamentTypeface { Aquatico }

В условии дважды по ошибке использовалась переменная.

семья , но это не совсем так.

В3009 [CWE-393] Странно, что этот метод всегда возвращает одно и то же значение false. KeyCounterAction.cs 19

public bool OnPressed(T action, bool forwards) { if (!EqualityComparer<T>.

Default.Equals(action, Action)) return false; IsLit = true; if (forwards) Increment(); return false; }

Метод всегда будет возвращать ЛОЖЬ .

На наличие таких ошибок я обычно проверяю вызывающий код, так как часто просто нигде не используют возвращаемое значение, то и ошибки нет (кроме некрасивого стиля программирования).

В данном случае я наткнулся на следующий код:

public bool OnPressed(T action) => Target.Children .

OfType<KeyCounterAction<T>>() .

Any(c => c.OnPressed(action, Clock.Rate >= 0));

Как видите, результат, возвращаемый методом OnPressed , используется.

И поскольку он всегда ЛОЖЬ , то результат вызывающего абонента OnPressed тоже всегда будет ЛОЖЬ .

Думаю, вам стоит еще раз перепроверить этот код, так как велика вероятность ошибки.

Еще одна похожая ошибка:

  • V3009 [CWE-393] Странно, что этот метод всегда возвращает одно и то же значение false. KeyCounterAction.cs 30
В3042 [CWE-476] Возможное исключение NullReferenceException. '?.

' и '.

' операторы используются для доступа к членам объекта val.NewValue TournamentTeam.cs 41

public TournamentTeam() { Acronym.ValueChanged += val => { if (.

) FlagName.Value = val.NewValue.Length >= 2 // <= ? val.NewValue?.

Substring(0, 2).

ToUpper() : string.Empty; }; .

}

В состоянии оператора ?: с переменной val.NewValue работать небезопасно.

Анализатор сделал такой вывод, потому что далее в then-ветви для доступа к переменной используют безопасный вариант работы через оператор условного доступа.

val.NewValue?.

Подстрока(.

) .

Еще одна похожая ошибка:

  • V3042 [CWE-476] Возможное исключение NullReferenceException. '?.

    ' и '.

    ' операторы используются для доступа к членам объекта val.NewValue TournamentTeam.cs 48

В3042 [CWE-476] Возможное исключение NullReferenceException. '?.

' и '.

' операторы используются для доступа к членам объекта API SetupScreen.cs 77

private void reload() { .

new ActionableInfo { Label = "Current User", ButtonText = "Change Login", Action = () => { api.Logout(); // <= .

}, Value = api?.

LocalUser.Value.Username, .

}, .

} private class ActionableInfo : LabelledDrawable<Drawable> { .

public Action Action; .

}

Этот код менее понятен, но я думаю, что здесь все же есть ошибка.

Создайте объект типа ActionableInfo .

Поле Действие инициализируется лямбдой, в теле которой небезопасно работать с потенциально нулевой ссылкой API .

Анализатор посчитал этот паттерн ошибкой, так как в дальнейшем при инициализации параметра Ценить с переменной API работать безопасно.

Ошибку я назвал неоднозначной, поскольку лямбда-код предполагает отложенное выполнение и тогда, возможно, разработчик каким-то образом гарантирует ненулевое значение ссылки API .

Но это лишь предположение, так как тело лямбды не содержит никаких признаков безопасной работы со ссылкой (предварительные проверки, например).

В3066 [CWE-683] Возможно, неправильный порядок аргументов, передаваемых в метод Atan2: «diff.X» и «diff.Y».

SliderBall.cs 182

public void UpdateProgress(double completionProgress) { .

Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI); .

}

Анализатор заподозрил, что при работе с методом Атан2 сорт Математика Разработчик перепутал порядок аргументов.

Объявление Атан2 :

// Parameters: // y: // The y coordinate of a point. // // x: // The x coordinate of a point. public static double Atan2(double y, double x);

Как видите, значения были переданы в обратном порядке.

Не берусь судить, ошибка ли это, поскольку метод ОбновлениеПрогресс содержит довольно много нетривиальных вычислений.

Просто отмечу факт возможной проблемы в коде.

В3080 [CWE-476] Возможное разыменование нуля.

Рассмотрите возможность проверки «Beatmap».

РабочийBeatmap.cs 57

protected virtual Track GetVirtualTrack() { .

var lastObject = Beatmap.HitObjects.LastOrDefault(); .

}

Анализатор указал на опасность доступа по нулевой ссылке Карта битов .

Вот что это такое:

public IBeatmap Beatmap { get { try { return LoadBeatmapAsync().

Result; } catch (TaskCanceledException) { return null; } } }

Что ж, анализатор прав.

Подробнее о том, как PVS-Studio находит подобные ошибки, а также о нововведениях в C# 8.0, связанных с подобной тематикой (работа с потенциально нулевыми ссылками), можно прочитать в статье " Ссылочные типы, допускающие значение NULL, в C# 8.0 и статический анализ ".

В3083 [CWE-367] Возможен небезопасный вызов события ObjectConverted, NullReferenceException. Рассмотрите возможность присвоения события локальной переменной перед ее вызовом.

BeatmapConverter.cs 82

private List<T> convertHitObjects(.

) { .

if (ObjectConverted != null) { converted = converted.ToList(); ObjectConverted.Invoke(obj, converted); } .

}

Некритичная и довольно распространенная ошибка.

Между проверкой события на равенство нулевой и при его вызове событие может быть отписано, что приведет к сбою программы.

Одно из возможных исправлений:

private List<T> convertHitObjects(.

) { .

converted = converted.ToList(); ObjectConverted?.

Invoke(obj, converted); .

}

В3095 [CWE-476] Объект «столбцы» использовался до того, как было проверено его соответствие нулю.

Проверить строки: 141, 142. SquareGraph.cs 141

private void redrawProgress() { for (int i = 0; i < ColumnCount; i++) columns[i].

State = i <= progress ? ColumnState.Lit : ColumnState.Dimmed; columns?.

ForceRedraw(); }

Обход коллекции столбцы цикл производится небезопасно.

При этом разработчик предположил, что ссылка столбцы может быть нулевым, поскольку далее в коде для доступа к коллекции используется оператор условного доступа.

В3119 Вызов переопределенного события OnNewResult может привести к непредсказуемому поведению.

Рассмотрите возможность явной реализации методов доступа к событиям или используйте ключевое слово «sealed».

DrawableRuleset.cs 256

private void addHitObject(TObject hitObject) { .

drawableObject.OnNewResult += (_, r) => OnNewResult?.

Invoke(r); .

} public override event Action<JudgementResult> OnNewResult;

Анализатор предупреждает об опасности использования переопределенного или виртуального события.

В чем именно заключается опасность – предлагаю прочитать в описание к диагностике.

Также в свое время я написал статью на эту тему» Виртуальные события в C#: что-то пошло не так ".

Еще одна похожая небезопасная конструкция в коде:

  • V3119 Вызов переопределенного события может привести к непредсказуемому поведению.

    Рассмотрите возможность явной реализации методов доступа к событиям или используйте ключевое слово «sealed».

    DrawableRuleset.cs 257

В3123 [CWE-783] Возможно, '??' оператор работает не так, как ожидалось.

Его приоритет ниже приоритета других операторов в левой части.

OsuScreenStack.cs 45

private void onScreenChange(IScreen prev, IScreen next) { parallaxContainer.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT * ((IOsuScreen)next)?.

BackgroundParallaxAmount ?? 1.0f; }

Чтобы лучше понять проблему, приведу синтетический пример того, как работает код на данный момент:

x = (c * a) ?? b;

Ошибка произошла потому, что оператор "*" имеет более высокий приоритет, чем оператор "??" оператор.

Исправленная версия кода (добавлены скобки):

private void onScreenChange(IScreen prev, IScreen next) { parallaxContainer.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT * (((IOsuScreen)next)?.

BackgroundParallaxAmount ?? 1.0f); }

Еще одна аналогичная ошибка в коде: В3123 [CWE-783] Возможно, '??' оператор работает не так, как ожидалось.

Его приоритет ниже приоритета других операторов в левой части.

FramedReplayInputHandler.cs 103

private bool inImportantSection { get { .

return IsImportant(frame) && Math.Abs(CurrentTime - NextFrame?.

Time ?? 0) <= AllowedImportantTimeSpan; } }

Здесь, как и в предыдущем фрагменте кода, не учитывался приоритет операторов.

Теперь выражение передано в метод Математика.

Абс , рассчитывается следующим образом:

(a – b) ?? 0

Исправленный код:

private bool inImportantSection { get { .

return IsImportant(frame) && Math.Abs(CurrentTime – (NextFrame?.

Time ?? 0)) <= AllowedImportantTimeSpan; } }

В3142 [CWE-561] Обнаружен недостижимый код. Вполне возможно, что ошибка присутствует. DrawableHoldNote.cs 214

public override bool OnPressed(ManiaAction action) { if (!base.OnPressed(action)) return false; if (Result.Type == HitResult.Miss) // <= holdNote.hasBroken = true; .

}

Анализатор утверждает, что код обработчика OnPressed , начиная со второго оператора если , недоступен.

Это следует из предположения, что первое условие всегда верно, т. е.

метода base.OnPressed всегда вернусь ЛОЖЬ .

Давайте посмотрим на метод base.OnPressed :

public virtual bool OnPressed(ManiaAction action) { if (action != Action.Value) return false; return UpdateResult(true); }

Перейдем к методу Обновитьрезультат :

protected bool UpdateResult(bool userTriggered) { if (Time.Elapsed < 0) return false; if (Judged) return false; .

return Judged; }

Обратите внимание на реализацию свойства Судимый здесь не важно, так как по логике метода Обновитьрезультат отсюда следует, что последний оператор возвращаться эквивалентно этому:

return false;

Итак, метод Обновитьрезультат всегда вернусь ЛОЖЬ , что приведет к ошибке с недоступным кодом в коде выше стека.

В3146 [CWE-476] Возможное нулевое разыменование «набора правил».

«FirstOrDefault» может возвращать нулевое значение по умолчанию.

APILegacyScoreInfo.cs 24

public ScoreInfo CreateScoreInfo(RulesetStore rulesets) { var ruleset = rulesets.GetRuleset(OnlineRulesetID); var mods = Mods != null ? ruleset.CreateInstance() // <= .

GetAllMods().

Where(.

) .

ToArray() : Array.Empty<Mod>(); .

}

Анализатор считает вызов небезопасным набор правил.

CreateInstance() .

Переменная набор правил ранее получает значение в результате вызова Получить набор правил :

public RulesetInfo GetRuleset(int id) => AvailableRulesets.FirstOrDefault(.

);

Как видим, предупреждение анализатора оправдано, поскольку в цепочке вызовов есть ФёрстОрдефолт , который может возвращать значение нулевой .



Заключение

В общем, игровой проект «осу!» Порадовало небольшое количество ошибок.

Однако рекомендую разработчикам обратить внимание на обнаруженные проблемы.

И пусть игра и дальше радует своих поклонников.

А для любителей поковыряться в коде, напоминаю, хорошим подспорьем станет анализатор PVS-Studio, который легко скачать с официального сайта.

Также отмечу, что разовые проверки проекта, подобные описанной выше, не имеют ничего общего с использованием статического анализатора в реальной работе.

Максимальной эффективности в борьбе с ошибками можно добиться только при регулярном использовании инструмента как на сервере сборки, так и непосредственно на компьютере разработчика (инкрементный анализ).

В этом случае задача-максимум — вообще не допустить попадания ошибок в систему контроля версий, исправляя дефекты уже на этапе написания кода.

Удачи и творческих успехов!

Ссылки

Это первая публикация 2020 года.

Пользуясь случаем, приведу ссылки на статьи о проверке C#-проектов за последний год:



В «осу!» играй, не забывай об ошибках

Если вы хотите поделиться этой статьей с англоязычной аудиторией, воспользуйтесь ссылкой для перевода: Сергей Хренов.

Играйте в «осу!», но следите за ошибками.

.

Теги: #Разработка игр #C++ #.

NET #Visual Studio #pvs-studio #osu!

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.