Files
LeonarthCG 72ff9b1a7d Saving Princess: Security fixes for issues detected by Bandit (#6013)
* Saving Princess: absolute paths on suprocess.run

* Saving Princess: more error handling for downloads

* Saving Princess: rework launch_command setting

Apparently subprocess.Popen requires a list for args instead of a string everywhere but in Windows, so the change was preventing the game from running on Linux. Additionally, the game is now launched using absolute paths.

* Saving Princess: prevent bandit warnings

* Saving Princess: remove unnecessary compare_digest

* Saving Princess: fix Linux paths by using which

* Saving Princess: rename launch command setting

Previously, launch_command held a string. Now it holds a list of strings. Additionally, the defaults have changed.
To prevent the wrong type from being used, the setting has been renamed, effectively abandoning the original launch_command setting.

* Saving Princess: fix Linux default command return type

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2026-03-10 18:12:48 +00:00

186 lines
7.1 KiB
Python

import shutil
from typing import ClassVar, Dict, Any, Type, List, Union
import Utils
from BaseClasses import Tutorial, ItemClassification as ItemClass
from Options import PerGameCommonOptions, OptionError
from settings import Group, UserFilePath, LocalFolderPath, Bool
from worlds.AutoWorld import World, WebWorld
from worlds.LauncherComponents import components, Component, launch_subprocess, Type as ComponentType
from . import Options, Items, Locations
from .Constants import *
def launch_client(*args: str):
from .Client import launch
launch_subprocess(launch, name=CLIENT_NAME, args=args)
components.append(
Component(f"{GAME_NAME} Client", game_name=GAME_NAME, func=launch_client, component_type=ComponentType.CLIENT, supports_uri=True)
)
def get_default_launch_command() -> List[str]:
"""Returns platform-dependant default launch command for Saving Princess"""
if Utils.is_windows:
return []
else:
wine_path = shutil.which("wine")
return [wine_path] if wine_path is not None else ["/usr/bin/wine"]
class SavingPrincessSettings(Group):
class GamePath(UserFilePath):
"""Path to the game executable from which files are extracted"""
description = "the Saving Princess game executable"
is_exe = True
md5s = [GAME_HASH]
class InstallFolder(LocalFolderPath):
"""Path to the mod installation folder"""
description = "the folder to install Saving Princess Archipelago to"
class LaunchGame(Bool):
"""Set this to false to never autostart the game"""
class LaunchCommandWithArgs(List[str]):
"""
The console command that will be used to launch the game
The command will be executed with the installation folder as the current directory
Additional items in the list will be passed in as arguments
"""
exe_path: GamePath = GamePath("Saving Princess.exe")
install_folder: InstallFolder = InstallFolder("Saving Princess")
launch_game: Union[LaunchGame, bool] = True
launch_command_with_args: LaunchCommandWithArgs = LaunchCommandWithArgs(get_default_launch_command())
class SavingPrincessWeb(WebWorld):
theme = "partyTime"
bug_report_page = "https://github.com/LeonarthCG/saving-princess-archipelago/issues"
setup_en = Tutorial(
"Multiworld Setup Guide",
"A guide to setting up Saving Princess for Archipelago multiworld.",
"English",
"setup_en.md",
"setup/en",
["LeonarthCG"]
)
tutorials = [setup_en]
options_presets = Options.presets
option_groups = Options.groups
class SavingPrincessWorld(World):
"""
Explore a space station crawling with rogue machines and even rival bounty hunters
with the same objective as you - but with far, far different intentions!
Expand your arsenal as you collect upgrades to your trusty arm cannon and armor!
""" # Excerpt from itch
game = GAME_NAME
web = SavingPrincessWeb()
required_client_version = (0, 5, 0)
topology_present = False
item_name_to_id = {
key: value.code for key, value in (Items.item_dict.items() - Items.item_dict_events.items())
}
location_name_to_id = {
key: value.code for key, value in (Locations.location_dict.items() - Locations.location_dict_events.items())
}
item_name_groups = {
"Weapons": {key for key in Items.item_dict_weapons.keys()},
"Upgrades": {key for key in Items.item_dict_upgrades.keys()},
"Keys": {key for key in Items.item_dict_keys.keys()},
"Filler": {key for key in Items.item_dict_filler.keys()},
"Traps": {key for key in Items.item_dict_traps.keys()},
}
options_dataclass: ClassVar[Type[PerGameCommonOptions]] = Options.SavingPrincessOptions
options: Options.SavingPrincessOptions
settings_key = "saving_princess_settings"
settings: ClassVar[SavingPrincessSettings]
is_pool_expanded: bool = False
music_table: List[int]
def generate_early(self) -> None:
if not self.player_name.isascii():
raise OptionError(f"{self.player_name}'s name must be only ASCII.")
self.music_table = list(range(16))
self.is_pool_expanded = self.options.expanded_pool > 0
if self.options.music_shuffle:
self.random.shuffle(self.music_table)
# find zzz and purple and swap them back to their original positions
for song_id in [9, 13]:
song_index = self.music_table.index(song_id)
t = self.music_table[song_id]
self.music_table[song_id] = song_id
self.music_table[song_index] = t
def create_regions(self) -> None:
from .Regions import create_regions
create_regions(self.multiworld, self.player, self.is_pool_expanded)
def create_items(self) -> None:
items_made: int = 0
# now, for each item
item_dict = Items.item_dict_expanded if self.is_pool_expanded else Items.item_dict_base
for item_name, item_data in item_dict.items():
# create count copies of the item
for i in range(item_data.count):
self.multiworld.itempool.append(self.create_item(item_name))
items_made += item_data.count
# and create count_extra useful copies of the item
original_item_class: ItemClass = item_data.item_class
item_data.item_class = ItemClass.useful
for i in range(item_data.count_extra):
self.multiworld.itempool.append(self.create_item(item_name))
item_data.item_class = original_item_class
items_made += item_data.count_extra
# get the number of unfilled locations, that is, locations for items - items generated
location_count = len(Locations.location_dict_base)
if self.is_pool_expanded:
location_count = len(Locations.location_dict_expanded)
junk_count: int = location_count - items_made
# and generate as many junk items as unfilled locations
for i in range(junk_count):
self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
def create_item(self, name: str) -> Items.SavingPrincessItem:
return Items.item_dict[name].create_item(self.player)
def get_filler_item_name(self) -> str:
filler_list = list(Items.item_dict_filler.keys())
# check if this is going to be a trap
if self.random.randint(0, 99) < self.options.trap_chance:
filler_list = list(Items.item_dict_traps.keys())
# and return one of the names at random
return self.random.choice(filler_list)
def set_rules(self):
from .Rules import set_rules
set_rules(self)
def fill_slot_data(self) -> Dict[str, Any]:
slot_data = self.options.as_dict(
"death_link",
"expanded_pool",
"instant_saving",
"sprint_availability",
"cliff_weapon_upgrade",
"ace_weapon_upgrade",
"shake_intensity",
"iframes_duration",
)
slot_data["music_table"] = self.music_table
return slot_data