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
Share things you've made or found for games here.

Moderator: Mod Janitor

Ignore Topic
User avatar
SCYTHE13
Posts: 31
Joined: Jan 23, '25

Geolocation

Post by SCYTHE13 »

Thanks.
The Mod worked.
User avatar
Dunadan
Posts: 242
Joined: May 8, '24

Geolocation

Post by Dunadan »

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.
User avatar
Ramgis
Posts: 6
Joined: Jan 8, '25

Geolocation

Post by Ramgis »

Hey there,

the Game has been updated to Version 1.3.1. Its a major update.
Is the mod still up to date?
User avatar
Saren
Posts: 42
Joined: May 13, '25

Geolocation

Post by Saren »

Localization is outdated

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()
User avatar
Evilangel
Posts: 11
Joined: Apr 23, '25

Geolocation

Post by Evilangel »

Thank you a lot for this, this also fixes the body types to male/female?