Network base, new map, economy base

- Steam networking dependencies
- Full-size regions map
- Economy base structures and assets
This commit is contained in:
legatt 2026-10-14 12:10:14 +03:00
parent 4815fb4e1e
commit 1adddd3f1c
29 changed files with 993 additions and 32 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
assets-src/map/ee.map.bmp
out/ out/
build/ build/

View File

@ -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)

View File

@ -0,0 +1,3 @@
{
"content": []
}

View File

@ -0,0 +1,12 @@
{
"content": [
{
"tag": "coal",
"baseCost": 10
},
{
"tag": "oil",
"baseCost": 50
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 MiB

View File

@ -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"
} }

View File

@ -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"
] ]
} }
] ]

View File

@ -0,0 +1 @@
@

View File

@ -0,0 +1,2 @@
@sUBZ
Edミホ2

BIN
assets/map/ee.map Normal file

Binary file not shown.

View File

@ -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
View File

@ -0,0 +1,9 @@
include(${CMAKE_SOURCE_DIR}/cmake/library.cmake)
set(LIB economy)
# Find dependencies
# Link library
add_library_target(${LIB})

View 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;
};
}

View File

@ -0,0 +1,3 @@
export module Economy;
export import :Resources;
export import :Corporation;

View 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];
};
}

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

View File

@ -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(&regions.content); Engine::renderer->RegisterMapMesh(&regions.content);
//TODO: ShaderRefs builder from set of AssetMaterial //TODO: ShaderRefs builder from set of AssetMaterial

604
nudge.py Normal file
View 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()

View File

@ -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;

View File

@ -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 {

View File

@ -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] {

View 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)

View 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;
}

View File

@ -2,4 +2,13 @@ include(${CMAKE_SOURCE_DIR}/cmake/library.cmake)
set(LIB server) set(LIB server)
# Find dependencies
find_package(GameNetworkingSockets CONFIG REQUIRED)
# Link library
add_library_target(${LIB}) add_library_target(${LIB})
target_link_libraries(${LIB} PRIVATE utils)
target_link_libraries(${LIB} PRIVATE GameNetworkingSockets::shared)

View 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();
};
}

View 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);
}

View File

@ -26,6 +26,7 @@
"version>=": "0" "version>=": "0"
}, },
"rmlui", "rmlui",
"gamenetworkingsockets",
"gtest" "gtest"
], ],
"version": "0.0.1", "version": "0.0.1",