Network base, new map, economy base
- Steam networking dependencies - Full-size regions map - Economy base structures and assets
This commit is contained in:
parent
4815fb4e1e
commit
1adddd3f1c
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,4 @@
|
|||||||
|
assets-src/map/ee.map.bmp
|
||||||
out/
|
out/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,10 @@ add_subdirectory(sdk)
|
|||||||
add_subdirectory(p7lib)
|
add_subdirectory(p7lib)
|
||||||
add_subdirectory(utils)
|
add_subdirectory(utils)
|
||||||
|
|
||||||
#add_subdirectory(server/lib)
|
add_subdirectory(economy)
|
||||||
|
|
||||||
|
add_subdirectory(server/lib)
|
||||||
|
add_subdirectory(server/dedicated)
|
||||||
add_subdirectory(engine-api)
|
add_subdirectory(engine-api)
|
||||||
|
|
||||||
# add_subdirectory(renderers/magnum-renderer)
|
# add_subdirectory(renderers/magnum-renderer)
|
||||||
|
|||||||
3
assets-src/economy/enterprises/base.dat.json
Normal file
3
assets-src/economy/enterprises/base.dat.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"content": []
|
||||||
|
}
|
||||||
12
assets-src/economy/resources/base.dat.json
Normal file
12
assets-src/economy/resources/base.dat.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"tag": "coal",
|
||||||
|
"baseCost": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "oil",
|
||||||
|
"baseCost": 50
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 33 MiB |
@ -1,11 +1,57 @@
|
|||||||
{
|
{
|
||||||
"pepene": "#AA00FF",
|
"dineria": "#FFF600",
|
||||||
"scarna": "#FF007F",
|
"minderia": "#F2EA00",
|
||||||
|
"ragland": "#D8D100",
|
||||||
"dineria": "#FFFF00",
|
"trinadia": "#E5DD00",
|
||||||
"perdania": "#7FFF00",
|
"kostyania": "#BFB800",
|
||||||
"arstinia": "#FF0000",
|
"nikland": "#CCC500",
|
||||||
"alende": "#0000FF",
|
"perdania": "#B2AC00",
|
||||||
"zerandia": "#FF5400",
|
"eddoran": "#A5A000",
|
||||||
"mgahvatia": "#5400FF"
|
"inderia": "#999300",
|
||||||
}
|
"arstinia": "#8C8700",
|
||||||
|
"zerandia": "#CCC50A",
|
||||||
|
"levea": "#8C8707",
|
||||||
|
"berland": "#7F7B06",
|
||||||
|
"kurilliland": "#999407",
|
||||||
|
"irtazlin": "#7F7B00",
|
||||||
|
"akvaria": "#D8D10A",
|
||||||
|
"retorria": "#A5A008",
|
||||||
|
"north_rachia": "#BFB909",
|
||||||
|
"south_rachia": "#E5DE0B",
|
||||||
|
"tirandia": "#B2AC08",
|
||||||
|
"sverlopa": "#FFF719",
|
||||||
|
"strogskia": "#F2EA0C",
|
||||||
|
"fillirand": "#FFF60C",
|
||||||
|
"mgahvatia": "#D8CE15",
|
||||||
|
"glednar": "#E5DB16",
|
||||||
|
"kima": "#F2EA18",
|
||||||
|
"alende": "#EF7DB4",
|
||||||
|
"pepene": "#EF30EA",
|
||||||
|
"langat": "#D2FBB5",
|
||||||
|
"kuilemberg": "#CD5FD7",
|
||||||
|
"gardol": "#EA30F2",
|
||||||
|
"diosis": "#9EF1A0",
|
||||||
|
"bartil": "#8EF9AA",
|
||||||
|
"scarna": "#76D3AF",
|
||||||
|
"dabreg": "#A6ED6F",
|
||||||
|
"scenra": "#A8FA7B",
|
||||||
|
"prakirol": "#7AB1D0",
|
||||||
|
"illeton": "#7CC1E8",
|
||||||
|
"barlakir": "#EF45CE",
|
||||||
|
"assental": "#FB6BEA",
|
||||||
|
"castle-viktor": "#FD8BE7",
|
||||||
|
"sector-kaya": "#BD89C8",
|
||||||
|
"castle-avens": "#DD89AD",
|
||||||
|
"nanec": "#69A4FC",
|
||||||
|
"paradizna": "#90B9F5",
|
||||||
|
"dalovburg": "#8A9AD8",
|
||||||
|
"koberec": "#59BBFA",
|
||||||
|
"kichka_menrosh": "#4EDFFE",
|
||||||
|
"chenec": "#44CAFC",
|
||||||
|
"ishkivat": "#58DDF3",
|
||||||
|
"samac": "#57B8F7",
|
||||||
|
"shkadozhiv": "#66D6DD",
|
||||||
|
"zaknishce": "#5ACBE5",
|
||||||
|
"saranchet": "#51DFF1",
|
||||||
|
"nizhnegorsk": "#2AEAFE"
|
||||||
|
}
|
||||||
|
|||||||
@ -2,23 +2,80 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"subjects": [
|
"subjects": [
|
||||||
{
|
{
|
||||||
"tag": "FSC",
|
"tag": "UCB",
|
||||||
"color": "#FF0000",
|
"color": "#F7EF00",
|
||||||
"regions": [
|
"regions": [
|
||||||
"pepene",
|
"dineria",
|
||||||
"scarna"
|
"minderia",
|
||||||
|
"ragland",
|
||||||
|
"trinadia",
|
||||||
|
"kostyania",
|
||||||
|
"nikland",
|
||||||
|
"perdania",
|
||||||
|
"eddoran",
|
||||||
|
"inderia",
|
||||||
|
"arstinia",
|
||||||
|
"zerandia",
|
||||||
|
"levea",
|
||||||
|
"berland",
|
||||||
|
"kurilliland",
|
||||||
|
"irtazlin",
|
||||||
|
"akvaria",
|
||||||
|
"retorria",
|
||||||
|
"north_rachia",
|
||||||
|
"south_rachia",
|
||||||
|
"tirandia",
|
||||||
|
"sverlopa",
|
||||||
|
"strogskia",
|
||||||
|
"fillirand",
|
||||||
|
"mgahvatia",
|
||||||
|
"glednar",
|
||||||
|
"kima"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tag": "UCB",
|
"tag": "FSC",
|
||||||
"color": "#0000FF",
|
"color": "#AB5177",
|
||||||
"regions": [
|
"regions": [
|
||||||
"dineria",
|
|
||||||
"perdania",
|
|
||||||
"arstinia",
|
|
||||||
"alende",
|
"alende",
|
||||||
"zerandia",
|
"pepene",
|
||||||
"mgahvatia"
|
"kuilemberg",
|
||||||
|
"gardol",
|
||||||
|
"diosis",
|
||||||
|
"bartil",
|
||||||
|
"scarna",
|
||||||
|
"dabreg",
|
||||||
|
"scenra",
|
||||||
|
"prakirol",
|
||||||
|
"illeton"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "TRP",
|
||||||
|
"color": "#F77D93",
|
||||||
|
"regions": [
|
||||||
|
"barlakir",
|
||||||
|
"assental",
|
||||||
|
"castle-viktor",
|
||||||
|
"sector-kaya",
|
||||||
|
"castle-avens"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "SFL",
|
||||||
|
"color": "#392986",
|
||||||
|
"regions": [
|
||||||
|
"nanec",
|
||||||
|
"paradizna",
|
||||||
|
"dalovburg",
|
||||||
|
"koberec",
|
||||||
|
"kichka_menrosh",
|
||||||
|
"chenec",
|
||||||
|
"ishkivat",
|
||||||
|
"samac",
|
||||||
|
"shkadozhiv",
|
||||||
|
"zaknishce",
|
||||||
|
"saranchet"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
1
assets/economy/enterprises/base.dat
Normal file
1
assets/economy/enterprises/base.dat
Normal file
@ -0,0 +1 @@
|
|||||||
|
@
|
||||||
2
assets/economy/resources/base.dat
Normal file
2
assets/economy/resources/base.dat
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
@sUBZ
|
||||||
|
Edミホ2
|
||||||
BIN
assets/map/ee.map
Normal file
BIN
assets/map/ee.map
Normal file
Binary file not shown.
@ -1 +1,3 @@
|
|||||||
@@'QO<><02>p<EFBFBD> HKԳ<4B>4U<34><55><EFBFBD>f<EFBFBD><66><EFBFBD><14><>VT<56><54>T
|
(@@GQ?HKÔł÷ďÔăČp«QwE\ľů!÷}“U,ł
ł×9)†Y\fö˙ęňfŃŘÝĺf¸żĹĚf¬˛ Ąf“™‡Św
|
||||||
|
Ĺ̇Św{”™v{
|
||||||
|
ŃŘw Ą ążwŢ嬲w÷˙ęňwö˙ÎŘwŰĺęňw´}ďę0ďw×_Íň0ęw ńžŞůŽwŻÓvoí¦w{ú¨Đ±zčÁ|wÎEďękűwç‹ýȉ˝‰Ýwü¤iőą<C591>wŘšŠú»YwţßNüĘDwóÝX÷¸WwÝÖfĺËZńßQ
|
||||||
9
economy/CMakeLists.txt
Normal file
9
economy/CMakeLists.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
include(${CMAKE_SOURCE_DIR}/cmake/library.cmake)
|
||||||
|
|
||||||
|
set(LIB economy)
|
||||||
|
|
||||||
|
# Find dependencies
|
||||||
|
|
||||||
|
# Link library
|
||||||
|
|
||||||
|
add_library_target(${LIB})
|
||||||
25
economy/modules/Corporation.cppm
Normal file
25
economy/modules/Corporation.cppm
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export module Economy:Corporation;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
export namespace ee_economy {
|
||||||
|
|
||||||
|
struct EnterpriseDefinition {
|
||||||
|
std::uint32_t productionRecipe;
|
||||||
|
std::uint32_t constructionRecipe;
|
||||||
|
std::uint8_t constructionSpeed;
|
||||||
|
};
|
||||||
|
using EnterpriseList = std::vector<EnterpriseDefinition>;
|
||||||
|
|
||||||
|
class Enterprise {
|
||||||
|
public:
|
||||||
|
EnterpriseDefinition definition;
|
||||||
|
std::uint32_t stateId;
|
||||||
|
std::uint8_t damage;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Corporation {
|
||||||
|
public:
|
||||||
|
std::vector<Enterprise> enterprises;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
3
economy/modules/Economy.cppm
Normal file
3
economy/modules/Economy.cppm
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export module Economy;
|
||||||
|
export import :Resources;
|
||||||
|
export import :Corporation;
|
||||||
24
economy/modules/Resources.cppm
Normal file
24
economy/modules/Resources.cppm
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export module Economy:Resources;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
export namespace ee_economy {
|
||||||
|
|
||||||
|
struct Resource {
|
||||||
|
std::uint32_t tag;
|
||||||
|
std::uint8_t baseCost;
|
||||||
|
};
|
||||||
|
using ResourceList = std::vector<Resource>;
|
||||||
|
|
||||||
|
struct ProductionRecipe {
|
||||||
|
std::uint32_t tag;
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
std::uint32_t tag;
|
||||||
|
std::uint8_t cost;
|
||||||
|
};
|
||||||
|
static constexpr std::size_t MAX_ENTRY_COUNT = 4;
|
||||||
|
Entry in[MAX_ENTRY_COUNT];
|
||||||
|
Entry out[MAX_ENTRY_COUNT];
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@ -11,7 +11,7 @@ find_package(glm CONFIG REQUIRED)
|
|||||||
|
|
||||||
add_library_target(${LIB})
|
add_library_target(${LIB})
|
||||||
|
|
||||||
target_link_libraries(${LIB} PRIVATE p7)
|
|
||||||
target_link_libraries(${LIB} PUBLIC utils)
|
target_link_libraries(${LIB} PUBLIC utils)
|
||||||
|
target_link_libraries(${LIB} PUBLIC economy)
|
||||||
target_link_libraries(${LIB} PRIVATE glfw)
|
target_link_libraries(${LIB} PRIVATE glfw)
|
||||||
target_link_libraries(${LIB} PRIVATE glm::glm)
|
target_link_libraries(${LIB} PRIVATE glm::glm)
|
||||||
@ -1,5 +1,6 @@
|
|||||||
export module AssetData;
|
export module AssetData;
|
||||||
export import GameData;
|
export import GameData;
|
||||||
|
export import Economy;
|
||||||
export import views;
|
export import views;
|
||||||
import std;
|
import std;
|
||||||
|
|
||||||
@ -13,6 +14,8 @@ export namespace ee_assets {
|
|||||||
|
|
||||||
MAP_MESH,
|
MAP_MESH,
|
||||||
SAVE,
|
SAVE,
|
||||||
|
RESOURCE_LIST,
|
||||||
|
ENTERPRISE_LIST,
|
||||||
|
|
||||||
SHADER,
|
SHADER,
|
||||||
MATERIAL,
|
MATERIAL,
|
||||||
@ -41,6 +44,18 @@ export namespace ee_assets {
|
|||||||
ee_data::GameState content;
|
ee_data::GameState content;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct AssetResourceList {
|
||||||
|
using asset_tag = AssetTag;
|
||||||
|
|
||||||
|
ee_economy::ResourceList content;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AssetEnterpriseList {
|
||||||
|
using asset_tag = AssetTag;
|
||||||
|
|
||||||
|
ee_economy::EnterpriseList content;
|
||||||
|
};
|
||||||
|
|
||||||
using AssetShader = AssetBinary;
|
using AssetShader = AssetBinary;
|
||||||
|
|
||||||
struct AssetMaterial {
|
struct AssetMaterial {
|
||||||
|
|||||||
@ -15,8 +15,9 @@ void Client::Init() const {
|
|||||||
|
|
||||||
// TEST
|
// TEST
|
||||||
ee_assets::AssetSave& asset = Engine::assetManager->Get<ee_assets::AssetSave>(save);
|
ee_assets::AssetSave& asset = Engine::assetManager->Get<ee_assets::AssetSave>(save);
|
||||||
ViewModelSubject(asset.content.subjects[0]);
|
for (const ee_data::Subject &subject : asset.content.subjects) {
|
||||||
ViewModelSubject(asset.content.subjects[1]);
|
ViewModelSubject(subject);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const IDataChannel* Client::GetConnection() const {
|
const IDataChannel* Client::GetConnection() const {
|
||||||
|
|||||||
@ -41,7 +41,7 @@ int main(int argc, char **argv) {
|
|||||||
Engine::assetManager->LoadDir(ee_assets::MATERIAL, "assets/materials");
|
Engine::assetManager->LoadDir(ee_assets::MATERIAL, "assets/materials");
|
||||||
|
|
||||||
// Load assets
|
// Load assets
|
||||||
Engine::assetManager->LoadAsync(ee_assets::MAP_MESH, "assets/map/regions.map");
|
Engine::assetManager->LoadAsync(ee_assets::MAP_MESH, "assets/map/ee.map");
|
||||||
Engine::assetManager->LoadAsync(ee_assets::SAVE, "assets/scenarios/empires.save");
|
Engine::assetManager->LoadAsync(ee_assets::SAVE, "assets/scenarios/empires.save");
|
||||||
|
|
||||||
// Setup input manager
|
// Setup input manager
|
||||||
@ -50,7 +50,7 @@ int main(int argc, char **argv) {
|
|||||||
// Setup renderer
|
// Setup renderer
|
||||||
Engine::renderer = reinterpret_cast<ee_engine::IRenderer*>(Allocate<ee_vulkan::VulkanRenderer>(window));
|
Engine::renderer = reinterpret_cast<ee_engine::IRenderer*>(Allocate<ee_vulkan::VulkanRenderer>(window));
|
||||||
|
|
||||||
ee_assets::AssetMapMesh& regions = Engine::assetManager->FindOnce<ee_assets::AssetMapMesh>("assets/map/regions.map");
|
ee_assets::AssetMapMesh& regions = Engine::assetManager->FindOnce<ee_assets::AssetMapMesh>("assets/map/ee.map");
|
||||||
Engine::renderer->RegisterMapMesh(®ions.content);
|
Engine::renderer->RegisterMapMesh(®ions.content);
|
||||||
|
|
||||||
//TODO: ShaderRefs builder from set of AssetMaterial
|
//TODO: ShaderRefs builder from set of AssetMaterial
|
||||||
|
|||||||
604
nudge.py
Normal file
604
nudge.py
Normal file
@ -0,0 +1,604 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Region Mapper — большая цветовая карта -> JSON {region: "#RRGGBB"}.
|
||||||
|
Зависимости: pip install PyQt5 Pillow
|
||||||
|
Запуск: python nudge.py map.bmp
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt, QPoint, QPointF, QRect, QRectF, QTimer, pyqtSignal
|
||||||
|
from PyQt5.QtGui import QImage, QPixmap, QPainter, QColor, QPen, QKeySequence
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QApplication, QMainWindow, QWidget, QHBoxLayout, QVBoxLayout,
|
||||||
|
QLabel, QLineEdit, QPushButton, QListWidget, QListWidgetItem,
|
||||||
|
QSplitter, QFileDialog, QMessageBox, QInputDialog, QShortcut,
|
||||||
|
)
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
Image.MAX_IMAGE_PIXELS = None
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Утилиты
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
def rgb_to_hex(rgb):
|
||||||
|
return "#{:02X}{:02X}{:02X}".format(*rgb)
|
||||||
|
|
||||||
|
|
||||||
|
def hex_to_rgb(s):
|
||||||
|
s = s.lstrip("#")
|
||||||
|
return int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16)
|
||||||
|
|
||||||
|
|
||||||
|
def load_map(path):
|
||||||
|
"""Возвращает (QImage, buffer). buffer нужно держать живым во втором случае."""
|
||||||
|
|
||||||
|
# Штатный загрузчик Qt: нет лимита Pillow, нет лишних копий памяти.
|
||||||
|
img = QImage(path)
|
||||||
|
if not img.isNull():
|
||||||
|
return img, None
|
||||||
|
|
||||||
|
# Запасной путь через Pillow.
|
||||||
|
with Image.open(path) as f:
|
||||||
|
pil = f.convert("RGB")
|
||||||
|
w, h = pil.size
|
||||||
|
raw = pil.tobytes()
|
||||||
|
del pil
|
||||||
|
|
||||||
|
# ВАЖНО: этот конструктор QImage НЕ копирует данные.
|
||||||
|
# Буфер raw обязан жить столько же, сколько живёт QImage.
|
||||||
|
img = QImage(raw, w, h, w * 3, QImage.Format_RGB888)
|
||||||
|
if img.isNull():
|
||||||
|
raise RuntimeError("Не удалось декодировать изображение")
|
||||||
|
return img, raw
|
||||||
|
|
||||||
|
|
||||||
|
def make_overview(img, max_side=4096):
|
||||||
|
"""Маленькая копия для режима «вся карта на экране»."""
|
||||||
|
w, h = img.width(), img.height()
|
||||||
|
k = min(1.0, max_side / float(max(w, h)))
|
||||||
|
if k >= 1.0:
|
||||||
|
small = img
|
||||||
|
else:
|
||||||
|
small = img.scaled(max(1, int(w * k)), max(1, int(h * k)),
|
||||||
|
Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
||||||
|
return QPixmap.fromImage(small)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Просмотр карты
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
class MapViewer(QWidget):
|
||||||
|
pixelPicked = pyqtSignal(int, int, tuple) # x, y, (r, g, b)
|
||||||
|
zoomChanged = pyqtSignal(float)
|
||||||
|
cursorMoved = pyqtSignal(int, int)
|
||||||
|
|
||||||
|
LOD_SWITCH = 0.25 # ниже этого масштаба рисуем уменьшенную копию
|
||||||
|
MAX_SCALE = 64.0
|
||||||
|
|
||||||
|
def __init__(self, image, raw_buffer, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.image = image
|
||||||
|
self._raw = raw_buffer # держим буфер Pillow живым
|
||||||
|
self.W = image.width()
|
||||||
|
self.H = image.height()
|
||||||
|
|
||||||
|
print("Размер карты: {} x {} пикселей".format(self.W, self.H))
|
||||||
|
print("Создаю уменьшенную копию...")
|
||||||
|
self.overview = make_overview(image)
|
||||||
|
|
||||||
|
self.scale = 1.0 # экранных пикселей на пиксель карты
|
||||||
|
self.offset = QPointF(0, 0) # куда смещён левый верхний угол карты
|
||||||
|
self._fit_scale = 1.0
|
||||||
|
self._fitted = False
|
||||||
|
|
||||||
|
self._panning = False
|
||||||
|
self._pan_last = QPoint()
|
||||||
|
self._press_pos = None
|
||||||
|
self._press_offset = QPointF()
|
||||||
|
self._dragging = False
|
||||||
|
self.marker = None
|
||||||
|
|
||||||
|
self.setMouseTracking(True)
|
||||||
|
self.setFocusPolicy(Qt.StrongFocus)
|
||||||
|
self.setMinimumSize(200, 120)
|
||||||
|
|
||||||
|
# ---------- координаты ----------
|
||||||
|
|
||||||
|
def _to_image(self, pos):
|
||||||
|
x = math.floor((pos.x() - self.offset.x()) / self.scale)
|
||||||
|
y = math.floor((pos.y() - self.offset.y()) / self.scale)
|
||||||
|
return x, y
|
||||||
|
|
||||||
|
# ---------- масштаб ----------
|
||||||
|
|
||||||
|
def fitToWindow(self):
|
||||||
|
vw, vh = self.width(), self.height()
|
||||||
|
if vw < 2 or vh < 2:
|
||||||
|
return
|
||||||
|
s = min(vw / self.W, vh / self.H)
|
||||||
|
self._fit_scale = s
|
||||||
|
self.scale = s
|
||||||
|
self.offset = QPointF((vw - self.W * s) / 2.0,
|
||||||
|
(vh - self.H * s) / 2.0)
|
||||||
|
self._fitted = True
|
||||||
|
self.update()
|
||||||
|
self.zoomChanged.emit(s)
|
||||||
|
|
||||||
|
def actualSize(self):
|
||||||
|
self._zoom_at(QPointF(self.width() / 2, self.height() / 2), 1.0)
|
||||||
|
|
||||||
|
def zoomIn(self):
|
||||||
|
self._zoom_at(QPointF(self.width() / 2, self.height() / 2), self.scale * 1.5)
|
||||||
|
|
||||||
|
def zoomOut(self):
|
||||||
|
self._zoom_at(QPointF(self.width() / 2, self.height() / 2), self.scale / 1.5)
|
||||||
|
|
||||||
|
def _zoom_at(self, point, new_scale):
|
||||||
|
new_scale = max(self._fit_scale * 0.25, min(self.MAX_SCALE, new_scale))
|
||||||
|
# Точка карты под курсором должна остаться под курсором.
|
||||||
|
ix = (point.x() - self.offset.x()) / self.scale
|
||||||
|
iy = (point.y() - self.offset.y()) / self.scale
|
||||||
|
self.scale = new_scale
|
||||||
|
self.offset = QPointF(point.x() - ix * new_scale,
|
||||||
|
point.y() - iy * new_scale)
|
||||||
|
self.update()
|
||||||
|
self.zoomChanged.emit(new_scale)
|
||||||
|
|
||||||
|
def resizeEvent(self, event):
|
||||||
|
super().resizeEvent(event)
|
||||||
|
if not self._fitted:
|
||||||
|
self.fitToWindow()
|
||||||
|
|
||||||
|
# ---------- отрисовка ----------
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
p = QPainter(self)
|
||||||
|
p.fillRect(self.rect(), QColor(45, 45, 48))
|
||||||
|
p.setRenderHint(QPainter.SmoothPixmapTransform, self.scale < 1.0)
|
||||||
|
|
||||||
|
s = self.scale
|
||||||
|
ox, oy = self.offset.x(), self.offset.y()
|
||||||
|
|
||||||
|
if s < self.LOD_SWITCH:
|
||||||
|
# Далеко: рисуем маленькую копию, Qt сам обрежет по виджету.
|
||||||
|
target = QRectF(ox, oy, self.W * s, self.H * s)
|
||||||
|
p.drawPixmap(target, self.overview,
|
||||||
|
QRectF(0, 0, self.overview.width(), self.overview.height()))
|
||||||
|
else:
|
||||||
|
# Близко: берём из полного разрешения ТОЛЬКО видимый прямоугольник.
|
||||||
|
x0 = max(0, int(math.floor(-ox / s)))
|
||||||
|
y0 = max(0, int(math.floor(-oy / s)))
|
||||||
|
x1 = min(self.W, int(math.ceil((self.width() - ox) / s)))
|
||||||
|
y1 = min(self.H, int(math.ceil((self.height() - oy) / s)))
|
||||||
|
|
||||||
|
if x1 > x0 and y1 > y0:
|
||||||
|
src = QRect(x0, y0, x1 - x0, y1 - y0)
|
||||||
|
target = QRectF(ox + x0 * s, oy + y0 * s,
|
||||||
|
(x1 - x0) * s, (y1 - y0) * s)
|
||||||
|
p.drawImage(target, self.image, QRectF(src))
|
||||||
|
|
||||||
|
# Рамка карты
|
||||||
|
p.setPen(QPen(QColor(95, 95, 95), 1))
|
||||||
|
p.drawRect(QRectF(ox, oy, self.W * s, self.H * s))
|
||||||
|
|
||||||
|
# Метка последнего выбранного пикселя
|
||||||
|
if self.marker is not None and s >= 4:
|
||||||
|
mx, my = self.marker
|
||||||
|
p.setPen(QPen(QColor(255, 255, 255), 1))
|
||||||
|
p.drawRect(QRectF(ox + mx * s, oy + my * s, s, s))
|
||||||
|
p.end()
|
||||||
|
|
||||||
|
# ---------- мышь ----------
|
||||||
|
|
||||||
|
def wheelEvent(self, event):
|
||||||
|
d = event.angleDelta().y()
|
||||||
|
if d:
|
||||||
|
factor = 1.25 if d > 0 else 1.0 / 1.25
|
||||||
|
self._zoom_at(QPointF(event.pos()), self.scale * factor)
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
if event.button() in (Qt.RightButton, Qt.MiddleButton):
|
||||||
|
self._panning = True
|
||||||
|
self._pan_last = event.pos()
|
||||||
|
self.setCursor(Qt.ClosedHandCursor)
|
||||||
|
elif event.button() == Qt.LeftButton:
|
||||||
|
self._press_pos = event.pos()
|
||||||
|
self._press_offset = QPointF(self.offset)
|
||||||
|
self._dragging = False
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event):
|
||||||
|
if self._panning:
|
||||||
|
d = event.pos() - self._pan_last
|
||||||
|
self._pan_last = event.pos()
|
||||||
|
self.offset = QPointF(self.offset.x() + d.x(),
|
||||||
|
self.offset.y() + d.y())
|
||||||
|
self.update()
|
||||||
|
elif self._press_pos is not None:
|
||||||
|
d = event.pos() - self._press_pos
|
||||||
|
if d.manhattanLength() > 4:
|
||||||
|
self._dragging = True
|
||||||
|
if self._dragging:
|
||||||
|
self.offset = QPointF(self._press_offset.x() + d.x(),
|
||||||
|
self._press_offset.y() + d.y())
|
||||||
|
self.update()
|
||||||
|
else:
|
||||||
|
x, y = self._to_image(event.pos())
|
||||||
|
if 0 <= x < self.W and 0 <= y < self.H:
|
||||||
|
self.cursorMoved.emit(x, y)
|
||||||
|
else:
|
||||||
|
self.cursorMoved.emit(-1, -1)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event):
|
||||||
|
if event.button() in (Qt.RightButton, Qt.MiddleButton):
|
||||||
|
self._panning = False
|
||||||
|
self.setCursor(Qt.ArrowCursor)
|
||||||
|
elif event.button() == Qt.LeftButton and self._press_pos is not None:
|
||||||
|
if not self._dragging:
|
||||||
|
self._pick(event.pos())
|
||||||
|
self._press_pos = None
|
||||||
|
self._dragging = False
|
||||||
|
|
||||||
|
def _pick(self, pos):
|
||||||
|
x, y = self._to_image(pos)
|
||||||
|
if 0 <= x < self.W and 0 <= y < self.H:
|
||||||
|
c = self.image.pixelColor(x, y)
|
||||||
|
self.marker = (x, y)
|
||||||
|
self.update()
|
||||||
|
self.pixelPicked.emit(x, y, (c.red(), c.green(), c.blue()))
|
||||||
|
|
||||||
|
def keyPressEvent(self, event):
|
||||||
|
if event.key() in (Qt.Key_0, Qt.Key_F):
|
||||||
|
self.fitToWindow()
|
||||||
|
elif event.key() == Qt.Key_1:
|
||||||
|
self.actualSize()
|
||||||
|
elif event.key() in (Qt.Key_Plus, Qt.Key_Equal):
|
||||||
|
self.zoomIn()
|
||||||
|
elif event.key() == Qt.Key_Minus:
|
||||||
|
self.zoomOut()
|
||||||
|
else:
|
||||||
|
super().keyPressEvent(event)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Окно
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
def __init__(self, path):
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("Region Mapper — " + os.path.basename(path))
|
||||||
|
self.resize(1400, 850)
|
||||||
|
|
||||||
|
# Обычный dict: в Python 3.7+ сохраняет порядок вставки.
|
||||||
|
self.mapping = {} # name -> "#RRGGBB"
|
||||||
|
self.by_color = {} # (r, g, b) -> name
|
||||||
|
self.current_rgb = None
|
||||||
|
self._shown_once = False
|
||||||
|
|
||||||
|
image, raw = load_map(path)
|
||||||
|
self.viewer = MapViewer(image, raw)
|
||||||
|
self.viewer.pixelPicked.connect(self.on_pixel)
|
||||||
|
self.viewer.zoomChanged.connect(
|
||||||
|
lambda s: self.zoom_label.setText("Масштаб: {:.4f}×".format(s)))
|
||||||
|
self.viewer.cursorMoved.connect(
|
||||||
|
lambda x, y: self.coord_label.setText("X: {} Y: {}".format(x, y)))
|
||||||
|
|
||||||
|
self._build_ui()
|
||||||
|
|
||||||
|
def showEvent(self, event):
|
||||||
|
super().showEvent(event)
|
||||||
|
if not self._shown_once:
|
||||||
|
self._shown_once = True
|
||||||
|
# Финальный фит уже после того, как layout раздал размеры.
|
||||||
|
QTimer.singleShot(0, self.viewer.fitToWindow)
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
central = QWidget()
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
root = QHBoxLayout(central)
|
||||||
|
root.setContentsMargins(6, 6, 6, 6)
|
||||||
|
splitter = QSplitter(Qt.Horizontal)
|
||||||
|
|
||||||
|
# --- слева: тулбар + карта ---
|
||||||
|
left = QWidget()
|
||||||
|
lv = QVBoxLayout(left)
|
||||||
|
lv.setContentsMargins(0, 0, 0, 0)
|
||||||
|
tb = QHBoxLayout()
|
||||||
|
for text, slot in (("Вписать (0)", self.viewer.fitToWindow),
|
||||||
|
("1:1", self.viewer.actualSize),
|
||||||
|
("+", self.viewer.zoomIn),
|
||||||
|
("−", self.viewer.zoomOut)):
|
||||||
|
b = QPushButton(text)
|
||||||
|
b.clicked.connect(slot)
|
||||||
|
tb.addWidget(b)
|
||||||
|
self.zoom_label = QLabel("Масштаб: —")
|
||||||
|
tb.addWidget(self.zoom_label)
|
||||||
|
tb.addStretch()
|
||||||
|
lv.addLayout(tb)
|
||||||
|
lv.addWidget(self.viewer, 1)
|
||||||
|
splitter.addWidget(left)
|
||||||
|
|
||||||
|
# --- справа: панель разметки ---
|
||||||
|
panel = QWidget()
|
||||||
|
pv = QVBoxLayout(panel)
|
||||||
|
pv.setContentsMargins(8, 8, 8, 8)
|
||||||
|
pv.setSpacing(6)
|
||||||
|
|
||||||
|
pv.addWidget(QLabel("Текущий пиксель:"))
|
||||||
|
row = QHBoxLayout()
|
||||||
|
self.swatch = QLabel()
|
||||||
|
self.swatch.setFixedSize(64, 44)
|
||||||
|
self.swatch.setStyleSheet("background:#808080; border:1px solid #555;")
|
||||||
|
row.addWidget(self.swatch)
|
||||||
|
self.color_info = QLabel("Кликните ЛКМ по карте")
|
||||||
|
self.color_info.setWordWrap(True)
|
||||||
|
row.addWidget(self.color_info, 1)
|
||||||
|
pv.addLayout(row)
|
||||||
|
|
||||||
|
pv.addSpacing(10)
|
||||||
|
pv.addWidget(QLabel("Название региона:"))
|
||||||
|
self.name_input = QLineEdit()
|
||||||
|
self.name_input.setPlaceholderText("Например: pepene")
|
||||||
|
self.name_input.returnPressed.connect(self.commit)
|
||||||
|
pv.addWidget(self.name_input)
|
||||||
|
|
||||||
|
btn = QPushButton("Назначить / обновить цвет [Enter]")
|
||||||
|
btn.clicked.connect(self.commit)
|
||||||
|
pv.addWidget(btn)
|
||||||
|
|
||||||
|
pv.addSpacing(12)
|
||||||
|
pv.addWidget(QLabel("Регионы (порядок = порядок в JSON):"))
|
||||||
|
self.list_widget = QListWidget()
|
||||||
|
self.list_widget.itemDoubleClicked.connect(
|
||||||
|
lambda it: self.rename_region(it.data(Qt.UserRole)))
|
||||||
|
pv.addWidget(self.list_widget, 1)
|
||||||
|
|
||||||
|
row2 = QHBoxLayout()
|
||||||
|
b1 = QPushButton("Переименовать")
|
||||||
|
b1.clicked.connect(lambda: self.rename_region())
|
||||||
|
b2 = QPushButton("Удалить")
|
||||||
|
b2.clicked.connect(self.delete_region)
|
||||||
|
row2.addWidget(b1)
|
||||||
|
row2.addWidget(b2)
|
||||||
|
pv.addLayout(row2)
|
||||||
|
|
||||||
|
pv.addSpacing(12)
|
||||||
|
b_open = QPushButton("Открыть существующий JSON")
|
||||||
|
b_open.clicked.connect(self.load_json)
|
||||||
|
pv.addWidget(b_open)
|
||||||
|
|
||||||
|
b_save = QPushButton("Сохранить JSON [Ctrl+S]")
|
||||||
|
b_save.setStyleSheet("background:#2e7d32; color:white; "
|
||||||
|
"font-weight:bold; padding:6px;")
|
||||||
|
b_save.clicked.connect(self.save_json)
|
||||||
|
pv.addWidget(b_save)
|
||||||
|
|
||||||
|
splitter.addWidget(panel)
|
||||||
|
splitter.setStretchFactor(0, 1)
|
||||||
|
splitter.setStretchFactor(1, 0)
|
||||||
|
splitter.setSizes([1000, 330])
|
||||||
|
root.addWidget(splitter)
|
||||||
|
|
||||||
|
self.coord_label = QLabel("X: — Y: —")
|
||||||
|
self.statusBar().addPermanentWidget(self.coord_label)
|
||||||
|
self.statusBar().showMessage(
|
||||||
|
"ЛКМ — выбрать пиксель; ЛКМ-драг / ПКМ — панорама; "
|
||||||
|
"колесо — зум; 0 — вписать; 1 — 1:1")
|
||||||
|
|
||||||
|
QShortcut(QKeySequence("Ctrl+S"), self, self.save_json)
|
||||||
|
|
||||||
|
# ---------- выбор пикселя ----------
|
||||||
|
|
||||||
|
def on_pixel(self, x, y, rgb):
|
||||||
|
self.current_rgb = rgb
|
||||||
|
hexv = rgb_to_hex(rgb)
|
||||||
|
self.swatch.setStyleSheet("background:{}; border:1px solid #555;".format(hexv))
|
||||||
|
|
||||||
|
text = "{}\nRGB ({}, {}, {})\nПиксель ({}, {})".format(
|
||||||
|
hexv, rgb[0], rgb[1], rgb[2], x, y)
|
||||||
|
|
||||||
|
owner = self.by_color.get(rgb)
|
||||||
|
if owner:
|
||||||
|
text += "\nУже назначен: " + owner
|
||||||
|
self.name_input.setText(owner) # сразу готово к правке
|
||||||
|
self._select_in_list(owner)
|
||||||
|
else:
|
||||||
|
self.name_input.clear()
|
||||||
|
|
||||||
|
self.color_info.setText(text)
|
||||||
|
self.name_input.setFocus()
|
||||||
|
self.name_input.selectAll()
|
||||||
|
|
||||||
|
# ---------- запись / правка маппинга ----------
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
name = self.name_input.text().strip()
|
||||||
|
if not name:
|
||||||
|
QMessageBox.warning(self, "Название", "Введите название региона.")
|
||||||
|
return
|
||||||
|
if self.current_rgb is None:
|
||||||
|
QMessageBox.warning(self, "Цвет", "Сначала кликните по пикселю на карте.")
|
||||||
|
return
|
||||||
|
|
||||||
|
rgb = self.current_rgb
|
||||||
|
hexv = rgb_to_hex(rgb)
|
||||||
|
|
||||||
|
# Цвет уже занят другим регионом?
|
||||||
|
owner = self.by_color.get(rgb)
|
||||||
|
if owner is not None and owner != name:
|
||||||
|
ret = QMessageBox.question(
|
||||||
|
self, "Цвет занят",
|
||||||
|
"Цвет {} уже назначен региону «{}».\n"
|
||||||
|
"Переназначить его региону «{}»?".format(hexv, owner, name),
|
||||||
|
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
|
||||||
|
if ret != QMessageBox.Yes:
|
||||||
|
return
|
||||||
|
self.mapping.pop(owner, None)
|
||||||
|
self.by_color.pop(rgb, None)
|
||||||
|
|
||||||
|
# Освобождаем старый цвет, если регион редактируется.
|
||||||
|
if name in self.mapping:
|
||||||
|
old_rgb = hex_to_rgb(self.mapping[name])
|
||||||
|
if self.by_color.get(old_rgb) == name:
|
||||||
|
self.by_color.pop(old_rgb, None)
|
||||||
|
|
||||||
|
# Присваивание СУЩЕСТВУЮЩЕМУ ключу не меняет его позицию в dict.
|
||||||
|
self.mapping[name] = hexv
|
||||||
|
self.by_color[rgb] = name
|
||||||
|
|
||||||
|
self._rebuild_list(select=name)
|
||||||
|
self.name_input.clear()
|
||||||
|
self.name_input.setFocus()
|
||||||
|
|
||||||
|
def _rebuild_list(self, select=None):
|
||||||
|
self.list_widget.clear()
|
||||||
|
for i, (name, hexv) in enumerate(self.mapping.items(), 1):
|
||||||
|
it = QListWidgetItem("{:>3}. {} {}".format(i, name, hexv))
|
||||||
|
it.setData(Qt.UserRole, name)
|
||||||
|
it.setBackground(QColor(hexv))
|
||||||
|
c = QColor(hexv)
|
||||||
|
bright = 0.299 * c.red() + 0.587 * c.green() + 0.114 * c.blue()
|
||||||
|
it.setForeground(QColor(20, 20, 20) if bright > 140
|
||||||
|
else QColor(245, 245, 245))
|
||||||
|
self.list_widget.addItem(it)
|
||||||
|
if select is not None:
|
||||||
|
self._select_in_list(select)
|
||||||
|
|
||||||
|
def _select_in_list(self, name):
|
||||||
|
for i in range(self.list_widget.count()):
|
||||||
|
if self.list_widget.item(i).data(Qt.UserRole) == name:
|
||||||
|
self.list_widget.setCurrentRow(i)
|
||||||
|
return
|
||||||
|
|
||||||
|
def rename_region(self, name=None):
|
||||||
|
if name is None:
|
||||||
|
it = self.list_widget.currentItem()
|
||||||
|
if it is None:
|
||||||
|
return
|
||||||
|
name = it.data(Qt.UserRole)
|
||||||
|
|
||||||
|
new, ok = QInputDialog.getText(self, "Переименовать регион",
|
||||||
|
"Новое название:", QLineEdit.Normal, name)
|
||||||
|
if not ok:
|
||||||
|
return
|
||||||
|
new = new.strip()
|
||||||
|
if not new or new == name:
|
||||||
|
return
|
||||||
|
if new in self.mapping:
|
||||||
|
QMessageBox.warning(self, "Дубликат", "Регион с таким названием уже есть.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Пересобираем dict, сохраняя позицию переименованного элемента.
|
||||||
|
reordered = {}
|
||||||
|
for k, v in self.mapping.items():
|
||||||
|
reordered[new if k == name else k] = v
|
||||||
|
self.mapping = reordered
|
||||||
|
|
||||||
|
rgb = hex_to_rgb(self.mapping[new])
|
||||||
|
self.by_color.pop(rgb, None)
|
||||||
|
self.by_color[rgb] = new
|
||||||
|
self._rebuild_list(select=new)
|
||||||
|
|
||||||
|
def delete_region(self):
|
||||||
|
it = self.list_widget.currentItem()
|
||||||
|
if it is None:
|
||||||
|
return
|
||||||
|
name = it.data(Qt.UserRole)
|
||||||
|
hexv = self.mapping.pop(name, None)
|
||||||
|
if hexv is not None:
|
||||||
|
rgb = hex_to_rgb(hexv)
|
||||||
|
if self.by_color.get(rgb) == name:
|
||||||
|
self.by_color.pop(rgb, None)
|
||||||
|
self._rebuild_list()
|
||||||
|
|
||||||
|
# ---------- JSON ----------
|
||||||
|
|
||||||
|
def save_json(self):
|
||||||
|
if not self.mapping:
|
||||||
|
QMessageBox.information(self, "Пусто", "Сначала добавьте регион.")
|
||||||
|
return
|
||||||
|
path, _ = QFileDialog.getSaveFileName(self, "Сохранить маппинг",
|
||||||
|
"regions.json", "JSON (*.json)")
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(self.mapping, f, ensure_ascii=False, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
self.statusBar().showMessage(
|
||||||
|
"Сохранено регионов: {} → {}".format(len(self.mapping), path), 8000)
|
||||||
|
|
||||||
|
def load_json(self):
|
||||||
|
path, _ = QFileDialog.getOpenFileName(self, "Открыть маппинг",
|
||||||
|
"", "JSON (*.json)")
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError("Ожидался JSON-объект")
|
||||||
|
|
||||||
|
mapping, by_color = {}, {}
|
||||||
|
for name, value in data.items():
|
||||||
|
if not isinstance(value, str):
|
||||||
|
continue
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) != 7 or not v.startswith("#"):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
rgb = hex_to_rgb(v)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
name = str(name)
|
||||||
|
if rgb in by_color:
|
||||||
|
continue
|
||||||
|
mapping[name] = rgb_to_hex(rgb)
|
||||||
|
by_color[rgb] = name
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Ошибка чтения", str(e))
|
||||||
|
return
|
||||||
|
|
||||||
|
self.mapping = mapping
|
||||||
|
self.by_color = by_color
|
||||||
|
self._rebuild_list()
|
||||||
|
self.statusBar().showMessage("Загружено регионов: {}".format(len(mapping)), 5000)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
app.setApplicationName("Region Mapper")
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
path = sys.argv[1]
|
||||||
|
else:
|
||||||
|
path, _ = QFileDialog.getOpenFileName(
|
||||||
|
None, "Открыть карту", "",
|
||||||
|
"Изображения (*.bmp *.png *.jpg *.jpeg *.tif *.tiff);;Все файлы (*)")
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
QMessageBox.critical(None, "Файл не найден", path)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
win = MainWindow(path)
|
||||||
|
except Exception as exc:
|
||||||
|
QMessageBox.critical(None, "Не удалось открыть карту", repr(exc))
|
||||||
|
raise
|
||||||
|
|
||||||
|
win.show()
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -66,7 +66,8 @@ export namespace ee_sdk {
|
|||||||
return
|
return
|
||||||
key == "tag" ||
|
key == "tag" ||
|
||||||
key.ends_with("AssetID") ||
|
key.ends_with("AssetID") ||
|
||||||
key.ends_with("Type");
|
key.ends_with("Type") ||
|
||||||
|
key.ends_with("Recipe");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void CompileHashes(const std::basic_string<char> &key, json &value) {
|
static void CompileHashes(const std::basic_string<char> &key, json &value) {
|
||||||
@ -112,7 +113,7 @@ export namespace ee_sdk {
|
|||||||
std::from_chars(colorStrPointer, colorStrPointer + 2, g, 16); colorStrPointer += 2;
|
std::from_chars(colorStrPointer, colorStrPointer + 2, g, 16); colorStrPointer += 2;
|
||||||
std::from_chars(colorStrPointer, colorStrPointer + 2, b, 16); colorStrPointer += 2;
|
std::from_chars(colorStrPointer, colorStrPointer + 2, b, 16); colorStrPointer += 2;
|
||||||
|
|
||||||
hex = colorToHex(r, g, b);
|
hex = colorToHex(r, g, b, true);
|
||||||
|
|
||||||
LOG(INFO, "COMPILED COLOR: Replace string [%hs] with 0x[%X] (RGB %d, %d, %d) color", colorStr, hex, r, g, b);
|
LOG(INFO, "COMPILED COLOR: Replace string [%hs] with 0x[%X] (RGB %d, %d, %d) color", colorStr, hex, r, g, b);
|
||||||
value = hex;
|
value = hex;
|
||||||
|
|||||||
@ -28,6 +28,21 @@ export namespace ee_data
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export namespace ee_economy {
|
||||||
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(
|
||||||
|
Resource,
|
||||||
|
tag,
|
||||||
|
baseCost
|
||||||
|
)
|
||||||
|
|
||||||
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(
|
||||||
|
EnterpriseDefinition,
|
||||||
|
productionRecipe,
|
||||||
|
constructionRecipe,
|
||||||
|
constructionSpeed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export namespace ee_assets
|
export namespace ee_assets
|
||||||
{
|
{
|
||||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(
|
||||||
@ -40,6 +55,16 @@ export namespace ee_assets
|
|||||||
vertexShaderAssetID,
|
vertexShaderAssetID,
|
||||||
fragmentShaderAssetID
|
fragmentShaderAssetID
|
||||||
)
|
)
|
||||||
|
|
||||||
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(
|
||||||
|
AssetResourceList,
|
||||||
|
content
|
||||||
|
)
|
||||||
|
|
||||||
|
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(
|
||||||
|
AssetEnterpriseList,
|
||||||
|
content
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace ee_sdk {
|
export namespace ee_sdk {
|
||||||
|
|||||||
@ -117,6 +117,10 @@ int main(const int argc, char **argv) {
|
|||||||
IfNotExluded(excludedTypes, "scenarios", [jsonCompiler, src, out] {
|
IfNotExluded(excludedTypes, "scenarios", [jsonCompiler, src, out] {
|
||||||
CompileJson<ee_assets::AssetSave>(jsonCompiler, "scenarios", src, out);
|
CompileJson<ee_assets::AssetSave>(jsonCompiler, "scenarios", src, out);
|
||||||
});
|
});
|
||||||
|
IfNotExluded(excludedTypes, "economy", [jsonCompiler, src, out] {
|
||||||
|
CompileJson<ee_assets::AssetResourceList>(jsonCompiler, "economy/resources", src, out);
|
||||||
|
CompileJson<ee_assets::AssetEnterpriseList>(jsonCompiler, "economy/enterprises", src, out);
|
||||||
|
});
|
||||||
|
|
||||||
constexpr ee_sdk::MapMeshCompiler mapMeshCompiler;
|
constexpr ee_sdk::MapMeshCompiler mapMeshCompiler;
|
||||||
IfNotExluded(excludedTypes, "map", [mapMeshCompiler, src, out] {
|
IfNotExluded(excludedTypes, "map", [mapMeshCompiler, src, out] {
|
||||||
|
|||||||
12
server/dedicated/CMakeLists.txt
Normal file
12
server/dedicated/CMakeLists.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
include(${CMAKE_SOURCE_DIR}/cmake/executable.cmake)
|
||||||
|
|
||||||
|
set(EXE server_dedicated)
|
||||||
|
|
||||||
|
# Find dependencies
|
||||||
|
|
||||||
|
# Link library
|
||||||
|
|
||||||
|
add_executable_target(${EXE})
|
||||||
|
|
||||||
|
target_link_libraries(${EXE} PRIVATE utils)
|
||||||
|
target_link_libraries(${EXE} PRIVATE server)
|
||||||
17
server/dedicated/src/main.cpp
Normal file
17
server/dedicated/src/main.cpp
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import Network;
|
||||||
|
import utils;
|
||||||
|
|
||||||
|
int main(const int argc, char **argv) {
|
||||||
|
Logger::Init(L"/P7.Sink=Console");
|
||||||
|
Logger::RegisterThread(L"Main Server");
|
||||||
|
|
||||||
|
ee_network::Lobby lobby;
|
||||||
|
|
||||||
|
lobby.Create(7777);
|
||||||
|
|
||||||
|
|
||||||
|
Logger::UnregisterThread();
|
||||||
|
Logger::Release();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@ -2,4 +2,13 @@ include(${CMAKE_SOURCE_DIR}/cmake/library.cmake)
|
|||||||
|
|
||||||
set(LIB server)
|
set(LIB server)
|
||||||
|
|
||||||
add_library_target(${LIB})
|
# Find dependencies
|
||||||
|
|
||||||
|
find_package(GameNetworkingSockets CONFIG REQUIRED)
|
||||||
|
|
||||||
|
# Link library
|
||||||
|
|
||||||
|
add_library_target(${LIB})
|
||||||
|
|
||||||
|
target_link_libraries(${LIB} PRIVATE utils)
|
||||||
|
target_link_libraries(${LIB} PRIVATE GameNetworkingSockets::shared)
|
||||||
36
server/lib/modules/Network.cppm
Normal file
36
server/lib/modules/Network.cppm
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
module;
|
||||||
|
|
||||||
|
#include <steam/isteamnetworkingsockets.h>
|
||||||
|
|
||||||
|
export module Network;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
export namespace ee_network {
|
||||||
|
|
||||||
|
enum DisconnectionStatus {
|
||||||
|
DISCONNECTION_STATUS_NETWORKING_DESTROY = k_ESteamNetConnectionEnd_AppException_Min,
|
||||||
|
|
||||||
|
DISCONNECTION_STATUS_NONE = k_ESteamNetConnectionEnd_AppException_Max
|
||||||
|
};
|
||||||
|
|
||||||
|
class Lobby {
|
||||||
|
public:
|
||||||
|
explicit Lobby();
|
||||||
|
~Lobby();
|
||||||
|
|
||||||
|
void Create(std::uint16_t port);
|
||||||
|
void Connect(const char *addressStr);
|
||||||
|
void Disconnect();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool isConnected = false;
|
||||||
|
bool isHost = false;
|
||||||
|
|
||||||
|
ISteamNetworkingSockets *steam;
|
||||||
|
HSteamListenSocket server = 0;
|
||||||
|
HSteamNetConnection connection = 0;
|
||||||
|
|
||||||
|
void Init();
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
49
server/lib/src/Network.cpp
Normal file
49
server/lib/src/Network.cpp
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
module;
|
||||||
|
|
||||||
|
#include <steam/steamnetworkingsockets.h>
|
||||||
|
#include <steam/isteamnetworkingutils.h>
|
||||||
|
|
||||||
|
module Network;
|
||||||
|
import utils;
|
||||||
|
|
||||||
|
ee_network::Lobby::Lobby() {
|
||||||
|
if (SteamDatagramErrMsg error; !GameNetworkingSockets_Init(nullptr, error)) {
|
||||||
|
LOG(CRITICAL, "Failed to initialize GameNetworkingSockets. %s", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
steam = SteamNetworkingSockets();
|
||||||
|
}
|
||||||
|
|
||||||
|
ee_network::Lobby::~Lobby() {
|
||||||
|
if (isConnected) {
|
||||||
|
if (isHost)
|
||||||
|
steam->CloseListenSocket(server);
|
||||||
|
else steam->CloseConnection(
|
||||||
|
connection,
|
||||||
|
DISCONNECTION_STATUS_NETWORKING_DESTROY,
|
||||||
|
"Disconnected due app networking close",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GameNetworkingSockets_Kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ee_network::Lobby::Create(const std::uint16_t port) {
|
||||||
|
SteamNetworkingIPAddr address; address.Clear();
|
||||||
|
address.m_port = port;
|
||||||
|
|
||||||
|
SteamNetworkingConfigValue_t config;
|
||||||
|
|
||||||
|
server = steam->CreateListenSocketIP(address, 0, nullptr);
|
||||||
|
|
||||||
|
char addressStr[SteamNetworkingIPAddr::k_cchMaxString];
|
||||||
|
address.ToString(addressStr, sizeof(addressStr), true);
|
||||||
|
LOG(INFO,"Server started: %hs", addressStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ee_network::Lobby::Connect(const char *addressStr) {
|
||||||
|
SteamNetworkingIPAddr address; address.ParseString(addressStr);
|
||||||
|
connection = steam->ConnectByIPAddress(address, 0, nullptr);
|
||||||
|
}
|
||||||
@ -26,6 +26,7 @@
|
|||||||
"version>=": "0"
|
"version>=": "0"
|
||||||
},
|
},
|
||||||
"rmlui",
|
"rmlui",
|
||||||
|
"gamenetworkingsockets",
|
||||||
"gtest"
|
"gtest"
|
||||||
],
|
],
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user