Merge branch 'master' of lab.zaar.be:thefish/alchemyst-go
This commit is contained in:
commit
4ba69bfe75
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"version": "v0.0.1.4-2-g20fa78a",
|
"version": "v0.0.1.5",
|
||||||
"title": "Alchemyst",
|
"title": "Alchemyst",
|
||||||
"sizeX": 100,
|
"sizeX": 100,
|
||||||
"sizeY": 47,
|
"sizeY": 47,
|
||||||
"fpsLimit": 60,
|
"fpsLimit": 60,
|
||||||
"font": ".\/resources\/fonts-ttf\/LiberationMono-Bold.ttf",
|
"font": "./resources/fonts-ttf/LiberationMono-Bold.ttf",
|
||||||
"fontSize": "8x12",
|
"fontSize": "9x14",
|
||||||
"verbosity": "debug"
|
"verbosity": "debug"
|
||||||
}
|
}
|
@ -78,6 +78,9 @@ Pre-Computed Visiblity Trees on RogueBasin
|
|||||||
Adam Milazzo's FOV Method Roundup where a similar method described as 'permissive' is detailed
|
Adam Milazzo's FOV Method Roundup where a similar method described as 'permissive' is detailed
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const MIN_LIT_TO_BE_VISIBLE = 1
|
||||||
|
const MIN_WALL_LIT_TO_BE_VISIBLE = 4
|
||||||
|
|
||||||
var errNotFoundCell = errors.New("Cell not found")
|
var errNotFoundCell = errors.New("Cell not found")
|
||||||
var errOutOfBounds = errors.New("Cell out of bounds")
|
var errOutOfBounds = errors.New("Cell out of bounds")
|
||||||
|
|
||||||
@ -163,9 +166,9 @@ func (ps *precomputedShade) PrecomputeFovMap() {
|
|||||||
|
|
||||||
//Bresanham lines / Raycast
|
//Bresanham lines / Raycast
|
||||||
var lineX, lineY float64
|
var lineX, lineY float64
|
||||||
for i := 0; i < 360; i++ {
|
for i := 0; i < 720; i++ { // 1/2 of angles
|
||||||
dx := math.Sin(float64(i) / (float64(180) / math.Pi))
|
dx := math.Sin(float64(i) / (float64(360) / math.Pi)) //1/2 of angles
|
||||||
dy := math.Cos(float64(i) / (float64(180) / math.Pi))
|
dy := math.Cos(float64(i) / (float64(360) / math.Pi))
|
||||||
|
|
||||||
lineX = 0
|
lineX = 0
|
||||||
lineY = 0
|
lineY = 0
|
||||||
@ -204,11 +207,11 @@ func (ps *precomputedShade) recalc(level *gamemap.Level, initCoords types.Coords
|
|||||||
|
|
||||||
level.GetTile(initCoords).Visible = true
|
level.GetTile(initCoords).Visible = true
|
||||||
|
|
||||||
var fullShade = make([]byte, 360)
|
var fullShade = make([]byte, 720) // 1/2 of angles
|
||||||
for i := range fullShade {
|
for i := range fullShade {
|
||||||
fullShade[i] = 1
|
fullShade[i] = 1
|
||||||
}
|
}
|
||||||
var emptyShade = make([]byte, 360)
|
var emptyShade = make([]byte, 720) // 1/2 of angles
|
||||||
currentShade := emptyShade
|
currentShade := emptyShade
|
||||||
nextShade := emptyShade
|
nextShade := emptyShade
|
||||||
|
|
||||||
@ -236,7 +239,11 @@ func (ps *precomputedShade) recalc(level *gamemap.Level, initCoords types.Coords
|
|||||||
//fmt.Printf("\n level coords: %v", lc)
|
//fmt.Printf("\n level coords: %v", lc)
|
||||||
for _, angle := range cell.occludedAngles {
|
for _, angle := range cell.occludedAngles {
|
||||||
|
|
||||||
if level.GetTile(lc).BlocksSight {
|
if level.GetTile(lc).BlocksSight && ps.LightWalls {
|
||||||
|
if (nextShade[angle] == 0 && currentShade[angle] == 0) {
|
||||||
|
level.GetTile(lc).Visible = true
|
||||||
|
level.GetTile(lc).Explored = true
|
||||||
|
}
|
||||||
nextShade[angle] = 1
|
nextShade[angle] = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,7 +263,7 @@ func (ps *precomputedShade) ComputeFov(level *gamemap.Level, initCoords types.Co
|
|||||||
for _, cell := range ps.CellList {
|
for _, cell := range ps.CellList {
|
||||||
//fmt.Printf("\n coords: %v, distance: %f, lit: %d", cell.Coords, cell.distance, cell.lit)
|
//fmt.Printf("\n coords: %v, distance: %f, lit: %d", cell.Coords, cell.distance, cell.lit)
|
||||||
cs, err := ps.toLevelCoords(level, initCoords, cell.Coords)
|
cs, err := ps.toLevelCoords(level, initCoords, cell.Coords)
|
||||||
if cell.lit > 2 {
|
if cell.lit > MIN_LIT_TO_BE_VISIBLE {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -265,21 +272,25 @@ func (ps *precomputedShade) ComputeFov(level *gamemap.Level, initCoords types.Co
|
|||||||
}
|
}
|
||||||
|
|
||||||
//light walls, crutch
|
//light walls, crutch
|
||||||
if level.GetTile(cs).BlocksSight && ps.LightWalls {
|
//if level.GetTile(cs).BlocksSight && ps.LightWalls {
|
||||||
if cell.IsAdjacentTo(&types.Coords{0,0}) {
|
// if cell.IsAdjacentTo(&types.Coords{0,0}) {
|
||||||
level.GetTile(cs).Visible = true
|
// level.GetTile(cs).Visible = true
|
||||||
} else {
|
// } else {
|
||||||
for _, maybeNb := range ps.CellList {
|
// maybeLit := false
|
||||||
if //int(maybeNb.distance) == int(cell.distance-1) &&
|
// for _, maybeNb := range ps.CellList {
|
||||||
maybeNb.IsAdjacentTo(&cell.Coords) &&
|
// if //int(maybeNb.distance) == int(cell.distance-1) &&
|
||||||
//(maybeNb.X == cell.X || maybeNb.Y == cell.Y) &&
|
// maybeNb.IsAdjacentTo(&cell.Coords) &&
|
||||||
maybeNb.lit > 5 { //magic constant!
|
// (maybeNb.X == cell.X || maybeNb.Y == cell.Y) &&
|
||||||
level.GetTile(cs).Visible = true
|
// maybeNb.lit > MIN_WALL_LIT_TO_BE_VISIBLE { //magic constant!
|
||||||
level.GetTile(cs).Explored = true
|
// maybeLit = true
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// if maybeLit {
|
||||||
}
|
// level.GetTile(cs).Visible = true
|
||||||
|
// level.GetTile(cs).Explored = true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,35 +31,35 @@ func (ts *GameScreen) Enter() {
|
|||||||
Print(1, ts.mw.H-2, "Press [color=white]?[/color] for help")
|
Print(1, ts.mw.H-2, "Press [color=white]?[/color] for help")
|
||||||
}
|
}
|
||||||
func (ts *GameScreen) Exit() {
|
func (ts *GameScreen) Exit() {
|
||||||
ts.mw.GetLayer("base").ClearArea(1, ts.mw.H-2, 30, 1)
|
ts.mw.GetLayer("overlay").ClearArea(0, ts.mw.H-3, 30, 3)
|
||||||
//remove what we dont need
|
//remove what we dont need
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *GameScreen) HandleInput(input string) {
|
func (ts *GameScreen) HandleInput(input string) {
|
||||||
//ts.state.Do(func(){
|
//ts.state.Do(func(){
|
||||||
switch input {
|
switch input {
|
||||||
case "Up", "k", "8":
|
case "Up", "k", "KP_8":
|
||||||
ts.walk(ts.state, 0, -1)
|
ts.walk(ts.state, 0, -1)
|
||||||
break
|
break
|
||||||
case "Down", "j", "2":
|
case "Down", "j", "KP_2":
|
||||||
ts.walk(ts.state, 0, 1)
|
ts.walk(ts.state, 0, 1)
|
||||||
break
|
break
|
||||||
case "Left", "h", "4":
|
case "Left", "h", "KP_4":
|
||||||
ts.walk(ts.state, -1, 0)
|
ts.walk(ts.state, -1, 0)
|
||||||
break
|
break
|
||||||
case "Right", "l", "6":
|
case "Right", "l", "KP_6":
|
||||||
ts.walk(ts.state, 1, 0)
|
ts.walk(ts.state, 1, 0)
|
||||||
break
|
break
|
||||||
case "y", "7":
|
case "y", "KP_7":
|
||||||
ts.walk(ts.state, -1, -1)
|
ts.walk(ts.state, -1, -1)
|
||||||
break
|
break
|
||||||
case "u", "9":
|
case "u", "KP_9":
|
||||||
ts.walk(ts.state, 1, -1)
|
ts.walk(ts.state, 1, -1)
|
||||||
break
|
break
|
||||||
case "b", "1":
|
case "b", "KP_1":
|
||||||
ts.walk(ts.state, -1, 1)
|
ts.walk(ts.state, -1, 1)
|
||||||
break
|
break
|
||||||
case "n", "3":
|
case "n", "KP_3":
|
||||||
ts.walk(ts.state, 1, 1)
|
ts.walk(ts.state, 1, 1)
|
||||||
break
|
break
|
||||||
case "Shift+/":
|
case "Shift+/":
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
декодирование ввода в отдельном потоке
|
декодирование ввода в отдельном потоке
|
||||||
|
|
||||||
Реализация FPS и троттлинг
|
Реализация FPS и троттлинг
|
@ -1,5 +1,11 @@
|
|||||||
RLG и Golang - полезные советы
|
RLG и Golang - некоторые полезные советы
|
||||||
===
|
===
|
||||||
1. [Установка и некоторые особенности работы](./linux_go_blt.md) связки BLT + Go на Linux
|
0. [Выбираем инструменты](./choose_your_pill.md)
|
||||||
2. Реализация [некоторых возможностей](./go_chans_for_game.md) Go - chans, tickers, throttling
|
1. [Установка и некоторые особенности работы](linux_go_blt_install_quickstart.md) связки BLT + Go на Linux
|
||||||
3. [Система типов](./static_types_vs_ecs.md) - нативная или ECS?
|
2. Что [стоит и НЕ стоит](go_game_dos_and_donts.md) делать с возможностями Go - +chans, +tickers, +throttling, -closures
|
||||||
|
3. [Система типов](./static_types_vs_ecs.md) - нативная или ECS? На самом деле и то, и то
|
||||||
|
|
||||||
|
Дополнения
|
||||||
|
---
|
||||||
|
|
||||||
|
1. Как [не делать лишнюю работу](./makefile_and_crosscompiling.md) и почему это **важно**.
|
@ -1,47 +0,0 @@
|
|||||||
Установка и работа с BLT на Linux
|
|
||||||
==
|
|
||||||
|
|
||||||
Для Windows и Mac проблем с Go + BLT, насколько мне известно нет.
|
|
||||||
С Linux, которая моя основная рабочая ось - другая история, здесь вносят свой шарм особенности работы линкера.
|
|
||||||
Дело в том, что в составе BLT есть готовые биндинги для Go, НО! В Terminal/Include/Go по умолчанию
|
|
||||||
указаны такие флаги линкера CGO (стр 25)
|
|
||||||
```go
|
|
||||||
// #cgo LDFLAGS: -lBearLibTerminal
|
|
||||||
```
|
|
||||||
Что подразумевает глобальную вивдимость библиотеки. Увы, пока пакета с BLT для распространенных дистрибутивов Linux нет.
|
|
||||||
Поэтому беде нужно помочь руками. Сначала вручную показать линтеру, что такая библиотека есть, и потом перезагрузить
|
|
||||||
кеш путей к библиотекам::
|
|
||||||
```bash
|
|
||||||
$ sudo echo "/path/to/libbearterminal.so" > /etc/ld.so.conf.d/libbearterminal.conf && sudo ldconfig
|
|
||||||
```
|
|
||||||
|
|
||||||
Проблема тут в том, что эту же операцию придется проделать всем, кто захочет запустить ваше приложение с BLT. Вопреки
|
|
||||||
распространенному стереотипу - доля красноглазых пользователей Linux с каждым годом падает, и эта консольная магия для
|
|
||||||
большинства уже некомильфо.
|
|
||||||
|
|
||||||
Способ второй, которым воспользовался я, намного проще для пользователя.
|
|
||||||
Редактируем файл с биндингами примерно следующим образом:
|
|
||||||
```go
|
|
||||||
// #cgo LDFLAGS: -L. -Wl,-rpath -Wl,./ -lBearLibTerminal
|
|
||||||
// #include <stdlib.h>
|
|
||||||
// #include <BearLibTerminal.h>
|
|
||||||
import "C"
|
|
||||||
```
|
|
||||||
(знатоки С, простите, я этими флагами вообще
|
|
||||||
пользоваться не умею)
|
|
||||||
|
|
||||||
Далее - собираем приложение с libtcod ```go build -o test```.
|
|
||||||
|
|
||||||
Проверяем, что относительные пути записались в бинарник:
|
|
||||||
```bash
|
|
||||||
objdump -p test | grep RPATH
|
|
||||||
```
|
|
||||||
Результат должен быть таким:
|
|
||||||
```bash
|
|
||||||
RPATH ./
|
|
||||||
```
|
|
||||||
|
|
||||||
Ура! Теперь кладем libBearLibTerminal.so прямо в папку с main.go и запускам go run (или скомпилированный бинарник) прям
|
|
||||||
оттуда. Собранные таким образом бинарники будут искать библиотеку в той же папке, где находятся они сами.
|
|
||||||
|
|
||||||
Теперь при дистрибуции приложения можно просто положить .so файл библиотеки рядом, и все будет работать!
|
|
153
story/linux_go_blt_install_quickstart.md
Normal file
153
story/linux_go_blt_install_quickstart.md
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
Установка и работа с BLT на Linux
|
||||||
|
==
|
||||||
|
|
||||||
|
Про Windows и Mac в контексте связки Go + BLT, я говорить не буду, поскольку не ел устриц.
|
||||||
|
С Linux, которая моя основная рабочая ось - другая история, здесь вносят свой шарм особенности работы линкера.
|
||||||
|
Дело в том, что BLT написана на C. Но! есть готовые биндинги для Go, НО! В Terminal/Include/Go по умолчанию
|
||||||
|
указаны такие флаги линкера CGO (стр 25)
|
||||||
|
```go
|
||||||
|
// #cgo LDFLAGS: -lBearLibTerminal
|
||||||
|
```
|
||||||
|
Что подразумевает глобальную вивдимость библиотеки. Увы, пока пакета с BLT для
|
||||||
|
распространенных дистрибутивов Linux нет.
|
||||||
|
Поэтому беде нужно помочь руками.
|
||||||
|
|
||||||
|
#####Первый метод
|
||||||
|
|
||||||
|
Сначала вручную показать линтеру, что такая библиотека есть, и потом перезагрузить
|
||||||
|
кеш путей к библиотекам (пример для Ubuntu):
|
||||||
|
```bash
|
||||||
|
$ sudo echo "/path/to/libbearterminal.so" > /etc/ld.so.conf.d/libbearterminal.conf && sudo ldconfig
|
||||||
|
```
|
||||||
|
|
||||||
|
Проблема тут в том, что эту же операцию придется проделать всем, кто захочет запустить ваше приложение с BLT. Вопреки
|
||||||
|
распространенному стереотипу - доля красноглазых пользователей Linux с каждым годом падает, и эта консольная магия для
|
||||||
|
большинства уже некомильфо.
|
||||||
|
|
||||||
|
#####Второй метод
|
||||||
|
|
||||||
|
Способ второй, которым воспользовался я, с которым намного проще жить.
|
||||||
|
Редактируем файл с биндингами (BearLibTerminal.go) примерно следующим образом:
|
||||||
|
```go
|
||||||
|
// #cgo LDFLAGS: -L. -Wl,-rpath -Wl,./ -lBearLibTerminal
|
||||||
|
// #include <stdlib.h>
|
||||||
|
// #include <BearLibTerminal.h>
|
||||||
|
import "C"
|
||||||
|
```
|
||||||
|
(знатоки С, простите если что не так, я этими флагами вообще
|
||||||
|
пользоваться не умею)
|
||||||
|
|
||||||
|
Далее - собираем минимальное приложение с blt ```go build -o test```.
|
||||||
|
|
||||||
|
Проверяем, что относительные пути записались в бинарник:
|
||||||
|
```bash
|
||||||
|
objdump -p test | grep RPATH
|
||||||
|
```
|
||||||
|
Результат должен быть таким:
|
||||||
|
```bash
|
||||||
|
RPATH ./
|
||||||
|
```
|
||||||
|
|
||||||
|
Ура! Теперь кладем libBearLibTerminal.so прямо в папку с main.go и запускам go run
|
||||||
|
(или скомпилированный бинарник) прям оттуда. Собранные таким образом бинарники будут
|
||||||
|
искать библиотеку в той же папке, где находятся они сами.
|
||||||
|
|
||||||
|
Теперь при дистрибуции приложения можно просто положить .so файл библиотеки рядом,
|
||||||
|
и все будет работать!
|
||||||
|
|
||||||
|
Горутины и многопоточность
|
||||||
|
---
|
||||||
|
|
||||||
|
Вторая кочка, на которой мне пришлось споткнуться - то то, что вызов любой своей
|
||||||
|
функции не из main thread BLT воспринимает крайне нервно,
|
||||||
|
и сыпет фатальными ошибками. Это неприятно, тк часто Го выбирают именно за
|
||||||
|
многопоточность из коробки. Но справедливости ради точно так же ведет себя и большинство
|
||||||
|
других библиотек связанных с рендером и вводом-выводом, тот же SDL например. Так что
|
||||||
|
воспринимайте это как милую особенность использования CGO.
|
||||||
|
|
||||||
|
Лекарство тут ровно одно - вызывать сишные foreign functions из main thread.
|
||||||
|
Для реализации этого требования мне показалась полезной следующая конструкция
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "runtime"
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
// Рецепт чтобы убежать от [fatal] 'refresh' was not called from the main thread
|
||||||
|
// https://github.com/golang/go/wiki/LockOSThread
|
||||||
|
func init() {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameState struct {
|
||||||
|
Mainfunc chan func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// do запускает функцию f в контексте main thread.
|
||||||
|
func (g *GameState) Do(f func()) {
|
||||||
|
done := make(chan struct{}, 1)
|
||||||
|
g.Mainfunc <- func() {
|
||||||
|
f()
|
||||||
|
f = nil //zero pointer в замыкание
|
||||||
|
done <- struct{}{}
|
||||||
|
}
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var State = GameState{
|
||||||
|
Mainfunc: make(chan func()), //блокирующий канал(!)
|
||||||
|
}
|
||||||
|
// И где-то в Main Loop делаем примерно так:
|
||||||
|
func MainLoop(state GameState) {
|
||||||
|
...
|
||||||
|
//В этом select обработка ввода, рендер, пеерколючение состояний интерфейса итп
|
||||||
|
for f := range state.Mainfunc {
|
||||||
|
f()
|
||||||
|
}}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
State - это обычный Value Object, экземпляр типа GameState. Я его использую как
|
||||||
|
контейнер для важных для игры данных - географии уровня, состояния объектов и мобов,
|
||||||
|
разных тикеров, каналов для рендера и ввода-вывода итп[1]. Так как он глобальный
|
||||||
|
(или просто передается всюду по аргументам), то именно в него встроен метод Do.
|
||||||
|
|
||||||
|
Если нам скажем в пакете, где описывается некий предмет, надо нарисовать при его
|
||||||
|
поднятии какой-то супер-эффект на экране - мы поступаем вот так:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package item
|
||||||
|
|
||||||
|
import "main"
|
||||||
|
import blt "some.repo.ru/user/bearlibterminal"
|
||||||
|
|
||||||
|
var State main.GameState
|
||||||
|
|
||||||
|
//функция скажем поднятия особенного предмета...
|
||||||
|
func (item *SpecialItem) Pickup() {
|
||||||
|
|
||||||
|
....
|
||||||
|
//выполняем строго в main thread
|
||||||
|
State.Do(func() {
|
||||||
|
renderSuperEffect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
...
|
||||||
|
//тут собственно отрисовка эффекта
|
||||||
|
func renderSuperEffect() {
|
||||||
|
...
|
||||||
|
blt.Layer(0)
|
||||||
|
blt.Print(x,y, "WAAAGH")
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Здесь renderSuperEffect - непосредственная реализация эффекта, doSuperEffect -
|
||||||
|
запихивает в очередь на выполнения в main thread эту самую реализацию. Которая с успехом
|
||||||
|
выполняется в main loop. В целом картина именно такая, но больше подробностей можно
|
||||||
|
найти по ссылкам в комментариях.
|
||||||
|
|
||||||
|
[1]: Если такой контейнер аккуратно сериализовать (рекурсивно вместе со всем содержимым) и записать на диск... То потом можно его прочитать и десериализовать. Получив тем самым почти бесплатно Save / Load.
|
11
story/makefile_and_crosscompiling.md
Normal file
11
story/makefile_and_crosscompiling.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
Автоматизация сборки и тестирование
|
||||||
|
===
|
||||||
|
|
||||||
|
- Почему это важно: мелочи сжирают кучу времени. Не позволяйте им это делать!
|
||||||
|
- настройка под Linux: все внешние либы собраны и включены в монорепо (дело вкуса)
|
||||||
|
- Go-специфичные вещи: glide, go mod
|
||||||
|
- Кросскомпиляция, CGO для Mac и Linux. CGO_ENABLED=1, mingw, локальная видимость библиотек
|
||||||
|
- Makefile и нафига он нужен
|
||||||
|
- Таргеты: Убираем бардак за собой - distclean, build
|
||||||
|
- Автоматическое тестирование, testify. Не ленитесь писать тесты!
|
||||||
|
- Деплой/публикация после сборки
|
@ -2,8 +2,20 @@
|
|||||||
|
|
||||||
Плюсы использования нативной системы типов
|
Плюсы использования нативной системы типов
|
||||||
|
|
||||||
|
- Примитивы типа Coords, Rect
|
||||||
|
- Интерфейсы, type casting и переиспользование Blit
|
||||||
|
|
||||||
Минусы использования нативной системы типы
|
Минусы использования нативной системы типы
|
||||||
|
|
||||||
ECS - меняем бойлерплейт на относительное снижение связности
|
- Структура структуры (эм..) иммутабельна
|
||||||
|
|
||||||
Минутка рекламы gogue
|
ECS - достигаем относительного снижения связности ценой чудовищного бойлерплейта
|
||||||
|
|
||||||
|
- Делаем динамическую систему типов там и только там где нам надо
|
||||||
|
- Возможность физически впихнуть в кеш проца все актуальные данные
|
||||||
|
- Да, это много копипасты. Но мы используем статически типизированный язык, а не питон, что вы хотели?
|
||||||
|
|
||||||
|
Минутка рекламы gogue
|
||||||
|
|
||||||
|
- Толковый туториал, неплохой код
|
||||||
|
- Пользуйтесь и продвигайте
|
Loading…
x
Reference in New Issue
Block a user