bbGuild Architecture¶
Overview¶
bbGuild is a guild management extension for phpBB 3.3+. It provides guild roster management, character tracking, recruitment, a Message-of-the-Day / news system, and a tabbed portal page — with game-specific behavior (class data, race data, Battle.net-style APIs, character sync) supplied by separate "game plugin" extensions rather than built into core.
- Vendor namespace:
avathar\bbguild - Entry point:
ext.php - Requirements: PHP >= 8.1, phpBB >= 3.3, GD, cURL
- Version: tracked in the
ext::BBGUILD_VERSIONclass constant, notphpbb_config— see Design decisions
This document is a working architecture reference, not a full arc42 spec —
it covers system context, component boundaries, the design decisions worth
remembering the reasoning behind, and the cross-cutting rules that apply
across the codebase. For column-level database detail, see
contrib/database.md.
System Context¶
bbGuild core is a standard phpBB 3.3 extension: it registers routes,
controllers, ACP/UCP modules, an event listener, and a cron task through
phpBB's dependency-injection container (config/services.yml), and ships
its schema as a chain of phpbb\db\migration classes. ext.php extends
phpbb\extension\base and gates enablement on PHP/phpBB version and the
gd/curl extensions (is_enableable()); it also auto-disables known
child extensions before core disables (disable_step()), since a child's
services.yml references core-defined DI parameters that stop existing
once core is gone — though its hardcoded child list is currently stale
(see Known Gaps).
Game plugins — bbguildwow, bbguildgw2, bbguildlotro, bbguildeq,
bbguildeq2, bbguildffxi, bbguildffxiv, bbguildswtor,
bbguildlineage2 — are themselves separate phpBB extensions living at
ext/avathar/bbguild<game>/ (no separator: phpBB's extension class loader
can't handle a hyphenated directory name, and EPV rejects underscores in a
composer package name — that's why the family dropped the bbguild_<game>
naming in 2.0.0-b4). Each plugin:
- Depends on core through two independent, deliberately loose-coupled mechanisms — a hard runtime gate and an advisory packaging hint:
- A hard version gate in its own
ext.php::is_enableable(): it readsavathar\bbguild\ext::BBGUILD_VERSIONand refuses to enable if core is older than its ownMIN_BBGUILD_VERSIONconstant (e.g.bbguildwowcurrently requires>=2.0.0). This is the check that's actually enforced. - A
composer.json"suggest"/soft-require line naming the same minimum version, which is advisory only — Composer doesn't enforce it the way a hardrequirewould, since these packages aren't installed through Composer's dependency resolver in a typical phpBB deployment. - Migration
depends_on()chains are not used as a version gate — they intentionally pin to whatever core migration first introduced the schema the plugin actually needs (a correctness dependency), not to the current release. Bumping the version gate and bumping the schema dependency are two separate concerns kept in two separate places. - Registers a game provider (
game_provider_interface) taggedbbguild.game_providerin its ownconfig/services.yml, giving core the game's display name, boss/zone URL formats, armor types, regions, and (if it has one) an API client (game_api_interface). - Optionally registers a character-sync handler
(
character_sync_interface) taggedbbguild.character_sync, letting core's scheduled cron task delegate syncing an individual character back to the plugin that knows how to fetch its data. - Provides a
game_install_interfaceimplementation (extending core'sabstract_game_install) that seeds factions/classes/races/roles/specs into the shared reference tables (bb_factions,bb_classes,bb_races,bb_gameroles,bb_specializations) on demand from ACP — game data is not migrated in; it's installed interactively, same as the built-in "Custom" game (model/games/library/install_custom.php).
Both tag collections use phpbb\di\service_collection (see
Design decisions for why, not Symfony's
!tagged_iterator), wrapped by a thin core registry —
game_registry/character_sync_registry — that indexes providers/handlers
by game_id for O(1) lookup.
flowchart LR
subgraph core["bbGuild core (this repo)"]
GR[game_registry]
CSR[character_sync_registry]
CRON[cron: character_sync task]
end
subgraph wow["bbguildwow (example plugin)"]
WP["wow_provider\n(bbguild.game_provider)"]
WS["wow_sync_handler\n(bbguild.character_sync)"]
WI["wow_installer\n(abstract_game_install)"]
end
WP -- tagged service --> GR
WS -- tagged service --> CSR
CRON -- "get(game_id)" --> CSR
CRON -. "sync_character($row)" .-> WS
GR -- "get(game_id)" --> WI
Component View¶
Directory structure¶
bbguild/
├── acp/ # ACP module definitions (*_info.php + *_module.php)
├── config/ # DI services, routing, table parameters (YAML)
├── controller/ # Request controllers (ACP + frontend)
├── cron/ # Cron tasks (character_sync)
├── event/ # phpBB event listeners
├── language/ # Localization (en, de, fr, it, nl, es_x_tu, pl)
├── migrations/ # Database migrations (see contrib/database.md)
├── model/ # Business logic and data access
│ ├── admin/ # Utilities: curl, log, constants, util, asset_url_resolver
│ ├── api/ # Game API client abstraction (e.g. Battle.net)
│ ├── games/ # Game registry, character-sync registry, abstract installer
│ │ └── rpg/ # Classes/races/roles/factions/specialization CRUD
│ └── player/ # Player, guild, rank, recruitment models
├── portal/ # Portal block engine + guild context
│ └── modules/ # Module infrastructure + built-in modules
├── styles/ # Twig templates (prosilver)
├── ucp/ # User Control Panel modules
├── images/ # UI assets (emblems, icons, progressbar)
├── contrib/ # Documentation, diagrams, changelog
└── tests/ # Unit tests
Frontend template structure¶
main.html
├── Guild header (name, faction, realm, member count, emblem)
├── Tab bar (from bb_portal_tabs, #360)
└── view/welcome.html (portal renderer, active tab only)
├── portal-top (modules_top loop)
├── portal-content-wrapper
│ ├── portal-right (modules_right loop)
│ │ └── recruitment_side.html, custom blocks, etc.
│ └── portal-center (modules_center loop)
│ ├── motd_center.html
│ ├── roster_center.html (filters, table, pagination)
│ └── news_center.html, statistics, activity, etc.
└── portal-bottom (modules_bottom loop)
Service layer¶
All services are defined in config/services.yml and
config/portal_services.yml; table names are parameterised in
config/tables.yml.
| Service ID | Class | Purpose |
|---|---|---|
avathar.bbguild.controller |
controller\view_controller |
Frontend routes (guild page) |
avathar.bbguild.admin.main |
controller\admin_main |
ACP dashboard, settings, logs |
avathar.bbguild.admin.games |
controller\admin_games |
Game/faction/race/class/role management |
avathar.bbguild.admin.guild |
controller\admin_guild |
Guild/rank/player management |
avathar.bbguild.admin.portal |
controller\admin_portal |
Portal module/tab management |
avathar.bbguild.log |
model\admin\log |
bbGuild activity log |
avathar.bbguild.curl |
model\admin\curl |
HTTP client wrapper |
avathar.bbguild.util |
model\admin\util |
Request utilities |
avathar.bbguild.game_registry |
model\games\game_registry |
Game provider lookup |
avathar.bbguild.character_sync_registry |
model\games\character_sync_registry |
Character-sync handler lookup |
avathar.bbguild.cron.task.character_sync |
cron\task\character_sync |
Scheduled character-sync task (#361) |
avathar.bbguild.listener |
event\main_listener |
phpBB event hooks |
Portal services (config/portal_services.yml):
| Service ID | Class | Purpose |
|---|---|---|
avathar.bbguild.guild_context |
portal\guild_context |
Resolves guild, loads data, assigns header template vars |
avathar.bbguild.portal.renderer |
portal\portal_renderer |
Renders the active tab's layout for a guild |
avathar.bbguild.portal.columns |
portal\columns |
Column constants and helpers |
avathar.bbguild.portal.module_helper |
portal\module_helper |
Module rendering helper (template, language, columns) |
avathar.bbguild.portal.module_registry |
portal\module_registry |
Available module type lookup |
avathar.bbguild.portal.modules.manager |
portal\modules\manager |
Module CRUD operations |
avathar.bbguild.portal.modules.database_handler |
portal\modules\database_handler |
Module + tab layout persistence |
Built-in portal modules (tagged bbguild.portal.module):
- avathar.bbguild.portal.module.motd — Message of the Day
- avathar.bbguild.portal.module.recruitment — Recruitment status
- avathar.bbguild.portal.module.roster — Guild roster (center-only, grid/listing layout switcher, filters, pagination)
- avathar.bbguild.portal.module.statistics — Guild statistics (guild/race/rank breakdowns)
Routes¶
Defined in config/routing.yml:
| Route | Path | Controller |
|---|---|---|
avathar_bbguild_guild |
/guild/{guild_id} |
view_controller::handleview |
avathar_bbguild_00 |
/guild/{page}/{guild_id} |
view_controller::handleview ($page doubles as the initial tab slug) |
avathar_bbguild_player |
/guild/{guild_id}/player/{player_id}/{tab_slug} |
view_controller::playerdetail ($tab_slug is an optional trailing segment, defaulting to empty, which resolves to the Character tab) |
avathar_bbguild_01 |
/getfaction |
AJAX: faction selector |
avathar_bbguild_02 |
/getguildrank/{guild_id} |
AJAX: rank selector |
avathar_bbguild_03 |
/getplayerList/{game_id} |
AJAX: player list |
avathar_bbguild_04 |
/getclassrace/{game_id} |
AJAX: class/race selector |
ACP modules¶
The bbGuild ACP category (ACP_CAT_BBGUILD) has three sub-categories,
registered directly in their final position — including Game
settings, positioned right after General Settings — by
migrations/v200/release_2_0_0.php.
| Sub-category (langname → title) | Module (acp/*_module.php) |
Controller | Modes |
|---|---|---|---|
ACP_BBGUILD_MAINPAGE → General Settings |
main_module |
admin_main |
panel (Dashboard), config (Settings), logs (Activity Log) |
ACP_BBGUILD_GAMESETTINGS → Game settings |
game_module |
admin_games |
listgames, editgames, addfaction, addrace, addclass, addrole |
ACP_BBGUILD_PLAYER → Guild and Player management |
guild_module |
admin_guild |
addguild, editguild, listguilds |
ACP_BBGUILD_PLAYER → Guild and Player management |
player_module |
admin_guild |
addplayer, listplayers |
Notes:
- Portal management is not a standalone ACP module — it is a tab on
the guild-edit page (admin_guild::show_editguildportal, delegating to
admin_portal), which now also owns tab CRUD (#360).
- Game plugins register their own ACP modules into these categories.
For example bbguildwow adds battlenet_module (BattleNet API) under
Game settings and achievement_module under Guild and Player
management (its migrations depends_on the core migration that owns
the target category).
Game plugins¶
| Plugin | Game | API |
|---|---|---|
bbguildwow |
World of Warcraft | Battle.net |
bbguildgw2 |
Guild Wars 2 | - (GW2 API v2 in progress) |
bbguildlotro |
Lord of the Rings Online | - |
bbguildeq |
EverQuest | - |
bbguildeq2 |
EverQuest 2 | - |
bbguildffxi |
Final Fantasy XI | - |
bbguildffxiv |
Final Fantasy XIV | fragile third-party parsers |
bbguildswtor |
Star Wars: The Old Republic | - |
bbguildlineage2 |
Lineage 2 | - |
A built-in "Custom" game ships in core itself
(model/games/library/install_custom.php) for boards not tied to a
specific supported title.
In practice, 8 of the 9 game plugins are pure data-seeding plugins:
just a game/<game>_provider.php + game/<game>_installer.php pair (no
src/, controllers, entities, or new DB tables), seeding rows into core's
shared bb_classes/bb_races/bb_factions/bb_language/
bb_specializations tables. bbguildwow is the sole exception and is
treated as the reference implementation for what a full-featured plugin
looks like — it alone has its own API client, cron sync, ACP modules,
achievements, and an avatar driver (see bbguildwow/docs/ARCHITECTURE.md).
Plugins whose game has no named "build/spec" layer above their class list
(eq, eq2, ffxi, ffxiv, lineage2) ship an intentionally empty
spec_catalog() with a documented rationale rather than fabricated data —
this is a deliberate modeling decision per plugin, not missing work.
Log system¶
model/admin/log.php follows phpBB core's own log design: each entry
stores a log_type (a language key, e.g. L_ACTION_GUILD_ADDED) and
log_action (a json_decode-able array of vsprintf substitution args —
switched from PHP unserialize during the EPV CI hardening pass). The log
viewer resolves the language key and applies the args via vsprintf() at
render time; ACTION_* keys are short labels, VLOG_* keys are verbose
format strings whose first %s is always the username resolved from
log_userid. Valid types are enumerated in log::$valid_action_types.
Module responsibilities and dependencies¶
| Directory | Owns | Depends on |
|---|---|---|
controller/ |
Frontend route handler (view_controller) and the four ACP controllers (admin_main, admin_games, admin_guild, admin_portal); AJAX endpoints (ajax_controller); shared input validation (validator) |
model/, portal/ (frontend), phpBB DI services |
model/ |
Business logic and data access: player/ (player, guild, rank, recruitment queries), games/ (provider/installer/registry interfaces and the game_registry/character_sync_registry DI wiring), games/rpg/ (classes/races/roles/factions/specialization CRUD), admin/ (curl client, activity log, request-parsing util, asset URL resolver), api/ (Battle.net-style API client abstraction) |
phpBB dbal, config, cache services |
portal/ |
The portal rendering engine: guild_context (resolves the active guild + header vars), portal_renderer (resolves the active tab, assembles enabled modules per column), columns/module_helper/module_registry, and modules/ (module CRUD manager, database_handler for layout/config persistence, module_interface/module_base, and the built-in modules themselves: MOTD, Roster, Recruitment, Statistics) |
model/ for data, phpBB template |
event/ |
main_listener, the single EventSubscriberInterface implementation: assigns a global template flag on every page (core.common), registers bbGuild's language sets (core.user_setup), adds the guild-picker link to the page header (core.page_header), and registers the bbguild permission category (core.permissions) |
phpBB core events only |
migrations/ |
Schema, seed data, config keys, permissions, and ACP/UCP module registration, as two release-named migration classes — v200/release_2_0_0.php and v210/release_2_1_0.php, each writing its milestone's full end state directly (see contrib/database.md) |
phpBB migrator; v210 depends_on() v200 |
acp/ |
ACP module glue: an *_info.php (menu registration) + *_module.php (mode dispatch) pair per category, delegating the actual work to the matching controller/admin_*.php |
controller/, phpBB module.manager |
ucp/ |
The single UCP module (bbguild_module): character claim/add/update/delete for the logged-in user |
model/player/ |
cron/ |
task/character_sync.php — the scheduled task described under "Character-sync scheduler contract" below |
model/games/character_sync_registry, model/player/player |
Dependency direction is consistently controller/portal/cron → model → phpBB
core services; model/ and portal/ don't call back into controller/.
The one historical exception — portal's guild_context used to be
constructed inline by view_controller — was fixed by making it a DI
service (avathar.bbguild.guild_context) instead of view_controller
passing itself around as a service locator (see CLAUDE.md's "#346" fix).
Frontend request flow¶
view_controller::handleview()receives the request for/guild/{guild_id}(or the legacy/guild/{page}/{guild_id}route, where$pagedoubles as the initial tab slug).guild_contextresolves the guild by ID, loads guild data, and assigns header/dropdown template vars.portal_renderer::render()resolves the active tab (from the URL slug, falling back to the guild's lowest-tab_ordertab), loads that tab's enabled modules fromdatabase_handler, and assigns each into its column's template loop (top/center/right/bottom).main.htmlrenders the guild header plusview/welcome.html(the portal template), which renders the tab bar and each column's modules.
There's no separate "roster page" or "news page" template — the roster, news, MOTD, and recruitment views are all portal modules rendered inline on whichever tab an ACP admin places them on.
Design Decisions and Rationale¶
Portal tabs on top of columns (#360). The portal originally had a
single implicit page per guild, organized into four columns
(top/center/right/bottom) via bb_portal_modules.module_column +
module_order. #360 added bb_portal_tabs and a module_tab column
scoping each module to a tab, so a guild can have multiple pages (e.g.
"Overview", "Roster", "Rules") each with their own independent column
layout — rather than building a second, parallel page-routing mechanism,
tabs were layered onto the existing column engine by adding one more
scoping key. The migration backfills exactly one "Overview"/welcome tab
per existing guild_id, so upgrading is a zero-visible-change no-op until
an admin adds a second tab.
Character-sync scheduler contract (#361). Before #361, keeping
character data current in the source game was entirely manual (an admin or
the player re-triggering a fetch). #361 added a core-owned cron task
(cron/task/character_sync.php) that periodically picks a small batch of
the stalest characters (by player_last_synced) across every game that has
registered a sync handler, and delegates the actual fetch back to that
game's own plugin via character_sync_interface. Core deliberately knows
nothing about how any individual game's sync works — it only knows the
contract (get_game_id(), sync_character(array $player_row): bool) and
the registry that maps game_id → handler. This makes character sync an
extension point the same shape as the existing game-provider pattern,
rather than a WoW-specific feature bolted onto core.
sequenceDiagram
participant Cron as phpBB cron dispatch
participant Task as character_sync (task)
participant Registry as character_sync_registry
participant Player as player model
participant Handler as game plugin handler
participant DB as bb_players
Cron->>Task: is_runnable() / should_run()
Note over Task: only true if a handler is registered<br/>and MIN_INTERVAL (300s) has elapsed
Cron->>Task: run()
Task->>Player: get_stalest_players(game_ids, BATCH_SIZE=10)
Player-->>Task: player rows, oldest player_last_synced first
loop each stale player
Task->>Registry: get(player.game_id)
Registry-->>Task: handler | null
Task->>Handler: sync_character(player_row)
alt success
Handler-->>Task: true
else failure or thrown
Handler-->>Task: false
Task->>Task: log L_ERROR_CHARACTER_SYNC_FAILED
end
Task->>DB: update_last_synced(player_id)
end
Task->>Task: config.set(bbguild_sync_last_run, now)
The task is deliberately careful about when it builds a player instance:
phpBB's cron dispatch eagerly instantiates every registered cron.task
service roughly once a minute just to check is_runnable()/should_run(),
regardless of readiness — player's constructor unconditionally runs DB
queries, so character_sync builds it lazily inside run() (via
get_player()) rather than in the constructor, to avoid paying that cost
on every board, every minute, indefinitely.
Specialization system's layered model (#331). bb_specializations
inserts a layer between class and role: a spec belongs to exactly one
(game_id, class_id) and one role_id (e.g. "Frost Mage" is a class_id
Mage spec with role_id DPS). This mirrors how these games actually model
character builds — role alone ("DPS") is too coarse and class alone
("Mage") doesn't capture the build — without introducing a fourth
dimension; role and class stay first-class, independent lookup tables
(bb_gameroles, bb_classes), and specialization is purely a join between
them plus a display name/icon/order. bb_players.player_spec_id is
additive: the legacy free-text player_spec column stays in place.
Migrating existing values to player_spec_id was tracked separately as
331 Phase 5 (later split into its own issue, #381), and was ultimately¶
closed without a migration — existing installs aren't a support
concern (clean install is the supported upgrade path), so there's no real
backlog to backfill in practice. Both columns coexist indefinitely; the
front-end and ACP roster resolve spec display by preferring
player_spec_id and falling back to the legacy text when it's unset.
ext::BBGUILD_VERSION moved out of phpbb_config (#353). The version
used to live as a bbguild_version row in phpbb_config, updated by each
migration. That had a real bug: a fresh install never config.add'd the
row, it only ever config.update'd it, so a clean install had no version
row at all until the first upgrade ran. Moving the canonical version into a
PHP class constant (mirroring avatharbe/recenttopics's ext::RT_VERSION
pattern) removes the possibility of an unset/stale config row entirely —
the version is defined once, in code, and is always correct for whatever
code is actually deployed. v200rc1 explicitly removes any leftover
bbguild_version config row for installs upgrading from before this
change.
DKP/Accounting kept out of core. bbAccounts, bbDKP, and bbPoints
(dragon-kill-point tracking, raid loot logging, and point pools) are
shipped as entirely separate extensions with their own roadmap, not folded
into bbguild core. Guild/character/roster management and raid-loot
accounting are different problem domains with different audiences (every
guild wants a roster; a much smaller subset wants formal loot-point
tracking), and coupling them would force every bbGuild install to carry
DKP's schema and UI whether or not it's used. The DKP line does hard-depend
back on bbGuild core for guild/character data, but not the reverse — core
has no knowledge of bbAccounts/bbDKP, and legacy DKP-table lookups (see
CLAUDE.md's "DKP guard" fix) are defensively wrapped in an existence
check specifically so core doesn't error out on installs that don't have
the DKP extension at all.
Cross-cutting Concerns¶
Permissions¶
Six permissions in one ACP category (bbguild): a_bbguild (ACP access),
u_bbguild (view guild pages), and four character-management permissions
(u_charclaim, u_charadd, u_chardelete, u_charupdate). Default grants
are a deliberate mix of role-based grants (ROLE_USER_STANDARD/
ROLE_USER_FULL, ROLE_ADMIN_FULL/ROLE_ADMIN_STANDARD — the templates
most installs actually manage permissions through) and direct per-group
grants layered on top for REGISTERED, ADMINISTRATORS, GLOBAL_MODERATORS,
and GUESTS. The direct grants exist because installs that manage u_
permissions via direct per-group checkboxes instead of the stock role
templates never inherit a role-based grant at all — that gap was found and
closed incrementally across v200rc1 (view access) and v200rc2
(character-management access, needed so the UCP "bbGuild" tab isn't hidden
for an admin whose primary group isn't REGISTERED). GLOBAL_MODERATORS
intentionally stays view-only. See contrib/database.md if you need the
full grant matrix; it's also summarized in the README.
Migration and versioning convention¶
Migrations live in release-named directories, one per shipped minor
version — migrations/v200/release_2_0_0.php and
migrations/v210/release_2_1_0.php — rather than being numbered
sequentially, dated, or split into the many beta/rc-named files each
release-line actually developed through. Each of those two migrations
was squashed (#383, matching the 2.0.0 line's earlier squash of its
own beta chain into v200b3) from what had been a longer chain of
incremental beta/rc migrations, once the release shipped: since existing
installs are expected to clean-install rather than upgrade through old
beta history, the squashed migration writes its milestone's full final
schema/data/permissions/module state directly — e.g. v200/release_2_0_0.php
declares bb_language.language as VCHAR:10 from the start rather than
CHAR:2 then widened, and creates the Game settings ACP category in
its final position rather than under General Settings and moved
afterward. v210/release_2_1_0.php depends on v200/release_2_0_0.php
and is the only other migration in the chain. Each migration's
effectively_installed() checks a concrete artifact it itself creates — a
table, a column, a specific permission grant row — rather than comparing
against a version string, which is what makes moving the canonical version
out of phpbb_config (#353, above) safe: nothing in the migration chain's
own logic depends on that config value existing. depends_on() forms a
strict linear chain; each migration only ever depends on its immediate
predecessor. Downstream game plugins pin their own depends_on() to
these same two class names (a correctness dependency on the schema they
actually need), not to a version number.
Code Complexity¶
A cyclomatic-complexity pass (McCabe, per-function decision-point count)
confirms the architecture on paper: complexity is concentrated almost
entirely in core and bbguildwow, while the other 8 plugins are genuinely
trivial data-seeders (max complexity 7). Core averages 2.85/function
(628 functions, 9 above 20) and bbguildwow averages 3.84 (297 functions,
7 above 20) — healthy overall, with a long tail worth knowing about:
| CC | Location |
|---|---|
| 73 | ucp/bbguild_module.php::fill_addplayer() |
| 60 | acp/player_module.php::BuildTemplateAddEditplayers() |
| 52 | bbguildwow/model/achievement.php::syncCategories() |
| 45 | bbguildwow/model/achievement.php::setAchievements() |
| 33 | ucp/bbguild_module.php::main() |
The top two are both form-building/dispatch methods with many field/mode
branches inline — reasonable extraction candidates if ever revisited, and
the same pair CLAUDE.md's #354/#377 bugs already came from.
Known Gaps / Deliberately Out of Scope¶
- DKP & accounting is a separate extension family (bbAccounts/bbDKP/ bbPoints) with its own roadmap — see Design decisions above.
- Professions and player statistics (#230, #289) are roadmapped for 2.2.0 and not implemented yet.
bb_bosstable/bb_zonetablehave DI table-name parameters wired inconfig/tables.ymland are referenced by ACP controller constructors, but no migration creates them yet — schema is still TBD.- Legacy
player_spec→player_spec_idmigration (#331 Phase 5, later #381) was closed without a migration — both columns coexist onbb_playersindefinitely by design, not as an unfinished backlog item; see Design decisions above. - Game-plugin API coverage is uneven: only WoW (Battle.net) and GW2 have first-class APIs; FFXIV relies on fragile third-party parsers; the remaining games have no API path and are roster-managed manually.
ext.php::disable_step()'s child-extension list is stale: it only auto-disablesbbguildwowandbbguildeq2, not all 9 plugins — the other 7 would be left enabled (and erroring) if core were disabled first.- Two plugin-facing interfaces rely on weak typing where a shared DTO
would be safer:
game_install_interface::install()takes 6 positional params, five same-typed strings ($game_id, $game_name, $boss_base_url, $zone_base_url, $region), so nothing catches a plugin passing two of them swapped;character_sync_interface::sync_character(array $player_row)hands plugins the rawbb_playersrow, so every plugin's sync handler independently depends on core's exact column names with no shared source of truth. Neither has caused a bug yet, but both would fail silently rather than loudly. - Spec-icon assets are incomplete across several plugins that do have a
real specialization catalog:
bbguildgw2(27 elite specs),bbguildswtor(48 disciplines), andbbguildlotro's newly-added Brawler/Mariner classes (0 specs assigned yet) all ship with no icon art — each is self-documented as a follow-up in its own provider/install code, not a silent gap. bbguildeq's README class table doesn't match its installer's actual class_id/armor-type mapping — the README lists classes alphabetically with re-numbered ids (e.g. Bard shown as id 1/Plate), whilegame/eq_installer.phpassigns different ids in a different order with different armor types (Bard is actually id 7/Cloth);bbguildeq2's equivalent table is correct. Not caught by CI since it's a docs-only mismatch. (contrib/cleanup.sql's equivalent staleness across the family, previously noted here, has since been fixed.)