# Released under the MIT License. See LICENSE for details.
#
"""Playlist related functionality."""
from __future__ import annotations
import copy
import logging
from typing import Any, TYPE_CHECKING
import babase
if TYPE_CHECKING:
from typing import Sequence
from bascenev1._session import Session
PlaylistType = list[dict[str, Any]]
[docs]
def filter_playlist(
playlist: PlaylistType,
sessiontype: type[Session],
*,
add_resolved_type: bool = False,
remove_unowned: bool = True,
mark_unowned: bool = False,
name: str = '?',
) -> PlaylistType:
"""Return a filtered version of a playlist.
Strips out or replaces invalid or unowned game types, makes sure all
settings are present, and adds in a 'resolved_type' which is the actual
type.
"""
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
from bascenev1._map import get_filtered_map_name
from bascenev1._gameactivity import GameActivity
assert babase.app.classic is not None
goodlist: list[dict] = []
unowned_maps: Sequence[str]
available_maps: list[str] = list(babase.app.classic.maps.keys())
if (remove_unowned or mark_unowned) and babase.app.classic is not None:
unowned_maps = babase.app.classic.store.get_unowned_maps()
unowned_game_types = babase.app.classic.store.get_unowned_game_types()
else:
unowned_maps = []
unowned_game_types = set()
for entry in copy.deepcopy(playlist):
# 'map' used to be called 'level' here.
if 'level' in entry:
entry['map'] = entry['level']
del entry['level']
# We now stuff map into settings instead of it being its own thing.
if 'map' in entry:
entry['settings']['map'] = entry['map']
del entry['map']
# Update old map names to new ones.
entry['settings']['map'] = get_filtered_map_name(
entry['settings']['map']
)
if remove_unowned and entry['settings']['map'] in unowned_maps:
continue
# Ok, for each game in our list, try to import the module and grab
# the actual game class. add successful ones to our initial list
# to present to the user.
if not isinstance(entry['type'], str):
raise TypeError('invalid entry format')
try:
# Do some type filters for backwards compat.
if entry['type'] in (
'Assault.AssaultGame',
'Happy_Thoughts.HappyThoughtsGame',
'bsAssault.AssaultGame',
'bs_assault.AssaultGame',
'bastd.game.assault.AssaultGame',
):
entry['type'] = 'bascenev1lib.game.assault.AssaultGame'
if entry['type'] in (
'King_of_the_Hill.KingOfTheHillGame',
'bsKingOfTheHill.KingOfTheHillGame',
'bs_king_of_the_hill.KingOfTheHillGame',
'bastd.game.kingofthehill.KingOfTheHillGame',
):
entry['type'] = (
'bascenev1lib.game.kingofthehill.KingOfTheHillGame'
)
if entry['type'] in (
'Capture_the_Flag.CTFGame',
'bsCaptureTheFlag.CTFGame',
'bs_capture_the_flag.CTFGame',
'bastd.game.capturetheflag.CaptureTheFlagGame',
):
entry['type'] = (
'bascenev1lib.game.capturetheflag.CaptureTheFlagGame'
)
if entry['type'] in (
'Death_Match.DeathMatchGame',
'bsDeathMatch.DeathMatchGame',
'bs_death_match.DeathMatchGame',
'bastd.game.deathmatch.DeathMatchGame',
):
entry['type'] = 'bascenev1lib.game.deathmatch.DeathMatchGame'
if entry['type'] in (
'ChosenOne.ChosenOneGame',
'bsChosenOne.ChosenOneGame',
'bs_chosen_one.ChosenOneGame',
'bastd.game.chosenone.ChosenOneGame',
):
entry['type'] = 'bascenev1lib.game.chosenone.ChosenOneGame'
if entry['type'] in (
'Conquest.Conquest',
'Conquest.ConquestGame',
'bsConquest.ConquestGame',
'bs_conquest.ConquestGame',
'bastd.game.conquest.ConquestGame',
):
entry['type'] = 'bascenev1lib.game.conquest.ConquestGame'
if entry['type'] in (
'Elimination.EliminationGame',
'bsElimination.EliminationGame',
'bs_elimination.EliminationGame',
'bastd.game.elimination.EliminationGame',
):
entry['type'] = 'bascenev1lib.game.elimination.EliminationGame'
if entry['type'] in (
'Football.FootballGame',
'bsFootball.FootballTeamGame',
'bs_football.FootballTeamGame',
'bastd.game.football.FootballTeamGame',
):
entry['type'] = 'bascenev1lib.game.football.FootballTeamGame'
if entry['type'] in (
'Hockey.HockeyGame',
'bsHockey.HockeyGame',
'bs_hockey.HockeyGame',
'bastd.game.hockey.HockeyGame',
):
entry['type'] = 'bascenev1lib.game.hockey.HockeyGame'
if entry['type'] in (
'Keep_Away.KeepAwayGame',
'bsKeepAway.KeepAwayGame',
'bs_keep_away.KeepAwayGame',
'bastd.game.keepaway.KeepAwayGame',
):
entry['type'] = 'bascenev1lib.game.keepaway.KeepAwayGame'
if entry['type'] in (
'Race.RaceGame',
'bsRace.RaceGame',
'bs_race.RaceGame',
'bastd.game.race.RaceGame',
):
entry['type'] = 'bascenev1lib.game.race.RaceGame'
if entry['type'] in (
'bsEasterEggHunt.EasterEggHuntGame',
'bs_easter_egg_hunt.EasterEggHuntGame',
'bastd.game.easteregghunt.EasterEggHuntGame',
):
entry['type'] = (
'bascenev1lib.game.easteregghunt.EasterEggHuntGame'
)
if entry['type'] in (
'bsMeteorShower.MeteorShowerGame',
'bs_meteor_shower.MeteorShowerGame',
'bastd.game.meteorshower.MeteorShowerGame',
):
entry['type'] = (
'bascenev1lib.game.meteorshower.MeteorShowerGame'
)
if entry['type'] in (
'bsTargetPractice.TargetPracticeGame',
'bs_target_practice.TargetPracticeGame',
'bastd.game.targetpractice.TargetPracticeGame',
):
entry['type'] = (
'bascenev1lib.game.targetpractice.TargetPracticeGame'
)
gameclass = babase.getclass(entry['type'], GameActivity)
if entry['settings']['map'] not in available_maps:
raise babase.MapNotFoundError()
if remove_unowned and gameclass in unowned_game_types:
continue
if add_resolved_type:
entry['resolved_type'] = gameclass
if mark_unowned and entry['settings']['map'] in unowned_maps:
entry['is_unowned_map'] = True
if mark_unowned and gameclass in unowned_game_types:
entry['is_unowned_game'] = True
# Make sure all settings the game defines are present.
neededsettings = gameclass.get_available_settings(sessiontype)
for setting in neededsettings:
if setting.name not in entry['settings']:
entry['settings'][setting.name] = setting.default
goodlist.append(entry)
except babase.MapNotFoundError:
logging.warning(
'Map \'%s\' not found while scanning playlist \'%s\'.',
entry['settings']['map'],
name,
)
except ImportError as exc:
logging.warning(
'Import failed while scanning playlist \'%s\': %s', name, exc
)
except Exception:
logging.exception('Error in filter_playlist.')
return goodlist
[docs]
def get_default_free_for_all_playlist() -> PlaylistType:
"""Return a default playlist for free-for-all mode."""
# NOTE: these are currently using old type/map names,
# but filtering translates them properly to the new ones.
# (is kinda a handy way to ensure filtering is working).
# Eventually should update these though.
return [
{
'settings': {
'Epic Mode': False,
'Kills to Win Per Player': 10,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Doom Shroom',
},
'type': 'bs_death_match.DeathMatchGame',
},
{
'settings': {
'Chosen One Gets Gloves': True,
'Chosen One Gets Shield': False,
'Chosen One Time': 30,
'Epic Mode': 0,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Monkey Face',
},
'type': 'bs_chosen_one.ChosenOneGame',
},
{
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Zigzag',
},
'type': 'bs_king_of_the_hill.KingOfTheHillGame',
},
{
'settings': {'Epic Mode': False, 'map': 'Rampage'},
'type': 'bs_meteor_shower.MeteorShowerGame',
},
{
'settings': {
'Epic Mode': 1,
'Lives Per Player': 1,
'Respawn Times': 1.0,
'Time Limit': 120,
'map': 'Tip Top',
},
'type': 'bs_elimination.EliminationGame',
},
{
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'The Pad',
},
'type': 'bs_keep_away.KeepAwayGame',
},
{
'settings': {
'Epic Mode': True,
'Kills to Win Per Player': 10,
'Respawn Times': 0.25,
'Time Limit': 120,
'map': 'Rampage',
},
'type': 'bs_death_match.DeathMatchGame',
},
{
'settings': {
'Bomb Spawning': 1000,
'Epic Mode': False,
'Laps': 3,
'Mine Spawn Interval': 4000,
'Mine Spawning': 4000,
'Time Limit': 300,
'map': 'Big G',
},
'type': 'bs_race.RaceGame',
},
{
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Happy Thoughts',
},
'type': 'bs_king_of_the_hill.KingOfTheHillGame',
},
{
'settings': {
'Enable Impact Bombs': 1,
'Enable Triple Bombs': False,
'Target Count': 2,
'map': 'Doom Shroom',
},
'type': 'bs_target_practice.TargetPracticeGame',
},
{
'settings': {
'Epic Mode': False,
'Lives Per Player': 5,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Step Right Up',
},
'type': 'bs_elimination.EliminationGame',
},
{
'settings': {
'Epic Mode': False,
'Kills to Win Per Player': 10,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Crag Castle',
},
'type': 'bs_death_match.DeathMatchGame',
},
{
'map': 'Lake Frigid',
'settings': {
'Bomb Spawning': 0,
'Epic Mode': False,
'Laps': 6,
'Mine Spawning': 2000,
'Time Limit': 300,
'map': 'Lake Frigid',
},
'type': 'bs_race.RaceGame',
},
]
[docs]
def get_default_teams_playlist() -> PlaylistType:
"""Return a default playlist for teams mode."""
# NOTE: these are currently using old type/map names,
# but filtering translates them properly to the new ones.
# (is kinda a handy way to ensure filtering is working).
# Eventually should update these though.
return [
{
'settings': {
'Epic Mode': False,
'Flag Idle Return Time': 30,
'Flag Touch Return Time': 0,
'Respawn Times': 1.0,
'Score to Win': 3,
'Time Limit': 600,
'map': 'Bridgit',
},
'type': 'bs_capture_the_flag.CTFGame',
},
{
'settings': {
'Epic Mode': False,
'Respawn Times': 1.0,
'Score to Win': 3,
'Time Limit': 600,
'map': 'Step Right Up',
},
'type': 'bs_assault.AssaultGame',
},
{
'settings': {
'Balance Total Lives': False,
'Epic Mode': False,
'Lives Per Player': 3,
'Respawn Times': 1.0,
'Solo Mode': True,
'Time Limit': 600,
'map': 'Rampage',
},
'type': 'bs_elimination.EliminationGame',
},
{
'settings': {
'Epic Mode': False,
'Kills to Win Per Player': 5,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Roundabout',
},
'type': 'bs_death_match.DeathMatchGame',
},
{
'settings': {
'Respawn Times': 1.0,
'Score to Win': 1,
'Time Limit': 600,
'map': 'Hockey Stadium',
},
'type': 'bs_hockey.HockeyGame',
},
{
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Monkey Face',
},
'type': 'bs_keep_away.KeepAwayGame',
},
{
'settings': {
'Balance Total Lives': False,
'Epic Mode': True,
'Lives Per Player': 1,
'Respawn Times': 1.0,
'Solo Mode': False,
'Time Limit': 120,
'map': 'Tip Top',
},
'type': 'bs_elimination.EliminationGame',
},
{
'settings': {
'Epic Mode': False,
'Respawn Times': 1.0,
'Score to Win': 3,
'Time Limit': 300,
'map': 'Crag Castle',
},
'type': 'bs_assault.AssaultGame',
},
{
'settings': {
'Epic Mode': False,
'Kills to Win Per Player': 5,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Doom Shroom',
},
'type': 'bs_death_match.DeathMatchGame',
},
{
'settings': {'Epic Mode': False, 'map': 'Rampage'},
'type': 'bs_meteor_shower.MeteorShowerGame',
},
{
'settings': {
'Epic Mode': False,
'Flag Idle Return Time': 30,
'Flag Touch Return Time': 0,
'Respawn Times': 1.0,
'Score to Win': 2,
'Time Limit': 600,
'map': 'Roundabout',
},
'type': 'bs_capture_the_flag.CTFGame',
},
{
'settings': {
'Respawn Times': 1.0,
'Score to Win': 21,
'Time Limit': 600,
'map': 'Football Stadium',
},
'type': 'bs_football.FootballTeamGame',
},
{
'settings': {
'Epic Mode': True,
'Respawn Times': 0.25,
'Score to Win': 3,
'Time Limit': 120,
'map': 'Bridgit',
},
'type': 'bs_assault.AssaultGame',
},
{
'map': 'Doom Shroom',
'settings': {
'Enable Impact Bombs': 1,
'Enable Triple Bombs': False,
'Target Count': 2,
'map': 'Doom Shroom',
},
'type': 'bs_target_practice.TargetPracticeGame',
},
{
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Tip Top',
},
'type': 'bs_king_of_the_hill.KingOfTheHillGame',
},
{
'settings': {
'Epic Mode': False,
'Respawn Times': 1.0,
'Score to Win': 2,
'Time Limit': 300,
'map': 'Zigzag',
},
'type': 'bs_assault.AssaultGame',
},
{
'settings': {
'Epic Mode': False,
'Flag Idle Return Time': 30,
'Flag Touch Return Time': 0,
'Respawn Times': 1.0,
'Score to Win': 3,
'Time Limit': 300,
'map': 'Happy Thoughts',
},
'type': 'bs_capture_the_flag.CTFGame',
},
{
'settings': {
'Bomb Spawning': 1000,
'Epic Mode': True,
'Laps': 1,
'Mine Spawning': 2000,
'Time Limit': 300,
'map': 'Big G',
},
'type': 'bs_race.RaceGame',
},
{
'settings': {
'Epic Mode': False,
'Kills to Win Per Player': 5,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Monkey Face',
},
'type': 'bs_death_match.DeathMatchGame',
},
{
'settings': {
'Hold Time': 30,
'Respawn Times': 1.0,
'Time Limit': 300,
'map': 'Lake Frigid',
},
'type': 'bs_keep_away.KeepAwayGame',
},
{
'settings': {
'Epic Mode': False,
'Flag Idle Return Time': 30,
'Flag Touch Return Time': 3,
'Respawn Times': 1.0,
'Score to Win': 2,
'Time Limit': 300,
'map': 'Tip Top',
},
'type': 'bs_capture_the_flag.CTFGame',
},
{
'settings': {
'Balance Total Lives': False,
'Epic Mode': False,
'Lives Per Player': 3,
'Respawn Times': 1.0,
'Solo Mode': False,
'Time Limit': 300,
'map': 'Crag Castle',
},
'type': 'bs_elimination.EliminationGame',
},
{
'settings': {
'Epic Mode': True,
'Respawn Times': 0.25,
'Time Limit': 120,
'map': 'Zigzag',
},
'type': 'bs_conquest.ConquestGame',
},
]
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
Generator, Awaitable, Sequence, Self)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum