diff --git a/.gitignore b/.gitignore index ed28a3e..73da588 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ - +assets-src/map/ee.map.bmp out/ build/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e8fe47..56de837 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,7 +22,10 @@ add_subdirectory(sdk) add_subdirectory(p7lib) add_subdirectory(utils) -#add_subdirectory(server/lib) +add_subdirectory(economy) + +add_subdirectory(server/lib) +add_subdirectory(server/dedicated) add_subdirectory(engine-api) # add_subdirectory(renderers/magnum-renderer) diff --git a/assets-src/economy/enterprises/base.dat.json b/assets-src/economy/enterprises/base.dat.json new file mode 100644 index 0000000..af7148e --- /dev/null +++ b/assets-src/economy/enterprises/base.dat.json @@ -0,0 +1,3 @@ +{ + "content": [] +} \ No newline at end of file diff --git a/assets-src/economy/resources/base.dat.json b/assets-src/economy/resources/base.dat.json new file mode 100644 index 0000000..e354c34 --- /dev/null +++ b/assets-src/economy/resources/base.dat.json @@ -0,0 +1,12 @@ +{ + "content": [ + { + "tag": "coal", + "baseCost": 10 + }, + { + "tag": "oil", + "baseCost": 50 + } + ] +} \ No newline at end of file diff --git a/assets-src/map/ET.map.bmp b/assets-src/map/ET.map.bmp deleted file mode 100644 index 08feca0..0000000 Binary files a/assets-src/map/ET.map.bmp and /dev/null differ diff --git a/assets-src/regions.json b/assets-src/regions.json index 8d4270e..aed10ad 100644 --- a/assets-src/regions.json +++ b/assets-src/regions.json @@ -1,11 +1,57 @@ { - "pepene": "#AA00FF", - "scarna": "#FF007F", - - "dineria": "#FFFF00", - "perdania": "#7FFF00", - "arstinia": "#FF0000", - "alende": "#0000FF", - "zerandia": "#FF5400", - "mgahvatia": "#5400FF" -} \ No newline at end of file + "dineria": "#FFF600", + "minderia": "#F2EA00", + "ragland": "#D8D100", + "trinadia": "#E5DD00", + "kostyania": "#BFB800", + "nikland": "#CCC500", + "perdania": "#B2AC00", + "eddoran": "#A5A000", + "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" +} diff --git a/assets-src/scenarios/empires.save.json b/assets-src/scenarios/empires.save.json index f0fab0b..93807f2 100644 --- a/assets-src/scenarios/empires.save.json +++ b/assets-src/scenarios/empires.save.json @@ -2,23 +2,80 @@ "content": { "subjects": [ { - "tag": "FSC", - "color": "#FF0000", + "tag": "UCB", + "color": "#F7EF00", "regions": [ - "pepene", - "scarna" + "dineria", + "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", - "color": "#0000FF", + "tag": "FSC", + "color": "#AB5177", "regions": [ - "dineria", - "perdania", - "arstinia", "alende", - "zerandia", - "mgahvatia" + "pepene", + "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" ] } ] diff --git a/assets/economy/enterprises/base.dat b/assets/economy/enterprises/base.dat new file mode 100644 index 0000000..b0a09cf --- /dev/null +++ b/assets/economy/enterprises/base.dat @@ -0,0 +1 @@ +@ \ No newline at end of file diff --git a/assets/economy/resources/base.dat b/assets/economy/resources/base.dat new file mode 100644 index 0000000..241339f --- /dev/null +++ b/assets/economy/resources/base.dat @@ -0,0 +1,2 @@ +@sUBZ +EdÐÎ2 \ No newline at end of file diff --git a/assets/map/ee.map b/assets/map/ee.map new file mode 100644 index 0000000..5548cea Binary files /dev/null and b/assets/map/ee.map differ diff --git a/assets/scenarios/empires.save b/assets/scenarios/empires.save index 0ee991c..f0120f5 100644 --- a/assets/scenarios/empires.save +++ b/assets/scenarios/empires.save @@ -1 +1,3 @@ - @@'QOãÈpÿ HKԳÿ4UÿªÿfÿÿÿÿÿVTÿÿT \ No newline at end of file +(@@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õ¹�wؚŠú»YwþßNüÊDwóÝX÷¸WwÝÖfåËZñßQ \ No newline at end of file diff --git a/economy/CMakeLists.txt b/economy/CMakeLists.txt new file mode 100644 index 0000000..13bb1ce --- /dev/null +++ b/economy/CMakeLists.txt @@ -0,0 +1,9 @@ +include(${CMAKE_SOURCE_DIR}/cmake/library.cmake) + +set(LIB economy) + +# Find dependencies + +# Link library + +add_library_target(${LIB}) \ No newline at end of file diff --git a/economy/modules/Corporation.cppm b/economy/modules/Corporation.cppm new file mode 100644 index 0000000..a67e951 --- /dev/null +++ b/economy/modules/Corporation.cppm @@ -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; + + class Enterprise { + public: + EnterpriseDefinition definition; + std::uint32_t stateId; + std::uint8_t damage; + }; + + class Corporation { + public: + std::vector enterprises; + }; + +} \ No newline at end of file diff --git a/economy/modules/Economy.cppm b/economy/modules/Economy.cppm new file mode 100644 index 0000000..339f333 --- /dev/null +++ b/economy/modules/Economy.cppm @@ -0,0 +1,3 @@ +export module Economy; +export import :Resources; +export import :Corporation; diff --git a/economy/modules/Resources.cppm b/economy/modules/Resources.cppm new file mode 100644 index 0000000..d1b2fc2 --- /dev/null +++ b/economy/modules/Resources.cppm @@ -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; + + 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]; + }; + +} \ No newline at end of file diff --git a/engine-api/CMakeLists.txt b/engine-api/CMakeLists.txt index 2f00c5e..3b5bc4b 100644 --- a/engine-api/CMakeLists.txt +++ b/engine-api/CMakeLists.txt @@ -11,7 +11,7 @@ find_package(glm CONFIG REQUIRED) add_library_target(${LIB}) -target_link_libraries(${LIB} PRIVATE p7) target_link_libraries(${LIB} PUBLIC utils) +target_link_libraries(${LIB} PUBLIC economy) target_link_libraries(${LIB} PRIVATE glfw) target_link_libraries(${LIB} PRIVATE glm::glm) \ No newline at end of file diff --git a/engine-api/modules/AssetData.cppm b/engine-api/modules/AssetData.cppm index 6074d80..a4a0700 100644 --- a/engine-api/modules/AssetData.cppm +++ b/engine-api/modules/AssetData.cppm @@ -1,5 +1,6 @@ export module AssetData; export import GameData; +export import Economy; export import views; import std; @@ -13,6 +14,8 @@ export namespace ee_assets { MAP_MESH, SAVE, + RESOURCE_LIST, + ENTERPRISE_LIST, SHADER, MATERIAL, @@ -41,6 +44,18 @@ export namespace ee_assets { 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; struct AssetMaterial { diff --git a/engine/src/Client.cpp b/engine/src/Client.cpp index 91c6db7..9950a6e 100644 --- a/engine/src/Client.cpp +++ b/engine/src/Client.cpp @@ -15,8 +15,9 @@ void Client::Init() const { // TEST ee_assets::AssetSave& asset = Engine::assetManager->Get(save); - ViewModelSubject(asset.content.subjects[0]); - ViewModelSubject(asset.content.subjects[1]); + for (const ee_data::Subject &subject : asset.content.subjects) { + ViewModelSubject(subject); + } } const IDataChannel* Client::GetConnection() const { diff --git a/engine/src/main.cpp b/engine/src/main.cpp index d486919..7a37525 100644 --- a/engine/src/main.cpp +++ b/engine/src/main.cpp @@ -41,7 +41,7 @@ int main(int argc, char **argv) { Engine::assetManager->LoadDir(ee_assets::MATERIAL, "assets/materials"); // 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"); // Setup input manager @@ -50,7 +50,7 @@ int main(int argc, char **argv) { // Setup renderer Engine::renderer = reinterpret_cast(Allocate(window)); - ee_assets::AssetMapMesh& regions = Engine::assetManager->FindOnce("assets/map/regions.map"); + ee_assets::AssetMapMesh& regions = Engine::assetManager->FindOnce("assets/map/ee.map"); Engine::renderer->RegisterMapMesh(®ions.content); //TODO: ShaderRefs builder from set of AssetMaterial diff --git a/nudge.py b/nudge.py new file mode 100644 index 0000000..c8b9bff --- /dev/null +++ b/nudge.py @@ -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() \ No newline at end of file diff --git a/sdk/modules/compile.json.cppm b/sdk/modules/compile.json.cppm index 10dc8de..71d0b45 100644 --- a/sdk/modules/compile.json.cppm +++ b/sdk/modules/compile.json.cppm @@ -66,7 +66,8 @@ export namespace ee_sdk { return key == "tag" || key.ends_with("AssetID") || - key.ends_with("Type"); + key.ends_with("Type") || + key.ends_with("Recipe"); } static void CompileHashes(const std::basic_string &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, 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); value = hex; diff --git a/sdk/modules/io.cppm b/sdk/modules/io.cppm index 4fdb64a..28e26d7 100644 --- a/sdk/modules/io.cppm +++ b/sdk/modules/io.cppm @@ -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 { NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE( @@ -40,6 +55,16 @@ export namespace ee_assets vertexShaderAssetID, fragmentShaderAssetID ) + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE( + AssetResourceList, + content + ) + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE( + AssetEnterpriseList, + content + ) } export namespace ee_sdk { diff --git a/sdk/src/main.cpp b/sdk/src/main.cpp index 735abd2..d753a09 100644 --- a/sdk/src/main.cpp +++ b/sdk/src/main.cpp @@ -117,6 +117,10 @@ int main(const int argc, char **argv) { IfNotExluded(excludedTypes, "scenarios", [jsonCompiler, src, out] { CompileJson(jsonCompiler, "scenarios", src, out); }); + IfNotExluded(excludedTypes, "economy", [jsonCompiler, src, out] { + CompileJson(jsonCompiler, "economy/resources", src, out); + CompileJson(jsonCompiler, "economy/enterprises", src, out); + }); constexpr ee_sdk::MapMeshCompiler mapMeshCompiler; IfNotExluded(excludedTypes, "map", [mapMeshCompiler, src, out] { diff --git a/server/dedicated/CMakeLists.txt b/server/dedicated/CMakeLists.txt new file mode 100644 index 0000000..38d50ea --- /dev/null +++ b/server/dedicated/CMakeLists.txt @@ -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) \ No newline at end of file diff --git a/server/dedicated/src/main.cpp b/server/dedicated/src/main.cpp new file mode 100644 index 0000000..12abc4f --- /dev/null +++ b/server/dedicated/src/main.cpp @@ -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; +} \ No newline at end of file diff --git a/server/lib/CMakeLists.txt b/server/lib/CMakeLists.txt index bdb1f22..f1868de 100644 --- a/server/lib/CMakeLists.txt +++ b/server/lib/CMakeLists.txt @@ -2,4 +2,13 @@ include(${CMAKE_SOURCE_DIR}/cmake/library.cmake) set(LIB server) -add_library_target(${LIB}) \ No newline at end of file +# 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) \ No newline at end of file diff --git a/server/lib/modules/Network.cppm b/server/lib/modules/Network.cppm new file mode 100644 index 0000000..cfc8e8b --- /dev/null +++ b/server/lib/modules/Network.cppm @@ -0,0 +1,36 @@ +module; + +#include + +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(); + }; + +} \ No newline at end of file diff --git a/server/lib/src/Network.cpp b/server/lib/src/Network.cpp new file mode 100644 index 0000000..64cbd89 --- /dev/null +++ b/server/lib/src/Network.cpp @@ -0,0 +1,49 @@ +module; + +#include +#include + +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); +} diff --git a/vcpkg.json b/vcpkg.json index 11ede29..f694c64 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -26,6 +26,7 @@ "version>=": "0" }, "rmlui", + "gamenetworkingsockets", "gtest" ], "version": "0.0.1",