CS2 team dividers.
This commit is contained in:
@@ -79,6 +79,7 @@ class TeamSyncSummary:
|
||||
assigned: int = 0
|
||||
removed: int = 0
|
||||
created: list[str] = field(default_factory=list)
|
||||
positioned: int = 0 # team roles moved under a divider
|
||||
changes: list[str] = field(default_factory=list) # human-readable per-member lines
|
||||
errors: list[str] = field(default_factory=list)
|
||||
|
||||
@@ -309,6 +310,106 @@ async def sync_team_role(
|
||||
return result
|
||||
|
||||
|
||||
def plan_team_positions(
|
||||
ordered_names: list[str],
|
||||
managed: set[str],
|
||||
placements: dict[str, list[str]],
|
||||
) -> dict[str, int]:
|
||||
"""Work out the new position of every team role that needs to move.
|
||||
|
||||
``ordered_names`` is every role name ascending by Discord position, so index
|
||||
0 is the bottom of the role list (@everyone) and the last entry is the top.
|
||||
``placements`` maps a divider role name to the team roles that belong under
|
||||
it. Roles in ``managed`` are never moved (integration-managed roles, and
|
||||
anything at or above the bot's own role, cannot be repositioned).
|
||||
|
||||
Teams are pulled out of the list and re-inserted immediately below their
|
||||
divider, sorted so they read alphabetically top-to-bottom in the Discord UI.
|
||||
Everything else keeps its relative order; a role only appears in the result
|
||||
when its position actually changed. Roles are matched by name, mirroring the
|
||||
rest of the team sync - with duplicate role names the lowest one wins.
|
||||
"""
|
||||
present = set(ordered_names)
|
||||
placeable: dict[str, list[str]] = {}
|
||||
for divider, teams in placements.items():
|
||||
if divider not in present or divider in managed:
|
||||
continue
|
||||
# Descending here because the list is bottom-up: reversing it renders
|
||||
# alphabetically downwards from the divider.
|
||||
block = sorted({t for t in teams if t in present and t not in managed}, reverse=True)
|
||||
if block:
|
||||
placeable[divider] = block
|
||||
if not placeable:
|
||||
return {}
|
||||
|
||||
movable = {t for block in placeable.values() for t in block}
|
||||
remaining = [n for n in ordered_names if n not in movable]
|
||||
for divider, block in placeable.items():
|
||||
idx = remaining.index(divider)
|
||||
remaining[idx:idx] = block
|
||||
|
||||
old_pos = {name: i for i, name in enumerate(ordered_names)}
|
||||
return {
|
||||
name: i
|
||||
for i, name in enumerate(remaining)
|
||||
if name not in managed and old_pos.get(name) != i
|
||||
}
|
||||
|
||||
|
||||
async def apply_team_role_positions(
|
||||
guild: discord.Guild,
|
||||
log: logging.Logger = log,
|
||||
) -> tuple[int, list[str]]:
|
||||
"""Move every team role directly beneath its configured divider role.
|
||||
|
||||
Driven by the ``TEAM_DIVIDER_*`` config: teams whose sheet section matched
|
||||
one are placed under that role, the rest are left exactly where they are.
|
||||
Returns ``(roles_moved, errors)``; a no-op returns ``(0, [])``.
|
||||
"""
|
||||
placements: dict[str, list[str]] = {}
|
||||
for team, divider in sheets.get_team_dividers().items():
|
||||
placements.setdefault(divider, []).append(team)
|
||||
if not placements:
|
||||
return 0, [] # no dividers configured, or nothing matched
|
||||
|
||||
errors: list[str] = []
|
||||
by_name: dict[str, discord.Role] = {}
|
||||
for role in sorted(guild.roles, key=lambda r: r.position):
|
||||
by_name.setdefault(role.name, role)
|
||||
|
||||
# The bot can only reorder roles strictly below its own highest role.
|
||||
bot_top = max((r.position for r in guild.me.roles), default=0)
|
||||
for divider in list(placements):
|
||||
role = by_name.get(divider)
|
||||
if role is None:
|
||||
errors.append(f"Eraldajarolli '{divider}' ei leitud serverist")
|
||||
placements.pop(divider)
|
||||
elif role.position >= bot_top:
|
||||
errors.append(f"Eraldaja '{divider}' on boti rollist kõrgemal - ei saa liigutada")
|
||||
placements.pop(divider)
|
||||
if not placements:
|
||||
return 0, errors
|
||||
|
||||
ordered = sorted(guild.roles, key=lambda r: r.position)
|
||||
ordered_names = [r.name for r in ordered]
|
||||
managed = {r.name for r in ordered if r.managed or r.position >= bot_top}
|
||||
plan = plan_team_positions(ordered_names, managed, placements)
|
||||
if not plan:
|
||||
return 0, errors # already in the right place
|
||||
|
||||
positions = {by_name[n]: p for n, p in plan.items() if n in by_name}
|
||||
try:
|
||||
await guild.edit_role_positions(positions=positions)
|
||||
except discord.Forbidden:
|
||||
errors.append("Tiimirollide järjestamiseks puudub õigus")
|
||||
return 0, errors
|
||||
except discord.HTTPException as e:
|
||||
errors.append(f"Tiimirollide järjestamine ebaõnnestus: {e}")
|
||||
return 0, errors
|
||||
log.info("Positioned %d team role(s) under their dividers", len(positions))
|
||||
return len(positions), errors
|
||||
|
||||
|
||||
async def sync_all_team_roles(
|
||||
guild: discord.Guild,
|
||||
log: logging.Logger = log,
|
||||
@@ -339,6 +440,11 @@ async def sync_all_team_roles(
|
||||
if res.removed:
|
||||
bits.append("-" + ", -".join(res.removed))
|
||||
summary.changes.append(f"{member.display_name}: {', '.join(bits)}")
|
||||
|
||||
# Placement runs after the grant/remove pass so roles created this run are
|
||||
# positioned in the same sweep rather than waiting for the next one.
|
||||
summary.positioned, position_errors = await apply_team_role_positions(guild, log)
|
||||
summary.errors.extend(position_errors)
|
||||
return summary
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user