Compare commits

...

21 Commits

Author SHA1 Message Date
933e900868 Update changelog.md 2024-07-20 05:21:53 +00:00
3356565593 Added changelog viewing
Users can now see the latest changelog entry by hovering over the ERSCMU version text, or the 5 latest entries in the info section
2024-07-20 05:21:15 +00:00
5c5571047c Added launcher version text 2024-07-20 05:03:04 +00:00
37c35d104a Update ERSCMU.py 2024-07-20 04:58:31 +00:00
b2d89e8a54 Update ERSCMU.py 2024-07-20 04:57:48 +00:00
b9e9b56f10 Update changelog.md 2024-07-20 03:59:24 +00:00
2c8372c289 Ensure logo is included
Added a check to see if the logo is bundled with the program, and will download it if it isn't
2024-07-20 03:58:40 +00:00
fe134a53fe Update README.md 2024-07-20 03:39:22 +00:00
7f6934db84 Update README.md 2024-07-20 03:37:23 +00:00
542250aa7e Delete logo.png 2024-07-20 03:36:53 +00:00
dd53b72cc9 Upload files to "/" 2024-07-20 03:29:09 +00:00
5fd8a4e5e3 Update legacy/ERSCMU-tkinter.py 2024-07-20 03:18:17 +00:00
2e1f2ecd3f Add changelog.md 2024-07-20 03:16:59 +00:00
faedd43727 Added save file conversion info 2024-07-20 01:52:23 +00:00
49364acafa 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]
*Minor UI tweak on save file generator confirmation*
2024-07-20 01:51:04 +00:00
a99a30a71d 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]
2024-07-20 01:48:36 +00:00
45c6703817 Update README.md 2024-07-18 18:46:01 +00:00
e530b83ea6 Minor Tweaks
Improved readability of the settings window
2024-07-16 15:23:10 +00:00
ab5fa5f4e3 Upload files to "/" 2024-07-16 12:11:05 +00:00
03d4c53525 initial commit 2024-07-15 22:03:06 +00:00
e2258e270c Compatibility update with the new dynamic launcher 2024-07-15 21:52:56 +00:00
6 changed files with 397 additions and 19 deletions

281
ERSCMU.py
View File

@ -1,11 +1,13 @@
# 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'
@ -35,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"] = {
@ -435,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
@ -443,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):
@ -458,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))
@ -471,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")
@ -499,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)
@ -568,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:
@ -585,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)
@ -605,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()
@ -614,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)
@ -630,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()
@ -649,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)
@ -666,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)
@ -687,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"

View File

@ -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
View 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
View 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)

BIN
logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB