Skip to content
Merged
Show file tree
Hide file tree
Changes from 84 commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
b7db5ec
new fields, new command
K-ETFreeman Dec 6, 2024
adcc746
alpha
K-ETFreeman Dec 6, 2024
5933022
beta
K-ETFreeman Dec 25, 2024
5a3e05b
linter fixes + players.py bugfix
K-ETFreeman Dec 25, 2024
a2c42a5
linter
K-ETFreeman Dec 25, 2024
3c77c0b
linter
K-ETFreeman Dec 25, 2024
4ac5d49
Merge branch 'develop' into feature/#1032-working-veto-system
K-ETFreeman Dec 25, 2024
5bfea4f
optional map_pool_map_version_id in map class
K-ETFreeman Dec 26, 2024
998f03c
optional map_pool_map_version_id in NeroxisGeneratedMap class
K-ETFreeman Dec 26, 2024
dc8dc57
separated map version id from neroxis params
K-ETFreeman Dec 26, 2024
ec6a9f6
improvements
K-ETFreeman Jan 2, 2025
6f137fd
linter
K-ETFreeman Jan 2, 2025
c050a3c
Revert "linter"
K-ETFreeman Jan 2, 2025
515125d
linter
K-ETFreeman Jan 2, 2025
1b6a3b6
linter
K-ETFreeman Jan 2, 2025
8a822f7
commit before merge
K-ETFreeman Mar 22, 2025
a33db54
Merge branch 'develop' into feature/#1032-working-veto-system
K-ETFreeman Mar 22, 2025
2d22bb7
in progress
K-ETFreeman Mar 23, 2025
7dc151f
less garbage
K-ETFreeman Mar 23, 2025
638c661
linter
K-ETFreeman Mar 23, 2025
1e45324
asdfljknlskjfn
K-ETFreeman Mar 23, 2025
0de6ae8
fix
K-ETFreeman Mar 23, 2025
dc934d9
fix2
K-ETFreeman Mar 23, 2025
af45b0e
isort
K-ETFreeman Mar 23, 2025
cb79089
??
K-ETFreeman Mar 23, 2025
a76df29
improvements
K-ETFreeman Mar 25, 2025
8d7fa14
still bad?
K-ETFreeman Mar 25, 2025
aba93fa
avoid shadowing to not confuse mypy
K-ETFreeman Mar 25, 2025
5e6ebf0
removed infinity so mypy doesnt cry about types
K-ETFreeman Mar 25, 2025
a293b40
blank line doesnt contain whitespace
K-ETFreeman Mar 25, 2025
416b483
sorting removed because its unnecessary in new version of algo
K-ETFreeman Mar 25, 2025
ca425c8
added tests for calculate_dynamic_tokens_per_map
K-ETFreeman Apr 17, 2025
a1d5da2
linter
K-ETFreeman Apr 17, 2025
80933b8
added 1 more test
K-ETFreeman Apr 17, 2025
81251be
fix
K-ETFreeman Apr 17, 2025
16a15d2
refactor & tests for antirep
K-ETFreeman Apr 18, 2025
9018c65
linter
K-ETFreeman Apr 18, 2025
9ef1693
more types. Maybe correct ones.
K-ETFreeman Apr 18, 2025
d8aa760
very important file
K-ETFreeman Apr 18, 2025
70ff78c
antirep system: final version
K-ETFreeman Apr 19, 2025
ca61e9d
quotes
K-ETFreeman Apr 19, 2025
3872919
refactor
K-ETFreeman May 3, 2025
8c1cbfe
fix
K-ETFreeman May 3, 2025
3e32332
commented debug stuff
K-ETFreeman May 3, 2025
0b42bf1
apply_vetoes moved to VetoSystem
K-ETFreeman May 3, 2025
27b56fc
graceful wrong veto format behaviour
K-ETFreeman May 3, 2025
285851e
fix
K-ETFreeman May 3, 2025
e90dc52
Clean up veto_system.py formatting
Askaholic May 4, 2025
bf8db6c
fix
K-ETFreeman May 5, 2025
282cf14
Merge branch 'feature/#1032-working-veto-system' of https://github.co…
K-ETFreeman May 5, 2025
620fd12
fix
K-ETFreeman May 5, 2025
be19c44
revert accidental config.py push
K-ETFreeman May 5, 2025
522e5b1
Update server/ladder_service/veto_system.py
K-ETFreeman May 5, 2025
2b91d6b
Refactor veto system to VetoService
Askaholic May 18, 2025
1c16fd2
Fix test failures
Askaholic May 18, 2025
0a4ad9d
Add placeholder integration test
Askaholic May 18, 2025
90292ed
Refactor _is_valid_veto_config_for_queue
Askaholic May 18, 2025
129648f
temp commit before merge
K-ETFreeman Jul 24, 2025
3058a31
Merge branch 'feature/#1032-working-veto-system' of https://github.co…
K-ETFreeman Jul 24, 2025
2644646
- kick affected players from all queues
K-ETFreeman Jul 25, 2025
0c374d2
linter
K-ETFreeman Jul 25, 2025
d002367
less garbage
K-ETFreeman Jul 25, 2025
bee6f13
integration tests
K-ETFreeman Sep 7, 2025
73f6ced
more integration tests & fix
K-ETFreeman Sep 7, 2025
fed4882
flakefix
K-ETFreeman Sep 7, 2025
23bd8ec
isortfix
K-ETFreeman Sep 7, 2025
3e84ed1
Merge branch 'develop' into feature/#1032-working-veto-system
K-ETFreeman Oct 1, 2025
e5c8362
fix
K-ETFreeman Oct 2, 2025
7ef8a9e
Merge branch 'feature/#1032-working-veto-system' of https://github.co…
K-ETFreeman Oct 2, 2025
a766aa0
tests_fix
K-ETFreeman Oct 2, 2025
23c641a
probably fix (race condition)
K-ETFreeman Oct 2, 2025
6ac051a
probably fix 2
K-ETFreeman Oct 2, 2025
729e0f4
microfix
K-ETFreeman Oct 19, 2025
4de97a3
i want green
K-ETFreeman Oct 19, 2025
6b70550
i want green 2
K-ETFreeman Oct 19, 2025
158e4a6
tweaked antirep system a bit
K-ETFreeman Oct 21, 2025
14c2c2b
codacy warning suppression attempt
K-ETFreeman Oct 21, 2025
5401dad
allow num_maps == min_maps_after_vetoes if tokens_per_player is 0
K-ETFreeman Oct 26, 2025
b43a0ce
fix test
K-ETFreeman Oct 26, 2025
3decd56
removed accidental print
K-ETFreeman Oct 26, 2025
4ee987d
microfix
K-ETFreeman Oct 26, 2025
b9c6a85
clear
K-ETFreeman Oct 26, 2025
49eb2f8
fix. forgot this file!
K-ETFreeman Oct 26, 2025
3795aff
Merge pull request #1033 from K-ETFreeman/feature/#1032-working-veto-…
K-ETFreeman Oct 28, 2025
80d2a46
microfix
K-ETFreeman Nov 1, 2025
1f0edc3
micro-microfix
K-ETFreeman Nov 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
from .gameconnection import GameConnection
from .geoip_service import GeoIpService
from .ladder_service import LadderService
from .ladder_service.veto_system import VetoService
from .ladder_service.violation_service import ViolationService
from .lobbyconnection import LobbyConnection
from .message_queue_service import MessageQueueService
Expand Down Expand Up @@ -157,6 +158,7 @@
"RatingService",
"RatingService",
"ServerInstance",
"VetoService",
"ViolationService",
"game_service",
"protocol",
Expand Down Expand Up @@ -204,6 +206,7 @@ def __init__(
party_service=self.services["party_service"],
rating_service=self.services["rating_service"],
oauth_service=self.services["oauth_service"],
veto_service=self.services["veto_service"],
)

def write_broadcast(
Expand Down
4 changes: 3 additions & 1 deletion server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ def __init__(self):
self.GEO_IP_DATABASE_MAX_AGE_DAYS = 22

self.LADDER_1V1_OUTCOME_OVERRIDE = True
self.LADDER_ANTI_REPETITION_LIMIT = 2
self.LADDER_ANTI_REPETITION_LIMIT = 1
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You said there were issues with this being 2. Add a unit test which: enforces this being 1 until the underlying issue is fixed. Add a short description to the unit test of what the issue was.

self.LADDER_ANTI_REPETITION_WEIGHT_BASE_THRESHOLDS = [0.75, 0.5]
self.LADDER_ANTI_REPETITION_REPEAT_COUNTS_FACTOR = 0.8
self.LADDER_SEARCH_EXPANSION_MAX = 0.25
self.LADDER_SEARCH_EXPANSION_STEP = 0.05
self.LADDER_TOP_PLAYER_SEARCH_EXPANSION_MAX = 0.3
Expand Down
12 changes: 8 additions & 4 deletions server/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,10 +295,14 @@

matchmaker_queue_map_pool = Table(
"matchmaker_queue_map_pool", metadata,
Column("matchmaker_queue_id", Integer, ForeignKey("matchmaker_queue.id"), nullable=False),
Column("map_pool_id", Integer, ForeignKey("map_pool.id"), nullable=False),
Column("min_rating", Integer),
Column("max_rating", Integer),
Column("id", Integer, primary_key=True),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding my earlier comment. I just saw that this was not introduced by your PR but instead already existed in the code base.

I pasted your version of this definition into claude sonnet and prompted to generate a comment for the review, based on my earlier comment. This is its output:

The table name matchmaker_queue_map_pool makes it look like a simple join table, but it's actually a first-class entity with its own attributes (rating ranges, veto configuration). This deserves a more descriptive name that reflects what it represents in the domain: a configured assignment of a map pool to a queue.
Consider renaming to something like queue_map_pool_config or map_pool_assignment to better communicate its purpose. At minimum, please add a comment explaining what this table represents.

Code Suggestion:

# Configures how a map pool is used within a matchmaker queue.
# Each row represents an assignment of a map pool to a queue with specific
# parameters for rating eligibility and the veto system. A queue can have
# multiple assignments (e.g., different map pools for different rating ranges),
# and a map pool can be assigned to multiple queues with different configurations.
matchmaker_queue_map_pool = Table(
    "matchmaker_queue_map_pool", metadata,
    Column("id",                       Integer, primary_key=True),
    Column("matchmaker_queue_id",      Integer, ForeignKey("matchmaker_queue.id"), nullable=False),
    Column("map_pool_id",              Integer, ForeignKey("map_pool.id"),         nullable=False),
    Column("min_rating",               Integer),  # Minimum rating to use this map pool assignment
    Column("max_rating",               Integer),  # Maximum rating to use this map pool assignment
    Column("veto_tokens_per_player",   Integer, nullable=False),  # Number of maps each player can veto
    Column("max_tokens_per_map",       Integer, nullable=False),  # Max veto tokens applicable to one map
    Column("minimum_maps_after_veto",  Float,   nullable=False),  # Minimum maps remaining after vetoes
)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this already existed before your PR I'd say let's not rename this, but definitely do add the source comment. First check whether it makes sense though. The LLMs are good at generating stuff that sounds convincing but might be subtly or even fundamentally wrong.

Copy link
Contributor Author

@K-ETFreeman K-ETFreeman Nov 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://faforever.github.io/db/tables/matchmaker_queue_map_pool.html

this is just database entity
i am not sure why any comment is needed here because no other table has this sorta comments either

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the other database entities have descriptive names. This name is not helpful. Instead of renaming it, I'd suggest simply adding a source comment what this is for.

Column("matchmaker_queue_id", Integer, ForeignKey("matchmaker_queue.id"), nullable=False),
Column("map_pool_id", Integer, ForeignKey("map_pool.id"), nullable=False),
Column("min_rating", Integer),
Column("max_rating", Integer),
Column("veto_tokens_per_player", Integer, nullable=False),
Column("max_tokens_per_map", Integer, nullable=False),
Column("minimum_maps_after_veto", Float, nullable=False),
)

teamkills = Table(
Expand Down
77 changes: 65 additions & 12 deletions server/ladder_service/ladder_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@
import re
import statistics
from collections import defaultdict
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Coroutine, Optional
from typing import (
TYPE_CHECKING,
Any,
Callable,
ClassVar,
Coroutine,
Optional,
cast
)

import aiocron
import humanize
Expand Down Expand Up @@ -37,16 +45,23 @@
from server.games import InitMode, LadderGame
from server.games.ladder_game import GameClosedError
from server.ladder_service.game_name import game_name
from server.ladder_service.veto_system import VetoService
from server.ladder_service.violation_service import ViolationService
from server.matchmaker import (
MapPool,
MatchmakerQueue,
MatchmakerQueueMapPool,
OnMatchedCallback,
Search
)
from server.metrics import MatchLaunch
from server.players import Player, PlayerState
from server.types import GameLaunchOptions, Map, NeroxisGeneratedMap
from server.types import (
GameLaunchOptions,
Map,
MapPoolMap,
NeroxisGeneratedMap
)

if TYPE_CHECKING:
from server.lobbyconnection import LobbyConnection
Expand All @@ -66,12 +81,14 @@ def __init__(
database: FAFDatabase,
game_service: GameService,
violation_service: ViolationService,
veto_service: VetoService,
):
self._db = database
self._informed_players: set[Player] = set()
self.game_service = game_service
self.queues: dict[str, MatchmakerQueue] = {}
self.violation_service = violation_service
self.veto_service = veto_service

self._searches: dict[Player, dict[str, Search]] = defaultdict(dict)
self._allow_new_searches = True
Expand Down Expand Up @@ -106,7 +123,7 @@ async def update_data(self) -> None:
queue.team_size = info["team_size"]
queue.rating_peak = await self.fetch_rating_peak(info["rating_type"])
queue.map_pools.clear()
for map_pool_id, min_rating, max_rating in info["map_pools"]:
for matchmaker_queue_map_pool_id, map_pool_id, min_rating, max_rating, veto_tokens_per_player, max_tokens_per_map, minimum_maps_after_veto in info["map_pools"]:
map_pool_name, map_list = map_pool_maps[map_pool_id]
if not map_list:
self._logger.warning(
Expand All @@ -116,21 +133,32 @@ async def update_data(self) -> None:
name
)
queue.add_map_pool(
MapPool(map_pool_id, map_pool_name, map_list),
min_rating,
max_rating
MatchmakerQueueMapPool(
matchmaker_queue_map_pool_id,
MapPool(map_pool_id, map_pool_name, cast(list[MapPoolMap], map_list)),
min_rating,
max_rating,
veto_tokens_per_player,
max_tokens_per_map,
minimum_maps_after_veto
)
)
# Remove queues that don't exist anymore
for queue_name in list(self.queues.keys()):
if queue_name not in db_queues:
self.queues[queue_name].shutdown()
del self.queues[queue_name]

affected_players = self.veto_service.update_pools_veto_config(self.queues)
for player in affected_players:
self.cancel_search(player)

async def fetch_map_pools(self, conn) -> dict[int, tuple[str, list[Map]]]:
result = await conn.execute(
select(
map_pool.c.id,
map_pool.c.name,
map_pool_map_version.c.id.label("map_pool_map_version_id"),
map_pool_map_version.c.weight,
map_pool_map_version.c.map_params,
map_version.c.id.label("map_id"),
Expand Down Expand Up @@ -159,6 +187,7 @@ async def fetch_map_pools(self, conn) -> dict[int, tuple[str, list[Map]]]:
map_list.append(
Map(
id=row.map_id,
map_pool_map_version_id=row.map_pool_map_version_id,
folder_name=folder_name,
ranked=row.ranked,
weight=row.weight,
Expand All @@ -170,7 +199,7 @@ async def fetch_map_pools(self, conn) -> dict[int, tuple[str, list[Map]]]:
map_type = params["type"]
if map_type == "neroxis":
map_list.append(
NeroxisGeneratedMap.of(params, row.weight)
NeroxisGeneratedMap.of(params, row.weight, row.map_pool_map_version_id)
)
else:
self._logger.warning(
Expand All @@ -197,9 +226,13 @@ async def fetch_matchmaker_queues(self, conn):
matchmaker_queue.c.technical_name,
matchmaker_queue.c.team_size,
matchmaker_queue.c.params,
matchmaker_queue_map_pool.c.id.label("matchmaker_queue_map_pool_id"),
matchmaker_queue_map_pool.c.map_pool_id,
matchmaker_queue_map_pool.c.min_rating,
matchmaker_queue_map_pool.c.max_rating,
matchmaker_queue_map_pool.c.veto_tokens_per_player,
matchmaker_queue_map_pool.c.max_tokens_per_map,
matchmaker_queue_map_pool.c.minimum_maps_after_veto,
game_featuredMods.c.gamemod,
leaderboard.c.technical_name.label("rating_type")
)
Expand Down Expand Up @@ -228,9 +261,13 @@ async def fetch_matchmaker_queues(self, conn):
info["team_size"] = row.team_size
info["params"] = json.loads(row.params) if row.params else None
info["map_pools"].append((
row.matchmaker_queue_map_pool_id,
row.map_pool_id,
row.min_rating,
row.max_rating
row.max_rating,
row.veto_tokens_per_player,
row.max_tokens_per_map,
row.minimum_maps_after_veto
))
except Exception:
self._logger.warning(
Expand Down Expand Up @@ -532,10 +569,25 @@ def get_displayed_rating(player: Player) -> float:
)
rating = func(ratings)

pool = queue.get_map_pool_for_rating(rating)
if not pool:
map_pool = queue.get_map_pool_for_rating(rating)
if not map_pool:
raise RuntimeError(f"No map pool available for rating {rating}!")
game_map = pool.choose_map(played_map_ids)

self._logger.debug(
"______queue.map_pools[map_pool.id]________________: %s",
queue.map_pools[map_pool.id],
)
initial_weights = self.veto_service.generate_initial_weights_for_match(
all_players,
queue.map_pools[map_pool.id],
)
self._logger.debug(
"______initial_weights________________: %s",
initial_weights,
)
game_map = map_pool.choose_map(played_map_ids, initial_weights)

self._logger.debug("______game_map________________: %s", game_map)

game = self.game_service.create_game(
game_class=LadderGame,
Expand Down Expand Up @@ -587,7 +639,8 @@ def make_game_options(player: Player) -> GameLaunchOptions:
game_options=game_options,
team=game.get_player_option(player.id, "Team"),
faction=game.get_player_option(player.id, "Faction"),
map_position=game.get_player_option(player.id, "StartSpot")
map_position=game.get_player_option(player.id, "StartSpot"),
map_pool_map_version_id=game_map.map_pool_map_version_id
)

await self.launch_match(game, host, all_guests, make_game_options)
Expand Down
Loading
Loading