Skip to content

Commit 1f4dae6

Browse files
Zayn995claude
andcommitted
Save slot limits and autosave interval
Four sliders in a new 'Saving (quality of life)' section of the Player tab: manual, quick and autosave slot limits (SaveLoadVariables.cfg SavesLimit, vanilla 31/3/10) and the autosave interval in minutes (AutoSaveVariables.cfg AutoSaveIntervalTime, vanilla 600 s). Both files are plain cfg like CoreVariables, so they are extracted as-is and patched as <Name>.cfg_patch_<Mod>.cfg; the cache schema moves to 17. Values are read live and only deviations are written. Not play-tested. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent aff1742 commit 1f4dae6

8 files changed

Lines changed: 157 additions & 4 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ and bloodsucker cloaking; mutant health regen ×0 = the mutant version
3434
of "NPCs don't self-heal"),
3535
headshots, explosions, armor protection per damage type, weapon damage/
3636
spread/recoil/durability/fire rate/range/bleeding/ADS move speed/ADS
37-
aim-in speed/magazine size on three levels, technician upgrade locks (take both / no blueprint / no tiers), A-Life spawns (lair population, respawn, encounter frequency, mutant share, pack size, per-species weights), day length, consumable effect duration, artifacts per field + respawn, weightless quest items, NPC combat behaviour (guaranteed-hit shots, burst length, fire pauses, engagement range, NPC weapon range, NPC regen), stealth (crouch, movement noise, weather, flashlight), NPC awareness & nerve (alertness, search time, courage, stagger, attack cooldowns, weapon rank bonus), NPC flashlights (brightness & reach, beam width, use in combat, on/off hours), melee damage & range, interaction reach & talk distance, jamming, scoped sway,
37+
aim-in speed/magazine size on three levels, technician upgrade locks (take both / no blueprint / no tiers), A-Life spawns (lair population, respawn, encounter frequency, mutant share, pack size, per-species weights), day length, consumable effect duration, artifacts per field + respawn, weightless quest items, NPC combat behaviour (guaranteed-hit shots, burst length, fire pauses, engagement range, NPC weapon range, NPC regen), stealth (crouch, movement noise, weather, flashlight), NPC awareness & nerve (alertness, search time, courage, stagger, attack cooldowns, weapon rank bonus), NPC flashlights (brightness & reach, beam width, use in combat, on/off hours), melee damage & range, interaction reach & talk distance, save slots & autosave interval, jamming, scoped sway,
3838
breath hold, repeatable-quest cooldown, ammo
3939
damage/armor piercing/armor damage/cover penetration, NPC accuracy/vision/
4040
hearing/grenades, NPC gear quality (tilts squad loadout rolls toward

release/README.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ WHAT YOU CAN TWEAK (short tour)
1414
-------------------------------
1515
- Player: health, stamina (incl. per-action costs), walk/crouch and
1616
run/sprint speed, jump height, fall damage, breath hold, interaction
17-
reach and talk distance, max carry weight + where the overweight
18-
penalty starts, item weights per category,
17+
reach and talk distance, save slots and autosave interval, max carry
18+
weight + where the overweight penalty starts, item weights per category,
1919
radiation, bleeding, hunger & sleepiness, headshot multiplier,
2020
explosions, hit camera shake / aim punch
2121
- Vaulting: seven sliders for how Skif climbs and vaults, plus the

s2tweaker/gamedata.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@
5757
"ALifePrototypes/ALifeDirectorScenarioPrototypes.cfg.bin",
5858
"AIPrototypes/ThreatPrototypes.cfg.bin",
5959
"FlashlightPrototypes.cfg.bin", # NPC-Taschenlampen (05.09.2026)
60+
"SaveLoadVariables.cfg", # Speicherstaende-Limit (unbinarisiert)
61+
"AutoSaveVariables.cfg", # Autosave-Intervall (unbinarisiert)
6062
]
6163

6264
# Bei Aenderungen an NEEDED_FILES erhoehen -> alte Caches werden neu aufgebaut
63-
CACHE_SCHEMA = 16
65+
CACHE_SCHEMA = 17
6466

6567
# Mutanten-Art (Fraktion) -> Praefixe der Attacken-Structs in
6668
# AbilityPrototypes.cfg (verifiziert; docs/V15_DATA_RESEARCH.md).
@@ -370,6 +372,18 @@ def flashlights(self) -> CfgStruct:
370372
Lichtwerte; die Spieler-Lampe sitzt in Blueprint-Kurven."""
371373
return self._parse("FlashlightPrototypes.cfg")
372374

375+
@cached_property
376+
def saveload(self) -> CfgStruct:
377+
"""SaveLoadVariables (unbinarisiert): DefaultConfig.SavesLimit je
378+
Speichertyp, 0 = unbegrenzt."""
379+
return self._parse("SaveLoadVariables.cfg")
380+
381+
@cached_property
382+
def autosave(self) -> CfgStruct:
383+
"""AutoSaveVariables (unbinarisiert): DefaultConfig.AutoSaveIntervalTime
384+
in Sekunden."""
385+
return self._parse("AutoSaveVariables.cfg")
386+
373387
@cached_property
374388
def weatherselection(self) -> CfgStruct:
375389
return self._parse("WeatherSelectionPrototypes.cfg")

s2tweaker/gui.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,10 @@ def _update_dot(self):
453453
self.dot.pack(side="left", after=self.label)
454454

455455

456+
def fmt_min(v: float) -> str:
457+
return f"{int(round(v))} min"
458+
459+
456460
def fmt_int(v: float) -> str:
457461
return f"{v:.0f}"
458462

@@ -523,6 +527,8 @@ def fmt_dec(v: float) -> str:
523527
"npc_light": "npc_flashlight_factor", "npc_light_cone": "npc_flashlight_cone_factor",
524528
"npc_light_combat": "npc_flashlight_combat_factor",
525529
"npc_light_on": "npc_flashlight_on_hour", "npc_light_off": "npc_flashlight_off_hour",
530+
"save_manual": "manual_save_slots", "save_quick": "quick_save_slots",
531+
"save_auto": "auto_save_slots", "autosave_min": "autosave_interval_min",
526532
"mut_attack_cd": "mutant_attack_cooldown_factor",
527533
"mhp": "mutant_hp_factor", "mdmg": "mutant_damage_factor",
528534
"mspeed": "mutant_speed_factor", "mhearing": "mutant_hearing_factor",
@@ -3615,6 +3621,19 @@ def _build_body(self):
36153621
"Nexus. Not play-tested yet.")
36163622
ctk.CTkLabel(f, text="", height=2).pack()
36173623

3624+
f = self._section(body, "Saving (quality of life)")
3625+
self._slider(f, "save_manual", "Manual save slots", 10, 999, 1, 31, fmt_int,
3626+
"How many manual saves a campaign may hold before the "
3627+
"game makes you delete one (vanilla 31). The 'Unlimited "
3628+
"Saves' idea from Nexus. Not play-tested yet.")
3629+
self._slider(f, "save_quick", "Quick save slots", 1, 30, 1, 3, fmt_int,
3630+
"How many quick saves are kept (vanilla 3).")
3631+
self._slider(f, "save_auto", "Autosave slots", 1, 50, 1, 10, fmt_int,
3632+
"How many timed autosaves are kept (vanilla 10).")
3633+
self._slider(f, "autosave_min", "Autosave interval", 1, 60, 1, 10, fmt_min,
3634+
"Minutes between timed autosaves (vanilla 10).")
3635+
ctk.CTkLabel(f, text="", height=2).pack()
3636+
36183637
body = self._tab("Weight & items")
36193638
f = self._section(body, "Weight & inventory")
36203639
self._slider(f, "carry", "Max carry weight (hard limit)", 20, 500, 5, 80, fmt_kg)
@@ -4755,6 +4774,10 @@ def _collect(self) -> Settings:
47554774
melee_range_factor=s["melee_range"].get() / 100.0,
47564775
interaction_range_factor=s["interact"].get() / 100.0,
47574776
dialog_range_factor=s["dialog_range"].get() / 100.0,
4777+
manual_save_slots=int(s["save_manual"].get()),
4778+
quick_save_slots=int(s["save_quick"].get()),
4779+
auto_save_slots=int(s["save_auto"].get()),
4780+
autosave_interval_min=float(s["autosave_min"].get()),
47584781
ammo_damage_factor=s["ammo_dmg"].get() / 100.0,
47594782
ammo_piercing_factor=s["ammo_ap"].get() / 100.0,
47604783
ammo_armor_damage_factor=s["ammo_ad"].get() / 100.0,

s2tweaker/tweaks.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,11 @@ class Settings:
491491
npc_flashlight_combat_factor: float = 1.0 # FlashlightCombatUseChance je Rang (Deckel 1.0)
492492
npc_flashlight_on_hour: int = 22 # AIGlobals FlashlightTimeOfDayOn
493493
npc_flashlight_off_hour: int = 5 # AIGlobals FlashlightTimeOfDayOff
494+
# --- Speichern (SaveLoadVariables / AutoSaveVariables, beide unbinarisiert) ---
495+
manual_save_slots: int = 31 # SavesLimit.Manual (0 = unbegrenzt)
496+
quick_save_slots: int = 3 # SavesLimit.Quick
497+
auto_save_slots: int = 10 # SavesLimit.Auto
498+
autosave_interval_min: float = 10.0 # AutoSaveIntervalTime (Vanilla 600 s)
494499
# --- Munition (global ueber alle Munitionstypen) ---
495500
ammo_damage_factor: float = 1.0
496501
ammo_piercing_factor: float = 1.0 # verstaerkt die AP-Charakteristik
@@ -2325,6 +2330,38 @@ def _holdbreath_patch(gd: GameData, s: Settings) -> dict:
23252330
return {"DefaultHoldBreathParams": cfg}
23262331

23272332

2333+
def _saveload_patch(gd: GameData, s: Settings) -> dict:
2334+
"""SaveLoadVariables (unbinarisiert wie CoreVariables): SavesLimit je
2335+
Speichertyp (Vanilla Manual 31 / Quick 3 / Auto 10; 0 = unbegrenzt).
2336+
Nexus "Bode's Unlimited Saves", Recherche 05.09.2026. Live gelesen,
2337+
nur Abweichungen geschrieben."""
2338+
node = gd.saveload.children.get("DefaultConfig")
2339+
limits = node.children.get("SavesLimit") if node else None
2340+
if limits is None:
2341+
return {}
2342+
cfg: dict = {}
2343+
for key, wanted in (("Manual", s.manual_save_slots),
2344+
("Quick", s.quick_save_slots),
2345+
("Auto", s.auto_save_slots)):
2346+
raw = limits.values.get(key)
2347+
if raw is not None and wanted >= 0 and _neq(float(wanted), parse_number(raw)):
2348+
cfg[key] = str(int(round(wanted)))
2349+
return {"DefaultConfig": {"SavesLimit": cfg}} if cfg else {}
2350+
2351+
2352+
def _autosave_patch(gd: GameData, s: Settings) -> dict:
2353+
"""AutoSaveVariables (unbinarisiert): AutoSaveIntervalTime in Sekunden
2354+
(Vanilla 600 = 10 min). Der Regler arbeitet in Minuten."""
2355+
node = gd.autosave.children.get("DefaultConfig")
2356+
raw = node.values.get("AutoSaveIntervalTime") if node else None
2357+
if raw is None or s.autosave_interval_min <= 0:
2358+
return {}
2359+
wanted = s.autosave_interval_min * 60.0
2360+
if not _neq(wanted, parse_number(raw)):
2361+
return {}
2362+
return {"DefaultConfig": {"AutoSaveIntervalTime": str(int(round(wanted)))}}
2363+
2364+
23282365
def _corevars_patch(gd: GameData, s: Settings) -> dict:
23292366
cfg: dict = {}
23302367
if _neq(s.repair_cost_factor, 1.0):
@@ -3007,6 +3044,8 @@ def add(path: str, patches: dict):
30073044
add(f"ObjHoldBreathParamsPrototypes/ObjHoldBreathParamsPrototypes_patch_{n}.cfg",
30083045
_holdbreath_patch(gd, s))
30093046
add(f"CoreVariables.cfg_patch_{n}.cfg", _corevars_patch(gd, s))
3047+
add(f"SaveLoadVariables.cfg_patch_{n}.cfg", _saveload_patch(gd, s))
3048+
add(f"AutoSaveVariables.cfg_patch_{n}.cfg", _autosave_patch(gd, s))
30103049
add(f"StashPrototypes/StashPrototypes_patch_{n}.cfg", _stash_patch(gd, s))
30113050
# Drei Builder teilen sich die Generator-Datei (Mengen, Waffen-Zustand,
30123051
# Haendler-Bestand) und teils denselben PossibleItems-Eintrag -> mergen
@@ -3190,6 +3229,14 @@ def f(name, factor, vanilla=1.0):
31903229
lines.append(f"NPC flashlights on from {int(s.npc_flashlight_on_hour)}:00")
31913230
if _neq(s.npc_flashlight_off_hour, 5):
31923231
lines.append(f"NPC flashlights off at {int(s.npc_flashlight_off_hour)}:00")
3232+
if _neq(s.manual_save_slots, 31):
3233+
lines.append(f"Manual save slots {int(s.manual_save_slots)}")
3234+
if _neq(s.quick_save_slots, 3):
3235+
lines.append(f"Quick save slots {int(s.quick_save_slots)}")
3236+
if _neq(s.auto_save_slots, 10):
3237+
lines.append(f"Autosave slots {int(s.auto_save_slots)}")
3238+
if _neq(s.autosave_interval_min, 10.0):
3239+
lines.append(f"Autosave every {s.autosave_interval_min:g} min")
31933240
f("Ammo damage", s.ammo_damage_factor)
31943241
f("Ammo armor piercing", s.ammo_piercing_factor)
31953242
f("Ammo armor damage", s.ammo_armor_damage_factor)

test_generate.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@
9797
npc_flashlight_factor=2.0, npc_flashlight_cone_factor=1.5,
9898
npc_flashlight_combat_factor=0.5, npc_flashlight_on_hour=20,
9999
npc_flashlight_off_hour=6,
100+
manual_save_slots=200, quick_save_slots=10, auto_save_slots=20,
101+
autosave_interval_min=5,
100102
anomaly_electro_factor=0.5, anomaly_fire_factor=2.0,
101103
consumable_factor=2.0, rain_factor=2.0, emission_factor=0.5,
102104
emission_duration_factor=2.0,

tests/run_all.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"test_npc_combat.py",
4040
"test_npc_more.py",
4141
"test_npc_flashlight.py",
42+
"test_saves.py",
4243
"test_index_entries.py",
4344
"test_trader_condition.py",
4445
"test_emission_relext.py",

tests/test_saves.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Speicherstaende-Limit (SaveLoadVariables.cfg) und Autosave-Intervall
2+
(AutoSaveVariables.cfg) - Nexus-Recherche 05.09.2026 ("Bode's Unlimited
3+
Saves", 343 Endorsements; das Intervall als Komfortidee dazu).
4+
5+
Beide Dateien sind unbinarisiert wie CoreVariables, die Patch-Datei heisst
6+
darum <Name>.cfg_patch_<Mod>.cfg direkt in GameData/.
7+
8+
Prueft: Dateien im Extraktionsumfang + Schema-Bump, Vanilla-Werte live ==
9+
Settings-Defaults (sonst wuerde die Vanilla-Stellung patchen), Patch-Format
10+
({bpatch} auf beiden Ebenen, nur geaenderte Schluessel, Ganzzahlen), das
11+
Intervall in Sekunden, Neutralzustand, Tweak-Zeilen.
12+
"""
13+
import sys
14+
from pathlib import Path
15+
16+
ROOT = Path(__file__).resolve().parents[1]
17+
sys.path.insert(0, str(ROOT))
18+
VANILLA = str(ROOT / "vanilla" / "Stalker2" / "Content" / "GameLite" / "GameData")
19+
20+
from s2tweaker.cfgparse import parse_number
21+
from s2tweaker.gamedata import GameData, NEEDED_FILES, CACHE_SCHEMA
22+
from s2tweaker.tweaks import Settings, build_patches, summarize
23+
24+
for name in ("SaveLoadVariables.cfg", "AutoSaveVariables.cfg"):
25+
assert name in NEEDED_FILES, f"{name} fehlt im Extraktionsumfang"
26+
assert CACHE_SCHEMA >= 17, "neue Dateien ohne Schema-Bump"
27+
gd = GameData(VANILLA)
28+
SL = "SaveLoadVariables.cfg_patch_S2Tweaker.cfg"
29+
AS = "AutoSaveVariables.cfg_patch_S2Tweaker.cfg"
30+
31+
# --- 1) Vanilla live == Settings-Defaults --------------------------------
32+
limits = gd.saveload.children["DefaultConfig"].children["SavesLimit"].values
33+
default = Settings()
34+
for key, field in (("Manual", "manual_save_slots"), ("Quick", "quick_save_slots"),
35+
("Auto", "auto_save_slots")):
36+
vanilla = int(parse_number(limits[key]))
37+
assert vanilla == getattr(default, field), (key, vanilla, getattr(default, field))
38+
interval = parse_number(gd.autosave.children["DefaultConfig"].values["AutoSaveIntervalTime"])
39+
assert abs(interval - default.autosave_interval_min * 60.0) < 1e-9, interval
40+
print(f"Vanilla: Manual {limits['Manual']}, Quick {limits['Quick']}, Auto {limits['Auto']}, "
41+
f"Autosave {interval:g} s == Defaults OK")
42+
43+
# --- 2) Nur geaenderte Schluessel, Ganzzahlen, {bpatch} ------------------
44+
text = build_patches(gd, Settings(manual_save_slots=200, quick_save_slots=10))[SL]
45+
assert "DefaultConfig : struct.begin {bpatch}" in text, text
46+
assert "SavesLimit : struct.begin {bpatch}" in text, text
47+
assert "Manual = 200" in text and "Quick = 10" in text, text
48+
assert "Auto" not in text, "Auto geschrieben, obwohl auf Vanilla"
49+
assert text.count("struct.end") == 2, text
50+
text = build_patches(gd, Settings(autosave_interval_min=5))[AS]
51+
assert "DefaultConfig : struct.begin {bpatch}" in text and "AutoSaveIntervalTime = 300" in text, text
52+
text = build_patches(gd, Settings(autosave_interval_min=2.5))[AS]
53+
assert "AutoSaveIntervalTime = 150" in text, text
54+
print("Patches: SavesLimit nur Manual+Quick, Autosave 5 min -> 300 s OK")
55+
56+
# --- 3) Neutral + Tweak-Zeilen --------------------------------------------
57+
neutral = build_patches(gd, Settings())
58+
assert SL not in neutral and AS not in neutral
59+
lines = summarize(Settings(manual_save_slots=200, quick_save_slots=10,
60+
auto_save_slots=20, autosave_interval_min=5))
61+
for needle in ("Manual save slots 200", "Quick save slots 10", "Autosave slots 20",
62+
"Autosave every 5 min"):
63+
assert any(needle in ln for ln in lines), (needle, lines)
64+
print("Neutral leer, 4 Tweak-Zeilen OK")
65+
66+
print("\nSPEICHER-TEST OK")

0 commit comments

Comments
 (0)