소스 검색

Добавление кода

SVI 3 년 전
부모
커밋
0049eba34c

+ 24 - 0
cmd/desktop/main.go

@@ -0,0 +1,24 @@
+// package main -- пускач для настольног оприложения бота
+package main
+
+import (
+	"os"
+
+	"github.com/sirupsen/logrus"
+
+	"wartank/desktop"
+)
+
+func main() {
+	logrus.SetLevel(logrus.DebugLevel)
+	logrus.Infoln("main()")
+	desktop, err := desktop.NewDesktop()
+	if err != nil {
+		logrus.WithError(err).Errorf("main(): in create Desktop")
+		os.Exit(1)
+	}
+	if err := desktop.Run(); err != nil {
+		logrus.WithError(err).Errorf("main(): in run Desktop")
+		os.Exit(2)
+	}
+}

+ 20 - 0
cmd/server/main.go

@@ -0,0 +1,20 @@
+// package main -- пускач длся сервера на файбере
+package main
+
+import (
+	"log"
+	"os"
+	"wartank/server"
+)
+
+func main() {
+	serv, err := server.NewServer()
+	if err != nil {
+		log.Printf("main(): in make IServer, err=\n\t%v\n", err)
+		os.Exit(1)
+	}
+	if err := serv.Run(); err != nil {
+		log.Printf("main(): in run server, err=\n\t%v\n", err)
+		os.Exit(2)
+	}
+}

+ 86 - 0
desktop/desktop.go

@@ -0,0 +1,86 @@
+// package desktop -- главный тип локального приложения
+package desktop
+
+import (
+	"fmt"
+	"log"
+
+	"wartank/desktop/dict_bot"
+	"wartank/desktop/root"
+	"wartank/desktop/store_net"
+	"wartank/desktop/web_socket"
+	"wartank/desktop/win_main"
+	"wartank/pkg/components/kernel"
+	"wartank/pkg/types"
+)
+
+// Desktop -- главный тип локального приложения
+type Desktop struct {
+	*kernel.Kernel
+	store   types.IStore
+	winMain *win_main.WinMain
+	ws      types.IWebSocket
+	root    types.IRoot
+	dictBot types.IDictBot
+}
+
+// NewDesktop -- возвращает новый объект настольного приложения
+func NewDesktop() (*Desktop, error) {
+	log.Println("NewDesktop()")
+	kernel, err := kernel.NewKernel()
+	if err != nil {
+		return nil, fmt.Errorf("NewDesktop(): in create IKernel, err=\n\t%w", err)
+	}
+	sf := &Desktop{
+		Kernel: kernel,
+	}
+	sf.ws, err = web_socket.NewWebSocket(sf)
+	if err != nil {
+		return nil, fmt.Errorf("NewDesktop(): in create IWebSocket, err=\n\t%w", err)
+	}
+	sf.store, err = store_net.NewStoreNet(sf)
+	if err != nil {
+		return nil, fmt.Errorf("NewDesktop(): in create IStore, err=\n\t%w", err)
+	}
+	sf.root, err = root.NewRoot(sf)
+	if err != nil {
+		return nil, fmt.Errorf("NewDesktop(): in create IRoot, err=\n\t%w", err)
+	}
+	sf.dictBot, err = dict_bot.NewDictBot(sf)
+	if err != nil {
+		return nil, fmt.Errorf("NewDesktop(): in create IGamers, err=\n\t%w", err)
+	}
+	sf.winMain, err = win_main.NewWinMain(sf)
+	if err != nil {
+		return nil, fmt.Errorf("NewDesktop(): in create WinMain, err=\n\t%w", err)
+	}
+	return sf, nil
+}
+
+// Run -- запускает десктоп в работу
+func (sf *Desktop) Run() error {
+	sf.Slog().Infof("Desktop.Run()\n")
+	sf.winMain.Run()
+	sf.Wg().Wait()
+	return nil
+}
+
+// Store -- возвращает хранилище
+func (sf *Desktop) Store() types.IStore {
+	return sf.store
+}
+
+// Ws -- возвращает веб-сокет
+func (sf *Desktop) Ws() types.IWebSocket {
+	return sf.ws
+}
+
+// Root -- возвращает объект рута
+func (sf *Desktop) Root() types.IRoot {
+	return sf.root
+}
+
+// DictBot -- возвращает объект списка ботов игрока
+func (sf *Desktop) DictBot() types.IDictBot {
+	return sf.dictBot
+}

+ 27 - 0
desktop/dict_bot/bot/bot.go

@@ -0,0 +1,27 @@
+// package bot -- бот для хранения данных
+package bot
+
+import "fmt"
+
+// Bot -- бот для хранения данных
+type Bot struct {
+	name string
+	pass string
+}
+
+// NewBot -- возвращает нового бота
+func NewBot(name, pass string) (*Bot, error) {
+	{ // Предусловия
+		if name == "" {
+			return nil, fmt.Errorf("NewBot(): name is empty")
+		}
+		if pass == "" {
+			return nil, fmt.Errorf("NewBot(): pass is empty")
+		}
+	}
+	sf := &Bot{
+		name: name,
+		pass: pass,
+	}
+	return sf, nil
+}

+ 165 - 0
desktop/dict_bot/dict_bot.go

@@ -0,0 +1,165 @@
+// package dict_bot -- словарь ботов в игре
+package dict_bot
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"strings"
+
+	"wartank/desktop/dict_bot/bot"
+	"wartank/desktop/dict_bot/win_bot_add"
+	"wartank/desktop/dict_bot/win_bot_view"
+	"wartank/desktop/dict_bot/win_bots"
+	"wartank/pkg/types"
+)
+
+// DictBot -- словарь ботов в игре
+type DictBot struct {
+	desktop types.IDesktop
+	store   types.IStore
+	ws      types.IWebSocket
+	dict    map[string]types.IBot
+	winBots *win_bots.WinBots
+}
+
+// NewDictBot -- возвращает новый словарь ботов
+func NewDictBot(desktop types.IDesktop) (*DictBot, error) {
+	if desktop == nil {
+		return nil, fmt.Errorf("NewDictBot(): IDesktop == nil")
+	}
+	sf := &DictBot{
+		desktop: desktop,
+		store:   desktop.Store(),
+		ws:      desktop.Ws(),
+		dict:    make(map[string]types.IBot),
+	}
+	sf.load()
+	return sf, nil
+}
+
+// Загружает список ботов, к этому моменту логин рута точно прошёл
+func (sf *DictBot) load() {
+	log.Println("DictBot.load()")
+	dictResp, err := sf.ws.Read("/bot/list/load")
+	if err != nil {
+		if strings.Contains(err.Error(), "not found") {
+			return
+		}
+		log.Printf("DictBot.load(): in get bot, err=\n\t%v\n", err)
+		return
+	}
+	strErr := dictResp["err"]
+	if strErr != "" {
+		if strings.Contains(strErr, "not found") { // Это был первый запуск
+			lstBot := make([]string, 0)
+			binList, _ := json.Marshal(&lstBot)
+			dictReq := make(map[string]string)
+			dictReq["binData"] = string(binList)
+			err := sf.ws.Write("/bot/list/save", dictReq)
+			if err != nil {
+				log.Printf("DictBot.load(): in write new list bot, err=\n\t%v\n", strErr)
+			}
+		} else {
+			log.Printf("DictBot.load(): in response, err=\n\t%v\n", strErr)
+		}
+	}
+	log.Printf("DictBot.load(): dictResp=%#v\n", dictResp)
+	strList := dictResp["/bot/list"]
+	lstBot := make([]string, 0)
+	err = json.Unmarshal([]byte(strList), &lstBot)
+	if err != nil {
+		log.Printf("DictBot.load(): in unmarshal list bot, err=\n\t%v\n", err)
+		return
+	}
+	for _, bot := range lstBot {
+		sf.dict[bot] = ""
+	}
+}
+
+// Show -- показывает окно списка ботов
+func (sf *DictBot) Show() {
+	log.Println("DictBot.Show()")
+	dictBot := make(map[string]string)
+	for key := range sf.dict {
+		dictBot[key] = ""
+	}
+	var err error
+	sf.winBots, err = win_bots.NewWinBots(sf.desktop, sf.addShow, sf.view, dictBot)
+	if err != nil {
+		log.Printf("DictBot.Show(): in create WinBots, err=\n\t%v\n", err)
+		return
+	}
+	go sf.winBots.Run()
+}
+
+// Показывает окно добавления бота
+func (sf *DictBot) addShow() {
+	log.Println("DictBot.addShow()")
+	winBotAdd, err := win_bot_add.NewWinBotAdd(sf.desktop, sf.addNew)
+	if err != nil {
+		log.Printf("DictBot.addShow(): in create WinBotAdd, err=\n\t%v\n", err)
+		return
+	}
+	go winBotAdd.Run()
+}
+
+// Команда обратного вызова для добавления нового бота
+func (sf *DictBot) addNew(name, pass string) {
+	log.Printf("DictBot.addNew(): name=%q\tpass=%q\n", name, pass)
+	_, isOk := sf.dict[name]
+	if isOk {
+		log.Printf("DictBot.addNew(): бот с именем(%q) уже существует\n", name)
+		return
+	}
+	bot, err := bot.NewBot(name, pass)
+	if err != nil {
+		log.Printf("DictBot.addNew(): in add new bot(%q), err=\n\t%v\n", name, err)
+		return
+	}
+	sf.dict[name] = bot
+	{ // Работа с хранилищем
+		// Сохранить бота в хранилище
+		err = sf.store.Put("/bot/"+name, pass)
+		if err != nil {
+			log.Printf("DictBot.addNew(): in save new bot(%q), err=\n\t%v\n", name, err)
+			return
+		}
+		// Обновить список ботов
+		lstBot := make([]string, 0)
+		for key := range sf.dict {
+			lstBot = append(lstBot, key)
+		}
+		// Сохранить обновлённый список ботов
+		binNewList, err := json.Marshal(&lstBot)
+		if err != nil {
+			log.Printf("DictBot.addNew(): in marshal new list bots, err=\n\t%v\n", err)
+			return
+		}
+		err = sf.store.Put("/bot/list", string(binNewList))
+		if err != nil {
+			log.Printf("DictBot.addNew(): in save new bot(%q), err=\n\t%v\n", name, err)
+			return
+		}
+	}
+
+	if sf.winBots == nil {
+		return
+	}
+	dictBot := make(map[string]string)
+	for key := range sf.dict {
+		dictBot[key] = ""
+	}
+	sf.winBots.UpdateList(dictBot)
+}
+
+// Команда просмотра существующего бота
+func (sf *DictBot) view(nameBot string) {
+	log.Printf("DictBot.view(): nameBot=%q\n", nameBot)
+	winBotView, err := win_bot_view.NewWinBotView(sf.desktop, nameBot)
+	if err != nil {
+		log.Printf("DictBot.view(): in create win view on bot(%q), err=\n\t%v\n", nameBot, err)
+		return
+	}
+	go winBotView.Run()
+}

+ 87 - 0
desktop/dict_bot/win_bot_add/win_bot_add.go

@@ -0,0 +1,87 @@
+// package win_bot_add -- добавляет новый бот
+package win_bot_add
+
+import (
+	_ "embed"
+	"fmt"
+	"log"
+	"net/url"
+	"runtime"
+
+	"github.com/zserge/lorca"
+
+	"wartank/pkg/types"
+)
+
+//go:embed win_bot_add.html
+var strWinHtml string
+
+// WinBotAdd -- окно добавления бота
+type WinBotAdd struct {
+	desktop types.IDesktop
+	store   types.IStore
+	win     lorca.UI
+	ws      types.IWebSocket
+	fnAdd   func(name, pass string)
+}
+
+// NewWinBotAdd -- возвращает новое окно добавления бота
+func NewWinBotAdd(desktop types.IDesktop, fnAdd func(name, pass string)) (*WinBotAdd, error) {
+	{ // Предусловия
+		if desktop == nil {
+			return nil, fmt.Errorf("NewWinBotAdd(): IDesktop == nil")
+		}
+		if fnAdd == nil {
+			return nil, fmt.Errorf("NewWinBotAdd(): fnAdd == nil")
+		}
+	}
+
+	sf := &WinBotAdd{
+		desktop: desktop,
+		store:   desktop.Store(),
+		ws:      desktop.Ws(),
+		fnAdd:   fnAdd,
+	}
+
+	args := []string{}
+	if runtime.GOOS == "linux" {
+		args = append(args, "--class=Lorca")
+	}
+	var err error
+	sf.win, err = lorca.New("data:text/html,"+url.PathEscape(strWinHtml), "", 640, 480, args...)
+	if err != nil {
+		return nil, fmt.Errorf("NewWinBotAdd(): in create win, err=\n\t%w", err)
+	}
+	go sf.close()
+	return sf, nil
+}
+
+// Работает в отдельном потоке, главный цикл окна
+func (sf *WinBotAdd) Run() {
+	log.Println("NewWinBotAdd.Run()")
+	sf.win.Bind("close_win", sf.onClose)
+	sf.win.Bind("add", sf.onBotAdd)
+	<-sf.win.Done() // Ожидание закрытия окна
+}
+
+// Добавляет пользователя по требованию
+func (sf *WinBotAdd) onBotAdd() {
+	log.Printf("NewWinBotAdd.onBotAdd()\n")
+	name := sf.win.Eval(`document.getElementById("/bot/name").value`).String()
+	pass := sf.win.Eval(`document.getElementById("/bot/pass").value`).String()
+	go sf.fnAdd(name, pass)
+	sf.onClose()
+}
+
+// Закрывает приложение
+func (sf *WinBotAdd) onClose() {
+	log.Println("NewWinBotAdd.onClose()")
+	sf.win.Close()
+}
+
+// close -- ожидает отмены глобального контекста
+func (sf *WinBotAdd) close() {
+	<-sf.desktop.CtxApp().Done()
+	log.Println("NewWinBotAdd.close()")
+	sf.win.Close()
+}

+ 69 - 0
desktop/dict_bot/win_bot_add/win_bot_add.html

@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html lang="ru">
+
+	<head>
+		<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+		<title>WarTank bot</title>
+		<meta name="author" content="SVI">
+		<style>
+			html {
+				height: 100%;
+			}
+
+			body {
+				margin: 0;
+				color: #fff;
+				/* Растягиваем body по высоте html */
+				min-height: 100%;
+				display: grid;
+				grid-template-rows: auto 1fr auto;
+			}
+
+			header {
+				background: rgb(88, 88, 184);
+			}
+
+			main {
+				background: rgb(141, 112, 112);
+			}
+
+			footer {
+				background: black;
+			}
+
+			.my-label {
+				display: inline-block;
+				background: rgb(12, 54, 56);
+				font-family: 'Courier New', Courier, monospace;
+				margin-top: 0.5em;
+				margin-left: 0.5em;
+			}
+		</style>
+	</head>
+
+	<body>
+		<header role="banner">
+			<b>WarTank bot [FunnySoft 2022]</b>
+		</header>
+		<main role="main">
+			<h2>Добавление бота</h2>
+			<div class="my-label">
+				Бот:<div>
+					Логин:<input id="/bot/name" type="text" />
+					Пароль:<input id="/bot/pass" type="text" />
+				</div>
+				<button type="button" name="add" value="Доб" onclick="add()">Добавить</button>
+				<div style="background:red;" id="/bot/err"></div>
+			</div>
+			<hr width="95%" height="4px">
+			<div>
+				<button style="background:red;" type="button" name="close" value="Закрыть"
+					onclick="close_win()">Закрыть</button>
+			</div>
+		</main>
+		<footer role="contentinfo">
+			<div class="footer">Окно добавления бота</div>
+		</footer>
+	</body>
+
+</html>

+ 211 - 0
desktop/dict_bot/win_bot_view/win_bot_view.go

@@ -0,0 +1,211 @@
+// package win_bot_view -- просмотр состояния бота
+package win_bot_view
+
+import (
+	_ "embed"
+	"fmt"
+	"log"
+	"net/url"
+	"runtime"
+	"time"
+
+	"github.com/zserge/lorca"
+
+	"wartank/pkg/types"
+)
+
+//go:embed win_bot_view.html
+var strWinHtml string
+
+// WinBotView -- окно просмотра бота
+type WinBotView struct {
+	desktop types.IDesktop
+	store   types.IStore
+	win     lorca.UI
+	ws      types.IWebSocket
+	name    string
+}
+
+// NewWinBotView -- возвращает новое окно просмотра бота
+func NewWinBotView(desktop types.IDesktop, name string) (*WinBotView, error) {
+	{ // Предусловия
+		if desktop == nil {
+			return nil, fmt.Errorf("NewWinBotView(): IDesktop == nil")
+		}
+		if name == "" {
+			return nil, fmt.Errorf("NewWinBotView(): name шы уьзен")
+		}
+	}
+
+	sf := &WinBotView{
+		desktop: desktop,
+		store:   desktop.Store(),
+		ws:      desktop.Ws(),
+		name:    name,
+	}
+
+	args := []string{}
+	if runtime.GOOS == "linux" {
+		args = append(args, "--class=Lorca")
+	}
+	var err error
+	sf.win, err = lorca.New("data:text/html,"+url.PathEscape(strWinHtml), "", 640, 480, args...)
+	if err != nil {
+		return nil, fmt.Errorf("NewWinBotView(): in create win, err=\n\t%w", err)
+	}
+	go sf.close()
+	return sf, nil
+}
+
+// Работает в отдельном потоке, главный цикл окна
+func (sf *WinBotView) Run() {
+	log.Println("WinBotView.Run()")
+	sf.win.Bind("close_win", sf.onClose)
+	for {
+		select {
+		case <-sf.win.Done(): // Ожидание закрытия окна
+			sf.close()
+			return
+		default: // Дежурный вывод информации
+			go sf.update()
+			time.Sleep(time.Millisecond * 500)
+		}
+	}
+}
+
+// Обновляет информацию в окне
+func (sf *WinBotView) update() {
+	dictReq := make(map[string]string)
+	dictReq["name"] = sf.name
+	dictResp, err := sf.ws.Call("/bot/status", dictReq)
+	if err != nil {
+		log.Printf("WinBotView.update(): in read bot(%q),err=\n\t%v\n", sf.name, err)
+		return
+	}
+
+	// log.Printf("WinBotView.update(): dictRes=%#v\n", dictResp)
+
+	{ // Имя
+		name := dictResp["/bot/name"]
+		js := fmt.Sprintf(
+			`function UpdateName(){
+			var _el=document.getElementById("/bot/name");
+			_el.innerText=%q
+			}
+		UpdateName()`, name)
+		sf.win.Eval(js)
+	}
+	{ // Если онлайн
+		isOnline := dictResp["/bot/online"]
+		js := fmt.Sprintf(
+			`function UpdateIsOnlime(){
+			var _el=document.getElementById("/bot/online");
+			_el.innerText=%q
+			}
+			UpdateIsOnlime()`, isOnline)
+		sf.win.Eval(js)
+	}
+	{ // Топливо
+		fuel := dictResp["/bot/fuel"]
+		js := fmt.Sprintf(
+			`function UpdateFuel(){
+			var _el=document.getElementById("/bot/fuel");
+			_el.innerText=%q
+			}
+			UpdateFuel()`, fuel)
+		sf.win.Eval(js)
+	}
+	{ // Золото
+		gold := dictResp["/bot/gold"]
+		js := fmt.Sprintf(
+			`function UpdateGold(){
+			var _el=document.getElementById("/bot/gold");
+			_el.innerText=%q
+			}
+			UpdateGold()`, gold)
+		sf.win.Eval(js)
+	}
+	{ // Серебро
+		silver := dictResp["/bot/silver"]
+		js := fmt.Sprintf(
+			`function UpdateSilver(){
+			var _el=document.getElementById("/bot/silver");
+			_el.innerText=%q
+			}
+			UpdateSilver()`, silver)
+		sf.win.Eval(js)
+	}
+	{ // Серебро-время
+		silverTime := dictResp["/bank/silver-time"]
+		js := fmt.Sprintf(
+			`function UpdateSilverTime(){
+			var _el=document.getElementById("/bank/silver-time");
+			_el.innerText=%q
+			}
+			UpdateSilverTime()`, silverTime)
+		sf.win.Eval(js)
+	}
+	{ // Серебро-режим
+		silverMode := dictResp["/bank/silver-mode"]
+		js := fmt.Sprintf(
+			`function UpdateSilverMode(){
+			var _el=document.getElementById("/bank/silver-mode");
+			_el.innerText=%q
+			}
+			UpdateSilverMode()`, silverMode)
+		sf.win.Eval(js)
+	}
+	{ // Серебро-всего
+		silverAll := dictResp["/angar/silver-all"]
+		js := fmt.Sprintf(
+			`function UpdateSilverAll(){
+			var _el=document.getElementById("/angar/silver-all");
+			_el.innerText=%q
+			}
+			UpdateSilverAll()`, silverAll)
+		sf.win.Eval(js)
+	}
+	{ // Шахта-время
+		mineTime := dictResp["/bot/mine-time"]
+		js := fmt.Sprintf(
+			`function UpdateMineTime(){
+			var _el=document.getElementById("/bot/mine-time");
+			_el.innerText=%q
+			}
+			UpdateMineTime()`, mineTime)
+		sf.win.Eval(js)
+	}
+	{ // Шахта-режим
+		mineMode := dictResp["/bot/mine-mode"]
+		js := fmt.Sprintf(
+			`function UpdateMineMode(){
+			var _el=document.getElementById("/bot/mine-mode");
+			_el.innerText=%q
+			}
+			UpdateMineMode()`, mineMode)
+		sf.win.Eval(js)
+	}
+	{ // Шахта-руда
+		mineRuda := dictResp["/mine/ruda"]
+		js := fmt.Sprintf(
+			`function UpdateMineRuda(){
+			var _el=document.getElementById("/mine/ruda");
+			_el.innerText=%q
+			}
+			UpdateMineRuda()`, mineRuda)
+		sf.win.Eval(js)
+	}
+}
+
+// Закрывает окно
+func (sf *WinBotView) onClose() {
+	log.Println("WinBotView.onClose()")
+	sf.win.Close()
+}
+
+// close -- ожидает отмены глобального контекста
+func (sf *WinBotView) close() {
+	<-sf.desktop.CtxApp().Done()
+	log.Println("WinBotView.close()")
+	sf.win.Close()
+}

+ 110 - 0
desktop/dict_bot/win_bot_view/win_bot_view.html

@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html lang="ru">
+
+	<head>
+		<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+		<title>WarTank bot</title>
+		<meta name="author" content="SVI">
+		<style>
+			html {
+				height: 100%;
+			}
+
+			body {
+				margin: 0;
+				color: #fff;
+				/* Растягиваем body по высоте html */
+				min-height: 100%;
+				display: grid;
+				grid-template-rows: auto 1fr auto;
+			}
+
+			header {
+				background: rgb(88, 88, 184);
+			}
+
+			main {
+				background: rgb(141, 112, 112);
+			}
+
+			footer {
+				background: black;
+			}
+
+			.my-label {
+				display: inline-block;
+				background: rgb(12, 54, 56);
+				font-family: 'Courier New', Courier, monospace;
+				margin-top: 0.5em;
+				margin-left: 6px;
+				border-radius: 3px;
+			}
+		</style>
+	</head>
+
+	<body>
+		<header role="banner">
+			<b>WarTank bot [FunnySoft 2022]</b>
+		</header>
+		<main role="main">
+			<b>Просмотр бота</b>
+			<div>
+				<div class="my-label">Логин:
+					<div id="/bot/name" type="text"></div>
+				</div>
+				<div class="my-label">Онлайн:
+					<div id="/bot/online" type="text"></div>
+				</div>
+				<div class="my-label">Топливо:
+					<div id="/bot/fuel" type="text"></div>
+				</div>
+				<div class="my-label">Золото:
+					<div id="/bot/gold" type="text"></div>
+				</div>
+				<div class="my-label">Серебро-бот:
+					<div id="/bot/silver" type="text"></div>
+				</div>
+				<hr width="95%" height="4px">
+			</div>
+			<b>База</b>
+			<hr width="95%" height="4px">
+			<div>
+				<div>
+					<b>Банк</b><br>
+					<div class="my-label">Серебро-время:
+						<div id="/bank/silver-time" type="text"></div>
+					</div>
+					<div class="my-label">Серебро-режим:
+						<div id="/bank/silver-mode" type="text"></div>
+					</div>
+					<div class="my-label">Серебро-всего:
+						<div id="/angar/silver-all" type="text"></div>
+					</div>
+					<hr width="95%" height="4px">
+				</div>
+				<div>
+					<b>Шахта</b><br>
+					<div class="my-label">Шахта-время:
+						<div id="/bot/mine-time" type="text"></div>
+					</div>
+					<div class="my-label">Шахта-режим:
+						<div id="/bot/mine-mode" type="text"></div>
+					</div>
+					<div class="my-label">Руда:
+						<div id="/mine/ruda" type="text"></div>
+					</div>
+					<hr width="95%" height="4px">
+				</div>
+			</div>
+			<div style="background:red;" id="/bot/err"></div>
+			<div>
+				<button style="background:red;" type="button" name="close" value="Закрыть"
+					onclick="close_win()">Закрыть</button>
+			</div>
+		</main>
+		<footer role="contentinfo">
+			<div class="footer">Окно просмотра бота</div>
+		</footer>
+	</body>
+
+</html>

+ 127 - 0
desktop/dict_bot/win_bots/win_bots.go

@@ -0,0 +1,127 @@
+// package win_bots -- окно управления ботами
+package win_bots
+
+import (
+	_ "embed"
+	"fmt"
+	"log"
+	"net/url"
+	"runtime"
+
+	"github.com/zserge/lorca"
+
+	"wartank/pkg/types"
+)
+
+//go:embed win_bots.html
+var strWinHtml string
+
+// WinBots -- окно управления ботами
+type WinBots struct {
+	desktop types.IDesktop
+	store   types.IStore
+	win     lorca.UI
+	ws      types.IWebSocket
+	fnAdd   func()
+	fnView  func(nameBot string)
+	dictBot map[string]string // Список ботов
+}
+
+// NewWinBots -- возвращает новое окно управления ботами
+func NewWinBots(desktop types.IDesktop, fnAdd func(), fnView func(nameBot string), dictBot map[string]string) (*WinBots, error) {
+	{ // Предусловия
+		if desktop == nil {
+			return nil, fmt.Errorf("NewWinBots(): IDesktop == nil")
+		}
+		if fnAdd == nil {
+			return nil, fmt.Errorf("NewWinBots(): fnAdd == nil")
+		}
+		if fnView == nil {
+			return nil, fmt.Errorf("NewWinBots(): fnView == nil")
+		}
+		if dictBot == nil {
+			return nil, fmt.Errorf("NewWinBots(): dictBot == nil")
+		}
+	}
+
+	sf := &WinBots{
+		desktop: desktop,
+		store:   desktop.Store(),
+		ws:      desktop.Ws(),
+		fnAdd:   fnAdd,
+		fnView:  fnView,
+		dictBot: dictBot,
+	}
+
+	args := []string{}
+	if runtime.GOOS == "linux" {
+		args = append(args, "--class=Lorca")
+	}
+	var err error
+	sf.win, err = lorca.New("data:text/html,"+url.PathEscape(strWinHtml), "", 640, 480, args...)
+	if err != nil {
+		return nil, fmt.Errorf("NewWinBots(): in create win, err=\n\t%w", err)
+	}
+	go sf.close()
+	return sf, nil
+}
+
+// Обновляет список ботов
+func (sf *WinBots) UpdateList(dictBot map[string]string) {
+	log.Println("WinBots.UpdateList()")
+	sf.dictBot = dictBot
+	sf.setBots()
+}
+
+// Работает в отдельном потоке, главный цикл окна
+func (sf *WinBots) Run() {
+	log.Println("WinBots.Run()")
+	sf.win.Bind("close_win", sf.onClose)
+	sf.win.Bind("user_add", sf.onUsersAdd)
+	sf.win.Bind("user_view", sf.onUsersAdd)
+	sf.setBots()
+	<-sf.win.Done() // Ожидание закрытия окна
+}
+
+// Заполняет список ботов
+func (sf *WinBots) setBots() {
+	log.Println("WinBots.setBots()")
+	strList := ""
+	count := 0
+	for key := range sf.dictBot {
+		strCount := fmt.Sprint(count)
+		strList += `<div style="color:#eee;margin-top:3px;" id="/bot/` + strCount + `">` + key +
+			`&nbsp;&nbsp;&nbsp;<button type="button" name="add" value="Посм" onclick="bot_` + strCount + `()">Посмотреть</button></div>`
+		sf.win.Bind("bot_"+strCount, func() {
+			go sf.fnView(key)
+		})
+		count++
+	}
+	js := fmt.Sprintf(`
+	function SetBotList(){
+		var _el=document.getElementById("/bot/list");
+		_el.innerHTML=%q
+	}
+	SetBotList()
+	`, strList)
+	sf.win.Eval(js)
+}
+
+// Добавляет бота по требованию
+func (sf *WinBots) onUsersAdd() {
+	log.Printf("WinBots.onUsersAdd()\n")
+	go sf.fnAdd()
+}
+
+// Закрывает приложение
+func (sf *WinBots) onClose() {
+	log.Println("WinBots.onClose()")
+	sf.win.Close()
+}
+
+// close -- ожидает отмены глобального контекста
+func (sf *WinBots) close() {
+	<-sf.desktop.CtxApp().Done()
+	log.Println("WinBots.close()")
+	sf.win.Close()
+}

+ 68 - 0
desktop/dict_bot/win_bots/win_bots.html

@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html lang="ru">
+
+	<head>
+		<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+		<title>WarTank bot</title>
+		<meta name="author" content="SVI">
+		<style>
+			html {
+				height: 100%;
+			}
+
+			body {
+				margin: 0;
+				color: #fff;
+				/* Растягиваем body по высоте html */
+				min-height: 100%;
+				display: grid;
+				grid-template-rows: auto 1fr auto;
+			}
+
+			header {
+				background: rgb(88, 88, 184);
+			}
+
+			main {
+				background: rgb(141, 112, 112);
+			}
+
+			footer {
+				background: black;
+			}
+
+			.my-label {
+				display: inline-block;
+				background: rgb(12, 54, 56);
+				font-family: 'Courier New', Courier, monospace;
+				margin-top: 0.5em;
+				margin-left: 0.5em;
+			}
+		</style>
+	</head>
+
+	<body>
+		<header role="banner">
+			<b>WarTank bot [FunnySoft 2022]</b>
+		</header>
+		<main role="main">
+			<h2>Управление ботами</h2>
+			<div class="my-label">
+				Боты:<div id="/bot/list"></div>
+				<hr width="95%" height="4px">
+				<button type="button" name="add" value="Доб" onclick="user_add()">Добавить</button>
+				<div style="background:red;" id="/bot/list/err"></div>
+			</div>
+			<hr width="95%" height="4px">
+			<div>
+				<br>
+				<button style="background:red;" type="button" name="close" value="Закрыть"
+					onclick="close_win()">Закрыть</button>
+			</div>
+		</main>
+		<footer role="contentinfo">
+			<div class="footer">Окно управления игроками игры.</div>
+		</footer>
+	</body>
+
+</html>

+ 177 - 0
desktop/root/root.go

@@ -0,0 +1,177 @@
+// package root -- объект рута
+package root
+
+import (
+	"fmt"
+	"log"
+	"strings"
+	"time"
+
+	"wartank/desktop/root/win_root_login"
+	"wartank/desktop/root/win_root_set"
+	"wartank/pkg/components/safebool"
+	"wartank/pkg/types"
+)
+
+// Root -- объект рута
+type Root struct {
+	desktop      types.IDesktop
+	ws           types.IWebSocket
+	isLogin      *safebool.SafeBool
+	isPassSet    *safebool.SafeBool
+	winRootMake  *win_root_set.WinRootMake
+	winRootLogin *win_root_login.WinRootLogin
+}
+
+// NewRoot -- вовзращает новый объект рута
+func NewRoot(desktop types.IDesktop) (*Root, error) {
+	log.Println("NewRoot()")
+	if desktop == nil {
+		return nil, fmt.Errorf("NewRoot(): IDesktop == nil")
+	}
+	sf := &Root{
+		desktop:   desktop,
+		ws:        desktop.Ws(),
+		isLogin:   safebool.NewSafeBool(),
+		isPassSet: safebool.NewSafeBool(),
+	}
+	sf.checkRoot()
+	return sf, nil
+}
+
+// Проверяет, есть ли рут в системе
+func (sf *Root) checkRoot() {
+	log.Println("Root.checkRoot()")
+	for { // Цикл ожидания рут-пароля (если его нет)
+		isExists := sf.checkIsExists()
+		if !isExists {
+			sf.makePassRoot() // Раз пользователь сам создал пароль, то и проверка логина не нужна
+			return
+		}
+		// Пароля уже есть, проверяем логин
+		sf.checkLogin()
+		return
+	}
+
+	// Запрос пароля рута
+
+	// winUsers, err := win_users.NewWinUsers(sf.desktop)
+	// if err != nil {
+	// 	log.Printf("Root.checkRoot(): in create WinUsers, err=\n\t%v\n", err)
+	// 	return
+	// }
+	// go winUsers.Run()
+}
+
+// Проверка логина
+func (sf *Root) checkLogin() {
+	log.Println("Root.checkLogin()")
+	sf.makeWinLogin()
+	strPass := sf.winRootLogin.GetPass()
+	dictReq := make(map[string]string)
+	dictReq["pass"] = strPass
+	for {
+		dictResp, err := sf.ws.Call("/root/password/check", dictReq)
+		if err != nil {
+			err = fmt.Errorf("Root.checkLogin(): при выполнении запроса, err=\n\t%w", err)
+			sf.winRootLogin.SetError(err)
+			time.Sleep(time.Second * 2)
+			continue
+		}
+		log.Printf("WinRootLogin.onCheckPass(): resp=%q\n", dictResp)
+		strErr := dictResp["err"]
+		if strErr != "" {
+			err = fmt.Errorf("Root.checkLogin(): при сравнении паролей, err=\n\t%q", strErr)
+			sf.winRootLogin.SetError(err)
+			time.Sleep(time.Second * 2)
+			for {
+				oldPass := strPass
+				strPass = sf.winRootLogin.GetPass()
+				if oldPass != strPass {
+					break
+				}
+				time.Sleep(time.Millisecond * 100)
+			}
+			dictReq["pass"] = strPass
+			continue
+		}
+		// Всё ништяк
+		sf.winRootLogin.Close()
+		sf.isPassSet.Set()
+		return
+	}
+}
+
+// Показывает окно логина
+func (sf *Root) makeWinLogin() {
+	log.Println("Root.makeWinLogin()")
+	var err error
+	for {
+		sf.winRootLogin, err = win_root_login.NewWinRootLogin(sf.desktop)
+		if err != nil {
+			log.Printf("Root.makeWinLogin(): in create WinRootLogin, err=\n\t%v\n", err)
+			time.Sleep(time.Second * 2)
+			continue
+		}
+		break
+	}
+	go sf.winRootLogin.Run()
+}
+
+// Проверка на существование пароля рута
+func (sf *Root) checkIsExists() bool {
+	log.Println("Root.checkIsExists()")
+	var strErr string
+	for {
+		dictRoot, err := sf.ws.Read("/root/password/is_exists")
+		if err != nil {
+			log.Printf("Root.checkRoot(): in get password root, err=\n\t%v\n", err)
+			time.Sleep(time.Second * 2)
+			continue
+		}
+		strErr = dictRoot["err"]
+		if strings.Contains(strErr, "leveldb: not found") {
+			log.Printf("Root.checkRoot(): первый запуск, пароль рута не задан\n")
+			return false
+		}
+		strIsExists := dictRoot["/root/password/is_exists"]
+		if strIsExists == "true" {
+			return true
+		}
+		return false
+	}
+}
+
+// Создаёт пароль рута
+func (sf *Root) makePassRoot() {
+	log.Println("Root.makePassRoot()")
+	sf.makeWinRootMake()
+	strPass := sf.winRootMake.GetPass()
+	dictReq := make(map[string]string)
+	dictReq["pass"] = strPass
+	for {
+		err := sf.ws.Write("/root/password/set", dictReq)
+		if err != nil {
+			sf.winRootMake.SetError(err)
+			time.Sleep(time.Second * 2)
+			continue
+		}
+		sf.winRootMake.Close()
+		break
+	}
+}
+
+// Показывает окно создания пароля рута
+func (sf *Root) makeWinRootMake() {
+	var err error
+	for {
+		sf.winRootMake, err = win_root_set.NewWinRootMake(sf.desktop)
+		if err == nil {
+			go sf.winRootMake.Run()
+			return
+		}
+		log.Printf("Root.makeWinRootMake(): in create WinRoot, err=\n\t%v\n", err)
+		time.Sleep(time.Second * 2)
+		continue
+	}
+}

+ 106 - 0
desktop/root/win_root_login/win_root_login.go

@@ -0,0 +1,106 @@
+// package win_root_login -- запрашивает рутовый пароль при запуске
+package win_root_login
+
+import (
+	_ "embed"
+	"fmt"
+	"log"
+	"net/url"
+	"runtime"
+	"sync"
+	"time"
+
+	"github.com/zserge/lorca"
+
+	"wartank/pkg/types"
+)
+
+//go:embed win_root_login.html
+var strWinHtml string
+
+// WinRootLogin --  запрашивает рутовый пароль на приложение
+type WinRootLogin struct {
+	desktop  types.IDesktop
+	store    types.IStore
+	win      lorca.UI
+	ws       types.IWebSocket
+	block    sync.Mutex
+	rootPass string
+}
+
+// NewWinRootLogin -- возвращает новое окно запроса пароля для рута
+func NewWinRootLogin(desktop types.IDesktop) (*WinRootLogin, error) {
+	if desktop == nil {
+		return nil, fmt.Errorf("NewWinRootLogin(): IDesktop == nil")
+	}
+	sf := &WinRootLogin{
+		desktop: desktop,
+		store:   desktop.Store(),
+		ws:      desktop.Ws(),
+	}
+
+	args := []string{}
+	if runtime.GOOS == "linux" {
+		args = append(args, "--class=Lorca")
+	}
+	var err error
+	sf.win, err = lorca.New("data:text/html,"+url.PathEscape(strWinHtml), "", 640, 480, args...)
+	if err != nil {
+		return nil, fmt.Errorf("NewWinRootLogin(): in create win, err=\n\t%w", err)
+	}
+	go sf.close()
+
+	return sf, nil
+}
+
+// GetPass -- возвращает полученный пароль из формы
+func (sf *WinRootLogin) GetPass() string {
+	fnCheck := func() bool {
+		sf.block.Lock()
+		defer sf.block.Unlock()
+		return sf.rootPass == ""
+	}
+	for fnCheck() {
+		time.Sleep(time.Millisecond * 20)
+	}
+	sf.block.Lock()
+	defer sf.block.Unlock()
+	return sf.rootPass
+}
+
+func (sf *WinRootLogin) SetError(err error) {
+	js := fmt.Sprintf(`
+	function SetErrorGet(){
+		var _el=document.getElementById("/root/password/err");
+		_el.innerText="WinRootLogin.onCheckPass(): ошибка проверки пароля, err=\n\t%v"
+	}
+	SetErrorGet()
+	`, err)
+	sf.win.Eval(js)
+}
+
+func (sf *WinRootLogin) Close() {
+	sf.win.Close()
+}
+
+// Работает в отдельном потоке, главный цикл окна
+func (sf *WinRootLogin) Run() {
+	log.Println("WinRootLogin.Run()")
+	sf.win.Bind("check_pass", sf.onCheckPass)
+	<-sf.win.Done() // Ожидание закрытия окна
+}
+
+// Проверяет пароль рута
+func (sf *WinRootLogin) onCheckPass() {
+	log.Printf("WinRootLogin.onCheckPass()\n")
+	sf.block.Lock()
+	defer sf.block.Unlock()
+	sf.rootPass = sf.win.Eval(`document.getElementById("/root/password/val").value`).String()
+}
+
+// close -- ожидает отмены глобального контекста
+func (sf *WinRootLogin) close() {
+	<-sf.desktop.CtxApp().Done()
+	log.Println("WinRoot.close()")
+	sf.win.Close()
+}

+ 62 - 0
desktop/root/win_root_login/win_root_login.html

@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html lang="ru">
+
+	<head>
+		<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+		<title>WarTank bot</title>
+		<meta name="author" content="SVI">
+		<style>
+			html {
+				height: 100%;
+			}
+
+			body {
+				margin: 0;
+				color: #fff;
+				/* Растягиваем body по высоте html */
+				min-height: 100%;
+				display: grid;
+				grid-template-rows: auto 1fr auto;
+			}
+
+			header {
+				background: rgb(88, 88, 184);
+			}
+
+			main {
+				background: rgb(141, 112, 112);
+			}
+
+			footer {
+				background: black;
+			}
+
+			.my-label {
+				display: inline-block;
+				background: rgb(12, 54, 56);
+				font-family: 'Courier New', Courier, monospace;
+				margin-top: 0.5em;
+				margin-left: 0.5em;
+			}
+		</style>
+	</head>
+
+	<body>
+		<header role="banner">
+			<b>WarTank bot [FunnySoft 2022]</b>
+		</header>
+		<main role="main">
+			<h2>Ввод пароля пользователя</h2>
+			<div class="my-label">
+				Пароль:<input id="/root/password/val" type="text" />
+				<button type="button" name="save" value="Вход" onclick="check_pass()">Вход</button>
+				<div style="background:red;" id="/root/password/err"></div>
+			</div>
+			<hr width="95%" height="4px">
+		</main>
+		<footer role="contentinfo">
+			<div class="footer">Для правильной работы программы необходимо запустить <code>server</code>.</div>
+		</footer>
+	</body>
+
+</html>

+ 133 - 0
desktop/root/win_root_set/win_root_set.go

@@ -0,0 +1,133 @@
+// package win_root_set -- задаёт рутовый пароль на приложение
+package win_root_set
+
+import (
+	_ "embed"
+	"fmt"
+	"log"
+	"net/url"
+	"runtime"
+	"sync"
+	"time"
+
+	"github.com/zserge/lorca"
+
+	"wartank/pkg/types"
+)
+
+//go:embed win_root_set.html
+var strWinHtml string
+
+// WinRootMake --  задаёт рутовый пароль на приложение
+type WinRootMake struct {
+	desktop  types.IDesktop
+	store    types.IStore
+	win      lorca.UI
+	ws       types.IWebSocket
+	rootPass string // Пароль рута из формы
+	block    sync.Mutex
+}
+
+// NewWinRootMake -- возвращает новое окно пароля для рута
+func NewWinRootMake(desktop types.IDesktop) (*WinRootMake, error) {
+	if desktop == nil {
+		return nil, fmt.Errorf("NewWinRootMake(): IDesktop == nil")
+	}
+	sf := &WinRootMake{
+		desktop: desktop,
+		store:   desktop.Store(),
+		ws:      desktop.Ws(),
+	}
+
+	args := []string{}
+	if runtime.GOOS == "linux" {
+		args = append(args, "--class=Lorca")
+	}
+	var err error
+	sf.win, err = lorca.New("data:text/html,"+url.PathEscape(strWinHtml), "", 640, 480, args...)
+	if err != nil {
+		return nil, fmt.Errorf("WinRoot(): in create win, err=\n\t%w", err)
+	}
+	go sf.close()
+
+	return sf, nil
+}
+
+// GetPass -- возвращает полученный пароль из формы
+func (sf *WinRootMake) GetPass() string {
+	fnCheck := func() bool {
+		sf.block.Lock()
+		defer sf.block.Unlock()
+		return len(sf.rootPass) > 7
+	}
+	for !fnCheck() {
+		time.Sleep(time.Millisecond * 20)
+	}
+	sf.block.Lock()
+	defer sf.block.Unlock()
+	return sf.rootPass
+}
+
+// SetError -- устанавливает признак ошибки при операциях с паролем
+func (sf *WinRootMake) SetError(err error) {
+	js := fmt.Sprintf(`
+	function SetErrorSave(){
+		var _el=document.getElementById("/root/password/err");
+		_el.innerText="WinRootMake.onSetPass(): ошибка передачи при сохранении пароля, err=\n\t%v"
+	}
+	SetErrorSave()`, err)
+	sf.win.Eval(js)
+}
+
+func (sf *WinRootMake) Close() {
+	sf.win.Close()
+}
+
+// Работает в отдельном потоке, главный цикл окна
+func (sf *WinRootMake) Run() {
+	log.Println("WinRootMake.Run()")
+	sf.win.Bind("close_win", sf.onClose)
+	sf.win.Bind("set_pass", sf.onSetPass)
+	<-sf.win.Done() // Ожидание закрытия окна
+}
+
+// Сохраняет пароль рута
+func (sf *WinRootMake) onSetPass() {
+	log.Printf("WinRootMake.onSetPass()\n")
+	sf.block.Lock()
+	defer sf.block.Unlock()
+	sf.rootPass = sf.win.Eval(`document.getElementById("/root/password/val").value`).String()
+	if len(sf.rootPass) < 8 {
+		js := `
+		function SetError(){
+			var _el=document.getElementById("/root/password/err");
+			_el.innerText="WinRootMake.onSetPass(): пароль слишком короткий"
+		}
+		SetError()
+		`
+		sf.win.Eval(js)
+		return
+	}
+	js := `
+	function ResetError(){
+		var _el=document.getElementById("/root/password/err");
+		_el.innerText=""
+	}
+	ResetError()
+	`
+	sf.win.Eval(js)
+	log.Printf("WinRootMake.onSetPass(): pass=%q\n", sf.rootPass)
+}
+
+// Закрывает приложение
+func (sf *WinRootMake) onClose() {
+	log.Println("WinRootMake.onClose()")
+	sf.win.Close()
+}
+
+// close -- ожидает отмены глобального контекста
+func (sf *WinRootMake) close() {
+	<-sf.desktop.CtxApp().Done()
+	log.Println("WinRootMake.close()")
+	sf.win.Close()
+}

+ 67 - 0
desktop/root/win_root_set/win_root_set.html

@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html lang="ru">
+
+	<head>
+		<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+		<title>WarTank bot</title>
+		<meta name="author" content="SVI">
+		<style>
+			html {
+				height: 100%;
+			}
+
+			body {
+				margin: 0;
+				color: #fff;
+				/* Растягиваем body по высоте html */
+				min-height: 100%;
+				display: grid;
+				grid-template-rows: auto 1fr auto;
+			}
+
+			header {
+				background: rgb(88, 88, 184);
+			}
+
+			main {
+				background: rgb(141, 112, 112);
+			}
+
+			footer {
+				background: black;
+			}
+
+			.my-label {
+				display: inline-block;
+				background: rgb(12, 54, 56);
+				font-family: 'Courier New', Courier, monospace;
+				margin-top: 0.5em;
+				margin-left: 0.5em;
+			}
+		</style>
+	</head>
+
+	<body>
+		<header role="banner">
+			<b>WarTank bot [FunnySoft 2022]</b>
+		</header>
+		<main role="main">
+			<h2>ВНИМАНИЕ!<br>Этот пароль будет установлен на весь сервер!</h2>
+			<div id="status"><b>Пароль для root:</b></div>
+			<div class="my-label">
+				Пароль:<input id="/root/password/val" type="text" />
+				<button type="button" name="save" value="Сохранить" onclick="set_pass()">Сохранить</button>
+				<div style="background:red;" id="/root/password/err"></div>
+			</div>
+			<hr width="95%" height="4px">
+			<div>
+				<button style="background:red;" type="button" name="close" value="Закрыть"
+					onclick="close_win()">Закрыть</button>
+			</div>
+		</main>
+		<footer role="contentinfo">
+			<div class="footer">Для правильной работы программы необходимо запустить <code>server</code>.</div>
+		</footer>
+	</body>
+
+</html>

+ 76 - 0
desktop/store_net/store_net.go

@@ -0,0 +1,76 @@
+// package store_net -- реализация сетевого хранилища
+package store_net
+
+import (
+	"fmt"
+
+	"github.com/sirupsen/logrus"
+
+	"wartank/pkg/types"
+)
+
+// StoreNet -- реализвция сетевого хранилища
+type StoreNet struct {
+	desktop types.IDesktop
+	ws      types.IWebSocket
+}
+
+// NewStoreNet -- возвращает новый объект сетевого хранилища
+func NewStoreNet(desktop types.IDesktop) (*StoreNet, error) {
+	logrus.Infof("NewStoreNet()")
+	if desktop == nil {
+		return nil, fmt.Errorf("NewStoreNet(): IDesktop = =nil")
+	}
+	sf := &StoreNet{
+		desktop: desktop,
+		ws:      desktop.Ws(),
+	}
+	return sf, nil
+}
+
+// Find -- поиск объекта в хранилище
+func (sf *StoreNet) Find(prefix string) (map[string]string, error) {
+	logrus.Debugf("StoreNet.Find()")
+	mapReq := make(map[string]string)
+	mapReq["cmd"] = "/store/find"
+	mapReq["key"] = prefix
+
+	dictResp, err := sf.ws.Call("/store/find", mapReq)
+	if err != nil {
+		return nil, fmt.Errorf("StoreNet.Find(): in write StoreNet, err=\n\t%w", err)
+	}
+	strErr := dictResp["err"]
+	if strErr != "" {
+		return nil, fmt.Errorf("StoreNet.Find(): in response, err=\n\t%v", strErr)
+	}
+	return dictResp, nil
+}
+
+// Get -- получает содержимое записи по ключу
+func (sf *StoreNet) Get(key string) (string, error) {
+	logrus.Debugf("StoreNet.Get()")
+
+	dictResp, err := sf.ws.Read(key)
+	if err != nil {
+		return "", fmt.Errorf("StoreNet.Get(): in read StoreNet, err=\n\t%w", err)
+	}
+	strErr := dictResp["err"]
+	if string(strErr) != "" {
+		return "", fmt.Errorf("StoreNet.Get(): in response, err=\n\t%v", strErr)
+	}
+	strResp := dictResp["res"]
+	return strResp, nil
+}
+
+// Put -- помещает запись в хранилище
+func (sf *StoreNet) Put(key string, val string) error {
+	logrus.Debugf("StoreNet.Put()")
+	dictReq := make(map[string]string)
+	dictReq["key"] = key
+	dictReq["val"] = val
+	err := sf.ws.Write("/store/put", dictReq)
+	if err != nil {
+		return fmt.Errorf("StoreNet.Get(): in write StoreNet, err=\n\t%w", err)
+	}
+	return nil
+}

+ 218 - 0
desktop/web_socket/web_socket.go

@@ -0,0 +1,218 @@
+// package web_socket -- реализация высокоуровнего веб-сокета для работы десктопа
+package web_socket
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/url"
+	"os"
+	"sync"
+	"time"
+
+	"github.com/gorilla/websocket"
+
+	"wartank/pkg/components/safebool"
+	"wartank/pkg/types"
+)
+
+const (
+	strWebSocket = "web_socket"
+	TypeMsgBin   = 2
+)
+
+// WebSocket -- реализация высокоуровнего веб-сокета для работы десктопа
+type WebSocket struct {
+	kern      types.IKernel
+	slog      types.ISlog
+	url       string
+	isConnect *safebool.SafeBool
+	ws        *websocket.Conn
+	block     sync.RWMutex
+}
+
+// NewWebSocket -- возвращает новый веб-сокет
+func NewWebSocket(kern types.IKernel) (*WebSocket, error) {
+	log.Println("NewWebSocket()")
+	if kern == nil {
+		return nil, fmt.Errorf("NewWebSocket(): IKernel == nil")
+	}
+	url := os.Getenv("SERVER_URL")
+	if url == "" {
+		return nil, fmt.Errorf("NewWebSocket(): env SERVER_URL not set")
+	}
+	sf := &WebSocket{
+		kern:      kern,
+		slog:      kern.Slog(),
+		url:       url,
+		isConnect: safebool.NewSafeBool(),
+	}
+
+	sf.connect()
+	go sf.close()
+	return sf, nil
+}
+
+// Подключает веб-сокет к серверу
+func (sf *WebSocket) connect() {
+	log.Println("WebSocket.connect()")
+	fnConnect := func() {
+		u := url.URL{Scheme: "ws", Host: sf.url, Path: "/api/ws"}
+		strUrl := u.String()
+		log.Printf("WebSocket.connect(): wait connect to %q\n", strUrl)
+		var err error
+		sf.ws, _, err = websocket.DefaultDialer.Dial(strUrl, nil)
+		if err != nil {
+			log.Printf("WebSocket.connect(): in dial, err=\n\t%v\n", err)
+			time.Sleep(time.Second * 2)
+			return
+		}
+		sf.isConnect.Set()
+		log.Println("WebSocket.connect(): ok")
+	}
+	for !sf.isConnect.Get() {
+		select {
+		case <-sf.kern.CtxApp().Done():
+			return
+		default:
+			fnConnect()
+		}
+
+	}
+}
+
+// Read -- потокобезопасное чтение топика сервера
+func (sf *WebSocket) Read(topic string) (map[string]string, error) {
+	sf.block.Lock()
+	defer sf.block.Unlock()
+	dictResp, err := sf.read(topic)
+	if err != nil {
+		return nil, fmt.Errorf("WebSocket.Read(): in read, err=\n\t%w", err)
+	}
+	return dictResp, nil
+}
+
+// Скрытая потоко-небезопасна функция
+func (sf *WebSocket) read(topic string) (dictResp map[string]string, err error) {
+	var binResp []byte
+	for {
+		dictReq := make(map[string]string)
+		dictReq["topic"] = topic
+		binReq, err := json.Marshal(dictReq)
+		if err != nil {
+			return nil, fmt.Errorf("WebSocket.read(): in marshall topic(%q), err=\n\t%w", topic, err)
+		}
+		err = sf.ws.WriteMessage(TypeMsgBin, binReq)
+		if err != nil {
+			sf.slog.Errorf("WebSocket.read(): in write msg, err=\n\t%v\n", err)
+			sf.ws.Close()
+			sf.isConnect.Reset()
+			sf.connect()
+			continue
+		}
+
+		_, binResp, err = sf.ws.ReadMessage()
+		if err != nil {
+			sf.slog.Errorf("WebSocket.read(): in read msg, err=\n\t%v\n", err)
+			sf.ws.Close()
+			sf.isConnect.Reset()
+			sf.connect()
+			continue
+		}
+		break
+	}
+	dictResp = make(map[string]string)
+	err = json.Unmarshal(binResp, &dictResp)
+	if err != nil {
+		return nil, fmt.Errorf("WebSocket.read(): in unmarshal binResp, err=\n\t%w", err)
+	}
+	return dictResp, nil
+}
+
+// Write -- потокобезопасная запись топика
+func (sf *WebSocket) Write(topic string, dictReq map[string]string) error {
+	sf.block.Lock()
+	defer sf.block.Unlock()
+	err := sf.write(topic, dictReq)
+	if err != nil {
+		return fmt.Errorf("WebSocket.Write(): in write, err=\n\t%w", err)
+	}
+	return nil
+}
+
+// Скрытая потоко-небезопасна функция
+func (sf *WebSocket) write(topic string, dictReq map[string]string) error {
+	dictReq["topic"] = topic
+	binData, err := json.Marshal(dictReq)
+	if err != nil {
+		return fmt.Errorf("WebSocket.write(): in marshal msg, err=\n\t%w", err)
+	}
+	for {
+		err = sf.ws.WriteMessage(TypeMsgBin, binData)
+		if err != nil {
+			sf.slog.Errorf("WebSocket.write(): in write msg, err=\n\t%v\n", err)
+			sf.isConnect.Reset()
+			sf.ws.Close()
+			sf.connect()
+			continue
+		}
+		return nil
+	}
+}
+
+// IsConnect -- потокобезопасный признак подключенности сервера
+func (sf *WebSocket) IsConnect() bool {
+	sf.block.RLock()
+	defer sf.block.RUnlock()
+	return sf.isConnect.Get()
+}
+
+// Call -- потокобезопасный вызов удалённого топика
+func (sf *WebSocket) Call(topic string, dictReq map[string]string) (dictResp map[string]string, err error) {
+	sf.block.Lock()
+	defer sf.block.Unlock()
+	var binResp []byte
+	for {
+		dictReq["topic"] = topic
+		binReq, err := json.Marshal(dictReq)
+		if err != nil {
+			return nil, fmt.Errorf("WebSocket.Call(): in marshall topic(%q), err=\n\t%w", topic, err)
+		}
+		err = sf.ws.WriteMessage(TypeMsgBin, binReq)
+		if err != nil {
+			sf.slog.Errorf("WebSocket.Call(): in write msg, err=\n\t%v\n", err)
+			sf.ws.Close()
+			sf.isConnect.Reset()
+			sf.connect()
+			continue
+		}
+
+		_, binResp, err = sf.ws.ReadMessage()
+		if err != nil {
+			sf.slog.Errorf("WebSocket.Call(): in read msg, err=\n\t%v\n", err)
+			sf.ws.Close()
+			sf.isConnect.Reset()
+			sf.connect()
+			continue
+		}
+		break
+	}
+	dictResp = make(map[string]string)
+	err = json.Unmarshal(binResp, &dictResp)
+	if err != nil {
+		return nil, fmt.Errorf("WebSocket.Call(): in unmarshal binResp, err=\n\t%w", err)
+	}
+	return dictResp, nil
+}
+
+// Потокобезопасное ожидание закрытия в отдельном потоке
+func (sf *WebSocket) close() {
+	<-sf.kern.Done()
+	sf.block.Lock()
+	defer sf.block.Unlock()
+	if !sf.isConnect.Get() {
+		return
+	}
+	sf.isConnect.Reset()
+	sf.ws.Close()
+}

+ 141 - 0
desktop/win_main/win_main.go

@@ -0,0 +1,141 @@
+// package win_main -- главное окно приложения
+package win_main
+
+import (
+	_ "embed"
+	"fmt"
+	"log"
+	"net/url"
+	"runtime"
+	"sync"
+	"time"
+
+	"github.com/zserge/lorca"
+
+	// "wartank/desktop/win_users"
+	"wartank/pkg/components/safebool"
+	"wartank/pkg/types"
+)
+
+const (
+	strWinMainName = "win_main" // Контрольная строка для сторожа потоков
+)
+
+//go:embed win_main.html
+var strWinHtml string
+
+// WinMain -- главное окно приложения
+type WinMain struct {
+	desktop types.IDesktop
+	store   types.IStore
+	win     lorca.UI
+	block   sync.Mutex
+	isWork  *safebool.SafeBool
+	ws      types.IWebSocket
+}
+
+// NewWinMain -- возвращает новое окно десктопа
+func NewWinMain(desktop types.IDesktop) (*WinMain, error) {
+	if desktop == nil {
+		return nil, fmt.Errorf("NewWinMain(): IDesktop == nil")
+	}
+	sf := &WinMain{
+		desktop: desktop,
+		store:   desktop.Store(),
+		isWork:  safebool.NewSafeBool(),
+		ws:      desktop.Ws(),
+	}
+	args := []string{}
+	if runtime.GOOS == "linux" {
+		args = append(args, "--class=Lorca")
+	}
+	var err error
+	sf.win, err = lorca.New("data:text/html,"+url.PathEscape(strWinHtml), "", 640, 480, args...)
+	if err != nil {
+		return nil, fmt.Errorf("NewWinMain(): in create win, err=\n\t%w", err)
+	}
+	go sf.close()
+	return sf, nil
+}
+
+// Работает в отдельном потоке, главный цикл окна
+func (sf *WinMain) Run() {
+	log.Println("WinMain.Run()")
+	sf.win.Bind("close_win", sf.onClose)
+	sf.win.Bind("bot_list", sf.onUsers)
+	go sf.timeServer()
+	<-sf.win.Done() // Ожидание закрытия окна
+	sf.desktop.CancelApp()
+}
+
+// Открывает окно с пользователями
+func (sf *WinMain) onUsers() {
+	log.Println("WinMain.onUsers()")
+	go sf.desktop.DictBot().Show()
+}
+
+// Проверяет наличие работающего сервера, работает в отдельном потоке
+func (sf *WinMain) timeServer() {
+	for {
+		time.Sleep(time.Second * 2)
+		timeBeg := time.Now().UTC().UnixMicro()
+		dictResp, err := sf.ws.Read("/server/time")
+		if err != nil {
+			log.Printf("WinMain.timeServer(): in send request to server, err=\n\t%v\n", err)
+			sf.desktop.CancelApp()
+			return
+		}
+		timeEnd := time.Now().UTC().UnixMicro() - timeBeg
+		strErr := dictResp["err"]
+		if strErr != "" {
+			js := fmt.Sprintf(`
+		function SetTimeServer(){
+			var _el = document.getById("/server/time");
+			_el.innerText=%q;
+		}
+		SetTimeServer()
+		`, err)
+			sf.win.Eval(js)
+			continue
+		}
+		strTime := dictResp["/server/time"]
+
+		js := fmt.Sprintf(`
+		function SetTimeServer(){
+			var _el = document.getElementById("/server/time");
+			_el.innerText=%q;
+		}
+		SetTimeServer()
+		`, strTime)
+		sf.win.Eval(js)
+
+		js = fmt.Sprintf(`
+		function SetPingServer(){
+			var _el = document.getElementById("/server/ping");
+			_el.innerText="%v мкСек";
+		}
+		SetPingServer()
+		`, timeEnd)
+		sf.win.Eval(js)
+	}
+}
+
+// Закрывает приложение
+func (sf *WinMain) onClose() {
+	log.Println("WinMain.onClose()")
+	sf.win.Close()
+}
+
+// close -- ожидает отмены глобального контекста
+func (sf *WinMain) close() {
+	<-sf.desktop.CtxApp().Done()
+	log.Println("WinMain.close()")
+	sf.block.Lock()
+	defer sf.block.Unlock()
+	if !sf.isWork.Get() {
+		return
+	}
+	sf.isWork.Reset()
+	sf.win.Close()
+	sf.desktop.Wg().Done(strWinMainName)
+}

+ 70 - 0
desktop/win_main/win_main.html

@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html lang="ru">
+
+	<head>
+		<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+		<title>WarTank bot</title>
+		<meta name="author" content="SVI">
+		<style>
+			html {
+				height: 100%;
+			}
+
+			body {
+				margin: 0;
+				color: #fff;
+				/* Растягиваем body по высоте html */
+				min-height: 100%;
+				display: grid;
+				grid-template-rows: auto 1fr auto;
+			}
+
+			header {
+				background: rgb(88, 88, 184);
+			}
+
+			main {
+				background: rgb(141, 112, 112);
+			}
+
+			footer {
+				background: black;
+			}
+
+			.my-label {
+				display: inline-block;
+				background: rgb(12, 54, 56);
+				font-family: 'Courier New', Courier, monospace;
+				margin-top: 0.5em;
+				margin-left: 0.5em;
+			}
+		</style>
+	</head>
+
+	<body>
+		<header role="banner">
+			<b>WarTank bot [FunnySoft 2022]</b>
+		</header>
+		<main role="main">
+			<div id="status"><b>Статус:</b></div>
+			<div class="my-label">
+				Время сервера:<div id="/server/time"></div>
+			</div>
+			<div class="my-label">
+				Пинг сервера:<div id="/server/ping"></div>
+			</div>
+			<div class="my-label">
+				Аптайм сервера:<div id="/serv/uptime"></div>
+			</div>
+			<hr width="95%" height="4px">
+			<div>
+				<button type="button" name="users" value="Полз" onclick="bot_list()">Список ботов</button>
+				<button style="background:red;" type="button" name="close" value="Выход" onclick="close_win()">Выход</button>
+			</div>
+		</main>
+		<footer role="contentinfo">
+			<div class="footer">Для правильной работы программы необходимо запустить <code>server</code>.</div>
+		</footer>
+	</body>
+
+</html>