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_list = _package_list(packages)
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,
f"Did you forget? {packages_list} {havent} been submitted for review yet",
@ -156,7 +154,7 @@ def remind_outdated():
system_user = get_system_user()
for user in users:
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))) \
.all()
@ -234,7 +232,7 @@ def remind_video_url():
system_user = get_system_user()
for user in users:
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.type == PackageType.GAME,
Package.state == PackageState.APPROVED) \
@ -250,6 +248,35 @@ def remind_video_url():
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")
def detect_game_support():
task_id = uuid()

@ -230,7 +230,7 @@ def list_all_releases():
if maintainer is None:
error(404, "Maintainer not found")
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() ])

@ -644,6 +644,7 @@ class GameSupportForm(FlaskForm):
enable_support_detection = BooleanField(lazy_gettext("Enable support detection based on dependencies (recommended)"), [Optional()])
supported = StringField(lazy_gettext("Supported 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"))
@ -661,17 +662,25 @@ def game_support(package):
force_game_detection = package.supported_games.filter(and_(
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
if form and request.method == "GET":
form.enable_support_detection.data = package.enable_game_support_detection
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])
form.supports_all_games.data = package.supports_all_games
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():
detect_update_needed = False
if current_user not in package.maintainers:
if can_override:
try:
resolver = GameSupportResolver(db.session)
@ -695,6 +704,8 @@ def game_support(package):
else:
package.supported_games.filter_by(confidence=1).delete()
package.supports_all_games = form.supports_all_games.data
db.session.commit()
if detect_update_needed:
@ -708,7 +719,10 @@ def game_support(package):
all_game_support = package.supported_games.all()
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])
mod_conf_lines = ""

@ -15,7 +15,8 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
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 sqlalchemy import or_, and_
@ -23,7 +24,6 @@ from app.models import User, Package, PackageState, PackageScreenshot, PackageUp
PackageRelease, Permission, NotificationType, AuditSeverity, UserRank, PackageType
from app.tasks.importtasks import makeVCSRelease
from app.utils import addNotification, addAuditLog
from . import bp
@ -59,7 +59,8 @@ def view_user(username=None):
missing_game_support = user.maintained_packages.filter(
Package.state != PackageState.DELETED,
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()
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()
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
### mod.conf
### mod.conf / texture_pack.conf
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
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
ContentDB will analyse hard dependencies and work out which games a mod supports.

@ -17,6 +17,7 @@
import datetime
import enum
import typing
from flask import url_for
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],
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
repo = 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):
return self.getSortedDependencies(False)
def getSortedSupportedGames(self, include_unsupported=False):
query = self.supported_games
if not include_unsupported:
query = query.filter(PackageGameSupport.game.has(state=PackageState.APPROVED)).filter_by(supports=True)
def get_sorted_game_support(self):
query = self.supported_games.filter(PackageGameSupport.game.has(state=PackageState.APPROVED))
supported = query.all()
supported.sort(key=lambda x: -(x.game.score + 100000*x.confidence))
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):
return {
"name": self.name,

@ -80,7 +80,7 @@ def getMeta(urlstr, author):
@celery.task()
def updateAllGameSupport():
resolver = GameSupportResolver(db.session)
resolver.update_all()
resolver.init_all()
db.session.commit()
@ -153,6 +153,10 @@ def postReleaseCheckUpdate(self, release: PackageRelease, path):
if "supported_games" in tree.meta:
for game in get_games_from_csv(db.session, tree.meta["supported_games"]):
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:
for game in get_games_from_csv(db.session, tree.meta["unsupported_games"]):
game_is_supported[game.id] = False

@ -9,9 +9,8 @@
{{ _("Documentation") }}
</a>
<h2 class="mt-0">{{ self.title() }}</h2>
<p class="alert alert-warning">
This feature is experimental
<p>
{{ _("Game support is configured using the package's .conf file. See the documentation for more info") }}
</p>
<div class="list-group">
@ -29,7 +28,13 @@
</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"
href="{{ support.game.getURL('packages.view') }}">
<div class="row">
@ -83,7 +88,13 @@
</p>
{% 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>
{{ _("Editor Overrides") }}
<i class="ml-2 fas fa-user-edit"></i>

@ -445,8 +445,11 @@
{% endif %}
{% if package.type == package.type.MOD or package.type == package.type.TXP %}
{% set supported_games = package.getSortedSupportedGames() %}
{% if supported_games or package.type == package.type.MOD %}
{% set pair = package.get_sorted_game_support_pair() %}
{% 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>
{% if package.checkPerm(current_user, "EDIT_PACKAGE") %}
<a href="{{ package.getURL('packages.game_support') }}" class="btn btn-secondary btn-sm float-right">
@ -455,19 +458,63 @@
{% endif %}
{{ _("Compatible Games") }}
</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">
{{ _("This is an experimental feature.") }}
{{ _("Supported games are determined by an algorithm, and may not be correct.") }}

@ -10,6 +10,7 @@
<p>
{{ _("You should specify the games supported by your mods and texture packs.") }}
{{ _("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>
@ -28,7 +29,15 @@
</span>
</div>
<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 %}
{% for support in supported_games %}
<a class="badge badge-secondary"
@ -37,11 +46,9 @@
title=support.game.title, display_name=support.game.author.display_name) }}
</a>
{% endfor %}
{% else %}
<span class="text-muted">
<i>
{{ _("None listed, assumed to support all games") }}
</i>
{% elif not package.supports_all_games %}
<span class="text-danger">
{{ _("No supported games listed. Please either add supported games or check 'Supports all games'") }}
</span>
{% endif %}
</div>
@ -55,4 +62,13 @@
<p class="text-muted">{{ _("Nothing to do :)") }}</p>
{% endfor %}
</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 %}

@ -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')