Launcher: dynamic Launcher
This commit is contained in:
parent
9ee37b0ec5
commit
958829d491
105
Launcher.py
105
Launcher.py
|
@ -14,10 +14,11 @@ import itertools
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from enum import Enum, auto
|
|
||||||
from os.path import isfile
|
from os.path import isfile
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from typing import Iterable, Sequence, Callable, Union, Optional
|
from typing import Sequence, Union, Optional
|
||||||
|
|
||||||
|
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
|
@ -70,108 +71,12 @@ def browse_files():
|
||||||
webbrowser.open(file)
|
webbrowser.open(file)
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyArgumentList
|
components.extend([
|
||||||
class Type(Enum):
|
|
||||||
TOOL = auto()
|
|
||||||
FUNC = auto() # not a real component
|
|
||||||
CLIENT = auto()
|
|
||||||
ADJUSTER = auto()
|
|
||||||
|
|
||||||
|
|
||||||
class SuffixIdentifier:
|
|
||||||
suffixes: Iterable[str]
|
|
||||||
|
|
||||||
def __init__(self, *args: str):
|
|
||||||
self.suffixes = args
|
|
||||||
|
|
||||||
def __call__(self, path: str):
|
|
||||||
if isinstance(path, str):
|
|
||||||
for suffix in self.suffixes:
|
|
||||||
if path.endswith(suffix):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class Component:
|
|
||||||
display_name: str
|
|
||||||
type: Optional[Type]
|
|
||||||
script_name: Optional[str]
|
|
||||||
frozen_name: Optional[str]
|
|
||||||
icon: str # just the name, no suffix
|
|
||||||
cli: bool
|
|
||||||
func: Optional[Callable]
|
|
||||||
file_identifier: Optional[Callable[[str], bool]]
|
|
||||||
|
|
||||||
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
|
|
||||||
cli: bool = False, icon: str = 'icon', component_type: Type = None, func: Optional[Callable] = None,
|
|
||||||
file_identifier: Optional[Callable[[str], bool]] = None):
|
|
||||||
self.display_name = display_name
|
|
||||||
self.script_name = script_name
|
|
||||||
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
|
||||||
self.icon = icon
|
|
||||||
self.cli = cli
|
|
||||||
self.type = component_type or \
|
|
||||||
None if not display_name else \
|
|
||||||
Type.FUNC if func else \
|
|
||||||
Type.CLIENT if 'Client' in display_name else \
|
|
||||||
Type.ADJUSTER if 'Adjuster' in display_name else Type.TOOL
|
|
||||||
self.func = func
|
|
||||||
self.file_identifier = file_identifier
|
|
||||||
|
|
||||||
def handles_file(self, path: str):
|
|
||||||
return self.file_identifier(path) if self.file_identifier else False
|
|
||||||
|
|
||||||
|
|
||||||
components: Iterable[Component] = (
|
|
||||||
# Launcher
|
|
||||||
Component('', 'Launcher'),
|
|
||||||
# Core
|
|
||||||
Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True,
|
|
||||||
file_identifier=SuffixIdentifier('.archipelago', '.zip')),
|
|
||||||
Component('Generate', 'Generate', cli=True),
|
|
||||||
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'),
|
|
||||||
# SNI
|
|
||||||
Component('SNI Client', 'SNIClient',
|
|
||||||
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3',
|
|
||||||
'.apsmw', '.apl2ac')),
|
|
||||||
Component('Links Awakening DX Client', 'LinksAwakeningClient',
|
|
||||||
file_identifier=SuffixIdentifier('.apladx')),
|
|
||||||
Component('LttP Adjuster', 'LttPAdjuster'),
|
|
||||||
# Factorio
|
|
||||||
Component('Factorio Client', 'FactorioClient'),
|
|
||||||
# Minecraft
|
|
||||||
Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True,
|
|
||||||
file_identifier=SuffixIdentifier('.apmc')),
|
|
||||||
# Ocarina of Time
|
|
||||||
Component('OoT Client', 'OoTClient',
|
|
||||||
file_identifier=SuffixIdentifier('.apz5')),
|
|
||||||
Component('OoT Adjuster', 'OoTAdjuster'),
|
|
||||||
# FF1
|
|
||||||
Component('FF1 Client', 'FF1Client'),
|
|
||||||
# Pokémon
|
|
||||||
Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')),
|
|
||||||
# TLoZ
|
|
||||||
Component('Zelda 1 Client', 'Zelda1Client'),
|
|
||||||
# ChecksFinder
|
|
||||||
Component('ChecksFinder Client', 'ChecksFinderClient'),
|
|
||||||
# Starcraft 2
|
|
||||||
Component('Starcraft 2 Client', 'Starcraft2Client'),
|
|
||||||
# Wargroove
|
|
||||||
Component('Wargroove Client', 'WargrooveClient'),
|
|
||||||
# Zillion
|
|
||||||
Component('Zillion Client', 'ZillionClient',
|
|
||||||
file_identifier=SuffixIdentifier('.apzl')),
|
|
||||||
#Kingdom Hearts 2
|
|
||||||
Component('KH2 Client', "KH2Client"),
|
|
||||||
# Functions
|
# Functions
|
||||||
Component('Open host.yaml', func=open_host_yaml),
|
Component('Open host.yaml', func=open_host_yaml),
|
||||||
Component('Open Patch', func=open_patch),
|
Component('Open Patch', func=open_patch),
|
||||||
Component('Browse Files', func=browse_files),
|
Component('Browse Files', func=browse_files),
|
||||||
)
|
])
|
||||||
icon_paths = {
|
|
||||||
'icon': local_path('data', 'icon.ico' if is_windows else 'icon.png'),
|
|
||||||
'mcicon': local_path('data', 'mcicon.ico')
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def identify(path: Union[None, str]):
|
def identify(path: Union[None, str]):
|
||||||
|
|
|
@ -105,6 +105,7 @@ Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, E
|
||||||
Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
|
Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
|
||||||
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
|
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
|
||||||
|
|
||||||
|
Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion;
|
||||||
Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
|
Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
|
||||||
Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
|
Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
|
||||||
Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
|
Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -40,7 +40,7 @@ if __name__ == "__main__":
|
||||||
ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv)
|
ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv)
|
||||||
ModuleUpdate.update_ran = False # restore for later
|
ModuleUpdate.update_ran = False # restore for later
|
||||||
|
|
||||||
from Launcher import components, icon_paths
|
from worlds.LauncherComponents import components, icon_paths
|
||||||
from Utils import version_tuple, is_windows, is_linux
|
from Utils import version_tuple, is_windows, is_linux
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
from enum import Enum, auto
|
||||||
|
from typing import Optional, Callable, List, Iterable
|
||||||
|
|
||||||
|
from Utils import local_path, is_windows
|
||||||
|
|
||||||
|
|
||||||
|
class Type(Enum):
|
||||||
|
TOOL = auto()
|
||||||
|
FUNC = auto() # not a real component
|
||||||
|
CLIENT = auto()
|
||||||
|
ADJUSTER = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class Component:
|
||||||
|
display_name: str
|
||||||
|
type: Optional[Type]
|
||||||
|
script_name: Optional[str]
|
||||||
|
frozen_name: Optional[str]
|
||||||
|
icon: str # just the name, no suffix
|
||||||
|
cli: bool
|
||||||
|
func: Optional[Callable]
|
||||||
|
file_identifier: Optional[Callable[[str], bool]]
|
||||||
|
|
||||||
|
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
|
||||||
|
cli: bool = False, icon: str = 'icon', component_type: Type = None, func: Optional[Callable] = None,
|
||||||
|
file_identifier: Optional[Callable[[str], bool]] = None):
|
||||||
|
self.display_name = display_name
|
||||||
|
self.script_name = script_name
|
||||||
|
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
||||||
|
self.icon = icon
|
||||||
|
self.cli = cli
|
||||||
|
self.type = component_type or \
|
||||||
|
None if not display_name else \
|
||||||
|
Type.FUNC if func else \
|
||||||
|
Type.CLIENT if 'Client' in display_name else \
|
||||||
|
Type.ADJUSTER if 'Adjuster' in display_name else Type.TOOL
|
||||||
|
self.func = func
|
||||||
|
self.file_identifier = file_identifier
|
||||||
|
|
||||||
|
def handles_file(self, path: str):
|
||||||
|
return self.file_identifier(path) if self.file_identifier else False
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.__class__.__name__}({self.display_name})"
|
||||||
|
|
||||||
|
|
||||||
|
class SuffixIdentifier:
|
||||||
|
suffixes: Iterable[str]
|
||||||
|
|
||||||
|
def __init__(self, *args: str):
|
||||||
|
self.suffixes = args
|
||||||
|
|
||||||
|
def __call__(self, path: str):
|
||||||
|
if isinstance(path, str):
|
||||||
|
for suffix in self.suffixes:
|
||||||
|
if path.endswith(suffix):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
components: List[Component] = [
|
||||||
|
# Launcher
|
||||||
|
Component('', 'Launcher'),
|
||||||
|
# Core
|
||||||
|
Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True,
|
||||||
|
file_identifier=SuffixIdentifier('.archipelago', '.zip')),
|
||||||
|
Component('Generate', 'Generate', cli=True),
|
||||||
|
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'),
|
||||||
|
# SNI
|
||||||
|
Component('SNI Client', 'SNIClient',
|
||||||
|
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3',
|
||||||
|
'.apsmw', '.apl2ac')),
|
||||||
|
Component('Links Awakening DX Client', 'LinksAwakeningClient',
|
||||||
|
file_identifier=SuffixIdentifier('.apladx')),
|
||||||
|
Component('LttP Adjuster', 'LttPAdjuster'),
|
||||||
|
# Minecraft
|
||||||
|
Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True,
|
||||||
|
file_identifier=SuffixIdentifier('.apmc')),
|
||||||
|
# Ocarina of Time
|
||||||
|
Component('OoT Client', 'OoTClient',
|
||||||
|
file_identifier=SuffixIdentifier('.apz5')),
|
||||||
|
Component('OoT Adjuster', 'OoTAdjuster'),
|
||||||
|
# FF1
|
||||||
|
Component('FF1 Client', 'FF1Client'),
|
||||||
|
# Pokémon
|
||||||
|
Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')),
|
||||||
|
# TLoZ
|
||||||
|
Component('Zelda 1 Client', 'Zelda1Client'),
|
||||||
|
# ChecksFinder
|
||||||
|
Component('ChecksFinder Client', 'ChecksFinderClient'),
|
||||||
|
# Starcraft 2
|
||||||
|
Component('Starcraft 2 Client', 'Starcraft2Client'),
|
||||||
|
# Wargroove
|
||||||
|
Component('Wargroove Client', 'WargrooveClient'),
|
||||||
|
# Zillion
|
||||||
|
Component('Zillion Client', 'ZillionClient',
|
||||||
|
file_identifier=SuffixIdentifier('.apzl')),
|
||||||
|
#Kingdom Hearts 2
|
||||||
|
Component('KH2 Client', "KH2Client"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
icon_paths = {
|
||||||
|
'icon': local_path('data', 'icon.ico' if is_windows else 'icon.png'),
|
||||||
|
'mcicon': local_path('data', 'mcicon.ico')
|
||||||
|
}
|
|
@ -40,6 +40,9 @@ class WorldSource(typing.NamedTuple):
|
||||||
path: str # typically relative path from this module
|
path: str # typically relative path from this module
|
||||||
is_zip: bool = False
|
is_zip: bool = False
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip})"
|
||||||
|
|
||||||
|
|
||||||
# find potential world containers, currently folders and zip-importable .apworld's
|
# find potential world containers, currently folders and zip-importable .apworld's
|
||||||
world_sources: typing.List[WorldSource] = []
|
world_sources: typing.List[WorldSource] = []
|
||||||
|
@ -55,24 +58,35 @@ for file in os.scandir(folder):
|
||||||
# import all submodules to trigger AutoWorldRegister
|
# import all submodules to trigger AutoWorldRegister
|
||||||
world_sources.sort()
|
world_sources.sort()
|
||||||
for world_source in world_sources:
|
for world_source in world_sources:
|
||||||
if world_source.is_zip:
|
try:
|
||||||
importer = zipimport.zipimporter(os.path.join(folder, world_source.path))
|
if world_source.is_zip:
|
||||||
if hasattr(importer, "find_spec"): # new in Python 3.10
|
importer = zipimport.zipimporter(os.path.join(folder, world_source.path))
|
||||||
spec = importer.find_spec(world_source.path.split(".", 1)[0])
|
if hasattr(importer, "find_spec"): # new in Python 3.10
|
||||||
mod = importlib.util.module_from_spec(spec)
|
spec = importer.find_spec(world_source.path.split(".", 1)[0])
|
||||||
else: # TODO: remove with 3.8 support
|
mod = importlib.util.module_from_spec(spec)
|
||||||
mod = importer.load_module(world_source.path.split(".", 1)[0])
|
else: # TODO: remove with 3.8 support
|
||||||
|
mod = importer.load_module(world_source.path.split(".", 1)[0])
|
||||||
|
|
||||||
mod.__package__ = f"worlds.{mod.__package__}"
|
mod.__package__ = f"worlds.{mod.__package__}"
|
||||||
mod.__name__ = f"worlds.{mod.__name__}"
|
mod.__name__ = f"worlds.{mod.__name__}"
|
||||||
sys.modules[mod.__name__] = mod
|
sys.modules[mod.__name__] = mod
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
warnings.filterwarnings("ignore", message="__package__ != __spec__.parent")
|
warnings.filterwarnings("ignore", message="__package__ != __spec__.parent")
|
||||||
# Found no equivalent for < 3.10
|
# Found no equivalent for < 3.10
|
||||||
if hasattr(importer, "exec_module"):
|
if hasattr(importer, "exec_module"):
|
||||||
importer.exec_module(mod)
|
importer.exec_module(mod)
|
||||||
else:
|
else:
|
||||||
importlib.import_module(f".{world_source.path}", "worlds")
|
importlib.import_module(f".{world_source.path}", "worlds")
|
||||||
|
except Exception as e:
|
||||||
|
# A single world failing can still mean enough is working for the user, log and carry on
|
||||||
|
import traceback
|
||||||
|
import io
|
||||||
|
file_like = io.StringIO()
|
||||||
|
print(f"Could not load world {world_source}:", file=file_like)
|
||||||
|
traceback.print_exc(file=file_like)
|
||||||
|
file_like.seek(0)
|
||||||
|
import logging
|
||||||
|
logging.exception(file_like.read())
|
||||||
|
|
||||||
lookup_any_item_id_to_name = {}
|
lookup_any_item_id_to_name = {}
|
||||||
lookup_any_location_id_to_name = {}
|
lookup_any_location_id_to_name = {}
|
||||||
|
|
|
@ -15,6 +15,9 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
|
||||||
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \
|
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \
|
||||||
fluids, stacking_items, valid_ingredients, progressive_rows
|
fluids, stacking_items, valid_ingredients, progressive_rows
|
||||||
from .Locations import location_pools, location_table
|
from .Locations import location_pools, location_table
|
||||||
|
from worlds.LauncherComponents import Component, components
|
||||||
|
|
||||||
|
components.append(Component("Factorio Client", "FactorioClient"))
|
||||||
|
|
||||||
|
|
||||||
class FactorioWeb(WebWorld):
|
class FactorioWeb(WebWorld):
|
||||||
|
|
Loading…
Reference in New Issue