Add supports_all_games to make game support explicit

Fixes #388 and fixes #441
This commit is contained in:
rubenwardy 2023-06-18 21:11:17 +01:00
parent cb352fad47
commit c498818e8b
11 changed files with 232 additions and 44 deletions

@ -140,8 +140,6 @@ def remind_wip():
packages = [pkg[0] for pkg in packages] packages = [pkg[0] for pkg in packages]
packages_list = _package_list(packages) packages_list = _package_list(packages)
havent = "haven't" if len(packages) > 1 else "hasn't" havent = "haven't" if len(packages) > 1 else "hasn't"
if len(packages_list) + 54 > 100:
packages_list = packages_list[0:(100-54-1)] + ""
addNotification(user, system_user, NotificationType.PACKAGE_APPROVAL, addNotification(user, system_user, NotificationType.PACKAGE_APPROVAL,
f"Did you forget? {packages_list} {havent} been submitted for review yet", f"Did you forget? {packages_list} {havent} been submitted for review yet",
@ -156,7 +154,7 @@ def remind_outdated():
system_user = get_system_user() system_user = get_system_user()
for user in users: for user in users:
packages = db.session.query(Package.title).filter( packages = db.session.query(Package.title).filter(
Package.maintainers.any(User.id==user.id), Package.maintainers.contains(user),
Package.update_config.has(PackageUpdateConfig.outdated_at.isnot(None))) \ Package.update_config.has(PackageUpdateConfig.outdated_at.isnot(None))) \
.all() .all()
@ -234,7 +232,7 @@ def remind_video_url():
system_user = get_system_user() system_user = get_system_user()
for user in users: for user in users:
packages = db.session.query(Package.title).filter( packages = db.session.query(Package.title).filter(
or_(Package.author==user, Package.maintainers.any(User.id==user.id)), or_(Package.author==user, Package.maintainers.contains(user)),
Package.video_url==None, Package.video_url==None,
Package.type == PackageType.GAME, Package.type == PackageType.GAME,
Package.state == PackageState.APPROVED) \ Package.state == PackageState.APPROVED) \
@ -250,6 +248,35 @@ def remind_video_url():
db.session.commit() db.session.commit()
@action("Send missing game support notifications")
def remind_missing_game_support():
users = User.query.filter(
User.maintained_packages.any(and_(
Package.state != PackageState.DELETED,
Package.type.in_([PackageType.MOD, PackageType.TXP]),
~Package.supported_games.any(),
Package.supports_all_games == False))).all()
system_user = get_system_user()
for user in users:
packages = db.session.query(Package.title).filter(
Package.maintainers.contains(user),
Package.state != PackageState.DELETED,
Package.type.in_([PackageType.MOD, PackageType.TXP]),
~Package.supported_games.any(),
Package.supports_all_games == False) \
.all()
packages = [pkg[0] for pkg in packages]
packages_list = _package_list(packages)
addNotification(user, system_user, NotificationType.PACKAGE_APPROVAL,
f"You need to confirm whether the following packages support all games: {packages_list}",
url_for('todo.all_game_support', username=user.username))
db.session.commit()
@action("Detect game support") @action("Detect game support")
def detect_game_support(): def detect_game_support():
task_id = uuid() task_id = uuid()

@ -230,7 +230,7 @@ def list_all_releases():
if maintainer is None: if maintainer is None:
error(404, "Maintainer not found") error(404, "Maintainer not found")
query = query.join(Package) query = query.join(Package)
query = query.filter(Package.maintainers.any(id=maintainer.id)) query = query.filter(Package.maintainers.contains(maintainer))
return jsonify([ rel.getLongAsDictionary() for rel in query.limit(30).all() ]) return jsonify([ rel.getLongAsDictionary() for rel in query.limit(30).all() ])

@ -644,6 +644,7 @@ class GameSupportForm(FlaskForm):
enable_support_detection = BooleanField(lazy_gettext("Enable support detection based on dependencies (recommended)"), [Optional()]) enable_support_detection = BooleanField(lazy_gettext("Enable support detection based on dependencies (recommended)"), [Optional()])
supported = StringField(lazy_gettext("Supported games (Comma-separated)"), [Optional()]) supported = StringField(lazy_gettext("Supported games (Comma-separated)"), [Optional()])
unsupported = StringField(lazy_gettext("Unsupported games (Comma-separated)"), [Optional()]) unsupported = StringField(lazy_gettext("Unsupported games (Comma-separated)"), [Optional()])
supports_all_games = BooleanField(lazy_gettext("Supports all games (unless stated)"), [Optional()])
submit = SubmitField(lazy_gettext("Save")) submit = SubmitField(lazy_gettext("Save"))
@ -661,17 +662,25 @@ def game_support(package):
force_game_detection = package.supported_games.filter(and_( force_game_detection = package.supported_games.filter(and_(
PackageGameSupport.confidence > 1, PackageGameSupport.supports == True)).count() == 0 PackageGameSupport.confidence > 1, PackageGameSupport.supports == True)).count() == 0
can_override = can_edit and current_user not in package.maintainers
form = GameSupportForm() if can_edit else None form = GameSupportForm() if can_edit else None
if form and request.method == "GET": if form and request.method == "GET":
form.enable_support_detection.data = package.enable_game_support_detection form.enable_support_detection.data = package.enable_game_support_detection
manual_supported_games = package.supported_games.filter_by(confidence=11).all() form.supports_all_games.data = package.supports_all_games
form.supported.data = ", ".join([x.game.name for x in manual_supported_games if x.supports])
form.unsupported.data = ", ".join([x.game.name for x in manual_supported_games if not x.supports]) if can_override:
manual_supported_games = package.supported_games.filter_by(confidence=11).all()
form.supported.data = ", ".join([x.game.name for x in manual_supported_games if x.supports])
form.unsupported.data = ", ".join([x.game.name for x in manual_supported_games if not x.supports])
else:
form.supported = None
form.unsupported = None
if form and form.validate_on_submit(): if form and form.validate_on_submit():
detect_update_needed = False detect_update_needed = False
if current_user not in package.maintainers: if can_override:
try: try:
resolver = GameSupportResolver(db.session) resolver = GameSupportResolver(db.session)
@ -695,6 +704,8 @@ def game_support(package):
else: else:
package.supported_games.filter_by(confidence=1).delete() package.supported_games.filter_by(confidence=1).delete()
package.supports_all_games = form.supports_all_games.data
db.session.commit() db.session.commit()
if detect_update_needed: if detect_update_needed:
@ -708,7 +719,10 @@ def game_support(package):
all_game_support = package.supported_games.all() all_game_support = package.supported_games.all()
all_game_support.sort(key=lambda x: -x.game.score) all_game_support.sort(key=lambda x: -x.game.score)
supported_games = ", ".join([x.game.name for x in all_game_support if x.supports]) supported_games_list: List[str] = [x.game.name for x in all_game_support if x.supports]
if package.supports_all_games:
supported_games_list.insert(0, "*")
supported_games = ", ".join(supported_games_list)
unsupported_games = ", ".join([x.game.name for x in all_game_support if not x.supports]) unsupported_games = ", ".join([x.game.name for x in all_game_support if not x.supports])
mod_conf_lines = "" mod_conf_lines = ""

@ -15,7 +15,8 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from celery import uuid from celery import uuid
from flask import redirect, url_for, abort, render_template from flask import redirect, url_for, abort, render_template, flash
from flask_babel import gettext
from flask_login import current_user, login_required from flask_login import current_user, login_required
from sqlalchemy import or_, and_ from sqlalchemy import or_, and_
@ -23,7 +24,6 @@ from app.models import User, Package, PackageState, PackageScreenshot, PackageUp
PackageRelease, Permission, NotificationType, AuditSeverity, UserRank, PackageType PackageRelease, Permission, NotificationType, AuditSeverity, UserRank, PackageType
from app.tasks.importtasks import makeVCSRelease from app.tasks.importtasks import makeVCSRelease
from app.utils import addNotification, addAuditLog from app.utils import addNotification, addAuditLog
from . import bp from . import bp
@ -59,7 +59,8 @@ def view_user(username=None):
missing_game_support = user.maintained_packages.filter( missing_game_support = user.maintained_packages.filter(
Package.state != PackageState.DELETED, Package.state != PackageState.DELETED,
Package.type.in_([PackageType.MOD, PackageType.TXP]), Package.type.in_([PackageType.MOD, PackageType.TXP]),
~Package.supported_games.any()) \ ~Package.supported_games.any(),
Package.supports_all_games == False) \
.order_by(db.asc(Package.title)).all() .order_by(db.asc(Package.title)).all()
packages_with_no_screenshots = user.maintained_packages.filter( packages_with_no_screenshots = user.maintained_packages.filter(
@ -152,3 +153,22 @@ def all_game_support(username=None):
.order_by(db.asc(Package.title)).all() .order_by(db.asc(Package.title)).all()
return render_template("todo/game_support.html", user=user, packages=packages) return render_template("todo/game_support.html", user=user, packages=packages)
@bp.route("/users/<username>/confirm_supports_all_games/", methods=["POST"])
@login_required
def confirm_supports_all_games(username=None):
user: User = User.query.filter_by(username=username).one_or_404()
if current_user != user and not current_user.rank.atLeast(UserRank.EDITOR):
abort(403)
db.session.query(Package).filter(
Package.maintainers.contains(user),
Package.state != PackageState.DELETED,
Package.type.in_([PackageType.MOD, PackageType.TXP]),
~Package.supported_games.any(supports=True)).update({ "supports_all_games": True })
db.session.commit()
flash(gettext("Done"), "success")
return redirect(url_for("todo.all_game_support", username=current_user.username))

@ -12,7 +12,7 @@ user experience.
## Support sources ## Support sources
### mod.conf ### mod.conf / texture_pack.conf
You can use `supported_games` to specify games that your mod is compatible with. You can use `supported_games` to specify games that your mod is compatible with.
@ -24,6 +24,16 @@ Both of these are comma-separated lists of game technical ids. Any `_game` suffi
supported_games = minetest_game, repixture supported_games = minetest_game, repixture
unsupported_games = lordofthetest, nodecore, whynot unsupported_games = lordofthetest, nodecore, whynot
If your package supports all games by default, you can put "*` in supported_games.
You can still use unsupported_games to mark games as unsupported.
You can also specify games that you've tested in supported_games.
# Should work with all games but I've only tested using Minetest Game:
supported_games = *, minetest_games
# But doesn't work in capturetheflag
unsupported_game = capturetheflag
### Dependencies ### Dependencies
ContentDB will analyse hard dependencies and work out which games a mod supports. ContentDB will analyse hard dependencies and work out which games a mod supports.

@ -17,6 +17,7 @@
import datetime import datetime
import enum import enum
import typing
from flask import url_for from flask import url_for
from flask_babel import lazy_gettext from flask_babel import lazy_gettext
@ -410,6 +411,9 @@ class Package(db.Model):
review_thread = db.relationship("Thread", uselist=False, foreign_keys=[review_thread_id], review_thread = db.relationship("Thread", uselist=False, foreign_keys=[review_thread_id],
back_populates="is_review_thread", post_update=True) back_populates="is_review_thread", post_update=True)
# Supports all games by default, may have unsupported games
supports_all_games = db.Column(db.Boolean, nullable=False, default=False)
# Downloads # Downloads
repo = db.Column(db.String(200), nullable=True) repo = db.Column(db.String(200), nullable=True)
website = db.Column(db.String(200), nullable=True) website = db.Column(db.String(200), nullable=True)
@ -520,15 +524,24 @@ class Package(db.Model):
def getSortedOptionalDependencies(self): def getSortedOptionalDependencies(self):
return self.getSortedDependencies(False) return self.getSortedDependencies(False)
def getSortedSupportedGames(self, include_unsupported=False): def get_sorted_game_support(self):
query = self.supported_games query = self.supported_games.filter(PackageGameSupport.game.has(state=PackageState.APPROVED))
if not include_unsupported:
query = query.filter(PackageGameSupport.game.has(state=PackageState.APPROVED)).filter_by(supports=True)
supported = query.all() supported = query.all()
supported.sort(key=lambda x: -(x.game.score + 100000*x.confidence)) supported.sort(key=lambda x: -(x.game.score + 100000*x.confidence))
return supported return supported
def get_sorted_game_support_pair(self):
supported = self.get_sorted_game_support()
return [
[x for x in supported if x.supports],
[x for x in supported if not x.supports],
]
def has_game_support_confirmed(self):
return self.supports_all_games or \
self.supported_games.filter(PackageGameSupport.confidence > 1).count() > 0
def getAsDictionaryKey(self): def getAsDictionaryKey(self):
return { return {
"name": self.name, "name": self.name,

@ -80,7 +80,7 @@ def getMeta(urlstr, author):
@celery.task() @celery.task()
def updateAllGameSupport(): def updateAllGameSupport():
resolver = GameSupportResolver(db.session) resolver = GameSupportResolver(db.session)
resolver.update_all() resolver.init_all()
db.session.commit() db.session.commit()
@ -153,6 +153,10 @@ def postReleaseCheckUpdate(self, release: PackageRelease, path):
if "supported_games" in tree.meta: if "supported_games" in tree.meta:
for game in get_games_from_csv(db.session, tree.meta["supported_games"]): for game in get_games_from_csv(db.session, tree.meta["supported_games"]):
game_is_supported[game.id] = True game_is_supported[game.id] = True
has_star = any(map(lambda x: x.strip() == "*", tree.meta["supported_games"].split(",")))
if has_star:
package.supports_all_games = True
if "unsupported_games" in tree.meta: if "unsupported_games" in tree.meta:
for game in get_games_from_csv(db.session, tree.meta["unsupported_games"]): for game in get_games_from_csv(db.session, tree.meta["unsupported_games"]):
game_is_supported[game.id] = False game_is_supported[game.id] = False

@ -9,9 +9,8 @@
{{ _("Documentation") }} {{ _("Documentation") }}
</a> </a>
<h2 class="mt-0">{{ self.title() }}</h2> <h2 class="mt-0">{{ self.title() }}</h2>
<p>
<p class="alert alert-warning"> {{ _("Game support is configured using the package's .conf file. See the documentation for more info") }}
This feature is experimental
</p> </p>
<div class="list-group"> <div class="list-group">
@ -29,7 +28,13 @@
</div> </div>
</div> </div>
{% for support in package.getSortedSupportedGames(True) %} {% if package.supports_all_games %}
<div class="list-group-item">
{{ _("Supports all games unless otherwise stated") }}
</div>
{% endif %}
{% for support in package.get_sorted_game_support() %}
<a class="list-group-item list-group-item-action" <a class="list-group-item list-group-item-action"
href="{{ support.game.getURL('packages.view') }}"> href="{{ support.game.getURL('packages.view') }}">
<div class="row"> <div class="row">
@ -83,7 +88,13 @@
</p> </p>
{% endif %} {% endif %}
{% if package.checkPerm(current_user, "EDIT_PACKAGE") and current_user not in package.maintainers %} {{ render_checkbox_field(form.supports_all_games) }}
<p class="text-muted">
{{ _("Unless otherwise stated, this package should work with all games.") }}
{{ _("You can check this and still specify games in supported_games that you've tested.") }}
</p>
{% if form.supported and form.unsupported %}
<h3> <h3>
{{ _("Editor Overrides") }} {{ _("Editor Overrides") }}
<i class="ml-2 fas fa-user-edit"></i> <i class="ml-2 fas fa-user-edit"></i>

@ -445,8 +445,11 @@
{% endif %} {% endif %}
{% if package.type == package.type.MOD or package.type == package.type.TXP %} {% if package.type == package.type.MOD or package.type == package.type.TXP %}
{% set supported_games = package.getSortedSupportedGames() %} {% set pair = package.get_sorted_game_support_pair() %}
{% if supported_games or package.type == package.type.MOD %} {% set supported_games = pair[0] %}
{% set unsupported_games = pair[1] %}
{% set show_unsupported = package.supports_all_games or supported_games == [] %}
{% if supported_games or unsupported_games or package.type == package.type.MOD %}
<h3> <h3>
{% if package.checkPerm(current_user, "EDIT_PACKAGE") %} {% if package.checkPerm(current_user, "EDIT_PACKAGE") %}
<a href="{{ package.getURL('packages.game_support') }}" class="btn btn-secondary btn-sm float-right"> <a href="{{ package.getURL('packages.game_support') }}" class="btn btn-secondary btn-sm float-right">
@ -455,19 +458,63 @@
{% endif %} {% endif %}
{{ _("Compatible Games") }} {{ _("Compatible Games") }}
</h3> </h3>
<div style="max-height: 300px; overflow: hidden auto;">
{% for support in supported_games %}
<a class="badge badge-secondary"
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 specific game is required") }}
{% endfor %}
</div>
{% if package.type == package.type.MOD %} {% if package.supports_all_games %}
<p>
{{ _("Should support most games.") }}
{% if supported_games %}
<br>
{{ _("Tested with:") }}
{% endif %}
</p>
{% endif %}
{% if supported_games %}
<div style="max-height: 300px; overflow: hidden auto;" class="mb-3">
{% for support in supported_games %}
<a class="badge badge-secondary"
href="{{ support.game.getURL('packages.view') }}">
{{ _("%(title)s by %(display_name)s",
title=support.game.title, display_name=support.game.author.display_name) }}
</a>
{% endfor %}
</div>
{% elif not package.supports_all_games %}
<p>
{{ _("No specific game required") }}
</p>
{% if package.checkPerm(current_user, "EDIT_PACKAGE") %}
<div class="alert alert-warning">
<p>
{{ _("Is the above correct?") }}
{{ _("You need to either confirm this or tell ContentDB about supported games") }}
</p>
<a class="btn btn-sm btn-warning" href="{{ package.getURL('packages.game_support') }}">
Update
</a>
</div>
{% endif %}
{% endif %}
{% if unsupported_games and show_unsupported %}
<p>
{{ _("Except for:") }}
</p>
<div style="max-height: 300px; overflow: hidden auto;">
{% for support in unsupported_games %}
<a class="badge badge-danger"
href="{{ support.game.getURL('packages.view') }}">
<i class="fas fa-times mr-1"></i>
{{ _("%(title)s by %(display_name)s",
title=support.game.title, display_name=support.game.author.display_name) }}
</a>
{% endfor %}
</div>
{% endif %}
{% if package.type == package.type.MOD and (supported_games or unsupported_games) and
not package.has_game_support_confirmed() %}
<p class="text-muted small mt-2 mb-0"> <p class="text-muted small mt-2 mb-0">
{{ _("This is an experimental feature.") }} {{ _("This is an experimental feature.") }}
{{ _("Supported games are determined by an algorithm, and may not be correct.") }} {{ _("Supported games are determined by an algorithm, and may not be correct.") }}

@ -10,6 +10,7 @@
<p> <p>
{{ _("You should specify the games supported by your mods and texture packs.") }} {{ _("You should specify the games supported by your mods and texture packs.") }}
{{ _("Specifying game support makes it easier for players to find your content.") }} {{ _("Specifying game support makes it easier for players to find your content.") }}
{{ _("If your package supports all games unless otherwise stated, confirm this using 'Supports all games'") }}
</p> </p>
@ -28,7 +29,15 @@
</span> </span>
</div> </div>
<div class="col"> <div class="col">
{% set supported_games = package.getSortedSupportedGames() %} {% if package.supports_all_games %}
<span class="text-muted pr-2">
<i>
{{ _("Supports all games") }}
</i>
</span>
{% endif %}
{% set supported_games = package.get_sorted_game_support_pair()[0] %}
{% if supported_games %} {% if supported_games %}
{% for support in supported_games %} {% for support in supported_games %}
<a class="badge badge-secondary" <a class="badge badge-secondary"
@ -37,11 +46,9 @@
title=support.game.title, display_name=support.game.author.display_name) }} title=support.game.title, display_name=support.game.author.display_name) }}
</a> </a>
{% endfor %} {% endfor %}
{% else %} {% elif not package.supports_all_games %}
<span class="text-muted"> <span class="text-danger">
<i> {{ _("No supported games listed. Please either add supported games or check 'Supports all games'") }}
{{ _("None listed, assumed to support all games") }}
</i>
</span> </span>
{% endif %} {% endif %}
</div> </div>
@ -55,4 +62,13 @@
<p class="text-muted">{{ _("Nothing to do :)") }}</p> <p class="text-muted">{{ _("Nothing to do :)") }}</p>
{% endfor %} {% endfor %}
</div> </div>
<h2>Bulk support all games</h2>
<p>
{{ _("Click the button below to confirm that all games without listed supported_games (red text above) do support all games, except for any games listed in unsupported_games.") }}
</p>
<form method="post" action="{{ url_for('todo.confirm_supports_all_games', username=user.username) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="submit" value="{{ _('Confirm') }}" class="btn btn-primary">
</form>
{% endblock %} {% endblock %}

@ -0,0 +1,26 @@
"""empty message
Revision ID: 2ecff2f9972d
Revises: 23afcf580aae
Create Date: 2023-06-18 07:51:42.581955
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2ecff2f9972d'
down_revision = '23afcf580aae'
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table('package', schema=None) as batch_op:
batch_op.add_column(sa.Column('supports_all_games', sa.Boolean(), nullable=False, server_default="false"))
def downgrade():
with op.batch_alter_table('package', schema=None) as batch_op:
batch_op.drop_column('supports_all_games')