Deadlock Font Changer
Deadlock Font Changer — это графическая утилита, позволяющая легко заменять стандартный шрифт в игре Deadlock на любой пользовательский. Приложение имеет стильный неоновый интерфейс, автоматически находит игру и делает процесс смены шрифта максимально простым.
Функции
• Стильный интерфейс: Современный дизайн в неоновом стиле со скругленными углами и эффектами свечения.
• Автоматический поиск: Утилита сама находит директорию установки Deadlock на вашем компьютере.
• Выбор шрифта: Позволяет выбирать любой файл шрифта в формате .ttf или .otf.
• Безопасная установка: Автоматически создает резервную копию (.backup) оригинального файла конфигурации.
• Встроенная справка: Вся необходимая информация доступна по клику на иконку "i" прямо в приложении.
Установка и использование
Шаг 1: Важная подготовка
Перед первым запуском необходимо подготовить игру. Этот шаг выполняется только один раз!
• Скачайте архив с программой и распакуйте его.
• Найдите в папке с программой файл fonts.conf.
• Скопируйте этот файл и замените им оригинальный файл в папке с игрой по пути:
.../steamapps/common/Deadlock/game/citadel/panorama/fonts/
Шаг 2: Использование программы
• Запустите Deadlock Font Changer.exe.
• Если путь к игре не был найден автоматически, программа попросит указать его вручную.
• Нажмите "Выбрать .ttf/.otf файл" и выберите шрифт, который хотите установить.
• Нажмите "Применить шрифт".
• Готово! Запускайте игру и наслаждайтесь новым шрифтом.
Устранение неполадок
• Шрифт не применяется: Убедитесь, что вы выполнили "Шаг 1: Важная подготовка" и заменили fonts.conf в папке с игрой.
• Проблемы с правами доступа: Если появляется ошибка доступа, запустите Deadlock Font Changer.exe от имени администратора.
• Как вернуть стандартный шрифт:
• Перейдите в папку .../Deadlock/game/citadel/panorama/fonts/.
• Удалите измененный файл fonts.conf.
• Найдите файл fonts.conf.backup и переименуйте его обратно в fonts.conf.
Сообщество
Присоединяйтесь к нам, чтобы следить за обновлениями и другими модами!
The source code:
import sys
import os
import shutil
import webbrowser
import locale
from pathlib import Path
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QPushButton, QFileDialog,
QProgressBar, QGraphicsDropShadowEffect,
QSizePolicy, QSpacerItem, QDialog, QTextBrowser)
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSize, QPoint, QRectF
from PyQt6.QtGui import (QPainter, QColor, QBrush, QFont, QPainterPath, QIcon)
# --- ФУНКЦИЯ ДЛЯ ПРАВИЛЬНЫХ ПУТЕЙ В EXE ---
def resource_path(relative_path):
""" Получает абсолютный путь к ресурсам, работает и для dev, и для PyInstaller """
try:
# PyInstaller создает временную папку и хранит путь в _MEIPASS
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
# --- СЛОВАРЬ ЛОКАЛИЗАЦИИ ---
I18N = {
'ru': {
'app_title': "Deadlock Font Changer",
'info_title': "О программе",
'loading_title': "Deadlock Font Changer",
'loading_text': "Поиск Deadlock...",
'main_title': "DEADLOCK",
'main_subtitle': "Font Changer",
'status_found': "Deadlock найден: {path}",
'status_not_found': "Deadlock не найден. Укажите путь вручную.",
'status_path_set': "Путь к Deadlock задан: {path}",
'btn_select_path': "Указать путь к Deadlock",
'btn_select_font': "Выбрать .ttf/.otf файл",
'btn_apply': "Применить шрифт",
'font_not_selected': "Шрифт не выбран",
'font_selected': "Выбран: {font_name}",
'btn_ok': "ПОНЯТНО",
'dialog_select_folder': "Выберите папку с Deadlock",
'dialog_select_font': "Выберите шрифт",
'msg_error_title': "Ошибка",
'msg_success_title': "Успех",
'msg_wrong_folder': "Это не похоже на папку с Deadlock.\nПроверьте путь 'game/citadel/panorama/fonts'",
'msg_success_applied': "Шрифт успешно изменен!",
'msg_permission_error': "Ошибка доступа. \nЗапустите программу от имени администратора.",
'msg_unexpected_error': "Ошибка:\n{error}",
'readme_text': """
Обзор
Deadlock Font Changer — это графическая утилита для изменения конфигурации шрифтов в игре Deadlock.
ВАЖНЫЙ ШАГ:
Перед использованием скопируйте файл fonts.conf из папки с программой и замените им оригинальный файл в игре:
.../Deadlock/game/citadel/panorama/fonts/
• Укажите путь к Deadlock.
• Выберите файл шрифта.
• Нажмите "Применить шрифт".
Устранение неполадок
• Убедитесь, что вы выполнили "ВАЖНЫЙ ШАГ".
• Восстановление: Удалите fonts.conf и переименуйте fonts.conf.backup обратно.
"""
},
'en': {
'app_title': "Deadlock Font Changer",
'info_title': "About",
'loading_title': "Deadlock Font Changer",
'loading_text': "Searching for Deadlock...",
'main_title': "DEADLOCK",
'main_subtitle': "Font Changer",
'status_found': "Deadlock found: {path}",
'status_not_found': "Deadlock not found. Specify path manually.",
'status_path_set': "Deadlock path set: {path}",
'btn_select_path': "Select Deadlock Path",
'btn_select_font': "Select .ttf/.otf file",
'btn_apply': "Apply Font",
'font_not_selected': "No font selected",
'font_selected': "Selected: {font_name}",
'btn_ok': "OK",
'dialog_select_folder': "Select Deadlock Folder",
'dialog_select_font': "Select Font",
'msg_error_title': "Error",
'msg_success_title': "Success",
'msg_wrong_folder': "Incorrect Deadlock folder.\nCheck 'game/citadel/panorama/fonts'",
'msg_success_applied': "Font changed successfully!",
'msg_permission_error': "Permission error. \nRun as administrator.",
'msg_unexpected_error': "Error:\n{error}",
'readme_text': """
Overview
Deadlock Font Changer is a utility to change font configuration in Deadlock.
IMPORTANT STEP:
Before use, copy fonts.conf from the program folder and replace the original file in:
.../Deadlock/game/citadel/panorama/fonts/
• Specify Deadlock path.
• Select .ttf or .otf file.
• Click "Apply Font".
Troubleshooting
• Make sure you replaced fonts.conf manually.
• Restore: Delete fonts.conf and rename fonts.conf.backup back.
"""
}
}
CURRENT_LANGUAGE = 'en'
def tr(key, **kwargs):
return I18N[CURRENT_LANGUAGE].get(key, key).format(**kwargs)
# --- ШАБЛОН КОНФИГА ---
FONTS_CONF_TEMPLATE = '''
../../citadel/panorama/fonts
WINDOWSFONTDIR
~/.fonts
/usr/share/fonts
/usr/local/share/fonts
fonts
.uifont
FONTFILENAME
.ttf
.otf
WINDOWSTEMPDIR_FONTCONFIG_CACHE
~/.fontconfig
Stratum2
FONTNAME
Arial
FONTNAME
Times New Roman
FONTNAME
Courier New
FONTNAME
Noto Sans
FONTNAME
false
true
96
'''
# --- UI ЭЛЕМЕНТЫ ---
class GlowEffect(QGraphicsDropShadowEffect):
def __init__(self, color=QColor("#E040FB"), blur=35):
super().__init__()
self.setBlurRadius(blur); self.setColor(color); self.setXOffset(0); self.setYOffset(0)
class MainBackgroundWidget(QWidget):
def paintEvent(self, event):
painter = QPainter(self); painter.setRenderHint(QPainter.RenderHint.Antialiasing)
path = QPainterPath(); path.addRoundedRect(QRectF(self.rect()), 20, 20)
painter.setClipPath(path); painter.fillRect(self.rect(), QColor("#1a1a1a"))
class StyledButton(QPushButton):
def __init__(self, text_key, parent=None):
super().__init__(tr(text_key), parent); self.text_key = text_key
self.setMinimumHeight(40); self.setFont(QFont("Segoe UI", 11, QFont.Weight.Bold))
self.setGraphicsEffect(GlowEffect(blur=25)); self.update_style()
def update_style(self):
self.setStyleSheet("QPushButton{background-color:rgba(224,64,251,0.7);color:white;border:1px solid rgba(255,255,255,0.2);border-radius:8px;padding:8px 16px;} QPushButton:hover{background-color:rgba(224,64,251,0.9);} QPushButton:pressed{background-color:rgba(200,50,220,1);} QPushButton:disabled{background-color:rgba(85,85,85,0.5);color:#888;}")
def retranslate(self): self.setText(tr(self.text_key))
class InfoWin(QDialog):
def __init__(self, parent=None):
super().__init__(parent); self.setFixedSize(500, 550); self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Dialog)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground); self.setWindowModality(Qt.WindowModality.ApplicationModal)
bg = MainBackgroundWidget(); l = QVBoxLayout(self); l.setContentsMargins(0,0,0,0); l.addWidget(bg)
c = QVBoxLayout(bg); c.setContentsMargins(20, 20, 20, 20); c.setSpacing(15)
self.t = QLabel(); self.t.setFont(QFont("Segoe UI", 16, QFont.Weight.Bold)); self.t.setStyleSheet("color:white;"); self.t.setGraphicsEffect(GlowEffect(blur=20))
self.b = QTextBrowser(); self.b.setOpenExternalLinks(True); self.b.setStyleSheet("background:transparent;color:#DDDDDD;border:none;font-size:11pt;")
self.ok = StyledButton('btn_ok'); self.ok.clicked.connect(self.accept)
c.addWidget(self.t, alignment=Qt.AlignmentFlag.AlignCenter); c.addWidget(self.b); c.addWidget(self.ok, alignment=Qt.AlignmentFlag.AlignCenter)
self.re(); self._p = None
def re(self): self.t.setText(tr('info_title')); self.b.setHtml(tr('readme_text')); self.ok.retranslate()
def mousePressEvent(self, e):
if e.button() == Qt.MouseButton.LeftButton: self._p = e.globalPosition().toPoint()
def mouseMoveEvent(self, e):
if self._p: d = e.globalPosition().toPoint() - self._p; self.move(self.x() + d.x(), self.y() + d.y()); self._p = e.globalPosition().toPoint()
class MsgBox(QDialog):
def __init__(self, title_key, message, parent=None):
super().__init__(parent); self.setFixedSize(450, 220); self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Dialog)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground); self.setWindowModality(Qt.WindowModality.ApplicationModal)
bg = MainBackgroundWidget(); l = QVBoxLayout(self); l.setContentsMargins(0,0,0,0); l.addWidget(bg)
c = QVBoxLayout(bg); c.setSpacing(15); c.setAlignment(Qt.AlignmentFlag.AlignCenter)
t = QLabel(tr(title_key)); t.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold))
t.setStyleSheet("color:white;"); t.setGraphicsEffect(GlowEffect(blur=20))
t.setAlignment(Qt.AlignmentFlag.AlignCenter)
ml = QLabel(message); ml.setFont(QFont("Segoe UI", 11)); ml.setWordWrap(True)
ml.setAlignment(Qt.AlignmentFlag.AlignCenter); ml.setStyleSheet("color:#DDDDDD;")
b = StyledButton('btn_ok'); b.setMinimumWidth(140); b.clicked.connect(self.accept)
c.addWidget(t); c.addWidget(ml); c.addWidget(b); self._p = None
def mousePressEvent(self, e):
if e.button() == Qt.MouseButton.LeftButton: self._p = e.globalPosition().toPoint()
def mouseMoveEvent(self, e):
if self._p: d = e.globalPosition().toPoint() - self._p; self.move(self.x() + d.x(), self.y() + d.y()); self._p = e.globalPosition().toPoint()
# --- ПОИСК И ЛОГИКА ---
class SearchThread(QThread):
fin = pyqtSignal(str)
def run(self):
paths = [os.path.join(os.environ.get("ProgramFiles(x86)", "C:/Program Files (x86)"), "Steam/steamapps/common"), os.path.join(os.environ.get("ProgramFiles", "C:/Program Files"), "Steam/steamapps/common")]
for d in "CDEFGHIJKLMONPQRSTUVWXYZ":
p = f"{d}:\\SteamLibrary\\steamapps\\common"
if os.path.isdir(p): paths.append(p)
found = ""
for p in paths:
target = os.path.join(p, "Deadlock")
if os.path.isdir(target): found = target; break
self.fin.emit(found)
class MainWin(QMainWindow):
def __init__(self):
super().__init__(); self.gp = None; self.fp = None; self.setFixedSize(800, 600)
self.setWindowFlags(Qt.WindowType.FramelessWindowHint); self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.ui(); self.start_s(); self._p = None
def lang_toggle(self):
global CURRENT_LANGUAGE; CURRENT_LANGUAGE = 'ru' if CURRENT_LANGUAGE == 'en' else 'en'
self.re(); self.info_d.re()
def ui(self):
self.central = MainBackgroundWidget(); self.setCentralWidget(self.central)
root = QVBoxLayout(self.central); root.setContentsMargins(10, 10, 10, 20)
# Top Bar
top = QHBoxLayout(); self.info_d = InfoWin(self)
self.bl = QPushButton("RU/EN"); self.bl.setFixedSize(55, 30); self.bl.setCursor(Qt.CursorShape.PointingHandCursor)
self.bl.setStyleSheet("QPushButton{background:transparent;color:#888;border:2px solid #888;border-radius:15px;} QPushButton:hover{color:white;border-color:#E040FB;}")
self.bl.clicked.connect(self.lang_toggle)
bi = QPushButton("i"); bi.setFixedSize(30, 30); bi.setStyleSheet(self.bl.styleSheet()); bi.setCursor(Qt.CursorShape.PointingHandCursor)
bi.clicked.connect(self.info_d.exec)
bx = QPushButton("✕"); bx.setFixedSize(30, 30); bx.setCursor(Qt.CursorShape.PointingHandCursor)
bx.setStyleSheet("QPushButton{background:transparent;color:#888;border:none;} QPushButton:hover{background:red;color:white;border-radius:15px;}")
bx.clicked.connect(self.close)
top.addWidget(self.bl); top.addWidget(bi); top.addStretch(); top.addWidget(bx); root.addLayout(top)
# Center Area
self.ma = QVBoxLayout(); self.ma.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.ll = QLabel(); self.ll.setFont(QFont("Segoe UI", 32, QFont.Weight.Bold)); self.ll.setGraphicsEffect(GlowEffect())
self.ma.addWidget(self.ll, alignment=Qt.AlignmentFlag.AlignCenter)
self.cont = QWidget(); self.cont.hide(); cl = QVBoxLayout(self.cont); cl.setAlignment(Qt.AlignmentFlag.AlignCenter); cl.setSpacing(10)
self.t = QLabel(); self.t.setFont(QFont("Segoe UI", 75, QFont.Weight.Bold)); self.t.setStyleSheet("color:white;"); self.t.setGraphicsEffect(GlowEffect())
self.st = QLabel(); self.st.setFont(QFont("Segoe UI", 28, QFont.Weight.Light)); self.st.setStyleSheet("color:#aaa;margin-top:-10px;")
self.stat = QLabel(); self.stat.setWordWrap(True); self.stat.setStyleSheet("color:#888;")
self.bp = StyledButton('btn_select_path'); self.bp.clicked.connect(self.sel_p)
self.bf = StyledButton('btn_select_font'); self.bf.clicked.connect(self.sel_f); self.bf.setEnabled(False)
self.fil = QLabel(); self.fil.setStyleSheet("color:#aaa;")
self.ba = StyledButton('btn_apply'); self.ba.clicked.connect(self.apply_f); self.ba.setEnabled(False)
for w in [self.t, self.st, self.stat, self.bp, self.bf, self.ba, self.fil]: cl.addWidget(w, alignment=Qt.AlignmentFlag.AlignCenter)
self.ma.addWidget(self.cont); root.addLayout(self.ma); root.addStretch()
# Social
soc = QHBoxLayout(); soc.setSpacing(20); soc.setAlignment(Qt.AlignmentFlag.AlignCenter)
def open_u(u): return lambda: webbrowser.open(u)
links = {"discord.png": "https://discord.gg/PA3R9WKDw3", "youtube.png": "https://www.youtube.com/@dysonpeek", "gamebanana.png": "https://gamebanana.com/mods/616141"}
for img, url in links.items():
# ИСПОЛЬЗУЕМ resource_path для иконок соцсетей
img_path = resource_path(img)
if os.path.exists(img_path):
b = QPushButton(); b.setIcon(QIcon(img_path)); b.setIconSize(QSize(32,32)); b.setFixedSize(40,40); b.setCursor(Qt.CursorShape.PointingHandCursor)
b.setStyleSheet("background:transparent;border:none;"); b.clicked.connect(open_u(url)); soc.addWidget(b)
root.addLayout(soc); self.re()
def re(self):
self.setWindowTitle(tr('app_title')); self.setWindowIcon(QIcon(resource_path("icon.png")))
self.ll.setText(tr('loading_text')); self.t.setText(tr('main_title')); self.st.setText(tr('main_subtitle'))
self.bp.retranslate(); self.bf.retranslate(); self.ba.retranslate(); self.upd()
def upd(self):
self.stat.setText(tr('status_found', path=self.gp) if self.gp else tr('status_not_found'))
self.fil.setText(tr('font_selected', font_name=os.path.basename(self.fp)) if self.fp else tr('font_not_selected'))
def start_s(self):
self.s = SearchThread(); self.s.fin.connect(self.done); self.s.start()
def done(self, p):
self.ll.hide(); self.cont.show()
if p: self.gp = p; self.bp.hide(); self.bf.setEnabled(True)
self.upd()
def sel_p(self):
d = QFileDialog(self); d.setWindowTitle(tr('dialog_select_folder')); d.setFileMode(QFileDialog.FileMode.Directory)
if d.exec():
p = d.selectedFiles()[0]
if os.path.isdir(os.path.join(p, "game/citadel/panorama/fonts")): self.gp = p; self.bp.hide(); self.bf.setEnabled(True); self.upd()
else: MsgBox('msg_error_title', tr('msg_wrong_folder'), self).exec()
def sel_f(self):
d = QFileDialog(self); d.setWindowTitle(tr('dialog_select_font')); d.setNameFilter("Fonts (*.ttf *.otf)"); d.setFileMode(QFileDialog.FileMode.ExistingFile)
if d.exec(): self.fp = d.selectedFiles()[0]; self.ba.setEnabled(True); self.upd()
def apply_f(self):
try:
dest = os.path.join(self.gp, "game/citadel/panorama/fonts"); shutil.copy2(self.fp, dest)
c = os.path.join(dest, "fonts.conf")
if os.path.exists(c) and not os.path.exists(c+".backup"): shutil.copy2(c, c+".backup")
from fontTools.ttLib import TTFont
f = TTFont(self.fp); n = os.path.splitext(os.path.basename(self.fp))[0]
for r in f['name'].names:
if r.nameID == 4: n = r.string.decode('utf-16-be') if r.isUnicode() else r.string.decode('latin-1'); break
with open(c, 'w', encoding='utf-8') as fl:
fl.write(FONTS_CONF_TEMPLATE.replace('FONTNAME', n).replace('FONTFILENAME', os.path.basename(self.fp)))
MsgBox('msg_success_title', tr('msg_success_applied'), self).exec()
except Exception as e: MsgBox('msg_error_title', str(e), self).exec()
def mousePressEvent(self, e):
if e.button() == Qt.MouseButton.LeftButton: self._p = e.globalPosition().toPoint()
def mouseMoveEvent(self, e):
if self._p: d = e.globalPosition().toPoint() - self._p; self.move(self.x() + d.x(), self.y() + d.y()); self._p = e.globalPosition().toPoint()
if __name__ == "__main__":
app = QApplication(sys.argv)
try:
sl = locale.getlocale()[0]
if sl and ('ru' in sl.lower() or 'russian' in sl.lower()): CURRENT_LANGUAGE = 'ru'
except: pass
w = MainWin(); w.show(); sys.exit(app.exec())
• Discord: https://discord.gg/PA3R9WKDw3
• YouTube: https://www.youtube.com/@dysonpeek
• GameBanana: https://gamebanana.com/mods/616141