diff --git a/mpq/arr/images.tbl.txt b/mpq/arr/images.tbl.txt new file mode 100644 index 00000000..e7b4cc43 --- /dev/null +++ b/mpq/arr/images.tbl.txt @@ -0,0 +1,2318 @@ +zerg\avenger.grp +protoss\mshield.los +zerg\zavBirth.grp +zerg\zavbirth.lob +zerg\zavDeath.grp +zerg\zavExplo.grp +zerg\brood.grp +zerg\zbrShad.grp +zerg\rilirokor-death.grp +zerg\bugguy.grp +zerg\zbgShad.grp +thingy\zbgExplo.grp +zerg\cocoon.grp +zerg\isthrathaleth.grp +zerg\defiler.log +zerg\zdebirth.grp +zerg\zdeDeath.grp +zerg\droleth.grp +zerg\droleth.log +zerg\droleth.loo +zerg\zdrbirth.grp +zerg\droleth-death.grp +zerg\egg.grp +zerg\egg-shadow.grp +zerg\zegspawn.grp +zerg\zegDeath.grp +zerg\geszithalor.grp +zerg\geszithalor-attack.log +zerg\zguDeath.grp +zerg\hydralisk.grp +zerg\hydra.log +zerg\hydralisk-shadow.grp +zerg\hydralisk-birth.grp +zerg\hydralisk-remains.grp +zerg\uikerr.grp +zerg\uikShad.grp +thingy\spooge.grp +zerg\larva.grp +zerg\zlaDeath.grp +zerg\mutalisk.grp +zerg\mutalisk-attack.log +zerg\zmubirth.grp +zerg\zmuDeath.grp +zerg\ovileth.grp +zerg\zovBirth.grp +zerg\zovDeath.grp +zerg\matraleth.grp +zerg\zquDeath.grp +zerg\zquBirth.grp +zerg\ultrokor.grp +zerg\ultrokor-shadow.grp +zerg\zulbirth.grp +zerg\zulDeath.grp +zerg\zethrokor.grp +zerg\zethrokor-shadow.grp +zerg\zethrokor-birth.grp +zerg\zzebirth.lob +zerg\zethrokor-remains.grp +thingy\zAirDthL.grp +thingy\zAirDthS.grp +thingy\zBldDthS.grp +zerg\UCereb.grp +zerg\UCereb.lof +zerg\zucShad.grp +terran\control.grp +terran\control.lof +terran\control.lou +terran\control.lod +zerg\quazeth-pool.grp +zerg\quazeth-pool-damage.lof +zerg\quazeth-pool-shadow.grp +zerg\evolution-chamber.grp +zerg\evolution-chamber-damage.lof +zerg\evolution-chamber-shadow.grp +zerg\cagrant-cesiant.grp +zerg\cagrant-cesiant-damage.lof +zerg\cagrant-cesiant-shadow.grp +zerg\hachirosk.grp +zerg\hachirosk-damage.lof +zerg\hachirosk-shadow.grp +zerg\gathtelosk.grp +zerg\gathtelosk-damage.lof +zerg\gathtelosk-shadow.grp +zerg\caphrolosk.grp +zerg\caphrolosk-damage.lof +zerg\caphrolosk-shadow.grp +zerg\Lurker.grp +zerg\Lurker.lof +zerg\surkith-cesiant-shadow.grp +neutral\kerrChry.grp +neutral\\nkoShad.grp +zerg\vithrath-haunt.grp +zerg\vithrath-haunt-damage.lof +zerg\vithrath-haunt-shadow.grp +zerg\rakzogth-roost.grp +zerg\rakzogth-roost-damage.lof +zerg\rakzogth-roost-shadow.grp +zerg\matravil-nest.grp +zerg\matravil-nest-damage.lof +zerg\matravil-nest-shadow.grp +zerg\NydusPit.grp +zerg\NydusPit.lof +zerg\\nydus-cesiant-shadow.grp +zerg\Over1.grp +zerg\Over2.grp +zerg\ulgorg-cavern.grp +zerg\ulgorg-cavern-damage.lof +zerg\ulgorg-cavern-shadow.grp +zerg\Extract.grp +zerg\Extract.lof +zerg\Extract.los +zerg\excisant-shadow.grp +zerg\hydrith-den.grp +zerg\hydrith-den-damage.lof +zerg\hydrith-den-shadow.grp +zerg\mutath-spire.grp +zerg\mutath-spire-damage.lof +zerg\mutath-spire-shadow.grp +zerg\spraith-cesiant.grp +zerg\spraith-cesiant-damage.lof +zerg\spraith-cesiant-shadow.grp +zerg\Infest03.grp +zerg\ZBuild.grp +zerg\ZBShad.grp +zerg\zSpawn01.grp +zerg\zSpawn02.grp +zerg\zSpawn03.grp +thingy\ZRubbleS.grp +thingy\ZRubbleL.grp +protoss\solarion.grp +protoss\solarion-engine.grp +protoss\steward.grp +protoss\sshield.los +protoss\shuttle.grp +thingy\pshGlow.grp +protoss\dracadin.grp +protoss\dracadin-shadow.grp +protoss\dracadin-death.grp +protoss\templar.grp +protoss\pteShad.grp +protoss\dtemplar.grp +protoss\arbiter.grp +thingy\pabGlow.grp +protoss\archon.grp +protoss\archonT.grp +protoss\archonT.lol +protoss\archonT2.grp +protoss\scribe.grp +protoss\scout.grp +cm\p\panoptus-engine.grp +protoss\trilob.grp +protoss\ptrShad.grp +protoss\sapper.grp +protoss\witness.grp +protoss\zealot.grp +protoss\zealot-shadow.grp +protoss\archives.grp +protoss\archives.lof +protoss\pacShad.grp +protoss\assim.grp +protoss\assim.lof +protoss\assim.los +protoss\pasShad.grp +protoss\beacon.grp +protoss\beacon.lof +protoss\pbeShad.grp +protoss\citadel.grp +protoss\citadel.lof +protoss\pciShad.grp +protoss\forge.grp +protoss\forge.lof +protoss\forgeT.grp +protoss\pfoShad.grp +protoss\gateway.grp +protoss\gateway.lof +protoss\pgaShad.grp +protoss\gencore.grp +protoss\gencore.lof +protoss\gencoreT.grp +protoss\pgcShad.grp +neutral\Khyad01.grp +protoss\stasis.lof +protoss\\nexus.grp +protoss\lshield.los +protoss\\nexus.lof +protoss\pneGlow.grp +protoss\pneShad.grp +protoss\photon.grp +protoss\photon.lof +protoss\ppbShad.grp +protoss\prism.grp +protoss\prism.lof +protoss\pauShad.grp +protoss\pylon.grp +protoss\pylon.lof +protoss\ppyShad.grp +protoss\robotic.grp +protoss\robotic.lof +protoss\proShad.grp +protoss\bulwark.grp +protoss\bulwark-damage.lof +protoss\bulwark-overlay.grp +protoss\bulwark-shadow.grp +protoss\stargate.grp +protoss\stargate.lof +protoss\psgGlow.grp +protoss\psgShad.grp +neutral\stasis.grp +protoss\stasis.grp +protoss\pstShad.grp +neutral\temple.grp +protoss\warp.grp +protoss\warp.lof +protoss\texture.grp +protoss\pb1Glow.grp +protoss\pwaShad.grp +thingy\tBangS.grp +thingy\tBangL.grp +thingy\tBangX.grp +thingy\PRubbleS.grp +thingy\PRubbleL.grp +terran\BattleCr.grp +terran\BattleCr.lox +terran\BattleCr.lol +thingy\tbaGlow.grp +neutral\civilian.grp +neutral\\nciShad.grp +terran\trojan.grp +terran\dropship.lou +terran\dropship.lod +terran\trojan-engine.grp +terran\harakan.grp +terran\harakan-shadow.grp +terran\eidolon.grp +terran\eidolon-shadow.grp +terran\eidolon-remains.grp +thingy\NukeBeam.grp +thingy\NukeTarg.grp +terran\goliath.grp +terran\goliath.lol +terran\goliathT.grp +terran\tgoShad.grp +terran\ughost.grp +terran\ughShad.grp +terran\maverick.grp +terran\maverick-shadow.grp +terran\maverick-death.grp +terran\wraith.grp +terran\phoenix.lol +thingy\tphGlow.grp +terran\mason.grp +terran\mason.loo +terran\mason-engine.grp +terran\phalanx-tank.grp +terran\phalanx-tank-attack.lol +terran\phalanx-tank-turret.grp +terran\phalanx-tank-shadow.grp +terran\phalanx-siege.grp +terran\phalanx-siege-attack.lol +terran\phalanx-siege-turret.grp +terran\phalanx-siege.loa +terran\phalanx-siege-shadow.grp +terran\Vulture.grp +terran\Spider.grp +terran\tsmShad.grp +terran\wessel.grp +terran\wessel.lol +terran\wessel.lou +terran\wessel.lod +terran\wesselt.grp +terran\tveShad.grp +terran\Academy.grp +terran\academy.lof +terran\AcademyT.grp +terran\tacShad.grp +terran\TBarrack.grp +terran\tbarrack.lof +terran\tbarrack.lou +terran\tbarrack.lod +terran\tbrShad.grp +terran\chemlab.grp +terran\chemlab.lof +terran\chemlabT.grp +terran\tclShad.grp +terran\ComSat.grp +terran\comsat.lof +terran\ComSatC.grp +terran\ComSatT.grp +terran\tcsShad.grp +terran\controlT.grp +terran\tccShad.grp +terran\Depot.grp +terran\depot.lof +terran\DepotT.grp +terran\tdeShad.grp +terran\DryDocks.grp +terran\drydocks.lof +terran\DryDockC.grp +terran\DryDockT.grp +terran\tddShad.grp +terran\factory.grp +terran\factory.lof +terran\factory.lou +terran\factory.lod +terran\factoryT.grp +terran\tfaShad.grp +terran\GeneLab.grp +terran\genelab.lof +terran\GeneLabC.grp +terran\GeneLabT.grp +terran\tglShad.grp +neutral\ion.grp +terran\machines.grp +terran\machines.lof +terran\machineC.grp +terran\tmsShad.grp +terran\missile.grp +terran\missile.lof +terran\missileT.grp +terran\tmiShad.grp +neutral\Cbattle.grp +terran\physics.lof +neutral\cbaShad.grp +terran\physics.grp +terran\physicsC.grp +terran\tplShad.grp +terran\anchor.grp +terran\anchor.loa +terran\anchor-damage.lof +terran\anchor-shadow.grp +terran\anchor-attack-orange.grp +terran\refinery.grp +terran\refinery.lof +terran\refinery.los +terran\treShad.grp +terran\research.grp +terran\research.lof +terran\research.lou +terran\research.lod +terran\ResearcT.grp +terran\trlShad.grp +terran\\nukesilo.grp +terran\\nukesilo.lof +terran\NukeSilC.grp +terran\NukeSilT.grp +terran\tnsShad.grp +terran\\nukemiss.grp +thingy\NukeHit.grp +terran\starport.grp +terran\starport.lof +terran\starport.lou +terran\starport.lod +terran\StarpoT.grp +terran\tspShad.grp +terran\weaponpl.grp +terran\weaponpl.lof +terran\weaponpl.lou +terran\weaponpl.lod +terran\weaponpT.grp +terran\twpShad.grp +terran\TBldLrg.grp +terran\tb2Shad.grp +terran\tBldMed.grp +terran\tb3Shad.grp +terran\TBldSml.grp +terran\tb1Shad.grp +thingy\RubbleS.grp +thingy\RubbleL.grp +thingy\Dswarm.grp +neutral\Acritter.grp +neutral\\nacShad.grp +neutral\Bcritter.grp +neutral\\nbcShad.grp +neutral\Jcritter.grp +neutral\\njcShad.grp +neutral\geyser.grp +neutral\geyser.los +neutral\geyShad.grp +neutral\min01.grp +neutral\min01Sha.grp +neutral\min02.grp +neutral\min02Sha.grp +neutral\min03.grp +neutral\min03Sha.grp +zerg\zmarker.grp +zerg\zCirGlow.grp +terran\tMarker.grp +terran\tCirGlow.grp +protoss\pMarker.grp +protoss\pCirGlow.grp +thingy\eldFire.grp +thingy\eldSmall.grp +thingy\eldMed.grp +thingy\eldLarge.grp +thingy\esfHit.grp +thingy\esfSmall.grp +thingy\esfLarge.grp +thingy\sbaSmall.grp +thingy\sbaLarge.grp +thingy\edmFronS.grp +thingy\edmFronM.grp +thingy\edmFronL.grp +thingy\edmBackS.grp +thingy\edmBackM.grp +thingy\edmBackL.grp +thingy\edmHitS.grp +thingy\edmHitM.grp +thingy\edmHitL.grp +thingy\eradLrg.grp +thingy\Ensnare.grp +thingy\ensGooS.grp +thingy\ensGooM.grp +thingy\ensGooL.grp +thingy\ebbCloud.grp +thingy\ebbHitS.grp +thingy\ebbHitM.grp +thingy\ebbHitL.grp +thingy\recall.grp +neutral\Flag.grp +neutral\KerrEgg.grp +neutral\PsiEmit.grp +neutral\DataDisk.grp +neutral\KhChunk.grp +neutral\OreChunk.grp +neutral\GasOrb.grp +neutral\GasSac.grp +neutral\GasTank.grp +neutral\\norShad.grp +neutral\\ngoShad.grp +neutral\\ngsShad.grp +neutral\\ngcShad.grp +neutral\\nddShad.grp +neutral\\nddSha2.grp +neutral\\nflShad.grp +neutral\\nflSha2.grp +neutral\\nkhShad.grp +neutral\\nkhSha2.grp +neutral\\nkeShad.grp +neutral\\nkeSha2.grp +neutral\\npsShad.grp +neutral\\npsSha2.grp +thingy\plasma.grp +thingy\PlasDrip.grp +thingy\flamer.grp +thingy\smoke.grp +thingy\bDust.grp +thingy\pshield.grp +thingy\explo2.grp +thingy\dbl_exp.grp +thingy\HKexplod.grp +thingy\small.grp +thingy\tmnExplo.grp +thingy\GeySmok1.grp +thingy\GeySmok2.grp +thingy\GeySmok3.grp +thingy\GeySmok4.grp +thingy\GeySmok5.grp +thingy\GeySmoS1.grp +thingy\efgHit.grp +thingy\GreSmoke.grp +thingy\HKTrail.grp +thingy\psaExplo.grp +thingy\blackx.grp +thingy\elbBat.grp +thingy\elbHit.grp +thingy\elbMuzz.grp +thingy\pteglow.grp +thingy\oFireC.grp +thingy\oFireF.grp +thingy\oFireV.grp +thingy\bblood01.grp +thingy\bblood02.grp +thingy\bblood03.grp +thingy\bblood04.grp +thingy\dust01.grp +thingy\dust02.grp +thingy\dust03.grp +thingy\dust04.grp +thingy\dust05.grp +thingy\dust06.grp +thingy\dust07.grp +thingy\dust08.grp +thingy\dust09.grp +bullet\circle14.grp +bullet\zspark.grp +bullet\PDripHit.grp +bullet\Tentacle.grp +thingy\ep2Fire.grp +thingy\etgHit.grp +thingy\ep3Shot.grp +thingy\ep3Burst.grp +bullet\spores.grp +thingy\SporeHit.grp +thingy\SpoTrail.grp +thingy\gSmoke.grp +bullet\Spore2.grp +bullet\parasite.grp +thingy\consume.grp +thingy\eplMuzz.grp +bullet\pspark.grp +bullet\ephFire.grp +bullet\hks.grp +bullet\blastcan.grp +bullet\dragbull.grp +bullet\epbBul.grp +thingy\psiStorm.grp +bullet\scvspark.grp +bullet\tspark.grp +bullet\gemini.grp +bullet\missile.grp +thingy\ecaHit.grp +bullet\grenade.grp +thingy\mushroom.grp +thingy\elbfire.grp +thingy\elbfireW.grp +thingy\ettFlash.grp +thingy\esiFire.grp +thingy\tveGlowB.grp +thingy\tveGlowO.grp +thingy\tveGlowW.grp +bullet\eycBull.grp +thingy\eycBlast.grp +thingy\eycHit.grp +thingy\halmind.grp +thingy\eveCast.grp +thingy\emsHit.grp +protoss\archon.lob +thingy\emsBeam.grp +thingy\elect02a.grp +thingy\elect02.grp +thingy\emp.grp +thingy\empl.grp +thingy\ehaMed.grp +thingy\o022.grp +thingy\o032.grp +thingy\o048.grp +thingy\o062.grp +thingy\o072.grp +thingy\o094.grp +thingy\o110.grp +thingy\o122.grp +thingy\o146.grp +thingy\o224.grp +thingy\od022.grp +thingy\od032.grp +thingy\od048.grp +thingy\od062.grp +thingy\od072.grp +thingy\od094.grp +thingy\od110.grp +thingy\od122.grp +thingy\od146.grp +thingy\od224.grp +neutral\maprev.grp +thingy\Juice01.grp +thingy\Juice02.grp +thingy\StartLoc.grp +thingy\tileset\AshWorld\rock01.grp +thingy\tileset\AshWorld\Rock1Sha.grp +thingy\tileset\AshWorld\rock02.grp +thingy\tileset\AshWorld\Rock2Sha.grp +thingy\tileset\AshWorld\rock03.grp +thingy\tileset\AshWorld\Rock3Sha.grp +thingy\tileset\AshWorld\rock04.grp +thingy\tileset\AshWorld\Rock4Sha.grp +thingy\tileset\AshWorld\rock05.grp +thingy\tileset\AshWorld\Rock5Sha.grp +thingy\tileset\AshWorld\LALRoc01.grp +thingy\tileset\AshWorld\LALRoc02.grp +thingy\tileset\AshWorld\HASRoc01.grp +thingy\tileset\AshWorld\HASRoc02.grp +thingy\tileset\AshWorld\HASRoc03.grp +thingy\tileset\AshWorld\HASRoc04.grp +thingy\tileset\AshWorld\HASRoc05.grp +thingy\tileset\AshWorld\HASRoc06.grp +thingy\tileset\AshWorld\RORock01.grp +thingy\tileset\AshWorld\RORock02.grp +thingy\tileset\AshWorld\RORock03.grp +thingy\tileset\Jungle\HDRock01.grp +thingy\tileset\Jungle\HDRock1s.grp +thingy\tileset\Jungle\HDRock02.grp +thingy\tileset\Jungle\HDRock2s.grp +thingy\tileset\Jungle\HDRock03.grp +thingy\tileset\Jungle\HDRock3s.grp +thingy\tileset\Jungle\HDRock04.grp +thingy\tileset\Jungle\HDRock4s.grp +thingy\tileset\Jungle\JUbush01.grp +thingy\tileset\Jungle\JUbush1S.grp +thingy\tileset\Jungle\JUbush03.grp +thingy\tileset\Jungle\JUbush3s.grp +thingy\tileset\Jungle\JUbush05.grp +thingy\tileset\Jungle\JUbush5s.grp +thingy\tileset\Jungle\LDtree01.grp +thingy\tileset\Jungle\LDtree1s.grp +thingy\tileset\Jungle\LDtree02.grp +thingy\tileset\Jungle\LDtree2s.grp +thingy\tileset\Jungle\LDtree03.grp +thingy\tileset\Jungle\LDtree3s.grp +thingy\tileset\Jungle\LDtree04.grp +thingy\tileset\Jungle\LDtree4s.grp +thingy\tileset\Jungle\tree01.grp +thingy\tileset\Jungle\tree1sha.grp +thingy\tileset\Jungle\tree02.grp +thingy\tileset\Jungle\tree2sha.grp +thingy\tileset\Jungle\tree03.grp +thingy\tileset\Jungle\tree3sha.grp +thingy\tileset\Jungle\tree04.grp +thingy\tileset\Jungle\tree4sha.grp +thingy\tileset\Jungle\dd025.grp +thingy\tileset\Jungle\dd026.grp +thingy\tileset\Jungle\dd027.grp +thingy\tileset\Jungle\dd028.grp +thingy\tileset\Jungle\dd029.grp +thingy\tileset\Jungle\dd030.grp +thingy\tileset\Jungle\dd031.grp +thingy\tileset\Jungle\dd055.grp +thingy\tileset\Jungle\dd056.grp +thingy\tileset\Jungle\dd075.grp +thingy\tileset\Jungle\dd076.grp +thingy\tileset\Jungle\dd077.grp +thingy\tileset\Jungle\dd078.grp +thingy\tileset\Jungle\dd079.grp +thingy\tileset\Jungle\dd080.grp +thingy\tileset\Jungle\dd081.grp +thingy\tileset\Jungle\dd091.grp +thingy\tileset\Jungle\dd203.grp +thingy\tileset\Jungle\dd204.grp +thingy\tileset\Jungle\dd205.grp +thingy\tileset\Jungle\dd206.grp +thingy\tileset\Jungle\dd207.grp +thingy\tileset\Jungle\dd209.grp +thingy\tileset\Jungle\dd210.grp +thingy\tileset\Jungle\dd211.grp +thingy\tileset\Platform\Dish01.grp +thingy\tileset\Platform\Dish1Sha.grp +thingy\tileset\Platform\Dish02.grp +thingy\tileset\Platform\Dish2Sha.grp +thingy\tileset\Platform\Dish03.grp +thingy\tileset\Platform\Dish3Sha.grp +thingy\tileset\Platform\Glob01.grp +thingy\tileset\Platform\Glob1Sha.grp +thingy\tileset\Platform\Glob02.grp +thingy\tileset\Platform\Glob2Sha.grp +thingy\tileset\Platform\Glob03.grp +thingy\tileset\Platform\Glob3Sha.grp +thingy\tileset\Platform\Towr01.grp +thingy\tileset\Platform\Towr02.grp +thingy\tileset\Platform\tree01.grp +thingy\tileset\Platform\tree02.grp +thingy\tileset\Platform\LBSign01.grp +thingy\tileset\Platform\LBSign02.grp +thingy\tileset\Platform\LBSign03.grp +thingy\tileset\Platform\LBSign04.grp +thingy\tileset\Platform\LBSign05.grp +thingy\tileset\Platform\LBSign06.grp +thingy\tileset\Platform\LBSign07.grp +thingy\tileset\Platform\LBSign08.grp +thingy\tileset\Platform\SPThin01.grp +thingy\tileset\Platform\refinery.grp +thingy\tileset\Badlands\HDRock01.grp +thingy\tileset\Badlands\HDRock1S.grp +thingy\tileset\Badlands\HDRock02.grp +thingy\tileset\Badlands\HDRock2S.grp +thingy\tileset\Badlands\HDRock03.grp +thingy\tileset\Badlands\HDRock3S.grp +thingy\tileset\Badlands\HDRock04.grp +thingy\tileset\Badlands\HDRock4S.grp +thingy\tileset\Badlands\HDRock05.grp +thingy\tileset\Badlands\HDRock06.grp +thingy\tileset\Badlands\HDRock07.grp +thingy\tileset\Badlands\HDRock08.grp +thingy\tileset\Badlands\HDVent01.grp +thingy\tileset\Badlands\HDTree01.grp +thingy\tileset\Badlands\LDTree1S.grp +thingy\tileset\Badlands\HDTree02.grp +thingy\tileset\Badlands\LDTree2S.grp +thingy\tileset\Badlands\HDTree03.grp +thingy\tileset\Badlands\LDTree3S.grp +thingy\tileset\Badlands\HDTree04.grp +thingy\tileset\Badlands\LDTree4S.grp +thingy\tileset\Badlands\HGTree01.grp +thingy\tileset\Badlands\LCShop01.grp +thingy\tileset\Badlands\LCShop02.grp +thingy\tileset\Badlands\LCShop03.grp +thingy\tileset\Badlands\LCShop04.grp +thingy\tileset\Badlands\LCShop05.grp +thingy\tileset\Badlands\LCShop06.grp +thingy\tileset\Badlands\LCShop07.grp +thingy\tileset\Badlands\LCShop08.grp +thingy\tileset\Badlands\LCShop09.grp +thingy\tileset\Badlands\LCShopAA.grp +thingy\tileset\Badlands\LCSign01.grp +thingy\tileset\Badlands\LCSign02.grp +thingy\tileset\Badlands\LCSign03.grp +thingy\tileset\Badlands\LCSign04.grp +thingy\tileset\Badlands\LCSign05.grp +thingy\tileset\Badlands\LCSign06.grp +thingy\tileset\Badlands\LCSign07.grp +thingy\tileset\Badlands\LCSign08.grp +thingy\tileset\Badlands\LCSign09.grp +thingy\tileset\Badlands\LCSignAA.grp +thingy\tileset\Badlands\LCSignBB.grp +thingy\tileset\Badlands\LCSignCC.grp +thingy\tileset\install\CLPlate2.grp +thingy\tileset\install\CLPlate1.grp +thingy\tileset\install\CLPlat1T.grp +thingy\tileset\install\DCGun1.grp +thingy\tileset\install\DCGun2.grp +thingy\tileset\install\DCGun2.loa +thingy\tileset\install\CRDoor1.grp +thingy\tileset\install\DCFan1.grp +thingy\tileset\install\DICran1.grp +thingy\tileset\install\DICran2.grp +thingy\tileset\install\DICran3.grp +thingy\tileset\install\DICran4.grp +thingy\tileset\install\DIDoor1.grp +thingy\tileset\install\DIGear1.grp +thingy\tileset\install\DIGear2.grp +thingy\tileset\install\DIHatc1.grp +thingy\tileset\twilight\Ldarch.grp +thingy\tileset\twilight\Lddrill.grp +thingy\tileset\twilight\Ldxel01.grp +thingy\tileset\twilight\Ldxel02.grp +thingy\tileset\twilight\Ldxel03.grp +thingy\tileset\twilight\Ldxel04.grp +thingy\tileset\twilight\Ldxel05.grp +thingy\tileset\twilight\Ldxel06.grp +thingy\tileset\twilight\Ldxeltur.grp +thingy\tileset\twilight\JTree01.grp +thingy\tileset\twilight\JTree02.grp +thingy\tileset\twilight\JTree03.grp +thingy\tileset\twilight\JTree04.grp +thingy\tileset\twilight\JTree05.grp +thingy\tileset\twilight\rstatue.grp +thingy\tileset\ice\HDbld01.grp +thingy\tileset\ice\SHDbld01.grp +thingy\tileset\ice\HDbld02.grp +thingy\tileset\ice\SHDbld02.grp +thingy\tileset\ice\HDbld03.grp +thingy\tileset\ice\SHDbld03.grp +thingy\tileset\ice\HDbld04.grp +thingy\tileset\ice\SHDbld04.grp +thingy\tileset\ice\HDradarl.grp +thingy\tileset\ice\SHDradrl.grp +thingy\tileset\ice\HDradarR.grp +thingy\tileset\ice\SHDradrR.grp +thingy\tileset\ice\HDRock01.grp +thingy\tileset\ice\SHDRck01.grp +thingy\tileset\ice\HDRock02.grp +thingy\tileset\ice\SHDRck02.grp +thingy\tileset\ice\HDSTre01.grp +thingy\tileset\ice\HDSTre02.grp +thingy\tileset\ice\HDSTre03.grp +thingy\tileset\ice\HDSTre04.grp +thingy\tileset\ice\HDSpire.grp +thingy\tileset\ice\SHDSpire.grp +thingy\tileset\ice\HDTwr01.grp +thingy\tileset\ice\SHDTwr01.grp +thingy\tileset\ice\HDTwr02.grp +thingy\tileset\ice\SHDTwr02.grp +thingy\tileset\ice\LDBTre01.grp +thingy\tileset\ice\SLDBTre1.grp +thingy\tileset\ice\LDBTre02.grp +thingy\tileset\ice\SLDBTre2.grp +thingy\tileset\ice\LDBTre03.grp +thingy\tileset\ice\SLDBTre3.grp +thingy\tileset\ice\LDBTre04.grp +thingy\tileset\ice\SLDBTre4.grp +thingy\tileset\ice\LDbld01.grp +thingy\tileset\ice\SLDbld01.grp +thingy\tileset\ice\LDbld02.grp +thingy\tileset\ice\SLDbld02.grp +thingy\tileset\ice\LDComm.grp +thingy\tileset\ice\SLDComm.grp +thingy\tileset\ice\LDDtre01.grp +thingy\tileset\ice\SLDDtre1.grp +thingy\tileset\ice\LDDtre02.grp +thingy\tileset\ice\SLDDtre2.grp +thingy\tileset\ice\LDDish.grp +thingy\tileset\ice\SLDDish.grp +thingy\tileset\ice\LDRdr01.grp +thingy\tileset\ice\SLDRdr01.grp +thingy\tileset\ice\LDRdr02.grp +thingy\tileset\ice\SLDRdr02.grp +thingy\tileset\ice\LDRdr03.grp +thingy\tileset\ice\SLDRdr03.grp +thingy\tileset\ice\LDRck01.grp +thingy\tileset\ice\SLDRck01.grp +thingy\tileset\ice\LDRck02.grp +thingy\tileset\ice\SLDRck02.grp +thingy\tileset\ice\HDPipes.grp +thingy\tileset\ice\SHDPipes.grp +thingy\tileset\ice\LDthing.grp +thingy\tileset\ice\SLDthing.grp +thingy\tileset\ice\LDsmrock.grp +thingy\tileset\ice\HDRadr02.grp +thingy\tileset\ice\SHDRadr2.grp +thingy\tileset\ice\JGant1.grp +thingy\tileset\ice\RJBTree1.grp +thingy\tileset\ice\sRJBTre1.grp +thingy\tileset\ice\RJBTree2.grp +thingy\tileset\ice\sRJBTre2.grp +thingy\tileset\ice\RJBTree3.grp +thingy\tileset\ice\sRJBTre3.grp +thingy\tileset\ice\RJBTree4.grp +thingy\tileset\ice\sRJBTre4.grp +thingy\tileset\desert\HDBANT.grp +thingy\tileset\desert\HDBBROKE.grp +thingy\tileset\desert\SHDBBRKE.grp +thingy\tileset\desert\HDBGAS.grp +thingy\tileset\desert\SHDBGAS.grp +thingy\tileset\desert\HDBMED.grp +thingy\tileset\desert\SHDBMED.grp +thingy\tileset\desert\HDBMOSS.grp +thingy\tileset\desert\SHDBMOSS.grp +thingy\tileset\desert\HDBTENT.grp +thingy\tileset\desert\SHDBTENT.grp +thingy\tileset\desert\HDPLNT03.grp +thingy\tileset\desert\SHDPLNT3.grp +thingy\tileset\desert\JGBROKE.grp +thingy\tileset\desert\SJGBROKE.grp +thingy\tileset\desert\JGBGAS.grp +thingy\tileset\desert\SJGBGAS.grp +thingy\tileset\desert\JGBGEN.grp +thingy\tileset\desert\SJGBGEN.grp +thingy\tileset\desert\JGBSGN.grp +thingy\tileset\desert\SJGBSGN.grp +thingy\tileset\desert\JGBTENT.grp +thingy\tileset\desert\SJGBTENT.grp +thingy\tileset\desert\JGBCOMM.grp +thingy\tileset\desert\SJGBCOMM.grp +thingy\tileset\desert\JGBFACT.grp +thingy\tileset\desert\SJGBFACT.grp +thingy\tileset\desert\JGBRED.grp +thingy\tileset\desert\SJGBRED.grp +thingy\tileset\desert\JGPLNT01.grp +thingy\tileset\desert\JGPLNT02.grp +thingy\tileset\desert\SJGPLNT2.grp +thingy\tileset\desert\LDBAZ.grp +thingy\tileset\desert\SLDBAZ.grp +thingy\tileset\desert\LDBGAS.grp +thingy\tileset\desert\SLDBGAS.grp +thingy\tileset\desert\LDBGREN.grp +thingy\tileset\desert\SLDBGREN.grp +thingy\tileset\desert\LDBNEON.grp +thingy\tileset\desert\LDBSUKY.grp +thingy\tileset\desert\SLDBSUKY.grp +thingy\tileset\desert\LDBTENT.grp +thingy\tileset\desert\SLDBTENT.grp +thingy\tileset\desert\Ldneon.grp +thingy\tileset\desert\HDLbox01.grp +thingy\tileset\desert\SHDLbox1.grp +thingy\tileset\desert\HDMachn2.grp +thingy\tileset\desert\LDLbox01.grp +thingy\tileset\desert\SLDLbox1.grp +thingy\tileset\desert\LDMachn1.grp +thingy\tileset\desert\SLDMchn1.grp +thingy\tileset\desert\LDPlnt04.grp +thingy\tileset\desert\SLDPlnt4.grp +thingy\tileset\desert\Tgas.grp +thingy\tileset\desert\sarlacc.grp +zerg\Lurkegg.grp +zerg\vithrilisk.grp +thingy\zDvbirth.grp +zerg\zdvDeath.grp +zerg\zLubirth.grp +zerg\lakizilisk.grp +zerg\lakizilisk.grp +zerg\zZluShad.grp +zerg\overmind-cocoon.grp +zerg\ZovShad.grp +protoss\darchn.grp +protoss\darchnT.grp +protoss\darchnT.lol +protoss\darchnT2.grp +protoss\PdaDeath.grp +protoss\corsair.grp +thingy\pcsGlow.grp +thingy\elbFireC.grp +protoss\XwarpGat.grp +protoss\pwgShad.grp +protoss\XwarpFir.grp +protoss\XelTempl.grp +protoss\pxtShad.grp +terran\valkyrie.grp +terran\valkyrie-engine.grp +thingy\tbmAfter.grp +terran\cleric.grp +terran\cleric-shadow.grp +terran\cleric-remnants.grp +neutral\PsiDisr.grp +neutral\tpdShad.grp +neutral\Generate.grp +neutral\tgnShad.grp +thingy\Disrupt.grp +neutral\Kcritter.grp +neutral\\nckShad.grp +neutral\Scritter.grp +neutral\Icritter.grp +neutral\\ncicShad.grp +neutral\Uraj.grp +neutral\Khalis.grp +thingy\bsmoke.grp +bullet\Spike.grp +thingy\ZDvPuke.grp +thingy\ZDvHit.grp +thingy\PcsSplsh.grp +bullet\SMmissle.grp +thingy\tCureSml.grp +thingy\tCureMed.grp +thingy\tCureLrg.grp +thingy\tmeHealM.grp +thingy\tmeHealL.grp +thingy\PDaMyoSm.grp +thingy\PDaMyoMd.grp +thingy\PDaMyoLg.grp +thingy\TmeFlshS.grp +thingy\TmeFlshM.grp +thingy\TmeFlshL.grp +thingy\PDaPsySm.grp +thingy\PDaPsyMd.grp +thingy\PDaPsyLg.grp +thingy\PDaParSm.grp +thingy\PDaParMd.grp +thingy\PDaParLg.grp +thingy\ZdvGooS.grp +thingy\ZdvGooM.grp +thingy\ZdvGooL.grp +thingy\MaelHit.grp +cm\t\quarry.grp +cm\p\pennant.grp +cm\z\iroleth.grp +cm\t\ancile-engine.grp +cm\t\paladin.grp +cm\t\madrigal-base.grp +cm\t\savant-siphon.grp +cm\t\scrapyard.grp +cm\t\paladin-shadow.grp +cm\t\rotary.grp +cm\t\hippogriff.grp +cm\t\facade.grp +cm\t\facade-shadow.grp +cm\t\quarry-shadow.grp +cm\z\gorgrokor.grp +cm\p\ecclesiast.grp +cm\p\ecclesiast-shadow.grp +cm\p\atreus.grp +cm\p\atreus-shadow.grp +cm\p\amaranth.grp +cm\p\amaranth-shadow.grp +cm\t\facade-damage.lof +cm\t\wyvern.grp +cm\p\aurora.grp +cm\p\panoptus.grp +cm\t\cuirass.grp +cm\t\centaur.grp +cm\t\centaur-missile.grp +cm\t\centaur-engine.grp +cm\e\parasite-overlay.grp +cm\t\azazel-engine.grp +cm\dummy.grp +cm\t\paladin-attack.lol +cm\t\hippogriff-engine.grp +cm\t\scrapyard-shadow.grp +cm\p\augur.grp +cm\p\augur-energy.grp +cm\dummy.grp +cm\z\kalkiir-lake.grp +cm\z\kalkiir-lake-shadow.grp +cm\z\othstol-oviform.grp +cm\z\othstol-oviform-shadow.grp +cm\z\kalkiir-lake-damage.lof +cm\z\othstol-oviform-damage.lof +cm\t\oberon.grp +cm\t\oberon-engines.grp +cm\t\shaman.grp +cm\z\alaszil-arbor.grp +cm\z\alaszil-arbor-shadow.grp +cm\z\alaszil-arbor-damage.lof +cm\z\othstoleth.grp +cm\p\grand-library.grp +cm\p\grand-library-shadow.grp +cm\p\grand-library-damage.lof +cm\z\irol-iris.grp +cm\z\irol-iris-shadow.grp +cm\z\irol-iris-damage.lof +cm\z\zarcavrokor.grp +cm\\n\khaydarin-formation-shadow.grp +cm\t\treasury.grp +cm\t\treasury-shadow.grp +cm\t\treasury-damage.lof +cm\z\sire.grp +cm\z\sire-shadow.grp +cm\z\sire-damage.lof +cm\e\treasury-overlay.grp +cm\e\\nanitefield-overlay.grp +cm\e\cowardice-small.grp +cm\e\cowardice-medium.grp +cm\e\cowardice-large.grp +cm\e\malice-small.grp +cm\e\malice-medium.grp +cm\e\malice-large.grp +cm\e\malediction-small.grp +cm\e\malediction-medium.grp +cm\e\malediction-large.grp +cm\t\rotary-damage.lof +cm\t\rotary-shadow.grp +cm\p\simulacrum.grp +cm\p\simulacrum-shadow.grp +cm\t\savant-siphon-shadow.grp +cm\t\shaman-shadow.grp +cm\\n\temple-shadow.grp +cm\z\overmind-shadow.grp +cm\p\automatonregister.grp +cm\p\automatonregister-shadow.grp +cm\p\automatonregister-damage.lof +cm\z\overmind-shell-shadow.grp +cm\p\vagrant.grp +cm\t\crashed-minotaur-damage.lof +cm\p\vagrant-shadow.grp +cm\p\herald.grp +cm\t\psi-disrupter-damage.lof +cm\p\astralomen.grp +cm\p\astralomen-shadow.grp +cm\p\astralomen-damage.lof +cm\p\herald-shadow.grp +cm\p\epigraph.grp +cm\t\power-generator-damage.lof +cm\t\parthenon.grp +cm\t\parthenon-shadow.grp +cm\t\parthenon-damage.lof +cm\z\\nathrokor.grp +cm\z\\nathrokor-attack.lol +cm\e\\nathrokor-breath.grp +cm\p\empyrean.grp +cm\p\empyrean-attack.lol +cm\p\epigraph-engine.grp +cm\z\tethzorokor.grp +cm\p\khaydarin-formation-damage.lof +cm\p\phaselink-small.grp +cm\p\phaselink-medium.grp +cm\p\phaselink-large.grp +cm\p\exemplar-weaponhit.grp +cm\p\augur-death.grp +cm\e\energydecay.grp +cm\t\savant-form-switch.grp +cm\p\starsovereign.grp +cm\z\alkajelisk.grp +cm\z\keskathalor.grp +cm\z\keskathalor-shadow.grp +cm\t\apostle.grp +cm\t\apostle-shadow.grp +cm\t\apostle-death.grp +cm\p\exemplar-weapon.grp +cm\z\tethzorokor-shadow.grp +cm\p\temple-damage.lof +cm\z\infestedtemplar.grp +cm\z\infestedtemplar-shadow.grp +cm\p\argosy.grp +cm\p\argosy-shadow.grp +cm\p\argosy-damage.lof +cm\t\\naniteassembly.grp +cm\t\\naniteassembly-shadow.grp +cm\t\\naniteassembly-damage.lof +cm\t\hadron.grp +cm\t\hadron-shadow.grp +cm\t\hadron-damage.lof +cm\p\crucible.grp +cm\p\crucible-shadow.grp +cm\p\crucible-damage.lof +cm\t\aion.grp +cm\z\protathalor.grp +cm\p\stasis-cell-damage.lof +cm\p\gladius.grp +cm\p\magister.grp +cm\p\vassal.grp +cm\t\madcap.grp +cm\t\madcap-shadow.grp +cm\t\salamander.grp +cm\t\salamander-missile.grp +cm\t\gorgon.grp +cm\z\protathalor-shadow.grp +cm\t\phobos.grp +cm\t\penumbra.grp +cm\t\penumbra-shadow.grp +cm\t\cyclops.grp +cm\t\cyclops-shadow.grp +cm\t\phobos-missile.grp +cm\t\phobos-laser.grp +cm\t\phobos-attack.lol +cm\t\magnetar-yamato.lol +cm\t\phobos-engine.grp +cm\p\sanctumofsorrow.grp +cm\p\sanctumofsorrow-shadow.grp +cm\p\sanctumofsorrow-damage.lof +thingy\phohit.grp +thingy\pexplode.grp +cm\t\salamander-engine.grp +cm\z\vilgorokor.grp +cm\t\phobos-laser-impact.grp +cm\p\xelnaga-temple-damage.lof +cm\t\centaur-attackmissile.lol +cm\t\salamander-missile-impact.grp +cm\z\geszkath-grotto.grp +cm\z\geszkath-grotto-shadow.grp +cm\z\geszkath-grotto-damage.lof +cm\z\vilgorokor-shadow.grp +cm\p\warp-gate-damage.lof +cm\z\cikralisk.grp +cm\z\alkaj-chasm.grp +cm\z\alkaj-chasm-shadow.grp +cm\z\alkaj-chasm-damage.lof +cm\p\idol.grp +cm\p\idol-shadow.grp +cm\p\golem.grp +cm\p\golem-shadow.grp +cm\p\barghest.grp +cm\p\synthetic-synod.grp +cm\p\synthetic-synod-shadow.grp +cm\p\synthetic-synod-damage.lof +cm\p\pariah.grp +cm\p\pariah-shadow.grp +cm\t\talos.grp +cm\p\pariah-attack.grp +cm\p\pariah-attack.lol +cm\t\cuirass-shadow.grp +cm\t\gorgon-engine.grp +cm\p\aurora-engine.grp +cm\dummy.grp +cm\t\wyvern-engine.grp +cm\p\empyrean-engine.grp +cm\p\exemplar-engine.grp +cm\p\gladius-engine.grp +cm\p\starsovereign-engine.grp +cm\p\vassal-engine.grp +cm\z\bactalisk.grp +cm\z\bactalisk-shadow.grp +cm\p\pennant-shadow.grp +cm\p\pennant-damage.lof +cm\t\cyclops-weaponhit.grp +cm\p\maelstrom-weapon.grp +cm\p\gladius-weapon.grp +cm\z\kalkalisk.grp +cm\z\zorkiz-shroud.grp +cm\z\zorkiz-shroud-shadow.grp +cm\z\zorkiz-shroud-damage.lof +cm\z\kalkalisk-attack.lol +cm\p\manifold.grp +cm\p\manifold-shadow.grp +cm\p\architect.grp +cm\p\architect-engine.grp +cm\z\\nathrokor-birth.grp +cm\z\alkajelisk-birth.grp +cm\z\cikralisk-shadow.grp +cm\z\overmind-damage.lof +cm\z\ghitorokor.grp +cm\z\zobriolisk.grp +cm\z\overmind-shell-damage.lof +cm\z\bactalisk-attack.lol +cm\e\observance-casteffect.grp +cm\e\observance-large.grp +cm\e\observance-medium.grp +cm\e\observance-small.grp +cm\t\harakan-remnants.grp +cm\z\bactalisk-birth.grp +cm\z\bactalisk-death.grp +cm\t\heavy-missile.grp +cm\t\sundog-attack.lol +cm\p\epigraph-attack.lol +cm\z\keskathalor-birth.grp +cm\t\madrigal-missile.grp +cm\t\savant-weaponhit.grp +cm\p\starsovereign-attack.lol +cm\e\planetcracker-init.grp +cm\e\planetcracker-beam.grp +cm\e\planetcracker-blast-loop.grp +cm\e\planetcracker-blast.grp +cm\z\keskathalor-attack.lol +cm\t\diadem-base.grp +cm\t\diadem-turret.grp +cm\t\diadem-attackA2.grp +cm\t\diadem-damage.lof +cm\t\diadem-overlay.lol +cm\t\diadem-shadow.grp +cm\p\starsovereign-behavior.lol +cm\t\penumbra-attack.lol +cm\dummy.grp +cm\e\lazarusagent-revive.grp +cm\t\wraith-attacklaser.lol +cm\e\sublimeshepherd-overlay.grp +cm\e\khaydarineclipse-large.grp +cm\e\khaydarineclipse-medium.grp +cm\e\khaydarineclipse-small.grp +cm\e\cruciblepennant-large.grp +cm\e\cruciblepennant-medium.grp +cm\e\cruciblepennant-small.grp +cm\z\tosgrilisk.grp +cm\t\diadem-ion-field.grp +cm\e\ensnaringbrood-overlay.grp +cm\t\diadem-projectile.grp +cm\t\gorgon-laser.grp +cm\z\quazilisk.grp +cm\z\quazilisk-shadow.grp +cm\z\zoryusthaleth.grp +cm\z\zoryusthaleth-shadow.grp +cm\t\diadem-attack.lol +cm\z\zobriolisk-shadow.grp +cm\z\ghitorokor-birth.grp +cm\z\quazilisk.grp +cm\z\quazilisk-shadow.grp +cm\t\sundog-engine.grp +cm\t\pegasus-engine2.grp +cm\z\almaksalisk.grp +cm\z\almakis-antre.grp +cm\z\almakis-antre-shadow.grp +cm\z\almakis-antre-damage.lof +cm\z\chrysalis-damage.lof +cm\z\overmind-cocoon-damage.lof +cm\z\quazilisk-birth.grp +cm\z\rililisk.grp +cm\z\almaksalisk-birth.grp +cm\z\zoryusthaleth-birth.grp +cm\e\scvspark-small.grp +cm\cb\christmas\commandcenter.grp +cm\cb\christmas\\nexus.grp +cm\cb\christmas\hatchery.grp +cm\cb\christmas\lair.grp +cm\cb\christmas\hive.grp +cm\cb\christmas\scv.grp +cm\cb\christmas\probe.grp +cm\cb\christmas\drone.grp +cm\cb\christmas\infestedterran.grp +cm\z\larvosk-cesiant.grp +cm\z\larvosk-cesiant-shadow.grp +cm\z\larvosk-cesiant-damage.lof +cm\t\captaincy.grp +cm\t\captaincy-shadow.grp +cm\t\captaincy-damage.lof +cm\t\ironfoundry.grp +cm\t\ironfoundry-shadow.grp +cm\t\ironfoundry-damage.lof +cm\t\scrapyard-damage.lof +cm\cb\christmas\mineralfield01.grp +cm\cb\christmas\mineralfield01-shad.grp +cm\cb\christmas\mineralfield02.grp +cm\cb\christmas\mineralfield02-shad.grp +cm\cb\christmas\mineralfield03.grp +cm\cb\christmas\mineralfield03-shad.grp +cm\cb\christmas\mineralchunk.grp +cm\cb\christmas\mineralchunk-empty.grp +cm\t\vestry.grp +cm\t\vestry-shadow.grp +cm\t\vestry-damage.lof +cm\t\vestry-connector.grp +cm\e\protoss-warp-fade.grp +cm\t\heracles.grp +cm\t\heracles-shadow.grp +cm\t\pazuzu.grp +cm\e\deconstruction-small.grp +cm\e\deconstruction-medium.grp +cm\e\deconstruction-large.grp +cm\t\quarry-damage.lof +cm\p\hierophant.grp +cm\p\hierophant-shadow.grp +cm\p\clarion.grp +cm\p\legionnaire.grp +cm\p\legionnaire-shadow.grp +cm\f-t\aspirant.grp +cm\f-t\aspirant-shadow.grp +cm\f-t\aspirant-death.grp +cm\f-t\cohort.grp +cm\f-t\cohort-shadow.grp +cm\dummy.grp +cm\dummy.grp +cm\dummy.lof +cm\dummy.los +cm\t\blackjack.grp +cm\t\blackjack-shadow.grp +cm\e\magneticfield-large.grp +cm\e\magneticfield-medium.grp +cm\e\magneticfield-small.grp +cm\z\akistrokor.grp +cm\z\konvilisk.grp +cm\z\konvilisk-shadow.grp +cm\z\konvilisk-birth.grp +cm\z\konvilisk-death.grp +cm\e\legionnaire-teleport.grp +cm\t\claymore.grp +cm\t\claymore.grp +cm\t\claymore.grp +cm\p\scribe-death.grp +cm\z\gorgral-swamp.grp +cm\z\gorgral-swamp-shadow.grp +cm\z\gorgral-swamp-damage.lof +cm\p\warpanchor-underlay.grp +cm\e\keskathalor-acid-pool.grp +cm\z\alaszileth.grp +cm\p\texture-fade.grp +cm\e\irradiate-small.grp +cm\e\irradiate-medium.grp +cm\e\irradiate-large.grp +cm\dummy.grp +cm\p\demiurge.grp +cm\p\demiurge-shadow.grp +cm\t\siren.grp +cm\t\siren-shadow.grp +cm\p\charlatan.grp +cm\p\charlatan-shadow.grp +cm\e\canister-rifle-hit-blue.grp +cm\p\exemplar-attack.lol +cm\p\exemplar.grp +cm\t\claymore-attack.lol +cm\p\cantavis-hallucination-weapon.grp +cm\p\cantavis-hallucination-field.grp +cm\f-t\aspirant-weapon.grp +cm\t\cyprian-weaponhit.grp +cm\e\gaussriflehit-green.grp +cm\e\canisterriflehit-green.grp +cm\e\\neutronflare-orange.grp +cm\z\vorvrokor-birth.grp +cm\z\skithrokor.grp +cm\z\skithrokor-attack.lol +cm\t\azazel.grp +cm\z\tetcaerokor.grp +cm\t\apothecary-damage.lof +cm\z\sovroleth.grp +cm\z\liiralisk.grp +cm\z\liiralisk-shadow.grp +cm\z\liiralisk-attack.lol +cm\z\\nathrokor-birth.lob +cm\z\sovroleth-birth.lob +cm\t\olympian.grp +cm\t\olympian-shadow.grp +cm\t\autocrat.grp +cm\t\autocrat-shadow.grp +cm\t\guildhall.grp +cm\t\guildhall-shadow.grp +cm\t\striga.grp +cm\t\striga-shadow.grp +cm\f-t\luminary.grp +cm\t\striga-attack.lol +cm\z\axitrilisk.grp +cm\z\axitrilisk-shadow.grp +cm\z\axitrilisk-remnants.grp +cm\p\anthelion.grp +cm\t\anticthon.grp +cm\t\anticthon-shadow.grp +cm\t\tinkererstower.grp +cm\t\tinkererstower-shadow.grp +cm\t\biotic-bastion.grp +cm\t\biotic-bastion-shadow.grp +cm\t\tinkererstower-damage.lof +cm\t\biotic-bastion-damage.lof +cm\p\prostrationstage.grp +cm\p\prostrationstage-shadow.grp +cm\p\prostrationstage-damage.lof +cm\p\roguegallery.grp +cm\p\roguegallery-shadow.grp +cm\p\roguegallery-damage.lof +cm\p\strident-stratum.grp +cm\p\strident-stratum-shadow.grp +cm\p\strident-stratum-damage.lof +cm\p\monument-of-sin.grp +cm\p\monument-of-sin-shadow.grp +cm\p\monument-of-sin-damage.lof +cm\z\atrixilisk-attack.lol +cm\z\bactalisk-attack2.lol +cm\p\charlatan-attack.lol +cm\p\empress.grp +cm\z\axtoth-axis.grp +cm\z\axtoth-axis-shadow.grp +cm\z\axtoth-axis-damage.lof +cm\t\diadem-mouseresponse.grp +cm\t\diadem-attackA1.grp +cm\p\anthelion-teleport-depart.grp +cm\p\anthelion-teleport-arrive.grp +cm\z\skithrokor-birth.grp +cm\z\konvilisk-birth.grp +cm\z\geszithalor-birth.grp +cm\z\liiralisk-birth.grp +cm\e\dermalaccelerant-small.grp +cm\e\dermalaccelerant-medium.grp +cm\e\dermalaccelerant-large.grp +cm\e\dermalaccelerant-xlarge.grp +cm\e\sublimeshepherd-overlay-large.grp +cm\e\sublimeshepherd-overlay-medium.grp +cm\e\sublimeshepherd-overlay-small.grp +cm\t\magnetar.grp +cm\t\magnetar-engine.grp +cm\z\izirokor.grp +cm\z\izirokor-shadow.grp +cm\z\izirokor-remains.grp +cm\z\izirokor-birth.grp +cm\z\izirokor-projectile.grp +cm\t\cuirass-stonewall-protocol-overlay.grp +cm\t\cyclops-fragmentary-shells.grp +cm\z\ocsal-overlay.grp +cm\t\salvage-zap.grp +cm\t\salvage-disable-small.grp +cm\t\salvage-disable-medium.grp +cm\t\salvage-disable-large.grp +cm\e\resource-boost-overlay-small.grp +cm\e\resource-boost-overlay-medium.grp +cm\e\resource-boost-overlay-large.grp +cm\z\infested-small.grp +cm\z\infested-medium.grp +cm\z\infested-large.grp +cm\\n\gas-raw.grp +cm\p\demiurge-worship-overlay-small.grp +cm\p\demiurge-worship-overlay-medium.grp +cm\p\demiurge-worship-overlay-large.grp +cm\t\controlled-demolition.grp +cm\t\emergency-detonation.grp +cm\t\madrigal-death.grp +cm\t\penumbra-death-big.grp +cm\f-t\tarasque-muzzle-flash.grp +cm\t\blackjack-remains.grp +cm\z\sovroleth-birth.grp +cm\p\demiurge-gods-made-flesh-small.grp +cm\p\demiurge-gods-made-flesh-medium.grp +cm\p\demiurge-gods-made-flesh-large.grp +cm\f-t\cohort-birth.grp +cm\f-t\tarasque-birth.grp +cm\z\plague-field.grp +cm\p\charlatan-birth.grp +cm\t\anticthon-birth.grp +cm\z\gosvileth.grp +cm\z\gosvileth-birth.grp +cm\p\artisan.grp +cm\p\empress-birth.grp +cm\z\othstoleth-birth.grp +cm\p\mind-tyrant-stun-small.grp +cm\p\mind-tyrant-stun-medium.grp +cm\p\mind-tyrant-stun-large.grp +cm\p\mind-tyrant-stun-xlarge.grp +cm\z\spraith-colony-attack.lol +cm\dummy.grp +cm\z\akistrokor-birth.grp +cm\z\alaszileth-birth.grp +cm\z\kagralisk.grp +cm\z\kagralisk-shadow.grp +cm\z\kagralisk-remains.grp +cm\z\kagralisk-birth.grp +cm\z\kagralisk-weaponhit.grp +cm\z\izkag-iteth.grp +cm\z\izkag-iteth-shadow.grp +cm\z\izkag-iteth-damage.lof +cm\z\kagralisk-venom.grp +cm\z\evigrilisk.grp +cm\z\evigrilisk-shadow.grp +cm\z\evigrilisk-remains.grp +cm\z\gosvileth.log +cm\z\evigrilisk-birth.grp +cm\t\sentinel-base.grp +cm\t\sentinel-turret.grp +cm\t\sentinel-shadow.grp +cm\p\simulacrum.grp +cm\p\shield-recharge-medium.grp +cm\p\shield-recharge-xlarge.grp +cm\\n\vespene-ridge.grp +cm\\n\vespene-ridge-shadow.grp +cm\\n\vespene-ridge-puffs.los +cm\p\idol-weaponhit.grp +cm\t\bunker.lod +cm\t\bunker.lou +cm\t\watchdog.lod +cm\t\watchdog.lou +cm\t\sentinel-attack.lol +cm\t\salamander-death.grp +cm\p\photoncannon-attack.lol +terran\vulture-engine.grp +cm\z\almaksalisk-attack.lol +cm\z\konvilisk-attack.lol +cm\p\legionnaire-blade-vortex-S.grp +cm\p\legionnaire-blade-vortex-M.grp +cm\p\legionnaire-blade-vortex-L.grp +cm\p\magister-stellar-enforcement-S.grp +cm\p\magister-stellar-enforcement-M.grp +cm\p\magister-stellar-enforcement-L.grp +cm\p\magister-stellar-enforcement-X.grp +cm\p\exemplar-death.grp +cm\p\explosion-small.grp +cm\p\explosion-medium.grp +cm\p\explosion-large.grp +cm\p\starsovereign-death.grp +cm\t\guildhall-damage.lof +cm\p\matrix.grp +cm\p\matrix-shadow.grp +cm\p\matrix-damage.lof +cm\t\sundog.grp +cm\t\matador.grp +cm\t\matador-shadow.grp +cm\t\starpad.grp +cm\t\starpad-shadow.grp +cm\t\starpad-damage.lof +cm\t\ancile.grp +cm\t\future-station.grp +cm\t\future-station-shadow.grp +cm\t\future-station-damage.lof +cm\t\pegasus.grp +cm\t\pegasus-missile.grp +cm\t\pegasus-engine.grp +cm\t\pegasus-attacklaser.lol +cm\t\pegasus-attackmissile.lol +cm\t\cestus.grp +cm\t\cestus-shadow.grp +cm\t\cestus-damage.lof +cm\t\goliath-attack.lol +cm\p\matrix-attack.lol +cm\t\talos-resource.loo +cm\z\gosvileth.loo +cm\p\artisan.loo +cm\f-t\wendigo.grp +cm\f-t\southpaw.grp +cm\f-t\southpaw-engine.grp +cm\t\cyprian.grp +cm\t\cyprian-shadow.grp +cm\t\sevenstep.grp +cm\t\sevenstep-shadow.grp +cm\p\shield-hit-large.los +cm\p\shield-hit-medium.los +cm\p\shield-hit-small.los +cm\t\cataphract.grp +cm\t\cataphract-shadow.grp +cm\t\cataphract-attack.lol +cm\p\binding-plasma-overlay.grp +cm\t\dilettante.grp +cm\t\dilettante-shadow.grp +cm\t\dilettante-remains.grp +cm\t\hypnagogue.grp +cm\t\hypnagogue-shadow.grp +cm\t\hypnagogue-remains.grp +cm\t\ambassador.grp +cm\t\ambassador-shadow.grp +cm\t\heir-base.grp +cm\t\heir-turret.grp +cm\t\heir-shadow.grp +cm\t\heir-attack.lol +cm\t\heir-remains.grp +cm\t\horseman.grp +cm\t\pythean.grp +cm\p\panoptus-attack.lol +cm\p\cabalist-shadow.grp +cm\t\silvertongue.grp +cm\t\silvertongue-shadow.grp +cm\t\apothecary.grp +cm\t\apothecary-shadow.grp +cm\p\sequestered-icon.grp +cm\p\sequestered-icon-shadow.grp +cm\p\sequestered-icon-damage.lof +cm\p\patriarch.grp +cm\p\patriarch-shadow.grp +cm\p\vagrant-shade.grp +cm\p\accantor-attack.lol +cm\t\starpad-overlay.grp +cm\t\mason-remnants.grp +cm\p\optecton.grp +cm\p\optecton-shadow.grp +cm\t\aion-attack.lol +cm\e\energy-regen-small.grp +cm\e\energy-regen-medium.grp +cm\e\energy-regen-large.grp +cm\e\energy-regen-xlarge.grp +cm\p\ardent-authority.grp +cm\p\ardent-authority-shadow.grp +cm\p\ardent-authority-damage.lof +cm\p\auxiliary-oasis.grp +cm\p\auxiliary-oasis-shadow.grp +cm\p\auxiliary-oasis-damage.lof +cm\p\unitary-union.grp +cm\p\unitary-union-shadow.grp +cm\p\unitary-union-damage.lof +cm\t\vulture-remains.grp +cm\z\quazilisk-remains.grp +cm\p\zealot-remains.grp +cm\t\cyclops-remains.grp +cm\t\goliath-remains.grp +cm\p\ecclesiast-remains.grp +cm\p\legionnaire-remains.grp +cm\p\optecton-attack.lol +cm\t\cyprian-remains.grp +cm\t\shaman-remains.grp +cm\t\wraith-remains.grp +cm\z\zoryusthaleth-remains.grp +cm\t\iron-council.grp +cm\t\iron-council-shadow.grp +cm\t\iron-council-damage.lof +cm\t\mantle.grp +cm\t\mantle-shadow.grp +cm\t\mantle-damage.lof +cm\t\ramesses.grp +cm\t\ramesses-shadow.grp +cm\t\ramesses-attack.lol +cm\p\sycophant.grp +cm\p\sycophant-shadow.grp +cm\z\axitrilisk.grp +cm\z\axitrilisk-shadow.grp +cm\t\durendal.grp +cm\t\durendal-shadow.grp +cm\t\durendal-weaponfire.grp +cm\p\servitor.grp +cm\p\positron.grp +cm\p\positron-attack.lol +cm\cb\christmas\santahat-small.grp +cm\cb\christmas\santahat-medium.grp +cm\cb\christmas\santahat-large.grp +cm\cb\christmas\santahat-xlarge.grp +cm\cb\christmas\santahat-small-17.grp +cm\cb\christmas\santahat-medium-17.grp +cm\cb\christmas\santahat-large-17.grp +cm\cb\christmas\santahat-xlarge-17.grp +cm\cb\christmas\vespenechunk.grp +cm\cb\christmas\vespenechunk-empty.grp +bd\daniels-mason.grp +bd\smoochie-boy.grp +tb\pol\station.grp +tb\pol\station-shadow.grp +tb\pol\station-overlay.grp +tb\pol\station-damage.lof +tb\pol\intern.grp +tb\pol\intern-shadow.grp +tb\pol\intern-resource.loo +cm\z\ezigrisant.grp +cm\z\ezigrisant-shadow.grp +cm\t\wellbore-overlay.grp +cm\z\ezigrisant-damage.lof +cm\t\wellbore.grp +cm\t\wellbore-shadow.grp +cm\t\wellbore-damage.lof +cm\t\wellbore-puff.los +tb\pol\deputy.grp +tb\pol\deputy-shadow.grp +tb\pol\wat-trooper.grp +tb\pol\wat-trooper-shadow.grp +cm\t\madrigal-dirge-proc.grp +cm\z\spirtith-colony.grp +cm\z\spirtith-colony-shadow.grp +cm\z\spirtith-colony-damage.lof +cm\z\spirtith-colony-attack.lol +cm\t\diadem-attackA3.grp +cm\t\diadem-attackA4.grp +cm\t\diadem-attackB.grp +terran\wraith-engine.grp +cm\t\diadem-dustoff.lod +cm\t\diadem-duston.lou +terran\anchor-attack-green.grp +terran\anchor-attack-blue.grp +cm\t\rubble-unit-medium.grp +cm\t\rubble-unit-small.grp +cm\p\rubble-unit-medium.grp +cm\p\rubble-unit-small.grp +cm\z\rubble-u-medium.grp +cm\z\rubble-u-small.grp +cm\z\rubble-u-tiny.grp +cm\p\scribe.loo +cm\z\skortrith-cesiant.grp +cm\p\engram.grp +cm\p\engram-shadow.grp +cm\p\engram-damage.lof +cm\z\zarcavrokor-bone.grp +cm\t\diadem-beacon.grp +cm\t\cyprian-rail.grp +cm\t\cyprian-attack.lol +cm\t\sentinel-rail.grp +cm\z\bactalisk-acid.grp +cm\z\ocsal-larva.grp +cm\t\burst-lasers-red-small.grp +cm\t\burst-lasers-red.grp +cm\t\phalanx-mortar.grp +cm\z\ezigrisant-puff.los +cm\t\matador-turret.grp +cm\t\matador-turret.lol +cm\t\wyvern-turret.grp +cm\p\analogue.grp +cm\p\analogue-shadow.grp +cm\z\spore-explosion-large.grp +cm\t\pythean-engine.grp +cm\z\mortothrokor.grp +cm\z\mortothrokor-shadow.grp +cm\z\mortothrokor-remnants.grp +cm\z\vilgoleth.grp +cm\t\madrigal-turret.grp +cm\t\madrigal-turret.lol +cm\f-t\philoc.grp +cm\f-t\philoc-engine.grp +cm\t\heavy-missile-explosion-orange.grp +cm\t\heavy-missile-explosion-green.grp +cm\z\quazilisk-mutate.grp +cm\z\quazilisk-attack.grp +cm\z\\nathrokor-mutate.grp +cm\cb\aprilfools\quazilisk.grp +cm\gm\omnipresence\omnivale.grp +cm\gm\omnipresence\omnivale-shadow.grp +cm\t\lodestone.grp +cm\t\lodestone-shadow.grp +cm\t\lodestone-attack.loa +cm\p\potentate.grp +cm\p\potentate-shadow.grp +cm\z\ocsal-cesiant.grp +cm\z\ocsal-cesiant-damage.lof +cm\z\ocsal-cesiant-shadow.grp +cm\t\vulture-attack.lol +cm\t\starpad-landing.lou +cm\t\starpad-liftoff.lod +cm\t\magnetar-attack.lol +cm\t\hippogriff-attack1.lol +cm\t\hippogriff-attack2.lol +cm\p\lanifect.grp +cm\p\lanifect-engines.grp +cm\t\helix-bolt.grp +cm\t\helix-bolt-blue.grp +cm\z\vorvrokor.grp +cm\z\vorvrokor-shadow.grp +protoss\scribe-shadow.grp +protoss\dracadin-explosion.grp +protoss\dracadin-attack.lol +cm\p\lanifect-death.grp +cm\f-t\hotshot.grp +cm\f-t\hotshot-shadow.grp +cm\t\decaying-overlay.grp +cm\p\tyranny-overlay.grp +cm\z\vornath-pond.grp +cm\z\skibact-scab.grp +cm\z\mortothrokor-birth.grp +cm\z\mortothrokor-passive.grp +cm\t\quarry-connector.grp +cm\t\rotary-connector.grp +cm\f-t\qilin.grp +cm\f-t\qilin-engine.grp +cm\f-t\auriel.grp +cm\f-t\auriel-shadow.grp +cm\f-p\adherent.grp +cm\f-p\adherent-shadow.grp +cm\t\spoke-shell.grp +cm\f-z\\nathrelisk.grp +cm\t\anchor-grounded.grp +cm\t\anchor-grounded-shadow.grp +cm\z\carapace-swarm-small.grp +cm\p\repentantchorus-small.grp +cm\p\repentantchorus-medium.grp +cm\p\repentantchorus-large.grp +cm\z\chokral-chaszal.grp +cm\z\algoztalisk.grp +cm\z\okrimnilisk.grp +cm\z\cherbrathalor.grp +cm\z\cherbrathalor-shadow.grp +cm\z\cherbrathalor-attack.lol +cm\z\kalkalisk-birth.grp +cm\z\kalkalisk-birth-flying.grp +cm\z\gorgrokor-birth.grp +cm\z\tethzorokor-birth.grp +cm\z\vilgorokor-birth.grp +cm\z\protathalor-birth.grp +cm\z\cikralisk-birth.grp +cm\z\cherbrathalor-birth.grp +cm\z\okrimnilisk-birth.grp +cm\z\axitrilisk-birth.grp +cm\z\tosgrilisk-birth.grp +cm\z\tosgrilisk-birth-flying.grp +cm\z\iroleth-birth.grp +cm\z\tetcaerokor-birth.grp +cm\z\alkajelisk-birth-flying.grp +cm\z\zarcavrokor-birth.grp +cm\z\zarcavrokor-birth-flying.grp +cm\z\algoztalisk-birth.grp +cm\z\algoztalisk-birth-flying.grp +cm\t\gorgon-petrification.grp +cm\t\autocrat-controlled-opposition.grp +cm\t\maverick-stimpack-inject.grp +cm\t\maverick-stimpack-proc.grp +cm\t\maverick-stimpack-expire.grp +cm\p\ancestral-archives.grp +cm\p\ancestral-archives-shadow.grp +cm\p\ancestral-archives-damage.lof +cm\p\gladius-charge.grp +cm\p\optecton-muzzle.grp +cm\p\avenge-small.grp +cm\p\avenge-medium.grp +cm\p\avenge-large.grp +cm\p\avenge-xlarge.grp +cm\p\ascension-small.grp +cm\p\ascension-medium.grp +cm\p\ascension-large.grp +cm\p\ascension-xlarge.grp +cm\p\astral-rule-small.grp +cm\p\astral-rule-medium.grp +cm\p\astral-rule-large.grp +cm\p\astral-rule-xlarge.grp +cm\p\magister-weaponhit.grp +cm\t\olympian-dishonorable-discharge-1.grp +cm\t\olympian-dishonorable-discharge-1.grp +cm\t\olympian-dishonorable-discharge-1.grp +cm\t\magnetar-law-of-attraction-1.grp +cm\t\magnetar-law-of-attraction-2.grp +cm\t\magnetar-law-of-attraction-3.grp +cm\t\salamander-weaponhit.grp +cm\p\positron-projectile.grp +cm\p\demiurge-worship-proc-small.grp +cm\p\demiurge-worship-proc-medium.grp +cm\p\demiurge-worship-proc-large.grp +cm\p\demiurge-worship-proc-xlarge.grp +cm\p\demiurge-worship-overlay-xlarge.grp +cm\p\mind-tyrant-tyranny-cast-overlay.grp +cm\z\sasyrvoleth.grp +cm\z\sasyrvoleth-birth.grp +cm\z\protathalor-mutate.grp +cm\z\izirokor-mutate.grp +cm\z\skithrokor-mutate.grp +cm\p\legionnaire-blade-vortex-XL.grp +cm\t\lazarus-proc-S.grp +cm\t\lazarus-proc-M.grp +cm\t\lazarus-proc-L.grp +cm\t\lazarus-proc-XL.grp +cm\z\geszithalor-mutate.grp +cm\z\evigrilisk-mutate.grp +cm\z\vilgorokor-mutate.grp +cm\z\gorgrokor-mutate.grp +cm\z\skortrith-cesiant-shadow.grp +cm\t\ancile-death.grp +cm\t\trojan-death.grp +cm\p\envoy-death.grp +cm\z\ovileth-death.grp +cm\t\phalanx-tank-death.grp +cm\t\phalanx-siege-death.grp +cm\p\hierophant-death.grp +cm\t\salamander-rocket-trail.grp +cm\h\ackmed.grp +cm\h\ackmed-shadow.grp +cm\p\simulacrum-death.grp +cm\p\vassal-death.grp +cm\z\\nathrokor-death.grp +cm\z\venom-white.grp +cm\z\spores-overlay-orange.grp +cm\z\spores-overlay-orange-small.grp +cm\z\spores-overlay-yellow.grp +cm\z\kalkalisk-spore.grp +cm\z\skithrokor-spore.grp +cm\z\spore-hit-orange.grp +cm\z\spore-hit-orange-small.grp +cm\z\spore-hit-yellow.grp +cm\p\photon-blasters-white.grp +cm\p\photon-blasters-green.grp +cm\z\kagralisk-mutate.grp +cm\z\almaksalisk-mutate.grp +cm\z\cikralisk-mutate.grp +cm\z\vorvrokor-death.grp +cm\z\liiralisk-glave.grp +cm\z\spore-hit-green-small.grp +cm\z\lakizilisk-death.grp +cm\p\manifold-death.grp +cm\p\golem-death.grp +cm\p\idol-death.grp +cm\p\artisan-death.grp +cm\p\witness-death.grp +cm\t\savant-death.grp +cm\f-t\cyclops-futurist.grp +cm\f-t\cyclops-futurist-shadow.grp +cm\f-p\zealot-starbound.grp +cm\f-p\zealot-starbound-shadow.grp +cm\p\vibrance.grp +cm\t\blackjack-damaged.grp +cm\p\idol-attack.lol +cm\p\atreus-projectile.grp +cm\f-t\harakan-golden.grp +cm\f-t\harakan-golden-shadow.grp +cm\f-z\iziralisk.grp +cm\f-z\iziralisk-birth.grp +cm\f-z\iziralisk-mutate.grp +cm\f-z\iziralisk-projectile.grp +cm\f-z\iziralisk-remains.grp +cm\f-z\iziralisk-shadow.grp +cm\f-p\cicatus.grp +cm\f-p\cicatus-shadow.grp +cm\z\gorgrokor-death.grp +cm\z\kazitrith-cesiant.grp +cm\z\kazitrith-cesiant-shadow.grp +cm\z\tendril-amitosis-overlay.grp +cm\f-t\cleric-futurist.grp +cm\f-t\cleric-futurist-shadow.grp +cm\f-t\piety-ward.grp +cm\f-t\piety-ward-shadow.grp +cm\f-z\inzorthrokor.grp +cm\f-z\inzorthrokor-birth.grp +cm\f-z\inzorthrokor-remains.grp +cm\f-z\inzorthrokor-shadow.grp +cm\t\sieve.grp +cm\t\sieve-shadow.grp +cm\t\sieve-damage.lof +cm\t\sieve-gas.los +cm\p\cedent.grp +cm\p\cedent-shadow.grp +cm\p\cedent-damage.lof +cm\p\cedent-gas.los +cm\z\eskigant.grp +cm\z\eskigant-shadow.grp +cm\z\eskigant-damage.lof +cm\z\eskigant-gas.los +cm\p\oscilliant.grp +cm\p\oscilliant-shadow.grp +cm\t\madrigal-furia-remains.grp +cm\f-p\renesair.grp +cm\f-p\renesair-engine.grp +cm\f-p\renesair-death.grp +cm\f-p\extatus.grp +cm\f-p\extatus-engine.grp +cm\p\unar.grp +cm\p\unar-shadow.grp +cm\p\unar-orbital.grp +cm\f-p\ecclesiast-khalendric.grp +cm\f-p\ecclesiast-khalendric-shadow.grp +cm\f-p\ashguard.grp +cm\f-p\ashguard-shadow.grp +cm\z\rilirokor-birth.grp +cm\p\xenolith.grp +cm\p\xenolith-shadow.grp +cm\p\xenolith-damage.lof +cm\z\lakizilisk-mutate.grp +cm\z\zobriolisk-birth.grp +cm\t\savant-entropy.grp +cm\a\landlock.grp +cm\a\landlock-shadow.grp +cm\f-z\quazrokor.grp +cm\f-z\quazrokor-shadow.grp +cm\f-p\dracadin-order.grp +cm\f-p\dracadin-order-shadow.grp +cm\f-p\dracadin-order-death.grp +cm\t\savant-entropy-attack.lol +cm\t\savant-siphon-attack.lol +cm\t\savant-entropy-shadow.grp +cm\f-t\blackjack-golden.grp +cm\f-t\blackjack-golden-shadow.grp +cm\f-t\blackjack-golden-damaged.grp +cm\f-t\tiangou-base.grp +cm\f-t\tiangou-shadow.grp +cm\f-t\tiangou-turret.grp +cm\f-t\eidolon-nydus.grp +terran\tghShad.grp +cm\f-t\maverick-futurist.grp +cm\f-t\maverick-futurist-shadow.grp +cm\t\tencendur.grp +cm\t\tencendur-shadow.grp +cm\t\tencendur-overlay-bg.grp +cm\t\tencendur-overlay-ob.grp +cm\f-z\hydralisk-orkoti.grp +zerg\zhyShad.grp +cm\f-z\hydralisk-orkoti-death.grp +cm\t\tencendur-flag.grp +cm\f-p\manifold-starbound.grp +cm\f-p\manifold-starbound-shadow.grp +cm\f-t\tiangou-attack.lol +cm\t\cestus-landing.lou +cm\t\cestus-liftoff.lod +cm\p\architect-shadow.grp +cm\p\architect-attack.lol +cm\f-z\yblyleth.grp +cm\f-z\yblyleth-shadow.grp +cm\z\evigrilisk-remains.grp +cm\z\evigrilisk-birth.grp +cm\z\evigrilisk-mutate.grp +cm\z\corrosive-touch-explosion-medium.grp +cm\p\p-laser-large.grp +cm\a\a-rubble-small.grp +cm\a\a-rubble-medium.grp +cm\a\a-rubble-large.grp +cm\a\pilgrim.grp +cm\a\pilgrim-shadow.grp +cm\a\emissary.grp +cm\a\inkblot.grp +cm\z\corrosive-touch-explosion-small.grp +cm\z\skibact-scab-shadow.grp +neutral\stasis-cell-shadow.grp +cm\f-t\blackguard.grp +cm\f-t\blackguard-shadow.grp +cm\f-t\blackguard-attack.lol +cm\f-t\arhaun-base.grp +cm\f-t\arhaun-turret.grp +cm\f-t\arhaun-shadow.grp +cm\f-t\arhaun-attack.lol +cm\t\morningstar-field.grp +cm\z\zozarc-gorge.grp +cm\z\zozarc-gorge-shadow.grp +cm\z\zozarc-gorge-damage.lof +cm\z\cikralisk-attack.lol +cm\p\autocort.grp +cm\t\ogun.grp +cm\t\ogun-shadow.grp +cm\f-t\cannibal.grp +cm\f-t\cannibal-shadow.grp +cm\z\agrileth.grp +cm\z\agrileth-shadow.grp +cm\z\agrileth-remains.grp +cm\z\agrileth-birth.grp +cm\a\cardinal.grp +cm\a\cardinal-shadow.grp +cm\a\cardinal-death.grp +cm\a\vicar.grp +cm\a\vicar-shadow.grp +cm\a\vicar-death.grp +cm\a\vicar-attack.lol +cm\a\arch.grp +cm\a\arch-shadow.grp +cm\a\arch-damage.lof +cm\a\chantry.grp +cm\a\chantry-shadow.grp +cm\a\chantry-damage.lof +cm\a\function.grp +cm\a\function-shadow.grp +cm\a\function-damage.lof +cm\a\radix.grp +cm\a\radix-shadow.grp +cm\a\radix-damage.lof +cm\a\arch-gas.los +cm\a\missionary.grp +cm\a\missionary-shadow.grp +cm\z\izirokor-attack.lol +cm\a\a-death-small.grp +cm\a\a-death-large.grp +cm\t\wraith-death.grp +cm\f-t\vulture-hecaton.grp +cm\f-t\tartarus-mine.grp +cm\f-t\tartarus-mine-shadow.grp +cm\f-t\madrigal-hecaton-base.grp +cm\f-t\madrigal-hecaton-turret.grp +cm\z\zoryusthaleth-mutate.grp +cm\a\pilgrim.loo +cm\a\rove.grp +cm\t\burst-lasers-hit-green.grp +cm\t\burst-lasers-hit-cyan.grp +cm\a\cipher.grp +cm\a\cipher-shadow.grp +cm\a\gauntlet.grp +cm\a\gauntlet-shadow.grp +cm\a\cosmoscope.grp +cm\a\cosmoscope-shadow.grp +cm\a\bounty.grp +cm\a\bounty-shadow.grp +cm\a\tirade.grp +cm\a\dyad.grp +cm\a\dyad.grp +cm\a\helion.grp +cm\a\scepter.grp +cm\z\kethrostalor.grp +cm\z\kethrostalor-shadow.grp +cm\z\kethraj-keep.grp +cm\z\kethraj-keep-shadow.grp +cm\z\kethraj-keep-damage.lof +cm\z\kethrostalor-attack.lol +cm\f-z\azanthrokor.grp +cm\f-z\azanthrokor-birth.grp +cm\f-z\azanthrokor-mutate.grp +cm\f-z\azanthrokor-attack.lol +cm\z\kethrostalor-birth.grp +cm\a\proselyte.grp +cm\a\proselyte-shadow.grp +cm\t\triton-base.grp +cm\t\triton-turret.grp +cm\t\triton-shadow.grp +cm\t\triton-attack.lol +cm\t\triton-ability.lol +cm\a\helion-engine.grp +cm\\n\mineral-field-rich-01.grp +cm\\n\mineral-field-rich-02.grp +cm\\n\mineral-field-rich-03.grp +cm\\n\mineral-chunk-rich.grp +cm\a\a-rubble-tiny.grp +cm\f-z\sovrokor.grp +cm\f-t\valkyrie-hecaton.grp +cm\f-z\garvrokor.grp +cm\t\talos-engine.grp +cm\p\positron-shadow.grp +cm\f-p\simulacrum-astral.grp +cm\p\herald-glow.grp +cm\f-t\maverick-futurist-death.grp +cm\f-t\vulture-hecaton-attack.lol +cm\f-t\vulture-hecaton-engine.grp +cm\a\grandstand.grp +cm\a\grandstand-shadow.grp +cm\a\grandstand-damage.lof +cm\f-p\simulacrum-astral-shadow.grp +cm\a\mimic.grp +cm\a\mimic-shadow.grp +cm\a\intendent.grp +cm\a\intendent-shadow.grp +cm\a\intendent-attack1.lol +cm\a\intendent-attack2.lol +cm\a\object.grp +cm\a\object-shadow.grp +cm\f-t\cleric-futurist-remnants.grp +cm\a\abutment.grp +cm\a\abutment-shadow.grp +cm\a\abutment-damage.lof +cm\a\abutment-gas.los +cm\a\proscenium.grp +cm\a\proscenium-shadow.grp +cm\a\proscenium-damage.lof +cm\f-t\quadriga.grp +cm\f-t\quadriga-engine.grp +cm\f-t\quadriga-afterburner.grp +cm\a\helion-attack.lol +cm\cb\christmas\landlock.grp +cm\f-t\serathiel.grp +cm\f-t\serathiel-engine.grp +cm\f-t\serathiel-divine-discipline.grp +cm\a\a-construction-large.grp +cm\a\a-construction-large-shadow.grp +cm\a\a-construction-medium.grp +cm\a\a-construction-medium-shadow.grp +cm\a\a-construction-small.grp +cm\a\a-construction-small-shadow.grp +cm\z\ogroszaleth.grp +cm\z\ogroszaleth-shadow.grp +cm\z\ogroszaleth-birth.grp +cm\z\ogroszaleth-attack.lol +cm\z\malkothrokor.grp +cm\z\malkothrokor-shadow.grp +cm\z\malkothrokor-death.grp +cm\z\malkothrokor-birth.grp +cm\a\akreno.grp +cm\f-z\zethalor.grp +cm\f-z\zethalor-shadow.grp +cm\f-z\scrylyleth.grp +cm\f-z\scrylyleth-shadow.grp +cm\a\aphorist.grp +cm\a\aphorist-shadow.grp +cm\a\thaumalogue.grp +cm\a\thaumalogue-shadow.grp +cm\a\doronin.grp +cm\a\doronin-shadow.grp +cm\f-z\zethalor-remains.grp +cm\f-z\zethalor-birth.grp +cm\a\thaumalogue-remains.grp +cm\a\avatar.grp +cm\a\avatar-shadow.grp +cm\f-z\scrylyleth-birth.grp +cm\a\zylode.grp +cm\a\zylode-shadow.grp +cm\a\zylode-damage.lof +cm\a\conduit.grp +cm\a\conduit-shadow.grp +cm\a\conduit-damage.lof +cm\a\bestiary.grp +cm\a\bestiary-shadow.grp +cm\a\bestiary-damage.lof +cm\a\astrolabe.grp +cm\a\astrolabe-shadow.grp +cm\a\astrolabe-damage.lof +cm\a\a-fire-c.grp +cm\a\a-fire-f.grp +cm\a\a-fire-v.grp +cm\a\crematorium.grp +cm\a\eyelet.grp +cm\a\eyelet-shadow.grp +cm\a\obelisk.grp +cm\a\obelisk-shadow.grp +cm\a\obelisk-damage.lof +cm\a\agonist.grp +cm\a\zonguard.grp +cm\a\iconoclast.grp +cm\a\paraclete.grp +cm\a\constituent.grp +cm\a\auxine.grp +cm\a\phontis.grp +cm\a\phontis-shadow.grp +cm\a\petract.grp +cm\a\petract-shadow.grp +cm\a\lightning-hit-small.grp +cm\a\lightning-hit-medium.grp +cm\a\lightning-hit-medium.grp +cm\a\rove-engine.grp +cm\a\embedded-overlay.grp +cm\f-t\maverick-upstart.grp +cm\f-t\maverick-upstart-shadow.grp +cm\f-t\maverick-upstart-death.grp +cm\f-t\vulture-upstart.grp +cm\f-t\vulture-upstart-engine.grp +cm\f-t\goremast.grp +cm\f-t\goremast-shadow.grp +cm\f-t\morrigan.grp +cm\f-t\morrigan-shadow.grp +cm\f-t\morrigan-remains.grp +cm\f-t\cutlass.grp +cm\f-t\cutlass-shadow.grp +cm\f-t\cutlass-turret.grp +cm\f-t\cutlass-turret.lol +cm\f-t\broadside.grp +cm\f-t\broadside-shadow.grp +cm\f-t\broadside-turret.grp +cm\a\dorminge.grp +cm\a\dorminge-shadow.grp +cm\a\dorminge-damage.lof +cm\a\warringwinds-overlay.grp +cm\a\domain.grp +cm\a\domain-shadow.grp +cm\a\domain-damage.lof +cm\a\panoply.grp +cm\a\panoply-shadow.grp +cm\a\panoply-damage.lof +cm\a\lotiro.grp +cm\a\lotiro-shadow.grp +cm\a\lotiro-damage.lof +cm\a\cosmoscope-damage.lof +cm\a\landlock-damage.lof +hl\fr\ferrum.grp +hl\fr\ferrum-attack.lol +hl\fr\chalyb.grp +hl\fr\chalyb-attack.lol +hl\fr\circitor.grp +hl\fr\circitor-attack.lol +hl\fr\exigo.grp +hl\fr\exigo-attack1.lol +hl\fr\exigo-attack2.lol +hl\fr\athla.grp +hl\fr\athla-special.loo +hl\fr\vicere.grp +hl\fr\vicere-attack1.lol +hl\fr\vicere-attack2.lol +hl\fr\machinamentum.grp +hl\fr\machinamentum-shadow.grp +hl\fr\machinamentum-damage.lof +cm\\n\big-infested-kerrigan.grp +cm\\n\big-infested-kerrigan-shadow.grp +cm\a\lekrot.grp +cm\a\lekrot-shadow.grp +cm\a\lekrot-damage.lof +cm\a\hearthshard.grp +cm\a\hearthshard-shadow.grp +cm\a\hearthshard-damage.lof +cm\a\insurant.grp +cm\a\insurant-shadow.grp +cm\a\silversoul.grp +cm\a\silversoul-shadow.grp +cm\a\silversoul-remains.grp +cm\a\silversoul-attack.lol +cm\a\ghostwrit.grp +cm\a\ghostwrit-shadow.grp +cm\a\ghostwrit-attack.lol +cm\a\endbore.grp +cm\a\alacyne.grp +cm\a\alacyne-engine.grp +cm\a\elgird.grp +cm\a\elgird-engine.grp +cm\a\histrion.grp +cm\a\histrion-engine.grp +cm\a\egocent.grp +cm\h\raynor-vulture.grp +cm\h\raynor-vulture-engine.grp +cm\h\raynor-vulture-attack.lol +cm\a\limire.grp +cm\a\tortare.grp +cm\f-a\scralia.grp +terran\civilian-death.grp +terran\civilian-remains.grp +cm\a\pilgrim-remains.grp +cm\b\thrombocyte.grp +cm\b\thrombocyte-shadow.grp +cm\f-z\rozirokor.grp +cm\f-z\rozirokor-shadow.grp +cm\f-z\rozirokor-remains.grp +cm\f-z\rozirokor-birth.grp +cm\f-z\rozirokor-mutate.grp +cm\f-z\zytryleth.grp +cm\f-z\zytryleth-shadow.grp +cm\f-z\zytryleth-damage.lof +cm\f-z\inzorthrokor-mutate.grp +cm\f-t\camazotz.grp +cm\f-t\camazotz-shadow.grp +cm\b\grimoire.grp +cm\b\grimoire-shadow.grp +cm\b\grimoire-damage.lof +cm\b\rubble-b-small.grp +cm\b\rubble-b-large.grp +cm\b\rubble-u-tiny.grp +cm\b\rubble-u-small.grp +cm\b\rubble-u-medium.grp +cm\z\vornath-pond-shadow.grp +cm\z\chokral-chaszal-shadow.grp +cm\p\atreus-attack.lol +cm\p\atreus-remains.grp +zerg\zethrokor-mutate.grp +cm\f-p\simulacrum-khalendric.grp +cm\f-p\simulacrum-khalendric-shadow.grp +cm\f-p\idol-khalendric.grp +cm\f-p\idol-khalendric-shadow.grp +cm\f-p\idol-khalendric-remains.grp +cm\f-p\vestige.grp +cm\f-p\vestige-shadow.grp +cm\f-t\arquebus.grp +cm\f-t\arquebus-shadow.grp +cm\f-t\arquebus-attack.lol +cm\b\acqui.grp +cm\b\acqui-shadow.grp +cm\b\acqui-resource.loo +zerg\hydralisk-mutate.grp +cm\f-z\alzurokor.grp +cm\f-z\alzurokor-shadow.grp +cm\f-z\hydrokor.grp +cm\f-z\hydrokor-shadow.grp +cm\f-z\hydrokor-birth.grp +cm\f-z\hydrokor-mutate.grp +cm\f-z\hydrokor-remains.grp +cm\f-z\sovrokor-shadow.grp +cm\f-z\sovrokor-remains.grp +cm\f-z\sovrokor-birth.grp +cm\f-z\sovrokor-mutate.grp +cm\b\\nihilist.grp +cm\b\\nihilist-shadow.grp +terran\eidolon-attack.lol +cm\f-t\eidolon-nydus-attack.lol +cm\f-t\eidolon-nydus-remains.grp +terran\valkyrie-attack.lol +cm\f-t\valkyrie-hecaton-engine.grp +cm\f-t\valkyrie-hecaton-attack.lol +cm\a\impact-dissipation.grp +cm\a\crushing-delivery-small.grp +cm\a\crushing-delivery-medium.grp +cm\a\crushing-delivery-large.grp +cm\p\servitor-shadow.grp +cm\p\gladius-attack.lol diff --git a/mpq/arr/portdata.tbl.txt b/mpq/arr/portdata.tbl.txt new file mode 100644 index 00000000..4f68c15a --- /dev/null +++ b/mpq/arr/portdata.tbl.txt @@ -0,0 +1,587 @@ +# tbl-text v1 - StarCraft: Brood War string table source +# One entry per line, in index order. +# <0> separates the parts of a multi-part entry +# raw byte N (decimal 0-255), e.g. <10> newline, <4> colour code +# << a literal '<' <> nothing (empty-entry marker) +# Lines starting with '#' are comments; blank lines are ignored. + +tmarine\TMaFid0 +tmarine\TMaTlk0 +tghost\TGhFid0 +tghost\TGhTlk0 +tfirebat\TFbFid0 +tfirebat\TFbTlk0 +tvulture\TVuFid0 +tvulture\TVuTlk0 +TSpider\TMnFid0 +TSpider\TMnTlk0 +tgoliath\TGoFid0 +tgoliath\TGoTlk0 +ttank\TTaFid0 +ttank\TTaTlk0 +tSCV\TSCFid0 +tSCV\TSCTlk0 +tphoenix\TPhFid0 +tphoenix\TPhTlk0 +tvessel\TVeFid0 +tvessel\TVeTlk0 +tdropshi\TDrFid0 +tdropshi\TDrTlk0 +tbattlec\TBaFid0 +tbattlec\TBaTlk0 +UKerr\UKeFid0 +UKerr\UKeTlk0 +UJim\URaFid0 +UJim\URaTlk0 +UDuke\UDuFid0 +UDuke\UDuTlk0 +Ncivilian\NCiFid0 +Umengsk\UMeFid0 +Umengsk\UMeTlk0 +TAdvisor\TAdFid0 +TAdvisor\TAdTlk0 +ZLarva\ZLaFid0 +ZLarva\ZLaTlk0 +ZEgg\ZEgFid0 +ZEgg\ZEgTlk0 +ZZergl\ZZeFid0 +ZZergl\ZZeTlk0 +ZHydra\ZHyFid0 +ZHydra\ZHyTlk0 +ZUltra\ZUlFid0 +ZUltra\ZUlTlk0 +ZBrood\ZBrFid0 +ZBrood\ZBrTlk0 +ZDrone\ZDrFid0 +ZDrone\ZDrTlk0 +ZOver\ZOvFid0 +ZOver\ZOvTlk0 +ZMutalid\ZMuFid0 +ZMutalid\ZMuTlk0 +ZGuard\ZGuFid0 +ZGuard\ZGuTlk0 +ZQueen\ZQuFid0 +ZQueen\ZQuTlk0 +ZDefiler\ZDeFid0 +ZDefiler\ZDeTlk0 +ZAvenger\ZAvFid0 +ZAvenger\ZAvTlk0 +Zcocoon\ZCoFid0 +ZChrysalis\ZChFid0 +ZInfestT\ZBGFid0 +ZInfestT\ZBGTlk0 +UZasz\Fid0 +UZasz\Tlk0 +UDagg\UDgFid0 +UDagg\UDgTlk0 +UIKerr\UIKFid0 +UIKerr\UIKTlk0 +UHunters\UHuFid0 +UHunters\UHuTlk0 +ZAdvisor\ZAdFid0 +ZAdvisor\ZAdTlk0 +PProbe\PPrFid0 +PProbe\PPrTlk0 +PZealot\PZeFid0 +PZealot\PZeTlk0 +PDragoon\PDrFid0 +PDragoon\PDrTlk0 +PTemplar\PTeFid0 +PTemplar\PTeTlk0 +parchon\PArFid0 +parchon\PArTlk0 +pshuttle\PShFid0 +pshuttle\PShTlk0 +PScout\PScFid0 +PScout\PScTlk0 +PArbiter\PAbFid0 +PArbiter\PAbTlk0 +pcarrier\PCaFid0 +pcarrier\PCaTlk0 +PInterce\PInFid0 +PInterce\PInTlk0 +UDTemplar\UDTFid0 +UDTemplar\UDTTlk0 +UFenZeal\UFeFid0 +UFenZeal\UFeTlk0 +UFenDrag\UFdFid0 +UFenDrag\UFdTlk0 +UZeratul\UZeFid0 +UZeratul\UZeTlk0 +UTassadar\UTaFid0 +UTassadar\UTaTlk0 +PWitness\PWiFid0 +PWitness\PWiTlk0 +PTrilo\PTrFid0 +PTrilo\PTrTlk0 +Ukhad\NCrFid0 +Ukhad\NCrTlk0 +PAdvisor\PAdFid0 +PAdvisor\PAdTlk0 +nbiker\NBiFid0 +nbiker\NBiTlk0 +NBadCrit\NBCFid0 +NBadCrit\NBCTlk0 +NJungCrit\NJCFid0 +NJungCrit\NJCTlk0 +NLavaCrit\NLCFid0 +NLavaCrit\NLCTlk0 +NGasTank\NGTFid0 +NGOrb\NGOFid0 +NGasSac\NGSFid0 +NOre\NOCFid0 +UDisk\UDdFid0 +Udisk\UDdTlk0 +Upsi\UPsFid0 +Upsi\UPsTlk0 +Ushard\UShFid0 +Ushard\UShTlk0 +UFlag1\UF1Fid0 +UFlag1\UF1Tlk0 +UFlag2\UF2Fid0 +UFlag2\UF2Tlk0 +UFlag3\UF3Fid0 +UFlag3\UF3Tlk0 +UFlag4\UF4Fid0 +UFlag4\UF4Tlk0 +UFlag5\UF5Fid0 +UFlag5\UF5Tlk0 +UFlag6\UF6Fid0 +UFlag6\UF6Tlk0 +UFlag7\UF7Fid0 +UFlag7\UF7Tlk0 +UFlag8\UF8Fid0 +UFlag8\UF8Tlk0 +tmedic\TMeFid0 +tmedic\TMeTlk0 +Tbomber\TBmFid0 +TBomber\TBmTlk0 +TDugal\UdgFid0 +TDugal\UdgTlk0 +Talex\TaxFid0 +Talex\TaxTlk0 +TSam\TsmFid0 +TSam\TsmTlk0 +PArtanis\PatFid0 +PArtanis\PatTlk0 +PRaz\PRzFid0 +PRaz\PRzTlk0 +Zdevour\ZDvfid0 +Zdevour\ZDvtlk0 +ZLurker\ZLufid0 +ZLurker\ZLutlk0 +PDarchon\PDaFid0 +PDarchon\PDaTlk0 +PCorsair\PCsFid0 +PCorsair\PCsTlk0 +NRedCrit\NRDFid0 +NRedCrit\NRDTlk0 +NBluCrit\NLCFid0 +NBluCrit\NLCTlk0 +NIceCrit\NICFid0 +NIceCrit\NICTlk0 +UUraj\UUjFid0 +UUraj\UUjTlk0 +UKhalis\UKsFid0 +UKhalis\UKsTlk0 +UFlag9\UF9Fid0 +UFlag9\UF9Tlk0 +UFlag10\UF10Fid0 +UFlag10\UF10Tlk0 +UFlag11\UF11Fid0 +UFlag11\UF11Tlk0 +UFlag12\UF12Fid0 +UFlag12\UF12Tlk0 +t\cyprian\fid0 +t\cyprian\tlk0 +t\shaman\fid0 +t\shaman\tlk0 +p\augur\fid0 +p\augur\tlk0 +f\advisor_zorthos\fid0 +f\advisor_zorthos\tlk0 +z\vorvrokor\fid0 +z\vorvrokor\tlk0 +p\ecclesiast\fid0 +p\ecclesiast\tlk0 +p\patriarch\fid0 +p\patriarch\tlk0 +p\legionnaire\fid0 +p\legionnaire\tlk0 +z\protathalor\fid0 +z\protathalor\tlk0 +z\izirokor\fid0 +z\izirokor\tlk0 +p\amaranth\fid0 +p\amaranth\tlk0 +t\claymore\fid0 +t\claymore\tlk0 +t\dilettante\fid0 +t\dilettante\fid0 +t\wyvern\fid0 +t\wyvern\tlk0 +t\savant\fid0 +t\savant\tlk0 +p\potentate\fid0 +p\potentate\tlk0 +z\evigrilisk\fid0 +z\evigrilisk\tlk0 +p\sycophant\fid0 +p\sycophant\tlk0 +p\positron\fid0 +p\positron\tlk0 +t\madrigal\fid0 +t\madrigal\tlk0 +t\cyclops\fid0 +t\cyclops\tlk0 +t\sevenstep\fid0 +t\sevenstep\tlk0 +t\sundog\fid0 +t\sundog\tlk0 +t\olympian\fid0 +t\olympian\tlk0 +p\servitor\fid0 +p\servitor\tlk0 +sp\hadral\fid0 +sp\hadral\tlk0 +sp\thyron\fid0 +sp\thyron\tlk0 +t\salamander\fid0 +t\salamander\tlk0 +t\phobos\fid0 +t\phobos\tlk0 +sp\ackmed\fid0 +sp\ackmed\tlk0 +z\skithrokor\fid0 +z\skithrokor\tlk0 +p\vagrant\fid0 +p\vagrant\tlk0 +p\anthelion\fid0 +p\anthelion\tlk0 +f\advisor_nydustrinity\fid0 +f\advisor_nydustrinity\tlk0 +sp\nekron\fid0 +sp\nekron\tlk0 +p\gladius\fid0 +p\gladius\tlk0 +f\advisor_goldenrepublic\fid0 +f\advisor_goldenrepublic\tlk0 +p\herald\fid0 +p\herald\tlk0 +f\advisor_upstartt\fid0 +f\advisor_upstartt\tlk0 +f\advisor_starbound\fid0 +f\advisor_starbound\fid0 +p\optecton\fid0 +p\optecton\tlk0 +t\cataphract\fid0 +t\cataphract\tlk0 +a\advisor\fid0 +a\advisor\fid0 +t\striga\fid0 +t\striga\tlk0 +z\rililisk\fid0 +z\rililisk\tlk0 +p\pariah\fid0 +p\pariah\tlk0 +f\tarasque\fid0 +f\tarasque\tlk0 +p\exemplar\fid0 +p\exemplar\fid0 +f\advisor_futuristfederation\fid0 +f\advisor_futuristfederation\tlk0 +t\matador\fid0 +t\matador\tlk0 +f\advisor_imicren\fid0 +f\advisor_imicren\fid0 +p\atreus\fid0 +p\atreus\tlk0 +p\architect\fid0 +p\architect\fid0 +f\philoc\fid0 +f\philoc\tlk0 +t\gorgon\fid0 +t\gorgon\tlk0 +z\zoryusthaleth\fid0 +z\zoryusthaleth\tlk0 +f\adherent\fid0 +f\adherent\tlk0 +z\keskathalor\fid0 +z\keskathalor\tlk0 +z\mortothrokor\fid0 +z\mortothrokor\tlk0 +t\madcap\fid0 +t\madcap\tlk0 +p\hierophant\fid0 +p\hierophant\tlk0 +f\hotshot\fid0 +f\hotshot\tlk0 +t\hippogriff\fid0 +t\hippogriff\tlk0 +p\starsovereign\fid0 +p\starsovereign\tlk0 +p\epigraph\fid0 +p\epigraph\tlk0 +p\empyrean\fid0 +p\empyrean\tlk0 +p\empress\fid0 +p\empress\tlk0 +z\cikralisk\fid0 +z\cikralisk\tlk0 +z\kagralisk\fid0 +z\kagralisk\tlk0 +t\hypnagogue\fid0 +t\hypnagogue\tlk0 +t\pythean\fid0 +t\pythean\tlk0 +f\advisor_khalendricknights\fid0 +f\advisor_khalendricknights\tlk0 +f\advisor_orkoti\fid0 +f\advisor_orkoti\tlk0 +f\inzorthrokor\fid0 +f\inzorthrokor\tlk0 +p\artisan\fid0 +p\artisan\tlk0 +p\clarion\fid0 +p\clarion\tlk0 +p\charlatan\fid0 +p\charlatan\tlk0 +f\ashguard\fid0 +f\ashguard\tlk0 +p\oscilliant\fid0 +p\oscilliant\tlk0 +p\golem\fid0 +p\golem\tlk0 +p\magister\fid0 +p\magister\tlk0 +f\eidolon_nydus\fid0 +f\eidolon_nydus\tlk0 +sp\joran\fid0 +sp\joran\tlk0 +p\analogue\fid0 +p\analogue\tlk0 +sp\furen\fid0 +sp\furen\tlk0 +p\vassal\fid0 +p\vassal\tlk0 +p\vibrance\fid0 +p\vibrance\fid0 +p\idol\fid0 +p\idol\tlk0 +p\unar\fid0 +p\unar\tlk0 +sp\kortar\fid0 +sp\kortar\tlk0 +a\emissary\fid0 +a\emissary\tlk0 +sp\inc\goska\fid0 +sp\inc\goska\tlk0 +sp\inc\stroluum\fid0 +sp\inc\stroluum\tlk0 +sp\inc\tuzor\fid0 +sp\inc\tuzor\tlk0 +sp\inc\gorn\fid0 +sp\inc\gorn\tlk0 +sp\mephis\fid0 +sp\mephis\tlk0 +sp\inc\algannor\fid0 +sp\inc\algannor\tlk0 +sp\inc\thalazim\fid0 +sp\inc\thalazim\tlk0 +sp\inc\warmonger\fid0 +sp\inc\warmonger\tlk0 +sp\inc\yggdrasil\fid0 +sp\inc\yggdrasil\tlk0 +sp\organ_senescent\fid0 +sp\organ_senescent\tlk0 +sp\mirith\fid0 +sp\mirith\tlk0 +z\vilgorokor\fid0 +z\vilgorokor\tlk0 +z\tethzorokor\fid0 +z\tethzorokor\tlk0 +z\ghitorokor\fid0 +z\ghitorokor\tlk0 +z\tetcaerokor\fid0 +z\tetcaerokor\tlk0 +z\nathrokor\fid0 +z\nathrokor\tlk0 +z\sasyrvoleth\fid0 +z\sasyrvoleth\tlk0 +z\cherbrathalor\fid0 +z\cherbrathalor\tlk0 +z\okrimnilisk\fid0 +z\okrimnilisk\tlk0 +z\sovroleth\fid0 +z\sovroleth\tlk0 +z\algoztalisk\fid0 +z\algoztalisk\tlk0 +f\nathrelisk\fid0 +f\nathrelisk\tlk0 +z\almaksalisk\fid0 +z\almaksalisk\tlk0 +z\akistrokor\fid0 +z\akistrokor\tlk0 +z\kalkalisk\fid0 +z\kalkalisk\tlk0 +z\othstoleth\fid0 +z\othstoleth\tlk0 +z\zobriolisk\fid0 +z\zobriolisk\tlk0 +z\quazilisk\fid0 +z\quazilisk\tlk0 +z\tosgrilisk\fid0 +z\tosgrilisk\tlk0 +z\gosvileth\fid0 +z\gosvileth\tlk0 +z\gorgrokor\fid0 +z\gorgrokor\tlk0 +z\zarcavrokor\fid0 +z\zarcavrokor\tlk0 +z\liiralisk\fid0 +z\liiralisk\tlk0 +z\bactalisk\fid0 +z\bactalisk\tlk0 +z\iroleth\fid0 +z\iroleth\tlk0 +z\axitrilisk\fid0 +z\axitrilisk\tlk0 +t\ramesses\fid0 +t\ramesses\tlk0 +t\autocrat\fid0 +t\autocrat\tlk0 +t\heracles\fid0 +t\heracles\tlk0 +t\cuirass\fid0 +t\cuirass\tlk0 +f\auriel\fid0 +f\auriel\tlk0 +t\siren\fid0 +t\siren\tlk0 +sp\barlow\fid0 +sp\barlow\tlk0 +t\paladin\fid0 +t\paladin\tlk0 +sp\ajax\fid0 +sp\ajax\tlk0 +sp\eritreus\fid0 +sp\eritreus\tlk0 +t\apostle\fid0 +t\apostle\tlk0 +t\ancile\fid0 +t\ancile\tlk0 +f\wendigo\fid0 +f\wendigo\tlk0 +sp\piler\fid0 +sp\piler\tlk0 +sp\osina\fid0 +sp\osina\tlk0 +t\pazuzu\fid0 +t\pazuzu\tlk0 +sp\lannigan\fid0 +sp\lannigan\tlk0 +sp\esmond\fid0 +sp\esmond\tlk0 +sp\mazakel\fid0 +sp\mazakel\tlk0 +t\centaur\fid0 +t\centaur\tlk0 +t\aion\fid0 +t\aion\tlk0 +t\silvertongue\fid0 +t\silvertongue\tlk0 +t\durendal\fid0 +t\durendal\tlk0 +t\talos\fid0 +t\talos\tlk0 +z\konvilisk\fid0 +z\konvilisk\tlk0 +z\alkajelisk\fid0 +z\alkajelisk\tlk0 +t\anticthon\fid0 +t\anticthon\tlk0 +p\barghest\fid0 +p\barghest\tlk0 +p\simulacrum\fid0 +p\simulacrum\tlk0 +t\azazel\fid0 +t\azazel\tlk0 +sp\medly\fid0 +sp\medly\tlk0 +sp\terimanok\fid0 +sp\terimanok\tlk0 +sp\trodin\fid0 +sp\trodin\tlk0 +sp\tazarain\fid0 +sp\tazarain\tlk0 +sp\venammar\fid0 +sp\venammar\tlk0 +p\demiurge\fid0 +p\demiurge\tlk0 +p\aurora\fid0 +p\aurora\tlk0 +f\iziralisk\fid0 +f\iziralisk\tlk0 +a\avatar\fid0 +a\avatar\tlk0 +f\ecclesiast_khalendric\fid0 +f\ecclesiast_khalendric\tlk0 +f\ecclesiast_astral\fid0 +f\ecclesiast_astral\tlk0 +sp\pr0nogo\fid0 +sp\pr0nogo\tlk0 +f\arhaun\fid0 +f\arhaun\tlk0 +f\blackguard\fid0 +f\blackguard\tlk0 +f\blackjack_golden\fid0 +f\blackjack_golden\tlk0 +f\cicatus\fid0 +f\cicatus\tlk0 +f\cleric_futurist\fid0 +f\cleric_futurist\tlk0 +f\cyclops_futurist\fid0 +f\cyclops_futurist\tlk0 +f\dracadin_order\fid0 +f\dracadin_order\tlk0 +f\extatus\fid0 +f\extatus\tlk0 +f\harakan_golden\fid0 +f\harakan_golden\tlk0 +f\manifold_starbound\fid0 +f\manifold_starbound\tlk0 +f\qilin\fid0 +f\qilin\tlk0 +f\quazrokor\fid0 +f\quazrokor\tlk0 +f\renesair\fid0 +f\renesair\tlk0 +f\tiangou\fid0 +f\tiangou\tlk0 +f\zealot_starbound\fid0 +f\zealot_starbound\tlk0 +Ncivilian\NCiTlk0 +f\maverick_futurist\fid0 +f\maverick_futurist\tlk0 +f\maverick_upstart\fid0 +f\maverick_upstart\tlk0 +f\hydralisk_orkoti\fid0 +f\hydralisk_orkoti\tlk0 +f\maverick_golden\fid0 +f\maverick_golden\tlk0 +a\auxine\fid0 +a\auxine\tlk0 +sp\boyeau\fid0 +sp\boyeau\tlk0 +hl\fr\ferrum\fid0 +hl\fr\ferrum\tlk0 +hl\cv\prophet-1\fid0 +hl\cv\prophet-1\tlk0 +hl\cv\prophet-2\fid0 +hl\cv\prophet-2\tlk0 +b\advisor\fid0 +b\advisor\tlk0 +f\vestige\fid0 +f\vestige\tlk0 +f\rozirokor\fid0 +f\rozirokor\tlk0 diff --git a/mpq/rez/gluHist.tbl.txt b/mpq/rez/gluHist.tbl.txt new file mode 100644 index 00000000..756e153e --- /dev/null +++ b/mpq/rez/gluHist.tbl.txt @@ -0,0 +1,94 @@ +# tbl-text v1 - StarCraft: Brood War string table source +# One entry per line, in index order. +# <0> separates the parts of a multi-part entry +# raw byte N (decimal 0-255), e.g. <10> newline, <4> colour code +# << a literal '<' <> nothing (empty-entry marker) +# Lines starting with '#' are comments; blank lines are ignored. + +Tutorial: Boot Camp +1. Wasteland +2. Backwater Station +3. Desperate Alliance +4. The Jacobs Installation +5. Revolution +6. Norad II +7. The Trump Card +8. The Big Push +9. New Gettysberg +10. The Hammer Falls +Cinematic: Wasteland Patrol +Cinematic: The Downing of Norad II +Cinematic: Open Rebellion +Cinematic: The Inauguration +Terran Cinematic 5 +1. First Strike +2. Into the Flames +3. Higher Ground +4. The Hunt for Tassadar +5. Choosing Sides +6. Into the Darkness +7. Homeland +8. The Trial of Tassadar +9. Shadow Hunters +10. Eye of the Storm +Cinematic: The Fall of Fenix +Cinematic: The Ambush +Cinematic: The Return to Aiur +Cinematic: The Death of the Overmind +Protoss Cinematic 5 +1. Among the Ruins +2. Egression +3. The New Dominion +4. Agent of the Swarm +5. The Amerigo +6. The Dark Templar +7. The Culling +8. Eye for an Eye +9. The Invasion of Aiur +10. Full Circle +Cinematic: The Dream +Cinematic: Battle on the Amerigo +Cinematic: The Warp +Cinematic: The Invasion of Aiur +Zerg Cinematic 5 +Tutorial: Boot Camp +1. Strongarm +2. Den of the Beast +3. Extermination +Tutorial: Boot Camp +1. Strongarm +2. The Gauntlet +3. Den of the Beast +4. Force of Arms +5. Extermination +1. Escape from Aiur +2. Dunes of Shakuras +3. Legacy of the Xel<146>Naga +4. The Quest for Uraj +5. The Battle of Braxis +6. Return to Char +7. The Insurgent +8. Countdown +1. First Strike<32> +2. The Dylarian Shipyards +3. Ruins of Tarsonis +4. Assault on Korhal +5a. Emperor<146>s Fall (Ground Zero) +5b. Emperor<146>s Fall (Birds of War) +6. Emperor<146>s Flight +7. Patriot<146>s Blood +8. To Chain the Beast +1. Vile Disruption +2. Reign of Fire +3. The Kel-Morian Combine +4. The Liberation of Korhal +5. True Colors +6. Fury of the Swarm +7. Drawing of the Web +8. To Slay the Beast +9. The Reckoning +9a. Dark Origin +10. Omega +Cinematic: The Ascension +Cinematic: Fury of the Xel'Naga +Cinematic: UED Victory Report diff --git a/mpq/rez/help_txt.tbl.txt b/mpq/rez/help_txt.tbl.txt new file mode 100644 index 00000000..b6db2600 --- /dev/null +++ b/mpq/rez/help_txt.tbl.txt @@ -0,0 +1,47 @@ +# tbl-text v1 - StarCraft: Brood War string table source +# One entry per line, in index order. +# <0> separates the parts of a multi-part entry +# raw byte N (decimal 0-255), e.g. <10> newline, <4> colour code +# << a literal '<' <> nothing (empty-entry marker) +# Lines starting with '#' are comments; blank lines are ignored. + +<4>F10<2> - Game Menu<1> +<4>Alt-M<2> - Game Menu +<> +<4>Alt-H<2> - Help Menu +<4>Alt-O<2> - Options Menu +<> +<4>Ctrl-X<2> - Exit Starcraft +<4>Alt-X<2> - Exit Starcraft +<4>Ctrl-Q<2> - Quit Mission +<4>Alt-Q<2> - Quit Mission +<> +<4>Ctrl-M<2> - Toggle Music On/Off +<4>Ctrl-S<2> - Toggle SFX On/Off +<> +<4>Space Bar<2> - Last Transmission +<> +<4>Ctrl-<3>#<2> - Save Group <3># +<3>#<2> - Recall Group <3># +<4>Alt-<3>select<2> - Recall Group +<4>Ctrl-<3>select<2> - Select Unit Type +<3>double-click<2> - Select Unit Type +<> +<4>Shift-<3>F2<2> through <3>F8<2> - Save Camera Position +<3>F2<2> through <3>F8><2> - Restore Camera Position +<> +<4>Shift-<3>command<2> - Queue Order +<> +<4>Ctrl-C<2> - Center on Current Unit +<4>Alt-C<2> - Center on Current Unit +<> +<4>Tab<2> - Toggle Mini-map Terrain Mode +<4>Shift-Tab<2> - Toggle Mini-map Unit Mode +<> +Multiplayer Features: +<4>Enter<2> - Messaging Prompt +<4>Shift-Enter<2> - To All Messaging Prompt +<4>Ctrl-Enter<2> - To Allies Messaging Prompt +<> +<4>Ctrl-R<2> - Toggle Leaderboard Ranking Mode +<> diff --git a/mpq/rez/network.tbl.txt b/mpq/rez/network.tbl.txt new file mode 100644 index 00000000..4e474856 --- /dev/null +++ b/mpq/rez/network.tbl.txt @@ -0,0 +1,178 @@ +# tbl-text v1 - StarCraft: Brood War string table source +# One entry per line, in index order. +# <0> separates the parts of a multi-part entry +# raw byte N (decimal 0-255), e.g. <10> newline, <4> colour code +# << a literal '<' <> nothing (empty-entry marker) +# Lines starting with '#' are comments; blank lines are ignored. + +An error occurred while deleting game '%s' +An error occurred while replacing game '%s' +Replace the contents of game '%s'? +Delete the game '%s'? +Cannot create any more save files. +That file name is a reserved name. Please choose another name. +Zerg +Terran +Protoss +Random +Deputy Dad +hours +hour +minutes +minute +Mission completed. Continuing play with map triggers disabled. +*\/:><<|&+^?" +WARNING: Unplaceable unit! ID (%s), coordinates (%d,%d) +Team 1 +Team 2 +Team 3 +Team 4 +Force 1 +Force 2 +Force 3 +Force 4 +Observer +Observers +Elite Guard +Mar Sara +Kel-Morian Combine +Antiga +Delta Squadron +Omega Squadron +Alpha Squadron +Epsilon Squadron +Tiamat Brood +Surtur Brood +Fenris Brood +Jormungand Brood +Garm Brood +Grendel Brood +Baelrog Brood +Leviathan Brood +Ara Tribe +Sargas Tribe +Akilae Tribe +Furinax Tribe +Auriga Tribe +Venatir Tribe +Shelak Tribe +Velari Tribe +%s was eliminated. +Player %d was eliminated. BYE! +Player %s left the game. BYE! +%s had a Canada moment. +%s paused the game. (%d timeouts remaining) +%s resumed the game. +* Message Sent * +Building exit is blocked. +Cannot create more u. +Unable to create u. +Select Target +Select Location +uhh +u +%s is now playing as an observer. +You are now playing as an observer. +Player %s set network for low brain cells. +Player %s set network for high brain cells. +Player %s set network for extra high brain cells. +Anonymous +Illegal number of teams. +Bloop! It's %s! +Bye %s!!! +%s had a Canada moment. +Ignoring messages from %s. +Accepting messages from %s. +< +< +A network error has occured. +Error initializing standard.snp. Please reinstall StarCraft. +Unable to initialize network provider. +Unable to initialize device. +Invalid network provider. +Unable to register events. +Invalid player. +Invalid character. +Unable to join game. +Unable to join game. The host is running either a different version of Cosmonarchy Brood War, or a different version of Starcraft. +Unable to send message. +Unable to create single player game. +Invalid game options. +Unable to create multi player game. +Save Game File Not Found. +Error launching game. +Error loading scenario file. +Unable to load saved game file. +Unable to load the SCM associated with this save file. +Unable to distribute map. +Unable to write save file. +Unable to load campaign options. Please reinstall StarCraft. +You must own the full version of StarCraft to play the single player campaigns. +You must own the full version of StarCraft to create a multiplayer game. +You can only join games created by the owner of the CD that spawned your copy. +Unable to find the specified campaign map. +Network upgrade failed. +SYNC ERROR. +Saving +Screen captured to file '%s'. +%s was %s and played for %s<10> +On map "%s":<10> +Overall Score %u<10> %10u for %s<10> %10u for %s<10> %10u for %s<10> +Units Score %u<10> %10u Units %s<10> %10u Units %s<10> %10u Units %s<10> +Structures Score %u<10> %10u Structures %s<10> %10u Structures %s<10> %10u Structures %s<10> +Resources Score %u<10> %10u %s<10> %10u %s<10> %10u %s<10> +Destroy all of an opponent's buildings to eliminate him. +Accumulate 2500 ore.<10>Accumulate 2500 gas. +Accumulate 5000 ore.<10>Accumulate 5000 gas. +Accumulate 7500 ore.<10>Accumulate 7500 gas. +Accumulate 10000 ore.<10>Accumulate 10000 gas. +Get the most kills or razings before time runs out. +Capture an opponent's flag to eliminate him. +Destroy any opponent's HQs to eliminate him. +%s had his flag taken! +%s had their flag taken! +%s had his flag return to his base. +%s had their flag return to their base. +%s had his flag captured! +%s had their flag captured! +You must configure your system for voice chat before attempting to change settings. To configure, please exit Starcraft and run the voice configuration utility "STARVOX.exe". +Voice chat settings can only be modified while you are playing a multiplayer game. +Your system does not have DirectSound installed. Please run the setup program supplied with Starcraft to install DirectX drivers for your system. +Your system does not appear to be configured for voice chat. Please reinstall Starcraft using the install program on the Starcraft CDROM. +Another application is using audio. Please exit the other application and try running this application again. +An error occurred while attempting to initialize voice fonts: %s<10>Error 0x%08x %s +An error occurred while attempting to initialize voice capture: %s<10>Error 0x%08x %s +An error occurred while attempting to initialize sound: %s<10>Error 0x%08x %s +An error occurred while attempting to initialize voice playback: %s<10>Error 0x%08x %s +ERROR: cannot play music from network drive. +We recommend that you complete the Terran Campaign before playing this campaign.<10><10>Do you wish to play the Zerg Campaign anyway? +We recommend that you complete the Terran and Zerg Campaigns before playing this campaign.<10><10>Do you wish to play the Protoss Campaign anyway? +We recommend that you complete the Protoss Campaign before playing this campaign.<10><10>Do you wish to play the Terran Campaign anyway? +We recommend that you complete the Protoss and Terran Campaigns before playing this campaign.<10><10>Do you wish to play the Zerg Campaign anyway? +The Ackmen +Todny Terranus +Deputy Dads +Vacuum Busboys +The Ciox Crew +sk8rquippers +Crows from Kenya +Foreign Dogwalkers +N's Miracle Workers +Hydra5620 +Pathfinding Organ +Whathere? +The Friendship Buffet +BonziBossy +BingBongBorrowers +Amon's Lost Dogs +Knights of a Realm +Champions of Iquaria +Day 7: Autoveeqality +Solawrong! +Estranged Aviary +F.R.A.U.D.s +Tree Tribe +Shadowcloaked Skullcrushers +Askosi +Bloodsummon +Kogri diff --git a/mpq/rez/stat_txt.tbl.txt b/mpq/rez/stat_txt.tbl.txt new file mode 100644 index 00000000..0c551bf4 --- /dev/null +++ b/mpq/rez/stat_txt.tbl.txt @@ -0,0 +1,1820 @@ +# tbl-text v1 - StarCraft: Brood War string table source +# One entry per line, in index order. +# <0> separates the parts of a multi-part entry +# raw byte N (decimal 0-255), e.g. <10> newline, <4> colour code +# << a literal '<' <> nothing (empty-entry marker) +# Lines starting with '#' are comments; blank lines are ignored. + +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +Spider Mines +<> +<> +<> +<> +<> +<> +<> +<164><3><4>Flanking Spines <1>(passive)<10>When targeting debuffed enemies, Almaksalisk attacks ignore armor. +Anchor Attack requires:<10> Loaded ranged combatant +<> +<> +<164><3><4>Autophagy <1>(passive)<10>On death, Vilgorokors transform into Vilgoleths and immediately spawn 2 Rilirokors. +<164><3><4>Heretical Bedlam <1>(passive)<10>Dying hostiles that were struck by the Pariah in the last 2 seconds have their corpses ascended after 1 second. +<> +<164><3><4>Swarming Omen <1>(passive)<10>Hostiles afflicted by the Zoryusthaleth's attacks grant 33% increased movement speed to any of the Zoryusthaleth's allies within 10 range. +<> +<> +<> +t<3><4>Inject Stimpack <1>(<3>T<1>)<10>Take a dangerous injection that deals 5 damage. After a 1.5 second delay, gain 75% movement speed and 100% attack speed for 7.5 seconds. +e<3><4>Lay Lobotomy Mines <1>(<3>E<1>)<10>Places a Lobotomy Mine at the target site. Lobotomy Mines stun and slow targets that trip them. +e<3><4>Lay Tartarus Mines <1>(<3>E<1>)<10>Places a Tartarus Mine at the target site. Tartarus Mines incinerate and rend the armor of targets that trip them. +e<3><4>Siege Mode <1>(<3>E<1>)<10>Switches forms, gaining enhanced range and splash radius and disabling movement. +e<0><4>Tank Mode <1>(<3>E<1>)<10>Returns the Phalanx's weapon and mobility to normal. +d<3><4>Defensive Matrix <1>(<3>D<1>)<10>Fortifies the target with a 100 HP shield that cleanses all debuffs and heals the target for 10 HP per second.<10><10>Defensive Matrix lasts 10 seconds or until the shield is destroyed. +<164><3><4>Endless Limbs <1>(passive)<10>Okrimnilisks launch Omnispores at all targets within range, even while moving. +r<3><4>Irradiate <1>(<3>R<1>)<10>Launches a missile that creates a radiation field on impact that lasts 10 seconds.<10><10>Any target that walks through the field is afflicted with deadly radiation that deals 50 damage over 5 seconds. +g<3><4>Yamato Cannon <1>(<3>G<1>)<10>After channeling for 3 seconds, the Magnetar fires a laser blast that deals 250 damage in a 3-range area. +f<1><4>Morph into Inzorthrokor <1>(<3>F<1>)<10>Infestation strain. +Inzorthrokor requires:<10> Zorkiz Shroud +c<1><4>Train Heracles <1>(<3>C<1>)<10>Frontline infantry. +b<1><4>Build Tiangou <1>(<3>B<1>)<10>Relentless walker. +<> +c<1><4>Warp in Ecclesiast <1>(<3>C<1>)<10>Supportive ranger. +e<3><4>Smithy Mode <1>(<3>E<1>)<10>Switches forms, gaining the ability to repair structures at range and disabling movement. +e<0><4>Worker Mode <1>(<3>E<1>)<10>Returns the Ogun's repairs and mobility to normal. +<> +<> +<> +s<1><4>Morph into Protathalor <1>(<3>S<1>)<10>Mobile artillery strain. +<> +<> +<> +<> +<> +<> +<> +<> +b<1><4>Build Blackjack <1>(<3>B<1>)<10>Scrappy walker drone. +<164><3><4>Labyrinthine Targeters <1>(passive)<10>So long as they are facing the targets, Minotaurs can attack up to 5 hostiles at once. +Almaksalisk requires:<10> Alkag Iteth +b<1><4>Morph into Almaksalisk <1>(<3>B<1>)<10>Assault flyer. +f<1><4>Morph into Zoryusthaleth <1>(<3>F<1>)<10>Frontline support strain. +Zoryusthaleth requires:<10> Zorkiz Shroud +q<1><4>Morph into Quazilisks <1>(<3>Q<1>)<10>Pursuant strain. +Quazilisk requires:<10> Quazeth Pool +r<3><4>Burrow <1>(<3>R<1>)<10>Burrows the unit, concealing it from enemy vision and rendering it immobile. +r<0><4>Unburrow <1>(<3>R<1>)<10>Unburrows the unit, revealing it and allowing it to move. +<> +<> +w<3><4>Carapace Swarm <1>(<3>W<1>)<10>Creates damage-absorbing cloud cover that lasts for 25 seconds or until it has absorbed 1000 damage. +x<3><4>Parasitic Excision <1>(<3>X<1>)<10>After a 1-second channel, the Matraleth deposits a Vilgoleth at the target location. +<> +<> +g<3><4>Plague <1>(<3>G<1>)<10>After a 1-second channel, creates a 3-range zone that lasts 10 seconds. Victims that enter the zone are infected with Plague, and take up to 50 damage over 5 seconds.<10>On death, victims create a smaller 1.5-range infection zone that lasts 5 seconds. Plague cannot kill its victims. +<164><3><4>Ensnaring Brood <1>(passive)<10>Allies within 4 range of a Matraleth apply Ensnare with every attack. Ensnared targets move and attack 33% slower. Ensnare expires after 2 seconds. +a<1><4>Assemble Vassal <1>(<3>A<1>)<10>Light fighter. +<> +<> +<164><3><4>Robotic Fleet <1>(passive)<10>Solarions can assemble up to 12 Stewards to use in their attacks. Solarions cannot attack without any Stewards. +v<1><4>Warp in Prostration Stage <1>(<3>V<1>)<10>Relentless disciple production. +z<1><4>Mutate into Zorkiz Shroud <1>(<3>Z<1>)<10>Control node. Enables Zoryusthaleths and Lakizilisks. +e<1><4>Warp in Rogue Gallery <1>(<3>E<1>)<10>Profane disciple production. +<> +<> +<> +<> +e<3><4>Rapid Rendezvous <1>(<3>E<1>)<10>Leaps to the target ground ally within 5 range. +c<3><4>Hijack <1>(<3>C<1>)<10>Leaps to the target hostile, attaching to the victim and slowing their movement speed by 50%.<10><10>After 5 seconds, the Askosi combatant becomes the target. +e<3><4>Rematerialize <1>(<3>E<1>)<10>Pulls a target ally within 4 range to the combatant's location. +e<1><4>Mutate into Kalkiir Lake <1>(<3>E<1>)<10>Swarm node. Enables Liiralisks and Kalkalisks. +Terran Structure Armor +Zerg Structure Carapace +Protoss Structure Plating +t<3><4>Psionic Storm <1>(<3>T<1>)<10>Channels to summon a psionic field at the target site, dealing 20 damage per second to combatants and half damage to structures. Drains 20 energy per second. On recast, the field moves to the new target location. +e<3><4>Hallucination <1>(<3>E<1>)<10>Fires a psi bolt that creates a dazing zone for 10 seconds. Targets that pass through the zone hallucinate for 3 seconds. Weapons that damage hallucinating victims create a phantom strike that deals half damage. +r<3><4>Dimensional Recall <1>(<3>R<1>)<10>After a 4-second initial channel, the source opens a warp rift at the target site. Once opened, the warp rift drains 5 energy per second, and teleports 1 ally every 0.5 seconds to the source's position. +t<3><4>Stasis Field <1>(<3>T<1>)<10>After channeling for nearly a second, the Didact stuns and fortifies all combatants within 3 range of the target area for 8 seconds. +r<1><4>Warp in Archon <1>(<3>R<1>)<10>Frontline martyr. +<164><3><4>Nemean Reactors <1>(passive)<10>With every attack, Heracles gain +1 damage. This bonus falls off entirely after 2 seconds of not attacking. +<164><3><4>Deconstruction <1>(passive)<10>Targets struck by the Pazuzu are debuffed for 5 seconds, and deal 5% of their weapon damage per stack to themselves when attacking. +e<1><4>Build Pazuzu <1>(<3>E<1>)<10>Anti-mech hovertank. +a<1><4>Mutate into Vornath Pond <1>(<3>A<1>)<10>Swarm node. Enables Nathrokors and Vorvrokors. +a<1><4>Raise Aspirant <1>(<3>A<1>)<10>Ascendent walker. +t<1><4>Mutate into Skibact Scab <1>(<3>T<1>)<10>Assault node. Enables Skithrokors and Bactalisks. +Terran Infantry Armor +Terran Vehicle Plating +Terran Ship Plating +Zerg Carapace +Zerg Flyer Carapace +Protoss Armor +Protoss Plating +Terran Infantry Weapons +Terran Vehicle Weapons +Terran Ship Weapons +Zerg Melee Attacks +Zerg Missile Attacks +Zerg Flyer Attacks +Protoss Ground Weapons +Protoss Air Weapons +Plasma Shields +<164><3><4>Reconstitution <1>(passive)<10>2 seconds after taking direct damage, this strain releases a Carapace Swarm that lasts up to 5 seconds and absorbs up to 200 damage. +t<1><4>Warp in Monument of Sin <1>(<3>T<1>)<10>Capital stellar production. +Axtoth Axis requires:<10> Othstol Oviform +f<1><4>Bestow Harakan <1>(<3>F<1>)<10>Dutiful frontline infantry. +<> +x<1><4>Morph into Axitrilisk <1>(<3>X<1>)<10>Sustaining skirmisher strain. +Axitrilisk requires:<10> Axtoth Axis +<164><3><4>Perpetual Wind <1>(passive)<10>Upon taking fatal damage, Axitrilisks revert to a weaponless state and attempt to rejuvenate.<10><10>Should they survive in this form for eight seconds, Axitrilisks recover full use of their weapon. +<164><3><4>Anodyne Annex <1>(passive)<10>Dying hostiles that were struck by the Empress in the last 2 seconds become subserviant and zombified.<10>Zombies are stunned if allied masters are outside 8 range for longer than 1 second, and expire if outside 8 range for 5 seconds. Zombies have their corpses avenged 1 second after death. +b<1><4>Bestow Blackjack <1>(<3>B<1>)<10>Dutiful walker drone. +<164><3><4>Gamete Meiosis <1>(passive)<10>Whenever a broodspawn is birthed, allied Matraleths within 6 range of the birthing site gain +5 energy. +z<1><4>Bestow Luminary <1>(<3>Z<1>)<10>Disruptive specialist craft. Provides detection. +a<1><4>Bestow Aspirant <1>(<3>A<1>)<10>Unstable strider. +s<1><4>Train Striga <1>(<3>S<1>)<10>Isolating infantry. +z<1><4>Raise Luminary <1>(<3>Z<1>)<10>Disruptive craft. Provides detection. +<164><3><4>Incineration <1>(passive)<10>Victims of Auriel attacks take 4 damage per second for 2 seconds. If a victim dies while ablaze, their corpse is incinerated, leaving behind a landborne fire field that applies Apollyoid Adhesive. +q<1><4>Splice Tarasque <1>(<3>Q<1>)<10>Frontal biotank. +g<1><4>Assemble Analogue <1>(<3>G<1>)<10>Durable disruption drone. +z<1><4>Splice Anticthon <1>(<3>Z<1>)<10>Corpse engine. +c<1><4>Mutate to Chokral Chaszal <1>(<3>C<1>)<10>Assault node. Enables Cherbrathalors, Okrimnilisks, and Algoztalisks. +x<1><4>Mutate into Axtoth Axis <1>(<3>X<1>)<10>Skirmisher node. Enables Mortothrokors, Axitrilisks, and Sasyrvoleths. +b<1><4>Build Biotic Bastion <1>(<3>B<1>)<10>Enables Anticthons and Aions. +t<1><4>Train Auriel <1>(<3>T<1>)<10>Purifying infantry. +d<1><4>Morph into Mortothrokor <1>(<3>D<1>)<10>Rampaging melee strain. +w<1><4>Build Tinkerer's Tower <1>(<3>W<1>)<10>Enables Sirens and Strigas. +e<1><4>Profanate Empress <1>(<3>E<1>)<10>Reinforcing destroyer. +<164><3><4>Escape Velocity <1>(passive)<10>Algoztalisk projectiles gain +1 armor penetration for every range traveled. +<> +<> +<> +Iron Foundry requires:<10> Daedala +f<1><4>Build Iron Foundry <1>(<3>F<1>)<10>Advanced vehicle production. +<> +z<1><4>Mutate into Kazitrith Cesiant <1>(<3>Z<1>)<10>Support cell. +<164><3><4>Plethoral Gestation <1>(passive)<10>This strain is not consumed by tissue construction, and emerges when its work is complete. +<164><3><4>Aberrant Gestation <1>(passive)<10>This strain mutates directly into advanced tissues. +<164><3><4>Radiant Rifts <1>(passive)<10>Upon completing a harvest, Artisans channel for 2 seconds, returning their cargo at the end of the channel. +<> +<164><3><4>Wear and Tear <1>(passive)<10>When under half health, Blackjacks lose 33% movement and attack speeds. +t<1><4>Morph into Gosvileth <1>(<3>T<1>)<10>Advanced worker strain. +<164><3><4>Criticality <1>(passive)<10>Victims of the Aspirant are debuffed for 2 seconds. If killed during this time, they create a 2-range energy field that lasts 5 seconds and restores 4 shields or HP per second for all allied ground combatants. +<> +Gosvil Aspect requires:<10> Irol Iris +<> +a<1><4>Assemble Artisan <1>(<3>A<1>)<10>Advanced harvester. +<164><3><4>Caustic Discharge <1>(passive)<10>Keskathalor attacks create a pool of acid that deals 30 impact damage over 5 seconds to all ground targets. Targets that move through the acid gain 150% movement speed for 3 seconds. +<164><3><4>Ion Field <1>(passive)<10>Diadem attacks leave behind a field that deals 100 damage over 10 seconds to all ground targets. Units that move through the field gain 150% attack speed for 2 seconds. +<164><3><4>Apollyoid Conflagration <1>(passive)<10>Tartarus Mines incinerate hostiles within 2 range for 5 seconds, and rends their armor by 1 for the same duration.<10><10>If the mine is destroyed or manually detonated, nearby hostiles only have their armor rended. +<27><0><4>Cancel Ability<1>(<3>ESC<1>) +<164><3><4>Dazing Detonation <1>(passive)<10>Lobotomy Mines stun all targets within 3 range for 3 seconds, and slow their movement speeds by 33% for an additional 3 seconds. If the mine is destroyed, nearby targets are only slowed. +Liiral Aspect requires:<10> Kalkiir Lake +e<1><4>Morph into Liiralisk <1>(<3>E<1>)<10>Skirmisher strain. +Sovrol Aspect requires:<10> Tosvrol Haunt +d<1><4>Morph into Sovroleths <1>(<3>D<1>)<10>Symbiote strain. +<164><3><4>Whetted Appetite <1>(passive)<10>Seeing death empowers the Mortothrokor, granting 5% lifesteal and increased attack speed.<10><10>This bonus can stack and expires after being out of combat for 2 seconds. +<> +c<1><4>Splice Cohort <1>(<3>C<1>)<10>Everlasting infantry. +<164><3><4>Mitosal Fog <1>(passive)<10>When the Nathrelisk attacks, it releases a cloud that heals all units within 1 range for 2 HP before dissipating. +z<0><4>Swarming Strains <1>(<3>Z<1>) +x<0><4>Assault Strains <1>(<3>X<1>) +c<0><4>Control Strains <1>(<3>C<1>) +<27><0><4>Release Storm <1>(<3>ESC<1>) +<164><3><4>Dishonorable Discharge <1>(passive)<10>When striking targets with their backs turned, this combatant's attacks convert excess armor penetration into bonus damage. +a<1><4>Train Autocrat <1>(<3>A<1>)<10>Reigning infantry. +e<1><4>Train Olympian <1>(<3>E<1>)<10>Elite infantry. +g<1><4>Build Guildhall <1>(<3>G<1>)<10>Enables Olympians and Autocrats. +Guildhall requires:<10> Daedala +<> +r<3><4>Aural Assault <1>(<3>R<1>)<10>Fires a projectile that blinds all targets within 2 range for 2 seconds. Blinded combatants have their weapon range reduced to a maximum of 2. +Okrimnil Aspect requires:<10> Chokral Chazsal +<164><3><4>Fleeting Focus <1>(passive)<10>When struck by a mobile hostile within 2 range, Strigas repulse their attacker backwards by 1 range. +z<3><4>Lobotomize <1>(<3>Z<1>)<10>Detonates the Lobotomy Mine immediately, applying its slow effects to all combatants within 2 range. +c<3><4>Conflagrate <1>(<3>Z<1>)<10>Detonates the Tartarus Mine immediately, applying its armor rend to all combatants within 2 range. +c<3><4>Controlled Demolition <1>(<3>C<1>)<10>After 10 seconds, the structure explodes in a controlled demolition, dealing no damage to nearby targets. This process can be cancelled. +e<3><4>Emergency Detonation <1>(<3>E<1>)<10>After 3 seconds, the structure self-destructs violently, dealing up to 250 damage to all targets within 3 range. +<> +<164><3><4>Crushing Tide <1>(passive)<10>This strain can strike up to three targets with every attack. Each one of their attacks can bounce to one additional target for 50% damage. +<> +<> +a<0><4>Assimilated Strains <1>(<3>A<1>) +t<1><4>Warp in Astral Omen <1>(<3>T<1>)<10>Corvette production. +<164><3><4>Kelvin Munitions <1>(passive)<10>Victims of this combatant's weapon have their movement speed slowed by 33% for 3 seconds. +<164><3><4>Offset: Cone <1>(passive)<10>This combatant's weapon fires its projectiles in a cone pattern. +<> +<164><3><4>Leading the Blind <1>(passive)<10>Victims of the Charlatan's attacks hallucinate for 1 second. Attackers that damage hallucinations launch a phantom strike that deals half damage. +c<1><4>Profanate Charlatan <1>(<3>C<1>)<10>Chaotic ranger. +<164><3><4>Astral Blessing <1>(passive)<10>Victims of this combatant's attacks radiate power, restoring 2 shields to any allies of attacker within 3 range. +<> +<164><3><4>Lashing Limbs <1>(passive)<10>While moving, Evigrilisks can strike up to 4 targets within their weapon range.<10><10>While stationary, Evigrilisks focus their attacks on a single target. +e<1><4>Warp in Empyrean <1>(<3>E<1>)<10>Frontal destroyer. +v<1><4>Morph into Evigrilisk <1>(<3>V<1>)<10>Versatile assault strain. +Evigrilisk requires:<10> Alkag Iteth +<164><3><4>Eternal Service <1>(passive)<10>This combatant restores 1 shield point for every point of damage dealt. +<164><3><4>Scorched Earth <1>(passive)<10>If a loaded Anvil is destroyed (other than by Controlled Demolition), its missile will detonate. +<> +r<1><4>Assemble Clarion <1>(<3>R<1>)<10>Support craft. +<164><3><4>Multi-Attack IV <1>(passive)<10>This combatant's primary weapon can fire at up to four targets. +Azanthrokor requires:<10> Ulgorg Cavern +a<1><4>Build Ancile <1>(<3>A<1>)<10>Weaponless specialist bulwark. +x<1><4>Warp in Matrix <1>(<3>X<1>)<10>Enduring tower. +d<1><4>Build Sundog <1>(<3>D<1>)<10>Ambush fighter. +<164><3><4>Shroud of Adun <1>(passive)<10>Didacts cloak all allies within 4 range. +r<5><4>Morph into Azanthrokor <1>(<3>R<1>)<10>Besieging corvette strain. +<164><3><4>Larval Gestation <1>(passive)<10>This cell generates one Larva every 14 seconds. +x<1><4>Warp in Exemplar <1>(<3>X<1>)<10>Anti-armor frigate. +Mimic requires:<10> Cipher +a<1><4>Build Starpad <1>(<3>A<1>)<10>Light orbital production. +v<1><4>Mutate into Larvosk Cesiant <1>(<3>V<1>)<10>Production cell. +<164><3><4>Forward March <1>(passive)<10>This combatant's secondary weapon can be fired while moving. +Vicar requires:<10> Gauntlet +a<1><4>Build Matador <1>(<3>A<1>)<10>Frontline microtank. +a<1><4>Build Paladin <1>(<3>A<1>)<10>Artillery walker. +<164><3><4>Multi-Attack V <1>(passive)<10>This combatant's primary weapon can fire at up to five targets. +<164><3><4>Multi-Attack III <1>(passive)<10>This combatant's primary weapon can fire at up to three targets. +x<1><4>Build Hadron <1>(<3>X<1>)<10>Enables Hippogriffs and Phoboses. +1<0><4>Select Faction 1 <1>(<3>1<1>)<10><2>Orion Imperium, Divine Primacy, Ataltreth Tendril +<164><3><4>Offset: Linear <1>(passive)<10>This combatant's weapon fires its projectiles in a line pattern. +f<1><4>Build Hippogriff <1>(<3>F<1>)<10>Siege corvette. +<164><3><4>Empathy Core <1>(passive)<10>Each time the Analogue is hit by an attack, it expends 5 energy. If they survive the attack, they clone it as a phantom strike, retaliating against their attacker for half damage. +s<1><4>Raise Mimic <1>(<3>S<1>)<10>Empathic recycler. +s<1><4>Raise Vicar <1>(<3>S<1>)<10>Shepherding trooper. +s<1><4>Raise Cardinal <1>(<3>S<1>)<10>Frontline recycler. +r<1><4>Raise Pilgrim <1>(<3>R<1>)<10>Worker. +e<1><4>Morph into Lakizilisk <1>(<3>E<1>)<10>Area denial ambusher. +r<5><4>Morph into Gorgrokor <1>(<3>R<1>)<10>Durable melee strain. +Lakizilisk requires:<10> Zorkiz Shroud +Gorgrokor requires:<10> Ulgorg Cavern +b<1><4>Build Rotary <1>(<3>B<1>)<10>Enables Sundogs and Salamanders. +<164><3><4>Projection <1>(passive)<10>This combatant's melee attacks project forward, striking all targets in front of the attacker. +d<1><4>Raise Dyad <1>(<3>D<1>)<10>Dedicated constructor. +Dyad requires:<10> Cipher OR Gauntlet +Proselyte requires:<10> Radix OR Function +<164><3><4>Nihilism <1>(passive)<10>Upon taking fatal damage, Vagrants transform into a sinister shade.<10><10>Shades gain 50% increased movement and attack speeds, and take 2 damage per second until expiring. +d<1><4>Mutate into Tosvrol Haunt <1>(<3>D<1>)<10>Sunblot node. Enables Ghitorokors, Sovroleths, and Tosgrilisks. +Tosvrol Haunt requires:<10> Othstol Oviform +v<1><4>Warp in Vagrant <1>(<3>V<1>)<10>Posthumous ranger. +<164><3><4>Blade Vortex <1>(passive)<10>Victims of Legionnaire attacks serve as beacons for hostile Legionnaires, allowing instant teleportation when within 3 range. This effect expires after 2 seconds. +<164><3><4>Lithe Organelle <1>(passive)<10>This strain's projectiles bounce to up to two nearby targets, dealing 33% reduced damage with each bounce. +s<1><4>Raise Missionary <1>(<3>S<1>)<10>All-purpose trooper. +s<1><4>Raise Proselyte <1>(<3>S<1>)<10>Nimble trooper. +g<1><4>Morph into Agrileth <1>(<3>G<1>)<10>Construction strain. +x<1><4>Build Aion <1>(<3>X<1>)<10>Reclaiming armor. +t<1><4>Morph into Nathrelisk <1>(<3>R<1>)<10>Sustaining fighter strain. +<164><3><4>Corrosive Pathogen <1>(passive)<10>Vithrilisk attacks debuff all hostiles within 2 range of their target, reducing their attack speed by 25% and armor by 1 for 3 seconds. +<164><3><4>Corrosive Touch <1>(passive)<10>This strain's attacks debuff enemies, increasing attack cooldown by 25% and reducing armor by 1 for 3 seconds. +Nathrelisk requires:<10> Vornath Pond +Stewards +ESC +z<1><4>Morph into Zethrokors <1>(<3>Z<1>)<10>Fast melee strain. +h<1><4>Morph into Hydralisk <1>(<3>H<1>)<10>All-purpose ranged strain. +t<1><4>Morph into Ultrokor <1>(<3>T<1>)<10>Unstoppable melee strain. +d<1><4>Morph into Droleth <1>(<3>D<1>)<10>Worker strain. +v<1><4>Morph into Ovileth <1>(<3>V<1>)<10>Transport strain. Provides detection. +t<1><4>Morph into Mutalisk <1>(<3>T<1>)<10>Fast harassment air strain. +g<1><4>Morph into Geszithalor <1>(<3>G<1>)<10>Artillery strain. +q<1><4>Morph into Matraleth <1>(<3>Q<1>)<10>Disruptive specialist strain. +d<1><4>Morph into Isthrathaleth <1>(<3>D<1>)<10>Area-denial specialist strain. +Protathalor requires:<10> Mutath Spire +<164><3><4>Repentant Chorus <1>(passive)<10>While decloaked, this combatant grants +1 armor penetration to all allies within 5 range. +a<1><4>Train Maverick <1>(<3>A<1>)<10>All-purpose infantry. +g<1><4>Train Eidolon <1>(<3>G<1>)<10>Disruptive specialist infantry. +f<1><4>Train Harakan <1>(<3>F<1>)<10>Durable frontline infantry. +v<1><4>Build Vulture <1>(<3>V<1>)<10>Fast anti-infantry harasser. +g<1><4>Build Goliath <1>(<3>G<1>)<10>Versatile walker. +t<1><4>Build Phalanx <1>(<3>T<1>)<10>Durable artillery. +s<1><4>Build Mason <1>(<3>S<1>)<10>Worker. +w<1><4>Build Wraith <1>(<3>W<1>)<10>Light fighter craft. +s<1><4>Build Seraph <1>(<3>S<1>)<10>Offensive support craft. Provides detection. +d<1><4>Build Trojan <1>(<3>D<1>)<10>Transport craft. +b<1><4>Build Minotaur <1>(<3>B<1>)<10>Capital ship. +x<1><4>Arm Morningstar <1>(<3>X<1>)<10>Constructs and arms a Morningstar missile, to be deployed by a specialist. +w<1><4>Assemble Witness <1>(<3>W<1>)<10>Cloaked spy craft. Provides detection. +e<1><4>Assemble Scribe <1>(<3>E<1>)<10>Robotic worker. +z<1><4>Warp in Zealot <1>(<3>Z<1>)<10>Durable melee warrior. +d<1><4>Warp in Dracadin <1>(<3>D<1>)<10>Ranged strider. +t<1><4>Warp in Cantavis <1>(<3>T<1>)<10>Area-denial specialist. +s<1><4>Assemble Envoy <1>(<3>S<1>)<10>Transport craft. +s<1><4>Warp in Panoptus <1>(<3>S<1>)<10>Durable frigate. +a<1><4>Warp in Didact <1>(<3>A<1>)<10>Disruptive support craft. +c<1><4>Warp in Solarion <1>(<3>C<1>)<10>Capital ship. +t<1><4>Assemble Steward <1>(<3>T<1>)<10>Robotic craft used for the Solarion's attack. +v<1><4>Assemble Accantor <1>(<3>V<1>)<10>Artillery crawler. +r<1><4>Assemble Plasma Shell <1>(<3>R<1>)<10>Munitions used for the Accantor's attack. +r<1><4>Warp in Epigraph <1>(<3>R<1>)<10>Assault corvette. +r<1><4>Train Cyprian <1>(<3>R<1>)<10>Mid-range infantry. +z<1><4>Morph into Tethzorokor <1>(<3>Z<1>)<10>Heavy frontline strain. +h<1><4>Mutate into Hachirosk <1>(<3>H<1>)<10>Essential production cell. +c<1><4>Mutate into Cagrant Cesiant <1>(<3>C<1>)<10>Essential utility cell. +e<1><4>Mutate into Excisant <1>(<3>E<1>)<10>Vespene processor. +s<1><4>Mutate into Quazeth Pool <1>(<3>S<1>)<10>Swarm node. Enables Zethrokors and Quazilisks. +a<1><4>Mutate into Almakis Antre <1>(<3>A<1>)<10>Subjugation node. Enables Tetcaerokors, Isthrathaleths, and Akistrokors. +d<1><4>Mutate into Hydrith Den <1>(<3>D<1>)<10>Assault node. Enables Hydralisks and Izirokors. +c<1><4>Mutate into Nydus Cesiant <1>(<3>C<1>)<10>Utility organ. Provides transport between nodes. +r<1><4>Mutate into Mutath Spire <1>(<3>R<1>)<10>Swarm node. Enables Mutalisks, Protathalors, and Cikralisks. +q<1><4>Mutate into Matravil Nest <1>(<3>Q<1>)<10>Subjugation node. Enables Konvilisks, Vilgorokos, and Matraleths. +r<1><4>Mutate into Ulgorg Cavern <1>(<3>R<1>)<10>Control node. Enables Ultrokors and Gorgrokors. +d<1><4>Morph into Okrimnilisk <1>(<3>D<1>)<10>Platform strain. +e<1><4>Mutate into Caphrolosk <1>(<3>E<1>)<10>Aberrant production cell with mutant Larvae. +g<1><4>Mutate into Gathtelosk <1>(<3>G<1>)<10>Virile production cell with twice-effective Larvae. +r<1><4>Meld Rove <1>(<3>R<1>)<10>Speedy vehicle. +s<1><4>Mutate into Spraith Cesiant <1>(<3>S<1>)<10>Anti-air defensive cell. +e<1><4>Mutate into Surkith Cesiant <1>(<3>E<1>)<10>Anti-surface defensive cell. +c<0><4>Place Nydus Exit <1>(<3>C<1>)<10>Allows transport of units between linked Nydus Cesiants. +n<1><4>Warp in Nexus <1>(<3>N<1>)<10>Town center and worker production. +q<1><4>Warp in Aquifer <1>(<3>Q<1>)<10>Vespene processor. +e<1><4>Warp in Pylon <1>(<3>E<1>)<10>Power font. +g<1><4>Warp in Gateway <1>(<3>G<1>)<10>Core disciple production. +f<1><4>Warp in Embassy <1>(<3>F<1>)<10>Utilitarian production. +x<1><4>Warp in Warden <1>(<3>X<1>)<10>All-purpose tower. +<164><3><4>Adaptive Amitosis <1>(passive)<10>Allies within 6 range of the Kazitrith Cesiant gain two secondary attacks that deals half damage to targets within 3 range of their primary target. +r<1><4>Build Sieve <1>(<3>R<1>)<10>Vespene processor. +Tethzor Aspect requires:<10> Tethvith Swamp +r<1><4>Warp in Lattice <1>(<3>R<1>)<10>Light robotic production. +Cikralisk requires:<10> Mutath Spire +a<1><4>Warp in Ancestral Archives <1>(<3>A<1>)<10>Core disciple production. +s<1><4>Warp in Stargate <1>(<3>S<1>)<10>Core stellar production. +2<0><4>Select Faction 2 <1>(<3>2<1>)<10><2>Hecaton Coalition, Order of the Sacred Saber, Imicren Tendril +d<1><4>Warp in Synthetic Synod <1>(<3>D<1>)<10>Absorbing robotic production. +3<0><4>Select Faction 3 <1>(<3>3<1>)<10><2>Futurist Federation, Khalendric Knights, Orkoti Tendril +4<0><4>Select Faction 4 <1>(<3>4<1>)<10><2>Golden Republic, Starbound, Goszirith Tendril +c<1><4>Build Ministry <1>(<3>C<1>)<10>Town center and worker production. +r<1><4>Build Reservoir <1>(<3>R<1>)<10>Vespene processor. +b<1><4>Build Stockade <1>(<3>B<1>)<10>Infantry production. +e<1><4>Build Atlas <1>(<3>E<1>)<10>Tier 2 tech center. +t<1><4>Build Watchdog <1>(<3>T<1>)<10>Anti-air tower. +5<0><4>Select Faction 5 <1>(<3>5<1>)<10><2>Nydus Trinity, Astral Omen, Zorthos Tendril +e<1><4>Build Anchor <1>(<3>E<1>)<10>Infantry garrison. +f<1><4>Build Fulcrum <1>(<3>F<1>)<10>Vehicle production. +a<1><4>Build Starport <1>(<3>A<1>)<10>Spacecraft production. +s<1><4>Build Daedala <1>(<3>S<1>)<10>Tier 3 tech center. +6<0><4>Select Faction 6 <1>(<3>6<1>)<10><2>Upstart Terran +z<1><4>Morph into Sovrokors <1>(<3>Z<1>)<10>Uncountable melee strain. +x<1><4>Build Anvil <1>(<3>X<1>)<10>Missile storage. +c<1><4>Build Commandment <1>(<3>C<1>)<10>Enables Azazels and Seraphs. +v<1><4>Build Covenant <1>(<3>V<1>)<10>Enables Eidolons and Savants. +r<1><4>Install Arch <1>(<3>R<1>)<10>Vespene processor. +c<1><4>Build Palladium <1>(<3>C<1>)<10>Enables Phalanxes and Madrigals. +m<0><4>Move <1>(<3>M<1>) +s<0><4>Stop <1>(<3>S<1>) +a<0><4>Attack <1>(<3>A<1>) +q<0><4>Patrol <1>(<3>Q<1>) +h<0><4>Hold Position <1>(<3>H<1>) +w<0><4>Way Points (<3>W<1>) +z<0><4>Land <1>(<3>Z<1>) +z<0><4>Liftoff <1>(<3>Z<1>)<10>After 0.5 seconds, the structure lifts off, becoming airborne. +y<0><4>Set Rally Point <1>(<3>Y<1>) +r<0><4>Recharge Shields <1>(<3>R<1>)<10>Recharges the shields of the target unit or structure. +s<0><4>Select Larva <1>(<3>S<1>) +g<0><4>Gather <1>(<3>G<1>) +c<0><4>Return Cargo <1>(<3>C<1>) +r<0><4>Repair <1>(<3>R<1>)<10>Heals the target for a portion of its resource costs. +b<0><4>Basic Construction <1>(<3>B<1>) +v<0><4>Advanced Construction <1>(<3>V<1>) +b<0><4>Basic Mutation <1>(<3>B<1>) +v<0><4>Advanced Mutation <1>(<3>V<1>) +v<0><4>Advanced Morph <1>(<3>V<1>) +e<0><4>Load <1>(<3>E<1>) +d<0><4>Unload All <1>(<3>D<1>) +x<0><4>Missile Strike <1>(<3>X<1>) +p<0><4>Place COP <1>(<3>P<1>) +<27><0><4>Cancel <1>(<3>ESC<1>) +<27><0><4>Cancel <1>(<3>ESC<1>) +<27><0><4>Cancel Construction <1>(<3>ESC<1>) +<27><0><4>Cancel Unit Training <1>(<3>ESC<1>) +e<1><4>Mutate into Eskigant <1>(<3>E<1>)<10>Vespene processor. +q<1><4>Warp in Cedent <1>(<3>Q<1>)<10>Vespene processor. +<27><0><4>Cancel Last <1>(<3>ESC<1>) +<27><0><4>Cancel Addon <1>(<3>ESC<1>) +<27><0><4>Cancel Morph <1>(<3>ESC<1>) +<27><0><4>Cancel Mutation <1>(<3>ESC<1>) +c<1><4>Install Landlock <1>(<3>C<1>)<10>Town center. +<27><0><4>Cancel Mutation <1>(<3>ESC<1>) +<27><0><4>Cancel Missile Strike <1>(<3>ESC<1>) +<27><0><4>Halt Construction <1>(<3>ESC<1>) +d<1><4>Build Qilin <1>(<3>D<1>)<10>Supportive transport craft. +c<1><4>Build Claymore <1>(<3>C<1>)<10>Assault hovertank. +<164><3><4>Nanite Chambers <1>(passive)<10>Allies loaded into the Qilin are healed for 4 HP per second. +e<1><4>Warp in Adherent <1>(<3>E<1>)<10>Stolid skirmisher. +b<1><4>Install Radix <1>(<3>B<1>)<10>Biotic production. +r<1><4>Train Siren <1>(<3>R<1>)<10>Eclectic specialist. +a<1><4>Morph into Rilirokors <1>(<3>A<1>)<10>Fodder strain. +<164><3><4>Worship <1>(passive)<10>When a hostile combatant takes fatal damage within 6 range of a Demiurge, they undergo a Crisis of Faith, and will serve the Demiurge after 5 seconds.<10>The combatants expire normally if no Demiurges are within 6 range during the Crisis. +g<1><4>Build Arhaun <1>(<3>G<1>)<10>Strike walker. +g<1><4>Build Southpaw <1>(<3>G<1>)<10>Disruptive vehicle. +q<1><4>Morph into Quazrokors <1>(<3>Q<1>)<10>Harrying strain. +a<1><4>Build Wendigo <1>(<3>A<1>)<10>Frontline assault craft. +Starport requires:<10> Atlas +z<1><4>Mutate into Spirtith Cesiant <1>(<3>Z<1>)<10>Advanced anti-air defensive cell. +Daedala requires:<10> Atlas +<164><3><4>Proximity Cloaking <1>(passive)<10>This combatant is persistently cloaked. Treading within 3 range of a hostile breaks cloak for 2 seconds. +<164><3><4>Steady Shroud <1>(passive)<10>This agent is persistently cloaked. Attacking or using an ability breaks cloak for 2 seconds. +Quazrokors Require:<10> Quazeth Pool +f<1><4>Install Function <1>(<3>F<1>)<10>Mechanized production. +Captaincy requires:<10> Atlas +c<1><4>Build Tencendur <1>(<3>C<1>)<10>Heavy garrison. +Tencendur requires:<10> Atlas +f<1><4>Build Facade <1>(<3>F<1>)<10>Cloaking outpost. +r<1><4>Warp in Herald <1>(<3>R<1>)<10>Frontline instigator. +s<1><4>Install Cosmoscope <1>(<3>S<1>)<10>Skyborne production. +e<1><4>Install Cipher <1>(<3>E<1>)<10>Combat control relay. +g<1><4>Install Gauntlet <1>(<3>G<1>)<10>Assault relay. +c<1><4>Install Chantry <1>(<3>C<1>)<10>Sustaining shrine. +b<1><4>Meld Object <1>(<3>B<1>)<10>Imposing crawler. +c<1><4>Meld Scepter <1>(<3>C<1>)<10>Commanding armor. +t<1><4>Meld Intendent <1>(<3>T<1>)<10>Assault crawler. +z<1><4>Mutate into Skortrith Cesiant <1>(<3>Z<1>)<10>Advanced anti-surface cell. +Scepter requires:<10> Cipher +<164><3><4>Gravity Well <1>(passive)<10>Anthelion attacks radiate out from their center of mass. Hostiles damaged by the Anthelion are pulled toward it. +<164><3><4>Counter-Proposal <1>(passive)<10>Once per second, the Anticthon reclaims the closest corpse within 3 range. +<164><3><4>Endless Eclipse <1>(passive)<10>Anthelions channel for 5 seconds before instantly manifesting at their destination, which must be revealed before movement can begin. +<164><3><4>Observance <1>(passive)<10>While any Seraph remains within 6 range of hostiles, their armor is reduced by 1 every 5 seconds.<10><10>Armor returns after 5 seconds of not being observed. +Zethrokors Require:<10> Quazeth Pool +Hydralisk requires:<10> Hydrith Den +Ultrok Aspect requires:<10> Ulgorg Cavern +Mutalisk requires:<10> Mutath Spire +Geszithalor requires:<10> Geszkath Grotto +Matraleth requires:<10> Matravil Nest +Isthrath Aspect requires:<10> Almakis Antre +<164><3><4>Fighting Flight <1>(passive)<10>The Tarasque's secondary weapon can fire at up to 5 nearby targets. +Caphrol Aspect requires:<10> Irol Iris +Gathtel Aspect requires:<10> Othstol Oviform +Nydal Aspect requires:<10> Irol Iris +a<1><4>Morph into Cikralisk <1>(<3>A<1>)<10>Crowd-controlling strain. +Vilgorokor requires:<10> Matravil Nest +g<1><4>Morph into Vilgorokor <1>(<3>G<1>)<10>Frontline reinforcement strain. +Matravil Nest requires:<10> Irol Iris +z<1><4>Mutate into Zozarc Gorge <1>(<3>Z<1>)<10>Control node. Enables Zobriolisks and Zarcavrokors. +c<1><4>Assemble Autocort <1>(<3>C<1>)<10>Constructor. +r<1><4>Train Cannibal <1>(<3>R<1>)<10>Raiding infantry. +Skortra Aspect requires:<10> Irol Iris +Intendent requires:<10> Gauntlet +<164><3><4>Tendril Amitosis <1>(passive)<10>The Skortrith Cesiant can strike up to 2 additional targets within 2 range of their primary victim for half damage. +Solarion Attack requires:<10> Stewards +<164><3><4>Shattering Winds <1>(passive)<10>Wendigo attacks debuff targets for 2 seconds, rending 1 armor per stack. +Missile Strike requires:<10> Armed Anvil +Facade requires:<10> Atlas +e<1><4>Meld Helion <1>(<3>E<1>)<10>Air superiority craft. +Kills: +Evolving +Upgrading +Upgrading +Evolving +Researching +Developing +Morphing +Building +Opening Warp Gate +Morphing +Building +Building +Damage: +Armor: +Shields: +Mining Delay: +Production<32> +Unit Ptr: +Current Order: +Next Order: +% Complete +Order: +Mutating +Adding On +Opening Warp Rift +Ascending Blaster +Stewards +Shells +Nukes +Mines +Next Level: +Minerals: +Vespene per return: +Depleted +Mutating +Under Construction +Opening Warp Rift +Cancel Morph (%s) +Cancel Construction (%s) +Cancel Warp (%s) +Cancel Upgrade +Cancel Research +Cancel Morph +Unload Unit (%s) +Click: Select Unit<10>Shift+Click: Deselect Unit<10>Ctrl+Click: Select Unit Type +Show Terrain (<3>Tab<1>) +Hide Terrain (<3>Tab<1>) +Diplomacy +Messaging +Game Menu (<3>F10<1>) +<164><3><4>Sermon Salve <1>(passive)<10>While allied combatants and structures remain peaceful within 8 range of the Chantry, they regenerate 5 HP per second. +<164><3><4>Discordant Chorus <1>(passive)<10>Enemies struck by this combatant have their armor reduced by 1 for 2 seconds. This penalty does not stack. +<164><3><4>Astral Splendor <1>(passive)<10>Once every 2 seconds, this core restores 10 shields to allies within 8 range. Allies can only be restored once every 2 seconds. +x<1><4>Build Cestus <1>(<3>X<1>)<10>Warding outpost. +b<1><4>Warp in Bulwark <1>(<3>B<1>)<10>Protective core. +v<1><4>Morph into Yblyleth <1>(<3>V<1>)<10>Expanding strain. +<164><3><4>Cagra Autophagy <1>(passive)<10>Every 2 seconds while burrowed, Yblyleths consume the closest corpse within 5 range, spawning 2 Rilirokors. +<164><3><4>Cagragenesis <1>(passive)<10>While burrowed, this strain generates cagra in an 8-tile radius. +<164><3><4>Corrosive Touch <1>(passive)<10>This strain's air attacks debuff enemies, increasing attack cooldown by 25% and reducing armor by 1 for 3 seconds. +<164><3><4>Sermon Salve <1>(passive)<10>Every second strike, the Cardinal's weapon releases a restorative pulse, healing all allies within 3 range for damage dealt. +s<1><4>Meld Emissary <1>(<3>S<1>)<10>Swift transport. +<164><3><4>Reintegration <1>(passive)<10>While wounded, stepping over a corpse consumes it, healing for 50HP over 10 seconds. +Disabled +<6>Disabled +<6>Disabled +<6>Unpowered +<3>Detector +<4>Hallucination +Units +Units +Resources +Kills +Score +Units to go +Units to go +Resources to go +Kills to go +Score to go +b<1><4>Meld Inkblot <1>(<3>B<1>)<10>Subsuming scout craft. +Inkblot Requires:<10> Cipher +t<1><4>Meld Tirade <1>(<3>T<1>)<10>Assault craft. +Tirade Requires:<10> Gauntlet +Sovrokors Require:<10> Quazeth Pool +<164><3><4>Lithe Organelle <1>(passive)<10>Projectiles from this strain's anti-ground weapon bounce to up to two nearby targets, dealing 33% reduced damage with each bounce. +Cannot add-on there. Liftoff is disabled. +Not enough minerals. +Not enough Vespene gas. +You can't build next to minerals or vespene. +d<1><4>Profanate Demiurge <1>(<3>D<1>)<10>Domineering automaton. +You can't build off the map. +You can't build near the edge of the map. +You can't build there. +You must explore there first. +You must currently be able to see the location. +Find a Vespene node to occupy. +You must mutate on the Cagra. +You must warp in a power field. +Couldn't reach the building site. +You can't land there. +Not enough energy. +Not enough energy. +Not enough energy. +<> +<164><3><4>Hotdrop <1>(passive)<10>Trojans have 150% unload speed compared to other transports. +<> +Must target heavily damaged structure. +Unit's waypoint list is full. +Order limit reached. +Must target allied and completed units. +Not enough life remaining. +Vespene Geyser depleted. +Invalid target. +Unable to target structure. +Must target units. +Unable to attack target. +Invalid target. +Must target mechanical units. +Invalid target. +Must target damaged units or complete structures. +Must target Terran units. +Invalid target. +Must target non-robotic ground units. +Must target ground. +Target out of range. +Target is too close. +Can only pick up your own transportable units. +Not enough room for unit. +Must gather from a Mineral Field or Vespene Geyser. +<> +<> +<> +Must gather gases from your own refinery. +Invalid target. +Invalid target. +Invalid target. +Units in stasis can't be targeted. +Must target non-hovering ground units. +Must target passable terrain. +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +Askosi Biotic Bolting +Askosi Mechani Bolting +Askosi Skyskip Bolting +Askosi Structure Bolting +Missile launch detected. +Tosgril Aspect requires:<10> Tosvrol Haunt +t<1><4>Morph into Tosgrilisk <1>(<3>T<1>)<10>Crowd-controlling assault strain. +Irol Aspect requires:<10> Irol Iris<10> No cargo +<164><3><4>Seismic Spines <1>(passive)<10>Lakizilisk projectiles return to them after reaching maximum range. +g<1><4>Morph into Ghitorokor <1>(<3>G<1>)<10>Aggressive frontline strain. +b<1><4>Mutate to Tethvith Swamp <1>(<3>B<1>)<10>Assault node. Enables Tethzorokors and Vithrilisks. +c<1><4>Warp in Crucible <1>(<3>C<1>)<10>Advanced power font. Accelerates structures. +g<1><4>Mutate to Gorgrok Apiary <1>(<3>G<1>)<10>Enables Gorgrokors. +Ghitor Aspect requires:<10> Tosvrol Haunt +Bactal Aspect requires:<10> Skibact Scab +Agrileth requires:<10> Quazeth Pool +<164><3><4>Captivating Claws <1>(passive)<10>This strain's attacks apply Ensnare, slowing the movement and attack speeds of their victims by 33% for 2 seconds. +Irol Aspect requires:<10> Irol Iris +b<1><4>Morph into Zobriolisk <1>(<3>B<1>)<10>Versatile assault strain. +<> +b<1><4>Morph into Bactalisk <1>(<3>B<1>)<10>Crowd-controlling assault strain. +Zobrion Aspect requires:<10> Zozarc Gorge +g<1><4>Warp in Magister <1>(<3>G<1>)<10>Battle platform. +z<0><4>Advanced Warp <1>(<3>Z<1>) +b<0><4>Basic Warp <1>(<3>B<1>) +g<1><4>Warp in Gladius <1>(<3>G<1>)<10>Crowd-controlling corvette. +<> +<> +g<1><4>Build Monogram <1>(<3>G<1>)<10>Reinforcing hovercraft. +g<1><4>Build Gorgon <1>(<3>G<1>)<10>All-purpose corvette. +a<1><4>Build Salamander <1>(<3>A<1>)<10>Area-denial cruiser. +<164><3><4>Signify: Cowardice <1>(passive)<10>Enemies struck by this combatant have their movement speed reduced by 50% for 3 seconds. +<> +<164><3><4>Signify: Malice <1>(passive)<10>Enemies struck by 5 consecutive attacks have their attack speed reduced by 33%. This effect expires at the same time as Signify: Cowardice. +e<1><4>Warp in Legionnaire <1>(<3>E<1>)<10>Melee skirmisher. +r<1><4>Warp in Hierophant <1>(<3>R<1>)<10>Disruptive strider. +Spirtil Aspect requires:<10> Irol Iris +<164><3><4>Module: Power Siphon <1>(passive)<10>Savants with this module active are immobilized, and steal 10% attack speed from their victims for 2 seconds. +s<1><4>Build Phobos <1>(<3>S<1>)<10>Capital ship. +d<3><4>Energy Decay <1>(<3>D<1>)<10>Sabotages all enemy specialists within the target area, damaging them for every point of energy they spend. Lasts 10 seconds. +s<1><4>Build Penumbra <1>(<3>S<1>)<10>Ultimate walker. +<164><3><4>Incendiary Payload <1>(passive)<10>Salamander projectiles set fire to the ground, leaving behind a flame field for 5 seconds which applies Apollyoid Adhesive to ground targets. +<164><3><4>Temporal Batteries <1>(passive)<10>Structures powered by 2 or more Pylons have 140% shield regeneration +g<1><4>Warp in Argosy <1>(<3>G<1>)<10>Heavy stellar production. +<164><3><4>Providence <1>(passive)<10>Generates a protective shield while energy holds, intercepting all weapon hits within 2 range. +b<1><4>Build Captaincy <1>(<3>B<1>)<10>Advanced infantry production. +g<1><4>Assemble Golem <1>(<3>G<1>)<10>Sturdy frontline drone. +t<1><4>Build Mantle <1>(<3>T<1>)<10>Walker outpost. +<164><3><4>Apollyoid Adhesive <1>(passive)<10>Flame-based attacks set victims alight, rending 1 armor per stack, and dealing 3 damage every second.<10><10>Apollyoid Adhesive burns itself out after 2 seconds of not being stacked. +<> +<> +Mining Efficiency +<> +<> +d<1><4>Assemble Idol <1>(<3>D<1>)<10>Ranged assault drone. +Mutath Spire requires:<10> Irol Iris +<164><3><4>Stellar Enforcement <1>(passive)<10>Magisters mark their victims for 3 seconds, granting +2 weapon range to any attacks while the debuff holds. +c<3><4>The Grief of All Gods <1>(<3>C<1>)<10>Channels the Star Sovereign's glassing cannon for 5 seconds before unleashing a plasma beam, dealing 2500 damage over 15 seconds to all nearby ground targets, and consuming all corpses in the area. +<164><3><4>Shroud Piercer <1>(passive)<10>The Penumbra's cannon launches a projectile that deals half damage to any combatant it passes through en route to its target. +<164><3><4>Argent Capacitors <1>(passive)<10>Each attack adds 5 energy to the Phobos. Once maximum energy is reached, the ship's attack speed is doubled, and each shot consumes 10 energy. +Alkag Iteth requires:<10> Irol Iris +e<3><4>Place Piety Ward <1>(<3>E<1>)<10>Places a defensive ward that shields allies from attacks. +a<1><4>Assemble Renesair <1>(<3>A<1>)<10>Reconstructing interceptor. +<164><3><4>Lucilleri Pheremones <1>(passive)<10>Allies within 5 range of the Sasyrvoleth have 100% base lifesteal. +<164><3><4>Astral Tether <1>(passive)<10>When the Matrix attacks, it radiates power from both itself and its target, restoring 2 shields to any allies of the Matrix within 3 range. +<164><3><4>Triumphant Rise <1>(passive)<10>Victims of this warrior's attacks become energized for 1.5 seconds. Attacks on energized enemies bleed 2 shield points to all allies of the warrior within 3 range. +d<1><4>Train Madcap <1>(<3>D<1>)<10>Mechanized infantry. +<164><3><4>Cabeirus Feeders <1>(passive)<10>While stationary, the Madcap gains a 15% attack speed bonus per attack, capping at a 150% bonus. This bonus falls off by 20% for every second the Madcap spends moving. +<164><3><4>Miasma <1>(passive)<10>This strain's attacks infect its victims with Plague for 1.5 seconds. Plagued victims release Miasma fields on death, spreading the infection to all nearby targets. +g<3><4>Land Grab <1>(<3>G<1>)<10>Orders the Wyvern to fire its weapon at the target site. +r<1><4>Warp in Strident Stratum <1>(<3>R<1>)<10>Utilitarian production. +Mantle requires:<10> Atlas +<> +r<1><4>Build Wyvern <1>(<3>R<1>)<10>Area-denial corvette. +v<1><4>Train Savant <1>(<3>V<1>)<10>Versatile infantry. +z<1><4>Build Apothecary <1>(<3>Z<1>)<10>Alchemical outpost. +Apothecary requires:<10> Daedala +f<1><4>Build Future Station <1>(<3>F<1>)<10>Enables Claymores and Penumbras. +r<1><4>Build Ramesses <1>(<3>R<1>)<10>Duelist walker. +t<1><4>Build Triton <1>(<3>T<1>)<10>Shocking walker. +d<1><4>Build Durendal <1>(<3>D<1>)<10>Frontline walker. +c<1><4>Build Cataphract <1>(<3>C<1>)<10>Assault platform. +d<1><4>Train Dilettante <1>(<3>D<1>)<10>Precarious duelist. +v<1><4>Train Sevenstep <1>(<3>V<1>)<10>Area-denial infantry. +g<1><4>Train Hypnagogue <1>(<3>G<1>)<10>Relocating specialist. +a<1><4>Build Nanite Assembly <1>(<3>A<1>)<10>Advanced vehicle and spacecraft production. +Nanite Assembly requires:<10> Daedala +<> +<164><3><4>Law of Attraction <1>(passive)<10>Enemies struck by this combatant are pulled 2 range closer. +a<1><4>Warp in Aurora <1>(<3>A<1>)<10>Nimble fighter. +c<1><4>Build Centaur <1>(<3>C<1>)<10>All-purpose destroyer. +t<1><4>Build Magnetar <1>(<3>T<1>)<10>Undeniable cruiser. +<164><3><4>Sunchaser Salvo <1>(passive)<10>While the Centaur is within 4 range of its target, it will also fire its secondary weapon. +<164><3><4>Mitosis <1>(passive)<10>Upon taking damage, the Zoryusthaleth incubates, spawning one Rilirokor after a two-second delay. +w<1><4>Build Cuirass <1>(<3>W<1>)<10>Durable frontline walker. +<164><3><4>Stonewall Protocol <1>(passive)<10>Upon taking fatal damage, the Cuirass deactivates, regains 50 HP, and begins self-repairing for 15 HP per second until fully-healed. If the Cuirass is dealt fatal damage again before it can fully repair, it is destroyed. +<> +r<5><4>Morph into Iroleth <1>(<3>R<1>)<10>Transport strain. Provides detection. +<> +t<1><4>Build Lodestone <1>(<3>T<1>)<10>Defensive emplacement. Detects hidden enemies. +t<1><4>Build Parthenon <1>(<3>T<1>)<10>Supportive production. +d<1><4>Build Madrigal <1>(<3>D<1>)<10>Versatile hovertank. +r<1><4>Build Scrapyard <1>(<3>R<1>)<10>Enables Blackjacks and Matadors. +Cooling down... +c<1><4>Morph into Sasyrvoleth <1>(<3>C<1>)<10>Sustaining support strain. +Sasyrvoleth requires:<10> Axtoth Axis +<164><3><4>Concurring Chorus <1>(passive)<10>For 1 second after attacking, Manifolds provide +1 armor penetration to allies within 3 range. +e<1><4>Warp in Cicatus <1>(<3>E<1>)<10>Destitute skirmisher. +<164><3><4>Servile Vigor <1>(passive)<10>Cohorts revive once after death, gaining a 10-second timed life. They can consume a nearby corpse to become permanent again. +a<1><4>Warp in Extatus <1>(<3>A<1>)<10>Stolid fighter. +<> +<> +s<1><4>Assemble Oscilliant <1>(<3>S<1>)<10>Assault drone. +a<1><4>Assemble Unar <1>(<3>A<1>)<10>Sniper drone. +r<1><4>Warp in Ashguard <1>(<3>R<1>)<10>Conquering strider. +<164><3><4>Astral Charge <1>(passive)<10>When this combatant attacks, it ascends allies within 2 range. +<164><3><4>Detection <1>(passive)<10>Hidden targets within vision range will be revealed. +<164><3><4>Skyward Stare <1>(passive)<10>While burrowed, this strain can attack airborne hostiles. +d<1><4>Train Blackguard <1>(<3>D<1>)<10>Mechanized sniper. +y<0><4>Build Addon <1>(<3>Y<1>) +<> +Askosi Structure Bolting +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +w<1><4>Warp in Xenolith <1>(<3>W<1>)<10>Recalling relay. +Advanced Warps require:<10> Basic Warp +Empowered Warps require:<10> Advanced Warp +Supreme Warps require:<10> Empowered Warp +<164><3><4>Writhing Web <1>(passive)<10>For every allied Sovroleth within 8 range, lose 5% movement speed and gain 5% weapon range. +<164><3><4>Babylon Plating <1>(passive)<10>Phalanxes gain +4 armor while in Tank mode. +Ogun requires:<10> Quarry +<164><3><4>Blood Tithe <1>(passive)<10>When an Apostle attacks a target, allies within 3 range of the Apostle are healed for 5 HP. +e<1><4>Build Cyclops <1>(<3>E<1>)<10>Crowd-controlling walker. +<164><3><4>Nanite Drones <1>(passive)<10>While in Smithy Mode, Oguns repair all structures within 4 range. +Nanite Drones:<10> Must be in Smithy Mode +<> +<> +<164><3><4>Corpse Cocoon <1>(passive)<10>Corpses beneath this strain are consumed after 2 seconds, spawning an additional cell. +<> +<> +<> +<> +<164><3><4>Villanous Fervor <1>(passive)<10>As long as the Mind Tyrant remains within 10 range, damage dealt to the malefactor is first inflicted upon the Mind Tyrant's energy, they gain 33% increased movement speed, and hostiles they strike are tyrannized on death. +Engram requires:<10> Advanced Warp +g<1><4>Build Ogun <1>(<3>G<1>)<10>Sturdy constructor. +Talos requires:<10> Quarry +t<1><4>Build Talos <1>(<3>T<1>)<10>Airborne worker. +<> +<164><3><4>Deathless Procession <1>(passive)<10>This combatant gains 25% movement speed while its shields hold. +a<3><4>Nanorepair <1>(<3>A<1>)<10>Heals a target ground ally for 9 HP per second. While being healed by at least one Cleric, the ally gains +1 armor. +x<1><4>Warp in Engram <1>(<3>X<1>)<10>Heavy tower. +a<3><4>Heal <1>(<3>A<1>)<10>Heals a target ground ally for 9 HP per second. +<27><0><4>Rescind Control <1>(<3>ESC<1>) +<164><3><4>Fusion Sealers <1>(passive)<10>Taloses have 33% increased repair speeds compared to Masons. +<> +Must target pathable terrain. +t<1><4>Warp in Pennant <1>(<3>T<1>)<10>Increases nearby resource regeneration rate. +Crucible requires:<10> Basic Warp +Aquifer requires:<10> Basic Warp +Matrix requires:<10> Basic Warp +<164><3><4>Indomitable Annex <1>(passive)<10>Each consecutive attack on the same target grants this combatant +5 damage. This bonus can stack up to 5 times, after which point the target will be annexed on death. +<164><3><4>Vengeful March <1>(passive)<10>After attacking, this combatant gains 33% increased movement speed that decays over 1 second. +<164><3><4>Particle Annihilation <1>(passive)<10>Positron projectiles set off secondary explosions once for every 1 range traveled. +r<1><4>Profanate Pariah <1>(<3>R<1>)<10>Misbegotten frontline infantry. +f<3><4>Skyfall <1>(<3>F<1>)<10>Grounds the Paladin and launches a salvo of 20 missiles at the target site. +<> +t<1><4>Profanate Mind Tyrant <1>(<3>T<1>)<10>Dominating specialist. +d<3><4>Activate Disruption Field <1>(<3>D<1>)<10>Creates an energy field beneath the Lanifect, disabling the weapons of all grounded combatants within it. +a<1><4>Build Armistice <1>(<3>A<1>)<10>Mobile garrison. +c<3><4>Maledictus <1>(<3>C<1>)<10>Channels for five seconds before declaring an allied combatant as its malefactor.<10>So long as their Tyrant is nearby, malefactors gain increased durability and speed, and victims of their attacks are tyrannized on death.<10>Mind Tyrants can only have one malefactor at a time. +e<3><4>Maelstrom <1>(<3>E<1>)<10>Fires an energy bolt that slows the movement speed of all targets within 2 range of the target area by 50% for 5 seconds. Hostile corpses within the area of effect are ascended. +<164><3><4>Idolatry <1>(passive)<10>The Sycophant recharges shields for the closest ally within 3 range. On death, the Sycophant heals allies within 3 range for 50 shields. +d<3><4>Deactivate Disruption Field <1>(<3>D<1>)<10>Returns the Lanifect's weapons systems to normal. +s<1><4>Warp in Sycophant <1>(<3>S<1>)<10>Innervating skirmisher. +e<1><4>Build Pythean <1>(<3>E<1>)<10>Heavy transport craft. +<> +<> +Xenolith requires:<10> Empowered Warp +e<3><4>Prying Eyes <1>(<3>E<1>)<10>Projects a cloaking field that lasts up to 10 seconds. Allies within 8 range benefit from Proximity Cloaking. +Cloaking... +<> +<164><3><4>Ablative Spores <1>(passive)<10>Kalkalisk projectiles damage all targets they pass through, dealing 1 less damage for every additional target. +<> +a<1><4>Warp in Ardent Authority <1>(<3>A<1>)<10>Core robotic production. +b<1><4>Assemble Barghest <1>(<3>B<1>)<10>Fiendish experiment. +e<1><4>Profanate Anthelion <1>(<3>E<1>)<10>Catastrophic starbreach. +<> +<> +q<1><4>Build Quarry <1>(<3>Q<1>)<10>Auxiliary worker production. +<> +v<5><4>Morph into Vithrilisk <1>(<3>V<1>)<10>Heavy fighter strain. +c<1><4>Train Cleric <1>(<3>C<1>)<10>Support infantry. +<> +v<1><4>Build Valkyrie <1>(<3>V<1>)<10>Air superiority corvette. +r<1><4>Warp in Lanifect <1>(<3>R<1>)<10>Air superiority fighter. +a<1><4>Profanate Cabalist <1>(<3>A<1>)<10>Cloaked melee assassin. +<> +<> +<> +Vithril Aspect requires:<10> Tethvith Swamp +e<1><4>Warp in Unitary Union <1>(<3>E<1>)<10>Robotic production. +<7>Blind +<7>Corrosive Spores +per rocket +Unitary Union requires:<10> Advanced Warp +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +Must target injured non-mechanical ground units +Invalid target. +Must target enemy units +Must target non-mechanical units. +Unable to target structure. +Must target units with energy. +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +z<1><4>Morph to Izirokor <1>(<3>Z<1>)<10>Close-combat strain. +<> +Izirokor requires:<10> Hydrith Den +t<1><4>Morph into Keskathalor <1>(<3>T<1>)<10>Artillery strain. +Keskathalor requires:<10> Geszkath Grotto +e<1><4>Morph into Alkajelisk <1>(<3>E<1>)<10>Capital strain. +Alkaj Aspect requires:<10> Kethraj Keep +<164><3><4>Imperious March <1>(passive)<10>This combatant's shields have +1 armor. +z<3><4>Switch to Entropy Gauntlets <1>(<3>Z<1>)<10>Activates the Savant's crowd-control module. +x<0><4>Supreme Warp <1>(<3>X<1>) +t<1><4>Morph into Kethrostalor <1>(<3>T<1>)<10>Unmatched artillery strain. +Kethrostalor requires:<10> Kethraj Keep +z<3><4>Switch to Power Siphon <1>(<3>Z<1>)<10>Activates the Savant's sustained fire module. +v<1><4>Morph into Konvilisk <1>(<3>V<1>)<10>Reinforcing duelist strain. +<164><3><4>Reactive Payload <1>(passive)<10>For every 4 range travelled, this combatant's missiles split into an additional projectiles, homing in on the closest combatants within 3 range of the original target. +x<1><4>Mutate to Othstol Oviform <1>(<3>X<1>)<10>Tier 3 tech node. +e<1><4>Mutate into Kethraj Keep <1>(<3>E<1>)<10>Artillery assault node. +Entropy Gauntlets Require:<10> Module switch +Power Siphon requires:<10> Module switch +a<1><4>Morph to Kagralisk <1>(<3>A<1>)<10>Bursting ranged strain. +<> +<> +<164><3><4>Fertile Spines <1>(passive)<10>Konvilisk attacks begin an incubation period of 2 seconds, after which afflicted units birth 2 broodspawn.<10><10>This status cannot be applied if the target is already incubating. +x<1><4>Mutate into Irol Iris <1>(<3>X<1>)<10>Tier 2 tech node. +Othstoleth requires:<10> Othstol Oviform +r<1><4>Morph into Akistrokor <1>(<3>R<1>)<10>Controlling specialist strain. +s<1><4>Assemble Servitor <1>(<3>S<1>)<10>Assault drone. +t<1><4>Assemble Positron <1>(<3>T<1>)<10>Crowd-control drone. +a<1><4>Profanate Analogue <1>(<3>A<1>)<10>Durable disruption drone. +d<0><4>Supreme Structure <1>(<3>D<1>) +r<1><4>Morph into Othstoleth <1>(<3>R<1>)<10>Massive transport strain. +Kagralisk requires:<10> Alkag Iteth +d<1><4>Warp in Grand Library <1>(<3>D<1>)<10>Core disciple production. +<> +<164><3><4>Ablative Whiplash <1>(passive)<10>Skithrokor projectiles damage all targets they pass through, dealing 1 less damage for every additional target. Once they reach their primary target, the projectiles return to the Skithrokor. +Reservoir requires:<10> Atlas +s<1><4>Build Treasury <1>(<3>S<1>)<10>Frontier town center. +<164><3><4>Tranquility Array <1>(passive)<10>Upon taking shield damage, the Golem radiates energy, restoring 2 shields for allies within 3 range. +<> +<164><3><4>Interception <1>(passive)<10>This combatant's projectiles can be intercepted by hostiles. +t<3><4>Nanite Field <1>(passive)<10>Every second, Shamans release a pulse that heals allies within 3 range for 4 HP. +<164><3><4>Virile Coating <1>(passive)<10>Victims of the Tetcaerokor's attacks begin incubating, birthing 1 Rililisk if they survive for 2 seconds.<10><10>This status cannot be applied if the target is already incubating. +r<1><4>Warp in Automaton Register <1>(<3>R<1>)<10>Heavy robotic production. +z<1><4>Mutate into Alkag Iteth <1>(<3>Z<1>)<10>Assault node. Enables Kagralisks, Evigrilisks, and Almaksalisks. +x<1><4>Build Sentinel <1>(<3>X<1>)<10>All-purpose tower. +c<1><4>Assemble Simulacrum <1>(<3>C<1>)<10>Robotic swarmer. +<164><3><4>Lazarus Agent <1>(passive)<10>When attacking a hostile, Apostles reclaim one corpse within 3 range of themselves. +Sentinel requires:<10> Atlas +<164><3><4>Kinetic Penetration <1>(passive)<10>This combatant's projectiles travel to maximum range, piercing targets with insufficient armor, and becoming intercepted by targets with more armor. Additional victims take half damage. +<> +<164><3><4>Pulsing Attack <1>(passive)<10>This combatant's attacks radiate out from their own position. +<> +<> +<164><3><4>Prolonged Arms <1>(passive)<10>This combatant's projectiles travel to maximum range, and can be intercepted by hostiles. +a<1><4>Mutate into Alkaj Chasm <1>(<3>A<1>)<10>Siege node. Enables Alkajelisks. +Geszkath Grotto requires:<10> Othstol Oviform +s<1><4>Mutate into Geszkath Grotto <1>(<3>S<1>)<10>Siege node. Enables Keskathalors, Geszithalors, and Alkajelisks. +Nathrokors Require:<10> Vornath Pond +r<1><4>Morph into Nathrokors <1>(<3>R<1>)<10>Swarming fighter strain. +<164><3><4>Module: Entropy Gauntlets <1>(passive)<10>Savants with this module active are mobile, and benefit from Kinetic Penetration. +f<1><4>Assemble Manifold <1>(<3>F<1>)<10>Robotic skirmisher. +<> +r<1><4>Assemble Architect <1>(<3>R<1>)<10>Lumbering artillery. +<> +a<1><4>Warp in Amaranth <1>(<3>A<1>)<10>Brawling skirmisher. +<164><3><4>Beginner's Luck <1>(passive)<10>Something about Dilettantes make them harder to hit. Every 2 seconds, they can dodge an attack. +Konvilisk requires:<10> Matravil Nest +<164><3><4>Mind Matter <1>(passive)<10>Combatants struck by Sevenstep attacks begin charging. After 1 second, the combatant discharges a piercing projectile that travels back towards the Sevenstep. +s<1><4>Warp in Atreus <1>(<3>S<1>)<10>Stalwart strider. +<164><3><4>Autovitality <1>(passive)<10>4 seconds after taking hull damage from an enemy, this combatant creates a clone of itself, inheriting shields, life, and statuses. +<164><3><4>Petrification <1>(passive)<10>Gorgon's attacks slow victims by 10% per stack. Stacks expire after 2 seconds. +<164><3><4>Adrenal Implant <1>(passive)<10>While Trinity Eidolons are cloaked, they move 33% faster and regenerate life twice as fast. +<164><3><4>Reclamation <1>(Cornerstone)<10>This combatant is a Reclaimer, capable of raising the dead. Reclaimed combatants move 25% slower, and restore half of all damage taken when out of combat. +<164><3><4>Parasitic Spores <1>(passive)<10>Targets killed by the Alkajelisk spawn a spore that seeks out nearby hostiles and explodes, dealing 15 damage to all hostiles within 1.5 range. +Babylon Plating requires:<10> Tank Mode +<164><3><4>Furia Salvo <1>(passive)<10>While deployed, the Madrigal fires a staggered salvo of missiles at all targets within weapon range. +Furia Salvo requires:<10> Furia Mode +<164><3><4>Dirge <1>(passive)<10>While in Imperioso Mode, Madrigal missiles home in on targets, dealing area damage. +Dirge requires:<10> Imperioso Mode +<164><3><4>Incineration <1>(passive)<10>While in Imperioso Mode, Madrigal missiles home in on targets, incinerating them over time. +Incineration requires:<10> Imperioso Mode +<164><3><4>Binding Plasma <1>(passive)<10>This combatant's projectiles linger on their victims for 0.5 seconds before bouncing to a nearby hostile.<10><10>This effect ceases if there are no bounce targets within 3 range, or if there are no sources within 8 range. +<164><3><4>Searing Acid <1>(passive)<10>The Geszithalor's attacks gain +10 damage for every 3 range traveled. +w<1><4>Build Wellbore <1>(<3>W<1>)<10>Advanced vespene processor. +<164><3><4>Intervention <1>(passive)<10>Whenever an enemy is within 5 range of the Star Sovereign, a salvo of missiles with Interception are fired from its front. +s<1><4>Warp in Star Sovereign <1>(<3>S<1>)<10>Ultimate capital ship. +Diadem requires:<10> Daedala +c<1><4>Build Diadem <1>(<3>C<1>)<10>Artillery tower. +Akis Aspect requires:<10> Almakis Antre +Eidolon requires:<10> Covenant +<> +x<0><4>Supreme Construction <1>(<3>X<1>) +t<1><4>Warp in Optecton <1>(<3>T<1>)<10>Artillery strider. +a<1><4>Warp in Patriarch <1>(<3>A<1>)<10>Heroic infantry. +g<3><4>Through the Looking Glass <1>(<3>G<1>)<10>Channels for 2 seconds to manifest a psychic portal that teleports all allied combatants within 6 range to the chosen destination. +<164><3><4>Khaydarin Charge <1>(passive)<10>Allied structures within 8 range of the Crucible gain 25% increased efficiency. +<164><3><4>Prismatic Surge <1>(passive)<10>All resource nodes within 5 range of any Pennant gain 200% resource regeneration rate. This bonus does not stack. +Wellbore requires:<10> Daedala +<164><3><4>Terminal Surge <1>(passive)<10>Whenever a Vassal dies, allies within 5 range gain 25% increased movement and attack speed for 2 seconds. +<> +z<3><4>Blissful Embrace <1>(<3>Z<1>)<10>Channels onto the target. If the target takes fatal damage during the channel, it is infested, regaining 33% of its maximum HP.<10><10>Infested combatants lose vision range and movement speed, while gaining HP regeneration and armor. Infested structures also spread cagra. +<27><0><4>Cancel Embrace <1>(<3>ESC<1>) +s<1><4>Morph into Skithrokor <1>(<3>S<1>)<10>Airborne duelist strain. +Skithrokor requires:<10> Skibact Scab +<> +<> +<> +<> +Blackjack requires:<10> Scrapyard +Matador requires:<10> Scrapyard +Cleric requires:<10> Vestry +Shaman requires:<10> Vestry +Siren requires:<10> Tinkerer's Tower +Striga requires:<10> Tinkerer's Tower +Penumbra requires:<10> Future Station +Claymore requires:<10> Future Station +Sundog requires:<10> Rotary +Phobos requires:<10> Hadron +Salamander requires:<10> Rotary +Hippogriff requires:<10> Hadron +<> +Attack:<10> Must be burrowed. +z<1><4>Mutate into Ezigrisant <1>(<3>Z<1>)<10>Advanced vespene cell. +Ezigrisant requires:<10> Othstol Oviform +Excisant requires:<10> Irol Iris +Mortothrokor requires:<10> Axtoth Axis +<164><3><4>Perpetual Wind <1>(passive)<10>The Axitrilisk is regenerating... +<164><3><4>Signify: Ascension <1>(passive)<10>If hostiles are killed while under the effect of Signify: Malice, their corpse is ascended after 1 second, granting 33% increased attack speed to allies of the signifier within the vision range of the fallen hostile. +<164><3><4>Transcendent Fall <1>(passive)<10>Allies that fall within 4 range of the Herald have their corpses avenged after 1 second. When the Herald falls, hostile corpses within 4 range are ascended. +<164><3><4>Nanite Field <1>(passive)<10>Every second, Shamans release a pulse that heals allies within 6 range for 4 HP. +<164><3><4>Adrenal Frenzy <1>(passive)<10>After landing an attack, this combatant gains 25% increased movement speed for 2 seconds. +<164><3><4>Bloody Rain <1>(passive)<10>Cargo loaded into the Kethrostalor is fired with its next attack. +d<1><4>Manifest Droleth <1>(<3>D<1>)<10>Worker strain. +a<1><4>Manifest Mason <1>(<3>A<1>)<10>Worker. +s<1><4>Manifest Scribe <1>(<3>S<1>)<10>Worker automata. +<> +<> +<164><3><4>Transcendent Tide <1>(passive)<10>Each time a Potentate attacks an enemy within 3 range, all allies within 3 range are ascended, granting 25% increased attack speed. +a<1><4>Warp in Potentate <1>(<3>A<1>)<10>Ascendent ranger +a<1><4>Train Hotshot <1>(<3>A<1>)<10>Close-combat infantry. +<164><3><4>Devouring Instinct <1>(passive)<10>If a victim takes fatal damage within 1 second of being attacked by this strain, it is infested. +<164><3><4>Hidebreaker <1>(passive)<10>This strain's attacks rend 1 armor for 2 seconds. +<164><3><4>Hidebreaker <1>(passive)<10>This strain's air attacks rend 1 armor for 2 seconds. +v<1><4>Warp in Vibrance <1>(<3>V<1>)<10>Durable area-denial drone. +z<1><4>Morph to Iziralisk <1>(<3>Z<1>)<10>Rending assault strain. +Iziralisk requires:<10> Hydrith Den +<164><3><4>Hidebreaker II <1>(passive)<10>This strain's attacks rend 2 armor for 2 seconds. +Savant requires:<10> Covenant +Phalanx requires:<10> Palladium +Madrigal requires:<10> Palladium +Tiangou requires:<10> Scrapyard +Tarasque requires:<10> Palladium +Seraph requires:<10> Commandment +Azazel requires:<10> Commandment +Luminary requires:<10> Commandment +Olympian requires:<10> Guildhall +Autocrat requires:<10> Guildhall +Anticthon requires:<10> Biotic Bastion +Aion requires:<10> Biotic Bastion +<> +<> +<> +Expansion Zerg Campaign Area Town +<> +<164><3><4>Phase Link <1>(passive)<10>Clarions passively link all shielded allies within 3 range. 75% of shield damage is distributed across all linked allies. +<164><3><4>Harmony Drive <1>(passive)<10>Clarions gain increased movement speed and acceleration based on shields. +Expansion Protoss Campaign Area Town +<> +c<1><4>Build Philoc <1>(<3>C<1>)<10>All-purpose fighter. +<164><3><4>Death Cycle <1>(passive)<10>Broodspawn have timed life when away from cagra and sires. On attack, they gain 1 second of timed life back. On death, Broodspawn apply Corrosive Touch to hostiles within 3 range for 1 second. +Expansion Terran Campaign Area Town +<> +<> +<164><3><4>Artillery Targeting <1>(passive)<10>This structure launches an artillery beacon at its target, and fires its primary attack at the beacon's position. +<> +<164><3><4>Titanic Stride <1>(passive)<10>This massive combatant collides only with structures and other immense units. +<164><3><4>Solitary Focus <1>(passive)<10>After completing its attack, this combatant deals +1 damage per shot and gains +1 range. This can stack up to 4 times, and 1 stack is lost for every tile moved without attacking. +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +Vorval Aspect requires:<10> Vornath Pond +View Players +u<0>Speed Up (<3>U<1>) +p<0>Play (<3>P<1>) +p<0>Pause (<3>P<1>) +d<0>Slow Down (<3>D<1>) +Replay Progress +Paused +Unlimited +b<1><4>Build Vestry <1>(<3>B<1>)<10>Enables Clerics and Shamans. +z<1><4>Morph into Zarcavrokor <1>(<3>Z<1>)<10>Crowd-controlling melee strain. +Zarcav Aspect requires:<10> Zozarc Gorge +c<1><4>Morph into Tetcaerokor <1>(<3>C<1>)<10>Reinforcing melee strain. +x<1><4>Morph into Kalkalisk <1>(<3>X<1>)<10>Assault flyer. +Tetcaer Aspect requires:<10> Almakis Antre +v<1><4>Morph into Vorvrokor <1>(<3>V<1>)<10>Harrying melee strain. +Kalkath Aspect requires:<10> Kalkiir Lake +Garvrok Aspect Requires:<10> Vornath Pond +v<1><4>Morph into Garvrokor <1>(<3>V<1>)<10>Anti-armor melee strain. +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<164><3><4>Silent Strides <1>(passive)<10>While cloaked, Cabalists teleport into melee range of attack targets within 3 range, and decloak on arrival. After decloaking, the Cabalist's first strike deals 150% damage. +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +Protoss Structure Plating +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<164><3><4>Caustic Fission <1>(passive)<10>For every 4 range traveled, this strain's projectiles will bounce to 1 additional target. +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<27><0><4>Cancel Warp <1>(<3>ESC<1>) +<> +<> +<> +<> +<> +<> +<> +<> +<> +<164><3><4>Fore Castle <1>(passive)<10>This combatant has +2 armor when facing incoming attacks. +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<164><3><4>Reverse Thrust <1>(passive)<10>While facing their target, Wyverns can attack while moving. +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<164><3><4>Last Orders <1>(passive)<10>Upon taking fatal damage, Auroras regain 30 HP and fly towards the nearest hostile. If they reach the target area, they deal 25 damage in a small area, and consume all corpses within 2 range of their impact site. +<> +<> +<164><3><4>Saber Siphon <1>(passive)<10>This strain gains life equal to damage dealt. +t<1><4>Train Apostle <1>(<3>T<1>)<10>Reanimation specialist. +<> +<164><3><4>Kaiser Rampage <1>(passive)<10>Ultrokors consume nearby corpses, gaining 200% movement and attack speeds for 2 seconds.<10><10>Corpses are not consumed if a Kaiser Rampage buff has more than 0.5 seconds remaining. +<> +<> +<> +<> +<> +<> +<> +<164><3><4>Martyrous Tempest <1>(passive)<10>On death, Archons release a Psionic Storm that lasts for 10 seconds. +x<3><4>Skryspawn <1>(<3>X<1>)<10>Channels to spawn 6 Skryrokors over 3 seconds. +Gestating... +<> +z<3><4>Lobotomy Grenade <1>(<3>Z<1>)<10>Fires a grenade that Lobotomizes all combatants within 2 range of the target area. +s<1><4>Build Shaman <1>(<3>S<1>)<10>Restorative vehicle. +z<3><4>Lobotomy Warhead <1>(<3>Z<1>)<10>Fires a missile that Lobotomizes its target for 3 seconds. Targets within 2 range of the victim are slowed by 33% for 3 seconds. +<164><3><4>Endomorphic Accelerant <1>(passive)<10>Iroleths double the regeneration rate of resources within 7 range. +<> +Ocsal Aspect requires:<10> Irol Iris +Click: Select Unit Type<10>Shift+Click: Deselect One Unit<10>Ctrl+Shift+Click: Deselect Unit Type +<164><3><4>Metastasis <1>(passive)<10>Victims of Plague spread the effect to those nearby when killed. +a<1><4>Mutate into Ocsal Cesiant <1>(<3>A<1>)<10>Mutated production cell. +e<3><4>Imperioso Mode <1>(<3>E<1>)<10>Returns the Madrigal's weapon and mobility to normal. +<> +<164><3><4>Corpse Autophagy <1>(passive)<10>Vilgoleths expire after 10 seconds or until destroyed. Every second, Vilgoleths consume the closest corpse within 5 range, spawning 2 Rilirokors. +e<3><4>Furia Mode <1>(<3>E<1>)<10>Switches forms, gaining Furia Salvo and disabling movement. +<164><3><4>Vengeance <1>(passive)<10>Should this combatant's corpse remain intact 1 second after death, it shall be avenged, granting 33% increased movement speed to allies within the fallen combatant's vision range. +e<3><4>Visionary Mode <1>(<3>E<1>)<10>Switches forms, gaining detection, doubling Nanite Field's range, and disabling movement. +e<3><4>Support Mode <1>(<3>E<1>)<10>Returns the Shaman's vision, Nanite Field range, and mobility to normal. +<164><3><4>Fossilizer Rounds <1>(passive)<10>Negative effects, such as stuns and slows, have their durations extended by Goska's attacks. +<> +<164><3><4>Relentless Aggression <1>(passive)<10>Ghitorokors gain 10% increased attack speed with each attack until they reach the attack speed cap. This bonus falls off after 2 seconds of not being refreshed. +g<1><4>Train Silvertongue <1>(<3>G<1>)<10>Deceiving infantry. +<164><3><4>Recursion Rounds <1>(passive)<10>Each Cyclops attack leaves behind a smart munition that detonates after 2 seconds, damaging all hostiles within 2 range.<0><10> +g<1><4>Build Dugout <1>(<3>G<1>)<10>Light garrison. +z<1><4>Build Azazel <1>(<3>Z<1>)<10>Defensive support craft. +e<1><4>Train Engineer <1>(<3>E<1>)<10>Worker. +s<1><4>Build Station <1>(<3>S<1>)<10>Town center. +d<1><4>Build Dispensary <1>(<3>D<1>)<10>Vespene refinery. +a<1><4>Build Academy <1>(<3>A<1>)<10>Officer production. +f<1><4>Train Deputy <1>(<3>F<1>)<10>Ranged infantry. +w<1><4>Build WAT Trooper <1>(<3>W<1>)<10>Crowd-controlling infantry. +s<1><4>Build Slapdash <1>(<3>S<1>)<10>Frontline infantry. +r<1><4>Build Priestess <1>(<3>R<1>)<10>Restorative infantry. +b<1><4>Build Barker <1>(<3>B<1>)<10>Frontline cavalry. +<27><0><4>Cancel Demolition <1>(<3>ESC<1>) +<164><3><4>Evasive Maneuvers <1>(passive)<10>After falling below 30 HP, Stewards will return to their Solarion, regaining all HP and becoming cleansed of any debuffs. +<> +Chokral Chaszal requires:<10> Othstol Oviform +<> +<> +<164><3><4>Heavenly Passage <1>(passive)<10>Missiles launched by Empyreans can be intercepted by airborne hostiles. +<164><3><4>Astral Rule <1>(passive)<10>Victims of the Patriarch's attacks are debuffed for 2 seconds, and restore 5 shields to any attackers. +g<1><4>Morph into Algoztalisk <1>(<3>G<1>)<10>Relentless corvette strain. +Algoz Aspect requires:<10> Chokral Chazsal +<164><3><4>Reclamation Fields <1>(passive)<10>This combatant's attacks create Lazarus fields that reclaim a corpse within 2 range after 1.5 seconds. +<> +<164><3><4>Fan of Teeth <1>(passive)<10>Upon taking damage, this strain launches a retaliatory bone in the direction of their attacker that travels up to 3 range, piercing any hostiles in its path. +Cherbrath Aspect requires:<10> Chokral Chazsal +c<1><4>Morph into Cherbrathalor <1>(<3>C<1>)<10>Crowd-controlling artillery strain. +e<3><4>Sublime Shepherd <1>(passive)<10>All allies within 6 range of the source gain +2 armor when facing incoming attacks. +<164><3><4>Ocsal Gestation <1>(passive)<10>This cell generates one Ocsal Larva every 14 seconds. Ocsal Larvae morph directly into mutated strains. +<164><3><4>Swarmhold <1>(passive)<10>This cell generates one Larva every 14 seconds. Larvae generated by this cell create twice as many Zerg. +<164><3><4>Recursion Rounds <1>(passive)<10>Each Cyclops attack leaves behind a dangerous shell that detonates after 2 seconds, damaging all combatants within 2 range. +<> +e<3><4>Cinder Mode <1>(<3>E<1>)<10>Switches forms, gaining 25% bonus attack speed and 2 armor, while disabling movement. +e<0><4>Tank Mode <1>(<3>E<1>)<10>Returns the Matador's weapon, armor, and mobility to normal. diff --git a/mpq/rez/tips.tbl.txt b/mpq/rez/tips.tbl.txt new file mode 100644 index 00000000..6c35225b --- /dev/null +++ b/mpq/rez/tips.tbl.txt @@ -0,0 +1,112 @@ +# tbl-text v1 - StarCraft: Brood War string table source +# One entry per line, in index order. +# <0> separates the parts of a multi-part entry +# raw byte N (decimal 0-255), e.g. <10> newline, <4> colour code +# << a literal '<' <> nothing (empty-entry marker) +# Lines starting with '#' are comments; blank lines are ignored. + +Cosmonarchy generates a config file when you first run the game. Review it for options like quick-targeting and grid hotkeys. +While selecting your own units or structures, right-clicking will issue a contextual order based on what your units and target are. These orders includes move, set rally point, follow, and enter transport. +Subtitles in most scenarios can be toggled on or off in sound settings. +Vespene Geysers can be directly harvested from. Geysers can be capped with race-specific structures to increase their Vespene yield. +The letters in parenthesis on command card labels denote what keyboard shortcut you can use to issue the order. +Mousing over a selected unit or structure's weapon or armor tooltip will provide at-a-glance statistics and statuses. Holding shift will toggle verbose tooltips based on your config. +Mission objectives can be reviewed in the game menu. +Units of any race can be loaded into a garrison, such as an Anchor. While garrisoned, units are shielded from attacks, and ranged units are free to return fire. Ranged units get +2 weapon range while inside a garrison. +Terrans can use Vultures to lay Lobotomy Mines and consolidate map control. Lobotomy Mines are revealed by detection, but have punishing stun and slow effects if triggered. +Remember that queueing units will spend your money without giving you immediate return. If you have many units in a queue, you should consider building more factories. +Many units have active and passive abilities that allow skilled players to benefit from good positioning, timing, and precision. Refer to a unit's command card to see their abilities. +A wide variety of build orders and opening strategies are at play in Cosmonarchy. Scout your opponent with a worker or early combatant to gather valuable information, and plan your composition and attack angles accordingly. +You can augment your economy with supportive infrastructure like Quarries, Larvosk Cesiants, Embassies, and Crucibles; benefits include additional worker queues and empowered production rates. +Some stealthy units are cloaked. Terran cloaking is negated by moving close to the unit, while Protoss cloaking is negated when the unit attacks. All cloaked units can be revealed by detectors. +If a factory is destroyed, any queued units are cancelled, offering a full resource refund. Destroyed Eggs and structures in progress give no refunds, and cancelled structures give a 75% refund. +Press Tab to toggle terrain on the minimap. This can be useful for focusing on known enemy positions. +Transports like the Trojan, the Envoy, and the Ovileth can be used to ferry ground units and small structures between locations, but are fragile and defenseless. "Death drops" work best when you have the element of surprise, or when you distract your opponent's defense force with a cross-map attack. +Holding shift and pressing tab will cycle between different minimap display modes, each with their own advantages and disadvantages. +Quick-targeting is a config option that will instantly map order targets to your mouse cursor without asking you to confirm them with a left mouse click. Quick-building carries this functionality over to issuing construction orders. +Terran structures can liftoff, fly to a new destination, and land. +Issuing an attack command targeting terrain will order your selection to engage targets they encounter while moving towards the destination. +Power fields generated by allied Pylons and Crucibles will also provide their benefits to your units and structures, allowing you to warp in infrastructure anywhere your team has power. +Regardless of who they used to belong to, Terran addons can be reactivated by placing new factories in the appropriate positions. +You can edit your config to change what APM thresholds you want warnings at. This is useful for setting benchmarks and goals for your improvement as a player. +If you find the onscreen display of scenario objectives too distracting, you can disable it in your config. +Anvils can arm warheads for deployment by Eidolons or Seraphs. If an armed Anvil is destroyed in any manner other than a controlled demolition, the warhead will detonate. +You can deselect units and structures by holding the shift key while left-clicking them. +While group-selecting, you can quickly select a specific unit or structure by left-clicking on its wireframe in the status display. +With so many hidden dangers, it is often advisable to construct detectors like Anciles, Witnesses, and Ovileths. Not only will they reveal hidden units, they are often mobile enough to act as adept scouts. +Some advanced transports provide much more space than their basic counterparts. These transports often move slower, making them ideal for large army transfers and less fitting for hit-and-run tactics. +You can queue an unload order before loading any cargo. This can be ideal for planning quick harassment drops. +Most Zerg structures require cagra, which is spread by Hachirosks, Cagrant Cesiants, and their mutations. Cagra also blocks enemies from constructing structures, as long as your cagra generators remain intact. +Mind the minimap! While it holds valuable and ever-changing information, you can also use it instead of the game display to issue targeted orders. +Join us on Discord to organize games, challenge other players, and share your thoughts! discord.com/invite/s5SKBmY +Zerg structures can mutate into more advanced and specialized variants. This ranges from static defense, to unit production, to techtree advancements. +You can recall up to 8 unique previous selections by holding alt and left-clicking a unit or structure. If they were previously in a selection with other units or structures, they will all be selected. +Playing the tutorials and other singleplayer content is a great way to jump into the world of Cosmonarchy, and familiarize yourself with the techtrees. Some scenario content feature factional differences between the active armies, adding even more variety into the mix! +Almost every Zerg unit can mutate into a more specialized strain. These may be superior to the base unit depending on your enemy's army, so choose your composition wisely. +Visit fraudsclub.com for information on setting up your own custom scenarios! +Keep an ear out for odd noises! Abilities and effects have audio cues associated with them, so an unexpected sound probably means trouble. +At high tech tiers, expect to see units capable of reclaiming, tyrannizing, infesting, and hijacking the opposition. +Skilled players make ample usage of queued orders by holding shift as they issue commands. +Without a unit cap, there's nothing stopping you from stationing healthy defense forces to protect your flanks during a frontal assault on the enemy. +Zerg ground units have the ability to Burrow, increasing health regeneration and concealing themselves from hostile vision unless revealed by a detector. Lakizilisks can attack while burrowed, allowing Zerg players to set deadly traps. +Units will automatically acquire targets within their acquisition range. This range differs between units and is often based on their maximum weapon range. +Overwhelmed by choice? You can see the techtrees of all races at fraudsclub.com/cmbw/ +Many units cost as much or more than entire structures, and often have much higher mineral costs than vespene costs, or vice versa. Balancing your spending and expanding your income is a vital skill in Cosmonarchy. +While far from perfect, the AI of Cosmonarchy has plenty of tricks up their sleeve. Studying their tactics and compositions may prove useful for newer Cosmonarchs. +The most deadly strategies take shape in the fog of war. Skilled players always seek to keep tabs on their enemies with active scouting and consistent map presence. +Killing any unit or structure will leave behind remains that can be exploited by certain units. Keep an eye on this resource, as it grows in perpetuity. +Selecting a single unit or structure will display their command card, player name, and force name. This is useful for pinpointing what a unit or structure does and who they belong to. +When a push starts to falter, knowing when to begin retreating to a forward post is vital. Area denial and map control are huge aspects of keeping momentum on your side. +Some powerful weapons and abilities leave behind effect fields. These can be threatening, supportive, or some combination of the two. +Most maps have lanes of engagement that larger armies gravitate towards. Take advantage of smaller paths and unexpected angles to take your opponents by surprise. +Ctrl+S toggles sound effects on and off. Ctrl+M does the same for music. +After two workers per resource node, there are diminishing returns to saturating nodes with additional harvesters. If you just expanded to a new location, transfer some workers from a saturated base for a quick spike in income! +The default competitive ruleset grants victory when all of your opponents lose all of their factories. This includes town centers and incomplete structures. +While there are many ways to get advantages in Cosmonarchy, they primarily boil down to claiming map control, improving your economy, raising an army, and ascending to a new tech tier. You will rarely have enough resources to do all of these at once, so train your decisionmaking and coordinate with any allies you have. +Construction orders will begin even if an allied unit is in the way. Hostile units will block construction attempts. +In team games, it pays to have a game plan already figured out. Many maps bias players for or against certain strategies depending on their starting locations, encouraging different roles within a team. Learn the maps to better plan your team's path to victory. +You can save a camera position by pressing shift+F1 through F8. Pressing the key without holding shift will move your camera to the saved position. +While it might be tempting to move up the techtree, remember that you can't properly express the power of that tier without sufficient income. Higher-tier units are pricey, and so too are higher-tier factories. +Reading tips is not as instructive as losing to the AI. +Each race has their own unique construction method. Advanced workers are specialized for certain tasks, and will likely be more optimal to use in longer battles. +Holding ctrl when left-clicking a unit or structure will select all nearby units or structures of that type. You can also do this by double-left-clicking. +If an area of the map is becoming a hotspot, use your race's unique traits to lock it down. Terrans specialize in artillery and battlefield repairs, Protoss prefer to outlast the opposition, and Zerg tend to overwhelm hostiles with superior numbers and flanking positions. +Everything's better with friends, and Cosmonarchy has plenty of player-vs-AI content! +Unlike the other races, Protoss have no tech requirements for their structures. This compensates for their higher resource costs, but makes their openers dangerously unpredictable. +Events like coming under attack and completing a unit or structure will send notifications on the minimap. You can center to the last notification by pressing spacebar. +Cosmonarchy is all about the balance of power, and momentum can shift at any time. If you've been holding a push since the early game, your opponent probably doesn't have much infrastructure down, so don't give up over a few early losses! +You can save your current selection to a control group by holding ctrl and pressing 1-0 on the home row. With extended control groups, you can also use R, T, F, G, V, and B. To add your current selection to a control group without overwriting it, use shift instead of ctrl. +You can use an ally's transports, garrisons, and teleporters to reposition around the map, assist in fending off an attack, or consolidate a forward post. +In team games, you can use ctrl+enter to write a message to your allies, or shift+enter to write a message to all players. +Many units gain situational value based on nearby allies. Good target selection includes knowing when to focus these specialists and when to instead attack the surrounding units. +Armor penetration allows a unit's damage to ignore a certain amount of their target's armor, but does not increase damage if there is more armor pen than armor. +Basic combat roles are fulfilled by multiple units of different tiers. If you have enough resources, you can choose to replace your core lower-tier with higher-tier ones as the battle rages on. +Many status effects will buff or debuff your units, changing their combat effectiveness. Some effects stun units, rendering them uncontrollable for a short time. +With Skyways, Didacts, and Nydus Cesiants, there are many methods of relocating your army without hoofing it to every destination. +If a leaderboard is active, you can use ctrl+R to toggle between displaying all players and displaying only the current leader. +You can access the debug console by pressing F12. This is especially useful when playing the unit tester, where you can spawn different combinations of units and test out various compositions. +If you like what you see in Cosmonarchy, you'd better get hyped for the future... +Anticipating the enemy's movements is sometimes most important when using short-range units. Skilled players will lead their opponents and even surround their victims if they have enough units to do so. +Practicing openers, either alone or against AI, is a great way to get familiar with the game's techtrees and internalize how powerful an enemy player could be given enough time. +Replays are an excellent resource for those who wish to learn from their mistakes, especially if you were caught off-guard by your opponent's moves. +Practice gets you ever closer to perfect, and we host tournaments through our Discord server. Consider signing up if you want an excuse to grind! +The most powerful units in Cosmonarchy are often among the slowest. If you find yourself face to face with the biggest guns in the universe, remember that they simply cannot be everywhere at once! +Team-based interactions are a big focus of Cosmonarchy's design. Collaborate with your ally by using supportive auras and abilities, and you'll be surprised how effective the races are together. +Each race has multiple options for defensive and supportive structures. Knowledgeable players will design their base layouts to conform to the terrain while still benefiting from critical infrastructure. +Unless you're saving up for something specific, avoid having a big bank of unspent resources. Those resources could be units and structures instead of a number on the screen! +All races have ways of staving off your army's demise, be it through healing, shielding, or spawning reinforcements. Skillful use of these supportive specialists can make an enormous difference in the outcome of battles. +Units on higher ground gain a weapon range bonus when engaging targets on low ground. This bonus is +2 for every height level difference, so use caution around cliffs! +With the exception of the Terran Blackjack, there are no penalties for losing life on a unit, so remember to move wounded combatants out of immediate danger. This will extend their lifespan and allow them to keep contributing to the current skirmish. +The most powerful abilities in Cosmonarchy have channel times and audiovisual reads associated with them. These omens should be heeded in case you need to focus a specific enemy unit or relocate your army out of harm's way. +Some projectiles explode on contact, allowing enemies to intercept them. This adds another layer of positional gameplay, and stresses the importance of a durable frontline. +Structures can be more than what they say on the tin. Intelligent base layouts give plenty of room for allies to move through, while still giving your enemies grief when they attempt to storm the gates. +When facing off against experienced players, or even Cosmonarchy's built-in AI, don't expect to claim easy victories! There is a lot of information to learn and a lot of skills to practice, but with enough time and effort, you'll see marked improvement in your own gameplay. +With all the responsibility of a Cosmonarch on your shoulders, it's easy to get overly-focused on one aspect of play. Micromanaging your army, expanding your economy and tech, and queueing reinforcements are all vital actions for success, so if you ever find yourself spending more than a few seconds doing any one task, consider switching to the next thing that demands your attention. +You don't always need to expand to a resource area in order to gain an advantage. Sometimes, denying your opponent access to an asset is enough to give you the upper hand. +Once you grow comfortable with the mechanics and techtrees of Cosmonarchy, consider how you might "mind-game" your opponents. Feinting with empty transports, hiding secret outposts, and even distracting them with chat messages are all fair-game on the battlefield! +Getting a good night's sleep and staying physically active will help tremendously with focus, and could certainly make you a better RTS player! +It's always a good idea to emulate more experienced players, even if you don't fully understand why they did something. If a strategy worked against you, it might work against your next opponent, too! +Every action has an opportunity cost, and this is especially true for early moves. If you attempt a rush and fail to deal lasting damage, you'll have lost a lot of momentum! +Sometimes, the greatest strategy is the one nobody's even dreamt of. +If you're struggling with a matchup, ask a more experienced player for replays and advice! You might be surprised at what another perspective can reveal. +You can lower the AI difficulty by becoming better. diff --git a/mpq/rez/unitnames.tbl.txt b/mpq/rez/unitnames.tbl.txt new file mode 100644 index 00000000..13851b72 --- /dev/null +++ b/mpq/rez/unitnames.tbl.txt @@ -0,0 +1,1547 @@ +# tbl-text v1 - StarCraft: Brood War string table source +# One entry per line, in index order. +# <0> separates the parts of a multi-part entry +# raw byte N (decimal 0-255), e.g. <10> newline, <4> colour code +# << a literal '<' <> nothing (empty-entry marker) +# Lines starting with '#' are comments; blank lines are ignored. + +Terran Maverick<0>*<0>Ground Units +Terran Eidolon<0>*<0>Ground Units +Terran Vulture<0>*<0>Ground Units +Terran Goliath<0>*<0>Ground Units +Goliath Turret<0>*<0>Special +Terran Phalanx<0>Tank Mode<0>Ground Units +Phalanx Turret<0>*<0>Special +Terran Mason<0>*<0>Ground Units +Terran Wraith<0>*<0>Air Units +Terran Seraph<0>*<0>Air Units +Terran Apostle<0>*<0>Ground Units +Terran Trojan<0>*<0>Air Units +Terran Minotaur<0>*<0>Air Units +Terran Spider Mine<0>*<0>Special +Terran Morningstar<0>*<0>Special +Terran Shaman<0>Support Mode<0>Ground Units +Protoss Manifold<0>*<0>Ground Units +Protoss Architect<0>*<0>Ground Units +Terran Madcap<0>*<0>Ground Units +Terran Cuirass<0>*<0>Ground Units +Terran Talos<0>*<0>Air Units +Protoss Star Sovereign<0>*<0>Air Units +Terran Salamander<0>*<0>Air Units +Terran Gorgon<0>*<0>Air Units +Terran Phobos<0>*<0>Air Units +Protoss Gladius<0>*<0>Air Units +Protoss Magister<0>*<0>Air Units +Terran Madrigal<0>Imperioso Mode<0>Ground Units +Terran Centaur<0>*<0>Air Units +Zerg Almaksalisk<0>*<0>Air Units +Terran Phalanx<0>Siege Mode<0>Ground Units +Phalanx Siege Turret<0>*<0>Special +Terran Harakan<0>*<0>Ground Units +Scanner Sweep<0>*<0>Special +Terran Cleric<0>*<0>Ground Units +Zerg Larva<0>*<0>Special +Zerg Egg<0>*<0>Special +Zerg Zethrokor<0>*<0>Ground Units +Zerg Hydralisk<0>*<0>Ground Units +Zerg Ultrokor<0>*<0>Ground Units +Zerg Rilirokor<0>*<0>Ground Units +Zerg Droleth<0>*<0>Ground Units +Zerg Ovileth<0>*<0>Air Units +Zerg Mutalisk<0>*<0>Air Units +Zerg Geszithalor<0>*<0>Air Units +Zerg Matraleth<0>*<0>Air Units +Zerg Isthrathaleth<0>*<0>Ground Units +Zerg Gosvileth<0>*<0>Unused +Zerg Alkajelisk<0>*<0>Air Units +Terran Pazuzu<0>*<0>Ground Units +Terran Cohort<0>*<0>Factional Units +Zerg Kalkalisk<0>*<0>Air Units +Zerg Evigrilisk<0>*<0>Ground Units +Terran Penumbra<0>*<0>Ground Units +Zerg Nathrokor<0>*<0>Air Units +Terran Cyclops<0>*<0>Ground Units +Zerg Gorgrokor<0>*<0>Air Units +Zerg Othstoleth<0>*<0>Air Units +Terran Valkyrie<0>*<0>Air Units +Zerg Cocoon<0>*<0>Special +Protoss Lanifect<0>*<0>Air Units +Protoss Cabalist<0>*<0>Ground Units +Zerg Vithrilisk<0>*<0>Air Units +Protoss Mind Tyrant<0>*<0>Ground Units +Protoss Scribe<0>*<0>Ground Units +Protoss Zealot<0>*<0>Ground Units +Protoss Dracadin<0>*<0>Ground Units +Protoss Cantavis<0>*<0>Ground Units +Protoss Archon<0>*<0>Ground Units +Protoss Envoy<0>*<0>Air Units +Protoss Panoptus<0>*<0>Air Units +Protoss Didact<0>*<0>Air Units +Protoss Solarion<0>*<0>Air Units +Protoss Steward<0>*<0>Air Units +Protoss Legionnaire<0>*<0>Ground Units +Protoss Amaranth<0>*<0>Ground Units +Protoss Hierophant<0>*<0>Ground Units +Terran Heracles<0>*<0>Ground Units +Protoss Atreus<0>*<0>Ground Units +Protoss Ecclesiast<0>*<0>Ground Units +Protoss Aurora<0>*<0>Air Units +Protoss Clarion<0>*<0>Air Units +Protoss Empyrean<0>*<0>Air Units +<> +Protoss Witness<0>*<0>Air Units +Protoss Plasma Shell<0>*<0>Ground Units +Protoss Demiurge<0>*<0>Ground Units +Protoss Pariah<0>*<0>Ground Units +Tassadar<0>*<<>Heroes +Protoss Simulacrum<0>*<0>Ground Units +Protoss Charlatan<0>*<0>Ground Units +Terran Wyvern<0>*<0>Air Units +Terran Cyprian<0>*<0>Ground Units +Zerg Rililisk<0>*<0>Air Units +Protoss Exemplar<0>*<0>Air Units +Zerg Quazilisk<0>*<0>Ground Units +Zerg Keskathalor<0>*<0>Ground Units +Zerg Zoryusthaleth<0>*<0>Ground Units +Terran Azazel<0>*<0>Air Units +Protoss Idol<0>*<0>Ground Units +Protoss Golem<0>*<0>Ground Units +Map Revealer<0>*<0>Special +Protoss Augur<0>*<0>Ground Units +Zerg Lakizilisk<0>*<0>Ground Units +Protoss Barghest<0>*<0>Ground Units +Disruption Field<0>*<0>Special +Terran Ministry<0>*<0>Buildings +Terran Comsat Station<0>*<0>Addons +Terran Anvil<0>*<0>Addons +Terran Supply Depot<0>*<0>Buildings +Terran Reservoir<0>*<0>Buildings +Terran Stockade<0>*<0>Buildings +Zerg Liiralisk<0>*<0>Ground Units +Terran Fulcrum<0>*<0>Buildings +Terran Starport<0>*<0>Buildings +Terran Commandment<0>*<0>Addons +Terran Daedala<0>*<0>Buildings +Terran Covenant<0>*<0>Addons +Terran Magnetar<0>*<0>Air Units +Terran Vestry<0>*<0>Addons +Terran Palladium<0>*<0>Addons +Terran Rotary<0>*<0>Addons +Terran Atlas<0>*<0>Buildings +Zerg Sovroleth<0>*<0>Air Units +Terran Watchdog<0>*<0>Buildings +Terran Anchor<0>*<0>Buildings +Zerg Kagralisk<0>*<0>Ground Units +Terran Diadem<0>*<0>Buildings +Zerg Vorvrokor<0>*<0>Ground Units +Zerg Bactalisk<0>*<0>Ground Units +Zerg Axtoth Axis<0>*<0>Buildings +Zerg Hachirosk<0>*<0>Buildings +Zerg Caphrolosk<0>*<0>Buildings +Zerg Gathtelosk<0>*<0>Buildings +Zerg Nydus Cesiant<0>*<0>Buildings +Zerg Hydrith Den<0>*<0>Buildings +Zerg Rakzogth Roost<0>*<0>Buildings +Terran Olympian<0>*<0>Ground Units +Zerg Matravil Nest<0>*<0>Buildings +Zerg Almakis Antre<0>*<0>Buildings +Zerg Ulgorg Cavern<0>*<0>Buildings +Zerg Mutath Spire<0>*<0>Buildings +Zerg Quazeth Pool<0>*<0>Buildings +Zerg Cagrant Cesiant<0>*<0>Buildings +Zerg Spraith Cesiant<0>*<0>Buildings +Terran Autocrat<0>*<0>Ground Units +Zerg Surkith Cesiant<0>*<0>Buildings +Terran Guildhall<0>*<0>Addons +Terran Striga<0>*<0>Ground Units +Zerg Excisant<0>*<0>Buildings +Terran Luminary<0>*<0>Factional Units +Terran Siren<0>*<0>Ground Units +Zerg Othstol Oviform<0>*<0>Buildings +Zerg Tethvith Swamp<0>*<0>Buildings +Protoss Nexus<0>*<0>Buildings +Protoss Lattice<0>*<0>Buildings +Protoss Pylon<0>*<0>Buildings +Protoss Aquifer<0>*<0>Buildings +Protoss Automaton Register<0>*<0>Buildings +Protoss Prostration Stage<0>*<0>Buildings +Protoss Gateway<0>*<0>Buildings +Protoss Rogue Gallery<0>*<0>Buildings +Protoss Warden<0>*<0>Buildings +Terran Fountainhead<0>*<0>Buildings +Protoss Cenotaph<0>*<0>Buildings +Protoss Ancestral Archives<0>*<0>Buildings +Protoss Embassy<0>*<0>Buildings +Protoss Stargate<0>*<0>Buildings +Protoss Argosy<0>*<0>Buildings +Protoss Fleet Beacon<0>*<0>Buildings +Zerg Alkag Iteth<0>*<0>Buildings +Protoss Synthetic Synod<0>*<0>Buildings +Protoss Vagrant<0>*<0>Ground Units +Zerg Alaszileth<0>*<0>Air Units +Terran Quarry<0>*<0>Addons +Terran Tinkerer's Tower<0>*<0>Addons +Mineral Field<0>Type 1<0>Resources +Mineral Field<0>Type 2<0>Resources +Mineral Field<0>Type 3<0>Resources +Protoss Anthelion<0>*<0>Air Units +Zerg Izirokor<0>*<0>Ground Units +Zerg Irulent Ivoa<0>*<0>Buildings +Protoss Artisan<0>*<0>Air Units +Protoss Empress<0>*<0>Air Units +Protoss Prudence<0>*<0>Air Units +Zerg Skithrokor<0>*<0>Air Units +Terran Paladin<0>*<0>Ground Units +Terran Savant<0>*<0>Ground Units +Vespene Geyser<0>*<0>Resources +Terran Hadron<0>*<0>Addons +Terran Biotic Bastion<0>*<0>Addons +Zerg Axitrilisk<0>*<0>Ground Units +Town Center Marker<0>*<0>Special +Zerg Zorkiz Shroud<0>*<0>Buildings +Terran Aspirant<0>*<0>Factional Units +Terran Blackjack<0>*<0>Ground Units +Terran Claymore<0>*<0>Ground Units +Zerg Tosvrol Haunt<0>*<0>Buildings +Terran Iron Foundry<0>*<0>Buildings +Terran Captaincy<0>*<0>Buildings +Terran Scrapyard<0>*<0>Buildings +Protoss Crucible<0>*<0>Buildings +Dark Swarm<0>*<0>Special +Terran Nanite Assembly<0>*<0>Buildings +Zerg Kalkiir Lake<0>*<0>Buildings +Left Upper Level Door<0>*<0>Doors +Right Upper Level Door<0>*<0>Doors +Left Pit Door<0>*<0>Doors +Right Pit Door<0>*<0>Doors +Terran Sentinel<0>*<0>Buildings +Terran Tarasque<0>*<0>Factional Units +Terran Anticthon<0>*<0>Ground Units +Zerg Kethraj Keep<0>*<0>Buildings +Terran Atelier<0>*<0>Buildings +Start Location<0>*<0>U +Terran Treasury<0>*<0>Buildings +Zerg Larvosk Cesiant<0>*<0>Buildings +Zerg Konvilisk<0>*<0>Ground Units +Zerg Akistrokor<0>*<0>Air Units +Zerg Geszkath Grotto<0>*<0>Buildings +Protoss Ardent Authority<0>*<0>Buildings +Vespene Ridge<0>*<0>Resources +Zerg Iroleth<0>*<0>Air Units +Protoss Monument of Sin<0>*<0>Buildings +Zerg Irol Iris<0>*<0>Buildings +Protoss Sanctum of Sorrow<0>*<0>Buildings +Protoss Grand Library<0>*<0>Buildings +Zerg Sibraelosk<0>*<0>Buildings +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Reserved<0>*<0>* +Protoss Matrix<0>*<0>Buildings +Terran Sundog<0>*<0>Air Units +Terran Starpad<0>*<0>Buildings +Terran Ancile<0>*<0>Air Units +Terran Matador<0>*<0>Ground Units +Terran Future Station<0>*<0>Addons +Zerg Zarcavrokor<0>*<0>Air Units +Terran Cestus<0>*<0>Buildings +Terran Aerie<0>*<0>Addons +Terran Pegasus<0>*<0>Air Units +Terran Hippogriff<0>*<0>Air Units +Terran Oberon<0>*<0>Air Units +Terran Aion<0>*<0>Ground Units +Protoss Herald<0>*<0>Ground Units +Protoss Epigraph<0>*<0>Air Units +Zerg Tethzorokor<0>*<0>Ground Units +Zerg Protathalor<0>*<0>Ground Units +Zerg Vilgorokor<0>*<0>Ground Units +Zerg Cikralisk<0>*<0>Ground Units +Zerg Tosgrilisk<0>*<0>Air Units +Zerg Ghitorokor<0>*<0>Air Units +Zerg Zobriolisk<0>*<0>Ground Units +Zerg Tetcaerokor<0>*<0>Air Units +Terran Wendigo<0>*<0>Air Units +Terran Southpaw<0>*<0>Ground Units +Zerg Selkerilisk<0>*<0>Ground Units +Protoss Strident Stratum<0>*<0>Buildings +Protoss Auxiliary Oasis<0>*<0>Buildings +Terran Mantle<0>*<0>Buildings +Terran Ramesses<0>*<0>Ground Units +Terran Durendal<0>*<0>Ground Units +Terran Cataphract<0>*<0>Ground Units +Terran Apothecary<0>*<0>Buildings +Terran Dilettante<0>*<0>Ground Units +Terran Sevenstep<0>*<0>Ground Units +Terran Hypnagogue<0>*<0>Ground Units +Terran Parthenon<0>*<0>Buildings +Terran Armistice<0>*<0>Ground Units +Terran Pythean<0>*<0>Air Units +Zerg Ogroszaleth<0>*<0>Ground Units +Zerg Spirtith Cesiant<0>*<0>Buildings +Zerg Skortrith Cesiant<0>*<0>Buildings +Protoss Engram<0>*<0>Buildings +unused<0>*<0>Buildings +unused<0>*<0>Buildings +Terran Thespian<0>*<0>Ground Units +Terran Silvertongue<0>*<0>Ground Units +Terran Throneroom<0>*<0>Addons +Terran Emperor<0>*<0>Ground Units +Terran Jester<0>*<0>Ground Units +Zerg Agrileth<0>*<0>Ground Units +unused<0>*<0>Buildings +Protoss Sequestered Icon<0>*<0>Buildings +Protoss Unitary Union<0>*<0>Buildings +Protoss Servitor<0>*<0>Ground Units +Protoss Positron<0>*<0>Ground Units +Protoss Analogue<0>*<0>Ground Units +Protoss Unar<0>*<0>Ground Units +Protoss Maven<0>*<0>Ground Units +Protoss Cloudform<0>*<0>Air Units +Protoss Vibrance<0>*<0>Ground Units +Protoss Oscilliant<0>*<0>Ground Units +Protoss Potentate<0>*<0>Ground Units +Protoss Sycophant<0>*<0>Ground Units +Protoss Optecton<0>*<0>Ground Units +Protoss Patriarch<0>*<0>Ground Units +Protoss Apostate<0>*<0>Ground Units +Zerg Mortothrokor<0>*<0>Ground Units +Zerg Cherbrathalor<0>*<0>Ground Units +Zerg Okrimnilisk<0>*<0>Ground Units +Zerg Algoztalisk<0>*<0>Air Units +Zerg Elthrazalisk<0>*<0>Ground Units +Zerg Begrothilisk<0>*<0>Ground Units +Zerg Oztukrokor<0>*<0>Ground Units +Zerg Kethrostalor<0>*<0>Ground Units +Zerg Rakzilisk<0>*<0>Ground Units +Terran Philoc<0>*<0>Factional Units +Zerg Begrozith Barrow<0>*<0>Buildings +Zerg Chokral Chazsal<0>*<0>Buildings +Protoss Bulwark<0>*<0>Air Units +Terran Civilian<0>*<0>Special +Sarah Kerrigan<0>*<0>Heroes +Infested Kerrigan<0>*<0>Heroes +Crashed Minotaur<0>*<0>Special Buildings +Terran Psi Disruptor<0>*<0>Special Buildings +Terran Power Generator<0>*<0>Special Buildings +Zerg Overmind<0>*<0>Special Buildings +Zerg Overmind<0>(With Shell)<0>Special Buildings +Zerg Chrysalis<0>*<0>Special Buildings +Zerg Cerebrate<0>*<0>Special Buildings +Zerg Overmind Cocoon<0>*<0>Special Buildings +Khaydarin Formation<0>*<0>Special Buildings +Protoss Temple<0>*<0>Special Buildings +Xel'Naga Temple<0>*<0>Special Buildings +Protoss Warp Gate<0>*<0>Special Buildings +Protoss Stasis Cell<0>*<0>Special Buildings +Protoss Monogram<0>*<0>Ground Units +Protoss Hall of Heretics<0>*<0>Buildings +Zerg Nephrilosk<0>*<0>Buildings +Protoss Vagrant Shade<0>*<0>Special +Protoss Accantor<0>*<0>Ground Units +Zerg Ocsal Larva<0>*<0>Special +Zerg Ocsal Cesiant<0>*<0>Buildings +Zerg Kazitrith Cesiant<0>*<0>Buildings +Reserved<0>Buttonset Ocsal Larva Swarm<0>* +Reserved<0>Buttonset Ocsal Larva Assault<0>* +Reserved<0>Buttonset Ocsal Larva Control<0>* +Terran Control Node<0>*<0>Special Buildings +Protoss Vassal<0>*<0>Air Units +Reserved<0>Buttonset Mixed Mason Training<0>* +Daniel's Son<0>*<0>Special Units +Smoochie Boy<0>*<0>Special Units +Zerg Ezigrisant<0>*<0>Buildings +Terran Wellbore<0>*<0>Buildings +Protoss Pennant<0>*<0>Buildings +<> +Reserved<0>Buttonset Basic Police Structures<0>* +Reserved<0>Buttonset Advanced Police Structures<0>* +<> +Terran Matador<0>Cinder Mode<0>Ground Units +Terran Cuirass<0>Trench Mode<0>Ground Units +Matador Turret<0>Tank Mode<0>* +Matador Turret<0>Cinder Mode<0>* +Terran Lobotomy Mine<0>*<0>* +Wyvern Turret<0>*<0>* +Zerg Axitrilisk<0>Weaponless<0>Ground Units +Zerg Vilgoleth<0>*<0>Ground Units +Terran Shaman<0>Visionary Mode<0>Ground Units +Madrigal Turret<0>Imperioso<0>* +Terran Madrigal<0>Furia Mode<0>Ground Units +Madrigal Turret<0>Furia<0>* +Omnivale<0>*<0>Special Buildings +Terran Lodestone<0>*<0>Addons<0> +Floor Hatch<0>*<0>* +<0>*<0>* +Terran Lodestone<0>*<0>Addons<0> +<> +<> +<> +<> +<> +<> +<> +Terran Hotshot<0>*<0>Factional Units +Zerg Vornath Pond<0>*<0>Buildings +Zerg Skibact Scab<0>*<0>Buildings +Terran Qilin<0>*<0>Factional Units +Terran Auriel<0>*<0>Factional Units +Protoss Adherent<0>*<0>Factional Units +Protoss Extatus<0>*<0>Factional Units +Zerg Nathrelisk<0>*<0>Factional Units +Zerg Iziralisk<0>*<0>Factional Units +<> +<> +Terran Trenchfoot<0>*<0>Buildings +Zerg Sasyrvoleth<0>*<0>Air Units +Terran Tencendur<0>*<0>Buildings +Ackmed<0>Harakan<0>Heroes +Terran Cyclops<0>Futurist Federation<0>Factional Units +Protoss Zealot<0>Starbound<0>Factional Units +Protoss Ecclesiast<0>Khalendric Knights<0>Factional Units +Terran Harakan<0>Golden Republic<0>Factional Units +Protoss Cicatus<0>*<0>Factional Units +Zerg Inzorthrokor<0>*<0>Factional Units +Volatile Egg<0>*<0>Special +Terran Piety Ward<0>*<0>Special +Terran Cleric<0>Futurist Federation<0>Factional Units +Terran Sieve<0>*<0>Buildings +Protoss Cedent<0>*<0>Buildings +Zerg Eskigant<0>*<0>Buildings +Zerg Quazrokor<0>Zorthos Tendril<0>Factional Units +Protoss Renesair<0>Order of the Sacred Saber<0>Factional Units +Protoss Ashguard<0>Khalendric Knights<0>Factional Units +Zerg Lakizilisk<0>Zorthos Tendril<0>Factional Units +Protoss Xenolith<0>*<0>Buildings +Terran Facade<0>*<0>Buildings +Terran Savant<0>Power Siphon<0>Ground Units +Askosi Landlock<0>*<0>Buildings +Protoss Dracadin<0>Order of the Sacred Saber<0>Factional Units +Terran Blackjack<0>Golden Republic<0>Factional Units +Terran Eidolon<0>Nydus Trinity<0>Factional Units +Terran Tiangou<0>Base<0>Factional Units +Terran Tiangou<0>Turret<0>Factional Units +Terran Maverick<0>Futurist Federation<0>Factional Units +Zerg Hydralisk<0>Orkoti Tendril<0>Factional Units +Protoss Manifold<0>Starbound<0>Factional Units +Zerg Yblyleth<0>Zorthos Tendril<0>Factional Units +Askosi Pilgrim<0>*<0>Ground Units +Basic Askosi Facilities<0>*<0>* +Advanced Askosi Facilities<0>*<0>* +Supreme Askosi Facilities<0>*<0>* +Askosi Proselyte<0>*<0>Ground Units +Askosi Dyad<0>*<0>Ground Units +Askosi Missionary<0>*<0>Ground Units +Askosi Cardinal<0>*<0>Ground Units +Askosi Mimic<0>*<0>Ground Units +Askosi Vicar<0>*<0>Ground Units +Askosi Rove<0>*<0>Ground Units +Askosi Object<0>*<0>Ground Units +Askosi Scepter<0>*<0>Ground Units +Askosi Intendent<0>*<0>Ground Units +Askosi Helion<0>*<0>Air Units +Askosi Emissary<0>*<0>Air Units +Askosi Inkblot<0>*<0>Air Units +Askosi Tirade<0>*<0>Air Units +Askosi Arch<0>*<0>Buildings +Askosi Grandstand<0>Cannon Module<0>Buildings +Askosi Chantry<0>*<0>Buildings +Askosi Radix<0>*<0>Buildings +Askosi Cipher<0>*<0>Buildings +Askosi Function<0>*<0>Buildings +Askosi Gauntlet<0>*<0>Buildings +Askosi Cosmoscope<0>*<0>Buildings +Ragnasaur<0>*<0>Critters +Rhynadon<0>*<0>Critters +Bengalaas<0>*<0>Critters +Scantid<0>*<0>Critters +Ursadon<0>*<0>Critters +Kakaru<0>*<0>Critters +Flag<0>*<0>Powerups +Young Chrysalis<0>*<0>Powerups +Psi Emitter<0>*<0>Powerups +Data Disk<0>*<0>Powerups +Khaydarin Crystal<0>*<0>Powerups +Uraj<0>*<0>Powerups +Khalis<0>*<0>Powerups +Mineral Chunk<0>*<0>Powerups +Mineral Chunk<0>Depleted<0>Powerups +Vespene Barrel<0>*<0>Powerups +Vespene Barrel<0>Depleted<0>Powerups +Vespene Tank<0>*<0>Powerups +Vespene Tank<0>Depleted<0>Powerups +Vespene Sac<0>*<0>Powerups +Vespene Sac<0>Depleted<0>Powerups +Terran Beacon<0>*<0>Special +Protoss Beacon<0>*<0>Special +Zerg Beacon<0>*<0>Special +Terran Blackguard<0>*<0>Faction Units +Terran Arhaun<0>*<0>Faction Units +Arhaun Turret<0>*<0>Faction Units +Reserved<0>Buttonset Terran Addon Submenu<0>* +Zerg Zozarc Gorge<0>*<0>Buildings +Terran Ogun<0>Worker Mode<0>Ground Units +Protoss Autocort<0>*<0>Ground Units +Terran Cannibal<0>*<0>Factional Units +Terran Ogun<0>Smithy Mode<0>Ground Units +Basic Zerg Mutations<0>*<0>* +Advanced Zerg Mutations<0>*<0>* +Francisco Goska<0>Eidolon<0>Heroes +Stroluum<0>Magnorokor<0>Heroes +Cerebrate Tuzor<0>*<0>Special Buildings +Zerg Skryrokor<0>*<0>Air Units +Terran Madrigal<0>Hecaton Imperioso<0>Factional Units +Madrigal Turret<0>Hecaton Imperioso<0>Special +Terran Vulture<0>Hecaton<0>Factional Units +Terran Tartarus Mine<0>Hecaton<0>Special +Terran Madrigal<0>Hecaton Furia<0>Factional Units +Madrigal Turret<0>Hecaton Furia<0>Special +Askosi Bounty<0>*<0>Buildings +Zerg Azanthrokor<0>Orkoti Tendril<0>Factional Units +Terran Triton<0>*<0>Ground Units +Triton Turret<0>*<0>Special +Mineral Field<0>Rich 1<0>Resources +Mineral Field<0>Rich 2<0>Resources +Mineral Field<0>Rich 3<0>Resources +Mineral Chunk<0>Rich<0>Powerups +Mineral Chunk<0>Rich Depleted<0>Powerups +Zerg Sovrokor<0>Goszirith Tendril<0>Factional Units +Terran Valkyrie<0>Hecaton<0>Factional Units +Zerg Garvrokor<0>Goszirith Tendril<0>Factional Units +Protoss Simulacrum<0>Astral Omen<0>Factional Units +Protoss Warlock<0>Astral Omen<0>Factional Units +Scenario Unit 1<0>*<0>Special +Scenario Unit 2<0>*<0>Special +Scenario Unit 3<0>*<0>Special +Scenario Unit 4<0>*<0>Special +Scenario Unit 5<0>*<0>Special +Scenario Unit 6<0>*<0>Special +Scenario Unit 7<0>*<0>Special +Scenario Unit 8<0>*<0>Special +Askosi Grandstand<0>Holdout Module<0>Buildings +Askosi Grandstand<0>Vision Module<0>Buildings +Protoss Warp Gate<0>The Fall<0>Special Buildings +Zerg Cerebrate<0>The Fall<0>Special Buildings +Zerg Overmind<0>The Fall (With Shell)<0>Special Buildings +Zerg Overmind<0>The Fall<0>Special Buildings +Askosi Avatar<0>*<0>Ground Units +Askosi Abutment<0>*<0>Buildings +Askosi Proscenium<0>*<0>Buildings +Terran Quadriga<0>Golden Republic<0>Factional Units +Askosi Thaumalogue<0>*<0>Ground Units +Askosi Petract<0>*<0>Ground Units +Askosi Crematorium<0>*<0>Air Units +Askosi Zylode<0>*<0>Buildings +Askosi Astrolabe<0>*<0>Buildings +Terran Serathiel<0>Hecaton Coalition<0>Factional Units +Zerg Malkothrokor<0>*<0>Ground Units +<> +Askosi Panoply<0>*<0>Structures +Askosi Aphorist<0>*<0>Ground Units +Askosi Doronin<0>*<0>Ground Units +Askosi Agonist<0>*<0>Ground Units +Askosi Phontis<0>*<0>Ground Units +Askosi Zonguard<0>*<0>Ground Units +Askosi Akreno<0>*<0>Air Units +Askosi Iconoclast<0>*<0>Air Units +Askosi Paraclete<0>*<0>Air Units +Zerg Zethalor<0>Zorthos Tendril<0>Factional Units +Zerg Scrylyleth<0>Zorthos Tendril<0>Factional Units +Askosi Bestiary<0>*<0>Structures +Askosi Conduit<0>*<0>Structures +Askosi Obelisk<0>*<0>Structures +Askosi Eyelet<0>*<0>Special Units +Askosi Auxine<0>*<0>Special Units +Askosi Constituent<0>*<0>Air Units +Terran Goremast<0>Upstart<0>Factional Units +Terran Broadside<0>Upstart<0>Factional Units +Broadside Turret<0>Upstart<0>Factional Units +Terran Vulture<0>Upstart<0>Factional Units +Terran Cutlass<0>Futurist Federation<0>Factional Units +Cutlass Turret<0>Futurist Federation<0>Factional Units +Terran Maverick<0>Upstart<0>Factional Units +Protoss Tritius<0>Astral Omen<0>Factional Units +Terran Morrigan<0>Upstart<0>Factional Units +Terran Ajax<0>Futurist Federation<0>Factional Units +Askosi Dorminge<0>*<0>Structures +Askosi Lotiro<0>*<0>Structures +Askosi Domain<0>*<0>Structures +Terran Maverick<0>Golden Republic<0>Factional Units +Terran Shaman<0>Support, Upstart<0>Factional Units +Terran Shaman<0>Adrenaline, Upstart<0>Factional Units +Askosi Insurant<0>*<0>Ground Units +Askosi Silversoul<0>*<0>Ground Units +Askosi Ghostwrit<0>*<0>Ground Units +Askosi Azjoran<0>*<0>Ground Units +Askosi Elgird<0>*<0>Ground Units +Askosi Histrion<0>*<0>Ground Units +Askosi Egocent<0>*<0>Special Units +Askosi Alacyne<0>*<0>Ground Units +Askosi Begolor<0>*<0>Ground Units +Askosi Limire<0>*<0>Air Units +Askosi Endbore<0>*<0>Air Units +Askosi Tortare<0>*<0>Air Units +Askosi Cybrene<0>*<0>Air Units +Askosi Lekrot<0>*<0>Structures +Askosi Hearthshard<0>*<0>Structures +Terran Nuriel<0>*<0>Factional Units +Terran Phoenix<0>*<0>Factional Units +Terran Archbishop<0>*<0>Factional Units +Protoss Pathlight<0>*<0>Factional Units +Protoss Saladius<0>*<0>Factional Units +Protoss Garantis<0>*<0>Factional Units +Zerg Zoltorokor<0>*<0>Factional Units +Forerunner Ferrum<0>*<0>Air Units +Forerunner Chalyb<0>*<0>Air Units +Forerunner Circitor<0>*<0>Air Units +Forerunner Exigo<0>*<0>Air Units +Forerunner Athla<0>*<0>Air Units +Forerunner Vicere<0>*<0>Air Units +Forerunner Machinamentum<0>*<0>Structures +Infested Kerrigan<0>Big<0>Heroes +Jim Raynor<0>Vulture<0>Heroes +Norad II<0>Minotaur<0>Heroes +Askosi Scralia<0>Sons of Ash<0>Factional Units +Zerg Rozirokor<0>Imicren Tendril<0>Factional Units +Bloodsummon Thrombocyte<0>*<0>Ground Units +Bloodsummon Grimoire<0>*<0>Structures +Zerg Zytryleth<0>Zorthos Tendril<0>Factional Units +Terran Camazotz<0>Nydus Trinity<0>Factional Units +Protoss Simulacrum<0>Khalendric Knights<0>Factional Units +Protoss Idol<0>Khalendric Knights<0>Factional Units +Protoss Vestige<0>Starbound<0>Factional Units +Terran Arquebus<0>Hecaton Coalition<0>Factional Units +Bloodsummon Acqui<0>*<0>Ground Units +Zerg Alzurokor<0>Imicren Tendril<0>Factional Units +Zerg Hydrokor<0>Goszirith Tendril<0>Factional Units +Protoss Gladius<0>Order of the Sacred Saber<0>Factional Units +Terran Eidolon<0>Golden Republic<0>Factional Units +Zerg Zethalisk<0>Orkoti Tendril<0>Factional Units +Bloodsummon Nihilist<0>*<0>Ground Units +Faction Selection Askosi<0>*<0>* +Terran Fleshthief<0>Nydus Trinity<0>Factional Units +Forerunner Lumen<0>*<0>Air Units +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> diff --git a/tools/hooks/pre-commit b/tools/hooks/pre-commit new file mode 100755 index 00000000..26942063 --- /dev/null +++ b/tools/hooks/pre-commit @@ -0,0 +1,36 @@ +#!/bin/sh +# Refuse a commit whose .tbl files do not match their .tbl.txt sources. +# +# The .tbl.txt files are the source of truth; the binary .tbl files are build +# artifacts that the game actually loads. Committing one without the other +# ships strings that nobody authored. +# +# Install: ln -sf ../../tools/hooks/pre-commit .git/hooks/pre-commit +# Bypass: git commit --no-verify + +set -e + +root=$(git rev-parse --show-toplevel) + +# Nothing staged under mpq/ that could affect a table? Nothing to check. +if ! git diff --cached --name-only --diff-filter=ACM | grep -qE '\.tbl(\.txt)?$'; then + exit 0 +fi + +if ! command -v bun >/dev/null 2>&1; then + echo "pre-commit: bun not found; skipping .tbl freshness check" >&2 + exit 0 +fi + +echo "pre-commit: checking .tbl files against their .tbl.txt sources" +if ! (cd "$root/tools/tbl" && bun run --silent check); then + cat >&2 <<'EOF' + +pre-commit: rejected - see STALE entries above. + + cd tools/tbl && bun run build # recompile, then stage the .tbl files + git commit --no-verify # or bypass, if you know why + +EOF + exit 1 +fi diff --git a/tools/linux/README.md b/tools/linux/README.md new file mode 100644 index 00000000..32c073aa --- /dev/null +++ b/tools/linux/README.md @@ -0,0 +1,55 @@ +# Linux / Wine setup + +Cosmonarchy runs under Wine, but a stock prefix gets four things wrong. Each one +fails in a way that does not name its own cause, so they are collected here. + +```sh +tools/linux/setup-bottle.sh --check # report, change nothing +tools/linux/setup-bottle.sh # apply +``` + +Defaults to the Bottles bottle named `Blizzard`. Use `--bottle NAME` for a +different bottle, `--prefix PATH` for a plain `WINEPREFIX`, and `--game-dir PATH` +if the StarCraft directory cannot be read from the prefix's registry. + +Every change is backed up next to the file it modifies. `--check` exits non-zero +when something is missing, so it works in a pre-flight. + +## What it fixes + +| Symptom | Cause | Fix | +| --- | --- | --- | +| Fraud Launcher 2 exits instantly | .NET 8 loads `icu.dll`, which Wine forwards to a nonexistent `icuuc68.dll` | `DOTNET_SYSTEM_GLOBALIZATION_USENLS=1` | +| `Fonts could not be loaded` | `aize_debug.qdp` opens `lucon.ttf` / `arialuni.ttf` with FreeType; Wine ships neither | drop DejaVu in under those filenames | +| Entirely black screen | Wine loads its own `ddraw.dll`, ignoring cnc-ddraw, so StarCraft does a real 640×480 fullscreen mode switch | `ddraw=native,builtin` | +| Black window, correct size | cnc-ddraw's `renderer=auto` draws black here | `renderer=gdi` | + +### Notes + +- The launcher failure shows up in Wine's output as + `err:module:find_forwarded_export module not found for forward + 'icuuc68.u_charsToUChars_68'`. `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1` also + works but discards culture support, so NLS is preferred. +- FreeType opens fonts by path, not by name, so any monospace / wide-coverage + TTF under those two filenames satisfies it. Needs `fonts-dejavu` (or + Liberation / Noto, which the script also accepts). +- `renderer=gdi` and clearing `shader=` were changed together, so which one + mattered is not isolated. The bundled GLSL shader only applies to the OpenGL + renderer, so it is inert under GDI either way — if you want it back, try + `renderer=opengl` first. +- Only the global `[ddraw]` section of `ddraw.ini` is touched; the per-game + preset sections below it are left alone, and CRLF endings are preserved. +- The `ddraw` override is written to `user.reg` directly, so close the game + first — Wine rewrites that file from memory when the last process exits. + +## Verified on + +Bottles `Blizzard` bottle, runner **GE-Proton10-25**, X11, dual 2560×1440. +Plain system wine 10.0 could not boot even vanilla StarCraft here (null call +through `storm.dll` ordinal 313 during 640×480 init), so GE-Proton is the +supported runner. + +Fraud Launcher 2 installs to `C:\Cosmonarchy\`: `Starcraft\` is a stock 1.16.1 +install, `Release\` is a git clone of this repo. The game runs from +`Release\Cosmonarchy BW.exe`, and `_qdp-hotloader.qdp` packs `Release\mpq\` into +`Release\temp.mpq` and loads it alongside the plugins. diff --git a/tools/linux/add-dll-override.py b/tools/linux/add-dll-override.py new file mode 100755 index 00000000..c2b8f57c --- /dev/null +++ b/tools/linux/add-dll-override.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Add a DllOverrides entry to a Wine prefix's user.reg. + +Usage: add-dll-override.py USER_REG DLL VALUE +e.g. add-dll-override.py ~/.../Blizzard/user.reg ddraw native,builtin + +Only safe while no wineserver is live for that prefix - Wine rewrites user.reg +from memory when the last process exits, which would drop the edit. +""" +import sys + +SECTION = r'[Software\\Wine\\DllOverrides]' + + +def main(path, dll, value): + with open(path, encoding='utf-8', errors='surrogateescape') as fh: + text = fh.read() + + entry = '"%s"="%s"\n' % (dll, value) + if entry in text: + print('already set: %s = %s' % (dll, value)) + return 0 + + i = text.find(SECTION) + if i == -1: + text = text.rstrip('\n') + '\n\n' + SECTION + ' 0\n' + entry + else: + j = text.index('\n', i) + 1 # past the section header + while j < len(text) and text[j] == '#': # past #time= and friends + j = text.index('\n', j) + 1 + text = text[:j] + entry + text[j:] + + with open(path, 'w', encoding='utf-8', errors='surrogateescape') as fh: + fh.write(text) + print('set: %s = %s' % (dll, value)) + return 0 + + +if __name__ == '__main__': + if len(sys.argv) != 4: + sys.exit(__doc__) + sys.exit(main(*sys.argv[1:])) diff --git a/tools/linux/patch-ddraw-ini.py b/tools/linux/patch-ddraw-ini.py new file mode 100755 index 00000000..af82e6df --- /dev/null +++ b/tools/linux/patch-ddraw-ini.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Set keys in the [ddraw] section of a cnc-ddraw ddraw.ini. + +Usage: patch-ddraw-ini.py [--check] DDRAW_INI KEY=VALUE [KEY=VALUE ...] + +Only the global [ddraw] section is touched; the hundreds of per-game preset +sections further down the file are left alone. CRLF line endings are preserved, +because cnc-ddraw writes the file back out as a Windows INI. + +Exit status: 0 if the file already matches (or was updated), 1 under --check if +it does not match. +""" +import re +import sys + + +def main(argv): + check = False + if argv and argv[0] == '--check': + check, argv = True, argv[1:] + if len(argv) < 2: + sys.exit(__doc__) + + path, assignments = argv[0], argv[1:] + wanted = {} + for a in assignments: + if '=' not in a: + sys.exit('not a KEY=VALUE pair: %s' % a) + k, v = a.split('=', 1) + wanted[k.strip().lower()] = v + + with open(path, 'rb') as fh: + raw = fh.read().decode('utf-8', 'surrogateescape') + + newline = '\r\n' if '\r\n' in raw else '\n' + lines = raw.split(newline) + + out, in_ddraw, pending, changes = [], False, dict(wanted), [] + for line in lines: + stripped = line.strip() + if stripped.startswith('['): + # leaving [ddraw]: append any keys the section never had + if in_ddraw and pending: + for k, v in pending.items(): + out.append('%s=%s' % (k, v)) + changes.append('added %s=%s' % (k, v)) + pending = {} + in_ddraw = stripped.lower() == '[ddraw]' + elif in_ddraw: + m = re.match(r'^(\s*)([A-Za-z_][A-Za-z0-9_]*)(\s*)=(.*)$', line) + if m and m.group(2).lower() in pending: + key = m.group(2).lower() + want = pending.pop(key) + if m.group(4) != want: + changes.append('%s=%s -> %s=%s' % (m.group(2), m.group(4), m.group(2), want)) + line = '%s=%s' % (m.group(2), want) + out.append(line) + + if in_ddraw and pending: + for k, v in pending.items(): + out.append('%s=%s' % (k, v)) + changes.append('added %s=%s' % (k, v)) + + if not changes: + print('already set: %s' % ', '.join('%s=%s' % kv for kv in wanted.items())) + return 0 + + if check: + for c in changes: + print('would change: %s' % c) + return 1 + + with open(path, 'wb') as fh: + fh.write(newline.join(out).encode('utf-8', 'surrogateescape')) + for c in changes: + print(c) + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/tools/linux/setup-bottle.sh b/tools/linux/setup-bottle.sh new file mode 100755 index 00000000..bd34c073 --- /dev/null +++ b/tools/linux/setup-bottle.sh @@ -0,0 +1,229 @@ +#!/usr/bin/env bash +# Prepare a Bottles/Wine prefix so Fraud Launcher 2 and Cosmonarchy BW will run. +# +# Fixes two things that a stock Wine prefix gets wrong: +# +# 1. Fraud Launcher 2 (.NET 8 WPF) aborts at startup because Wine's icu.dll +# forwards to icuuc68.dll, which does not exist: +# err:module:find_forwarded_export module not found for forward +# 'icuuc68.u_charsToUChars_68' used by L"C:\\windows\\system32\\icu.dll" +# Setting DOTNET_SYSTEM_GLOBALIZATION_USENLS=1 makes .NET use Windows NLS +# instead of ICU. (DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 also works but +# throws away culture support, so prefer NLS.) +# +# 2. aize_debug.qdp initialises its console with FreeType against two Windows +# system fonts that Wine does not ship, and dies with "Fonts could not be +# loaded": +# %systemroot%\fonts\lucon.ttf (Lucida Console) +# %systemroot%\fonts\arialuni.ttf (Arial Unicode MS) +# FreeType opens these by path, not by name, so any monospace / wide-coverage +# TTF dropped in under those filenames satisfies it. +# +# 3. Cosmonarchy ships cnc-ddraw (ddraw.dll beside StarCraft.exe), but Wine +# loads its own builtin ddraw.dll unless told otherwise, so cnc-ddraw is +# silently ignored. StarCraft then performs a real exclusive-fullscreen +# 640x480 DirectDraw mode switch, which on a multi-monitor X11 desktop +# shows up as an entirely black screen. Overriding ddraw to native,builtin +# makes Wine load cnc-ddraw so the game renders in a normal window. +# +# 4. With cnc-ddraw actually loaded, its default renderer=auto still draws a +# black window here; renderer=gdi renders correctly. The bundled GLSL +# shader only applies to the OpenGL renderer, so it is cleared alongside. +# (Both were changed together, so which one mattered is not isolated - if +# you want the shader back, try renderer=opengl first.) +# +# Usage: +# tools/linux/setup-bottle.sh [--check] [--bottle NAME | --prefix PATH] +# +# --check report what is missing, change nothing +# --bottle NAME Bottles bottle name (default: Blizzard) +# --prefix PATH a plain WINEPREFIX, instead of a Bottles bottle +# --game-dir PATH the StarCraft directory holding ddraw.ini +# (default: read from the prefix's registry InstallPath) +# +set -euo pipefail + +BOTTLE=Blizzard +PREFIX= +GAMEDIR= +CHECK=0 + +while [ $# -gt 0 ]; do + case "$1" in + --check) CHECK=1; shift ;; + --bottle) BOTTLE="${2:?--bottle needs a name}"; shift 2 ;; + --prefix) PREFIX="${2:?--prefix needs a path}"; shift 2 ;; + --game-dir) GAMEDIR="${2:?--game-dir needs a path}"; shift 2 ;; + -h|--help) sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) echo "unknown argument: $1" >&2; exit 2 ;; + esac +done + +if [ -z "$PREFIX" ]; then + for base in \ + "$HOME/.var/app/com.usebottles.bottles/data/bottles/bottles" \ + "$HOME/.local/share/bottles/bottles" + do + if [ -d "$base/$BOTTLE" ]; then PREFIX="$base/$BOTTLE"; break; fi + done +fi + +if [ -z "$PREFIX" ] || [ ! -d "$PREFIX/drive_c" ]; then + echo "error: no Wine prefix found (bottle '$BOTTLE')." >&2 + echo " pass --bottle NAME or --prefix /path/to/prefix" >&2 + exit 1 +fi + +echo "prefix: $PREFIX" +rc=0 + +note() { if [ "$CHECK" = 1 ]; then echo " MISSING $1"; rc=1; else echo " fixed $1"; fi; } +ok() { echo " ok $1"; } + +# --- 1. fonts ----------------------------------------------------------------- +# Each entry is "target-filenamecandidate source paths, first match wins". +install_font() { + local target="$1"; shift + local fontdir dest src + fontdir="$PREFIX/drive_c/windows/Fonts" + [ -d "$fontdir" ] || fontdir="$PREFIX/drive_c/windows/fonts" + mkdir -p "$fontdir" + dest="$fontdir/$target" + + if [ -s "$dest" ]; then ok "$target"; return 0; fi + + if [ "$CHECK" = 1 ]; then note "$target"; return 0; fi + + for src in "$@"; do + if [ -s "$src" ]; then + cp -- "$src" "$dest" + note "$target <- $(basename "$src")" + return 0 + fi + done + + # last resort: ask fontconfig for anything matching + if command -v fc-match >/dev/null 2>&1; then + src=$(fc-match -f '%{file}' "${FC_QUERY:-sans}" 2>/dev/null || true) + if [ -n "$src" ] && [ -s "$src" ]; then + cp -- "$src" "$dest" + note "$target <- $(basename "$src") (fontconfig)" + return 0 + fi + fi + + echo " FAILED $target - no source font found; install fonts-dejavu" >&2 + rc=1 +} + +echo "fonts (for aize_debug.qdp's FreeType console):" +FC_QUERY=monospace install_font lucon.ttf \ + /usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf \ + /usr/share/fonts/dejavu/DejaVuSansMono.ttf \ + /usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf \ + /usr/share/fonts/liberation/LiberationMono-Regular.ttf +FC_QUERY=sans install_font arialuni.ttf \ + /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf \ + /usr/share/fonts/dejavu/DejaVuSans.ttf \ + /usr/share/fonts/truetype/noto/NotoSans-Regular.ttf \ + /usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf + +# --- 2. .NET globalization (Fraud Launcher 2) --------------------------------- +echo ".NET globalization (for Fraud Launcher 2):" +VAR=DOTNET_SYSTEM_GLOBALIZATION_USENLS +YML="$PREFIX/bottle.yml" + +if [ -f "$YML" ]; then + if grep -q "^ *$VAR:" "$YML"; then + ok "$VAR set in bottle.yml" + elif [ "$CHECK" = 1 ]; then + note "$VAR in bottle.yml" + else + cp -- "$YML" "$YML.bak-$(date +%Y%m%d%H%M%S)" + python3 - "$YML" "$VAR" <<'PY' +import re, sys +path, var = sys.argv[1], sys.argv[2] +s = open(path, encoding='utf-8').read() +line = " %s: '1'\n" % var +if re.search(r'^Environment_Variables:\s*\{\}\s*$', s, re.M): + s = re.sub(r'^Environment_Variables:\s*\{\}\s*$', + "Environment_Variables:\n" + line.rstrip('\n'), s, count=1, flags=re.M) +elif re.search(r'^Environment_Variables:\s*$', s, re.M): + s = re.sub(r'^(Environment_Variables:\s*\n)', r'\1' + line, s, count=1, flags=re.M) +else: + raise SystemExit("could not find Environment_Variables: in %s" % path) +open(path, 'w', encoding='utf-8').write(s) +PY + note "$VAR in bottle.yml (backup written alongside)" + fi +else + # plain WINEPREFIX: no bottle.yml to edit, so tell the caller how to export it + if [ "$CHECK" = 1 ]; then + note "$VAR (no bottle.yml; export it yourself)" + else + echo " note no bottle.yml here - run the launcher with:" + echo " $VAR=1 wine 'Fraud Launcher 2.exe'" + fi +fi + +# --- 3. ddraw override, so Wine loads cnc-ddraw rather than its own builtin ---- +echo "ddraw override (cnc-ddraw):" +REG="$PREFIX/user.reg" +HELPER="$(dirname "$0")/add-dll-override.py" +if [ ! -f "$REG" ]; then + echo " FAILED no user.reg in $PREFIX" >&2 + rc=1 +elif grep -q '^"ddraw"=' "$REG"; then + ok "ddraw = $(grep -m1 '^\"ddraw\"=' "$REG" | cut -d= -f2- | tr -d '\"')" +elif [ "$CHECK" = 1 ]; then + note "ddraw=native,builtin in user.reg" +elif pgrep -f "wineserver" >/dev/null 2>&1 && pgrep -af wineserver | grep -qF "$PREFIX"; then + echo " SKIPPED a wineserver is live for this prefix - close the game and re-run" >&2 + rc=1 +elif [ ! -f "$HELPER" ]; then + echo " FAILED helper not found: $HELPER" >&2 + rc=1 +else + cp -- "$REG" "$REG.bak-$(date +%Y%m%d%H%M%S)" + python3 "$HELPER" "$REG" ddraw native,builtin >/dev/null + note "ddraw=native,builtin in user.reg (backup written alongside)" +fi + + +# --- 4. cnc-ddraw renderer ---------------------------------------------------- +echo "cnc-ddraw renderer:" +if [ -z "$GAMEDIR" ]; then + # HKLM\Software\Wow6432Node\Blizzard Entertainment\Starcraft -> InstallPath + winpath=$(grep -a -m1 '^"InstallPath"=' "$PREFIX/system.reg" 2>/dev/null \ + | sed 's/^"InstallPath"="//; s/"$//') + if [ -n "$winpath" ]; then + rel=$(printf '%s' "$winpath" | sed 's/\\\\/\//g; s|^[A-Za-z]:||; s|/*$||') + GAMEDIR="$PREFIX/drive_c$rel" + fi +fi + +INI="$GAMEDIR/ddraw.ini" +PATCHER="$(dirname "$0")/patch-ddraw-ini.py" +if [ -z "$GAMEDIR" ] || [ ! -f "$INI" ]; then + echo " SKIPPED no ddraw.ini found${GAMEDIR:+ at $GAMEDIR}; pass --game-dir" >&2 +elif [ ! -f "$PATCHER" ]; then + echo " FAILED helper not found: $PATCHER" >&2 + rc=1 +elif python3 "$PATCHER" --check "$INI" renderer=gdi shader= >/dev/null 2>&1; then + ok "renderer=gdi in $(basename "$GAMEDIR")/ddraw.ini" +elif [ "$CHECK" = 1 ]; then + note "renderer=gdi in $(basename "$GAMEDIR")/ddraw.ini" +else + cp -- "$INI" "$INI.bak-$(date +%Y%m%d%H%M%S)" + python3 "$PATCHER" "$INI" renderer=gdi shader= >/dev/null + note "renderer=gdi, shader= in $(basename "$GAMEDIR")/ddraw.ini (backup written alongside)" +fi + + +echo +if [ "$CHECK" = 1 ] && [ "$rc" != 0 ]; then + echo "check failed: run without --check to apply." +elif [ "$rc" = 0 ]; then + echo "bottle ready." +fi +exit $rc diff --git a/tools/tbl/.gitignore b/tools/tbl/.gitignore new file mode 100644 index 00000000..f4e2c6d6 --- /dev/null +++ b/tools/tbl/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.tsbuildinfo diff --git a/tools/tbl/README.md b/tools/tbl/README.md new file mode 100644 index 00000000..f002e313 --- /dev/null +++ b/tools/tbl/README.md @@ -0,0 +1,167 @@ +# tbl + +A round-trip compiler between StarCraft: Brood War `.tbl` string tables and a +plain-text `.tbl.txt` source format. TypeScript, [Effect](https://effect.website) v4, Bun. + +Every `.tbl` shipped in this repo decompiles and recompiles **byte for byte +identically** — that is enforced by the test suite. + +## Usage + +```sh +bun install + +bun run tbl decompile ../../mpq/arr/images.tbl # -> images.tbl.txt +$EDITOR ../../mpq/arr/images.tbl.txt +bun run tbl compile ../../mpq/arr/images.tbl.txt # -> images.tbl + +bun run tbl verify ../../mpq/rez/stat_txt.tbl # prove a round trip is lossless +bun run tbl info ../../mpq/rez/stat_txt.tbl # entry counts, offset headroom +``` + +| Command | Flags | +| --- | --- | +| `decompile ` | `-o `, `--annotate` (`# [index]` above each entry), `--bare` (no banner), `--dialect` | +| `compile ` | `-o `, `--dedupe` (share identical entries), `--dialect` | +| `build [root]` | `--check` (fail instead of writing), `--dedupe` | +| `verify ` | — | +| `info ` | — | + +`build` walks a directory for `.tbl.txt` sources and recompiles each one in place — +the whole `mpq` tree in one go: + +```sh +bun run build # recompile every table under ../../mpq +bun run check # fail if any committed .tbl is out of date +``` + +`tools/hooks/pre-commit` runs `check` so a stale `.tbl` cannot be committed beside an +edited `.tbl.txt`. Install it with +`ln -sf ../../tools/hooks/pre-commit .git/hooks/pre-commit`. + +`decompile` defaults to `.txt`, so `images.tbl` becomes `images.tbl.txt` +and never lands on the unrelated `images.txt` that DatEdit uses for its name +lists. `compile` strips that `.txt` back off. + +## The binary format + +``` +u16le count +u16le offset[count] // absolute, from the start of the file +... NUL-terminated string data +``` + +One index does not always mean one string. `unitnames.tbl` stores +`name\0subname\0group\0` under a single index, and `stat_txt.tbl` stores a +hotkey byte followed by button text. Everything from `offset[i]` up to the next +offset in the file therefore belongs to entry `i`, which is how this tool reads +them — including bytes past the last offset, which `stat_txt.tbl` relies on. + +## The text format + +One entry per line, in index order. `.tbl` strings are raw bytes in a legacy +code page and are full of Windows paths, so backslash escaping would be both +lossy and miserable to hand-edit. Angle brackets are the only special +characters: + +| Token | Meaning | +| --- | --- | +| `<0>` | separates the parts of a multi-part entry | +| `` | the raw byte `N` (decimal 0-255) — colour codes, `<10>` newline | +| `<<` | a literal `<` | +| `<>` | nothing; marks an otherwise empty line | + +Everything else is literal, so a GRP path stays readable: + +``` +zerg\avenger.grp +Terran Phalanx<0>Tank Mode<0>Ground Units +e<0><4>Tank Mode <1>(<3>E<1>)<10>Returns the Phalanx's weapon and mobility to normal. +``` + +Lines starting with `#` are comments and blank lines are ignored. An entry that +would begin with `#` or a space, or end with a space, has that character written +as `<35>` / `<32>` so no editor can quietly eat it. Output is ASCII by +construction, so re-saving the file as UTF-8 cannot corrupt it — a stray +non-ASCII character is a parse error pointing at the line and column. + +## The GPTP dialect + +Two tables in this repo are *already* loaded from text by the engine, not from a +`.tbl` at all: `rez/stat.txt` and `arr/sounds.txt`. CM-GPTP's `load_stat_txt` +(`hooks/limits/tbl_extender.cpp`) parses them with `collapse_control_codes` and +builds a **u32-offset** string table in memory, which is how they escape the +ceiling described below. `--dialect gptp` reads and writes that format. + +The two dialects agree on what an entry is — bytes, NUL-separated — and differ +only in escaping: + +| | `--dialect gptp` | tbl-text v1 | +| --- | --- | --- | +| `` | byte N; **wraps** above 255 (`char()` narrowing in the C++) | byte N; over 255 is an error | +| `<<` | not special — two literal `<` | a literal `<` | +| `<>` | not special — two literal chars | nothing (empty-entry marker) | +| `\n`, `\\` | newline, one backslash | no backslash escapes | +| bytes > 127 | allowed raw | must be written `` | +| `#` line | **an entry** whose text is `#…` | a comment | +| blank line | **an entry** | ignored | + +That last pair is the dangerous one: the engine turns *every* line into an entry, +so a comment inserted into `stat.txt` would become a string and shift every index +after it. `--dialect gptp` therefore refuses `--annotate` and writes no banner. + +Writing is unambiguous rather than byte-faithful to any hand-written file: `<` is +written `<60>` (this dialect has no `<<`, so a literal `<` before digits would be +read back as a control code), `\` is written `\\` only when the next byte would +make it an escape, and bytes above 127 are written `` so output stays ASCII. +Round trips are checked against both shipped files in `test/dialect.test.ts`. + +## The 64 KiB ceiling + +Offsets are `u16`, so no string may start past byte 65535. `stat_txt.tbl` is +currently **26 bytes** away from that limit: + +``` +$ bun run tbl info ../../mpq/rez/stat_txt.tbl + entries 1813 + size 65590 bytes (3628 header + 61962 strings) + highest offset 65509 of 65535 (26 bytes of headroom) +``` + +`compile --dedupe` points identical entries at one copy of the string data, +which buys room back without changing a single string: + +``` + entries 1813 (904 sharing string data) + size 64325 bytes (3628 header + 60697 strings) + highest offset 64244 of 65535 (1291 bytes of headroom) +``` + +It is off by default so that a plain recompile reproduces the original bytes. +Overflowing the ceiling is a hard error that names the offending entry and +suggests the flag. + +## Library + +```ts +import { Effect } from "effect" +import { Tbl, TblFiles, TblText } from "./src/index.ts" + +const program = Effect.gen(function*() { + const files = yield* TblFiles + const table = yield* files.readTbl("images.tbl") // ReadonlyArray> + yield* files.writeText("images.tbl.txt", table) +}) +``` + +`Tbl.decode` / `Tbl.encode` and `TblText.decode` / `TblText.encode` are pure and +need no services. Failures are tagged errors — `TblBinaryError`, +`TblTextError`, `TblEncodeError` — so `Effect.catchTag` can pick them apart. + +## Development + +```sh +bun test # 76 tests: byte-exact round trips over all 8 shipped .tbl files, + # plus the GPTP dialect against rez/stat.txt and arr/sounds.txt +bun run typecheck +``` diff --git a/tools/tbl/bun.lock b/tools/tbl/bun.lock new file mode 100644 index 00000000..bf77e4a4 --- /dev/null +++ b/tools/tbl/bun.lock @@ -0,0 +1,62 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@cosmonarchy/tbl", + "dependencies": { + "@effect/platform-bun": "4.0.0-rc.112", + "effect": "4.0.0-rc.112", + }, + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5.9.0", + }, + }, + }, + "packages": { + "@effect/platform-bun": ["@effect/platform-bun@4.0.0-rc.112", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.112" }, "peerDependencies": { "effect": "^4.0.0-rc.112" } }, "sha512-Y5n2HhV/vsbrJls9ukX42RL7zqXQxISHegWFsP9njsCyDmGTnq7QYSO82rglwrLPwtTrv5dkSRTuGw4+oXxgMg=="], + + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-rc.112", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.3" }, "peerDependencies": { "effect": "^4.0.0-rc.112" } }, "sha512-ttjz0xKamFN7vL8pNDYVwddJLjZvqKePc05djlz2VcdaKbLsnYbtMnL1rbOfHgEnIUSHGh7FkjaN4DM1Ov81sQ=="], + + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], + + "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="], + + "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="], + + "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], + + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + + "@types/node": ["@types/node@26.3.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "effect": ["effect@4.0.0-rc.112", "", { "dependencies": { "fast-check": "^4.9.0", "msgpackr": "^2.0.5" } }, "sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A=="], + + "fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="], + + "msgpackr": ["msgpackr@2.0.6", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-plGul/tqjt9vqWFR9zyqyLZls6gb5KTLvnsRO2B9+TZ8tNiXiVI/CR8LiDWlAX4IUeVkQ9gsmzKA6voC2QOciw=="], + + "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], + + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + + "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + } +} diff --git a/tools/tbl/package.json b/tools/tbl/package.json new file mode 100644 index 00000000..24ef2175 --- /dev/null +++ b/tools/tbl/package.json @@ -0,0 +1,26 @@ +{ + "name": "@cosmonarchy/tbl", + "version": "1.0.0", + "type": "module", + "description": "Round-trip compiler between StarCraft: Brood War .tbl string tables and a plain-text .tbl.txt source format.", + "license": "MIT", + "module": "src/index.ts", + "bin": { + "tbl": "src/cli.ts" + }, + "scripts": { + "tbl": "bun run src/cli.ts", + "build": "bun run src/cli.ts build ../../mpq", + "check": "bun run src/cli.ts build ../../mpq --check", + "test": "bun test", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@effect/platform-bun": "4.0.0-rc.112", + "effect": "4.0.0-rc.112" + }, + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5.9.0" + } +} diff --git a/tools/tbl/src/Errors.ts b/tools/tbl/src/Errors.ts new file mode 100644 index 00000000..4a4e48df --- /dev/null +++ b/tools/tbl/src/Errors.ts @@ -0,0 +1,29 @@ +/** + * Tagged errors for every failure mode of the .tbl <-> .tbl.txt pipeline. + */ +import { Schema } from "effect" + +/** The bytes of a `.tbl` file could not be interpreted as a string table. */ +export class TblBinaryError extends Schema.TaggedError()("TblBinaryError", { + message: Schema.String, + /** Byte offset the failure was detected at, when known. */ + offset: Schema.optional(Schema.Number) +}) {} + +/** A table could not be laid out as a `.tbl` file (entry count / offset limits). */ +export class TblEncodeError extends Schema.TaggedError()("TblEncodeError", { + message: Schema.String, + /** Entry index the failure was detected at, when known. */ + index: Schema.optional(Schema.Number), + /** Byte offset that overflowed the 16-bit offset table, when known. */ + offset: Schema.optional(Schema.Number) +}) {} + +/** A `.tbl.txt` source file could not be parsed. */ +export class TblTextError extends Schema.TaggedError()("TblTextError", { + message: Schema.String, + /** 1-based line number. */ + line: Schema.Number, + /** 1-based column number. */ + column: Schema.Number +}) {} diff --git a/tools/tbl/src/Tbl.ts b/tools/tbl/src/Tbl.ts new file mode 100644 index 00000000..49194f13 --- /dev/null +++ b/tools/tbl/src/Tbl.ts @@ -0,0 +1,200 @@ +/** + * The StarCraft: Brood War `.tbl` string table. + * + * Layout: + * + * ``` + * u16le count + * u16le offset[count] // absolute, from the start of the file + * ... NUL-terminated string data + * ``` + * + * An indexed entry is not necessarily a single string: `unitnames.tbl` stores + * `name\0subname\0group\0` under one index, and `stat_txt.tbl` stores a hotkey + * byte followed by button text. Everything from `offset[i]` up to the next + * offset in the file therefore belongs to entry `i`, which is why an entry is + * modelled as a list of NUL-separated {@link TblEntry} parts. + */ +import { Effect } from "effect" +import { TblBinaryError, TblEncodeError } from "./Errors.ts" + +/** One indexed entry: its NUL-separated parts, without the terminators. */ +export type TblEntry = ReadonlyArray + +/** A whole string table, in index order. */ +export type TblTable = ReadonlyArray + +/** Entry count and offsets are both `u16`, so a table holds at most 65535 entries. */ +export const MAX_ENTRIES = 0xffff + +/** Every offset must be addressable by a `u16`. */ +export const MAX_OFFSET = 0xffff + +const splitParts = (bytes: Uint8Array): ReadonlyArray => { + const parts: Array = [] + let start = 0 + for (let i = 0; i < bytes.length; i++) { + if (bytes[i] === 0) { + parts.push(bytes.subarray(start, i)) + start = i + 1 + } + } + parts.push(bytes.subarray(start)) + return parts +} + +/** Serialise one entry to `part\0part\0...\0`, including the final terminator. */ +const entryToBlock = (entry: TblEntry): Uint8Array => { + const parts = entry.length === 0 ? [new Uint8Array(0)] : entry + let size = parts.length + for (const part of parts) size += part.length + const block = new Uint8Array(size) + let cursor = 0 + for (const part of parts) { + block.set(part, cursor) + cursor += part.length + 1 // the gap is already 0 + } + return block +} + +/** A byte-exact string key for a block, used only to spot duplicate entries. */ +const blockKey = (block: Uint8Array): string => { + let key = "" + for (let i = 0; i < block.length; i += 4096) { + key += String.fromCharCode(...block.subarray(i, i + 4096)) + } + return key +} + +/** + * Decode the bytes of a `.tbl` file. + * + * Entry boundaries are derived from the sorted set of offsets, so shared and + * out-of-order offsets decode correctly, and bytes that no index points at are + * kept as trailing parts of the preceding entry rather than silently dropped. + */ +export const decode = Effect.fn("Tbl.decode")( + function*(bytes: Uint8Array): Effect.fn.Return { + if (bytes.length < 2) { + return yield* new TblBinaryError({ + message: `truncated header: expected at least 2 bytes, got ${bytes.length}` + }) + } + + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength) + const count = view.getUint16(0, true) + if (count === 0) return [] + + const headerEnd = 2 + count * 2 + if (bytes.length < headerEnd) { + return yield* new TblBinaryError({ + message: `truncated offset table: ${count} entries need ${headerEnd} bytes, file is ${bytes.length}`, + offset: bytes.length + }) + } + + const offsets: Array = new Array(count) + for (let i = 0; i < count; i++) { + const offset = view.getUint16(2 + i * 2, true) + if (offset < headerEnd || offset >= bytes.length) { + return yield* new TblBinaryError({ + message: `entry ${i} points at ${offset}, outside the string data (${headerEnd}..${bytes.length - 1})`, + offset + }) + } + offsets[i] = offset + } + + // Boundaries let an entry run to the next offset rather than the first NUL. + const bounds = [...new Set(offsets)].sort((a, b) => a - b) + const endOf = new Map() + for (let i = 0; i < bounds.length; i++) { + endOf.set(bounds[i]!, bounds[i + 1] ?? bytes.length) + } + + const entries: Array = new Array(count) + for (let i = 0; i < count; i++) { + const start = offsets[i]! + const end = endOf.get(start)! + const block = bytes.subarray(start, end) + if (block[block.length - 1] !== 0) { + return yield* new TblBinaryError({ + message: `entry ${i} is not NUL-terminated before the next entry at ${end}`, + offset: end - 1 + }) + } + entries[i] = splitParts(block.subarray(0, block.length - 1)) + } + + return entries + } +) + +export interface EncodeOptions { + /** + * Point identical entries at a single copy of the string data. Off by + * default so that re-encoding a decoded file reproduces it byte for byte; + * worth turning on for tables that are close to the 64 KiB offset ceiling. + */ + readonly dedupe?: boolean | undefined +} + +/** Encode a table into the bytes of a `.tbl` file. */ +export const encode = Effect.fn("Tbl.encode")( + function*(table: TblTable, options?: EncodeOptions): Effect.fn.Return { + const count = table.length + if (count > MAX_ENTRIES) { + return yield* new TblEncodeError({ + message: `${count} entries exceeds the ${MAX_ENTRIES} the u16 count field can hold`, + index: MAX_ENTRIES + }) + } + + const headerEnd = 2 + count * 2 + const offsets: Array = new Array(count) + const blocks: Array = [] + const shared = new Map() + let cursor = headerEnd + + for (let i = 0; i < count; i++) { + const block = entryToBlock(table[i]!) + + if (options?.dedupe) { + const key = blockKey(block) + const hit = shared.get(key) + if (hit !== undefined) { + offsets[i] = hit + continue + } + shared.set(key, cursor) + } + + if (cursor > MAX_OFFSET) { + return yield* new TblEncodeError({ + message: + `entry ${i} would start at ${cursor}, past the ${MAX_OFFSET} a u16 offset can address` + + (options?.dedupe ? "" : " (try --dedupe)"), + index: i, + offset: cursor + }) + } + + offsets[i] = cursor + blocks.push(block) + cursor += block.length + } + + const out = new Uint8Array(cursor) + const view = new DataView(out.buffer) + view.setUint16(0, count, true) + for (let i = 0; i < count; i++) view.setUint16(2 + i * 2, offsets[i]!, true) + + let write = headerEnd + for (const block of blocks) { + out.set(block, write) + write += block.length + } + + return out + } +) diff --git a/tools/tbl/src/TblFiles.ts b/tools/tbl/src/TblFiles.ts new file mode 100644 index 00000000..4058dfb4 --- /dev/null +++ b/tools/tbl/src/TblFiles.ts @@ -0,0 +1,72 @@ +/** + * File-level operations: the two codecs plus a filesystem. + */ +import { Context, Effect, FileSystem, Layer } from "effect" +import type { PlatformError } from "effect/PlatformError" +import type { TblBinaryError, TblEncodeError, TblTextError } from "./Errors.ts" +import * as Tbl from "./Tbl.ts" +import * as TblText from "./TblText.ts" + +const decoder = new TextDecoder("utf-8") +const encoder = new TextEncoder() + +export class TblFiles extends Context.Service Effect.Effect + /** Encode and write a binary `.tbl`. */ + readonly writeTbl: ( + path: string, + table: Tbl.TblTable, + options?: Tbl.EncodeOptions + ) => Effect.Effect + /** Read and parse a `.tbl.txt` source file. */ + readonly readText: ( + path: string, + options?: TblText.TextDecodeOptions + ) => Effect.Effect + /** Render and write a `.tbl.txt` source file. */ + readonly writeText: ( + path: string, + table: Tbl.TblTable, + options?: TblText.TextEncodeOptions + ) => Effect.Effect +}>()("cosmonarchy/tbl/TblFiles") { + static readonly layer = Layer.effect( + TblFiles, + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + + const readTbl = Effect.fn("TblFiles.readTbl")(function*(path: string) { + return yield* Tbl.decode(yield* fs.readFile(path)) + }) + + const writeTbl = Effect.fn("TblFiles.writeTbl")( + function*(path: string, table: Tbl.TblTable, options?: Tbl.EncodeOptions) { + const bytes = yield* Tbl.encode(table, options) + yield* fs.writeFile(path, bytes) + return bytes.length + } + ) + + const readText = Effect.fn("TblFiles.readText")( + function*(path: string, options?: TblText.TextDecodeOptions) { + const bytes = yield* fs.readFile(path) + // The GPTP dialect permits raw bytes above 127, so it is parsed from + // bytes; v1 is ASCII by construction and safe to decode as text. + if (options?.dialect === "gptp") return TblText.decodeGptp(bytes) + return yield* TblText.decode(decoder.decode(bytes)) + } + ) + + const writeText = Effect.fn("TblFiles.writeText")( + function*(path: string, table: Tbl.TblTable, options?: TblText.TextEncodeOptions) { + const bytes = encoder.encode(TblText.encode(table, options)) + yield* fs.writeFile(path, bytes) + return bytes.length + } + ) + + return TblFiles.of({ readTbl, writeTbl, readText, writeText }) + }) + ) +} diff --git a/tools/tbl/src/TblText.ts b/tools/tbl/src/TblText.ts new file mode 100644 index 00000000..9b45f91e --- /dev/null +++ b/tools/tbl/src/TblText.ts @@ -0,0 +1,362 @@ +/** + * The `.tbl.txt` source format: one entry per line, in index order. + * + * `.tbl` strings are raw bytes in a legacy code page and are full of Windows + * paths, so backslash escaping would be both lossy and miserable to hand-edit. + * Angle brackets are used instead, and they are the only special characters: + * + * | Token | Meaning | + * | ------- | -------------------------------------------------- | + * | `<0>` | separates the parts of a multi-part entry | + * | `` | the raw byte `N` (decimal, 0-255) - colours, `\n` | + * | `<<` | a literal `<` | + * | `<>` | nothing; marks an otherwise empty line | + * + * Everything else is written literally, so `zerg\avenger.grp` stays exactly + * that. Lines starting with `#` are comments and blank lines are ignored; an + * entry that would begin with `#` or a space, or end with a space, has that + * character written as `<35>` / `<32>` so no editor can quietly eat it. + * + * Encoding is ASCII-only by construction, which keeps the file safe from being + * re-saved as UTF-8. + */ +import { Effect } from "effect" +import { TblTextError } from "./Errors.ts" +import type { TblEntry, TblTable } from "./Tbl.ts" + +const LT = 0x3c +const HASH = 0x23 +const SPACE = 0x20 +const TAB = 0x09 +const TILDE = 0x7e +const BACKSLASH = 0x5c +const LOWER_N = 0x6e + +/** + * Which text dialect a file is written in. + * + * - `v1` - this tool's own `.tbl.txt` format, described above. + * - `gptp` - the format CM-GPTP's `collapse_control_codes` accepts, used by the + * tables the engine already reads as text: `rez/stat.txt` and `arr/sounds.txt`. + * + * They agree on what an entry is - bytes, NUL-separated - and differ only in + * escaping. See {@link decodeEntryGptp} for the exact rules. + */ +export type Dialect = "v1" | "gptp" + +const renderPart = (part: Uint8Array): string => { + let out = "" + for (const byte of part) { + if (byte === LT) out += "<<" + else if (byte >= SPACE && byte <= TILDE) out += String.fromCharCode(byte) + else out += `<${byte}>` + } + return out +} + +/** Render one entry as a single line. Never fails, and never returns `""`. */ +export const encodeEntry = (entry: TblEntry): string => { + let line = entry.map(renderPart).join("<0>") + if (line === "") return "<>" + if (line.startsWith("#")) line = `<${HASH}>` + line.slice(1) + else if (line.startsWith(" ")) line = `<${SPACE}>` + line.slice(1) + if (line.endsWith(" ")) line = line.slice(0, -1) + `<${SPACE}>` + return line +} + +/** Parse one line into an entry. `line` is 1-based, for error messages. */ +export const decodeEntry = Effect.fn("TblText.decodeEntry")( + function*(source: string, line: number): Effect.fn.Return { + const parts: Array = [] + let current: Array = [] + let i = 0 + + while (i < source.length) { + const char = source.charCodeAt(i) + + if (char === LT) { + if (source.charCodeAt(i + 1) === LT) { + current.push(LT) + i += 2 + continue + } + const close = source.indexOf(">", i + 1) + if (close === -1) { + return yield* new TblTextError({ + message: "unterminated escape: `<` with no matching `>` (write a literal `<` as `<<`)", + line, + column: i + 1 + }) + } + const body = source.slice(i + 1, close) + if (body === "") { + i = close + 1 // `<>` contributes nothing + continue + } + if (!/^[0-9]{1,3}$/.test(body)) { + return yield* new TblTextError({ + message: `invalid escape \`<${body}>\`: expected a decimal byte 0-255, \`<<\` or \`<>\``, + line, + column: i + 1 + }) + } + const value = Number(body) + if (value > 0xff) { + return yield* new TblTextError({ + message: `byte ${value} is out of range: expected 0-255`, + line, + column: i + 1 + }) + } + if (value === 0) { + parts.push(Uint8Array.from(current)) + current = [] + } else { + current.push(value) + } + i = close + 1 + continue + } + + if (char > TILDE) { + return yield* new TblTextError({ + message: + `non-ASCII character ${JSON.stringify(source[i])} (U+${ + char.toString(16).toUpperCase().padStart(4, "0") + }): .tbl strings are raw bytes, write it as \`\``, + line, + column: i + 1 + }) + } + if (char < SPACE && char !== TAB) { + return yield* new TblTextError({ + message: `control character ${char} must be written as \`<${char}>\``, + line, + column: i + 1 + }) + } + + current.push(char) + i++ + } + + parts.push(Uint8Array.from(current)) + return parts + } +) + +/** + * Parse one line the way CM-GPTP's `collapse_control_codes` does + * (`hooks/limits/tbl_extender.cpp`), then split it into parts on NUL. + * + * The rules, transcribed from that function: + * + * - `` where the body is entirely decimal digits becomes the byte `N`. + * C++ parses it into a `u32` and narrows with `char(...)`, so values above + * 255 wrap rather than being rejected - replicated here, because the point of + * this decoder is to say what the engine will actually do. + * - Anything else in angle brackets - `<>`, ``, an unclosed `<` - is left + * completely literal. There is no `<<` escape in this dialect. + * - `\n` becomes a newline and `\\` becomes one backslash. Any other backslash + * pair is left alone, so `zerg\avenger.grp` survives but `zerg\night.grp` + * would not. + * - Every other byte is literal, including bytes above 127. + * + * Unlike {@link decode}, there is no comment or blank-line syntax: the engine + * turns *every* line into an entry, so a `#` line would become a `#` string. + */ +export const decodeEntryGptp = (source: Uint8Array): TblEntry => { + const out: Array = [] + let i = 0 + + while (i < source.length) { + const byte = source[i]! + + if (byte === LT) { + const close = source.indexOf(0x3e, i + 1) // '>' + if (close !== -1) { + let digits = close > i + 1 + for (let j = i + 1; j < close; j++) { + const d = source[j]! + if (d < 0x30 || d > 0x39) { + digits = false + break + } + } + if (digits) { + let value = 0 + for (let j = i + 1; j < close; j++) value = value * 10 + (source[j]! - 0x30) + // u32 parse then char() narrowing, exactly as the C++ does. + if (value <= 0xffffffff) { + out.push(value & 0xff) + i = close + 1 + continue + } + } + } + out.push(LT) + i++ + continue + } + + if (byte === BACKSLASH && i + 1 < source.length) { + const next = source[i + 1]! + if (next === LOWER_N) { + out.push(0x0a) + i += 2 + continue + } + if (next === BACKSLASH) { + out.push(BACKSLASH) + i += 2 + continue + } + } + + out.push(byte) + i++ + } + + return splitOnNul(Uint8Array.from(out)) +} + +/** Split a collapsed line into its NUL-separated parts. */ +const splitOnNul = (bytes: Uint8Array): TblEntry => { + const parts: Array = [] + let start = 0 + for (let i = 0; i < bytes.length; i++) { + if (bytes[i] === 0) { + parts.push(bytes.subarray(start, i)) + start = i + 1 + } + } + parts.push(bytes.subarray(start)) + return parts +} + +/** + * Render one entry in the GPTP dialect. + * + * Escaping is chosen to be unambiguous under {@link decodeEntryGptp} rather + * than to match any particular hand-written file byte for byte: + * + * - `<` is written `<60>`, because this dialect has no `<<` and a literal `<` + * followed by digits and `>` would be read back as a control code. + * - `\` is written `\\` only when the next byte would make it an escape (`n` or + * another `\`), so `zerg\avenger.grp` stays readable. + * - Bytes outside printable ASCII are written ``, so output is always ASCII + * and cannot be corrupted by a re-save. The engine accepts raw high bytes too + * - `rez/stat.txt` uses them - but it does not need them. + */ +export const encodeEntryGptp = (entry: TblEntry): string => { + const joined: Array = [] + entry.forEach((part, index) => { + if (index > 0) joined.push(0) + for (const byte of part) joined.push(byte) + }) + + let out = "" + for (let i = 0; i < joined.length; i++) { + const byte = joined[i]! + const next = joined[i + 1] + if (byte === LT) out += "<60>" + else if (byte === BACKSLASH && (next === LOWER_N || next === BACKSLASH)) out += "\\\\" + else if (byte >= SPACE && byte <= TILDE) out += String.fromCharCode(byte) + else out += `<${byte}>` + } + return out +} + +export interface TextEncodeOptions { + /** Prepend the format banner. Defaults to `true`. Ignored for `gptp`. */ + readonly header?: boolean | undefined + /** + * Emit a `# [index]` comment above every entry. Defaults to `false`. + * Ignored for `gptp`, which has no comment syntax. + */ + readonly annotate?: boolean | undefined + /** Which dialect to write. Defaults to `v1`. */ + readonly dialect?: Dialect | undefined +} + +export interface TextDecodeOptions { + /** Which dialect to read. Defaults to `v1`. */ + readonly dialect?: Dialect | undefined +} + +const BANNER = [ + "# tbl-text v1 - StarCraft: Brood War string table source", + "# One entry per line, in index order.", + "# <0> separates the parts of a multi-part entry", + "# raw byte N (decimal 0-255), e.g. <10> newline, <4> colour code", + "# << a literal '<' <> nothing (empty-entry marker)", + "# Lines starting with '#' are comments; blank lines are ignored." +] + +/** Render a whole table as `.tbl.txt` source. */ +export const encode = (table: TblTable, options?: TextEncodeOptions): string => { + if (options?.dialect === "gptp") return encodeGptp(table) + const lines: Array = [] + if (options?.header !== false) lines.push(...BANNER, "") + for (let i = 0; i < table.length; i++) { + if (options?.annotate) lines.push(`# [${i}]`) + lines.push(encodeEntry(table[i]!)) + } + return lines.join("\n") + "\n" +} + +/** + * Render a whole table in the GPTP dialect: one line per entry, nothing else. + * + * There is deliberately no banner and no `--annotate` support here. The engine + * reads every line as an entry, so a comment would silently become a string and + * shift every index after it. + */ +export const encodeGptp = (table: TblTable): string => + table.map(encodeEntryGptp).join("\n") + "\n" + +/** + * Parse a whole file in the GPTP dialect. + * + * Takes bytes rather than a string because this dialect allows raw bytes above + * 127, which have no single correct interpretation as text. Never fails: every + * byte sequence is a valid GPTP source file. + */ +export const decodeGptp = (bytes: Uint8Array): TblTable => { + const lines: Array = [] + let start = 0 + for (let i = 0; i < bytes.length; i++) { + if (bytes[i] === 0x0a) { // '\n' + let end = i + if (end > start && bytes[end - 1] === 0x0d) end-- // '\r' + lines.push(bytes.subarray(start, end)) + start = i + 1 + } + } + // A trailing newline ends the last line rather than starting an empty one, + // matching std::getline. + if (start < bytes.length) { + let end = bytes.length + if (end > start && bytes[end - 1] === 0x0d) end-- + lines.push(bytes.subarray(start, end)) + } + return lines.map(decodeEntryGptp) +} + +/** Parse `.tbl.txt` source into a table. */ +export const decode = Effect.fn("TblText.decode")( + function*(text: string): Effect.fn.Return { + const source = text.startsWith("\uFEFF") ? text.slice(1) : text + const entries: Array = [] + const lines = source.split("\n") + + for (let i = 0; i < lines.length; i++) { + let line = lines[i]! + if (line.endsWith("\r")) line = line.slice(0, -1) + if (line === "" || line.startsWith("#")) continue + entries.push(yield* decodeEntry(line, i + 1)) + } + + return entries + } +) diff --git a/tools/tbl/src/cli.ts b/tools/tbl/src/cli.ts new file mode 100644 index 00000000..8fc5cc5a --- /dev/null +++ b/tools/tbl/src/cli.ts @@ -0,0 +1,325 @@ +#!/usr/bin/env bun +import { BunRuntime, BunServices } from "@effect/platform-bun" +import { Console, Effect, FileSystem, Layer, Option } from "effect" +import { Argument, Command, Flag } from "effect/unstable/cli" +import { TblFiles } from "./TblFiles.ts" +import * as Tbl from "./Tbl.ts" +import * as TblText from "./TblText.ts" + +/** `foo.tbl` -> `foo.tbl.txt`, so a decompile never lands on an unrelated `foo.txt`. */ +const textPathFor = (tbl: string) => `${tbl}.txt` + +/** `foo.tbl.txt` -> `foo.tbl`, `foo.txt` -> `foo.tbl`. */ +const tblPathFor = (text: string) => + text.endsWith(".tbl.txt") + ? text.slice(0, -".txt".length) + : text.endsWith(".txt") + ? `${text.slice(0, -".txt".length)}.tbl` + : `${text}.tbl` + +const resolve = (given: Option.Option, fallback: string) => + Option.getOrElse(given, () => fallback) + +const output = Flag.string("output").pipe( + Flag.withAlias("o"), + Flag.withDescription("Where to write the result"), + Flag.optional +) + +const dialect = Flag.choice("dialect", ["v1", "gptp"]).pipe( + Flag.withDescription( + "Text format: v1 (this tool's .tbl.txt) or gptp (what CM-GPTP's collapse_control_codes reads)" + ), + Flag.withDefault("v1" as const) +) + +const decompile = Command.make( + "decompile", + { + input: Argument.file("input", { mustExist: true }).pipe( + Argument.withDescription("The .tbl file to read") + ), + output, + annotate: Flag.boolean("annotate").pipe( + Flag.withDescription("Emit a `# [index]` comment above every entry"), + Flag.withDefault(false) + ), + bare: Flag.boolean("bare").pipe( + Flag.withDescription("Omit the format banner comment"), + Flag.withDefault(false) + ), + dialect + }, + Effect.fn(function*({ annotate, bare, dialect, input, output }) { + const files = yield* TblFiles + if (dialect === "gptp" && annotate) { + // The engine reads every line as an entry, so a `# [n]` comment would + // become a string and shift every index after it. + yield* Console.error("error: --annotate has no meaning for --dialect gptp, which has no comment syntax") + process.exitCode = 1 + return + } + const target = resolve(output, textPathFor(input)) + const table = yield* files.readTbl(input) + const written = yield* files.writeText(target, table, { annotate, header: !bare, dialect }) + yield* Console.log( + `${input} -> ${target} (${table.length} entries, ${written} bytes${dialect === "gptp" ? ", gptp dialect" : ""})` + ) + }) +).pipe( + Command.withDescription("Read a binary .tbl and write editable .tbl.txt source"), + Command.withAlias("d"), + Command.withExamples([ + { command: "tbl decompile mpq/arr/images.tbl", description: "Writes mpq/arr/images.tbl.txt" }, + { + command: "tbl decompile mpq/rez/stat_txt.tbl --annotate -o stat_txt.txt", + description: "Number every entry, for a table addressed by index" + } + ]) +) + +const compile = Command.make( + "compile", + { + input: Argument.file("input", { mustExist: true }).pipe( + Argument.withDescription("The .tbl.txt source file to read") + ), + output, + dedupe: Flag.boolean("dedupe").pipe( + Flag.withDescription("Point identical entries at one copy of the string data"), + Flag.withDefault(false) + ), + dialect + }, + Effect.fn(function*({ dedupe, dialect, input, output }) { + const files = yield* TblFiles + const target = resolve(output, tblPathFor(input)) + const table = yield* files.readText(input, { dialect }) + const written = yield* files.writeTbl(target, table, { dedupe }) + yield* Console.log(`${input} -> ${target} (${table.length} entries, ${written} bytes)`) + }) +).pipe( + Command.withDescription("Compile .tbl.txt source into a binary .tbl"), + Command.withAlias("c"), + Command.withExamples([ + { command: "tbl compile mpq/arr/images.tbl.txt", description: "Writes mpq/arr/images.tbl" }, + { + command: "tbl compile mpq/rez/stat_txt.tbl.txt --dedupe", + description: "Share duplicate strings to stay under the 64 KiB offset ceiling" + } + ]) +) + +const bytesEqual = (a: Uint8Array, b: Uint8Array) => + a.length === b.length && a.every((byte, i) => byte === b[i]) + +/** Every `.tbl.txt` under `root`, depth first, in a stable order. */ +const sourcesUnder = Effect.fn("sourcesUnder")(function*(root: string) { + const fs = yield* FileSystem.FileSystem + const entries = yield* fs.readDirectory(root, { recursive: true }) + return entries + .filter((entry) => entry.endsWith(".tbl.txt")) + .map((entry) => `${root}/${entry}`) + .sort() +}) + +/** + * Which dialect a source file is written in. + * + * `decompile` writes the `# tbl-text v1` banner unless `--bare`, and the GPTP + * dialect cannot carry a comment at all - every line there is an entry - so the + * banner is an unambiguous marker. Files the engine reads directly + * (`arr/images.tbl.txt`) are GPTP; the rest are v1 build artefacts. + */ +const dialectOf = Effect.fn("dialectOf")(function*(path: string) { + const fs = yield* FileSystem.FileSystem + const head = (yield* fs.readFile(path)).subarray(0, 64) + const text = new TextDecoder("utf-8", { fatal: false }).decode(head) + return text.startsWith("# tbl-text v1") ? "v1" as const : "gptp" as const +}) + +const build = Command.make( + "build", + { + root: Argument.directory("root", { mustExist: true }).pipe( + Argument.withDescription("Directory to search for .tbl.txt sources"), + Argument.withDefault(".") + ), + check: Flag.boolean("check").pipe( + Flag.withDescription("Write nothing; exit non-zero if any .tbl is out of date"), + Flag.withDefault(false) + ), + dedupe: Flag.boolean("dedupe").pipe( + Flag.withDescription("Point identical entries at one copy of the string data"), + Flag.withDefault(false) + ) + }, + Effect.fn(function*({ check, dedupe, root }) { + const fs = yield* FileSystem.FileSystem + const files = yield* TblFiles + const sources = yield* sourcesUnder(root) + + if (sources.length === 0) { + yield* Console.error(`no .tbl.txt sources found under ${root}`) + process.exitCode = 1 + return + } + + let stale = 0 + for (const source of sources) { + const target = tblPathFor(source) + const table = yield* files.readText(source, { dialect: yield* dialectOf(source) }) + + if (!check) { + const written = yield* files.writeTbl(target, table, { dedupe }) + yield* Console.log(` built ${target} (${table.length} entries, ${written} bytes)`) + continue + } + + // Compare against what is actually on disk, so a hand-edited .tbl is + // caught just as surely as a forgotten recompile. + const rebuilt = yield* Tbl.encode(table, { dedupe }) + const current = yield* fs.readFile(target).pipe(Effect.orElseSucceed(() => new Uint8Array())) + if (bytesEqual(current, rebuilt)) { + yield* Console.log(` ok ${target}`) + } else { + yield* Console.error(` STALE ${target} does not match ${source}`) + stale++ + } + } + + if (check && stale > 0) { + yield* Console.error( + `\n${stale} of ${sources.length} table(s) out of date - run \`bun run tbl build ${root}\`` + ) + process.exitCode = 1 + return + } + yield* Console.log(`\n${sources.length} table(s) ${check ? "up to date" : "built"}`) + }) +).pipe( + Command.withDescription("Compile every .tbl.txt under a directory into its binary .tbl"), + Command.withAlias("b"), + Command.withExamples([ + { command: "tbl build mpq", description: "Recompile every table in the mpq tree" }, + { + command: "tbl build mpq --check", + description: "Fail if any committed .tbl is out of date - use in a pre-commit hook" + } + ]) +) + +const verify = Command.make( + "verify", + { + input: Argument.file("input", { mustExist: true }).pipe( + Argument.withDescription("The .tbl file to round-trip") + ) + }, + Effect.fn(function*({ input }) { + const files = yield* TblFiles + const original = yield* files.readTbl(input) + const text = TblText.encode(original) + const reparsed = yield* TblText.decode(text) + const rebuilt = yield* Tbl.encode(reparsed) + const source = yield* Tbl.encode(original) + + if (!bytesEqual(source, rebuilt)) { + yield* Console.error(`${input}: FAIL - re-encoding does not reproduce the original bytes`) + process.exitCode = 1 + return + } + yield* Console.log(`${input}: ok - ${original.length} entries survive .tbl -> .tbl.txt -> .tbl byte for byte`) + }) +).pipe( + Command.withDescription("Check that a .tbl survives a full round trip unchanged") +) + +const info = Command.make( + "info", + { + input: Argument.file("input", { mustExist: true }).pipe( + Argument.withDescription("The .tbl file to inspect") + ) + }, + Effect.fn(function*({ input }) { + // Read the offset table off the file itself: a re-encode would describe + // what this tool would write, not what is actually on disk. + const fs = yield* FileSystem.FileSystem + const bytes = yield* fs.readFile(input) + const table = yield* Tbl.decode(bytes) + + const headerEnd = 2 + table.length * 2 + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength) + const distinct = new Set() + let highest = headerEnd + for (let i = 0; i < table.length; i++) { + const offset = view.getUint16(2 + i * 2, true) + distinct.add(offset) + if (offset > highest) highest = offset + } + + const parts = new Map() + const control = new Set() + for (const entry of table) { + parts.set(entry.length, (parts.get(entry.length) ?? 0) + 1) + for (const part of entry) { + for (const byte of part) if (byte < 0x20 || byte > 0x7e) control.add(byte) + } + } + + const shared = table.length - distinct.size + + yield* Console.log( + [ + input, + ` entries ${table.length}${shared === 0 ? "" : ` (${shared} sharing string data)`}`, + ` size ${bytes.length} bytes (${headerEnd} header + ${bytes.length - headerEnd} strings)`, + ` highest offset ${highest} of ${Tbl.MAX_OFFSET} (${Tbl.MAX_OFFSET - highest} bytes of headroom)`, + ` parts per entry ${ + [...parts.entries()].sort(([a], [b]) => a - b).map(([n, count]) => `${count}x${n}`).join(", ") + }`, + ` non-ascii bytes ${ + control.size === 0 ? "none" : [...control].sort((a, b) => a - b).map((b) => `<${b}>`).join(" ") + }` + ].join("\n") + ) + }) +).pipe( + Command.withDescription("Show entry counts, offset headroom and byte usage for a .tbl") +) + +const tbl = Command.make("tbl").pipe( + Command.withDescription("Compile between StarCraft: Brood War .tbl string tables and .tbl.txt source"), + Command.withSubcommands([decompile, compile, build, verify, info]) +) + +tbl.pipe( + Command.run({ version: "1.0.0" }), + Effect.catchTags({ + TblBinaryError: (error) => + Effect.gen(function*() { + yield* Console.error( + `error: ${error.message}${error.offset === undefined ? "" : ` (at byte ${error.offset})`}` + ) + process.exitCode = 1 + }), + TblEncodeError: (error) => + Effect.gen(function*() { + yield* Console.error(`error: ${error.message}`) + process.exitCode = 1 + }), + TblTextError: (error) => + Effect.gen(function*() { + yield* Console.error(`error: line ${error.line}, column ${error.column}: ${error.message}`) + process.exitCode = 1 + }), + PlatformError: (error) => + Effect.gen(function*() { + yield* Console.error(`error: ${error.message}`) + process.exitCode = 1 + }) + }), + Effect.provide(Layer.provideMerge(TblFiles.layer, BunServices.layer)), + BunRuntime.runMain +) diff --git a/tools/tbl/src/index.ts b/tools/tbl/src/index.ts new file mode 100644 index 00000000..094b26e8 --- /dev/null +++ b/tools/tbl/src/index.ts @@ -0,0 +1,4 @@ +export * from "./Errors.ts" +export * as Tbl from "./Tbl.ts" +export * from "./TblFiles.ts" +export * as TblText from "./TblText.ts" diff --git a/tools/tbl/test/Tbl.test.ts b/tools/tbl/test/Tbl.test.ts new file mode 100644 index 00000000..0963de53 --- /dev/null +++ b/tools/tbl/test/Tbl.test.ts @@ -0,0 +1,131 @@ +import { describe, expect, it } from "bun:test" +import * as Tbl from "../src/Tbl.ts" +import { entry, failWith, show, succeed } from "./helpers.ts" + +/** Hand-assemble a .tbl so the tests do not lean on the encoder. */ +const build = (blocks: Array) => { + const header = 2 + blocks.length * 2 + const data = blocks.join("") + const bytes = new Uint8Array(header + data.length) + const view = new DataView(bytes.buffer) + view.setUint16(0, blocks.length, true) + let cursor = header + blocks.forEach((block, i) => { + view.setUint16(2 + i * 2, cursor, true) + for (let j = 0; j < block.length; j++) bytes[cursor + j] = block.charCodeAt(j) + cursor += block.length + }) + return bytes +} + +/** Assemble a .tbl from an explicit offset table plus raw string data. */ +const buildWithOffsets = (offsets: Array, data: string) => { + const header = 2 + offsets.length * 2 + const bytes = new Uint8Array(header + data.length) + const view = new DataView(bytes.buffer) + view.setUint16(0, offsets.length, true) + offsets.forEach((offset, i) => view.setUint16(2 + i * 2, offset, true)) + for (let j = 0; j < data.length; j++) bytes[header + j] = data.charCodeAt(j) + return bytes +} + +describe("decode", () => { + it("reads a simple table", async () => { + const table = await succeed(Tbl.decode(build(["one\0", "two\0"]))) + expect(show(table)).toEqual([["one"], ["two"]]) + }) + + it("keeps the NUL-separated parts of a multi-part entry together", async () => { + const table = await succeed(Tbl.decode(build(["Maverick\0*\0Ground Units\0", "Egg\0"]))) + expect(show(table)).toEqual([["Maverick", "*", "Ground Units"], ["Egg"]]) + }) + + it("gives the last entry everything up to the end of the file", async () => { + const table = await succeed(Tbl.decode(build(["a\0", "hotkey\0button text\0"]))) + expect(show(table)).toEqual([["a"], ["hotkey", "button text"]]) + }) + + it("reads an empty string entry", async () => { + expect(show(await succeed(Tbl.decode(build(["\0", "x\0"]))))).toEqual([[""], ["x"]]) + }) + + it("returns an empty table when the count is zero", async () => { + expect(await succeed(Tbl.decode(Uint8Array.from([0, 0])))).toEqual([]) + }) + + it("resolves shared offsets to the same entry", async () => { + // Entries 0 and 1 point at one copy of "one", as a deduped table would. + const bytes = buildWithOffsets([8, 8, 12], "one\0three\0") + expect(show(await succeed(Tbl.decode(bytes)))).toEqual([["one"], ["one"], ["three"]]) + }) + + it("keeps trailing bytes no offset points at, as stat_txt.tbl relies on", async () => { + const bytes = buildWithOffsets([4], "a\0button text\0") + expect(show(await succeed(Tbl.decode(bytes)))).toEqual([["a", "button text"]]) + }) + + it("rejects a file too short to hold a header", async () => { + const error = await failWith(Tbl.decode(Uint8Array.from([1]))) + expect(error.message).toContain("truncated header") + }) + + it("rejects a truncated offset table", async () => { + const error = await failWith(Tbl.decode(Uint8Array.from([9, 0, 0, 0]))) + expect(error.message).toContain("truncated offset table") + }) + + it("rejects an offset that points outside the string data", async () => { + const bytes = build(["one\0"]) + new DataView(bytes.buffer).setUint16(2, 999, true) + const error = await failWith(Tbl.decode(bytes)) + expect(error.message).toContain("outside the string data") + expect(error.offset).toBe(999) + }) + + it("rejects an entry that is not NUL-terminated", async () => { + const error = await failWith(Tbl.decode(build(["one", "two\0"]))) + expect(error.message).toContain("not NUL-terminated") + }) +}) + +describe("encode", () => { + it("reproduces the exact bytes it decoded", async () => { + const bytes = build(["Maverick\0*\0Ground Units\0", "\0", "Egg\0"]) + expect(await succeed(Tbl.encode(await succeed(Tbl.decode(bytes))))).toEqual(bytes) + }) + + it("writes a well-formed header", async () => { + const bytes = await succeed(Tbl.encode([entry("ab"), entry("c")])) + const view = new DataView(bytes.buffer) + expect(view.getUint16(0, true)).toBe(2) + expect(view.getUint16(2, true)).toBe(6) + expect(view.getUint16(4, true)).toBe(9) // 6 + "ab\0" + expect(bytes.length).toBe(11) // 6 header + "ab\0" + "c\0" + }) + + it("treats an entry with no parts as a single empty string", async () => { + expect(show(await succeed(Tbl.decode(await succeed(Tbl.encode([[]])))))).toEqual([[""]]) + }) + + it("shares identical entries when asked, and still decodes the same", async () => { + const table = Array.from({ length: 64 }, () => entry("Ground Units")) + const plain = await succeed(Tbl.encode(table)) + const shared = await succeed(Tbl.encode(table, { dedupe: true })) + expect(shared.length).toBeLessThan(plain.length) + expect(show(await succeed(Tbl.decode(shared)))).toEqual(show(table)) + }) + + it("refuses to write an offset a u16 cannot address", async () => { + const table = Array.from({ length: 100 }, (_, i) => entry(`${i}`.padEnd(1000, "x"))) + const error = await failWith(Tbl.encode(table)) + expect(error.message).toContain("past the 65535") + expect(error.message).toContain("--dedupe") + expect(error.offset).toBeGreaterThan(Tbl.MAX_OFFSET) + }) + + it("refuses to write more entries than the count field holds", async () => { + const table = Array.from({ length: Tbl.MAX_ENTRIES + 1 }, () => entry("")) + const error = await failWith(Tbl.encode(table)) + expect(error.message).toContain("exceeds the 65535") + }) +}) diff --git a/tools/tbl/test/TblText.test.ts b/tools/tbl/test/TblText.test.ts new file mode 100644 index 00000000..1b8f95ed --- /dev/null +++ b/tools/tbl/test/TblText.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from "bun:test" +import * as TblText from "../src/TblText.ts" +import { entry, failWith, show, succeed } from "./helpers.ts" + +describe("encodeEntry", () => { + it("leaves backslashes and ordinary ASCII alone", () => { + expect(TblText.encodeEntry(entry("zerg\\avenger.grp"))).toBe("zerg\\avenger.grp") + }) + + it("joins the parts of a multi-part entry with <0>", () => { + expect(TblText.encodeEntry(entry("Terran Maverick", "*", "Ground Units"))) + .toBe("Terran Maverick<0>*<0>Ground Units") + }) + + it("escapes colour codes, newlines and high bytes as ", () => { + const bytes = Uint8Array.from([0x04, 0x45, 0x01, 0x0a, 0xa4]) + expect(TblText.encodeEntry([bytes])).toBe("<4>E<1><10><164>") + }) + + it("escapes a literal < as <<", () => { + expect(TblText.encodeEntry(entry("a", () => { + expect(TblText.encodeEntry(entry(""))).toBe("<>") + expect(TblText.encodeEntry(entry("", ""))).toBe("<0>") + }) + + it("protects a leading # from being read back as a comment", () => { + expect(TblText.encodeEntry(entry("#1 unit"))).toBe("<35>1 unit") + }) + + it("protects leading and trailing spaces from editors that trim them", () => { + expect(TblText.encodeEntry(entry(" pad "))).toBe("<32>pad<32>") + expect(TblText.encodeEntry(entry(" "))).toBe("<32>") + }) +}) + +describe("decodeEntry", () => { + const parse = (line: string) => succeed(TblText.decodeEntry(line, 1)) + + it("round-trips every construct the encoder emits", async () => { + const cases = [ + entry("zerg\\avenger.grp"), + entry("Terran Maverick", "*", "Ground Units"), + entry(""), + entry("", ""), + entry("#1 unit"), + entry(" pad "), + entry("a at the edges as empty parts", async () => { + expect(show([await parse("<0>a<0>")])).toEqual([["", "a", ""]]) + }) + + it("reads <> as nothing anywhere in the line", async () => { + expect(show([await parse("a<>b")])).toEqual([["ab"]]) + }) + + it("rejects an unterminated escape", async () => { + const error = await failWith(TblText.decodeEntry("abc<12", 7)) + expect(error.message).toContain("unterminated escape") + expect(error.line).toBe(7) + expect(error.column).toBe(4) + }) + + it("rejects a non-numeric escape", async () => { + const error = await failWith(TblText.decodeEntry("a", 1)) + expect(error.message).toContain("invalid escape ``") + expect(error.column).toBe(2) + }) + + it("rejects a byte above 255", async () => { + const error = await failWith(TblText.decodeEntry("<256>", 1)) + expect(error.message).toContain("out of range") + }) + + it("rejects non-ASCII characters, which would be ambiguous bytes", async () => { + const error = await failWith(TblText.decodeEntry("café", 3)) + expect(error.message).toContain("non-ASCII") + expect(error.message).toContain("U+00E9") + expect(error.column).toBe(4) + }) +}) + +describe("decode", () => { + it("skips comments and blank lines", async () => { + const table = await succeed(TblText.decode("# a comment\n\nfirst\n\n# another\nsecond\n")) + expect(show(table)).toEqual([["first"], ["second"]]) + }) + + it("accepts CRLF line endings and a UTF-8 BOM", async () => { + const table = await succeed(TblText.decode("# c\r\nfirst\r\nsecond\r\n")) + expect(show(table)).toEqual([["first"], ["second"]]) + }) + + it("reports the real line number of a bad escape", async () => { + const error = await failWith(TblText.decode("# banner\nfine\nbroken<9z>\n")) + expect(error.line).toBe(3) + }) + + it("emits a banner by default and omits it when bare", () => { + const table = [entry("a")] + expect(TblText.encode(table)).toStartWith("# tbl-text v1") + expect(TblText.encode(table, { header: false })).toBe("a\n") + expect(TblText.encode(table, { header: false, annotate: true })).toBe("# [0]\na\n") + }) + + it("round-trips a whole table through the text form", async () => { + const table = [entry("first"), entry("a", "b"), entry(""), entry("# hash")] + expect(show(await succeed(TblText.decode(TblText.encode(table))))).toEqual(show(table)) + }) +}) diff --git a/tools/tbl/test/dialect.test.ts b/tools/tbl/test/dialect.test.ts new file mode 100644 index 00000000..5f3960cf --- /dev/null +++ b/tools/tbl/test/dialect.test.ts @@ -0,0 +1,133 @@ +/** + * The GPTP dialect: the text format CM-GPTP's `collapse_control_codes` + * (`hooks/limits/tbl_extender.cpp`) accepts, which the engine already reads for + * `rez/stat.txt` and `arr/sounds.txt`. + * + * These tests are the reference for that parser. If they and the C++ ever + * disagree, one of the two is a bug. + */ +import { describe, expect, it } from "bun:test" +import * as Tbl from "../src/Tbl.ts" +import * as TblText from "../src/TblText.ts" +import { failWith, show, succeed } from "./helpers.ts" + +const repo = `${import.meta.dir}/../../..` +const bytes = (text: string) => Buffer.from(text, "latin1") +const one = (line: string) => show([TblText.decodeEntryGptp(bytes(line))])[0]! + +describe("GPTP dialect - control codes", () => { + it("collapses into the raw byte N", () => { + expect(one("a<65>b")).toEqual(["aAb"]) + expect(one("<4>Tank Mode")).toEqual(["\x04Tank Mode"]) + }) + + it("treats <0> as a part separator", () => { + expect(one("e<0>Tank Mode")).toEqual(["e", "Tank Mode"]) + }) + + it("leaves a trailing <0> as an empty final part", () => { + expect(one("Spider Mines<0>")).toEqual(["Spider Mines", ""]) + }) + + it("leaves non-numeric angle brackets completely literal", () => { + // There is no `<<` escape in this dialect, and `<>` is not an empty marker. + expect(one("")).toEqual([""]) + expect(one("<>")).toEqual(["<>"]) + expect(one("a { + // from_chars parses into u32, then `line[i] = char(control_char)` truncates, + // so <256> collapses to a NUL and therefore splits the entry. + expect(one("<256>")).toEqual(["", ""]) + expect(one("<321>")).toEqual([String.fromCharCode(321 & 0xff)]) // 65 -> "A" + }) + + it("handles the two backslash escapes and leaves other pairs alone", () => { + expect(one("a\\nb")).toEqual(["a\nb"]) + expect(one("a\\\\b")).toEqual(["a\\b"]) + expect(one("zerg\\avenger.grp")).toEqual(["zerg\\avenger.grp"]) + }) + + it("keeps raw bytes above 127", () => { + expect(one("\xa4passive")).toEqual(["\xa4passive"]) + }) +}) + +describe("GPTP dialect - whole files", () => { + it("has no comment or blank-line syntax: every line is an entry", () => { + const table = TblText.decodeGptp(bytes("a\n\n# not a comment\nb\n")) + expect(show(table)).toEqual([["a"], [""], ["# not a comment"], ["b"]]) + }) + + it("does not turn a trailing newline into an extra entry", () => { + expect(TblText.decodeGptp(bytes("a\nb\n")).length).toBe(2) + expect(TblText.decodeGptp(bytes("a\nb")).length).toBe(2) + }) + + it("tolerates CRLF", () => { + expect(show(TblText.decodeGptp(bytes("a\r\nb\r\n")))).toEqual([["a"], ["b"]]) + }) +}) + +describe("GPTP dialect - encoding is unambiguous", () => { + const roundTrips = (entry: Tbl.TblEntry) => + show([TblText.decodeEntryGptp(bytes(TblText.encodeEntryGptp(entry)))])[0] + + it("escapes a literal < so it cannot be read back as a control code", () => { + const entry = [Buffer.from("<10>literal")] + expect(TblText.encodeEntryGptp(entry)).toBe("<60>10>literal") + expect(roundTrips(entry)).toEqual(["<10>literal"]) + }) + + it("escapes a backslash only when it would otherwise become an escape", () => { + expect(TblText.encodeEntryGptp([Buffer.from("zerg\\avenger.grp")])).toBe("zerg\\avenger.grp") + expect(TblText.encodeEntryGptp([Buffer.from("zerg\\night.grp")])).toBe("zerg\\\\night.grp") + expect(roundTrips([Buffer.from("zerg\\night.grp")])).toEqual(["zerg\\night.grp"]) + }) + + it("writes pure ASCII even for high bytes", () => { + const text = TblText.encodeEntryGptp([Buffer.from("\xa4passive", "latin1")]) + expect(text).toBe("<164>passive") + expect([...text].every((c) => c.charCodeAt(0) <= 0x7e)).toBe(true) + }) +}) + +describe("GPTP dialect - the shipped text tables", () => { + const load = async (path: string) => + TblText.decodeGptp(new Uint8Array(await Bun.file(`${repo}/${path}`).arrayBuffer())) + + // stat.txt has grown past the table it was cloned from: the strings added + // beyond index 1813 (weapon names, mostly) live only in the text file, which + // is what the engine reads. stat_txt.tbl is the frozen legacy artefact. + it("rez/stat.txt has outgrown the 1813 entries stat_txt.tbl indexes", async () => { + const text = await load("mpq/rez/stat.txt") + const binary = await succeed( + Tbl.decode(new Uint8Array(await Bun.file(`${repo}/mpq/rez/stat_txt.tbl`).arrayBuffer())) + ) + expect(text.length).toBe(2999) + expect(binary.length).toBe(1813) + }) + + it("rez/stat.txt is far too large to be a u16 .tbl - the text loader is load-bearing", async () => { + const table = await load("mpq/rez/stat.txt") + const error = await failWith(Tbl.encode(table)) + expect(error._tag).toBe("TblEncodeError") + expect(error.message).toContain("past the 65535") + }) + + for (const path of ["mpq/rez/stat.txt", "mpq/arr/sounds.txt"]) { + it(`${path} survives gptp -> table -> gptp with an identical table`, async () => { + const table = await load(path) + const reparsed = TblText.decodeGptp(bytes(TblText.encodeGptp(table))) + expect(show(reparsed)).toEqual(show(table)) + }) + + it(`${path} converts losslessly to tbl-text v1 and back`, async () => { + const table = await load(path) + const back = await succeed(TblText.decode(TblText.encode(table))) + expect(show(back)).toEqual(show(table)) + }) + } +}) diff --git a/tools/tbl/test/helpers.ts b/tools/tbl/test/helpers.ts new file mode 100644 index 00000000..b1810404 --- /dev/null +++ b/tools/tbl/test/helpers.ts @@ -0,0 +1,16 @@ +import { Effect } from "effect" + +const encoder = new TextEncoder() + +/** Build an entry from ASCII part strings. */ +export const entry = (...parts: Array) => parts.map((part) => encoder.encode(part)) + +/** Run an effect that is expected to succeed. */ +export const succeed = (effect: Effect.Effect) => Effect.runPromise(effect) + +/** Run an effect that is expected to fail, returning the error it failed with. */ +export const failWith = (effect: Effect.Effect): Promise => Effect.runPromise(Effect.flip(effect)) + +/** Readable view of a decoded table, for assertions. */ +export const show = (table: ReadonlyArray>) => + table.map((e) => e.map((part) => Array.from(part).map((b) => String.fromCharCode(b)).join(""))) diff --git a/tools/tbl/test/roundtrip.test.ts b/tools/tbl/test/roundtrip.test.ts new file mode 100644 index 00000000..e615ff7b --- /dev/null +++ b/tools/tbl/test/roundtrip.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from "bun:test" +import * as Tbl from "../src/Tbl.ts" +import * as TblText from "../src/TblText.ts" +import { show, succeed } from "./helpers.ts" + +const repo = `${import.meta.dir}/../../..` + +const FIXTURES = [ + "mpq/arr/images.tbl", + "mpq/arr/portdata.tbl", + "mpq/rez/gluHist.tbl", + "mpq/rez/help_txt.tbl", + "mpq/rez/network.tbl", + "mpq/rez/stat_txt.tbl", + "mpq/rez/tips.tbl", + "mpq/rez/unitnames.tbl" +] + +describe("round trip against the shipped game data", () => { + for (const fixture of FIXTURES) { + it(`${fixture} survives .tbl -> .tbl.txt -> .tbl byte for byte`, async () => { + const original = new Uint8Array(await Bun.file(`${repo}/${fixture}`).arrayBuffer()) + + const table = await succeed(Tbl.decode(original)) + const text = TblText.encode(table) + const reparsed = await succeed(TblText.decode(text)) + const rebuilt = await succeed(Tbl.encode(reparsed)) + + expect(rebuilt.length).toBe(original.length) + expect(rebuilt).toEqual(original) + }) + + it(`${fixture} decompiles to pure ASCII`, async () => { + const original = new Uint8Array(await Bun.file(`${repo}/${fixture}`).arrayBuffer()) + const text = TblText.encode(await succeed(Tbl.decode(original))) + const offender = [...text].find((char) => char.charCodeAt(0) > 0x7e) + expect(offender).toBeUndefined() + }) + } + + it("renders images.tbl as plain, unescaped GRP paths", async () => { + const original = new Uint8Array(await Bun.file(`${repo}/mpq/arr/images.tbl`).arrayBuffer()) + const table = await succeed(Tbl.decode(original)) + const lines = TblText.encode(table, { header: false }).split("\n") + expect(lines[0]).toBe("zerg\\avenger.grp") + expect(lines[1]).toBe("protoss\\mshield.los") + }) + + it("renders unitnames.tbl multi-part entries with <0> separators", async () => { + const original = new Uint8Array(await Bun.file(`${repo}/mpq/rez/unitnames.tbl`).arrayBuffer()) + const table = await succeed(Tbl.decode(original)) + const lines = TblText.encode(table, { header: false }).split("\n") + expect(lines[0]).toBe("Terran Maverick<0>*<0>Ground Units") + expect(lines[5]).toBe("Terran Phalanx<0>Tank Mode<0>Ground Units") + }) + + it("renders stat_txt.tbl colour codes and newlines as ", async () => { + const original = new Uint8Array(await Bun.file(`${repo}/mpq/rez/stat_txt.tbl`).arrayBuffer()) + const table = await succeed(Tbl.decode(original)) + const line = TblText.encode(table, { header: false }).split("\n")[338]! + expect(line).toBe( + "e<0><4>Tank Mode <1>(<3>E<1>)<10>Returns the Phalanx's weapon and mobility to normal." + ) + }) + + it("dedupe reclaims stat_txt.tbl headroom without changing a single string", async () => { + const original = new Uint8Array(await Bun.file(`${repo}/mpq/rez/stat_txt.tbl`).arrayBuffer()) + const table = await succeed(Tbl.decode(original)) + const deduped = await succeed(Tbl.encode(table, { dedupe: true })) + + expect(deduped.length).toBeLessThan(original.length) + expect(show(await succeed(Tbl.decode(deduped)))).toEqual(show(table)) + }) + + it("reports how little offset headroom stat_txt.tbl has left", async () => { + const original = new Uint8Array(await Bun.file(`${repo}/mpq/rez/stat_txt.tbl`).arrayBuffer()) + const view = new DataView(original.buffer) + const count = view.getUint16(0, true) + let highest = 0 + for (let i = 0; i < count; i++) highest = Math.max(highest, view.getUint16(2 + i * 2, true)) + expect(highest).toBeLessThanOrEqual(Tbl.MAX_OFFSET) + expect(Tbl.MAX_OFFSET - highest).toBeLessThan(64) // 26 bytes at time of writing + }) +}) diff --git a/tools/tbl/tsconfig.json b/tools/tbl/tsconfig.json new file mode 100644 index 00000000..8b45a9e9 --- /dev/null +++ b/tools/tbl/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleResolution": "bundler", + "moduleDetection": "force", + "types": ["bun"], + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +}