Thanks.
The Mod worked.
We have a Steam curator now. You should be following it. https://store.steampowered.com/curator/44994899-RPGHQ/
Drova Dediversified 1.2.2.1 — Drova - Forsaken Kin
Looking for the download?
This mod's files are on ModHQ.
Go to mod page
Moderator: Mod Janitor
Recent Patch 1.2.4 of the game did not trigger a file validation for me and the mod did not even have to be re-installed for it to appear to be functioning with this version of the game. No mod update required.
Hey there,
the Game has been updated to Version 1.3.1. Its a major update.
Is the mod still up to date?
the Game has been updated to Version 1.3.1. Its a major update.
Is the mod still up to date?
Localization is outdated
Devfags, here's a Python script to do the localization file changes
Devfags, here's a Python script to do the localization file changes
Code: Select all
import sys
import os
import re
from pathlib import Path
from typing import Dict, List, Tuple
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget,
QPushButton, QLabel, QLineEdit, QTableWidget, QTableWidgetItem,
QFileDialog, QProgressBar, QTextEdit, QMessageBox, QHeaderView,
QDialog, QDialogButtonBox, QFormLayout, QGroupBox, QSplitter
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QDateTime
from PyQt6.QtGui import QFont, QIcon
class AddRuleDialog(QDialog):
"""Dialog for adding or editing replacement rules."""
def __init__(self, parent=None, rule=None):
super().__init__(parent)
self.setWindowTitle("Add Replacement Rule" if rule is None else "Edit Replacement Rule")
self.setModal(True)
self.resize(400, 150)
layout = QFormLayout()
self.find_edit = QLineEdit()
self.replace_edit = QLineEdit()
if rule:
self.find_edit.setText(rule[0])
self.replace_edit.setText(rule[1])
layout.addRow("Find:", self.find_edit)
layout.addRow("Replace with:", self.replace_edit)
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addRow(buttons)
self.setLayout(layout)
def get_rule(self) -> Tuple[str, str]:
return (self.find_edit.text().strip(), self.replace_edit.text().strip())
class FileProcessorThread(QThread):
"""Thread for processing files to avoid blocking the UI."""
progress_updated = pyqtSignal(int)
status_updated = pyqtSignal(str)
finished_processing = pyqtSignal(int, int) # files_processed, replacements_made
def __init__(self, folder_path: str, rules: Dict[str, str]):
super().__init__()
self.folder_path = folder_path
self.rules = rules
self.total_files = 0
self.files_processed = 0
self.total_replacements = 0
def run(self):
"""Process all .loc files in the folder and subfolders."""
try:
# Find all .loc files recursively
self.status_updated.emit(f"Searching for .loc files recursively in: {self.folder_path}")
loca_files = list(Path(self.folder_path).rglob("*.loc"))
self.total_files = len(loca_files)
if self.total_files == 0:
self.status_updated.emit("No .loc files found in the selected folder or its subdirectories.")
self.finished_processing.emit(0, 0)
return
self.status_updated.emit(f"Found {self.total_files} .loc files in folder and subdirectories")
for i, file_path in enumerate(loca_files):
self.status_updated.emit(f"Processing: {file_path.relative_to(Path(self.folder_path))}")
self.process_file(file_path)
self.files_processed += 1
progress = int((i + 1) / self.total_files * 100)
self.progress_updated.emit(progress)
self.status_updated.emit(f"Processed {self.files_processed}/{self.total_files} files")
self.finished_processing.emit(self.files_processed, self.total_replacements)
except Exception as e:
self.status_updated.emit(f"Error during processing: {str(e)}")
def process_file(self, file_path: Path):
"""Process a single .loc file."""
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
original_content = content
file_replacements = 0
# Apply all replacement rules
for find_text, replace_text in self.rules.items():
if find_text and replace_text: # Skip empty rules
new_content = content.replace(find_text, replace_text)
replacements_in_this_rule = content.count(find_text)
file_replacements += replacements_in_this_rule
content = new_content
# Write back only if changes were made
if content != original_content:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
self.total_replacements += file_replacements
except Exception as e:
self.status_updated.emit(f"Error processing {file_path.name}: {str(e)}")
class LocaReplacerGUI(QMainWindow):
"""Main GUI application for LOC file string replacement."""
def __init__(self):
super().__init__()
self.setWindowTitle("LOC File String Replacer")
self.setGeometry(100, 100, 800, 600)
# Initialize default replacement rules
self.default_rules = {
"Afrin": "Ansgar",
"Bardok": "Bardon",
"Carima": "Corliss",
"Caspara": "Caitir",
"Cengiz": "Conchobar",
"Ebru": "Eghan",
"Emko": "Emrys",
"Emre": "Enan",
"Ester": "Eolande",
"Farina": "Fennella",
"Ismar": "Inness",
"Junali": "Giorsail",
"Karmi": "Kelvyn",
"Kessia": "Kiora",
"Minira": "Mabina",
"Monko": "Maol",
"Ruhan": "Reghan",
"Tuz": "Trevet",
"Zoltan": "Ultan",
"Last One Standing": "Last Man Standing"
}
self.setup_ui()
self.load_default_rules()
def setup_ui(self):
"""Set up the user interface."""
central_widget = QWidget()
self.setCentralWidget(central_widget)
# Main layout
main_layout = QVBoxLayout(central_widget)
# Folder selection section
folder_group = QGroupBox("Root Folder (searches all subdirectories)")
folder_layout = QHBoxLayout(folder_group)
self.folder_path_edit = QLineEdit()
self.folder_path_edit.setPlaceholderText("Select folder to search for .loc files (includes all subdirectories)")
self.folder_path_edit.setReadOnly(True)
self.browse_button = QPushButton("Browse")
self.browse_button.clicked.connect(self.browse_folder)
folder_layout.addWidget(self.folder_path_edit)
folder_layout.addWidget(self.browse_button)
main_layout.addWidget(folder_group)
# Splitter for rules and log
splitter = QSplitter(Qt.Orientation.Vertical)
# Replacement rules section
rules_group = QGroupBox("Replacement Rules")
rules_layout = QVBoxLayout(rules_group)
# Rules table
self.rules_table = QTableWidget()
self.rules_table.setColumnCount(2)
self.rules_table.setHorizontalHeaderLabels(["Find", "Replace With"])
self.rules_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
self.rules_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
rules_layout.addWidget(self.rules_table)
# Rule management buttons
rule_buttons_layout = QHBoxLayout()
self.add_rule_button = QPushButton("Add Rule")
self.add_rule_button.clicked.connect(self.add_rule)
self.edit_rule_button = QPushButton("Edit Rule")
self.edit_rule_button.clicked.connect(self.edit_rule)
self.delete_rule_button = QPushButton("Delete Rule")
self.delete_rule_button.clicked.connect(self.delete_rule)
self.reset_rules_button = QPushButton("Reset to Defaults")
self.reset_rules_button.clicked.connect(self.reset_to_defaults)
rule_buttons_layout.addWidget(self.add_rule_button)
rule_buttons_layout.addWidget(self.edit_rule_button)
rule_buttons_layout.addWidget(self.delete_rule_button)
rule_buttons_layout.addWidget(self.reset_rules_button)
rule_buttons_layout.addStretch()
rules_layout.addLayout(rule_buttons_layout)
splitter.addWidget(rules_group)
# Status and log section
status_group = QGroupBox("Status")
status_layout = QVBoxLayout(status_group)
# Progress bar
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
status_layout.addWidget(self.progress_bar)
# Status log
self.status_log = QTextEdit()
self.status_log.setMaximumHeight(150)
self.status_log.setFont(QFont("Consolas", 9))
status_layout.addWidget(self.status_log)
splitter.addWidget(status_group)
splitter.setSizes([400, 200])
main_layout.addWidget(splitter)
# Process button
process_layout = QHBoxLayout()
process_layout.addStretch()
self.process_button = QPushButton("Process Files")
self.process_button.clicked.connect(self.process_files)
self.process_button.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
font-weight: bold;
padding: 10px;
border: none;
border-radius: 5px;
}
QPushButton:hover {
background-color: #45a049;
}
QPushButton:disabled {
background-color: #cccccc;
}
""")
process_layout.addWidget(self.process_button)
process_layout.addStretch()
main_layout.addLayout(process_layout)
# Add initial status message
self.log_message("Ready. Select a root folder - will search all subdirectories for .loc files.")
def load_default_rules(self):
"""Load the default replacement rules into the table."""
self.rules_table.setRowCount(len(self.default_rules))
for row, (find_text, replace_text) in enumerate(self.default_rules.items()):
self.rules_table.setItem(row, 0, QTableWidgetItem(find_text))
self.rules_table.setItem(row, 1, QTableWidgetItem(replace_text))
def browse_folder(self):
"""Open folder selection dialog."""
folder = QFileDialog.getExistingDirectory(self, "Select Root Folder (will search all subdirectories for .loc files)")
if folder:
self.folder_path_edit.setText(folder)
self.log_message(f"Selected folder: {folder} (will search recursively)")
def add_rule(self):
"""Add a new replacement rule."""
dialog = AddRuleDialog(self)
if dialog.exec() == QDialog.DialogCode.Accepted:
find_text, replace_text = dialog.get_rule()
if find_text and replace_text:
row = self.rules_table.rowCount()
self.rules_table.insertRow(row)
self.rules_table.setItem(row, 0, QTableWidgetItem(find_text))
self.rules_table.setItem(row, 1, QTableWidgetItem(replace_text))
self.log_message(f"Added rule: '{find_text}' → '{replace_text}'")
else:
QMessageBox.warning(self, "Invalid Rule", "Both 'Find' and 'Replace' fields must be filled.")
def edit_rule(self):
"""Edit the selected replacement rule."""
current_row = self.rules_table.currentRow()
if current_row >= 0:
find_item = self.rules_table.item(current_row, 0)
replace_item = self.rules_table.item(current_row, 1)
if find_item and replace_item:
current_rule = (find_item.text(), replace_item.text())
dialog = AddRuleDialog(self, current_rule)
if dialog.exec() == QDialog.DialogCode.Accepted:
find_text, replace_text = dialog.get_rule()
if find_text and replace_text:
find_item.setText(find_text)
replace_item.setText(replace_text)
self.log_message(f"Updated rule: '{find_text}' → '{replace_text}'")
else:
QMessageBox.warning(self, "Invalid Rule", "Both 'Find' and 'Replace' fields must be filled.")
else:
QMessageBox.information(self, "No Selection", "Please select a rule to edit.")
def delete_rule(self):
"""Delete the selected replacement rule."""
current_row = self.rules_table.currentRow()
if current_row >= 0:
find_item = self.rules_table.item(current_row, 0)
if find_item:
find_text = find_item.text()
reply = QMessageBox.question(
self, "Confirm Deletion",
f"Are you sure you want to delete the rule for '{find_text}'?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.rules_table.removeRow(current_row)
self.log_message(f"Deleted rule for: '{find_text}'")
else:
QMessageBox.information(self, "No Selection", "Please select a rule to delete.")
def reset_to_defaults(self):
"""Reset rules table to default values."""
reply = QMessageBox.question(
self, "Reset to Defaults",
"This will replace all current rules with the default set. Continue?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.rules_table.clearContents()
self.load_default_rules()
self.log_message("Reset to default replacement rules.")
def get_current_rules(self) -> Dict[str, str]:
"""Get current replacement rules from the table."""
rules = {}
for row in range(self.rules_table.rowCount()):
find_item = self.rules_table.item(row, 0)
replace_item = self.rules_table.item(row, 1)
if find_item and replace_item:
find_text = find_item.text().strip()
replace_text = replace_item.text().strip()
if find_text and replace_text:
rules[find_text] = replace_text
return rules
def process_files(self):
"""Start processing files with current rules."""
folder_path = self.folder_path_edit.text().strip()
if not folder_path:
QMessageBox.warning(self, "No Folder Selected", "Please select a folder containing .loc files.")
return
if not os.path.exists(folder_path):
QMessageBox.warning(self, "Invalid Path", "The selected folder does not exist.")
return
rules = self.get_current_rules()
if not rules:
QMessageBox.warning(self, "No Rules", "Please add at least one replacement rule.")
return
# Confirm processing
reply = QMessageBox.question(
self, "Confirm Processing",
f"This will process all .loc files in:\n{folder_path}\n\n"
f"Including all subdirectories recursively.\n"
f"With {len(rules)} replacement rules.\n\nContinue?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply != QMessageBox.StandardButton.Yes:
return
# Start processing in background thread
self.process_button.setEnabled(False)
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
self.processor_thread = FileProcessorThread(folder_path, rules)
self.processor_thread.progress_updated.connect(self.progress_bar.setValue)
self.processor_thread.status_updated.connect(self.log_message)
self.processor_thread.finished_processing.connect(self.on_processing_finished)
self.processor_thread.start()
def on_processing_finished(self, files_processed: int, replacements_made: int):
"""Handle completion of file processing."""
self.process_button.setEnabled(True)
self.progress_bar.setVisible(False)
self.log_message(f"Processing complete!")
self.log_message(f"Files processed: {files_processed}")
self.log_message(f"Total replacements made: {replacements_made}")
QMessageBox.information(
self, "Processing Complete",
f"Processing finished successfully!\n\n"
f"Files processed: {files_processed}\n"
f"Total replacements made: {replacements_made}"
)
def log_message(self, message: str):
"""Add a message to the status log."""
self.status_log.append(f"[{QDateTime.currentDateTime().toString('hh:mm:ss')}] {message}")
self.status_log.ensureCursorVisible()
def main():
"""Main function to run the application."""
app = QApplication(sys.argv)
app.setApplicationName("LOC File String Replacer")
# Set application style
app.setStyle('Fusion')
window = LocaReplacerGUI()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
Thank you a lot for this, this also fixes the body types to male/female?