Upload files to "/"
This commit is contained in:
823
fallout_shelter_qt_editor.py
Normal file
823
fallout_shelter_qt_editor.py
Normal file
@@ -0,0 +1,823 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fallout Shelter Save Editor - PyQt GUI
|
||||
A comprehensive PyQt-based tool for editing Fallout Shelter save files.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import base64
|
||||
import zlib
|
||||
import gzip
|
||||
import struct
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
import traceback
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QTabWidget, QTextEdit, QLabel, QLineEdit, QPushButton, QFileDialog,
|
||||
QMessageBox, QGroupBox, QGridLayout, QSpinBox, QDoubleSpinBox,
|
||||
QScrollArea, QFrame, QSplitter, QTreeWidget, QTreeWidgetItem,
|
||||
QHeaderView, QComboBox, QCheckBox, QProgressBar, QStatusBar,
|
||||
QMenuBar, QAction, QToolBar, QTableWidget, QTableWidgetItem
|
||||
)
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
|
||||
from PyQt5.QtGui import QFont, QIcon, QPixmap
|
||||
|
||||
|
||||
class SaveDecryptionWorker(QThread):
|
||||
"""Worker thread for save file decryption/analysis."""
|
||||
|
||||
progress_updated = pyqtSignal(int)
|
||||
status_updated = pyqtSignal(str)
|
||||
finished_signal = pyqtSignal(dict)
|
||||
error_signal = pyqtSignal(str)
|
||||
|
||||
def __init__(self, filepath: str):
|
||||
super().__init__()
|
||||
self.filepath = filepath
|
||||
|
||||
def run(self):
|
||||
"""Run the decryption process."""
|
||||
try:
|
||||
self.status_updated.emit("Loading save file...")
|
||||
self.progress_updated.emit(10)
|
||||
|
||||
with open(self.filepath, 'rb') as f:
|
||||
raw_data = f.read()
|
||||
|
||||
self.status_updated.emit("Decoding base64...")
|
||||
self.progress_updated.emit(20)
|
||||
|
||||
decoded_data = base64.b64decode(raw_data)
|
||||
|
||||
self.status_updated.emit("Attempting decompression...")
|
||||
self.progress_updated.emit(40)
|
||||
|
||||
# Try various decompression methods
|
||||
result = self.try_decompress(decoded_data)
|
||||
|
||||
if result:
|
||||
self.status_updated.emit("Save file loaded successfully!")
|
||||
self.progress_updated.emit(100)
|
||||
self.finished_signal.emit(result)
|
||||
else:
|
||||
self.status_updated.emit("Could not decrypt save file")
|
||||
self.error_signal.emit("Unable to decrypt or decompress save file")
|
||||
|
||||
except Exception as e:
|
||||
self.error_signal.emit(f"Error loading save: {str(e)}")
|
||||
|
||||
def try_decompress(self, data: bytes) -> Optional[Dict]:
|
||||
"""Try various decompression methods."""
|
||||
methods = [
|
||||
("Raw JSON", self.try_raw_json),
|
||||
("Zlib", self.try_zlib),
|
||||
("Gzip", self.try_gzip),
|
||||
("Custom Format", self.try_custom_format),
|
||||
]
|
||||
|
||||
for i, (name, method) in enumerate(methods):
|
||||
self.status_updated.emit(f"Trying {name}...")
|
||||
self.progress_updated.emit(40 + (i * 15))
|
||||
|
||||
try:
|
||||
result = method(data)
|
||||
if result:
|
||||
return {
|
||||
"method": name,
|
||||
"data": result,
|
||||
"raw_size": len(data)
|
||||
}
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
def try_raw_json(self, data: bytes) -> Optional[Dict]:
|
||||
"""Try parsing as raw JSON."""
|
||||
text = data.decode('utf-8')
|
||||
return json.loads(text)
|
||||
|
||||
def try_zlib(self, data: bytes) -> Optional[Dict]:
|
||||
"""Try zlib decompression."""
|
||||
decompressed = zlib.decompress(data)
|
||||
text = decompressed.decode('utf-8')
|
||||
return json.loads(text)
|
||||
|
||||
def try_gzip(self, data: bytes) -> Optional[Dict]:
|
||||
"""Try gzip decompression."""
|
||||
decompressed = gzip.decompress(data)
|
||||
text = decompressed.decode('utf-8')
|
||||
return json.loads(text)
|
||||
|
||||
def try_custom_format(self, data: bytes) -> Optional[Dict]:
|
||||
"""Try custom Fallout Shelter format."""
|
||||
# Try with header
|
||||
if len(data) > 4:
|
||||
header = struct.unpack('<I', data[:4])[0]
|
||||
if 4 < header < len(data):
|
||||
compressed_data = data[4:]
|
||||
decompressed = zlib.decompress(compressed_data)
|
||||
text = decompressed.decode('utf-8')
|
||||
return json.loads(text)
|
||||
return None
|
||||
|
||||
|
||||
class FalloutShelterSaveEditor(QMainWindow):
|
||||
"""Main application window."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.save_data = None
|
||||
self.save_filepath = None
|
||||
self.save_method = None
|
||||
self.is_modified = False
|
||||
|
||||
self.init_ui()
|
||||
self.setup_connections()
|
||||
|
||||
def init_ui(self):
|
||||
"""Initialize the user interface."""
|
||||
self.setWindowTitle("Fallout Shelter Save Editor")
|
||||
self.setGeometry(100, 100, 1200, 800)
|
||||
|
||||
# Create menu bar
|
||||
self.create_menu_bar()
|
||||
|
||||
# Create toolbar
|
||||
self.create_toolbar()
|
||||
|
||||
# Create status bar
|
||||
self.status_bar = QStatusBar()
|
||||
self.setStatusBar(self.status_bar)
|
||||
|
||||
# Progress bar for status bar
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setVisible(False)
|
||||
self.status_bar.addPermanentWidget(self.progress_bar)
|
||||
|
||||
# Central widget
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
# Main layout
|
||||
main_layout = QVBoxLayout(central_widget)
|
||||
|
||||
# File info section
|
||||
self.create_file_info_section(main_layout)
|
||||
|
||||
# Main content area with tabs
|
||||
self.create_main_content(main_layout)
|
||||
|
||||
# Update UI state
|
||||
self.update_ui_state()
|
||||
|
||||
def create_menu_bar(self):
|
||||
"""Create the menu bar."""
|
||||
menubar = self.menuBar()
|
||||
|
||||
# File menu
|
||||
file_menu = menubar.addMenu('File')
|
||||
|
||||
open_action = QAction('Open Save File', self)
|
||||
open_action.setShortcut('Ctrl+O')
|
||||
open_action.triggered.connect(self.open_save_file)
|
||||
file_menu.addAction(open_action)
|
||||
|
||||
save_action = QAction('Save', self)
|
||||
save_action.setShortcut('Ctrl+S')
|
||||
save_action.triggered.connect(self.save_file)
|
||||
file_menu.addAction(save_action)
|
||||
|
||||
save_as_action = QAction('Save As...', self)
|
||||
save_as_action.setShortcut('Ctrl+Shift+S')
|
||||
save_as_action.triggered.connect(self.save_file_as)
|
||||
file_menu.addAction(save_as_action)
|
||||
|
||||
file_menu.addSeparator()
|
||||
|
||||
backup_action = QAction('Create Backup', self)
|
||||
backup_action.triggered.connect(self.create_backup)
|
||||
file_menu.addAction(backup_action)
|
||||
|
||||
file_menu.addSeparator()
|
||||
|
||||
exit_action = QAction('Exit', self)
|
||||
exit_action.setShortcut('Ctrl+Q')
|
||||
exit_action.triggered.connect(self.close)
|
||||
file_menu.addAction(exit_action)
|
||||
|
||||
# Edit menu
|
||||
edit_menu = menubar.addMenu('Edit')
|
||||
|
||||
refresh_action = QAction('Refresh Data', self)
|
||||
refresh_action.setShortcut('F5')
|
||||
refresh_action.triggered.connect(self.refresh_data)
|
||||
edit_menu.addAction(refresh_action)
|
||||
|
||||
# Help menu
|
||||
help_menu = menubar.addMenu('Help')
|
||||
|
||||
about_action = QAction('About', self)
|
||||
about_action.triggered.connect(self.show_about)
|
||||
help_menu.addAction(about_action)
|
||||
|
||||
def create_toolbar(self):
|
||||
"""Create the toolbar."""
|
||||
toolbar = QToolBar()
|
||||
self.addToolBar(toolbar)
|
||||
|
||||
# Open button
|
||||
open_btn = QPushButton("Open Save")
|
||||
open_btn.clicked.connect(self.open_save_file)
|
||||
toolbar.addWidget(open_btn)
|
||||
|
||||
# Save button
|
||||
self.save_btn = QPushButton("Save")
|
||||
self.save_btn.clicked.connect(self.save_file)
|
||||
self.save_btn.setEnabled(False)
|
||||
toolbar.addWidget(self.save_btn)
|
||||
|
||||
# Backup button
|
||||
self.backup_btn = QPushButton("Backup")
|
||||
self.backup_btn.clicked.connect(self.create_backup)
|
||||
self.backup_btn.setEnabled(False)
|
||||
toolbar.addWidget(self.backup_btn)
|
||||
|
||||
def create_file_info_section(self, parent_layout):
|
||||
"""Create the file information section."""
|
||||
info_group = QGroupBox("Save File Information")
|
||||
info_layout = QGridLayout(info_group)
|
||||
|
||||
# File path
|
||||
info_layout.addWidget(QLabel("File:"), 0, 0)
|
||||
self.file_path_label = QLabel("No file loaded")
|
||||
self.file_path_label.setStyleSheet("font-weight: bold;")
|
||||
info_layout.addWidget(self.file_path_label, 0, 1)
|
||||
|
||||
# File size
|
||||
info_layout.addWidget(QLabel("Size:"), 1, 0)
|
||||
self.file_size_label = QLabel("-")
|
||||
info_layout.addWidget(self.file_size_label, 1, 1)
|
||||
|
||||
# Decryption method
|
||||
info_layout.addWidget(QLabel("Method:"), 2, 0)
|
||||
self.method_label = QLabel("-")
|
||||
info_layout.addWidget(self.method_label, 2, 1)
|
||||
|
||||
# Modified status
|
||||
info_layout.addWidget(QLabel("Status:"), 3, 0)
|
||||
self.status_label = QLabel("Not modified")
|
||||
info_layout.addWidget(self.status_label, 3, 1)
|
||||
|
||||
parent_layout.addWidget(info_group)
|
||||
|
||||
def create_main_content(self, parent_layout):
|
||||
"""Create the main content area with tabs."""
|
||||
self.tab_widget = QTabWidget()
|
||||
|
||||
# Raw Data tab
|
||||
self.create_raw_data_tab()
|
||||
|
||||
# Vault Info tab
|
||||
self.create_vault_info_tab()
|
||||
|
||||
# Resources tab
|
||||
self.create_resources_tab()
|
||||
|
||||
# Dwellers tab
|
||||
self.create_dwellers_tab()
|
||||
|
||||
# Rooms tab
|
||||
self.create_rooms_tab()
|
||||
|
||||
parent_layout.addWidget(self.tab_widget)
|
||||
|
||||
def create_raw_data_tab(self):
|
||||
"""Create the raw data viewing/editing tab."""
|
||||
raw_widget = QWidget()
|
||||
layout = QVBoxLayout(raw_widget)
|
||||
|
||||
# JSON editor
|
||||
self.json_editor = QTextEdit()
|
||||
self.json_editor.setFont(QFont("Consolas", 10))
|
||||
self.json_editor.textChanged.connect(self.on_data_modified)
|
||||
layout.addWidget(self.json_editor)
|
||||
|
||||
# Format button
|
||||
format_btn = QPushButton("Format JSON")
|
||||
format_btn.clicked.connect(self.format_json)
|
||||
layout.addWidget(format_btn)
|
||||
|
||||
self.tab_widget.addTab(raw_widget, "Raw Data")
|
||||
|
||||
def create_vault_info_tab(self):
|
||||
"""Create the vault information tab."""
|
||||
vault_widget = QWidget()
|
||||
layout = QVBoxLayout(vault_widget)
|
||||
|
||||
# Scroll area for vault info
|
||||
scroll = QScrollArea()
|
||||
scroll_widget = QWidget()
|
||||
scroll_layout = QGridLayout(scroll_widget)
|
||||
|
||||
# Vault basic info
|
||||
basic_group = QGroupBox("Basic Information")
|
||||
basic_layout = QGridLayout(basic_group)
|
||||
|
||||
# Vault name
|
||||
basic_layout.addWidget(QLabel("Vault Name:"), 0, 0)
|
||||
self.vault_name_edit = QLineEdit()
|
||||
self.vault_name_edit.textChanged.connect(self.on_data_modified)
|
||||
basic_layout.addWidget(self.vault_name_edit, 0, 1)
|
||||
|
||||
# Vault number
|
||||
basic_layout.addWidget(QLabel("Vault Number:"), 1, 0)
|
||||
self.vault_number_spin = QSpinBox()
|
||||
self.vault_number_spin.setRange(1, 999)
|
||||
self.vault_number_spin.valueChanged.connect(self.on_data_modified)
|
||||
basic_layout.addWidget(self.vault_number_spin, 1, 1)
|
||||
|
||||
# Experience
|
||||
basic_layout.addWidget(QLabel("Experience:"), 2, 0)
|
||||
self.experience_spin = QSpinBox()
|
||||
self.experience_spin.setRange(0, 999999999)
|
||||
self.experience_spin.valueChanged.connect(self.on_data_modified)
|
||||
basic_layout.addWidget(self.experience_spin, 2, 1)
|
||||
|
||||
scroll_layout.addWidget(basic_group, 0, 0)
|
||||
|
||||
scroll.setWidget(scroll_widget)
|
||||
layout.addWidget(scroll)
|
||||
|
||||
self.tab_widget.addTab(vault_widget, "Vault Info")
|
||||
|
||||
def create_resources_tab(self):
|
||||
"""Create the resources editing tab."""
|
||||
resources_widget = QWidget()
|
||||
layout = QVBoxLayout(resources_widget)
|
||||
|
||||
# Resources group
|
||||
resources_group = QGroupBox("Vault Resources")
|
||||
resources_layout = QGridLayout(resources_group)
|
||||
|
||||
# Common resources
|
||||
self.resource_spins = {}
|
||||
resources = [
|
||||
("Caps", "caps", 0, 999999999),
|
||||
("Food", "food", 0, 999999),
|
||||
("Water", "water", 0, 999999),
|
||||
("Power", "power", 0, 999999),
|
||||
("Stimpaks", "stimpaks", 0, 999999),
|
||||
("RadAway", "radaway", 0, 999999),
|
||||
("Nuka Cola Quantum", "nuka_quantum", 0, 999999),
|
||||
]
|
||||
|
||||
for i, (display_name, key, min_val, max_val) in enumerate(resources):
|
||||
resources_layout.addWidget(QLabel(f"{display_name}:"), i, 0)
|
||||
|
||||
spin = QSpinBox()
|
||||
spin.setRange(min_val, max_val)
|
||||
spin.valueChanged.connect(self.on_data_modified)
|
||||
self.resource_spins[key] = spin
|
||||
resources_layout.addWidget(spin, i, 1)
|
||||
|
||||
layout.addWidget(resources_group)
|
||||
layout.addStretch()
|
||||
|
||||
self.tab_widget.addTab(resources_widget, "Resources")
|
||||
|
||||
def create_dwellers_tab(self):
|
||||
"""Create the dwellers management tab."""
|
||||
dwellers_widget = QWidget()
|
||||
layout = QVBoxLayout(dwellers_widget)
|
||||
|
||||
# Dwellers table
|
||||
self.dwellers_table = QTableWidget()
|
||||
self.dwellers_table.setColumnCount(8)
|
||||
self.dwellers_table.setHorizontalHeaderLabels([
|
||||
"Name", "Level", "Health", "Happiness", "Strength", "Perception", "Endurance", "Charisma"
|
||||
])
|
||||
|
||||
# Make table stretch
|
||||
header = self.dwellers_table.horizontalHeader()
|
||||
header.setSectionResizeMode(QHeaderView.Stretch)
|
||||
|
||||
layout.addWidget(self.dwellers_table)
|
||||
|
||||
# Dwellers controls
|
||||
controls_layout = QHBoxLayout()
|
||||
|
||||
refresh_dwellers_btn = QPushButton("Refresh Dwellers")
|
||||
refresh_dwellers_btn.clicked.connect(self.refresh_dwellers)
|
||||
controls_layout.addWidget(refresh_dwellers_btn)
|
||||
|
||||
controls_layout.addStretch()
|
||||
layout.addLayout(controls_layout)
|
||||
|
||||
self.tab_widget.addTab(dwellers_widget, "Dwellers")
|
||||
|
||||
def create_rooms_tab(self):
|
||||
"""Create the rooms management tab."""
|
||||
rooms_widget = QWidget()
|
||||
layout = QVBoxLayout(rooms_widget)
|
||||
|
||||
# Rooms tree
|
||||
self.rooms_tree = QTreeWidget()
|
||||
self.rooms_tree.setHeaderLabels(["Room Type", "Level", "Position", "Status"])
|
||||
layout.addWidget(self.rooms_tree)
|
||||
|
||||
# Rooms controls
|
||||
controls_layout = QHBoxLayout()
|
||||
|
||||
refresh_rooms_btn = QPushButton("Refresh Rooms")
|
||||
refresh_rooms_btn.clicked.connect(self.refresh_rooms)
|
||||
controls_layout.addWidget(refresh_rooms_btn)
|
||||
|
||||
controls_layout.addStretch()
|
||||
layout.addLayout(controls_layout)
|
||||
|
||||
self.tab_widget.addTab(rooms_widget, "Rooms")
|
||||
|
||||
def setup_connections(self):
|
||||
"""Set up signal connections."""
|
||||
pass
|
||||
|
||||
def open_save_file(self):
|
||||
"""Open a save file."""
|
||||
filepath, _ = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"Open Fallout Shelter Save File",
|
||||
"",
|
||||
"Save Files (*.sav);;All Files (*)"
|
||||
)
|
||||
|
||||
if filepath:
|
||||
self.load_save_file(filepath)
|
||||
|
||||
def load_save_file(self, filepath: str):
|
||||
"""Load a save file using worker thread."""
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setValue(0)
|
||||
|
||||
# Create and start worker thread
|
||||
self.worker = SaveDecryptionWorker(filepath)
|
||||
self.worker.progress_updated.connect(self.progress_bar.setValue)
|
||||
self.worker.status_updated.connect(self.status_bar.showMessage)
|
||||
self.worker.finished_signal.connect(self.on_save_loaded)
|
||||
self.worker.error_signal.connect(self.on_load_error)
|
||||
self.worker.start()
|
||||
|
||||
def on_save_loaded(self, result: Dict):
|
||||
"""Handle successful save file loading."""
|
||||
self.save_data = result["data"]
|
||||
self.save_filepath = self.worker.filepath
|
||||
self.save_method = result["method"]
|
||||
self.is_modified = False
|
||||
|
||||
# Update UI
|
||||
self.update_file_info()
|
||||
self.populate_data()
|
||||
self.update_ui_state()
|
||||
|
||||
self.progress_bar.setVisible(False)
|
||||
self.status_bar.showMessage("Save file loaded successfully", 3000)
|
||||
|
||||
def on_load_error(self, error_msg: str):
|
||||
"""Handle save file loading error."""
|
||||
self.progress_bar.setVisible(False)
|
||||
self.status_bar.showMessage("Failed to load save file", 3000)
|
||||
|
||||
QMessageBox.critical(self, "Error", f"Failed to load save file:\n{error_msg}")
|
||||
|
||||
def update_file_info(self):
|
||||
"""Update the file information display."""
|
||||
if self.save_filepath:
|
||||
filename = os.path.basename(self.save_filepath)
|
||||
self.file_path_label.setText(filename)
|
||||
|
||||
file_size = os.path.getsize(self.save_filepath)
|
||||
self.file_size_label.setText(f"{file_size:,} bytes")
|
||||
|
||||
self.method_label.setText(self.save_method or "Unknown")
|
||||
|
||||
self.update_status_label()
|
||||
|
||||
def update_status_label(self):
|
||||
"""Update the modification status label."""
|
||||
if self.is_modified:
|
||||
self.status_label.setText("Modified")
|
||||
self.status_label.setStyleSheet("color: orange; font-weight: bold;")
|
||||
else:
|
||||
self.status_label.setText("Not modified")
|
||||
self.status_label.setStyleSheet("color: green;")
|
||||
|
||||
def populate_data(self):
|
||||
"""Populate all tabs with save data."""
|
||||
if not self.save_data:
|
||||
return
|
||||
|
||||
# Raw data tab
|
||||
json_text = json.dumps(self.save_data, indent=2)
|
||||
self.json_editor.setPlainText(json_text)
|
||||
|
||||
# Vault info tab
|
||||
self.populate_vault_info()
|
||||
|
||||
# Resources tab
|
||||
self.populate_resources()
|
||||
|
||||
# Dwellers tab
|
||||
self.refresh_dwellers()
|
||||
|
||||
# Rooms tab
|
||||
self.refresh_rooms()
|
||||
|
||||
def populate_vault_info(self):
|
||||
"""Populate vault information."""
|
||||
if not self.save_data:
|
||||
return
|
||||
|
||||
# Try to find vault info in common locations
|
||||
vault_info = self.save_data
|
||||
|
||||
# Vault name
|
||||
name_fields = ["vaultName", "name", "VaultName"]
|
||||
for field in name_fields:
|
||||
if field in vault_info:
|
||||
self.vault_name_edit.setText(str(vault_info[field]))
|
||||
break
|
||||
|
||||
# Vault number
|
||||
number_fields = ["vaultNumber", "number", "VaultNumber"]
|
||||
for field in number_fields:
|
||||
if field in vault_info:
|
||||
self.vault_number_spin.setValue(int(vault_info[field]))
|
||||
break
|
||||
|
||||
# Experience
|
||||
exp_fields = ["experience", "exp", "Experience"]
|
||||
for field in exp_fields:
|
||||
if field in vault_info:
|
||||
self.experience_spin.setValue(int(vault_info[field]))
|
||||
break
|
||||
|
||||
def populate_resources(self):
|
||||
"""Populate resource information."""
|
||||
if not self.save_data:
|
||||
return
|
||||
|
||||
# Try to find resources in common locations
|
||||
resources_data = self.save_data.get("resources", self.save_data)
|
||||
|
||||
# Map of UI keys to possible data keys
|
||||
resource_mappings = {
|
||||
"caps": ["caps", "money", "Caps"],
|
||||
"food": ["food", "Food"],
|
||||
"water": ["water", "Water"],
|
||||
"power": ["power", "electricity", "Power"],
|
||||
"stimpaks": ["stimpaks", "stimpak", "Stimpaks"],
|
||||
"radaway": ["radaway", "RadAway", "radAway"],
|
||||
"nuka_quantum": ["nuka_quantum", "quantum", "NukaQuantum"],
|
||||
}
|
||||
|
||||
for ui_key, data_keys in resource_mappings.items():
|
||||
if ui_key in self.resource_spins:
|
||||
for data_key in data_keys:
|
||||
if data_key in resources_data:
|
||||
self.resource_spins[ui_key].setValue(int(resources_data[data_key]))
|
||||
break
|
||||
|
||||
def refresh_dwellers(self):
|
||||
"""Refresh the dwellers table."""
|
||||
if not self.save_data:
|
||||
return
|
||||
|
||||
# Try to find dwellers data
|
||||
dwellers_data = self.save_data.get("dwellers", [])
|
||||
if not isinstance(dwellers_data, list):
|
||||
dwellers_data = []
|
||||
|
||||
self.dwellers_table.setRowCount(len(dwellers_data))
|
||||
|
||||
for i, dweller in enumerate(dwellers_data):
|
||||
if isinstance(dweller, dict):
|
||||
# Name
|
||||
name = dweller.get("name", f"Dweller {i+1}")
|
||||
self.dwellers_table.setItem(i, 0, QTableWidgetItem(str(name)))
|
||||
|
||||
# Level
|
||||
level = dweller.get("level", 1)
|
||||
self.dwellers_table.setItem(i, 1, QTableWidgetItem(str(level)))
|
||||
|
||||
# Health
|
||||
health = dweller.get("health", 100)
|
||||
self.dwellers_table.setItem(i, 2, QTableWidgetItem(str(health)))
|
||||
|
||||
# Happiness
|
||||
happiness = dweller.get("happiness", 100)
|
||||
self.dwellers_table.setItem(i, 3, QTableWidgetItem(str(happiness)))
|
||||
|
||||
# SPECIAL stats
|
||||
special = dweller.get("special", {})
|
||||
stats = ["strength", "perception", "endurance", "charisma"]
|
||||
for j, stat in enumerate(stats):
|
||||
value = special.get(stat, 1)
|
||||
self.dwellers_table.setItem(i, 4+j, QTableWidgetItem(str(value)))
|
||||
|
||||
def refresh_rooms(self):
|
||||
"""Refresh the rooms tree."""
|
||||
self.rooms_tree.clear()
|
||||
|
||||
if not self.save_data:
|
||||
return
|
||||
|
||||
# Try to find rooms data
|
||||
rooms_data = self.save_data.get("rooms", [])
|
||||
if not isinstance(rooms_data, list):
|
||||
rooms_data = []
|
||||
|
||||
for i, room in enumerate(rooms_data):
|
||||
if isinstance(room, dict):
|
||||
room_type = room.get("type", "Unknown")
|
||||
level = room.get("level", 1)
|
||||
position = f"({room.get('x', 0)}, {room.get('y', 0)})"
|
||||
status = room.get("status", "Active")
|
||||
|
||||
item = QTreeWidgetItem([room_type, str(level), position, status])
|
||||
self.rooms_tree.addTopLevelItem(item)
|
||||
|
||||
def on_data_modified(self):
|
||||
"""Handle data modification."""
|
||||
if not self.is_modified:
|
||||
self.is_modified = True
|
||||
self.update_status_label()
|
||||
self.update_ui_state()
|
||||
|
||||
def format_json(self):
|
||||
"""Format the JSON in the raw data tab."""
|
||||
try:
|
||||
text = self.json_editor.toPlainText()
|
||||
data = json.loads(text)
|
||||
formatted = json.dumps(data, indent=2)
|
||||
self.json_editor.setPlainText(formatted)
|
||||
except json.JSONDecodeError as e:
|
||||
QMessageBox.warning(self, "JSON Error", f"Invalid JSON: {e}")
|
||||
|
||||
def save_file(self):
|
||||
"""Save the current file."""
|
||||
if not self.save_filepath:
|
||||
self.save_file_as()
|
||||
return
|
||||
|
||||
if self.write_save_file(self.save_filepath):
|
||||
self.is_modified = False
|
||||
self.update_status_label()
|
||||
self.update_ui_state()
|
||||
self.status_bar.showMessage("File saved successfully", 3000)
|
||||
else:
|
||||
QMessageBox.critical(self, "Error", "Failed to save file")
|
||||
|
||||
def save_file_as(self):
|
||||
"""Save as a new file."""
|
||||
filepath, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"Save Fallout Shelter Save File",
|
||||
"",
|
||||
"Save Files (*.sav);;All Files (*)"
|
||||
)
|
||||
|
||||
if filepath:
|
||||
if self.write_save_file(filepath):
|
||||
self.save_filepath = filepath
|
||||
self.is_modified = False
|
||||
self.update_file_info()
|
||||
self.update_ui_state()
|
||||
self.status_bar.showMessage("File saved successfully", 3000)
|
||||
else:
|
||||
QMessageBox.critical(self, "Error", "Failed to save file")
|
||||
|
||||
def write_save_file(self, filepath: str) -> bool:
|
||||
"""Write the save data to file."""
|
||||
try:
|
||||
# Get current data from JSON editor
|
||||
json_text = self.json_editor.toPlainText()
|
||||
data = json.loads(json_text)
|
||||
|
||||
# Convert to JSON string
|
||||
json_string = json.dumps(data, separators=(',', ':'))
|
||||
|
||||
# Compress (try to match original method)
|
||||
if self.save_method == "Zlib":
|
||||
compressed = zlib.compress(json_string.encode('utf-8'))
|
||||
else:
|
||||
# Default to zlib
|
||||
compressed = zlib.compress(json_string.encode('utf-8'))
|
||||
|
||||
# Encode to base64
|
||||
encoded = base64.b64encode(compressed)
|
||||
|
||||
# Write to file
|
||||
with open(filepath, 'wb') as f:
|
||||
f.write(encoded)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error writing save file: {e}")
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def create_backup(self):
|
||||
"""Create a backup of the current save file."""
|
||||
if not self.save_filepath:
|
||||
QMessageBox.warning(self, "Warning", "No save file loaded")
|
||||
return
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_path = f"{self.save_filepath}.backup_{timestamp}"
|
||||
|
||||
try:
|
||||
shutil.copy2(self.save_filepath, backup_path)
|
||||
QMessageBox.information(self, "Success", f"Backup created:\n{os.path.basename(backup_path)}")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to create backup:\n{e}")
|
||||
|
||||
def refresh_data(self):
|
||||
"""Refresh all data displays."""
|
||||
self.populate_data()
|
||||
|
||||
def update_ui_state(self):
|
||||
"""Update UI element states based on current state."""
|
||||
has_save = self.save_data is not None
|
||||
|
||||
self.save_btn.setEnabled(has_save and self.is_modified)
|
||||
self.backup_btn.setEnabled(has_save)
|
||||
|
||||
# Update window title
|
||||
title = "Fallout Shelter Save Editor"
|
||||
if self.save_filepath:
|
||||
filename = os.path.basename(self.save_filepath)
|
||||
title += f" - {filename}"
|
||||
if self.is_modified:
|
||||
title += " *"
|
||||
self.setWindowTitle(title)
|
||||
|
||||
def show_about(self):
|
||||
"""Show about dialog."""
|
||||
QMessageBox.about(
|
||||
self,
|
||||
"About Fallout Shelter Save Editor",
|
||||
"Fallout Shelter Save Editor v1.0\n\n"
|
||||
"A PyQt-based tool for editing Fallout Shelter save files.\n\n"
|
||||
"Features:\n"
|
||||
"• Load and save encrypted save files\n"
|
||||
"• Edit vault information and resources\n"
|
||||
"• View and manage dwellers\n"
|
||||
"• Raw JSON editing\n"
|
||||
"• Automatic backups"
|
||||
)
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Handle window close event."""
|
||||
if self.is_modified:
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Unsaved Changes",
|
||||
"You have unsaved changes. Do you want to save before closing?",
|
||||
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Save:
|
||||
self.save_file()
|
||||
event.accept()
|
||||
elif reply == QMessageBox.Discard:
|
||||
event.accept()
|
||||
else:
|
||||
event.ignore()
|
||||
else:
|
||||
event.accept()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function."""
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName("Fallout Shelter Save Editor")
|
||||
app.setApplicationVersion("1.0")
|
||||
|
||||
# Set application style
|
||||
app.setStyle('Fusion')
|
||||
|
||||
# Create and show main window
|
||||
window = FalloutShelterSaveEditor()
|
||||
window.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Reference in New Issue
Block a user