Иконка ресурса

Программа mSettings 1.0

Создайте и подтвердите аккаунт для скачивания
Поддерживаемые версии
  1. 1.19
  2. 1.20
  3. 1.21
Я прекрасно понимаю, что сейчас каждый второй знает как настроить server.properties, но все же не все настройки мы знаем и есть люди которые еще не умеют настраивать его, да и просто чтобы легче и быстрее настроить т.к там есть подсказки, чекбоксы где надо (лишнее не напишите) и поля ввода для кастома (по типу motd, seed, resuorce-pack)
можно значение менять колесиком, нажмите на настройку (выделите ее) и крутите колесико мыши
сделан на python через pyqt6, shutil, pathlib и datetime
Если кому надо вот исходник:
Python:
import sys
import os
import shutil
from pathlib import Path
from datetime import datetime

from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QScrollArea, QFrame, QLabel, QLineEdit, QCheckBox, QComboBox,
    QPushButton, QFileDialog, QMessageBox, QSpinBox, QFormLayout
)
from PyQt6.QtCore import Qt, QEvent
from PyQt6.QtGui import QFont, QWheelEvent

SETTINGS_DEFINITION = {

    "motd": {
        "type": "motd",
        "label": "MOTD (сообщение в списке серверов)",
        "hint": "Текст, который показывается в списке серверов Minecraft. Можно использовать &x&F&F... для градиента.",
        "default": "A Minecraft Server",
    },
    "server-port": {
        "type": "int",
        "label": "Порт сервера",
        "hint": "Порт для подключения игроков. По умолчанию 25565. Меняйте только если запускаете несколько серверов.",
        "default": "25565",
        "min": 1,
        "max": 65535,
    },
    "server-ip": {
        "type": "text",
        "label": "IP сервера",
        "hint": "Оставьте пустым для автоматического определения. Укажите IP если у сервера несколько сетевых интерфейсов.",
        "default": "",
    },
    "max-players": {
        "type": "int",
        "label": "Максимум игроков",
        "hint": "Максимальное количество одновременных подключений (0-2147483647).",
        "default": "20",
        "min": 0,
        "max": 2147483647,
    },
    "online-mode": {
        "type": "bool",
        "label": "Онлайн-режим (лицензия)",
        "hint": "true — только лицензионные игроки. false — пиратка (оффлайн-режим). ВНИМАНИЕ: при false любой может зайти под чужим ником!",
        "default": "true",
    },
    "white-list": {
        "type": "bool",
        "label": "Белый список (whitelist)",
        "hint": "true — только игроки из whitelist.json могут зайти. false — могут зайти все.",
        "default": "false",
    },
    "enforce-whitelist": {
        "type": "bool",
        "label": "Принудительный белый список",
        "hint": "true — кикает игроков не из whitelist, если они уже на сервере при включении.",
        "default": "false",
    },

    "level-name": {
        "type": "text",
        "label": "Название мира (папка)",
        "hint": "Имя папки с миром. По умолчанию 'world'. Можно указать своё для нескольких миров.",
        "default": "world",
    },
    "level-seed": {
        "type": "seed",
        "label": "Сид мира",
        "hint": "Сид генерации мира. Оставьте пустым для случайного. Работает только при первой генерации!",
        "default": "",
    },
    "level-type": {
        "type": "combo",
        "label": "Тип мира",
        "hint": "Тип генерации мира. normal — обычный, flat — плоский, largebiomes — большие биомы, amplified — амплифицированный.",
        "default": "minecraft:normal",
        "values": ["minecraft:normal", "minecraft:flat", "minecraft:largebiomes", "minecraft:amplified"],
    },
    "generator-settings": {
        "type": "text",
        "label": "Настройки генератора",
        "hint": "JSON-настройки для кастомной генерации мира. Обычно не трогайте.",
        "default": "{}",
    },
    "generate-structures": {
        "type": "bool",
        "label": "Генерировать структуры",
        "hint": "true — деревни, крепости, храмы и т.д. false — только ландшафт.",
        "default": "true",
    },

    "gamemode": {
        "type": "combo",
        "label": "Режим игры по умолчанию",
        "hint": "Режим для новых игроков. survival — выживание, creative — креатив, adventure — приключение, spectator — наблюдение.",
        "default": "survival",
        "values": ["survival", "creative", "adventure", "spectator"],
    },
    "force-gamemode": {
        "type": "bool",
        "label": "Принудительный режим игры",
        "hint": "true — при каждом входе игроку устанавливается режим из gamemode. false — сохраняется его режим.",
        "default": "false",
    },
    "difficulty": {
        "type": "combo",
        "label": "Сложность",
        "hint": "peaceful — мирная (без мобов), easy — лёгкая, normal — нормальная, hard — сложная.",
        "default": "easy",
        "values": ["peaceful", "easy", "normal", "hard"],
    },
    "hardcore": {
        "type": "bool",
        "label": "Хардкор",
        "hint": "true — хардкорный режим (одна жизнь, после смерти — наблюдение). Меняет сложность на hard!",
        "default": "false",
    },
    "pvp": {
        "type": "bool",
        "label": "PvP",
        "hint": "true — игроки могут драться друг с другом. false — PvP отключено.",
        "default": "true",
    },
    "allow-flight": {
        "type": "bool",
        "label": "Разрешить полёт",
        "hint": "true — не кикает игроков за полёт. Полезно если есть плагины на полёт.",
        "default": "false",
    },
    "allow-nether": {
        "type": "bool",
        "label": "Включить Нижний мир (Nether)",
        "hint": "true — порталы в ад работают. false — порталы не активируются.",
        "default": "true",
    },

    "enable-command-block": {
        "type": "bool",
        "label": "Командные блоки",
        "hint": "true — командные блоки работают. false — не работают. Требуется для многих механизмов!",
        "default": "false",
    },
    "op-permission-level": {
        "type": "combo",
        "label": "Уровень прав операторов (/op)",
        "hint": "1 — обход спавн-защиты. 2 — команды управления игроками. 3 — команды управления сервером. 4 — всё (полный доступ).",
        "default": "4",
        "values": ["1", "2", "3", "4"],
    },
    "function-permission-level": {
        "type": "combo",
        "label": "Уровень прав для функций (datapacks)",
        "hint": "Минимальный уровень прав для выполнения команд из датапаков.",
        "default": "2",
        "values": ["1", "2", "3", "4"],
    },
    "broadcast-console-to-ops": {
        "type": "bool",
        "label": "Транслировать команды операторам",
        "hint": "true — операторы видят в чате какие команды выполняют другие операторы. Полезно для модерации.",
        "default": "true",
    },

    "require-resource-pack": {
        "type": "bool",
        "label": "Обязательный ресурс-пак",
        "hint": "true — игроки обязаны скачать ресурс-пак. false — по желанию.",
        "default": "false",
    },
    "resource-pack": {
        "type": "text",
        "label": "Ссылка на ресурс-пак",
        "hint": "Прямая ссылка на скачивание .zip файла ресурс-пака. Должна быть доступна без авторизации.",
        "default": "",
    },
    "resource-pack-id": {
        "type": "text",
        "label": "ID ресурс-пака",
        "hint": "Уникальный идентификатор ресурс-пака. Можно оставить пустым.",
        "default": "",
    },
    "resource-pack-sha1": {
        "type": "text",
        "label": "SHA-1 хеш ресурс-пака",
        "hint": "Хеш файла для проверки целостности. Можно оставить пустым.",
        "default": "",
    },
    "resource-pack-prompt": {
        "type": "text",
        "label": "Текст запроса ресурс-пака",
        "hint": "Сообщение, которое увидят игроки при запросе установки ресурс-пака.",
        "default": "",
    },

    "view-distance": {
        "type": "int",
        "label": "Дальность прорисовки (view-distance)",
        "hint": "Радиус прогрузки чанков вокруг игрока (3-32). Больше = красивее, но больше нагрузка на сервер.",
        "default": "10",
        "min": 3,
        "max": 32,
    },
    "simulation-distance": {
        "type": "int",
        "label": "Дальность симуляции",
        "hint": "Радиус в чанках где работают мобы, фермы и редстоун. Обычно равен view-distance или меньше.",
        "default": "10",
        "min": 3,
        "max": 32,
    },
    "entity-broadcast-range-percentage": {
        "type": "int",
        "label": "Дальность отправки данных о существах (%)",
        "hint": "Процент от view-distance на котором игроки видят мобов. 100 = стандарт.",
        "default": "100",
        "min": 10,
        "max": 1000,
    },
    "max-tick-time": {
        "type": "int",
        "label": "Максимальное время тика (мс)",
        "hint": "Если тик длится дольше — сервер аварийно останавливается. -1 = отключено. По умолчанию 60000.",
        "default": "60000",
        "min": -1,
        "max": 2147483647,
    },
    "max-chained-neighbor-updates": {
        "type": "int",
        "label": "Максимум цепных обновлений блоков",
        "hint": "Лимит каскадных обновлений редстоуна. 1000000 по умолчанию. Уменьшите если лагает от редстоуна.",
        "default": "1000000",
        "min": 0,
        "max": 2147483647,
    },
    "rate-limit": {
        "type": "int",
        "label": "Ограничение пакетов",
        "hint": "Максимум пакетов в секунду от игрока. 0 = отключено.",
        "default": "0",
        "min": 0,
        "max": 100000,
    },

    "hide-online-players": {
        "type": "bool",
        "label": "Скрывать список игроков",
        "hint": "true — игроки не видят список онлайна в меню паузы. Полезно для стримерских серверов.",
        "default": "false",
    },
    "enable-status": {
        "type": "bool",
        "label": "Отображать сервер в списке",
        "hint": "true — сервер виден в общем списке серверов (если не через прокси). false — скрыт.",
        "default": "true",
    },
    "prevent-proxy-connections": {
        "type": "bool",
        "label": "Блокировать прокси-подключения",
        "hint": "true — блокирует подключения через VPN/прокси. Может блокировать некоторых игроков.",
        "default": "false",
    },
    "use-native-transport": {
        "type": "bool",
        "label": "Нативная оптимизация сети",
        "hint": "true — использует системную оптимизацию для сети. Рекомендуется на Windows.",
        "default": "true",
    },
    "network-compression-threshold": {
        "type": "int",
        "label": "Порог сжатия пакетов",
        "hint": "Пакеты больше этого размера (в байтах) будут сжаты. 256 = стандарт. -1 = отключить сжатие.",
        "default": "256",
        "min": -1,
        "max": 2147483647,
    },

    "spawn-protection": {
        "type": "int",
        "label": "Защита спавна (радиус)",
        "hint": "Радиус в блоках вокруг спавна где только операторы могут строить. 0 = отключено.",
        "default": "16",
        "min": 0,
        "max": 2147483647,
    },
    "spawn-monsters": {
        "type": "bool",
        "label": "Спавн монстров",
        "hint": "true — монстры спавнятся. false — только мирные мобы.",
        "default": "true",
    },
    "sync-chunk-writes": {
        "type": "bool",
        "label": "Синхронная запись чанков",
        "hint": "true — чанки записываются синхронно (безопаснее но медленнее). false — асинхронно (быстрее).",
        "default": "true",
    },
    "pause-when-empty-seconds": {
        "type": "int",
        "label": "Пауза когда пусто (сек)",
        "hint": "Сервер ставится на паузу через N секунд после выхода последнего игрока. -1 = отключено.",
        "default": "-1",
        "min": -1,
        "max": 3600,
    },
    "player-idle-timeout": {
        "type": "int",
        "label": "Кик за бездействие (минут)",
        "hint": "Кикает игрока после N минут афк. 0 = отключено.",
        "default": "0",
        "min": 0,
        "max": 2147483647,
    },
}


class NoScrollComboBox(QComboBox):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)

    def wheelEvent(self, event: QWheelEvent):
        if self.hasFocus():
            super().wheelEvent(event)
        else:
            event.ignore()


class NoScrollSpinBox(QSpinBox):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)

    def wheelEvent(self, event: QWheelEvent):
        if self.hasFocus():
            super().wheelEvent(event)
        else:
            event.ignore()


class FocusCheckBox(QCheckBox):
    def __init__(self, text, parent=None):
        super().__init__(text, parent)
        self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)


class FocusLineEdit(QLineEdit):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)


class SettingsEditor(QMainWindow):
    def __init__(self):
        super().__init__()
        self.file_path = None
        self.properties = {}
        self.widgets = {}
        self.backup_path = None
        self.init_ui()

    def init_ui(self):
        self.setWindowTitle("mSettings — Редактор server.properties")
        self.setGeometry(100, 100, 850, 750)
        self.setMinimumSize(700, 500)

        central = QWidget()
        self.setCentralWidget(central)
        layout = QVBoxLayout(central)
        layout.setContentsMargins(14, 14, 14, 14)
        layout.setSpacing(8)

        top_frame = QFrame()
        top_layout = QHBoxLayout(top_frame)
        top_layout.setContentsMargins(0, 0, 0, 0)

        self.file_label = QLabel("Файл не выбран")
        self.file_label.setStyleSheet("color: #888888; font-style: italic; font-size: 12px;")

        open_btn = QPushButton("📂 Открыть .properties")
        open_btn.clicked.connect(self.open_file)

        save_btn = QPushButton("💾 Сохранить")
        save_btn.setObjectName("saveBtn")
        save_btn.clicked.connect(self.save_file)
        save_btn.setEnabled(False)

        save_as_btn = QPushButton("📄 Сохранить как...")
        save_as_btn.clicked.connect(self.save_as_file)
        save_as_btn.setEnabled(False)

        top_layout.addWidget(self.file_label)
        top_layout.addStretch()
        top_layout.addWidget(open_btn)
        top_layout.addWidget(save_as_btn)
        top_layout.addWidget(save_btn)
        layout.addWidget(top_frame)

        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setFrameShape(QFrame.Shape.NoFrame)

        self.scroll_content = QWidget()
        self.form_layout = QFormLayout(self.scroll_content)
        self.form_layout.setSpacing(14)
        self.form_layout.setContentsMargins(4, 4, 4, 24)

        scroll.setWidget(self.scroll_content)
        layout.addWidget(scroll)

        bottom_frame = QFrame()
        bottom_layout = QHBoxLayout(bottom_frame)
        bottom_layout.setContentsMargins(0, 0, 0, 0)

        self.status_label = QLabel("")
        self.status_label.setStyleSheet("color: #888888; font-size: 12px;")

        bottom_layout.addWidget(self.status_label)
        layout.addWidget(bottom_frame)

        self.setup_style()

    def setup_style(self):
        self.setStyleSheet("""
            QMainWindow { background-color: #1a1a1a; }
            QWidget { color: #d0d0d0; }
            QLineEdit, QComboBox, QSpinBox {
                background-color: #252525; color: #d0d0d0;
                border: 1px solid #3a3a3a; border-radius: 6px;
                padding: 8px 12px; font-size: 13px;
                selection-background-color: #404040;
            }
            QLineEdit:focus, QComboBox:focus, QSpinBox:focus {
                border: 1px solid #606060;
                background-color: #2a2a2a;
            }
            QComboBox::drop-down {
                border: none; padding-right: 10px;
            }
            QComboBox QAbstractItemView {
                background-color: #252525; color: #d0d0d0;
                selection-background-color: #404040; border: 1px solid #3a3a3a;
            }
            QPushButton {
                background-color: #333333; color: #d0d0d0;
                border: none; border-radius: 6px; padding: 8px 18px;
                font-weight: bold; font-size: 13px;
            }
            QPushButton:hover { background-color: #404040; }
            QPushButton:pressed { background-color: #505050; }
            QPushButton:disabled { background-color: #252525; color: #666666; }
            QPushButton#saveBtn { background-color: #2d5a27; }
            QPushButton#saveBtn:hover { background-color: #367030; }
            QCheckBox {
                color: #d0d0d0; font-size: 13px; spacing: 8px;
            }
            QCheckBox::indicator {
                width: 18px; height: 18px; border-radius: 4px;
                border: 2px solid #3a3a3a; background-color: #252525;
            }
            QCheckBox::indicator:checked {
                background-color: #2d5a27; border-color: #2d5a27;
            }
            QCheckBox:focus {
                outline: none;
            }
            QScrollArea { border: none; background-color: transparent; }
            QScrollBar:vertical {
                background-color: #1a1a1a; width: 8px; border-radius: 4px;
            }
            QScrollBar::handle:vertical {
                background-color: #3a3a3a; border-radius: 4px; min-height: 30px;
            }
            QScrollBar::handle:vertical:hover { background-color: #505050; }
            QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }
        """)

    def open_file(self):
        path, _ = QFileDialog.getOpenFileName(
            self, "Открыть server.properties",
            "", "Properties (*.properties);;Все файлы (*.*)"
        )
        if not path:
            return
        self.file_path = path
        self.file_label.setText(Path(path).name)
        self.load_properties(path)
        self.build_ui()
        self.status_label.setText(f"Загружен: {path} | Строчек: {len(self.properties)}")

        for btn in self.findChildren(QPushButton):
            if btn.objectName() == "saveBtn":
                btn.setEnabled(True)
            if btn.text() == "📄 Сохранить как...":
                btn.setEnabled(True)

    def load_properties(self, path):
        self.properties = {}
        with open(path, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                if "=" in line:
                    key, value = line.split("=", 1)
                    self.properties[key.strip()] = value.strip()

    def save_file(self):
        if not self.file_path:
            self.save_as_file()
            return

        self.create_backup()
        self.collect_values()

        with open(self.file_path, "w", encoding="utf-8") as f:
            for key, value in self.properties.items():
                f.write(f"{key}={value}\n")

        self.status_label.setText(f"✅ Сохранено в {Path(self.file_path).name}")
        QMessageBox.information(self, "Успех", f"Файл сохранён!\nБэкап: {self.backup_path}")

    def save_as_file(self):
        path, _ = QFileDialog.getSaveFileName(
            self, "Сохранить как...",
            "server.properties",
            "Properties (*.properties);;Все файлы (*.*)"
        )
        if path:
            self.file_path = path
            self.file_label.setText(Path(path).name)
            self.save_file()

    def create_backup(self):
        if not self.file_path or not os.path.exists(self.file_path):
            return
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        backup_dir = Path(self.file_path).parent / "mSettings_backups"
        backup_dir.mkdir(exist_ok=True)
        self.backup_path = backup_dir / f"{Path(self.file_path).stem}_{timestamp}.properties"
        shutil.copy2(self.file_path, self.backup_path)

    def build_ui(self):
        while self.form_layout.count():
            child = self.form_layout.takeAt(0)
            if child.widget():
                child.widget().deleteLater()

        self.widgets = {}

        for key, definition in SETTINGS_DEFINITION.items():
            label_text = definition["label"]
            hint_text = definition["hint"]
            setting_type = definition["type"]
            default = definition.get("default", "")
            current_value = self.properties.get(key, default)

            row_widget = QWidget()
            row_widget.setStyleSheet("background-color: transparent;")
            row_layout = QVBoxLayout(row_widget)
            row_layout.setContentsMargins(0, 4, 0, 4)
            row_layout.setSpacing(2)

            main_label = QLabel(label_text)
            main_label.setStyleSheet("font-weight: bold; color: #d0d0d0; font-size: 13px; background-color: transparent;")
            row_layout.addWidget(main_label)

            if setting_type == "bool":
                widget = FocusCheckBox("Включено")
                widget.setChecked(current_value.lower() == "true")
                row_layout.addWidget(widget)

            elif setting_type == "combo":
                widget = NoScrollComboBox()
                widget.addItems(definition.get("values", []))
                widget.setCurrentText(current_value)
                row_layout.addWidget(widget)

            elif setting_type == "int":
                widget = NoScrollSpinBox()
                widget.setMinimum(definition.get("min", -2147483647))
                widget.setMaximum(definition.get("max", 2147483647))
                try:
                    widget.setValue(int(current_value))
                except ValueError:
                    widget.setValue(int(default))
                row_layout.addWidget(widget)

            elif setting_type == "motd":
                widget = FocusLineEdit()
                widget.setText(current_value)
                widget.setPlaceholderText("MOTD сервера...")
                widget.setStyleSheet("font-size: 14px; padding: 10px;")
                row_layout.addWidget(widget)

            elif setting_type == "seed":
                widget = FocusLineEdit()
                widget.setText(current_value)
                widget.setPlaceholderText("Оставьте пустым для случайного сида")
                row_layout.addWidget(widget)

            else:
                widget = FocusLineEdit()
                widget.setText(current_value)
                row_layout.addWidget(widget)

            hint_label = QLabel(hint_text)
            hint_label.setWordWrap(True)
            hint_label.setStyleSheet("color: #707070; font-size: 11px; padding-left: 4px; background-color: transparent;")
            row_layout.addWidget(hint_label)

            separator = QFrame()
            separator.setFrameShape(QFrame.Shape.HLine)
            separator.setStyleSheet("background-color: #2a2a2a; max-height: 1px;")
            row_layout.addWidget(separator)

            self.form_layout.addRow(row_widget)
            self.widgets[key] = widget

    def collect_values(self):
        for key, widget in self.widgets.items():
            definition = SETTINGS_DEFINITION.get(key, {})
            setting_type = definition.get("type", "text")

            if setting_type == "bool":
                self.properties[key] = "true" if widget.isChecked() else "false"
            elif setting_type == "combo":
                self.properties[key] = widget.currentText()
            elif setting_type == "int":
                self.properties[key] = str(widget.value())
            else:
                self.properties[key] = widget.text()


def main():
    app = QApplication(sys.argv)
    app.setApplicationName("mSettings")
    window = SettingsEditor()
    window.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()
Классификация ПО
  1. Программа
Операционная система
  1. Windows
Набор технологий
Python
Автор
i_am_Shure
Скачивания
4
Просмотры
40
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок

Поделиться ресурсом

Назад
Сверху Снизу