- Поддерживаемые версии
- 1.19
- 1.20
- 1.21
Я прекрасно понимаю, что сейчас каждый второй знает как настроить server.properties, но все же не все настройки мы знаем и есть люди которые еще не умеют настраивать его, да и просто чтобы легче и быстрее настроить т.к там есть подсказки, чекбоксы где надо (лишнее не напишите) и поля ввода для кастома (по типу motd, seed, resuorce-pack)
можно значение менять колесиком, нажмите на настройку (выделите ее) и крутите колесико мыши
сделан на python через pyqt6, shutil, pathlib и datetime
Если кому надо вот исходник:
можно значение менять колесиком, нажмите на настройку (выделите ее) и крутите колесико мыши
сделан на 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()
- Классификация ПО
- Программа
- Операционная система
- Windows
- Набор технологий
- Python