BearLibTerminal - псевдоконсольное окно для рогалика

Форум библиотеки BeaRLib

Модератор: Apromix

Аватара пользователя
Loinon
Сообщения: 43
Зарегистрирован: 19 июл 2016, 18:20

Re: BearLibTerminal - псевдоконсольное окно для рогалика

Сообщение Loinon » 03 ноя 2016, 16:57

Да, мой косяк :) Не в том месте изменял фильтр. Поставил в метод, который не вызывается. Сейчас все норм)
Стоит только захотеть...

Nyahjiaxx
Сообщения: 2
Зарегистрирован: 09 окт 2016, 22:43

Re: BearLibTerminal - псевдоконсольное окно для рогалика

Сообщение Nyahjiaxx » 13 ноя 2016, 00:08

Хочу стереть текст с экрана по нажатию на кнопку мышки.
Текст не стирается.
Я что-то неправильно делаю или внутрях что-то сломалось?

Win10x64
python3.5.2 + pip install bearlibterminal
visual studio 2015, dll взял x86
Скрытый текст: ПОКАЗАТЬ

Код: Выделить всё

#include "stdafx.h"
#include <iostream>
using namespace std;

#include "BearLibTerminal.h"
#pragma comment (lib, "BearLibTerminal.lib")
TERMINAL_TAKE_CARE_OF_WINMAIN;

int main()
{	
	terminal_open();
	terminal_refresh();
	terminal_print(0, 0, "Hello World!");
	while (true)
	{
		if (terminal_has_input())
		{
			int key = terminal_read();
			std::cout << key << std::endl;
			if (key == TK_CLOSE) { break; }
			if (key == TK_ESCAPE) { break; }
			if (key = TK_MOUSE_LEFT) { terminal_clear(); }
		}
		terminal_refresh();
	}
	terminal_close();
    return 0;
}

Код: Выделить всё

from bearlibterminal import terminal

terminal.open()
terminal.refresh()
terminal.print_(0,0, "Hello World!")
while True:
    if terminal.has_input():
        key = terminal.read()
        print(key)
        if key == terminal.TK_CLOSE:
            break
        if key == terminal.TK_ESCAPE:
            break
        if key == terminal.TK_MOUSE_LEFT:
            terminal.clear()
        if key == terminal.TK_MOUSE_RIGHT:
            terminal.clear()
    terminal.refresh()

terminal.close()

Аватара пользователя
Cfyz
Сообщения: 776
Зарегистрирован: 30 ноя 2006, 10:03
Откуда: Санкт-Петербург
Контактная информация:

Re: BearLibTerminal - псевдоконсольное окно для рогалика

Сообщение Cfyz » 13 ноя 2016, 11:38

Не очищается потому, что нажатие мыши приложением вовсе не регистрируется, так как мышь по умолчанию "выключена". Чтобы terminal_read() возвращал TK_MOUSE_xxx, надо включить это посредством:

Код: Выделить всё

terminal_set("input.filter=[keyboard, mouse]");
См. описание опции input.filter.

Еще мимоходом замечу, что выполнять terminal_read() только после проверки terminal_has_input() чревато 100% потреблением одного ядра CPU: когда ввода нет, приложение будет молотить одни terminal_refresh() без устали. В данных примерах этого не видно из-за включенной по умолчанию вертикальной синхронизации, но если в цикле будет выполняться больше работы или настройки у пользователя будут нестандартные, может выйти неаккуратно. Какого-то универсального варианта тут все равно нет, может так и задумано 60-80 раз в секунду перерисовывать, просто стоит иметь в виду и строить циклы соответствующе.
Пытается раскуклиться

Аватара пользователя
Apromix
Мастер
Сообщения: 1236
Зарегистрирован: 04 июл 2011, 10:44
Откуда: Украина, Черновцы
Контактная информация:

Re: BearLibTerminal - псевдоконсольное окно для рогалика

Сообщение Apromix » 14 ноя 2016, 12:35

Cfyz писал(а):выполнять terminal_read() только после проверки terminal_has_input() чревато 100% потреблением одного ядра CPU: когда ввода нет, приложение будет молотить одни terminal_refresh() без устали
А как лучше? Вот у меня вроде так же:
Скрытый текст: ПОКАЗАТЬ

Код: Выделить всё

program ForgottenSaga;

uses
  SysUtils,
  Engine in 'Engine.pas',
  BearLibTerminal in 'BearLibTerminal.pas',
  ForgottenSaga.Classes in '..\ForgottenSaga.Classes.pas',
  ForgottenSaga.Entities in '..\ForgottenSaga.Entities.pas',
  ForgottenSaga.Scenes in '..\ForgottenSaga.Scenes.pas';

var
  Key: Word = 0;
  Tick: Integer = 0;

begin
  terminal_open();
  Saga := TSaga.Create(TMap.Size.Width + TUI.PanelWidth, TMap.Size.Height);
  try
    Saga.Init;
    terminal_set(Format('window.title=%s', [__('Forgotten Saga')]));
    Saga.Stages.Render;
    terminal_refresh();
    repeat
      Saga.Stages.Render;
      Key := 0;
      if terminal_has_input() then
        Key := terminal_read();
      Saga.Stages.Update(Key);
      if (Tick > 59) then
      begin
        Saga.Stages.Timer;
        Tick := 0;
      end;
      // if (Key <> 0) then
      terminal_refresh();
      Inc(Tick);
      terminal_delay(1);
    until (Key = TK_CLOSE);
    terminal_close();
  finally
    Saga.Free;
  end;

end.

И еще: как вывести fps?

Аватара пользователя
Loinon
Сообщения: 43
Зарегистрирован: 19 июл 2016, 18:20

Re: BearLibTerminal - псевдоконсольное окно для рогалика

Сообщение Loinon » 14 ноя 2016, 21:54

Я считал фпс так: измерял время, необходимое для совершения одного игрового цикла и делил 1000 на это время.
На c# это позволяет класс Stopwatch. У меня получалось 62 кадра в секунду с vsync :)

Код: Выделить всё

Stopwatch sWatch = new Stopwatch();  
while (<someCondition>)
{
    sWatch.Start ();
    Input.Read ();
    Output.Refresh();
    sWatch.Stop();
    Console.WriteLine ((1000/sWatch.ElapsedMilliseconds).ToString());  
    sWatch.Reset();
}
Если говорить об отрисовке каждый цикл, то метод Output.Refresh() проверяет свой список List<Canvas> EnabledCanvases (Canvas - класс, содержащий списки GraphicTile'ов в двумерном массиве. В двух словах - полотно для интерфейса) на наличие изменений в его двумерном массиве из cписков GraphicTile'ов. Если изменения есть, то этот Canvas отправляется в output.Draw(Canvas cnvs). Там, пробегая по массиву GraphicTile'ов, он проверяет у каждый структуры GraphicTile флаг Changed и если он true, то тайл рисуется, при этом значение Changed выставляется в False. У этой же структуры есть поля с сеттерами для offset, charCode и color (каждый сеттер проверяет: если устанавливаемое значение отличается от нынешнего, то Changed также выставляется в true). Таким образом, терминал совершает ф-цию PutExt() только тогда, когда меняется GraphicTile. Ну, это упрощенно выглядит так. В коде же все несколько иначе... Если интересно, то:
Структура графического тайла.
Скрытый текст: ПОКАЗАТЬ

Код: Выделить всё

	struct GraphicTile
	{
		private int code;
		private Color color;
		private Point delta;

		public int Code
		{
			get
			{
				return code;
			}
			set
			{
				if (value != code)
				{
					code = value;
					Changed = true;
				}
			}
		}
		public Color Color
		{
			get
			{
				return color;
			}
			set
			{
				if (value != Color)
				{
					Changed = true;
					color = value;
				}
			}
		}
		public Point Delta
		{
			get
			{
				return delta;
			}
			set
			{
				if (value != delta)
				{
					delta = value;
					Changed = true;
				}
			}
		}
		public bool Changed { get; set; }

		public GraphicTile (int code = 0, Color color = default (Color),
								  Point delta = default (Point))
		{
			this.code = code;
			this.color = color;
			this.delta = delta;
			Changed = true;
		}
	}
Метод отрисовки.
Скрытый текст: ПОКАЗАТЬ

Код: Выделить всё

		private static int Draw (Canvas cnvs)
		{
			int tilesCount = 0;
			if (cnvs.Changed)
			{
				Terminal.Layer (cnvs.LastLayer);
				for (int x = 0; x < cnvs.Size.Width; x++)
					for (int y = 0; y < cnvs.Size.Height; y++)
					{
						bool changedList = false;

						foreach (GraphicTile tile in cnvs.MultiTiles[x, y])
							if (tile.Changed && !changedList)
								changedList = true;

						if (changedList)
						{
							for (int i = 0; i < cnvs.MultiTiles[x, y].Count; i++)
							{
								if (i == 0)
									Terminal.Composition (Terminal.TK_OFF);
								if (i > 0)
									Terminal.Composition (Terminal.TK_ON);

								Terminal.Color (cnvs.MultiTiles[x, y][i].Color);
								Terminal.PutExt (new Point (cnvs.AbsolutePosition.X + x, cnvs.AbsolutePosition.Y + y),
													  cnvs.MultiTiles[x, y][i].Delta,
													  cnvs.MultiTiles[x, y][i].Code);
								tilesCount++;

								GraphicTile temp = cnvs.MultiTiles[x, y][i];
								temp.Changed = false;
								cnvs.MultiTiles[x, y][i] = temp;
							}
						}
					}
			}
			return tilesCount;
		}
Ну и для полноты картины Canvas
Скрытый текст: ПОКАЗАТЬ

Код: Выделить всё

class Canvas: PhysicalObject, IVisible
	{
		public List<GraphicTile>[,] MultiTiles { get; private set; }
		public int LastLayer { get; set; }
		public Color Color
		{
			set
			{
				for (int x = 0; x < Size.Width; x++)
					for (int y = 0; y < Size.Height; y++)
						for (int i = 0; i < MultiTiles[x, y].Count; i++)
						{
							GraphicTile tile = MultiTiles[x, y][i];
							tile.Color = value;
							MultiTiles[x, y][i] = tile;
						}
			}
		}
		public bool Enabled
		{
			get
			{
				return Output.EnabledCanvases.Exists (x => x == this);
			}
			set
			{
				if (value && !Enabled)
				{
					Output.EnabledCanvases.Add (this);
					Refresh ();
				}
				else if (!value && Enabled)
				{
					Output.ClearArea (AbsolutePosition, Size, LastLayer);
					Output.EnabledCanvases.Remove (this);
				}
			}
		}
		public override Size Size
		{
			get
			{
				return base.Size;
			}
			set
			{
				//TODO: Добавить ресайз с копированием старого массива мультайлов в новый
				Output.ClearArea (AbsolutePosition, Size, LastLayer);
				base.Size = value;
				CreateMultitilesArr ();
			}
		}
		public override Point RelativePosition
		{
			get
			{
				return base.RelativePosition;
			}
			set
			{
				Output.ClearArea (AbsolutePosition, Size, LastLayer);
				base.RelativePosition = value;
				Refresh ();
			}
		}
		public bool Changed
		{
			get
			{
				foreach (List<GraphicTile> multiTile in MultiTiles)
					foreach (GraphicTile tile in multiTile)
						if (tile.Changed)
							return true;
				return false;
			}
		}

		public Canvas (PhysicalObject parent, Size size, int layer, bool enabled = false,
							Point relPos = default (Point))
			: base (parent, size, relPos)
		{
			LastLayer = layer;
			Moved += Move;
			CreateMultitilesArr ();
			if (enabled)
				Output.EnabledCanvases.Add (this);
		}

		private void CreateMultitilesArr ()
		{
			MultiTiles = new List<GraphicTile>[Size.Width, Size.Height];
			for (int x = 0; x < Size.Width; x++)
				for (int y = 0; y < Size.Height; y++)
					MultiTiles[x, y] = new List<GraphicTile> ();
		}
		private void Move (object source, EventArgs args)
		{
			MovedInfo Moveinfo = (args as MovedInfo);
			Output.ClearArea (Moveinfo.OldAbsPosition, Size, LastLayer);
			Refresh ();
		}

		public virtual void Fill (GraphicTile tile)
		{
			for (int x = 0; x < Size.Width; x++)
				for (int y = 0; y < Size.Height; y++)
					for (int i = 0; i < MultiTiles[x, y].Count; i++)
						MultiTiles[x, y][i] = tile;
		}
		public void Refresh ()
		{
			for (int x = 0; x < Size.Width; x++)
				for (int y = 0; y < Size.Height; y++)
					for (int i = 0; i < MultiTiles[x, y].Count; i++)
					{
						GraphicTile tile = MultiTiles[x, y][i];
						tile.Changed = true;
						MultiTiles[x, y][i] = tile;
					}
		}
		public void AddInnerLayer (int count = 1, GraphicTile tile = default (GraphicTile))
		{
			for (int x = 0; x < Size.Width; x++)
				for (int y = 0; y < Size.Height; y++)
				{
					for (int i = 0; i < count; i++)
						MultiTiles[x, y].Add (tile);
				}
		}
	}
Стоит только захотеть...

Аватара пользователя
Cfyz
Сообщения: 776
Зарегистрирован: 30 ноя 2006, 10:03
Откуда: Санкт-Петербург
Контактная информация:

Re: BearLibTerminal - псевдоконсольное окно для рогалика

Сообщение Cfyz » 14 ноя 2016, 22:39

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

В приведенном коде все в порядке: есть пауза в минимум 1 мс (т. е. уже не более 100 FPS) и, если я понял правильно, обсчет производится только раз в каждые 60 итераций. Но по традиции придраться хотя бы раз к каждому куску кода, могу предложить чуть другую формулировку таймера:

Код: Выделить всё

now := GetTickCount;
if now >= NextUpdateTime then begin
    Saga.Stages.Timer;
    NextUpdateTime := now + 60;
end;
Это отвязывает период обновления от флуктуаций длительности отрисовки и обсчета. И еще на мой взгляд выглядит более прямолинейно.

Хм, вообще-то можно переложить работу по выполнению небольшой паузы в конце цикла на библиотеку. Всего-то надо сделать, чтобы terminal_refresh() "досыпал" итерацию до нужного (разумеется, задаваемого) FPS.
Apromix писал(а):И еще: как вывести fps?
Вывести из имеющихся данных? Я чаще всего считаю фреймы, отрисованные/обработанные за секунду. NextFramerateUpdate := now + 1000, если по аналогии с кодом выше. Вывести на экран? Ну тут выбор широкий, можно аккуратно в заголовок окна приписать.
Loinon писал(а):Я считал фпс так: измерял время, необходимое для совершения одного игрового цикла и делил 1000 на это время.
Одна итерация дает большую погрешность (и чем выше скорость работы, тем больше скачет). Обычно отталкиваются от времени. Раз измеряется frames per second, то и считают количество фреймов, отрисованных за одну секунду. Это тоже не идеальный способ, но как правило хватает =).
Loinon писал(а):Если говорить об отрисовке каждый цикл, <...> Таким образом, терминал совершает ф-цию PutExt() только тогда, когда меняется GraphicTile.
Ну, тут речь шла больше про то, что если цикл не ограничивать, то он всегда будет что-то делать и съест CPU. А terminal_refresh() в текущем виде ничего не ограничивает.
Loinon писал(а):В коде же все несколько иначе... Если интересно, то
Ваша придирка такова:

Код: Выделить всё

bool changedList = cnvs.MultiTiles[x, y].Any(tile => tile.Changed);
C# же все-таки. Эх, нам бы в С++ такое.
Пытается раскуклиться

Аватара пользователя
Loinon
Сообщения: 43
Зарегистрирован: 19 июл 2016, 18:20

Re: BearLibTerminal - псевдоконсольное окно для рогалика

Сообщение Loinon » 15 ноя 2016, 00:31

Хорошая придирка : )) Вообще, весь код надо рефакторить у себя и закоментить...
Стоит только захотеть...

Аватара пользователя
Loinon
Сообщения: 43
Зарегистрирован: 19 июл 2016, 18:20

Re: BearLibTerminal - псевдоконсольное окно для рогалика

Сообщение Loinon » 15 ноя 2016, 02:22

Cfyz писал(а):Ну, тут речь шла больше про то, что если цикл не ограничивать, то он всегда будет что-то делать и съест CPU. А terminal_refresh() в текущем виде ничего не ограничивает.
Думаю, тогда можно сделать так, чтобы рефрешилось только при изменении хотя бы одного какого либо тайла в методе по отрисовке. В реалтайм игре тогда останется только Terminal.Read() после HasInput().
Последний раз редактировалось Loinon 20 ноя 2016, 21:29, всего редактировалось 3 раза.
Стоит только захотеть...

Аватара пользователя
Loinon
Сообщения: 43
Зарегистрирован: 19 июл 2016, 18:20

Re: BearLibTerminal - псевдоконсольное окно для рогалика

Сообщение Loinon » 15 ноя 2016, 03:03

Несколько размышлений по поводу враппера на с# есть.
Думаю, имеет смысл парсить в методе Terminal.Set () аргументы форматированной строки в понятный для либы формат. Чтобы можно было писать Terminal.Set("window.size = {0}", Settings.Default.ScreenSize). То же с булевым значением.
Скрытый текст: ПОКАЗАТЬ

Код: Выделить всё

for (int i = 0; i < args.Length; i++)
{
	if (args[i] is Bitmap)
	{
		Bitmap bitmap = args[i] as Bitmap;
		BitmapData data = bitmap.LockBits
		(
			new Rectangle (0, 0, bitmap.Width, bitmap.Height),
			ImageLockMode.ReadOnly,
			PixelFormat.Format32bppArgb
		);
		bitmaps[bitmap] = data;
		args[i] = string.Format ("0x{0:X}", (System.UInt64) data.Scan0.ToInt64 ());
	}
	if (args[i] is Size)
	{
		args[i] = string.Format ("{0}x{1}", ((Size) args[i]).Width, ((Size) args[i]).Height);
	}
	if (args[i] is bool)
	{
		args[i] = args[i].ToString ().ToLower ();
	}
}
То же самое с Terminal.Get ()...
Скрытый текст: ПОКАЗАТЬ

Код: Выделить всё

if (type == typeof (Size))
{
	string[] widthAndHeight = (result_str.Split ('x'));
	return (T) Activator.CreateInstance (typeof (Size), int.Parse (widthAndHeight[0]), int.Parse (widthAndHeight[1]));
}
Еще подстроил под себя некоторые методы, изменил формальные параметры с int x и int y на Point position:
Скрытый текст: ПОКАЗАТЬ

Код: Выделить всё

private static extern int Print (int x, int y, string text);
public static int Print (Point position, string text)
{
	return Print (position.X, position.Y, text);
}
Думаю, еще было бы здорово закомментировать XML-комментариями каждый метод кратким описанием.
Скрытый текст: ПОКАЗАТЬ

Код: Выделить всё

/// <summary>
/// Эта функция инициализирует библиотеку, применяя настройки по умолчанию. Вызов этой функции не выводит окно на экран, оно не будет показано до первого вызова функции refresh.
/// </summary>
/// <returns></returns>
DllImport [("BearLibTerminal.dll", EntryPoint = "terminal_open", CallingConvention = CallingConvention.Cdecl)]
public static extern bool Open ();
Изображение
Стоит только захотеть...

Аватара пользователя
Apromix
Мастер
Сообщения: 1236
Зарегистрирован: 04 июл 2011, 10:44
Откуда: Украина, Черновцы
Контактная информация:

Re: BearLibTerminal - псевдоконсольное окно для рогалика

Сообщение Apromix » 15 ноя 2016, 08:02

Loinon писал(а):Думаю, еще было бы здорово закомментировать XML-комментариями каждый метод кратким описанием.
Красиво выглядит на скрине, но придется держать 2 копии враппера на разных языках (я имею ввиду человечьи - русский и английский).

Аватара пользователя
Cfyz
Сообщения: 776
Зарегистрирован: 30 ноя 2006, 10:03
Откуда: Санкт-Петербург
Контактная информация:

Re: BearLibTerminal - псевдоконсольное окно для рогалика

Сообщение Cfyz » 09 янв 2017, 11:05

Апдейт 0.15.0

Основное изменение в том, что раньше функции print()/measure() принимали параметры области вывода и выравнивания довольно мутным образом, через теги форматирования:

Код: Выделить всё

terminal_printf(2, 1, "[bbox=%dx%d][align=center]%s", w, h, str);
Хуже того, они возвращали либо ширину, либо высоту выведенного текста в зависимости от наличия тего bbox.

Теперь print()/measure() принимают параметры обычным образом и всегда возвращают оба измерения строки:

Код: Выделить всё

y += terminal_print_ext(x, y, width, 0, TK_ALIGN_CENTER, message).height;
Тайлсетам был добавлен режим выравнивания 'dead-center', который заставляет игнорировать типографические характеристики символов. Это должно быть полезно для использования старых добрых ASCII в качестве элементов карты/уровня, так как они будут выровнены строго по центру:
Изображение

И прочие изменения-исправления:
* Параметры TrueType-шрифта 'use-box-drawing' и 'use-block-elements' заставят библиотеку использовать эти символы из шрифта.
* Кейпад теперь порождает символы и может быть использован во вводе, например read_str().
* Можно запросить номер версии библиотеки в виде строки посредством terminal_get("version").
* Поправлен маппинг первых 32 символов кодовой страницы CP437 (ранее они просто игнорировались, например в шрифтах libtcod).
* Добавлена поддержка растровых шрифтов в оттенках серого без полупрозрачности (например, шрифты libtcod).
* Установка шрифта без указания размера тайла более не работает (можно было все поломать).
* Небольшие улучшения во враппере C# (перегрузки функций и поддержка Size в функциях Set/Get).

Еще я слегка поменял расположение враппера для Python, теперь прилагается остов пакета (директория с __init__.py, setup.py, вот это все) вместо одного PyBearLibTerminal.py. Впрочем файл или целый пакет все так же можно скопировать себе в проект. См. README.md где есть немного про это (на английском). Копирование директории bearlibterminal лучше тем, что потом легко переключаться между пакетом и локальной копией без изменения кода (импорт идентичен).

Windows / Linux / OS X / PyPi
Пытается раскуклиться

ssalvador
Сообщения: 2
Зарегистрирован: 04 фев 2017, 11:54

Re: BearLibTerminal - псевдоконсольное окно для рогалика

Сообщение ssalvador » 04 фев 2017, 12:07

Добрый день.

Очень приятная библиотека, спасибо за проделанную работу. Планируется ли в ближайшем будущем доработать поддержку full screen для OS X? И есть ли у библиотеки возможность вернуть разрешение экрана, на котором запускается приложение?

Аватара пользователя
Cfyz
Сообщения: 776
Зарегистрирован: 30 ноя 2006, 10:03
Откуда: Санкт-Петербург
Контактная информация:

Re: BearLibTerminal - псевдоконсольное окно для рогалика

Сообщение Cfyz » 07 фев 2017, 13:36

ssalvador писал(а):Планируется ли в ближайшем будущем доработать поддержку full screen для OS X?
Да, точно будет, нужно только разобраться со всем этим что на моей корявой виртуальной машине само по себе задача >_<.
ssalvador писал(а):И есть ли у библиотеки возможность вернуть разрешение экрана, на котором запускается приложение?
Сейчас нет. Можно добавить пару соответствующих свойств для получения размера некоторого текущего/начального экрана (мне самому пригодится), но полноценная реализация не так проста в свете различных мультимониторных конфигураций.
Пытается раскуклиться

Аватара пользователя
Apromix
Мастер
Сообщения: 1236
Зарегистрирован: 04 июл 2011, 10:44
Откуда: Украина, Черновцы
Контактная информация:

Re: BearLibTerminal - псевдоконсольное окно для рогалика

Сообщение Apromix » 08 фев 2017, 12:32

А для Java планируется биндинг? Когда ждать его?

ssalvador
Сообщения: 2
Зарегистрирован: 04 фев 2017, 11:54

Re: BearLibTerminal - псевдоконсольное окно для рогалика

Сообщение ssalvador » 08 фев 2017, 19:00

Да, точно будет, нужно только разобраться со всем этим что на моей корявой виртуальной машине само по себе задача >_<.
Понял. Если будет рабочий билд, я могу погонять его на своем лаптопе.
Сейчас нет. Можно добавить пару соответствующих свойств для получения размера некоторого текущего/начального экрана (мне самому пригодится), но полноценная реализация не так проста в свете различных мультимониторных конфигураций.
Да, это было бы полезно. Пока что приходится выкручиваться с помощью SDL. Вручную писать кросс-платформенное определение я пока что способен осилить.

Ответить

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 13 гостей