Files
aowow/setup/tools/filegen/img-maps.ss.php
Sarjuuk 615c203c7a Setup/img-maps
* script cleanup
 * fixed subzone generation and made color less garish
 * fixed alphamap generation and alphamapcheck not pointing to the same path
 * fixed padding UtilityScript args with unexpected var types
2024-06-21 16:57:33 +02:00

876 lines
38 KiB
PHP

<?php
if (!defined('AOWOW_REVISION'))
die('illegal access');
if (!CLI)
die('not in cli mode');
CLISetup::registerSetup("build", new class extends SetupScript
{
use TrComplexImage;
protected $info = array(
'img-maps' => [[ ], CLISetup::ARGV_PARAM, 'Generate zone and continental maps and the corresponding \'zones\' datasets.' ],
/* 1 */ 'spawnmaps' => [['1'], CLISetup::ARGV_OPTIONAL, 'Fallback to generate alpha masks for each zone to match creature and gameobject spawn points.'],
/* 2 */ 'subzones' => [['2'], CLISetup::ARGV_OPTIONAL, 'Generate additional area maps with highlighting for subzones (optional; skipped by default)' ],
/* 4 */ 'skip-zones' => [['3'], CLISetup::ARGV_OPTIONAL, 'Prevent default output of zone maps.' ]
);
protected $useGlobalStrings = true;
protected $dbcSourceFiles = ['worldmapoverlay', 'worldmaparea', 'dungeonmap'];
protected $requiredDirs = ['datasets/'];
private const M_MAPS = (1 << 0);
private const M_SPAWNS = (1 << 1);
private const M_SUBZONES = (1 << 2);
private $modeMask = self::M_SPAWNS | self::M_MAPS;
private const SPAWNMAP_WH = 1000; // it is square
private const MAP_W = 1002;
private const MAP_H = 668;
private const A_THRESHOLD = 95; // alpha threshold to define subZones: set it too low and you have unspawnable areas inside a zone; set it too high and the border regions overlap
private const COLOR_WHITE = [255, 255, 255]; // rgb
private const COLOR_BLACK = [ 0, 0, 0]; // rgb
private const COLOR_SUBZONE = [ 0, 230, 255, 74]; // rgba - note: rgb is 0-255, a is 0-127
private const AREA_FLAG_DEFAULT_FLOOR_TERRAIN = 0x004; // Default Dungeon Floor is Terrain
private const AREA_FLAG_NO_DEFAULT_FLOOR = 0x100; // Don't use Default Dungeon Floor (typically 1)
private const CONTINENTS = [0, 1, 530, 571]; // Map.dbc/id
private const DEST_DIRS = array(
['static/images/wow/maps/%snormal/', 488, 325],
['static/images/wow/maps/%soriginal/', 0, 0], // 1002, 668
['static/images/wow/maps/%ssmall/', 224, 149],
['static/images/wow/maps/%szoom/', 772, 515]
);
private const TILEORDER = array(
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
);
private const MAP_FILE_PATTERN = '/((\w{4})\/interface\/worldmap(?:\/microdungeon\/([^\/]+))?\/([^\/]+)\/)(\4)(?:(\d{1,2})_)?(\d{1,2})\.(?:blp|png)/i';
// src, resourcePath, localized, [tileOrder], [[dest, destW, destH]]
private $genSteps = array(
self::M_MAPS => ['WorldMap/', null, true, self::TILEORDER, self::DEST_DIRS ],
self::M_SPAWNS => ['WorldMap/', null, true, self::TILEORDER, [['cache/alphaMaps/', 0, 0]]],
self::M_SUBZONES => ['WorldMap/', null, true, self::TILEORDER, self::DEST_DIRS ]
);
private $progress = 0;
private $wmOverlays = [];
private $dmFloorData = [];
private $wmAreas = [];
private $multiLevelZones = [];
private $mapFiles = []; // [nameINT][floorIdx][loc][tileIdx] => filePath
private $microDungeons = [];
public function __construct()
{
$this->imgPath = CLISetup::$srcDir.$this->imgPath;
$this->maxExecTime = ini_get('max_execution_time');
// init directories
foreach ($this->genSteps as [, , , , $outInfo])
{
foreach ($outInfo as $dir)
{
if (strpos($dir[0], '%s') === false)
$this->requiredDirs[] = $dir[0];
else
foreach (CLISetup::$localeIds as $l)
$this->requiredDirs[] = sprintf($dir[0], strtolower(Util::$localeStrings[$l]).'/');
}
}
}
public function generate() : bool
{
// find out what to generate
$opts = array_slice(array_keys($this->info), 1);
$getO = CLISetup::getOpt(...$opts);
$mask = 0x0;
if ($getO['spawnmaps'])
$mask |= self::M_SPAWNS;
if ($getO['subzones'])
$mask |= self::M_SUBZONES;
if (!$getO['skip-zones'])
$mask |= self::M_MAPS;
// unless manually prompted drop spawnmap generation if 90% of spawns have core generated area info
$npcPct = DB::World()->selectCell('SELECT SUM(IF(`zoneId` > 0, 1, 0)) / COUNT(*) FROM creature') ?? 0;
$goPct = DB::World()->selectCell('SELECT SUM(IF(`zoneId` > 0, 1, 0)) / COUNT(*) FROM gameobject') ?? 0;
if (!($mask & self::M_SPAWNS) && $npcPct > 0.9 && $goPct > 0.9)
$this->modeMask &= ~self::M_SPAWNS;
$this->modeMask = $mask ?: $this->modeMask;
if (!$this->modeMask) // why would you do this..?
return true;
// removed unused genSteps
foreach ($this->genSteps as $idx => $_)
if (!($idx & $this->modeMask))
unset($this->genSteps[$idx]);
if (!$this->checkSourceDirs())
{
CLI::write('[img-maps] One or more source directories are missing.', CLI::LOG_ERROR);
$this->success = false;
return false;
}
sleep(2);
if ($this->prepare())
{
$this->buildMaps();
$this->buildZonesFile();
}
return $this->success;
}
private function buildMapsFUTURE() : void
{
$sumFloors = array_sum(array_column($this->dmFloorData, 1));
$sumAreas = count($this->wmAreas);
$sumMaps = count(CLISetup::$localeIds) * ($sumAreas + $sumFloors);
CLI::write('Processing '.$sumAreas.' zone maps and '.$sumFloors.' dungeon maps from Interface/WorldMap/ for locale: '.Lang::concat(array_intersect_key(Util::$localeStrings, array_flip(CLISetup::$localeIds))));
/* todo: retrain brain and generate maps by given files and GlobalStrings. Then assign dbc data to them not the other way round like it is now.
foreach ($this->mapFiles as $name => [$floors, $isMultilevel])
{
// skip redundant data of a microDungeons
if (in_array($name, $this->microDungeons))
continue;
$this->wmAreas = $this->wmAreas[$name] ?? [];
if (!$this->wmAreas)
{
CLI::write('[img-maps] no WMA data for map file '.CLI::bold($name), CLI::LOG_WARN);
continue;
}
$wmaId = $this->wmAreas['id'];
$zoneId = $this->wmAreas['areaId'];
$mapId = $this->wmAreas['mapId'];
$flags = $this->wmAreas['flags'] ?? 0; // flags added in 4.x
if ($isMultilevel)
$this->multiLevelZones[$zoneId] = [];
// TODO
// - Ahn'Kahet (4494) has a secondary map file, that is not referenced in DungeonMap.dbc but looks nice. Lets manually reference it.
// if (isset($floorData[4494]))
// $floorData[4494][1] = 2;
if ($zoneId == 206)
var_dump($floors);
foreach ($floors as $locId => [$floorData, $basePath])
{
ksort($floorData);
$resOverlay = null;
if (!$isMultilevel)
$resOverlay = $this->generateOverlay($wmaId, $name, $basePath);
// create spawn-maps if wanted
if ($resOverlay && $this->modeMask & self::M_SPAWNS)
{
$outFile = $this->genSteps[self::M_SPAWNS][self::$GEN_IDX_DEST_INFO][0][0] . $zoneId . '.png';
if (!$this->buildSpawnMap($resOverlay, $outFile))
$this->success = false;
}
foreach ($floorData as $floorIdx => $tileData)
{
$outFile = $zoneId;
// naming of the base floor file is a bit wonky. unsure when the -0 suffix should be implicit or explicit
// just note, that the floor names from GlobalStrings.lua always have a '0' as base level suffix
if (!$floorIdx && $isMultilevel && !($flags & self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN))
CLI::write('[img-maps] zone '.$name.' is multilevel and has base level map file, but is not flagged for use', CLI::LOG_INFO);
if ($isMultilevel && !$floorIdx)
{
if (in_array($mapId, self::CONTINENTS))
$outFile .= '-0';
else if ($this->wmAreas['defaultDungeonMapId'] < 0)
$outFile .= '-0';
// else
// implicit -0
}
else if ($isMultilevel)
$outFile .= '-'.$floorIdx;
if ($isMultilevel)
$this->multiLevelZones[$zoneId][$floorIdx] = $outFile;
foreach ($tileData as $tileIdx => $filePath)
{
}
}
}
if ($isMultilevel)
$this->multiLevelZones[$zoneId] = array_values($this->multiLevelZones[$zoneId]);
if ($this->modeMask & self::M_SUBZONES)
{
// get subzones for mapFile from wmaData and apply overlays
}
}
*/
foreach (CLISetup::$localeIds as $progressLoc => $l)
{
// source for mapFiles
$mapSrcDir = '';
if ($this->modeMask & self::M_SPAWNS)
$mapSrcDir = $this->genSteps[self::M_SPAWNS][1][$l] ?? '';
if (!$mapSrcDir && $this->modeMask & self::M_SUBZONES)
$mapSrcDir = $this->genSteps[self::M_SUBZONES][1][$l] ?? '';
if (!$mapSrcDir)
$mapSrcDir = $this->genSteps[self::M_MAPS][1][$l] ?? '';
if (!$mapSrcDir)
{
$this->success = false;
CLI::write(' - no suitable localized map files found for locale '.$l, CLI::LOG_ERROR);
continue;
}
foreach ($this->wmAreas as $progressArea => $areaEntry)
{
$curMap = $progressArea + count($this->wmAreas) * $progressLoc;
$this->status = ' - ' . str_pad($curMap.'/'.($sumMaps), 10) . str_pad('('.number_format($curMap * 100 / $sumMaps, 2).'%)', 9);
$wmaId = $areaEntry['id'];
$zoneId = $areaEntry['areaId'];
$mapId = $areaEntry['mapId'];
$textureStr = $areaEntry['nameINT'];
$flags = $areaEntry['flags'] ?? 0; // flags added in 4.x
[$floorStr, $nFloors] = $this->dmFloorData[in_array($mapId, self::CONTINENTS) ? -$wmaId : $mapId] ?? ['', 0];
if ($nFloors && !isset($this->multiLevelZones))
$this->multiLevelZones[$zoneId] = [];
CLI::write(
str_pad('['.$areaEntry['areaId'].']', 7) .
str_pad($areaEntry['nameINT'], 22) .
str_pad('Overlays: '.count($this->wmOverlays[$areaEntry['id']] ?? []), 14) .
str_pad('Dungeon Maps: '.($nFloors + ((($flags ?? 0) & self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN) ? 1 : 0)), 18)
);
$srcPath = $mapSrcDir.'/'.$textureStr;
if (!CLISetup::fileExists($srcPath))
{
$this->success = false;
CLI::write('worldmap file '.$srcPath.' missing for selected locale '.Util::$localeStrings[$l], CLI::LOG_ERROR);
continue;
}
$resOverlay = null;
// zone has overlays (is in open world; is not multilevel)
if (isset($this->wmOverlays[$wmaId]))
{
$resOverlay = $this->generateOverlay($wmaId, $srcPath);
// create spawn-maps if wanted
if ($this->modeMask & self::M_SPAWNS)
$this->buildSpawnMap($resOverlay, $zoneId);
}
if (!($this->modeMask & self::M_MAPS))
continue;
// check, if the current zone is multiLeveled
$floors = [0];
if ($floorStr)
$floors = array_merge($floors, explode(' ', $floorStr));
// - Ahn'Kahet (4494) has a secondary map file, that is not referenced in DungeonMap.dbc but looks nice. Lets manually reference it.
if ($zoneId == 4494)
$floors[] = 2;
$resMap = null;
foreach ($floors as $floorIdx)
{
ini_set('max_execution_time', $this->maxExecTime);
$file = $srcPath.'/'.$textureStr;
// todo: Dalaran [4395] has no level 0 but is not skipped here
if (!$floorIdx && !($flags & self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN) && !in_array($mapId, self::CONTINENTS))
continue;
if ($nFloors && ($floorIdx || $flags & self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN))
$this->multiLevelZones[$zoneId][$floorIdx] = $zoneId . '-' . $floorIdx;
if ($floorIdx)
$file .= $floorIdx . '_';
$doSkip = 0x0;
$outFile = [];
foreach (self::DEST_DIRS as $sizeIdx => [$path, $width, $height])
{
$outFile[$sizeIdx] = sprintf($path, strtolower(Util::$localeStrings[$l]).'/') . $zoneId;
/* dataset 'zones' requires that ...
* 3959 - Black Temple: starts with empty floor suffix
* 4075 - Sunwell: starts with empty floor suffix
* 4723 - Map 650 CoT: 5-man reuses raid map (649) but only the upper floor. Check DungeonMap.dbc
*/
if ($nFloors && ($floorIdx || $flags & self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN))
$outFile[$sizeIdx] .= '-'.$floorIdx;
$outFile[$sizeIdx] .= '.jpg';
if (!CLISetup::getOpt('force') && file_exists($outFile[$sizeIdx]))
{
CLI::write($this->status.' - file '.$outFile[$sizeIdx].' was already processed', CLI::LOG_BLANK, true, true);
$doSkip |= (1 << $sizeIdx);
}
}
if ($doSkip == 0xF)
continue;
$resMap = $this->assembleImage($file, self::TILEORDER, self::MAP_W, self::MAP_H);
if (!$resMap)
{
CLI::write(' - could not create image resource for zone '.$zoneId.($nFloors ? ' floor '.$floorIdx : ''), CLI::LOG_ERROR);
$this->success = false;
continue;
}
if ($resOverlay && !$floorIdx)
{
imagecopymerge($resMap, $resOverlay, 0, 0, 0, 0, imagesx($resOverlay), imagesy($resOverlay), 100);
imagedestroy($resOverlay);
}
// create map
if ($this->modeMask & self::M_MAPS)
{
foreach (self::DEST_DIRS as $sizeIdx => [, $width, $height])
{
if ($doSkip & (1 << $sizeIdx))
continue;
if (!$this->writeImageFile($resMap, $outFile[$sizeIdx], $width ?: self::MAP_W, $height ?: self::MAP_H))
$this->success = false;
}
}
}
// also create subzone-maps
if ($resMap && isset($this->wmOverlays[$wmaId]) && $this->modeMask & self::M_SUBZONES)
$this->buildSubZones($resMap, $wmaId, $l);
if ($resMap)
imagedestroy($resMap);
// this takes a while; ping mysql just in case
DB::Aowow()->selectCell('SELECT 1');
}
}
}
private function prepare() : bool
{
$this->wmOverlays = DB::Aowow()->select('SELECT *, `worldMapAreaId` AS ARRAY_KEY, `id` AS ARRAY_KEY2 FROM dbc_worldmapoverlay WHERE `textureString` <> ""');
$this->wmAreas = DB::Aowow()->select('SELECT `id`, `mapId`, `areaId`, UPPER(`nameINT`) AS `nameINT`, IF(`areaId`, `areaId`, -`id`) AS ARRAY_KEY FROM dbc_worldmaparea');
$this->dmFloorData = DB::Aowow()->select('SELECT IF(`mapId` IN (?a), -`worldMapAreaId`, `mapId`) AS ARRAY_KEY, GROUP_CONCAT(DISTINCT `floor` SEPARATOR " ") AS "0", COUNT(DISTINCT `floor`) AS "1" FROM dbc_dungeonmap WHERE `worldMapAreaId` <> 0 GROUP BY ARRAY_KEY', self::CONTINENTS);
if (!$this->wmOverlays || !$this->wmAreas || !$this->dmFloorData)
{
CLI::write('[img-maps] - could not read required dbc files: WorldMapArea.dbc ['.count($this->wmAreas ?: []).' entries]; WorldMapOverlay.dbc ['.count($this->wmOverlays ?: []).'] entries; DungeonMap.dbc ['.count($this->dmFloorData ?: []).' entries]', CLI::LOG_ERROR);
$this->success = false;
return false;
}
// DM fixups...
// unpack + sort floors
array_walk($this->dmFloorData, function (&$x) { $x[0] = explode(' ', $x[0]); sort($x[0]); });
// move Dalaran from Howling Fjord to .. well .. Dalaran
$this->dmFloorData[-4395] = $this->dmFloorData[-495];
unset($this->dmFloorData[-495]);
// "custom" - show second level of Ahn'Kahet not shown but present in-game
$this->dmFloorData[619][0][] = 2;
$this->dmFloorData[619][1]++;
// WMA fixups...
foreach ($this->wmAreas as &$a)
{
// flags added in 4.x but required for 3.3.5. Where are they? Derived from defaultDungeonMapId (also refered to as defaultDungeonFloor) being < 0 ?
// no idea, hardcode this shit
switch ($a['areaId'])
{ // i deem the missing '-0' a mistake > v < this will not be perpetuated
case 4273: // Ulduar > base + 5 > 4273: ['4273-0', '4273-1', '4273-2', '4273-3', '4273-4', '4273-5']
case 4075: // SunwellPlateau > base + 1 > 4075: ['4075', '4075-1'],
case 3959: // BlackTemple > base + 7 > 3959: ['3959', '3959-1', '3959-2', '3959-3', '3959-4', '3959-5', '3959-6', '3959-7'],
$a['flags'] = self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN;
break;
case 4100: // CoTStratholme > base + 1 > 4100: ['4100-1', '4100-2'],
$a['flags'] = self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN | self::AREA_FLAG_NO_DEFAULT_FLOOR;
break;
default:
$a['flags'] = $a['flags'] ?? 0; // flags added in 4.x
}
if ($a['areaId'])
continue;
switch ($a['id'])
{
case 13: $a['areaId'] = -6; break; // Kalimdor
case 14: $a['areaId'] = -3; break; // Eastern Kingdoms
case 466: $a['areaId'] = -2; break; // Outland
case 485: $a['areaId'] = -5; break; // Northrend
}
}
$this->wmAreas[-1] = ['id' => -1, 'areaId' => -1, 'flags' => 0x0, 'mapId' => 0, 'nameINT' => 'World'];
$this->wmAreas[-4] = ['id' => -4, 'areaId' => -4, 'flags' => 0x0, 'mapId' => 0, 'nameINT' => 'Cosmic'];
ksort($this->wmAreas); // just so we can sift through the log more easily
/*
i should be walking through interface/worldmap first and THEN check the worldmaparea / dungeonmap from the file pattern
floorIdx is optional and per map. (e.g. continents share their floors and yes continents can have dungeon maps)
> <locStr>/interface/worldmap/<wma.NameINT>/<wma.NameINT>(_<dm.floorIdx>)<tileIdx>.blp
> <locStr>/interface/worldmap/microdungeon/<parentWma.nameINT>/<wma.NameINT>/<wma.NameINT>(_<dm.floorIdx>)<tileIdx>.blp
microdungeons (5.x+?) may be redundant with regluar map files.
e.g.:
> enGB/interface/worldmap/microdungeon/durotar/burningbladecoven/burningbladecoven8_12.blp
from nameInt "durotar" we get wmaId = 4, areaTableId = 14 and mapId = 1 (floorIdx = 8 from file string)
with mapId and floor (and wmaId) we get the coordinates from dungeonmap.dbc
thus the map file name is: <areaTableId>-<floorIdx>.png > 14-8.png
and the floor is named: DUNGEON_FLOOR_<wma.nameINT><floorIdx> > DUNGEON_FLOOR_DUROTAR8 (Aquelarre del Filo Ardiente) *nyak nyak nyak*
note: some map file may have no floorIdx but the tileIdx is still separated by an underscore. Those files should be ignored.
*/
/* FUTURE
foreach (CLISetup::filesInPath(self::MAP_FILE_PATTERN, true) as $file)
{
if (!preg_match(self::MAP_FILE_PATTERN, $file, $m))
continue;
[, $basePath, $locStr, $mdParent, $nameINT, $nameINT, $floorIdx, $tileIdx] = $m;
$loc = CLISetup::$expectedPaths[strtolower(substr($locStr, 0, 2)).strtoupper(substr($locStr, 2))] ?? LOCALE_EN;
if ($mdParent)
$this->microDungeons[] = strtolower($nameINT);
$key = strtolower($mdParent ?: $nameINT);
$this->mapFiles[$key][0][$loc][0][$floorIdx ?: 0][$tileIdx] = $file;
$this->mapFiles[$key][0][$loc][1] = $basePath;
$this->mapFiles[$key][1] = ($this->mapFiles[$key][1] ?? false) ?: (($floorIdx ?: 0) > 1);
}
*/
return true;
}
private function buildMaps() : void
{
$sumFloors = array_sum(array_column($this->dmFloorData, 1));
$sumAreas = count($this->wmAreas);
$sumMaps = count(CLISetup::$localeIds) * ($sumAreas + $sumFloors);
CLI::write('[img-maps] Processing '.$sumAreas.' zone maps and '.$sumFloors.' dungeon maps from Interface/WorldMap/ for locale: '.Lang::concat(array_intersect_key(Util::$localeStrings, array_flip(CLISetup::$localeIds))));
foreach (CLISetup::$localeIds as $l)
{
// source for mapFiles
$mapSrcDir = '';
if ($this->modeMask & self::M_SPAWNS)
$mapSrcDir = $this->genSteps[self::M_SPAWNS][1][$l] ?? '';
if (!$mapSrcDir && $this->modeMask & self::M_SUBZONES)
$mapSrcDir = $this->genSteps[self::M_SUBZONES][1][$l] ?? '';
if (!$mapSrcDir)
$mapSrcDir = $this->genSteps[self::M_MAPS][1][$l] ?? '';
if (!$mapSrcDir)
{
CLI::write('[img-maps] - No suitable localized map files found for locale ['.$l.': '.Util::$localeStrings[$l].'].', CLI::LOG_ERROR);
$this->success = false;
continue;
}
foreach ($this->wmAreas as $areaEntry)
{
$resOverlay = null;
$resMap = null;
$wmaId = $areaEntry['id'];
$zoneId = $areaEntry['areaId'];
$mapId = $areaEntry['mapId'];
$textureStr = $areaEntry['nameINT'];
$flags = $areaEntry['flags'];
[$dmFloors, $nFloors] = $this->dmFloorData[in_array($mapId, self::CONTINENTS) ? -$zoneId : $mapId] ?? [[0], 0];
$this->progress += ($nFloors ?: 1) + ($flags & self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN ? 1 : 0);
$this->status = ' - ' . str_pad($this->progress.'/'.($sumMaps), 10) . str_pad('('.number_format($this->progress * 100 / $sumMaps, 2).'%)', 9);
// includes base level...
if ($flags & self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN)
{
array_unshift($dmFloors, 0); // 0 => 0, 1 => 1, etc.
$nFloors++;
// .. which is not set in dbc 0 => 1, 1 => 2, etc.
if ($flags & self::AREA_FLAG_NO_DEFAULT_FLOOR)
$dmFloors = array_combine($dmFloors, array_map(function ($x) { return ++$x; }, $dmFloors));
}
else if ($dmFloors != [0]) // 1 => 1, 2 => 2, etc.
$dmFloors = array_combine($dmFloors, $dmFloors);
CLI::write(
'['.Util::$localeStrings[$l].'] ' .
str_pad('['.$areaEntry['areaId'].']', 7) .
str_pad($areaEntry['nameINT'], 22) .
str_pad('Overlays: '.count($this->wmOverlays[$areaEntry['id']] ?? []), 14) .
str_pad('Dungeon Maps: '.$nFloors, 18)
);
$srcPath = $mapSrcDir.'/'.$textureStr;
if (!CLISetup::fileExists($srcPath))
{
CLI::write('[img-maps] - WorldMap file path '.$srcPath.' missing for selected locale '.Util::$localeStrings[$l], CLI::LOG_ERROR);
$this->success = false;
continue;
}
$srcPath .= '/';
// zone has overlays (is in open world; is not multilevel)
if (isset($this->wmOverlays[$wmaId]) && ($this->modeMask & (self::M_MAPS | self::M_SPAWNS | self::M_SUBZONES)))
{
$resOverlay = $this->generateOverlay($wmaId, $srcPath);
// create spawn-maps if wanted
if ($resOverlay && ($this->modeMask & self::M_SPAWNS))
$this->buildSpawnMap($resOverlay, $zoneId);
}
// check if we can create base map anyway
$png = $srcPath.$textureStr.'1.png';
$blp = $srcPath.$textureStr.'1.blp';
$hasBaseMap = CLISetup::fileExists($blp) || CLISetup::fileExists($png);
foreach ($dmFloors as $srcFloorIdx => $outFloorIdx)
{
ini_set('max_execution_time', $this->maxExecTime);
$doSkip = 0x0;
$outPaths = [];
$srcFile = $srcPath.$textureStr;
$outFile = $zoneId;
if (!$srcFloorIdx && !$hasBaseMap)
{
CLI::write('[img-maps] - Zone has no base floor, but is referenced with base floor in dmFloors.', CLI::LOG_WARN);
continue;
}
if ($srcFloorIdx)
$srcFile .= $srcFloorIdx.'_';
if ($nFloors > 1)
if ($outFloorIdx || $flags & self::AREA_FLAG_DEFAULT_FLOOR_TERRAIN)
$outFile .= '-'.$outFloorIdx;
if ($nFloors > 1)
$this->multiLevelZones[$zoneId][$outFile] = $outFile;
if (!($this->modeMask & (self::M_MAPS | self::M_SUBZONES)))
continue;
foreach (self::DEST_DIRS as $sizeIdx => [$path, $width, $height])
{
$outPaths[$sizeIdx] = sprintf($path, strtolower(Util::$localeStrings[$l]).'/') . $outFile . '.jpg';
if (!CLISetup::getOpt('force') && file_exists($outPaths[$sizeIdx]))
{
CLI::write($this->status.' - file '.$outPaths[$sizeIdx].' was already processed', CLI::LOG_BLANK, true, true);
$doSkip |= (1 << $sizeIdx);
}
}
// can't skip map creation if we are to generate subzones later. although they may already exist and get skipped anyway *shrug*
if ($doSkip == 0xF && !($this->modeMask & self::M_SUBZONES))
continue;
$resMap = $this->assembleImage($srcFile, self::TILEORDER, self::MAP_W, self::MAP_H);
if (!$resMap)
{
CLI::write('[img-maps] - Could not create image resource for '.($nFloors ? 'floor '.$srcFloorIdx : 'base level'), CLI::LOG_ERROR);
$this->success = false;
continue;
}
if ($resOverlay && !$nFloors)
{
imagecopymerge($resMap, $resOverlay, 0, 0, 0, 0, imagesx($resOverlay), imagesy($resOverlay), 100);
imagedestroy($resOverlay);
}
// create map
if ($this->modeMask & self::M_MAPS)
{
foreach (self::DEST_DIRS as $sizeIdx => [, $width, $height])
{
if ($doSkip & (1 << $sizeIdx))
continue;
if (!$this->writeImageFile($resMap, $outPaths[$sizeIdx], $width ?: self::MAP_W, $height ?: self::MAP_H))
$this->success = false;
}
}
}
// also create subzone-maps
if ($resMap && isset($this->wmOverlays[$wmaId]) && $this->modeMask & self::M_SUBZONES)
$this->buildSubZones($resMap, $wmaId, $l);
if ($resMap)
imagedestroy($resMap);
// this takes a while; ping mysql just in case
DB::Aowow()->selectCell('SELECT 1');
}
}
}
private function buildZonesFile() : void
{
$areaNames = array_combine(
array_column($this->wmAreas, 'areaId'),
array_map(function ($x) { return strtoupper($x); }, array_column($this->wmAreas, 'nameINT'))
);
if ($this->multiLevelZones)
{
ksort($this->multiLevelZones);
$this->multiLevelZones = array_map('array_values', $this->multiLevelZones);
}
else
{
CLI::write('[img-maps] No data fetched from either WorldMapArea.dbc or DungeonMap.dbc. Multilevel zones will not display.', CLI::LOG_ERROR);
$this->success = false;
}
$zoneAreas = [];
// careful: nameINT may end in a number and have > 9 floors attached. see: KARAZHAN17, ULDUAR771
foreach (CLISetup::searchGlobalStrings('/^DUNGEON_FLOOR_([a-z_]+(?:\d\d)?)(\d{1,2})\s=\s\"(.+)\";$/i') as $lId => [$_, $nameINT, $floor, $nameLOC])
{
// yes, multiple zones can point to the same map files
if ($zoneIds = array_keys($areaNames, $nameINT))
{
foreach ($zoneIds as $zId)
if (isset($this->multiLevelZones[$zId]))
$zoneAreas[$lId][$zId][$floor] = $nameLOC;
}
else
CLI::write('[img-maps] ['.$nameINT.'] from GlobalStrings.lua not found in WorldMapArea.dbc', CLI::LOG_WARN);
}
foreach (CLISetup::$locales as $lId => $loc)
{
Lang::load($lId);
// "custom" - show second level of Ahn'Kahet not shown but present in-game
if (isset($zoneAreas[$lId][4494]))
$zoneAreas[$lId][4494][2] = Lang::maps('floorN', [2]);
foreach ($zoneAreas[$lId] as $zoneId => $floorData)
{
$nStrings = count($floorData);
$nFloors = count($this->multiLevelZones[$zoneId] ?? []);
if ($nStrings == $nFloors)
continue;
// todo: just note for now, try to compensate later?
CLI::write('[img-maps] ['.$loc.'] '.str_pad('['.$zoneId.']', 7).'floor count mismatch between GlobalStrings: '.$nStrings.' and image files: '.$nFloors, CLI::LOG_WARN);
}
ksort($zoneAreas[$lId]);
$zoneAreas[$lId] = array_map('array_values', $zoneAreas[$lId]);
// don't convert numbers to int in json
$toFile = "Mapper.multiLevelZones = ".Util::toJSON($this->multiLevelZones, 0x0).";\n\n";
$toFile .= "var g_zone_areas = ".Util::toJSON($zoneAreas[$lId]).";";
$file = 'datasets/'.$loc.'/zones';
if (!CLISetup::writeFile($file, $toFile))
$this->success = false;
}
}
private function buildSpawnMap(/*GdImage*/ $resOverlay, int $zoneId) : void
{
// GdImage: < 8.0 resource; >= 8.0 object
if (gettype($resOverlay) != 'resource' && gettype($resOverlay) != 'object')
{
CLI::write('[img-maps] - no GdImage passed to buildSpawnMap', CLI::LOG_ERROR);
$this->success = false;
return;
}
$outFile = $this->genSteps[self::M_SPAWNS][self::$GEN_IDX_DEST_INFO][0][0] . $zoneId . '.png';
if (!CLISetup::getOpt('force') && file_exists($outFile))
{
CLI::write($this->status.' - file '.$outFile.' was already processed', CLI::LOG_BLANK, true, true);
return;
}
$tmp = imagecreate(self::SPAWNMAP_WH, self::SPAWNMAP_WH);
$cbg = imagecolorallocate($tmp, ...self::COLOR_WHITE);
$cfg = imagecolorallocate($tmp, ...self::COLOR_BLACK);
for ($y = 0; $y < self::SPAWNMAP_WH; $y++)
{
for ($x = 0; $x < self::SPAWNMAP_WH; $x++)
{
$a = imagecolorat($resOverlay, ($x * self::MAP_W) / self::SPAWNMAP_WH, ($y * self::MAP_H) / self::SPAWNMAP_WH) >> 24;
imagesetpixel($tmp, $x, $y, $a < self::A_THRESHOLD ? $cfg : $cbg);
}
}
imagecolordeallocate($tmp, $cbg);
imagecolordeallocate($tmp, $cfg);
if (!$this->writeImageFile($tmp, $outFile, self::SPAWNMAP_WH, self::SPAWNMAP_WH))
$this->success = false;
}
private function buildSubZones(/*GdImage*/ $resMap, int $wmaId, int $locId) : void
{
// GdImage: < 8.0 resource; >= 8.0 object
if (gettype($resMap) != 'resource' && gettype($resMap) != 'object')
{
CLI::write('[img-maps] - no GdImage passed to buildSubZones()', CLI::LOG_ERROR);
$this->success = false;
return;
}
foreach ($this->wmOverlays[$wmaId] as &$row)
{
$doSkip = 0x0;
$outFile = [];
foreach (self::DEST_DIRS as $sizeIdx => [$path, , ])
{
$outFile[$sizeIdx] = sprintf($path, strtolower(Util::$localeStrings[$locId]).'/') . $row['areaTableId'].'.jpg';
if (!CLISetup::getOpt('force') && file_exists($outFile[$sizeIdx]))
{
CLI::write($this->status.' - file '.$outFile[$sizeIdx].' was already processed', CLI::LOG_BLANK, true, true);
$doSkip |= (1 << $sizeIdx);
}
}
if ($doSkip == 0xF)
continue;
$subZone = imagecreatetruecolor(self::MAP_W, self::MAP_H);
imagecopy($subZone, $resMap, 0, 0, 0, 0, imagesx($resMap), imagesy($resMap));
imagecopy($subZone, $row['maskimage'], $row['x'], $row['y'], 0, 0, imagesx($row['maskimage']), imagesy($row['maskimage']));
foreach (self::DEST_DIRS as $sizeIdx => [, $width, $height])
{
if ($doSkip & (1 << $sizeIdx))
continue;
if (!$this->writeImageFile($subZone, $outFile[$sizeIdx], $width ?: self::MAP_W, $height ?: self::MAP_H))
$this->success = false;
}
imagedestroy($subZone);
}
}
private function generateOverlay(int $wmaId, string $basePath) // : ?GdImage
{
if (!isset($this->wmOverlays[$wmaId]))
return null;
$resOverlay = $this->createAlphaImage(self::MAP_W, self::MAP_H);
foreach ($this->wmOverlays[$wmaId] as &$row)
{
$i = 1;
$y = 0;
while ($y < $row['h'])
{
$x = 0;
while ($x < $row['w'])
{
$img = $this->loadImageFile($basePath . $row['textureString'] . $i);
if (!$img)
{
CLI::write('[img-maps] - overlay tile ' . $basePath . $row['textureString'] . $i . '.blp missing.', CLI::LOG_ERROR);
break 2;
}
imagecopy($resOverlay, $img, $row['x'] + $x, $row['y'] + $y, 0, 0, imagesx($img), imagesy($img));
// prepare subzone image
if ($this->modeMask & self::M_SUBZONES)
{
if (!isset($row['maskimage']))
{
$row['maskimage'] = $this->createAlphaImage($row['w'], $row['h']);
$row['maskcolor'] = imagecolorallocatealpha($row['maskimage'], ...self::COLOR_SUBZONE);
}
for ($my = 0; $my < imagesy($img); $my++)
for ($mx = 0; $mx < imagesx($img); $mx++)
if ((imagecolorat($img, $mx, $my) >> 24) < self::A_THRESHOLD)
imagesetpixel($row['maskimage'], $x + $mx, $y + $my, $row['maskcolor']);
}
imagedestroy($img);
$x += 256;
$i++;
}
$y += 256;
}
}
return $resOverlay;
}
});
?>