[[ ], CLISetup::ARGV_PARAM, 'Compiles map points from dbc and world db.' ], 'creatures' => [['1'], CLISetup::ARGV_OPTIONAL, '...just the creature positions.' ], 'objects' => [['2'], CLISetup::ARGV_OPTIONAL, '...just the gameobject positions.' ], 'soundemitter' => [['3'], CLISetup::ARGV_OPTIONAL, '...just the soundemitter positions.' ], 'areatrigger' => [['4'], CLISetup::ARGV_OPTIONAL, '...just the areatrigger and teleporter positions.'], 'instances' => [['5'], CLISetup::ARGV_OPTIONAL, '...just the instance portal positions.' ], 'waypoints' => [['6'], CLISetup::ARGV_OPTIONAL, '...just the creature waypoints.' ] ); protected $dbcSourceFiles = ['worldmaparea', 'map', 'taxipathnode', 'soundemitters', 'areatrigger', 'areatable']; protected $worldDependency = ['creature', 'creature_addon', 'gameobject', 'gameobject_template', 'vehicle_accessory', 'vehicle_accessory_template', 'script_waypoint', 'waypoints', 'waypoint_data', 'smart_scripts', 'areatrigger_teleport']; protected $setupAfter = [['dungeonmap', 'worldmaparea', 'zones'], ['img-maps']]; private $transports = []; private $overrideData = []; private $steps = array( 0x01 => ['creature', Type::NPC, false, '`creature` spawns', ], 0x02 => ['gameobject', Type::OBJECT, false, '`gameobject` spawns', ], 0x04 => ['soundemitter', Type::SOUND, false, 'SoundEmitters.dbc positions', ], 0x08 => ['areatrigger', Type::AREATRIGGER, false, 'AreaTrigger.dbc positions and teleporter endpoints' ], 0x10 => ['instances', Type::ZONE, false, 'Map.dbc instance portals positions' ], 0x20 => ['waypoints', Type::NPC, true, 'NPC waypoints from `script_waypoint`, `waypoints` & `waypoint_data`'], ); public function generate(array $ids = []) : bool { /*****************************/ /* find out what to generate */ /*****************************/ $opts = array_slice(array_keys($this->info), 1); $getOpt = CLISetup::getOpt(...$opts); $todoMask = 0x0; if ($getOpt['creatures']) $todoMask |= 0x01; if ($getOpt['objects']) $todoMask |= 0x02; if ($getOpt['soundemitter']) $todoMask |= 0x04; if ($getOpt['areatrigger']) $todoMask |= 0x08; if ($getOpt['instances']) $todoMask |= 0x10; if ($getOpt['waypoints']) $todoMask |= 0x20; if ($todoMask) foreach ($this->steps as $idx => $_) if (!($todoMask & $idx)) unset($this->steps[$idx]); /*********************************/ /* selectively truncate old data */ /*********************************/ if (!$todoMask || ($todoMask & 0x1F) == 0x1F) DB::Aowow()->query('TRUNCATE TABLE ?_spawns'); else foreach ($this->steps as $idx => [, $type, $isWP, ]) if (($idx & $todoMask) && !$isWP) DB::Aowow()->query('DELETE FROM ?_spawns WHERE `type` = ?d', $type); if (!$todoMask || ($todoMask & 0x20)) DB::Aowow()->query('TRUNCATE TABLE ?_creature_waypoints'); /**************************/ /* offsets for transports */ /**************************/ $this->transports = DB::World()->selectCol('SELECT `data0` AS `pathId`, `data6` AS ARRAY_KEY FROM gameobject_template WHERE `type` = ?d AND `data6` <> 0', OBJECT_MO_TRANSPORT); foreach ($this->transports as &$t) $t = DB::Aowow()->selectRow('SELECT `posX`, `posY`, `mapId` FROM dbc_taxipathnode tpn WHERE tpn.`pathId` = ?d AND `nodeIdx` = 0', $t); /*********************/ /* get override data */ /*********************/ $this->overrideData = DB::Aowow()->select('SELECT `type` AS ARRAY_KEY, `typeGuid` AS ARRAY_KEY2, `areaId`, `floor` FROM ?_spawns_override'); /**************/ /* perform... */ /**************/ foreach (array_values($this->steps) as $i => [$generator, $type, $isWP, $comment]) { $time = new Timer(500); $sum = 0; $lastOverride = 0; $nSteps = count($this->steps); $queryResult = $this->$generator(); $queryTotal = count($queryResult); $queryTotalLen = strlen($queryTotal); CLI::write(' - '.$generator); foreach ($queryResult as $spawn) { $notice = ''; $sum++; if ($time->update()) CLI::write(' * '.($i + 1).'/'.$nSteps.': '. CLI::bold($comment).' - '.sprintf('%'.$queryTotalLen.'d / %d (%4.1f%%)', $sum, $queryTotal, round(100 * $sum / $queryTotal, 1)), CLI::LOG_BLANK, true, true); $point = $this->transformPoint($spawn, $type, $notice); if ($notice && $lastOverride != $spawn['guid']) { CLI::write($notice, CLI::LOG_INFO); $time->reset(); $lastOverride = $spawn['guid']; // don't spam this for waypoints } if (!$point) { CLI::write('[points] '.str_pad('['.$spawn['guid'].']', 9).' '.(isset($spawn['point']) ? 'with path/point ['.$spawn['npcOrPath'].'; '.$spawn['point'].'] ' : '').'could not be matched to displayable area [A:'.($spawn['areaId'] ?? 0).'; X:'.$spawn['posY'].'; Y:'.$spawn['posX'].']', CLI::LOG_WARN); $time->reset(); continue; } $set = array_merge($spawn, $point); if (!$isWP) // REPLACE: because there is bogus data where one path may be assigned to multiple npcs { unset($set['map']); DB::Aowow()->query('REPLACE INTO ?_spawns (?#) VALUES (?a)', array_keys($set), array_values($set)); } else { unset($set['map'], $set['guid']); DB::Aowow()->query('REPLACE INTO ?_creature_waypoints (?#) VALUES (?a)', array_keys($set), array_values($set)); } } } /*****************************/ /* spawn vehicle accessories */ /*****************************/ if ($todoMask & 0x01) // only when creature is set { // get vehicle template accessories $accessories = DB::World()->select( 'SELECT vta.`accessory_entry` AS `typeId`, c.`guid`, vta.`entry`, COUNT(1) AS `nSeats` FROM vehicle_template_accessory vta LEFT JOIN creature c ON c.`id` = vta.`entry` GROUP BY `accessory_entry`, c.`guid` UNION SELECT va.`accessory_entry` AS `typeId`, va.`guid`, 0 AS `entry`, COUNT(1) AS `nSeats` FROM vehicle_accessory va GROUP BY `accessory_entry`, va.`guid`' ); // accessories may also be vehicles (e.g. "Kor'kron Infiltrator" is seated on "Kor'kron Suppression Turret" is seated on "Kor'kron Troop Transport") // so we will retry finding a spawned vehicle if none were found on the previous pass and a change occured $vGuid = 0; // not really used, but we need some kind of index $n = 0; $matches = -1; while ($matches) { $matches = 0; foreach ($accessories as $idx => $data) { $vehicles = []; if ($data['guid']) // vehicle already spawned $vehicles = DB::Aowow()->select('SELECT s.`areaId`, s.`posX`, s.`posY`, s.`floor` FROM ?_spawns s WHERE s.`guid` = ?d AND s.`type` = ?d', $data['guid'], Type::NPC); else if ($data['entry']) // vehicle on unspawned vehicle action $vehicles = DB::Aowow()->select('SELECT s.`areaId`, s.`posX`, s.`posY`, s.`floor` FROM ?_spawns s WHERE s.`typeId` = ?d AND s.`type` = ?d', $data['entry'], Type::NPC); if ($vehicles) { $matches++; foreach ($vehicles as $v) // if there is more than one vehicle, its probably due to overlapping zones for ($i = 0; $i < $data['nSeats']; $i++) DB::Aowow()->query('INSERT INTO ?_spawns (`guid`, `type`, `typeId`, `respawn`, `spawnMask`, `phaseMask`, `areaId`, `floor`, `posX`, `posY`, `pathId`) VALUES (?d, ?d, ?d, 0, 0, 1, ?d, ?d, ?f, ?f, 0)', --$vGuid, Type::NPC, $data['typeId'], $v['areaId'], $v['floor'], $v['posX'], $v['posY']); unset($accessories[$idx]); } } if ($matches) CLI::write(' * assigned '.$matches.' accessories on '.++$n.'. pass on vehicle accessories', CLI::LOG_BLANK, true, true); } if ($accessories) CLI::write('[spawns] - '.count($accessories).' accessories could not be fitted onto a spawned vehicle.', CLI::LOG_WARN); } /********************************/ /* restrict difficulty displays */ /********************************/ DB::Aowow()->query('UPDATE ?_spawns s, dbc_worldmaparea wma, dbc_map m SET s.`spawnMask` = 0 WHERE s.`areaId` = wma.`areaId` AND wma.`mapId` = m.`id` AND m.`areaType` IN (0, 3, 4)'); return true; } private function creature() : array { // [guid, type, typeId, map, posX, posY [, respawn, spawnMask, phaseMask, areaId, floor, pathId]] return DB::World()->select( 'SELECT c.`guid`, ?d AS `type`, c.`id` AS `typeId`, c.`map`, c.`position_x` AS `posX`, c.`position_y` AS `posY`, c.`spawntimesecs` AS `respawn`, c.`spawnMask`, c.`phaseMask`, c.`zoneId` AS `areaId`, IFNULL(ca.`path_id`, 0) AS `pathId` FROM creature c LEFT JOIN creature_addon ca ON ca.guid = c.guid', Type::NPC ); } private function gameobject() : array { // [guid, type, typeId, map, posX, posY [, respawn, spawnMask, phaseMask, areaId, floor, pathId]] return DB::World()->select( 'SELECT `guid`, ?d AS `type`, `id` AS `typeId`, `map`, `position_x` AS `posX`, `position_y` AS `posY`, `spawntimesecs` AS `respawn`, `spawnMask`, `phaseMask`, `zoneId` AS `areaId` FROM gameobject', Type::OBJECT ); } private function soundemitter() : array { // [guid, type, typeId, map, posX, posY [, respawn, spawnMask, phaseMask, areaId, floor, pathId]] return DB::Aowow()->select( 'SELECT `id` AS `guid`, ?d AS `type`, `soundId` AS `typeId`, `mapId` AS `map`, `posX`, `posY` FROM dbc_soundemitters', Type::SOUND ); } private function areatrigger() : array { // [guid, type, typeId, map, posX, posY [, respawn, spawnMask, phaseMask, areaId, floor, pathId]] $base = DB::Aowow()->select( 'SELECT `id` AS `guid`, ?d AS `type`, `id` AS `typeId`, `mapId` AS `map`, `posX`, `posY` FROM dbc_areatrigger', Type::AREATRIGGER ); $addData = DB::World()->select( 'SELECT -`ID` AS `guid`, ?d AS `type`, ID AS `typeId`, `target_map` AS `map`, `target_position_x` AS `posX`, `target_position_y` AS `posY` FROM areatrigger_teleport UNION SELECT -`entryorguid` AS `guid`, ?d AS `type`, entryorguid AS `typeId`, `action_param1` AS `map`, `target_x` AS `posX`, `target_y` AS `posY` FROM smart_scripts WHERE `source_type` = ?d AND `action_type` = ?d', Type::AREATRIGGER, Type::AREATRIGGER, SAI_SRC_TYPE_AREATRIGGER, SAI_ACTION_TELEPORT ); return array_merge($base, $addData); } private function instances() : array { // maps with set graveyard return DB::Aowow()->select( 'SELECT -`id` AS `guid`, ?d AS `type`, `id` AS `typeId`, `parentMapId` AS `map`, `parentX` AS `posX`, `parentY` AS `posY` FROM ?_zones WHERE `parentX` <> 0 AND `parentY` <> 0 AND `parentArea` = 0 AND (`cuFlags` & ?d) = 0', Type::ZONE, CUSTOM_EXCLUDE_FOR_LISTVIEW ); } private function waypoints() : array { return DB::World()->select( 'SELECT c.`guid`, w.`entry` AS `creatureOrPath`, w.`pointId` AS `point`, c.`zoneId` AS `areaId`, c.`map`, w.`waittime` AS `wait`, w.`location_x` AS `posX`, w.`location_y` AS `posY` FROM creature c JOIN script_waypoint w ON c.`id` = w.`entry` UNION SELECT c.`guid`, w.`entry` AS `creatureOrPath`, w.`pointId` AS `point`, c.`zoneId` AS `areaId`, c.`map`, 0 AS `wait`, w.`position_x` AS `posX`, w.`position_y` AS `posY` FROM creature c JOIN waypoints w ON c.`id` = w.`entry` UNION SELECT c.`guid`, -w.`id` AS `creatureOrPath`, w.`point`, c.`zoneId` AS `areaId`, c.`map`, w.`delay` AS `wait`, w.`position_x` AS `posX`, w.`position_y` AS `posY` FROM creature c JOIN creature_addon ca ON ca.`guid` = c.`guid` JOIN waypoint_data w ON w.`id` = ca.`path_id` WHERE ca.`path_id` <> 0' ); } private function transformPoint(array $point, int $type, ?string &$notice = '') : array { // npc/object is on a transport -> apply offsets to path of transport // note, that transport DO spawn outside of displayable area maps .. another todo i guess.. if (isset($this->transports[$point['map']])) { $point['posX'] += $this->transports[$point['map']]['posX']; $point['posY'] += $this->transports[$point['map']]['posY']; $point['map'] = $this->transports[$point['map']]['mapId']; } $area = $point['areaId'] ?? 0; $floor = -1; if (isset($this->overrideData[$type][$point['guid']])) { $area = $this->overrideData[$type][$point['guid']]['areaId']; $floor = $this->overrideData[$type][$point['guid']]['floor']; $notice = '[points] '.str_pad('['.$point['guid'].']', 9).' manually moved to [A:'.($point['areaId'] ?? 0).' => '.$area.'; F: '.$floor.']'; } if ($points = Game::worldPosToZonePos($point['map'], $point['posX'], $point['posY'], $area, $floor)) { if ($area) // if areaId is set, area was determined by TC .. we're fine .. mostly return ['areaId' => $area, 'posX' => $points[0]['posX'], 'posY' => $points[0]['posY'], 'floor' => $points[0]['floor']]; $point = Game::checkCoords($points); // try to determine best found point by alphamap return ['areaId' => $point['areaId'], 'posX' => $point['posX'], 'posY' => $point['posY'], 'floor' => $point['floor']]; } // cannot be placed on a map, try to reuse TC assigned areaId if ($area) { $selfOrParent = DB::Aowow()->selectCell('SELECT IF(`parentArea`, `parentArea`, `id`) FROM ?_zones WHERE `id` = ?d', $area); return ['areaId' => $selfOrParent, 'posX' => 0, 'posY' => 0, 'floor' => 0]; } // we know the instanced map; try to assign a zone this way if (!in_array($point['map'], [0, 1, 530, 571])) if ($area = DB::Aowow()->selectCell('SELECT `id` FROM ?_zones WHERE `mapId` = ?d AND `parentArea` = 0 AND (`cuFlags` & ?d) = 0', $point['map'], CUSTOM_EXCLUDE_FOR_LISTVIEW)) return ['areaId' => $area, 'posX' => 0, 'posY' => 0, 'floor' => 0]; return []; } }); ?>