Перейти к содержимому
Exeteres

TypeScript to Lua

Рекомендуемые сообщения

 

 

 

 

512px-TypeScript_Logo_(Blue).svg.png

 

 

 

 

Вместо вступления:

Я не считаю C-подобный синтаксис лучше синтаксиса lua и не буду заставлять вас переписывать все ваши программы на TypeScript! Я просто хочу поделится с вами альтернативой и рассказать про ее преимущества и недостатки.

 

# Что такое TypeScript?

TypeScript — язык программирования, представленный Microsoft в 2012 году и позиционируемый как средство разработки веб-приложений. Он создан для расширения JavaScript и он компилируется в JavaScript, но также существует инструмент для преобразования TypeScript кода в Lua. Вам может показаться, что этот транслятор крайне ограничен, но, поверьте мне, его возможности впечатляют.

 

# Почему его стоит попробовать?

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

Из-за большого размера контент каждого раздела будет скрыт под спойлер.

 

1. Статический анализ

Скрытый текст

Системы типов JavaScript и Lua во многом похожи, и потому они имеют некоторые общие проблемы, которые может решить TypeScript.

 

1.1 Проверка типов

Если функция или метод явно требует среди аргументов число, TypeScript не допустит что-то отличное от числа.

image.png.d5fc0b4b741592cf190d3a77c124c785.png

 

Lua тоже сообщит об этом, но только после запуска скрипта.

image.png.21ad2183b440c8b07a9b118bf2b45e19.png

 

1.2 Проверка существования полей и методов

Если вы попытаетесь обратится к несуществующему полю или методу, TypeScript сообщит вам об этом и даже поможет исправить, если вы, например, опечатались.

image.png.e6cf4d71e5b073e5d2595257259c12fa.png

 

В Lua мы узнаем об этом только после запуска скрипта.

image.png.610e5cdb29c1d11ea667e1c0587ecb49.png

 

2. Автодополнение

Скрытый текст

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

image.png.f2bb10226d369c05b792b6e659dfee3f.png

 

Или показать параметры, которые принимает метод.

image.png.e05fe44e605ca1ca6b43ba5ae2aa6ea8.png

 

Или текст документации.

image.png

 

3. ООП

Скрытый текст

3.1 Классы


class Player {
    username: string;

    constructor(username: string) {
        this.username = username;
    }

    greet() {
        print("Привет, " + this.username + "!");
    }
}

let alex = new Player("alex");
alex.greet(); // Привет, Alex!

 

Сгенерированный lua код

Скрытый текст


--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
Player = {}
Player.name = "Player"
Player.__index = Player
Player.prototype = {}
Player.prototype.__index = Player.prototype
Player.prototype.constructor = Player
function Player.new(...)
    local self = setmetatable({}, Player.prototype)
    self:____constructor(...)
    return self
end
function Player.prototype.____constructor(self, username)
    self.username = username
end
function Player.prototype.greet(self)
    print("Привет, " .. tostring(self.username) .. "!")
end
local alex = Player.new("alex")
alex:greet()

 

 

3.2 Модификаторы доступа

image.png

 

3.3 Наследование


class Player {
    username: string;

    constructor(username: string) {
        this.username = username;
    }
}

class Alex extends Player {
    constructor() {
        super("Alex");
    }
}

let alex = new Alex();
print(alex.username); // Alex

 

Сгенерированный lua код

Скрытый текст


--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
Player = {}
Player.name = "Player"
Player.__index = Player
Player.prototype = {}
Player.prototype.__index = Player.prototype
Player.prototype.constructor = Player
function Player.new(...)
    local self = setmetatable({}, Player.prototype)
    self:____constructor(...)
    return self
end
function Player.prototype.____constructor(self, username)
    self.username = username
end
Alex = {}
Alex.name = "Alex"
Alex.__index = Alex
Alex.prototype = {}
Alex.prototype.__index = Alex.prototype
Alex.prototype.constructor = Alex
Alex.____super = Player
setmetatable(Alex, Alex.____super)
setmetatable(Alex.prototype, Alex.____super.prototype)
function Alex.new(...)
    local self = setmetatable({}, Alex.prototype)
    self:____constructor(...)
    return self
end
function Alex.prototype.____constructor(self)
    Player.prototype.____constructor(self, "Alex")
end
local alex = Alex.new()
print(alex.username)

 

 

 

4. Стандартная библиотека и возможности языка

Скрытый текст

Многие методы стандартных типов TypeScript (такие как массивы и строки) также могут быть транслированы в lua.

 

4.1 Получение всех четных чисел массива, возведение в квадрат и соединение в строку через запятую


const items = [1, 2, 3, 4, 5];

items.push(6);

const result = items
    .filter(x => x % 2 == 0)
    .map(x => x ** 2)
    .join(", ");

print(result); // 4, 16, 36

 

Сгенерированный lua код

Скрытый текст


--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
-- Lua Library inline imports
function __TS__ArrayPush(arr, ...)
    local items = ({...})
    for ____, item in ipairs(items) do
        arr[#arr + 1] = item
    end
    return #arr
end

function __TS__ArrayFilter(arr, callbackfn)
    local result = {}
    do
        local i = 0
        while i < #arr do
            if callbackfn(_G, arr[i + 1], i, arr) then
                result[#result + 1] = arr[i + 1]
            end
            i = i + 1
        end
    end
    return result
end

function __TS__ArrayMap(arr, callbackfn)
    local newArray = {}
    do
        local i = 0
        while i < #arr do
            newArray[i + 1] = callbackfn(_G, arr[i + 1], i, arr)
            i = i + 1
        end
    end
    return newArray
end

local items = {
    1,
    2,
    3,
    4,
    5,
}
__TS__ArrayPush(items, 6)
local result = table.concat(__TS__ArrayMap(__TS__ArrayFilter(items, function(____, x) return x % 2 == 0 end), function(____, x) return x ^ 2 end), ", ")
print(result)

 

 

4.2 Модули


// main.ts
import { myFunction } from "./myLibrary";

myFunction();

// myLibrary.ts
export function myFunction() {
    print("Hi from myLibrary");
}

Сгенерированный lua код

Скрытый текст


-- main.lua
--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
local ____exports = {}
local ____myLibrary = require("myLibrary")
local myFunction = ____myLibrary.myFunction
myFunction(nil)
return ____exports

-- myLibrary.lua
--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
local ____exports = {}
function ____exports.myFunction(self)
    print("Hi from myLibrary")
end
return ____exports

 

 

4.3 Форматирование строк


const text = `2 + 2 = ${2 + 2}`;
print(text); // 2 + 2 = 4

Сгенерированный lua код


--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
local text = "2 + 2 = " .. tostring(2 + 2)
print(text)

 

# Как это работает?

Конечно же все не так просто. Компилятор просто так не узнает типы методов и полей объектов, с которыми мы будем работать. Для того, чтобы описать наше окружение необходимо написать так называемые файлы декларации или тайпинги. Хочу сразу вас обрадовать - это не ваша задача. Существует репозиторий с такими декларациями, в котором, на данный момент, существуют типы для большинства API и компонентов OpenOS и библиотеки GUI.

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

 

# Установка

Редактор кода

Вы можете использовать любой редактор кода с поддержкой TypeScript. Я рекомендую VSCode, который поддерживает его из коробки.

 

NodeJS

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

 

Использование плагина для VSCode (рекомендуется):

Скрытый текст

Вы можете установить плагин OpenComputersTS. В нем есть две команды:

  • OC-TS: Init - Создание нового проекта в пустой папке.
  • OC-TS: Mount - Подключение дисков из сохранений minecraft или эмулятора OCEmu в папку dist.

Для открытия окна с командами используйте сочетание Ctrl + Shift + P.

 

ici-RMCx-RWf.gif

 

Создание проекта вручную:

Скрытый текст

После установки NodeJS у вас должны появится команды npm  и node.

  1. Создайте новую папку для своего первого проекта
  2. Переключитесь в нее, используя терминал и все дальнешие действия выполняйте в ней
  3. Создайте npm пакет: npm init. После выполнения этой команды в папке появится файл package.json
  4. Добавьте в объект "scripts" в package.json строку "build": "tstl",
  5. Установите транспилер npm install --dev typescript-to-lua. После установки первого пакета у вас появится папка node_modules
  6. Установите тайпинги npm install --dev @opct/openos
  7. Создайте папку src для исходных файлов
  8. Создайте файл tsconfig.json со следующим содержимым:

{
    "compilerOptions": {
        "target": "esnext",
        "outDir": "dist",
        "module": "commonjs",
        "lib": ["esnext"],
        "strict": true,
        "moduleResolution": "node",
        "rootDir": "src",
        "types": ["lua-types/jit", "@opct/openos"]
    },
    "tstl": {
        "luaTarget": "JIT"
    }
}

Теперь вы можете размещать в src исходный код, например, main.ts со следующим содержимым:


import * as component from "component";
import { front } from "sides";

component.redstone.setOutput(front, 1);

 

Как запустить сгенерированный код в OpenComputers?

Самый удобный способ для доставки полученного кода - символическая ссылка. Вы можете заменить каталог dist ссылкой на папку диска.


# Создание ссылки через терминал
# linux / macos
ln -s /path/to/disk/home dist

# windows (cmd)
mklink /j dist C:\path\to\disk\home

Диски располагаются в папке .minecraft\saves\{сохранение}\opencomputers. Вам также необходимо отключить параметр filesystem.bufferChanges в файле .minecraft\config\opencomputers\settings.conf

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

 

Для компиляции используйте команду npm run build. Сгенерированные lua файлы появятся в папке dist.

 

# Особенности работы транспилера

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

 

1. Параметр self

Скрытый текст

Объявление функций

Одно из первых, что вы скорее всего попробуете сделать - объявить функцию:


function test() {}
test();

Однако результат может быть неожиданным:


--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
function test(self)
end
test(nil)

Зачем-то появился параметр self, который тут казалось бы не нужен.

А теперь попробуйте сделать так:


const obj = {
    a: 5,
    b: 10,
    sum() {
        print(this.a + this.b);
    },
};

obj.sum();

Результат:


--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
obj = {
    a = 5,
    b = 10,
    sum = function(self)
        print(self.a + self.b)
    end
}
obj:sum()

У функции все еще есть self, но в этот раз он необходим для доступа к полям obj.

Если мы теперь попробуем оторвать метод sum от объекта и вызвать отдельно:


const sum = obj.sum;
sum();

то увидем, что транспилер все еще передает nil вместо self:


sum = obj.sum
sum(nil)

Такое поведение связано с тем, что в JavaScript (для которого изначально был создан TypeScript) this (self) есть у любой функции (кроме стрелочных) и для сохранения совместимости с этой особенностью транспилер автоматически генерирует self параметр для любой функции. Если вы считаете такое поведение по умолчанию недопустимым, то можете отключить его:


// tsconfig.json
{
    "compilerOptions": { ... },
    "tstl": {
      	...,
        "noImplicitSelf": false,
        ...
    }
}

Тип self параметра

Вы также можете явно задать тип this так же как и тип любого другого параметра, например, назначить ему void:


function test(this: void): void {}
test();

И тогда транспилер уберет self параметр:


--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
function test()
end
test()

Особенно это важно при работе с декларациями. Например, мы хотим описать экземпляр некоторого класса:


declare interface Person {
    say(text: string): void;
}

declare const person: Person;

person.say("hi");

И ожидаемо получим:


--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
person:say("hi")

Но если нам понадобится описать какую-либо библиотеку (таблицу с функциями), то такое поведение может быть недопустимо, ведь эти функции не принимают self параметр. Можно вручную задать каждой функции this:void, а можно использовать директиву @noSelf:


/** @noSelf */
declare interface MyLibrary {
    method1(text: string): void;
    method2(num: number): void;
}

declare const lib: MyLibrary;

lib.method1("text");
lib.method2(123);

И тогда эти функции будут вызваны без двоеточия:


--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
lib.method1("text")
lib.method2(123)

 

 

2. Множественные значения

Скрытый текст

В TypeScript не поддерживаются множественные значения. Вместо этого используются кортежи.

Например, объявим функцию, которая возвращает несколько значений:


function getValues(): [string, number, boolean] {
    return ["text", 123, true];
}

const [str, num, bool] = getValues();

Этот подход сильно напоминает множественные возвращаемые значения и множественное присваивание в Lua. Тем не менее, сейчас кортежи просто превращаются в таблицы:


--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
function getValues(self)
    return {"text", 123, true}
end
str, num, bool = unpack(
    getValues(nil)
)

Чтобы заставить транспилер возвращать множественные значения, необходимо использовать директиву @tupleReturn:


/** @tupleReturn */
function getValues(): [string, number, boolean] {
    return ["text", 123, true];
}

const [str, num, bool] = getValues();

И тогда все будет красиво:


--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
function getValues(self)
    return "text", 123, true
end
str, num, bool = getValues(nil)

 

 

3. Индексы

Скрытый текст

Числовые индексы в таблицах Lua начинаются с 1, в то время как в TypeScript с 0. Транспилер сам выполняет преобразование индексов TypeScript в Lua, например:


const arr = [1, 2, 3];
print(arr[1]);

будет преобразовано в:


--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
arr = {1, 2, 3}
print(arr[2])

И мы в обоих случаях получим на экране второй элемент массива.

 

arr.length является полным аналогом оператора #:


const arr = [1, 2, 3];
print(arr.length);

Результат:


--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
arr = {1, 2, 3}
print(#arr)

 

 

# Ссылки

 

Изменено пользователем Exeteres
Раздел "Особенности работы транспилера"

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
1 час назад, Morkoffka сказал:

После установки NodeJS у вас должны появится команды npm  и node.

  1. Создайте новую папку для своего первого проекта
  2. Переключитесь в нее, используя терминал и все дальнешие действия выполняйте в ней
  3. Создайте npm пакет: npm init. После выполнения этой команды в папке появится файл package.json
  4. Добавьте в объект "scripts" в package.json строку "build": "tstl",
  5. Установите транспилер npm install --dev typescript-to-lua. После установки первого пакета у вас появится папка node_modules
  6. Установите тайпинги npm install --dev @opct/openos
  7. Создайте папку src для исходных файлов
  8. Создайте файл tsconfig.json со следующим содержимым:

А есть плагин для VSCode, чтобы сетапать воркспейс нажатием одной кнопки?

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

На тайпскрипте я, конечно, не писал, но пробовал MoonScript. Это такой язык, который транспилируется в Lua. У него тоже есть классы, сахара всякие. Но я на нём больше писать не хочу.

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

Здесь, видимо, всё то же. Так что для опенкомпов будет проще всё же писать на Lua.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
1 час назад, Fingercomp сказал:

На тайпскрипте я, конечно, не писал, но пробовал MoonScript. Это такой язык, который транспилируется в Lua. У него тоже есть классы, сахара всякие. Но я на нём больше писать не хочу.

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

Здесь, видимо, всё то же. Так что для опенкомпов будет проще всё же писать на Lua.

Тут оптимизатор нужен.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
В 06.01.2020 в 00:17, hohserg сказал:

А есть плагин для VSCode, чтобы сетапать воркспейс нажатием одной кнопки?

Хорошая идея, я об этом даже не подумал. Можно также добавить в этот плагин автоматический поиск дисков (папки .minecraft\saves\xxx\opencomputers) и их подключение. Я займусь его написанием, не думаю что это слишком сложно.

 

В 06.01.2020 в 01:11, Fingercomp сказал:

На тайпскрипте я, конечно, не писал, но пробовал MoonScript. Это такой язык, который транспилируется в Lua. У него тоже есть классы, сахара всякие. Но я на нём больше писать не хочу.

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

Здесь, видимо, всё то же. Так что для опенкомпов будет проще всё же писать на Lua.

Я видел MoonScript в соседней теме. Он конечно хорош, учитывая что был создан специально для lua, но разве в нем есть статический анализ? Это хоть и не спасает от ошибок в рантайме, но позволяет уберечь программиста от отладки очень глупых ошибок и опечаток и обнаружить их еще до компиляции (или во время нее). Что касается отладки - действительно, код не сильно читаем. Я пока не знаю, насколько это возможно, но хотелось бы иметь отладчик. Технически, транспилер поддерживает карты кода, что позволяет сопоставить исходный typescript и сгенерированный lua код. Я пока не нашел полноценного lua отладчика для opencomputers, но думаю что он есть. В крайнем случае можно использовать библиотеку debug.

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

Изменено пользователем Exeteres

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
6 часов назад, Morkoffka сказал:

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

А какие отладочные данные требуются?

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
В 06.01.2020 в 16:04, hohserg сказал:

А какие отладочные данные требуются?

Самые простые: уведомление VSCode об ошибке или брейкпоинте и передача строки, в которой произошла остановка. Если произошла ошибка, можно показать ее текст в редакторе.

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

Изменено пользователем Exeteres

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

 

В 06.01.2020 в 00:17, hohserg сказал:

А есть плагин для VSCode, чтобы сетапать воркспейс нажатием одной кнопки?

Небольшая демонстрация.

Скрытый текст

ici-RMCx-RWf.gif

 

Надо исправить некоторые недочеты и добавить поддержку Linux.

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

Изменено пользователем Exeteres

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
4 часа назад, Morkoffka сказал:

Добавить стек вызовов

Стэк вызовов уже есть, его можно получать так: ```ok,err = xpcall(code, debug.traceback, args...)```. Однако, это будет стэк вызовов Lua-кода и нужно как-то получить соответствие TypeScript-коду

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
3 минуты назад, hohserg сказал:

нужно как-то получить соответствие TypeScript-коду

Да, все так. Я надеюсь, что с этим разберутся карты кода.

Кстати, есть идея касательно транспорта. Можно использовать интернет карту. Она же может совершать запросы к localhostу?

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
1 минуту назад, hohserg сказал:

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

Да, так и планировалось. При запуске отладчика плагин будет встраивать в запускаемый файл кусок кода, который устанавливает обработчики ошибок и подключается к серверу. Я правда не знаю какой протокол для этого использовать: можно http, а можно tcp или вебсокеты (если в opencomputers они есть). Тогда можно будет отправлять данные в оба направления, например, чтобы уведомить программу продолжить работу после остановки на брейкпоинте или перезапустится.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

@hohserg Я обновил гайд и залил плагин в каталог.

Изменено пользователем Exeteres

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Попробовал. OC-TS: Mount че-то не работает - ввожу команду и ничего не происходит. Как выбрать назначение ссылки для dist?

~~~

Может, я че-то не так делаю? Раньше не юзал VSCode

Изменено пользователем hohserg

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
В 07.01.2020 в 02:15, hohserg сказал:

Попробовал. OC-TS: Mount че-то не работает - ввожу команду и ничего не происходит. Как выбрать назначение ссылки для dist?

~~~ 

Может, я че-то не так делаю? Раньше не юзал VSCode

Я нашел ошибку (и даже не одну). Я использовал неправильный оператор (return вместо continue), и поэтому он выходит из команды, если хотя бы одно сохранение не содержит папку opencomputers. У меня было только одно сохранение, поэтому я сразу не нашел этот баг.

Я исправил его и опубликовал новую версию.

Изменено пользователем Exeteres

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

image.png.a1a48cbdfae11eb0db05691d259e7801.png

Сохранения у мя лежат в %AppData%\opencomputers\saves\, а не в %AppData%\.minecraft\saves\

Можно вынести это в конфигурацию куда-нить?

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
1 минуту назад, hohserg сказал:

image.png.a1a48cbdfae11eb0db05691d259e7801.png

Сохранения у мя лежат в %AppData%\opencomputers\saves\, а не в %AppData%\.minecraft\saves\

Можно вынести это в конфигурацию куда-нить?

Да, можно сделать опцию. Так и сделаю.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
В 07.01.2020 в 17:12, hohserg сказал:

image.png.a1a48cbdfae11eb0db05691d259e7801.png

Сохранения у мя лежат в %AppData%\opencomputers\saves\, а не в %AppData%\.minecraft\saves\

Можно вынести это в конфигурацию куда-нить?

Добавил. Открыть настройки можно сочетанием Ctrl + ,

image.png.fac6e1e5ac23bc4266aa7cabc06385c4.png

 

Относительные пути будут разрешены относительно домашней папки (в винде C:\Users\username). Абсолютные модифицированы не будут.

Изменено пользователем Exeteres

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Запускаю таску watch, транслированный файл появляется(чекнул через проводник), но в VSCode не отображается, таска watch не завершается 

image.png.c1821530cee3f59fa9c62f8cb5e9dabf.png

 

image.png.c50dc763166e3e003efa41785b2fea91.png

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Присоединяйтесь к обсуждению

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

Гость
Ответить в тему...

×   Вы вставили отформатированное содержимое.   Удалить форматирование

  Разрешено использовать не более 75 эмодзи.

×   Ваша ссылка была автоматически встроена.   Отобразить как ссылку

×   Ваш предыдущий контент был восстановлен.   Очистить редактор

×   Вы не можете вставлять изображения напрямую. Загружайте или вставляйте изображения по ссылке.


×
×
  • Создать...