|
""" |
|
This script manipulates the SCUM database for the single player mode to |
|
increase the level of all skills and attributes to max (or the value of |
|
your choice). |
|
|
|
Edit the constants below to change the target values for the skills and |
|
attributes. Default is maxed out. |
|
|
|
Tested with Python 3.13 on December 28th, 2024 with SCUM Build 0.9.605.85940 |
|
""" |
|
|
|
from dataclasses import dataclass |
|
import datetime as dt |
|
from pathlib import Path |
|
import shutil |
|
import sqlite3 |
|
import struct |
|
import traceback |
|
from typing import Literal |
|
|
|
#### Configuration #### |
|
|
|
## Main attributes ## |
|
SET_ATTRIBUTES = { |
|
"BaseStrength": 8.0, # 1.0 to 8.0 |
|
"BaseConstitution": 5.0, # 1.0 to 5.0 |
|
"BaseDexterity": 5.0, # 1.0 to 5.0 |
|
"BaseIntelligence": 8.0, # 1.0 to 8.0 |
|
} |
|
|
|
## Skills ## |
|
""" |
|
You can remove skills from the list below and they will not be changed. |
|
If a new skill is added to the game, you can add it to the list below. |
|
|
|
The first number in each line is the skill level (0 - 3) |
|
The second number is the skill experience (0 - 10000000) |
|
""" |
|
|
|
SET_SKILLS = { |
|
"BoxingSkill": (3, 10000000), |
|
"AwarenessSkill": (3, 10000000), |
|
"RiflesSkill": (3, 10000000), |
|
"SnipingSkill": (3, 10000000), |
|
"CamouflageSkill": (3, 10000000), |
|
"SurvivalSkill": (3, 10000000), |
|
"MeleeWeaponsSkill": (3, 10000000), |
|
"HandgunSkill": (3, 10000000), |
|
"RunningSkill": (3, 10000000), |
|
"EnduranceSkill": (3, 10000000), |
|
"TacticsSkill": (3, 10000000), |
|
"CookingSkill": (3, 10000000), |
|
"ThieverySkill": (3, 10000000), |
|
"ArcherySkill": (3, 10000000), |
|
"DrivingSkill": (3, 10000000), |
|
"EngineeringSkill": (3, 10000000), |
|
"DemolitionSkill": (3, 10000000), |
|
"MedicalSkill": (3, 10000000), |
|
"MotorcycleSkill": (3, 10000000), |
|
"StealthSkill": (3, 10000000), |
|
"AviationSkill": (3, 10000000), |
|
"ResistanceSkill": (3, 10000000), |
|
"FarmingSkill": (3, 10000000), |
|
} |
|
|
|
# Other constants |
|
DB_PATH = Path.home() / "AppData/Local/SCUM/Saved/SaveFiles/SCUM.db" |
|
|
|
BODY_SIM_KEY_PADDING = 5 |
|
BODY_SIM_VALUE_PADDING = 10 |
|
|
|
|
|
@dataclass |
|
class PropertyType: |
|
"""Just a small class to define property types as they occur in the body simulation blob.""" |
|
|
|
name: bytes |
|
width: int # in bytes |
|
# Used for converting with Python types |
|
struct_type: Literal["<d", "<f", "<?"] |
|
|
|
|
|
DoubleProperty = PropertyType(name=b"DoubleProperty", width=8, struct_type="<d") |
|
FloatProperty = PropertyType(name=b"FloatProperty", width=4, struct_type="<f") |
|
BoolProperty = PropertyType(name=b"BoolProperty", width=1, struct_type="<?") |
|
|
|
|
|
def load_prisoner(con: sqlite3.Connection, id: int): |
|
"""Load prisoner from database.""" |
|
cur = con.execute("SELECT * FROM prisoner WHERE id = ?", (id,)) |
|
result = {desc[0]: val for desc, val in zip(cur.description, cur.fetchone())} |
|
return result |
|
|
|
|
|
def save_prisoner(con: sqlite3.Connection, prisoner: dict): |
|
"""Updates prisoner in database. Currently only sets body_simulation.""" |
|
return con.execute( |
|
"UPDATE prisoner SET body_simulation = ? WHERE id = ?", |
|
(prisoner["body_simulation"], prisoner["id"]), |
|
) |
|
|
|
|
|
def update_body_sim( |
|
body_sim: bytearray, |
|
key: bytes, |
|
value: float, |
|
property_type: PropertyType, |
|
): |
|
# Find the key in the body simulation blob |
|
key_offset = body_sim.index(key) |
|
|
|
# Make sure we are using the correct property type |
|
property_type_offset = key_offset + len(key) + BODY_SIM_KEY_PADDING |
|
assert ( |
|
body_sim[property_type_offset : property_type_offset + len(property_type.name)] |
|
== property_type.name |
|
) |
|
|
|
# Calculate offset of actual value |
|
value_offset = ( |
|
key_offset |
|
+ len(key) |
|
+ BODY_SIM_KEY_PADDING |
|
+ len(property_type.name) |
|
+ BODY_SIM_VALUE_PADDING |
|
) |
|
|
|
# Convert value to bytes |
|
value_bytes = struct.pack(property_type.struct_type, value) |
|
|
|
# Update value in body sim blob |
|
body_sim[value_offset : value_offset + property_type.width] = value_bytes |
|
|
|
|
|
def update_skills(con: sqlite3.Connection, prisoner: dict): |
|
"""Sets all skills to max level in the database.""" |
|
|
|
for (name,) in con.execute( |
|
"SELECT name FROM prisoner_skill WHERE prisoner_id = ?", (prisoner["id"],) |
|
): |
|
if name not in SET_SKILLS: |
|
continue |
|
|
|
new_level, new_experience = SET_SKILLS[name] |
|
|
|
# Finally, update the XML and other fields in the database |
|
con.execute( |
|
"UPDATE prisoner_skill SET level = ?, experience = ? WHERE prisoner_id = ? AND name = ?", |
|
(new_level, new_experience, prisoner["id"], name), |
|
) |
|
|
|
|
|
def choose_prisoner(con: sqlite3.Connection): |
|
"""Choose prisoner to update.""" |
|
cur = con.execute( |
|
"SELECT prisoner.id, user_profile.name FROM prisoner LEFT JOIN user_profile ON prisoner.user_profile_id = user_profile.id WHERE user_profile.authority_name is ?", |
|
(None,), |
|
) |
|
prisoners = {id: name for (id, name) in cur} |
|
|
|
if not prisoners: |
|
print("No prisoners found in local single player.") |
|
return None |
|
|
|
print("\nFound prisoners in local single player:\n") |
|
|
|
for id, name in prisoners.items(): |
|
print(f'"{name}" with ID {id}') |
|
|
|
selected_id = input("\nEnter prisoner ID: ") |
|
|
|
try: |
|
selected_id = int(selected_id) |
|
except ValueError: |
|
print("Invalid input.") |
|
return None |
|
|
|
if selected_id not in prisoners: |
|
print("Please enter a valid prisoner ID.") |
|
return None |
|
|
|
return selected_id |
|
|
|
|
|
def create_backup(db_path: Path): |
|
"""Creates a backup of the database.""" |
|
filename_safe_iso = dt.datetime.now().isoformat().replace(":", "-") |
|
backup_path = db_path.with_name(f"SCUM-bak-{filename_safe_iso}.db") |
|
shutil.copy(db_path, backup_path) |
|
return backup_path |
|
|
|
|
|
def main(): |
|
if not DB_PATH.exists(): |
|
print(f"Database file {DB_PATH} not found.") |
|
input("Press enter to exit.") |
|
return |
|
|
|
print("Creating backup... ") |
|
backup_path = create_backup(DB_PATH) |
|
print(f"Backed up to: {backup_path}") |
|
|
|
print("\nConnecting to database...") |
|
con = sqlite3.connect(DB_PATH) |
|
|
|
# Choose prisoner interactively |
|
prisoner_id = choose_prisoner(con) |
|
|
|
if not prisoner_id: |
|
input("Prisoner selection failed. Press enter to exit.") |
|
return |
|
|
|
print(f"Loading prisoner with ID {prisoner_id}...") |
|
prisoner = load_prisoner(con, prisoner_id) |
|
|
|
print("\nUpdating attributes... ", end="") |
|
body_sim = bytearray(prisoner["body_simulation"]) |
|
|
|
for attribute, value in SET_ATTRIBUTES.items(): |
|
update_body_sim( |
|
body_sim, |
|
attribute.encode("ascii"), |
|
value, |
|
DoubleProperty, |
|
) |
|
|
|
prisoner["body_simulation"] = bytes(body_sim) |
|
|
|
save_prisoner(con, prisoner) |
|
print("Success!") |
|
|
|
print("Updating skills... ", end="") |
|
update_skills(con, prisoner) |
|
print("Success!") |
|
|
|
con.commit() |
|
input("\nAll done! Press enter to exit.") |
|
|
|
|
|
if __name__ == "__main__": |
|
try: |
|
main() |
|
except KeyboardInterrupt: |
|
print("\nExiting...") |
|
except Exception: |
|
print("\n\nSomething went wrong...\n\n") |
|
traceback.print_exc() |
|
input("\n\nPress enter to exit.") |
@NickG1991 I finally had the time to check it out, unfortunately it's bad news.
While Nitrado indeed provisions a MySQL database, it doesn't seem like it's actually used by the SCUM server. Most likely, it still uses an SQLite database like it does on the single player. Sadly, Nitrado does not allow FTP access for SCUM so we can neither confirm nor exploit this.