Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
933e900868 | |||
3356565593 | |||
5c5571047c | |||
37c35d104a | |||
b2d89e8a54 | |||
b9e9b56f10 | |||
2c8372c289 | |||
fe134a53fe | |||
7f6934db84 | |||
542250aa7e | |||
dd53b72cc9 | |||
5fd8a4e5e3 | |||
2e1f2ecd3f | |||
faedd43727 | |||
49364acafa | |||
a99a30a71d | |||
45c6703817 | |||
e530b83ea6 | |||
ab5fa5f4e3 | |||
03d4c53525 | |||
e2258e270c | |||
58228cf531 |
293
ERSCMU.py
293
ERSCMU.py
@ -1,23 +1,13 @@
|
|||||||
import os
|
|
||||||
import requests
|
|
||||||
import zipfile
|
|
||||||
import shutil
|
|
||||||
import json
|
|
||||||
from datetime import datetime
|
|
||||||
import configparser
|
|
||||||
from PyQt5 import QtWidgets, QtGui
|
|
||||||
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QCheckBox, QComboBox, QDialog, QMessageBox, QFileDialog
|
|
||||||
from PyQt5.QtGui import QFont
|
|
||||||
from PyQt5.QtCore import Qt
|
|
||||||
|
|
||||||
# Define version number
|
# Define version number
|
||||||
PROGRAM_VERSION = "1.7.0.1"
|
PROGRAM_VERSION = "1.7.0.6"
|
||||||
|
LAUNCHER_VERSION = "1.1.0.0"
|
||||||
|
|
||||||
# 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')
|
||||||
if not os.path.exists(PERSISTENT_DIR):
|
if not os.path.exists(PERSISTENT_DIR):
|
||||||
os.makedirs(PERSISTENT_DIR)
|
os.makedirs(PERSISTENT_DIR)
|
||||||
|
|
||||||
|
LOGO_PATH = os.path.join(PERSISTENT_DIR, 'logo.ico')
|
||||||
CONFIG_FILE = os.path.join(PERSISTENT_DIR, 'mod_updater_config.json')
|
CONFIG_FILE = os.path.join(PERSISTENT_DIR, 'mod_updater_config.json')
|
||||||
DEFAULT_MOD_PATH = r"C:\Program Files (x86)\Steam\steamapps\common\ELDEN RING\Game\SeamlessCoop"
|
DEFAULT_MOD_PATH = r"C:\Program Files (x86)\Steam\steamapps\common\ELDEN RING\Game\SeamlessCoop"
|
||||||
GITHUB_API_URL = 'https://api.github.com/repos/LukeYui/EldenRingSeamlessCoopRelease/releases/latest'
|
GITHUB_API_URL = 'https://api.github.com/repos/LukeYui/EldenRingSeamlessCoopRelease/releases/latest'
|
||||||
@ -47,6 +37,41 @@ DEFAULT_VALUES = {
|
|||||||
}
|
}
|
||||||
FIRST_RUN = 1
|
FIRST_RUN = 1
|
||||||
|
|
||||||
|
def check_logo():
|
||||||
|
LOGO_URL = 'https://git.rolfsvaag.no/frarol96/ERSCMU/raw/branch/main/logo.ico'
|
||||||
|
if not os.path.isfile(LOGO_PATH):
|
||||||
|
response = requests.get(LOGO_URL, stream=True)
|
||||||
|
if response.status_code == 200:
|
||||||
|
# Open the local file in write-binary mode
|
||||||
|
with open(LOGO_PATH, 'wb') as file:
|
||||||
|
# Write the content in chunks to the local file
|
||||||
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
|
file.write(chunk)
|
||||||
|
print(f"Logo downloaded successfully to {LOGO_PATH}")
|
||||||
|
else:
|
||||||
|
print(f"Failed to download logo.\nHTTP status code: {response.status_code}")
|
||||||
|
else:
|
||||||
|
print(f"Logo discovered locally at {LOGO_PATH}")
|
||||||
|
|
||||||
|
def get_changelog(num_entries=1):
|
||||||
|
url = 'https://git.rolfsvaag.no/frarol96/ERSCMU/raw/branch/main/changelog.md'
|
||||||
|
response = requests.get(url)
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise Exception(f"Failed to fetch changelog. HTTP status code: {response.status_code}")
|
||||||
|
|
||||||
|
changelog = response.text.split('\n- ')
|
||||||
|
latest_entries = changelog[:num_entries]
|
||||||
|
|
||||||
|
formatted_entries = []
|
||||||
|
for entry in latest_entries:
|
||||||
|
if entry.strip():
|
||||||
|
lines = entry.split('\n')
|
||||||
|
version = lines[0].strip()
|
||||||
|
changes = '\n'.join([f"- {line.strip()}" for line in lines[1:] if line.strip()])
|
||||||
|
formatted_entries.append(f"version {version}:\n{changes}")
|
||||||
|
|
||||||
|
return '\n\n'.join(formatted_entries)
|
||||||
|
|
||||||
def ensure_vocabulary(config):
|
def ensure_vocabulary(config):
|
||||||
if "vocabulary" not in config:
|
if "vocabulary" not in config:
|
||||||
config["vocabulary"] = {
|
config["vocabulary"] = {
|
||||||
@ -447,6 +472,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;")
|
||||||
|
no_button.setStyleSheet("background-color: green;")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
@ -455,7 +534,7 @@ def open_settings_window():
|
|||||||
continue
|
continue
|
||||||
section_label = QLabel(section)
|
section_label = QLabel(section)
|
||||||
section_label.setFont(QFont("Arial", 12, QFont.Bold))
|
section_label.setFont(QFont("Arial", 12, QFont.Bold))
|
||||||
settings_grid.addWidget(section_label, row, 0, 1, 2)
|
settings_grid.addWidget(section_label, row, 0, 1, 4, alignment=Qt.AlignHCenter)
|
||||||
row += 1
|
row += 1
|
||||||
settings[section] = {}
|
settings[section] = {}
|
||||||
for key, value in config_parser.items(section):
|
for key, value in config_parser.items(section):
|
||||||
@ -470,8 +549,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))
|
||||||
@ -483,17 +569,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, 2, 1, 1)
|
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, 3, 1, 1)
|
settings_grid.addWidget(reset_button, row, 2, 1, 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")
|
||||||
@ -511,11 +606,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)
|
||||||
@ -580,15 +808,15 @@ def show_info():
|
|||||||
last_updated = config.get('last_updated', 'Unknown')
|
last_updated = config.get('last_updated', 'Unknown')
|
||||||
launcher_path = os.path.join(os.path.dirname(config["mod_path"]), config['launcher_name'])
|
launcher_path = os.path.join(os.path.dirname(config["mod_path"]), config['launcher_name'])
|
||||||
wdir = PERSISTENT_DIR
|
wdir = PERSISTENT_DIR
|
||||||
QMessageBox.information(None, "Info", f"Current Version: {installed_version}\nLast Updated: {last_updated}\nLauncher Path: {launcher_path}\nWorking Directory: {wdir}\nERSCMU Version: {PROGRAM_VERSION}")
|
QMessageBox.information(None, "Info", f"Current Version: {installed_version}\nLast Updated: {last_updated}\nLauncher Path: {launcher_path}\nWorking Directory: {wdir}\nERSCMU Version: {PROGRAM_VERSION}\nLauncher Version: {LAUNCHER_VERSION}\n\nChangelog (last 5 versions):\n{get_changelog(5)}")
|
||||||
|
|
||||||
def show_about():
|
def show_about():
|
||||||
about_text = f"""
|
about_text = f"""
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2024 Franz Rolfsvaag
|
Copyright (c) 2024 FreemoX
|
||||||
|
|
||||||
"Elden Ring Seamless Coop Updater" is in no ways affiliated with the creators of Elden Ring or the creator of the Seamless Coop mod.
|
"Elden Ring Seamless Coop Mod Updater" is in no ways affiliated with the creators of Elden Ring or the creator of the Seamless Coop mod.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
@ -597,6 +825,7 @@ The above copyright notice and this permission notice shall be included in all c
|
|||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
ERSCMU Version: {PROGRAM_VERSION}
|
ERSCMU Version: {PROGRAM_VERSION}
|
||||||
|
Launcher Version: {LAUNCHER_VERSION}
|
||||||
"""
|
"""
|
||||||
QMessageBox.information(None, "About", about_text)
|
QMessageBox.information(None, "About", about_text)
|
||||||
|
|
||||||
@ -617,7 +846,8 @@ def create_gui():
|
|||||||
main_layout = QVBoxLayout(central_widget)
|
main_layout = QVBoxLayout(central_widget)
|
||||||
|
|
||||||
# Set application icon
|
# Set application icon
|
||||||
app.setWindowIcon(QtGui.QIcon(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logo.ico')))
|
#app.setWindowIcon(QtGui.QIcon(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logo.ico')))
|
||||||
|
app.setWindowIcon(QtGui.QIcon(LOGO_PATH))
|
||||||
|
|
||||||
menu_bar = main_window.menuBar()
|
menu_bar = main_window.menuBar()
|
||||||
|
|
||||||
@ -626,14 +856,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)
|
||||||
|
|
||||||
@ -642,13 +875,17 @@ 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_tooltip_text = f"ERSCMU version.\nThis is the version of this program you're currently running.\nLauncher Version: {LAUNCHER_VERSION}\n\nChangelog:\n{get_changelog()}"
|
||||||
|
version_label.setToolTip(str(version_label_tooltip_text))
|
||||||
|
|
||||||
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()
|
||||||
@ -661,10 +898,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)
|
||||||
@ -678,9 +917,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)
|
||||||
@ -699,11 +940,5 @@ def create_gui():
|
|||||||
app.exec_()
|
app.exec_()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
create_gui()
|
check_logo()
|
||||||
|
create_gui()
|
||||||
|
|
||||||
# TODO
|
|
||||||
# - Add auto-conversion of vanilla save
|
|
||||||
#
|
|
||||||
# Command to convert into executable
|
|
||||||
# python -m PyInstaller --onefile --noconsole --icon="C:\Users\fraro\ERSC Updater\logo.ico" --distpath "C:\Users\fraro\ERSC Updater\output" "C:\Users\fraro\ERSC Updater\ERSC Updater.py"
|
|
63
README.md
63
README.md
@ -1,2 +1,61 @@
|
|||||||
ERSCMU.py is experimental with advanced features.
|
# ![ERSCMU Logo](logo.ico) Elden Ring Seamless Coop Mod Updater (ERSCMU)
|
||||||
ERSCMU-tkinter.py is a stable legacy version.
|
|
||||||
|
ERSCMU is a tool designed to help users keep their Elden Ring Seamless Coop mod up-to-date automatically. Users can download the exe installer from the releases tab, which fetches updated code, so users don't have to update the application manually for every update.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Automatically checks for the latest version of the Elden Ring Seamless Coop mod.
|
||||||
|
- Downloads and installs updates automatically.
|
||||||
|
- Backs up the previous version before updating.
|
||||||
|
- Allows configuration of mod settings through a GUI.
|
||||||
|
- Auto-discovers the mod installation path.
|
||||||
|
- Saves settings persistently in the AppData folder.
|
||||||
|
- Allows for automatic Vanilla -> Mod save file conversion
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Download the exe installer from the [releases tab](https://git.rolfsvaag.no/frarol96/ERSCMU/releases).
|
||||||
|
2. Run the installer and follow the on-screen instructions.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. **Launching the Application**:
|
||||||
|
- After installation, launch the Elden Ring Seamless Coop Mod Updater (ERSCMU) from your desktop or start menu.
|
||||||
|
|
||||||
|
2. **Initial Setup**:
|
||||||
|
- On the first run, the application will check for updates automatically.
|
||||||
|
- If the mod path is not detected, you can browse to the SeamlessCoop folder manually or use the auto-discover feature.
|
||||||
|
|
||||||
|
3. **Checking for Updates**:
|
||||||
|
- Click on `Check for Updates` in the application menu or the main window to manually check for updates.
|
||||||
|
|
||||||
|
4. **Mod Configuration**:
|
||||||
|
- Open the settings window through the `Settings` menu to configure mod settings.
|
||||||
|
- Save your settings, and they will be applied the next time you launch the mod.
|
||||||
|
|
||||||
|
5. **Launching the Mod**:
|
||||||
|
- Click on the `Launch Seamless Coop` button to start the mod with the configured settings.
|
||||||
|
|
||||||
|
## Configuration Files
|
||||||
|
|
||||||
|
- Configuration files are stored in the AppData folder: `C:\Users\<Your Username>\AppData\Roaming\ERSC Mod Updater`
|
||||||
|
- The main configuration file is `mod_updater_config.json`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
```
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
Contributions are welcome! Please fork the repository and submit a pull request for any improvements or bug fixes.
|
||||||
|
This project is in no way affiliated with the creators of Elden Ring or the creator of the Seamless Coop mod.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
[ERSCMU.py](https://git.rolfsvaag.no/frarol96/ERSCMU/src/branch/main/ERSCMU.py) is experimental with advanced features.
|
||||||
|
[ERSCMU-tkinter.py](https://git.rolfsvaag.no/frarol96/ERSCMU/src/branch/main/legacy/ERSCMU-tkinter.py) is a stable legacy version.
|
19
changelog.md
Normal file
19
changelog.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
- 1.7.0.6
|
||||||
|
- Added changelog viewing functionality for ERSCMU
|
||||||
|
- 1.7.0.5
|
||||||
|
- Fixed issue where the application would not have the logo embedded
|
||||||
|
- 1.7.0.4
|
||||||
|
- 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/converter [EXPERIMENTAL]
|
||||||
|
- 1.7.0.3
|
||||||
|
- Minor UI tweaks and readability of settings window
|
||||||
|
- 1.7.0.2
|
||||||
|
- Finalized dynamic launcher compatibility
|
||||||
|
- 1.7.0.1
|
||||||
|
- Introduction of dynamic launcher
|
||||||
|
- 1.6.1.2
|
||||||
|
- Compatibility update for ERSC v1.7.9
|
||||||
|
- Added "Display player ping & level" as overlay option
|
||||||
|
- 1.6.1.1
|
||||||
|
- Initial Git Commit of PyQt5 version
|
53
dynamic_launcher.py
Normal file
53
dynamic_launcher.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import requests
|
||||||
|
import importlib.util
|
||||||
|
import zipfile
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
import configparser
|
||||||
|
from PyQt5 import QtWidgets, QtGui
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
|
QLabel, QLineEdit, QPushButton, QCheckBox, QComboBox, QDialog,
|
||||||
|
QMessageBox, QFileDialog
|
||||||
|
)
|
||||||
|
from PyQt5.QtGui import QFont
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
# Define repository details
|
||||||
|
file_url = 'https://git.rolfsvaag.no/frarol96/ERSCMU/raw/branch/main/ERSCMU.py'
|
||||||
|
cache_dir = os.path.join(os.getenv('APPDATA'), 'ERSC Mod Updater')
|
||||||
|
cache_file = os.path.join(cache_dir, 'ERSCMU.py')
|
||||||
|
|
||||||
|
# Ensure cache directory exists
|
||||||
|
if not os.path.exists(cache_dir):
|
||||||
|
os.makedirs(cache_dir)
|
||||||
|
|
||||||
|
# Function to fetch the latest file
|
||||||
|
def fetch_latest_file(url, cache_path):
|
||||||
|
try:
|
||||||
|
print("Fetching latest version of the script...")
|
||||||
|
response = requests.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
with open(cache_path, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
print("Latest version fetched and cached.")
|
||||||
|
except requests.RequestException as e:
|
||||||
|
print(f"Failed to fetch the latest version. Using cached version if available.\nError: {e}")
|
||||||
|
|
||||||
|
# Fetch the latest version of the file or use the cached version
|
||||||
|
fetch_latest_file(file_url, cache_file)
|
||||||
|
|
||||||
|
# Ensure the cached file exists
|
||||||
|
if not os.path.exists(cache_file):
|
||||||
|
print("No cached version available. Exiting.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Prepare the global context with necessary imports
|
||||||
|
global_context = globals().copy()
|
||||||
|
|
||||||
|
# Import and run the cached script
|
||||||
|
with open(cache_file, 'r') as file:
|
||||||
|
exec(file.read(), global_context)
|
Loading…
Reference in New Issue
Block a user