mirror of
https://github.com/minetest/contentdb.git
synced 2024-12-22 14:02:24 +01:00
parent
765b5603c1
commit
d529634b7f
@ -16,15 +16,17 @@
|
|||||||
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from celery import group
|
from celery import group
|
||||||
from flask import redirect, url_for, flash, current_app
|
from flask import redirect, url_for, flash, current_app, jsonify
|
||||||
from sqlalchemy import or_, and_
|
from sqlalchemy import or_, and_
|
||||||
|
|
||||||
|
from app.logic.game_support import GameSupportResolver
|
||||||
from app.models import PackageRelease, db, Package, PackageState, PackageScreenshot, MetaPackage, User, \
|
from app.models import PackageRelease, db, Package, PackageState, PackageScreenshot, MetaPackage, User, \
|
||||||
NotificationType, PackageUpdateConfig, License, UserRank, PackageType
|
NotificationType, PackageUpdateConfig, License, UserRank, PackageType, PackageGameSupport
|
||||||
from app.tasks.forumtasks import importTopicList, checkAllForumAccounts
|
from app.tasks.forumtasks import importTopicList, checkAllForumAccounts
|
||||||
from app.tasks.importtasks import importRepoScreenshot, checkZipRelease, check_for_updates
|
from app.tasks.importtasks import importRepoScreenshot, checkZipRelease, check_for_updates
|
||||||
from app.utils import addNotification, get_system_user
|
from app.utils import addNotification, get_system_user
|
||||||
@ -321,3 +323,10 @@ def update_screenshot_sizes():
|
|||||||
screenshot.height = height
|
screenshot.height = height
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@action("Detect game support")
|
||||||
|
def detect_game_support():
|
||||||
|
resolver = GameSupportResolver()
|
||||||
|
resolver.update_all()
|
||||||
|
db.session.commit()
|
||||||
|
161
app/logic/game_support.py
Normal file
161
app/logic/game_support.py
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
# 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from typing import List, Dict
|
||||||
|
|
||||||
|
from app.logic.LogicError import LogicError
|
||||||
|
from app.models import Package, MetaPackage, PackageType, PackageState, PackageGameSupport, db
|
||||||
|
|
||||||
|
"""
|
||||||
|
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 meta package:
|
||||||
|
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 = {
|
||||||
|
"repixture", "tutorial", "runorfall", "realtest_mt5", "mevo", "xaenvironment",
|
||||||
|
"survivethedays"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class GameSupportResolver:
|
||||||
|
checked_packages = set()
|
||||||
|
checked_metapackages = set()
|
||||||
|
resolved_packages = {}
|
||||||
|
resolved_metapackages = {}
|
||||||
|
|
||||||
|
def resolve_for_meta_package(self, meta: MetaPackage, history: List[str]) -> set[Package]:
|
||||||
|
print(f"Resolving for {meta.name}", file=sys.stderr)
|
||||||
|
|
||||||
|
key = meta.name
|
||||||
|
if key in self.resolved_metapackages:
|
||||||
|
return self.resolved_metapackages.get(key)
|
||||||
|
|
||||||
|
if key in self.checked_metapackages:
|
||||||
|
print(f"Error, cycle found: {','.join(history)}", file=sys.stderr)
|
||||||
|
return set()
|
||||||
|
|
||||||
|
self.checked_metapackages.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_metapackages[key] = retval
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def resolve(self, package: Package, history: List[str]) -> set[Package]:
|
||||||
|
key = "{}/{}".format(package.author.username.lower(), package.name)
|
||||||
|
print(f"Resolving for {key}", file=sys.stderr)
|
||||||
|
|
||||||
|
history = history.copy()
|
||||||
|
history.append(key)
|
||||||
|
|
||||||
|
if package.type == PackageType.GAME:
|
||||||
|
return {package}
|
||||||
|
|
||||||
|
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 len(history) >= 50:
|
||||||
|
raise LogicError(500, f"Too deep! {', '.join(history)}")
|
||||||
|
|
||||||
|
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"Conflict! Supported games narrowed at {key}")
|
||||||
|
|
||||||
|
self.resolved_packages[key] = retval
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def update_all(self) -> None:
|
||||||
|
for package in Package.query.filter_by(type=PackageType.MOD, state=PackageState.APPROVED).all():
|
||||||
|
retval = self.resolve(package, [])
|
||||||
|
for game in retval:
|
||||||
|
support = PackageGameSupport(package, game)
|
||||||
|
db.session.add(support)
|
||||||
|
|
||||||
|
def update(self, package: Package) -> None:
|
||||||
|
previous_supported: Dict[Package, PackageGameSupport] = {}
|
||||||
|
for support in package.supported_games.all():
|
||||||
|
previous_supported[support.game] = support
|
||||||
|
|
||||||
|
retval = self.resolve(package, [])
|
||||||
|
for game in retval:
|
||||||
|
lookup = previous_supported.pop(game, None)
|
||||||
|
if lookup:
|
||||||
|
if lookup.confidence == 0:
|
||||||
|
lookup.supports = True
|
||||||
|
db.session.merge(lookup)
|
||||||
|
else:
|
||||||
|
support = PackageGameSupport(package, game)
|
||||||
|
db.session.add(support)
|
||||||
|
|
||||||
|
for game, support in previous_supported.items():
|
||||||
|
if support.confidence == 0:
|
||||||
|
db.session.remove(support)
|
@ -344,6 +344,25 @@ class Dependency(db.Model):
|
|||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
class PackageGameSupport(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
|
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=False)
|
||||||
|
package = db.relationship("Package", foreign_keys=[package_id])
|
||||||
|
|
||||||
|
game_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=False)
|
||||||
|
game = db.relationship("Package", foreign_keys=[game_id])
|
||||||
|
|
||||||
|
supports = db.Column(db.Boolean, nullable=False, default=True)
|
||||||
|
confidence = db.Column(db.Integer, nullable=False, default=0)
|
||||||
|
|
||||||
|
__table_args__ = (db.UniqueConstraint("game_id", "package_id", name="_package_game_support_uc"),)
|
||||||
|
|
||||||
|
def __init__(self, package, game):
|
||||||
|
self.package = package
|
||||||
|
self.game = game
|
||||||
|
|
||||||
|
|
||||||
class Package(db.Model):
|
class Package(db.Model):
|
||||||
query_class = PackageQuery
|
query_class = PackageQuery
|
||||||
|
|
||||||
@ -396,6 +415,12 @@ class Package(db.Model):
|
|||||||
|
|
||||||
dependencies = db.relationship("Dependency", back_populates="depender", lazy="dynamic", foreign_keys=[Dependency.depender_id])
|
dependencies = db.relationship("Dependency", back_populates="depender", lazy="dynamic", foreign_keys=[Dependency.depender_id])
|
||||||
|
|
||||||
|
supported_games = db.relationship("PackageGameSupport", back_populates="package", lazy="dynamic",
|
||||||
|
foreign_keys=[PackageGameSupport.package_id])
|
||||||
|
|
||||||
|
game_supported_mods = db.relationship("PackageGameSupport", back_populates="game", lazy="dynamic",
|
||||||
|
foreign_keys=[PackageGameSupport.game_id])
|
||||||
|
|
||||||
tags = db.relationship("Tag", secondary=Tags, back_populates="packages")
|
tags = db.relationship("Tag", secondary=Tags, back_populates="packages")
|
||||||
|
|
||||||
content_warnings = db.relationship("ContentWarning", secondary=ContentWarnings, back_populates="packages")
|
content_warnings = db.relationship("ContentWarning", secondary=ContentWarnings, back_populates="packages")
|
||||||
@ -471,6 +496,11 @@ class Package(db.Model):
|
|||||||
def getSortedOptionalDependencies(self):
|
def getSortedOptionalDependencies(self):
|
||||||
return self.getSortedDependencies(False)
|
return self.getSortedDependencies(False)
|
||||||
|
|
||||||
|
def getSortedSupportedGames(self):
|
||||||
|
supported = self.supported_games.all()
|
||||||
|
supported.sort(key=lambda x: -x.game.score)
|
||||||
|
return supported
|
||||||
|
|
||||||
def getAsDictionaryKey(self):
|
def getAsDictionaryKey(self):
|
||||||
return {
|
return {
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os, shutil, gitdb
|
import os, shutil, gitdb
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
@ -22,10 +23,11 @@ from kombu import uuid
|
|||||||
|
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from app.tasks import celery, TaskError
|
from app.tasks import celery, TaskError
|
||||||
from app.utils import randomString, post_bot_message, addSystemNotification, addSystemAuditLog, get_system_user
|
from app.utils import randomString, post_bot_message, addSystemNotification, addSystemAuditLog
|
||||||
from app.utils.git import clone_repo, get_latest_tag, get_latest_commit, get_temp_dir
|
from app.utils.git import clone_repo, get_latest_tag, get_latest_commit, get_temp_dir
|
||||||
from .minetestcheck import build_tree, MinetestCheckError, ContentType
|
from .minetestcheck import build_tree, MinetestCheckError, ContentType
|
||||||
from ..logic.LogicError import LogicError
|
from ..logic.LogicError import LogicError
|
||||||
|
from ..logic.game_support import GameSupportResolver
|
||||||
from ..logic.packages import do_edit_package, ALIASES
|
from ..logic.packages import do_edit_package, ALIASES
|
||||||
from ..utils.image import get_image_size
|
from ..utils.image import get_image_size
|
||||||
|
|
||||||
@ -113,6 +115,10 @@ def postReleaseCheckUpdate(self, release: PackageRelease, path):
|
|||||||
for meta in getMetaPackages(optional_depends):
|
for meta in getMetaPackages(optional_depends):
|
||||||
db.session.add(Dependency(package, meta=meta, optional=True))
|
db.session.add(Dependency(package, meta=meta, optional=True))
|
||||||
|
|
||||||
|
# Update game supports
|
||||||
|
resolver = GameSupportResolver()
|
||||||
|
resolver.update(package)
|
||||||
|
|
||||||
# Update min/max
|
# Update min/max
|
||||||
if tree.meta.get("min_minetest_version"):
|
if tree.meta.get("min_minetest_version"):
|
||||||
release.min_rel = MinetestRelease.get(tree.meta["min_minetest_version"], None)
|
release.min_rel = MinetestRelease.get(tree.meta["min_minetest_version"], None)
|
||||||
|
@ -419,6 +419,19 @@
|
|||||||
</dl>
|
</dl>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if package.type == package.type.MOD %}
|
||||||
|
<h3>{{ _("Supported Games") }}</h3>
|
||||||
|
{% for support in package.getSortedSupportedGames() %}
|
||||||
|
<a class="badge badge-primary"
|
||||||
|
href="{{ support.game.getURL('packages.view') }}">
|
||||||
|
{{ _("%(title)s by %(display_name)s",
|
||||||
|
title=support.game.title, display_name=support.game.author.display_name) }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
{{ _("No specifc game is required") }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<h3>
|
<h3>
|
||||||
{{ _("Information") }}
|
{{ _("Information") }}
|
||||||
</h3>
|
</h3>
|
||||||
|
34
migrations/versions/e571b3498f9e_.py
Normal file
34
migrations/versions/e571b3498f9e_.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: e571b3498f9e
|
||||||
|
Revises: 3710e5fbbe87
|
||||||
|
Create Date: 2022-02-01 19:30:59.537512
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'e571b3498f9e'
|
||||||
|
down_revision = '3710e5fbbe87'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.create_table('package_game_support',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('package_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('game_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('supports', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('confidence', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['game_id'], ['package.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('game_id', 'package_id', name='_package_game_support_uc')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_table('package_game_support')
|
Loading…
Reference in New Issue
Block a user