Как создать систему воспроизведения

Итак, как мне создать систему воспроизведения?

Вы можете узнать это из определенных игр, таких как Warcraft 3 или Starcraft, где вы можете смотреть игру снова после того, как она уже была сыграна.

В итоге вы получите относительно небольшой файл повтора. Поэтому мои вопросы:

  • Как сохранить данные? (пользовательский формат?) (небольшой размер файла)
  • Что должно быть спасено?
  • Как сделать его общим, чтобы его можно было использовать в других играх для записи периода времени (а не полного соответствия, например)?
  • Позвольте перемотке вперед и назад (WC3 не мог перемотать назад, насколько я помню)
74 голоса | спросил daddz 30 22010vEurope/Moscow11bEurope/MoscowTue, 30 Nov 2010 19:47:26 +0300 2010, 19:47:26

11 ответов


38

В этой замечательной статье рассматриваются многие вопросы: http://www.gamasutra.com/view/feature/2029/developing_your_own_replay_system.php

Несколько вещей, которые статья упоминает и делает хорошо:

  • Ваша игра должна быть детерминированной.
  • он записывает начальное состояние игровых систем в первом кадре и только вход игрока во время игрового процесса.
  • квантовать входные данные для более низкого количества бит. То есть. представляют собой поплавки в различных диапазонах (например, [0, 1] или [-1, 1] в пределах меньших бит). Квантованные входы также должны быть получены в ходе игры.
  • используйте один бит, чтобы определить, имеет ли входной поток новые данные. Поскольку некоторые потоки не будут часто меняться, это приводит к временной когерентности во входе.

Одним из способов дальнейшего улучшения коэффициента сжатия для большинства случаев было бы развязать все ваши входные потоки и полностью запустить их кодировку самостоятельно. Это будет выигрыш по методу дельта-кодирования, если вы кодируете ваш запуск в 8 бит, а сам запуск превышает 8 кадров (очень вероятно, если ваша игра не станет реальной кнопкой). Я использовал эту технику в гоночной игре, чтобы сжать 8 минут входов от 2 игроков, пока они мчатся по дорожке до нескольких сотен байт.

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

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

ответил jpaver 30 22010vEurope/Moscow11bEurope/MoscowTue, 30 Nov 2010 23:20:42 +0300 2010, 23:20:42
22

Кроме того, что «убедитесь, что нажатие клавиш является воспроизводимым», что может быть удивительно сложно, вы можете просто записать все состояние игры на каждом кадре. С небольшим умным сжатием вы можете значительно сжать его. Именно так Braid обрабатывает свой код перемотки времени, и он работает очень хорошо.

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

ответил ZorbaTHut 30 22010vEurope/Moscow11bEurope/MoscowTue, 30 Nov 2010 20:25:34 +0300 2010, 20:25:34
21

Вы можете просмотреть свою систему, как если бы она состояла из ряда состояний и функций, где функция f [j] с входом x [j] изменяет system state s [j] в состояние s [j + 1], например:

s [j + 1] = f [j] (s [j], x [j])

Состояние - это объяснение всего мира. Расположение игрока, расположение противника, оценка, оставшиеся боеприпасы и т. Д. Все, что вам нужно, чтобы нарисовать рамку вашей игры.

Функция - это все, что может повлиять на мир. Смена кадра, нажатие клавиши, сетевой пакет.

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

В целях этого объяснения сделаю следующие предположения:

Предположение 1:

Количество состояний для заданного хода игры намного больше, чем количество функций. У вас, вероятно, сотни тысяч состояний, но всего несколько десятков функций (смена кадров, нажатия клавиш, сетевой пакет и т. Д.). Конечно, количество входов должно быть равно количеству состояний минус единица.

Предположение 2:

Пространственная стоимость (память, диск) для хранения одного состояния намного больше, чем сохранение функции и ее ввода.

Предположение 3:

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

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

Метод 1:

Сохранить s [0] ... s [n]. Это очень просто, очень просто. Из-за предположения 2 пространственная стоимость этого достаточно высока.

Для шахмат это будет достигнуто путем рисования всей доски для каждого хода.

Метод 2:

Если вам нужна только перемотка вперед, вы можете просто сохранить s [0], а затем сохранить f [0] ... f [n-1] ( помните, что это только имя идентификатора функции) и x [0] ... x [n-1] (каков был вход для каждой из этих функций). Чтобы воспроизвести, вы просто начинаете с s [0] и вычисляете

s [1] = f [0] (s [0], x [0])
s [2] = f [1] (s [1], x [1])

и т. д.

Я хочу сделать небольшую аннотацию здесь. Несколько других комментаторов сказали, что игра «должна быть детерминированной». Любой, кто говорит, что должен снова взять Computer Science 101, потому что, если ваша игра не предназначена для работы на квантовых компьютерах, ВСЕ КОМПЬЮТЕРНЫЕ ПРОГРАММЫ ДЕТЕРМИНИСТИЧЕСКИЕ. Вот что делает компьютеры такими замечательными.

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

Если вы используете псевдослучайные числа, вы можете либо сохранить сгенерированные числа как часть вашего ввода x, либо сохранить состояние функции prng как часть вашего состояния s и его реализация как часть функции f.

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

Способ 3:

Теперь вы, скорее всего, захотите найти свою игру. То есть вычислите s [n] для произвольного n. Используя метод 2, вам нужно вычислить s [0] ... s [n-1], прежде чем вы сможете рассчитать s [n], который, согласно предположению 2, может быть довольно медленным.

Чтобы реализовать это, метод 3 является обобщением методов 1 и 2: сохранить f [0] ... f [n-1] и x [0] ... x [n-1] как метод 2, но также сохраните s [j], для всех j% Q == 0 для данной константы < код> Q. В более простых условиях это означает, что вы сохраняете закладку в одном из состояний Q. Например, для Q == 100 вы сохраняете s [0], s [100], s [200] ...

Чтобы вычислить s [n] для произвольного n, вы сначала загрузите ранее сохраненный s [floor (n /Q)], а затем вычислить все функции из floor (n /Q) в n. В лучшем случае вы будете вычислять функции Q. Меньшие значения Q быстрее вычисляются, но потребляют гораздо больше места, тогда как более крупные значения Q потребляют меньше места, но занимают больше времени для вычисления.

Метод 3 с Q == 1 совпадает с методом 1, тогда как метод 3 с Q == inf аналогичен методу2.

Для шахмат это будет достигнуто путем рисования каждого шага, а также одного из каждых 10 досок (для Q == 10).

Способ 4:

Если вы хотите отменить повторное воспроизведение, вы можете сделать небольшое изменение метода 3. Предположим Q == 100, и вы хотите рассчитать s [150] через s [90] в обратном порядке. С помощью немодифицированного метода 3 вам нужно будет сделать 50 вычислений, чтобы получить s [150], а затем еще 49 вычислений для получения s [149] и т. Д. Но поскольку вы уже рассчитали s [149], чтобы получить s [150], вы можете создать кэш с помощью s [100] ... s [150], когда вы вычисляете s [150] в первый раз, а затем вы уже s [149] в кеше, когда вам нужно его отобразить.

Вам нужно только восстановить кеш каждый раз, когда вам нужно вычислить s [j], для j == (k * Q) -1 для любого заданного < код> к. На этот раз увеличение Q приведет к меньшему размеру (только для кеша), но более продолжительным (только для воссоздания кеша). Оптимальное значение для Q можно рассчитать, если вы знаете размеры и время, необходимые для вычисления состояний и функций.

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

Метод 5:

Если состояния просто потребляют слишком много места или функции потребляют слишком много времени, вы можете создать решение, которое фактически реализует (не подделки) обратное воспроизведение. Для этого вы должны создать обратные функции для каждой из функций, которые у вас есть. Однако для этого требуется, чтобы каждая из ваших функций была инъекцией. Если это выполнимо, то для f ', обозначающего обратную функцию f, вычисление s [j-1] выполняется так же просто, как

s [j-1] = f '[j-1] (s [j], x [j-1])

Обратите внимание, что здесь функция и ввод являются j-1, а не j. Эта же функция и вход будут теми, которые вы использовали бы, если бы вы рассчитывали

s [j] = f [j-1] (s [j-1], x [j-1])

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

Этот метод, как есть, может отменить вычисление s [j-1], но только если у вас есть s [j]. Это означает, что вы можете смотреть только воспроизведение назад, начиная с того момента, когда вы решили переиграть назад. Если вы хотите воспроизвести назад с произвольной точки, вы должны смешать это с методом 4.

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

Метод 6:

Наконец, если вы не можете гарантировать, что все ваши функции являются инъекциями, вы можете сделать небольшой трюк, чтобы сделать это. Вместо того, чтобы каждая функция возвращала только новое состояние, вы также можете вернуть данные, которые она отбрасывала, например:

s [j + 1], r [j] = f [j] (s [j], x [j])

Где r [j] - это отброшенные данные. А затем создайте свои обратные функции, чтобы они отбросили данные, например:

s [j] = f '[j] (s [j + 1], x [j], r [j])

Кроме f [j] и x [j], вы также должны хранить r [j] для каждой функции. Еще раз, если вы хотите, чтобы иметь возможность искать, вы должны хранить закладки, например, с помощью метода 4.

Для шахмат это будет то же самое, что и метод 2, но в отличие от метода 2, который говорит только, какая часть идет туда, вам также нужно сохранить, откуда взялась каждая часть.

Реализация:

Так как это работает для всех типов состояний, со всеми видами функций, для конкретной игры вы можете сделать несколько предположений, которые облегчат реализацию. Фактически, если вы реализуете метод 6 со всем игровым состоянием, вы не только сможете воспроизвести данные, но и вернуться во времени и возобновить игру с любого момента. Это было бы замечательно.

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

Я видел, как это реализовано в некоторых крупных играх, в основном для мгновенного воспроизведения последних данных, когда происходит событие (фрагмент в fps или оценка в спортивных играх).

Я надеюсь, что это объяснение не было слишком скучным.

¹ Это не означает, что действуют некоторые программыкак они были недетерминированными (например, MS Windows ^^). Теперь серьезно, если вы можете сделать недетерминированную программу на детерминированном компьютере, вы можете быть уверены, что одновременно выиграете медаль Fields, премию Тьюринга и, возможно, даже Оскар и Грэмми за все, что стоит.

ответил 10 FebruaryEurope/MoscowbThu, 10 Feb 2011 11:18:50 +0300000000amThu, 10 Feb 2011 11:18:50 +030011 2011, 11:18:50
8

Сохраните начальное состояние генераторов случайных чисел. Затем сохраняйте временные метки, каждый вход (мышь, клавиатура, сеть, что угодно). Если у вас есть сетевая игра, у вас, вероятно, уже есть все это на месте.

Повторно установите RNG и воспроизведите вход. Вот и все.

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

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

ответил 30 22010vEurope/Moscow11bEurope/MoscowTue, 30 Nov 2010 19:53:37 +0300 2010, 19:53:37
8

Одна вещь, еще не затронутая другими ответами, - это опасность поплавков. Вы не можете сделать полностью детерминированное приложение, используя float.

Используя float, вы можете иметь полностью детерминированную систему, но только если:

  • Используя точно такой же двоичный
  • Использование точно такого же CPU

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

Совершенно очевидно, как это повлияет на бит AMD против Intel - скажем, мы используем 80 бит-поплавков, а другой 64, например, но почему же такое бинарное требование?

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

Возможно, вы сможете помочь, установив _control87 () /_controlfp () , чтобы использовать максимально возможную точность. Однако некоторые библиотеки также могут коснуться этого (по крайней мере, некоторая версия d3d).

ответил Jari Komppa 9 FebruaryEurope/MoscowbWed, 09 Feb 2011 09:54:24 +0300000000amWed, 09 Feb 2011 09:54:24 +030011 2011, 09:54:24
4

Я проголосую против детерминированного переигровки. Это FAR проще и FAR меньше подвержено ошибкам, чтобы сохранить состояние каждой сущности каждые 1 /Nth секунды.

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

Измените кодировку. Используйте как можно меньше бит для всего. Повтор не должен быть идеальным, если он выглядит достаточно хорошо. Даже если вы используете float для, скажем, заголовка, вы можете сохранить его в байте и получить 256 возможных значений (точность 1.4º). Это может быть достаточно или даже слишком сильно для вашей конкретной проблемы.

Используйте дельта-кодирование. Если ваши объекты не телепортируются (и если они это делают, обрабатывайте случай отдельно), кодируйте позиции как разницу между новой позицией и старой позицией - для коротких движений вы можете уйти с гораздо меньшим количеством бит, чем вам нужно для полных позиций ,

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

Наконец, gzip все это:)

ответил ggambett 10 FebruaryEurope/MoscowbThu, 10 Feb 2011 12:16:59 +0300000000pmThu, 10 Feb 2011 12:16:59 +030011 2011, 12:16:59
2

Сложно. Сначала и прежде всего прочитайте ответы Яри Комппы.

Повтор, сделанный на моем компьютере, может не работать на вашем компьютере, потому что результат поплавка СЛЕГДА отличается. Это большое дело.

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

Чтобы перескакивать файлы (что намного сложнее), вам нужно сбросить ПАМЯТЬ. Например, где каждая единица, деньги, время проходит, все состояние игры. Затем быстрая пересылка, но воспроизведение всего, кроме пропусков рендеринга, звука и т. Д., Пока вы не дойдете до требуемого места назначения. Это может произойти каждую минуту или 5 минут в зависимости от того, насколько быстро он переправляется.

Основные моменты - Работа со случайными числами - Копирование ввода (проигрывателя (ов)) и удаленных игроков (ов)) - Состояние сброса для перескакивания файлов а также... - ИМЕЮЩИЙ ПЛАТЬЯ НЕ БЫТЬ ВЕЩИ (да, я должен был кричать)

ответил 9 FebruaryEurope/MoscowbWed, 09 Feb 2011 12:24:13 +0300000000pmWed, 09 Feb 2011 12:24:13 +030011 2011, 12:24:13
2

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

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

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

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

ответил Atiaxi 1 MarpmTue, 01 Mar 2011 13:56:27 +03002011-03-01T13:56:27+03:0001 2011, 13:56:27
0

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

Скорее всего, вам понадобится логика игры RNG и все остальное RNG для таких вещей, как GUI, эффекты частиц, звуки. Как только вы это сделаете, вам нужно записать начальное состояние игровой логики RNG, затем игровые команды всех игроков в каждом кадре.

Во многих играх существует уровень абстракции между входным и логическим логическим процессором, где вход преобразуется в команды. Например, нажатие кнопки A на контроллере приводит к тому, что цифровая команда «прыжок» установлена ​​на «истина», и игровая логика реагирует на команды, не проверяя контроллер напрямую. При этом вам нужно будет только записать команды, которые влияют на логику игры (нет необходимости записывать команду «Пауза»), и, скорее всего, эти данные будут меньше, чем запись данных контроллера. Вам также не нужно беспокоиться о записи состояния схемы управления в случае, если игрок решил переназначить кнопки.

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

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

Возможно, самая важная часть написания системы воспроизведения, которая опирается на детерминизм, - это запись отладочного потока данных. Этот поток отладки содержит моментальный снимок как можно больше информации о каждом кадре (семена RNG, преобразования сущностей, анимации и т. Д.) И возможность тестировать этот записанный отладочный поток на состояние игры во время повторов. Это позволит вам быстро сообщить о несоответствиях в конце любого кадра. Это позволит сэкономить бесчисленные часы, чтобы вытащить ваши волосы из неизвестных не детерминированных ошибок. Что-то простое, как неинициализированная переменная, все испортит в 11-й час.

ПРИМЕЧАНИЕ. Если ваша игра связана с динамической потоковой передачей контента или у вас есть логика игры на нескольких потоках или на разных ядрах ... удачи.

ответил Lathentar 9 FebruaryEurope/MoscowbWed, 09 Feb 2011 08:48:52 +0300000000amWed, 09 Feb 2011 08:48:52 +030011 2011, 08:48:52
0

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

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

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

Сохраненные данные - это список изменений.
Изменения могут быть сохранены в различных форматах (двоичный, xml, ...).
Изменение состоит из идентификатора объекта, имени свойства, старого значения, нового значения.

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

Пример:

  • время от начала = t1, entity = игрок 1, свойство = позиция, изменено с a на b
  • время от начала = t1, сущность = система, свойство = игровой режим, измененный с c на d
  • время от начала = t2, entity = player 2, свойство = состояние, изменено с e на f
  • Чтобы ускорить перемотку /ускоренную перемотку вперед или запись только определенных временных диапазонов,
    необходимы ключевые кадры - при записи все время, время от времени сохраняйте состояние всей игры.
    Если вы записываете только определенные временные диапазоны, вначале сохраняйте начальное состояние.

    ответил Danny Varod 28 FebruaryEurope/MoscowbMon, 28 Feb 2011 22:30:48 +0300000000pmMon, 28 Feb 2011 22:30:48 +030011 2011, 22:30:48
    -1

    Если вам нужны идеи о том, как реализовать свою систему воспроизведения, выполните поиск в google о том, как реализовать undo /redo в приложении. Для некоторых это может быть очевидно, но, возможно, и не для всех, что отмена /повтор концептуально совпадает с воспроизведением игр. Это просто особый случай, когда вы можете перемотать назад, и в зависимости от приложения, обратиться к определенному моменту времени.

    Вы увидите, что никто, выполняющий undo /redo, не жалуется на детерминированные /недетерминированные, плавающие переменные или определенные процессоры.

    ответил 4 MarpmFri, 04 Mar 2011 12:46:00 +03002011-03-04T12:46:00+03:0012 2011, 12:46:00

    Похожие вопросы

    Популярные теги

    security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132