# ContentDB # Copyright (C) 2022 rubenwardy # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import sys from typing import List, Dict, Optional, Iterable import sqlalchemy.orm from app.logic.LogicError import LogicError from app.models import Package, MetaPackage, PackageType, PackageState, PackageGameSupport """ get_game_support(package): if package is a game: return [ package ] for all hard dependencies: support = support AND get_meta_package_support(dep) return support get_meta_package_support(meta): for package implementing mod name: support = support OR get_game_support(package) return support """ minetest_game_mods = { "beds", "boats", "bucket", "carts", "default", "dungeon_loot", "env_sounds", "fire", "flowers", "give_initial_stuff", "map", "player_api", "sethome", "spawn", "tnt", "walls", "wool", "binoculars", "bones", "butterflies", "creative", "doors", "dye", "farming", "fireflies", "game_commands", "keys", "mtg_craftguide", "screwdriver", "sfinv", "stairs", "vessels", "weather", "xpanes", } mtg_mod_blacklist = { "pacman", "tutorial", "runorfall", "realtest_mt5", "mevo", "xaenvironment", "survivethedays", "holidayhorrors", } class GameSupportResolver: session: sqlalchemy.orm.Session checked_packages = set() checked_modnames = set() resolved_packages: Dict[int, set[int]] = {} resolved_modnames: Dict[int, set[int]] = {} def __init__(self, session): self.session = session def resolve_for_meta_package(self, meta: MetaPackage, history: List[str]) -> set[int]: print(f"Resolving for {meta.name}", file=sys.stderr) key = meta.name if key in self.resolved_modnames: return self.resolved_modnames.get(key) if key in self.checked_modnames: print(f"Error, cycle found: {','.join(history)}", file=sys.stderr) return set() self.checked_modnames.add(key) retval = set() for package in meta.packages: if package.state != PackageState.APPROVED: continue if meta.name in minetest_game_mods and package.name in mtg_mod_blacklist: continue ret = self.resolve(package, history) if len(ret) == 0: retval = set() break retval.update(ret) self.resolved_modnames[key] = retval return retval def resolve(self, package: Package, history: List[str]) -> set[int]: key = package.id print(f"Resolving for {key}", file=sys.stderr) history = history.copy() history.append(key) if package.type == PackageType.GAME: return {package.id} if key in self.resolved_packages: return self.resolved_packages.get(key) if key in self.checked_packages: print(f"Error, cycle found: {','.join(history)}", file=sys.stderr) return set() self.checked_packages.add(key) if package.type != PackageType.MOD: raise LogicError(500, "Got non-mod") retval = set() for dep in package.dependencies.filter_by(optional=False).all(): ret = self.resolve_for_meta_package(dep.meta_package, history) if len(ret) == 0: continue elif len(retval) == 0: retval.update(ret) else: retval.intersection_update(ret) if len(retval) == 0: raise LogicError(500, f"Detected game support contradiction, {key} may not be compatible with any games") self.resolved_packages[key] = retval return retval def update_all(self) -> None: for package in self.session.query(Package).filter(Package.type == PackageType.MOD, Package.state != PackageState.DELETED).all(): retval = self.resolve(package, []) for game_id in retval: game = self.session.query(Package).get(game_id) support = PackageGameSupport(package, game, 1, True) self.session.add(support) """ Update game supported package on a package, given the confidence. Higher confidences outweigh lower ones. """ def set_supported(self, package: Package, game_is_supported: Dict[int, bool], confidence: int): previous_supported: Dict[int, PackageGameSupport] = {} for support in package.supported_games.all(): previous_supported[support.game.id] = support for game_id, supports in game_is_supported.items(): game = self.session.query(Package).get(game_id) lookup = previous_supported.pop(game_id, None) if lookup is None: support = PackageGameSupport(package, game, confidence, supports) self.session.add(support) elif lookup.confidence <= confidence: lookup.supports = supports lookup.confidence = confidence for game, support in previous_supported.items(): if support.confidence == confidence: self.session.delete(support) def update(self, package: Package) -> None: game_is_supported = {} if package.enable_game_support_detection: retval = self.resolve(package, []) for game_id in retval: game_is_supported[game_id] = True self.set_supported(package, game_is_supported, 1)