Skip to content

feat(team): add Team module for 0.3.0-Beta - #16

Open
j-gaertig-co-dev wants to merge 8 commits into
mainfrom
feature/team-module
Open

j-gaertig-co-dev wants to merge 8 commits into
mainfrom
feature/team-module

Conversation

@j-gaertig-co-dev

Copy link
Copy Markdown
Collaborator

Team module (new, 0.3.0-Beta)

Config-only team definitions (modules/team.yml), UUID-backed runtime membership/roles/invites/join-requests, MiniMessage colors incl. gradients/rainbow, and a real vanilla-scoreboard mirror so any command with a target selector can hit @a[team=pb_<id>].

Commands

/team list|info|accept|deny|leave|request|invite|add|kick|setrole|deny-request — full permission node per subcommand plus plainbase.team.admin bypass (OPs are always admin on every team).

Roles

MEMBER / ADMIN, per team. Members can only leave; admins do everything else. Promote via /team <team> setrole <player> admin (existing admin or console only).

Placeholders

%plainbase_team_names%, _count%, _primary%, _pending_invites%, %plainbase_teams_count%, %plainbase_team_role_<team>%, %plainbase_team_members_<team>%.

Bugs found & fixed during testing

  • Folia startup crash: Folia currently considers all scoreboard API broken (confirmed against PaperMC/Folia). TeamManager now detects Folia and skips the scoreboard mirror there entirely — membership/roles/commands/placeholders are unaffected, only the @a[team=...] selector trick is unavailable on Folia.
  • /team <team> add crash: threw UnsupportedOperationException when the player had no prior invite/request for that team (Map.getOrDefault(k, Set.of()) returns a shared immutable set, .remove() on it throws). Fixed to only mutate a set that actually exists.

Testing

Built and live-tested on both a Paper 26.2 and Folia 26.2 test server (RCON + a mineflayer bot acting as a real player): module enable/disable, /plainbase reload, full invite → join-reminder-on-login → accept/deny flow, add/kick/setrole, max-teams-per-player enforcement, permission checks (console vs. non-OP), placeholders, and clean shutdown/scoreboard cleanup on both platforms.

Docs

  • New wiki page Module-Team, linked from Home/Sidebar/Permissions.
  • README module table updated.

- Teams defined in modules/team.yml (name, MiniMessage color incl. gradients/rainbow)
- Runtime membership/roles/invites/join-requests persisted by UUID under data/teams/
- Roles per team: MEMBER / ADMIN. OPs and plainbase.team.admin bypass to admin on any team
- Commands: /team list, /team <team> info, /team <team> invite|add|kick|setrole|request,
  /team accept|deny|leave [team]
- Every command has its own permission node, independent of team membership
- Teams are mirrored to a real vanilla scoreboard team (pb_<id>) so any command
  with a target selector works, e.g. /gamemode creative @A[team=pb_team1]
  (documented limitation: a scoreboard entry can only be on one team at a time,
  matters only if max-teams-per-player > 1)
- New PlaceholderAPI placeholders: team_names, team_count, team_primary,
  team_pending_invites, teams_count, team_role_<team>, team_members_<team>
- config.yml version 1.6 -> 1.7 (new team module entry)
- pom.xml version -> 0.3.0-Beta
- Folia currently considers ALL scoreboard API broken (confirmed via
  PaperMC/Folia repo). Registering/looking up scoreboard teams there
  throws UnsupportedOperationException and disabled the whole plugin.
  TeamManager now detects Folia and skips the vanilla-scoreboard mirror
  entirely there; membership/roles/invites/commands/placeholders are
  unaffected, only @A[team=pb_<id>] selectors are unavailable on Folia.
- add() crashed with UnsupportedOperationException when a player had no
  prior invite/join-request for that team: Map.getOrDefault(k, Set.of())
  returns the shared immutable empty set, and .remove() on it throws.
  Now only mutates a set that's actually present.
Old schema mixed two orders (verb-first for self actions, team-first for
staff actions) and had no self-documenting help, no way to list pending
join requests, and dead per-command config toggles (declared in team.yml
but never actually read anywhere).

- Every subcommand is now /team <action> [team] [player] [role] — table-
  driven via TeamCommand.ACTIONS, so adding a new one later is one map
  entry + one switch case instead of touching parsing logic in multiple
  places.
- Bare /team prints a permission-aware help listing (only shows what the
  sender can actually run).
- New /team requests <team> (admin: see who's waiting to join) and
  /team invites (self: see your own pending invites) — previously you
  only found out about these reactively via chat notifications.
- /team info with no team now summarizes the sender's own memberships +
  roles instead of erroring.
- Renamed deny-request -> reject with its own permission node
  (plainbase.team.reject) instead of silently sharing plainbase.team.deny.
- team.yml's commands: block is now actually nested under team: (where
  the code reads it from) — it used to be a sibling top-level key, so
  every per-command enabled/disabled toggle was silently a no-op. Bumped
  to version 1.1.
- PlaceholderAPI: %plainbase_team_pending_invites% now uses the new
  TeamManager#getPendingInvites() instead of an O(teams) scan loop.

Tested end-to-end on Paper 26.2 + Folia 26.2 (RCON + a mineflayer bot as
a real player): help output, every subcommand, the per-action config
toggle actually taking effect, and the full invite/accept/request/add
flow.
- getPendingRequests()/getPendingInvites()/listRequests()/listInvites()/
  infoSelf() back the new /team requests, /team invites and no-arg
  /team info commands from the schema redesign.

- Independent full-project code review (delegated, fresh context) flagged
  that TeamManager#resolveTarget() called the deprecated
  Bukkit#getOfflinePlayer(String) SYNCHRONOUSLY on the command-dispatch
  thread for invite/add/kick/reject/setrole — that call can block on a
  Mojang lookup (main thread on Paper, the sender's region thread on
  Folia). The moderation module already has the correct pattern for this
  (ModerationCommandBase#resolveTarget: online/cached lookups resolve
  instantly, only a true cache-miss falls back to the async scheduler
  with the callback dispatched back onto the main/region thread) — teams
  now uses the exact same pattern instead of its own naive synchronous
  version.

Verified on Paper + Folia after the refactor: full invite/add/setrole/
reject/kick cycle including the async cache-miss fallback path (queried
a never-seen name), no regressions.
…ault

The team module commit bumped config.yml's shipped default to
version: 1.7 (new modules.team key) but never bumped the
corresponding entry in latestVersions, so upgraders on 1.6 never
saw the outdated-config warning. Verified: forcing an on-disk
config.yml back to 1.6 now logs '!!! OUTDATED CONFIG: config.yml !!!'
on both Paper and Folia.
refreshCacheBlocking() cleared and repopulated the ban/kick caches
without holding mutationLock, so a ban/unban committing to the DB
between the refresh's read and its cache swap could have its
update clobbered by a stale snapshot. The periodic refresh now
swaps the caches under the same lock the mutation methods already
use.
Each save*() scheduled an independent async task; two
near-simultaneous mutations to the same team could produce two
tasks racing to write the same YAML file concurrently, risking an
interleaved/corrupted write -- the same bug class already fixed
once for the moderation module's ban/kick files. Added one lock
per file so concurrent saves to the same file serialize, while
members/invites/requests still don't block each other.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant