Added tooltips and experimental save file generator

- Added basic tooltips for most fields
- Added advanced tooltips for mod scaling parameters, which calculates example values for up to 4 players
- Added basic save file generator
  - This simply clones the vanilla save file into an identical one with the user-specified file ending. [EXPERIMENTAL]
This commit is contained in:
frarol96 2024-07-20 01:48:36 +00:00
parent 45c6703817
commit a99a30a71d

216
ERSCMU.py
View File

@ -1,5 +1,5 @@
# Define version number # Define version number
PROGRAM_VERSION = "1.7.0.3" PROGRAM_VERSION = "1.7.0.4"
# Define a persistent path for the configuration files in the AppData folder # Define a persistent path for the configuration files in the AppData folder
PERSISTENT_DIR = os.path.join(os.getenv('APPDATA'), 'ERSC Mod Updater') PERSISTENT_DIR = os.path.join(os.getenv('APPDATA'), 'ERSC Mod Updater')
@ -435,6 +435,60 @@ def open_settings_window():
except Exception as e: except Exception as e:
print(f"Error in save_settings: {e}") print(f"Error in save_settings: {e}")
def clone_save_file():
config = load_config()
save_ext = config["settings"]["SAVE"].get("save_file_extension", "co2")
if save_ext == "sl2":
QMessageBox.critical(settings_window, "Error", "Save file extension cannot be set to .sl2")
return
appdata_dir = os.path.join(os.getenv('APPDATA'), 'EldenRing')
if not os.path.exists(appdata_dir):
QMessageBox.critical(settings_window, "Error", "Elden Ring AppData folder not found.")
return
subfolders = [f.path for f in os.scandir(appdata_dir) if f.is_dir() and f.name.isdigit()]
if not subfolders:
QMessageBox.critical(settings_window, "Error", "No subfolders found in Elden Ring AppData folder.")
return
save_folder = subfolders[0]
sl2_file = None
co2_file = None
for file in os.listdir(save_folder):
if file.endswith(".sl2"):
sl2_file = os.path.join(save_folder, file)
co2_file = os.path.join(save_folder, file.replace(".sl2", f".{save_ext}"))
break
if sl2_file:
if os.path.exists(co2_file):
msg_box = QMessageBox()
msg_box.setIcon(QMessageBox.Warning)
msg_box.setWindowTitle("Warning")
msg_box.setText(f"The coop mod save file already exists.\n({co2_file})\nDo you want to overwrite it?\n\n!!! THIS MAY LEAD TO PROGRESSION LOSS !!!")
yes_button = msg_box.addButton("Yes", QMessageBox.YesRole)
no_button = msg_box.addButton("No", QMessageBox.NoRole)
yes_button.setStyleSheet("background-color: red; margin-left: 0px;")
no_button.setStyleSheet("background-color: green; margin-right: 0px;")
msg_box.exec_()
if msg_box.clickedButton() == no_button:
return
try:
shutil.copyfile(sl2_file, co2_file)
QMessageBox.information(settings_window, "Success", f"Save file cloned successfully to '{co2_file}'")
except Exception as e:
QMessageBox.critical(settings_window, "Error", f"Failed to clone save file: {e}")
else:
QMessageBox.critical(settings_window, "Error", "No .sl2 save file found in the folder.")
settings_grid = QtWidgets.QGridLayout() settings_grid = QtWidgets.QGridLayout()
row = 0 row = 0
@ -458,8 +512,15 @@ def open_settings_window():
reset_button = QPushButton("Reset") reset_button = QPushButton("Reset")
reset_button.clicked.connect(lambda cb=checkbox, dv=default_value: cb.setChecked(dv == '1')) reset_button.clicked.connect(lambda cb=checkbox, dv=default_value: cb.setChecked(dv == '1'))
settings_grid.addWidget(reset_button, row, 1, 1, 1) settings_grid.addWidget(reset_button, row, 1, 1, 1)
if key == 'allow_invaders':
checkbox.setToolTip(str("Allow hostile invaders to invade your party."))
elif key == 'death_debuffs':
checkbox.setToolTip(str("Grant Death Rot debuffs upon death.\nCan be cleared by resting at a grace."))
elif key == 'allow_summons':
checkbox.setToolTip(str("Allow players to use summons."))
elif key == 'overhead_player_display': elif key == 'overhead_player_display':
dropdown = QComboBox() dropdown = QComboBox()
dropdown.setToolTip(str("How other players should be displayed"))
options = vocabulary[section][key] options = vocabulary[section][key]
dropdown.addItems(options.values()) dropdown.addItems(options.values())
dropdown.setCurrentIndex(list(options.keys()).index(value)) dropdown.setCurrentIndex(list(options.keys()).index(value))
@ -471,18 +532,26 @@ def open_settings_window():
settings_grid.addWidget(reset_button, row, 2, 1, 1) settings_grid.addWidget(reset_button, row, 2, 1, 1)
elif section == 'SAVE': elif section == 'SAVE':
entry = QLineEdit(value) entry = QLineEdit(value)
entry.setToolTip(str("Select a save file extension you'd like the mod to use.\nDo not include a period.\nCannot be set to \"sl2\""))
settings_grid.addWidget(QLabel(display_name), row, 0, 1, 1) settings_grid.addWidget(QLabel(display_name), row, 0, 1, 1)
settings_grid.addWidget(entry, row, 1, 1, 1) settings_grid.addWidget(entry, row, 1, 1, 1)
settings[section][key] = entry settings[section][key] = entry
disclaimer = QLabel("(Do not use sl2)") disclaimer = QLabel("(Do not use \"sl2\")")
disclaimer.setFont(QFont("Arial", 8, QFont.StyleItalic)) disclaimer.setFont(QFont("Arial", 8, QFont.StyleItalic))
settings_grid.addWidget(disclaimer, row+1, 1, 1, 1, alignment=Qt.AlignHCenter) settings_grid.addWidget(disclaimer, row+1, 1, 1, 1, alignment=Qt.AlignHCenter)
reset_button = QPushButton("Reset") reset_button = QPushButton("Reset")
reset_button.clicked.connect(lambda e=entry, dv=default_value: e.setText(dv)) reset_button.clicked.connect(lambda e=entry, dv=default_value: e.setText(dv))
settings_grid.addWidget(reset_button, row, 2, 1, 1) settings_grid.addWidget(reset_button, row, 2, 1, 1)
row += 1 row += 1
# Add the clone save button below the save file extension entry
clone_button = QPushButton("Generate Mod Save File")
clone_button.setToolTip(str("Clone your vanilla save file into a coop save file"))
clone_button.clicked.connect(clone_save_file)
settings_grid.addWidget(clone_button, row+1, 0, 1, 3)
row += 1
elif section == 'LANGUAGE': elif section == 'LANGUAGE':
dropdown = QComboBox() dropdown = QComboBox()
dropdown.setToolTip(str("Select a Language Override for the mod"))
options = get_locale_options(config["mod_path"]) options = get_locale_options(config["mod_path"])
dropdown.addItems(options) dropdown.addItems(options)
dropdown.setCurrentText(value.capitalize() if value else "Default") dropdown.setCurrentText(value.capitalize() if value else "Default")
@ -500,11 +569,144 @@ def open_settings_window():
reset_button = QPushButton("Reset") reset_button = QPushButton("Reset")
reset_button.clicked.connect(lambda e=entry, dv=default_value: e.setText(dv)) reset_button.clicked.connect(lambda e=entry, dv=default_value: e.setText(dv))
settings_grid.addWidget(reset_button, row, 2, 1, 1) settings_grid.addWidget(reset_button, row, 2, 1, 1)
if key == 'enemy_health_scaling':
tt_example_1 = 346 # Vulgar Militiamen (Liurnia) Example
# Calculate HP for 2, 3, and 4 players
tt_example_2 = tt_example_1 * (1 + int(value) / 100)
tt_example_3 = tt_example_1 * (1 + 2 * int(value) / 100)
tt_example_4 = tt_example_1 * (1 + 3 * int(value) / 100)
# Format numbers with space as thousand separator and no decimals
tt_example_1_formatted = f"{int(tt_example_1):,}".replace(",", " ")
tt_example_2_formatted = f"{int(tt_example_2):,}".replace(",", " ")
tt_example_3_formatted = f"{int(tt_example_3):,}".replace(",", " ")
tt_example_4_formatted = f"{int(tt_example_4):,}".replace(",", " ")
tooltip_text = (
f"Percentage increase of enemy non-boss HP per player.\n\n"
f"Vulgar Militiamen (Liurnia) example with a {int(value)}% scaling:\n"
f"Enemy HP with 1 player: {tt_example_1_formatted}\n"
f"Enemy HP with 2 players: {tt_example_2_formatted}\n"
f"Enemy HP with 3 players: {tt_example_3_formatted}\n"
f"Enemy HP with 4 players: {tt_example_4_formatted}"
)
entry.setToolTip(str(tooltip_text))
elif key == 'enemy_damage_scaling':
tt_example_1 = 130 # Vulgar Militiamen (Liurnia) Example
# Calculate damage for 2, 3, and 4 players
tt_example_2 = tt_example_1 * (1 + int(value) / 100)
tt_example_3 = tt_example_1 * (1 + 2 * int(value) / 100)
tt_example_4 = tt_example_1 * (1 + 3 * int(value) / 100)
# Format numbers with space as thousand separator and no decimals
tt_example_1_formatted = f"{int(tt_example_1):,}".replace(",", " ")
tt_example_2_formatted = f"{int(tt_example_2):,}".replace(",", " ")
tt_example_3_formatted = f"{int(tt_example_3):,}".replace(",", " ")
tt_example_4_formatted = f"{int(tt_example_4):,}".replace(",", " ")
tooltip_text = (
f"Percentage increase of enemy non-boss damage potential per player.\n\n"
f"Vulgar Militiamen (Liurnia) example with a {int(value)}% scaling:\n"
f"Enemy Damage with 1 player: {tt_example_1_formatted}\n"
f"Enemy Damage with 2 players: {tt_example_2_formatted}\n"
f"Enemy Damage with 3 players: {tt_example_3_formatted}\n"
f"Enemy Damage with 4 players: {tt_example_4_formatted}"
)
entry.setToolTip(str(tooltip_text))
elif key == 'enemy_posture_scaling':
tt_example_1 = 30 # Vulgar Militiamen (Liurnia) Example
# Calculate Poise for 2, 3, and 4 players
tt_example_2 = tt_example_1 * (1 + int(value) / 100)
tt_example_3 = tt_example_1 * (1 + 2 * int(value) / 100)
tt_example_4 = tt_example_1 * (1 + 3 * int(value) / 100)
# Format numbers with space as thousand separator and no decimals
tt_example_1_formatted = f"{int(tt_example_1):,}".replace(",", " ")
tt_example_2_formatted = f"{int(tt_example_2):,}".replace(",", " ")
tt_example_3_formatted = f"{int(tt_example_3):,}".replace(",", " ")
tt_example_4_formatted = f"{int(tt_example_4):,}".replace(",", " ")
tooltip_text = (
f"Percentage increase of enemy non-boss Posture/Poise (stun resistance) per player.\n\n"
f"Vulgar Militiamen (Liurnia) example with a {int(value)}% scaling:\n"
f"Enemy Poise with 1 player: {tt_example_1_formatted}\n"
f"Enemy Poise with 2 players: {tt_example_2_formatted}\n"
f"Enemy Poise with 3 players: {tt_example_3_formatted}\n"
f"Enemy Poise with 4 players: {tt_example_4_formatted}"
)
entry.setToolTip(str(tooltip_text))
elif key == 'boss_health_scaling':
tt_example_1 = 6080 # Godrick The Grafted HP Example
# Calculate HP for 2, 3, and 4 players
tt_example_2 = tt_example_1 * (1 + int(value) / 100)
tt_example_3 = tt_example_1 * (1 + 2 * int(value) / 100)
tt_example_4 = tt_example_1 * (1 + 3 * int(value) / 100)
# Format numbers with space as thousand separator and no decimals
tt_example_1_formatted = f"{int(tt_example_1):,}".replace(",", " ")
tt_example_2_formatted = f"{int(tt_example_2):,}".replace(",", " ")
tt_example_3_formatted = f"{int(tt_example_3):,}".replace(",", " ")
tt_example_4_formatted = f"{int(tt_example_4):,}".replace(",", " ")
tooltip_text = (
f"Percentage increase of enemy boss HP per player.\n\n"
f"Godrick The Grafted example with a {int(value)}% scaling:\n"
f"Boss HP with 1 player: {tt_example_1_formatted}\n"
f"Boss HP with 2 players: {tt_example_2_formatted}\n"
f"Boss HP with 3 players: {tt_example_3_formatted}\n"
f"Boss HP with 4 players: {tt_example_4_formatted}"
)
entry.setToolTip(str(tooltip_text))
elif key == 'boss_damage_scaling':
tt_example_1 = 200 # Godrick The Grafted Damage Example
# Calculate Damage for 2, 3, and 4 players
tt_example_2 = tt_example_1 * (1 + int(value) / 100)
tt_example_3 = tt_example_1 * (1 + 2 * int(value) / 100)
tt_example_4 = tt_example_1 * (1 + 3 * int(value) / 100)
# Format numbers with space as thousand separator and no decimals
tt_example_1_formatted = f"{int(tt_example_1):,}".replace(",", " ")
tt_example_2_formatted = f"{int(tt_example_2):,}".replace(",", " ")
tt_example_3_formatted = f"{int(tt_example_3):,}".replace(",", " ")
tt_example_4_formatted = f"{int(tt_example_4):,}".replace(",", " ")
tooltip_text = (
f"Percentage increase of enemy boss damage potential per player.\n\n"
f"Godrick The Grafted example with a {int(value)}% scaling:\n"
f"Boss Example Damage with 1 player: {tt_example_1_formatted}\n"
f"Boss Example Damage with 2 players: {tt_example_2_formatted}\n"
f"Boss Example Damage with 3 players: {tt_example_3_formatted}\n"
f"Boss Example Damage with 4 players: {tt_example_4_formatted}"
)
entry.setToolTip(str(tooltip_text))
elif key == 'boss_posture_scaling':
tt_example_1 = 105 # Godrick The Grafted Poise Example
# Calculate Poise for 2, 3, and 4 players
tt_example_2 = tt_example_1 * (1 + int(value) / 100)
tt_example_3 = tt_example_1 * (1 + 2 * int(value) / 100)
tt_example_4 = tt_example_1 * (1 + 3 * int(value) / 100)
# Format numbers with space as thousand separator and no decimals
tt_example_1_formatted = f"{int(tt_example_1):,}".replace(",", " ")
tt_example_2_formatted = f"{int(tt_example_2):,}".replace(",", " ")
tt_example_3_formatted = f"{int(tt_example_3):,}".replace(",", " ")
tt_example_4_formatted = f"{int(tt_example_4):,}".replace(",", " ")
tooltip_text = (
f"Percentage increase of enemy boss Posture/Poise (stun resistance) per player.\n\n"
f"Godrick The Grafted example with a {int(value)}% scaling:\n"
f"Boss Poise with 1 player: {tt_example_1_formatted}\n"
f"Boss Poise with 2 players: {tt_example_2_formatted}\n"
f"Boss Poise with 3 players: {tt_example_3_formatted}\n"
f"Boss Poise with 4 players: {tt_example_4_formatted}"
)
entry.setToolTip(str(tooltip_text))
row += 1 row += 1
layout.addLayout(settings_grid) layout.addLayout(settings_grid)
save_button = QPushButton("Save Settings") save_button = QPushButton("Save Settings")
save_button.setToolTip(str("Save your settings"))
save_button.clicked.connect(save_settings) save_button.clicked.connect(save_settings)
layout.addWidget(save_button) layout.addWidget(save_button)
settings_window.setLayout(layout) settings_window.setLayout(layout)
@ -615,14 +817,17 @@ def create_gui():
menu_bar.addAction(settings_action) menu_bar.addAction(settings_action)
check_updates_action = QtWidgets.QAction("Check for Updates", main_window) check_updates_action = QtWidgets.QAction("Check for Updates", main_window)
check_updates_action.setToolTip(str("Manually check for new mod updates"))
check_updates_action.triggered.connect(check_for_updates) check_updates_action.triggered.connect(check_for_updates)
menu_bar.addAction(check_updates_action) menu_bar.addAction(check_updates_action)
info_action = QtWidgets.QAction("Info", main_window) info_action = QtWidgets.QAction("Info", main_window)
info_action.setToolTip(str("Check some basic application info"))
info_action.triggered.connect(show_info) info_action.triggered.connect(show_info)
menu_bar.addAction(info_action) menu_bar.addAction(info_action)
about_action = QtWidgets.QAction("About", main_window) about_action = QtWidgets.QAction("About", main_window)
about_action.setToolTip(str("Disclaimer and license"))
about_action.triggered.connect(show_about) about_action.triggered.connect(show_about)
menu_bar.addAction(about_action) menu_bar.addAction(about_action)
@ -631,13 +836,16 @@ def create_gui():
version_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) version_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
version_label.setStyleSheet("padding-right: 10px;") # Optional: add some padding to the right version_label.setStyleSheet("padding-right: 10px;") # Optional: add some padding to the right
menu_bar.setCornerWidget(version_label, Qt.TopRightCorner) menu_bar.setCornerWidget(version_label, Qt.TopRightCorner)
version_label.setToolTip(str("ERSCMU version.\nThis is the version of this program you're currently running."))
mod_path_label = QLabel("Mod Folder:") mod_path_label = QLabel("Mod Folder:")
mod_path_entry = QLineEdit() mod_path_entry = QLineEdit()
mod_path_entry.setFixedWidth(400) mod_path_entry.setFixedWidth(400)
browse_button = QPushButton("Browse") browse_button = QPushButton("Browse")
browse_button.setToolTip(str("Manually select the Seamless Coop mod folder"))
browse_button.clicked.connect(browse_folder) browse_button.clicked.connect(browse_folder)
auto_button = QPushButton("Auto") auto_button = QPushButton("Auto")
auto_button.setToolTip(str("Automatically discover the Seamless Coop mod folder"))
auto_button.clicked.connect(auto_discover_mod_path) auto_button.clicked.connect(auto_discover_mod_path)
config = load_config() config = load_config()
@ -650,10 +858,12 @@ def create_gui():
mod_path_layout.addWidget(auto_button) mod_path_layout.addWidget(auto_button)
check_updates_button = QPushButton("Check for Updates") check_updates_button = QPushButton("Check for Updates")
check_updates_button.setToolTip(str("Manually check for new mod updates"))
check_updates_button.clicked.connect(check_for_updates) check_updates_button.clicked.connect(check_for_updates)
password_label = QLabel("Session Password:") password_label = QLabel("Session Password:")
password_entry = QLineEdit() password_entry = QLineEdit()
password_entry.setToolTip(str("Define your session password.\nNote that all players must have the same password!"))
password_entry.setEchoMode(QLineEdit.Password) password_entry.setEchoMode(QLineEdit.Password)
toggle_button = QPushButton("Show") toggle_button = QPushButton("Show")
toggle_button.clicked.connect(toggle_password) toggle_button.clicked.connect(toggle_password)
@ -667,9 +877,11 @@ def create_gui():
password_layout.addWidget(save_password_button) password_layout.addWidget(save_password_button)
launch_button = QPushButton("Launch Seamless Coop") launch_button = QPushButton("Launch Seamless Coop")
launch_button.setToolTip(str("Start Elden Ring Seamless Coop"))
launch_button.clicked.connect(launch_mod) launch_button.clicked.connect(launch_mod)
info_text = QLabel() info_text = QLabel()
info_text.setToolTip(str("Seamless Coop version and install date"))
update_info_text() update_info_text()
main_layout.addLayout(mod_path_layout) main_layout.addLayout(mod_path_layout)