From ca8007de988e8916d11730e0e95e8bb0c2071f92 Mon Sep 17 00:00:00 2001 From: Andy Vandenberghe Date: Thu, 24 Sep 2026 12:48:26 +0200 Subject: [PATCH] Add the authored swtor_unknown.png placeholder (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit class_id 0 — the class a character gets when a sync cannot map it — has always seeded this imagename with no file behind it, so it rendered as a broken image until avathar/bbguild#389 added a fallback chain. That chain ends at _unknown.png, so without this file its last step could never fire. Authored rather than sourced: no wiki or API has a generic "unknown class" icon, which is why avathar/bbguild#391 calls these out as the one part of the icon backlog that placeholders are the right answer for. A '?' glyph matches what the family already does here (bbguildwow's wow_unknown is a red '?' on a plate, bbguildlotro's race unknown a grey '?' on gold). Warm gold with a dark outline, so it stays legible on both light prosilver and dark styles; drawn at 8x and downsampled; 44x44 to match the set. swtor_icon_coverage_test parses the installer's seeded imagename values and asserts both icon directories cover them, with the remaining gaps listed explicitly — so a new gap fails CI instead of going unnoticed. Refs #3 Co-Authored-By: Claude Opus 5 (1M context) --- images/class_images/swtor_unknown.png | Bin 0 -> 1894 bytes tests/game/swtor_icon_coverage_test.php | 86 ++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 images/class_images/swtor_unknown.png create mode 100644 tests/game/swtor_icon_coverage_test.php diff --git a/images/class_images/swtor_unknown.png b/images/class_images/swtor_unknown.png new file mode 100644 index 0000000000000000000000000000000000000000..c298f54111cf9b3176d47fd059ad54d35698625d GIT binary patch literal 1894 zcmV-s2buVZP)AGo?WTon3QKpp-DRirwKKc3kD0mW@$-+Jg^iMQ9|N(m z-{fYJ$-U=%&hPg<=XcHluIj3;>WUNrkZphhAOVQVBijH~`Ct4`fZj6!I9&F=QbKHD!nI*&ptXf{6C8p7sU!kVLP!yU`4waFSWkBFFH`#F4;okkuq_{{U8UeX zD0#a``bCi~0xx1!L@!-2?e=PiAN) z&@>wxES{fQ?|Jf|wJuRA8-3U9A60j6x`|dSX+$CsfszyR%Q;3lG&pymfWIF-&A<6W zhwJShL?#uJp*9Da2l8KRpMagVJ!pooyr?O;CMSTzvN@b0kZdi!g zn!qx%@7TM3x)~SK_3p~1rnqBnCL*p$M8vs^BN#7gL?a&hvU$Anw=Ua|jw1}s<6V7& zZC!nr?sH+OO$ZueFuHp$vgb)K1I{vP5&>dSpPgeSaHf*U%L1!u05bsc&Yus*nlocH ziGX(h{+QnP;t3Ip`kYQiVXR|gEJ-Pew0#Nymd{VrL;24zBSs)zdTqVX+JXoZQ7##j zLqmdy*zss3lc)wT0EiAiw-CO^4m&}dEEYA!#zUODPyi7Sjd&<)yZ`k0;oc=p>FeQ% z-$@~ExNvF2ZMk=iSastfj2A;NfJj84v+ErU4HreSCgND*jY>bRn(HtD;AIOl%}i*8 zpaubIBt|2M>y5F!J=vjOES{gb&GY1b$DA1*Et{=hU8laWc|D3H4d)nA3J?LCAKY!b zPG1l~AUjU==T=wRch!pUq&38;fm{|q_WfR1GQa+2>G45hT!I1nvu&SIcYpdu3>S*v zN-;^q1N`{0gLwUovo4d4$xypTC)-99ErBU1698y(f{a#vbBh}@(%AjRxP(&DKEC~P z@)Nf%MJ_)EB7&5Jcr?I{ClBGb2Rd|JD(>mfJlm7a@1IGfo6V-$0kG|WX$MRT;5q=U zw0?*LEi~5Y9osg^wp*8CFgF3~dlFJgJpA()@#{UW>bg|iGsYb9lliYt*=pj0i12^; zdM{vGTGB}};=bWRk?;Nd&Fap!6}Xfeg^&UfU!gEs#`hmRfWxnzHVv7$7i#;b65{uq zI+@oq-L1_PSOBn879WR1UEnLXaYL&pl?{jpN(vN4%Xr}7y*S=Ez>ReYRVwS9J=y%% zfSjFfIi9N}rj~3103m6awT?6CxNDx5LZ}TK1M#SjoqPU?qbK@YeI~)>lKD|jHov6; zNi&5-wGM^^PCLhxi1|pweF!1odlJqu-t0ba;;}$jWA}BR%{|igURHTFeW_{ygC`>G zm@zU|hNldib10WJ$|VCq1OmDNh{~$Mv+b%Xz}C4z&yzTJaRi^;{PcU{U_cv7kw8IP z{um}!lsTy?z=9}njuDM`Si5R5gdjM_@O_21vw3miWEM(^wbxx&SJTxsID$DuxjMjt zqM^1(*GBQvhc+Sb6tuBOr=r-k?->5k(Tij764D;>o`|Sx#i8Nl`O<@0Ah43oH7uG?@7dC0kpAuy9aV# znoT*bCh(Nopv)&dMfVFy$;mll**{#7 ggSx7#`oGt|0o2.png for character rows and + * roster_classes/.png for the grid, so a seeded class with no + * file behind it has no icon. avathar/bbguild#389 makes that degrade + * instead of rendering broken, but the asset is still missing. + */ +class swtor_icon_coverage_test extends TestCase +{ + /** Gaps that are known and tracked; see #3 */ + private const KNOWN_GAPS = array( + // none — fully covered + ); + + /** Same, for the grid's artwork directory */ + private const KNOWN_ROSTER_GAPS = array( + 'swtor_unknown', + ); + + /** + * @return list Every imagename the installer seeds + */ + private function seeded_imagenames(): array + { + $files = glob(dirname(__DIR__, 2) . '/game/*_installer.php'); + $src = file_get_contents($files[0]); + + preg_match_all("/'imagename'\s*=>\s*'([^']+)'/", $src, $m); + + $names = array_values(array_unique(array_filter(array_map('trim', $m[1])))); + sort($names); + + return $names; + } + + private function assertNoUnexpectedGaps(string $dir, array $known): void + { + $base = dirname(__DIR__, 2) . '/images/' . $dir . '/'; + $missing = array(); + + foreach ($this->seeded_imagenames() as $name) + { + if (!file_exists($base . $name . '.png')) + { + $missing[] = $name; + } + } + + $unexpected = array_values(array_diff($missing, $known)); + + $this->assertSame( + array(), + $unexpected, + $dir . ' is missing icons that are not tracked as known gaps: ' . implode(', ', $unexpected) + ); + } + + public function test_class_images_cover_every_seeded_class(): void + { + $this->assertNoUnexpectedGaps('class_images', self::KNOWN_GAPS); + } + + public function test_roster_classes_cover_every_seeded_class(): void + { + $this->assertNoUnexpectedGaps('roster_classes', self::KNOWN_ROSTER_GAPS); + } + + public function test_the_installer_seeds_the_expected_class_count(): void + { + // Guards the parser: if the installer's array syntax changes, the + // coverage assertions must not silently pass on an empty list. + $this->assertCount(9, $this->seeded_imagenames()); + } +}