getMessage() . "\n" . $e->getTraceAsString(), FILE_APPEND); throw $e; } }); function config_set_base_dirs(array $dirs) { global $CONFIG_BASE_DIRS; $CONFIG_BASE_DIRS = array_map(function($d) { return normalize_path($d); }, $dirs); } function config_base_dirs() { global $CONFIG_BASE_DIRS; return $CONFIG_BASE_DIRS; } function config_map_base_dir($file, $normalized = false, $strict = true, $dirs = null) { if(!is_array($dirs)) $dirs = config_base_dirs(); if(!$normalized) $file = normalize_path($file); foreach($dirs as $dir) if(strpos($file, $dir) === 0) return $dir; if($strict) throw new Exception("File '$file' is not mapped to any base dir"); return null; } function config_real_path($rel_path, $strict = true) { $dirs = config_base_dirs(); foreach($dirs as $dir) if(is_file($dir . '/' . $rel_path)) return $dir . '/' . $rel_path; if($strict) throw new Exception("No file for relative path '$rel_path'"); return null; } function config_build_dir() { global $GAME_ROOT; return "$GAME_ROOT/build/tmp/"; } function config_set_worker_init_fn($fn) { global $CONFIG_INIT_WORKER_FUNC; $CONFIG_INIT_WORKER_FUNC = $fn; } function config_scan_files() { return scan_files_rec(config_base_dirs(), array('conf.js')); } function config_pack_bundle(array $cache_entries, $use_lz4 = false, $use_config_refs = false) { global $GAME_ROOT; $binary_format = 1; $version = game_version_code(); $MAP = array(); $STRIDMAP = array(); $payloads = array(); $payloads_offset = 0; foreach($cache_entries as $entry) { list($format, $payload) = _config_get_payload($entry, $use_lz4, $use_config_refs); $payload_size = strlen($payload); $payloads[] = array($payloads_offset, $payload, $format, $payload_size); $payloads_offset += $payload_size; } $header = array(); foreach($cache_entries as $idx => $entry) { if(isset($MAP[$entry->id])) throw new Exception("Duplicating config id for '{$entry->strid}' conflicts with '{$MAP[$entry->id]}'"); $MAP[$entry->id] = $entry->strid; $strid_crc = crc32($entry->strid); if(isset($STRIDMAP[$strid_crc])) throw new Exception("Duplicating config str id crc for '{$entry->strid}' conflicts with '{$STRIDMAP[$strid_crc]}'"); $STRIDMAP[$strid_crc] = $entry->strid; $header[] = array( $payloads[$idx][2], $entry->id, crc32($entry->strid), $entry->class_id, $payloads[$idx][0], $payloads[$idx][3] ); } $header_msgpack = config_msgpack_pack($header); $payloads_bundle = ''; foreach($payloads as $item) $payloads_bundle .= $item[1]; $packed_data = pack("C", $binary_format) . pack("V", $version) . pack("V", strlen($header_msgpack)) . $header_msgpack . $payloads_bundle; echo "CONF.BUNDLE: entries " . sizeof($cache_entries) . "; total " . kb($packed_data) . "; lz4 $use_lz4; refs $use_config_refs; CRC " . crc32($packed_data) . "\n"; return $packed_data; } function config_unpack_bundle($packed_data) { $packed_info = substr($packed_data, 0, 1+4+4); $info = unpack('Cformat/Vversion/Vheader_len', $packed_info); if($info['format'] !== 1) throw new Exception("Unknown format: {$info['format']}"); $header_msgpack = substr($packed_data, 1+4+4, $info['header_len']); $header = config_msgpack_unpack($header_msgpack); $payloads_bundle = substr($packed_data, 1+4+4 + $info['header_len']); $entries = array(); foreach($header as $item) { list($format, $id, $strid_crc, $class_id, $offset, $size) = $item; $payload = substr($payloads_bundle, $offset, $size); $entries[$id] = array($class_id, _config_unpack_payload($format, $payload)); } return $entries; } function _config_get_payload(ConfigCacheEntry $ce, $use_lz4, $use_config_refs) { $format = ConfigCacheEntry::FMT_BINARY; $payload = null; if($use_config_refs && $ce->payload_file) { $format = ConfigCacheEntry::FMT_FILE_REF; $payload = $ce->payload_file; } else { $payload = $ce->payload; if($use_lz4 && strlen($payload) > 512) { $format = ConfigCacheEntry::FMT_LZ4; $payload = lz4_compress($payload, 9); } } return array($format, $payload); } function _config_unpack_payload($format, $payload) { $msg_packed = null; if($format === ConfigCacheEntry::FMT_LZ4) $msg_packed = lz4_uncompress($payload); else if($format === ConfigCacheEntry::FMT_BINARY) $msg_packed = $payload; else if($format === ConfigCacheEntry::FMT_FILE_REF) $msg_packed = ensure_read($payload); else throw new Exception("Bad format: $format"); return config_msgpack_unpack($msg_packed); } function config_make_standalone_ext_bundle(array $configs, $file_path, $use_lz4 = true) { $cache_entries = array(); foreach($configs as $conf) { $payload = config_msgpack_pack($conf->export()); //creating fake cache entries $entry = new ConfigCacheEntry(); $entry->id = $conf->id; $entry->strid = $conf->strid; $entry->class_id = $conf->getClassId(); $entry->class = get_class($conf); $entry->payload = $payload; $entry->config = $conf; $cache_entries[] = $entry; } $packed_data = config_pack_bundle($cache_entries, $use_lz4); ensure_write($file_path, $packed_data); } function config_bench_load($file) { $base_dir = config_map_base_dir($file); list($proto_id, $_) = config_ensure_header($base_dir, $file); if(!$proto_id) throw new Exception("Bad proto_id: {$proto_id}"); $t = microtime(true); $parse_res = config_parse(config_base_dirs(), $file); if($parse_res->error !== 0) throw new Exception("Error({$parse_res->error}) while loading JSON from {$file}:\n" . $parse_res->error_descr); echo "PARSE: " . (microtime(true) - $t) . "\n"; $t = microtime(true); $config = config_load_ex($base_dir, $file, $parse_res->parsed_arr, $proto_id); echo "LOAD: " . (microtime(true) - $t) . "\n"; } function config_get_tmp_build_path($file) { $name = str_replace(":", "-", str_replace("\\", "-", str_replace("/", "-", normalize_path($file)))); $name = ltrim($name, "-"); return normalize_path(config_build_dir() . "/$name"); } function config_get_includes_map_path() { return config_build_dir() . "/includes.map"; } function config_load_includes_map($file = null) { $file = $file ? $file : config_get_includes_map_path(); $includes_map = array(array(), array()); if(is_file($file)) { $tmp_map = @unserialize(ensure_read($file)); if(is_array($tmp_map)) $includes_map = $tmp_map; } return $includes_map; } function config_save_includes_map(array $includes_map, $file = null) { $file = $file ? $file : config_get_includes_map_path(); ensure_write($file, serialize($includes_map)); } class ConfigFetchResult { public $all = array(); public $by_id = array(); public $by_path = array(); public $by_alias = array(); } function config_fetch_ex( array $files, $force_stale = false, $verbose = false, $includes_map_file = null, $max_workers = null ) { if(!$files) return new ConfigFetchResult(); if($max_workers === null) $max_workers = sizeof($files) < 20 ? 1 : 4; $includes_map_file = $includes_map_file ? $includes_map_file : config_get_includes_map_path(); $includes_map = config_load_includes_map($includes_map_file); $chunk_size = ceil(sizeof($files)/$max_workers); $jobs = array(); foreach(array_chunk($files, $chunk_size) as $idx => $chunk_files) $jobs[] = array($idx, $chunk_files); $results_by_job = null; $serial = $max_workers == 1; if(!$serial) { $results_by_job = _config_worker_run_procs($jobs, $includes_map_file, $force_stale, $verbose); //in case of any result error try serial processing if(array_search(false, $results_by_job, true/*strict*/) !== false) { if($verbose) echo "Result error detected, trying serial processing...\n"; $serial = true; } } if($serial) { $results_by_job = array(); foreach($jobs as $job) $results_by_job[] = _config_worker_func($job, $includes_map, $force_stale, $verbose); } list($result, $total_stales) = _config_fetch_cache_ex($results_by_job); if($verbose) echo "Miss/Hit: $total_stales/" . sizeof($result->all) . "\n"; config_save_includes_map($includes_map, $includes_map_file); return $result; } function _config_fetch_cache_ex($results_by_job) { $result = new ConfigFetchResult(); $total_stales = 0; foreach($results_by_job as $results) { foreach($results as $file => $item) { list($cache_file, $is_stale) = $item; $cache_entry = ConfigCacheEntry::unserialize(ensure_read($cache_file)); //NOTE: handling any cache corruption errors if($cache_entry === null) { $is_stale = true; $cache_entry = _config_invalidate_cache($file, $cache_file); } $includes = $cache_entry->includes; $includes_map[$file] = $includes; if(!$is_stale && count($includes) > 0 && need_to_regen($file, $includes)) { $is_stale = true; $cache_entry = _config_invalidate_cache($file, $cache_file); } if($is_stale) ++$total_stales; //we want results to be returned in the same order, so //we store entries by the file key and later retrieve array values $result->by_path[$file] = $cache_entry; $result->by_id[$cache_entry->id] = $cache_entry; $result->by_alias[$cache_entry->strid] = $cache_entry; } } $result->all = array_values($result->by_path); return array($result, $total_stales); } function _config_worker_run_procs(array $jobs, $includes_map_file, $force, $verbose) { $worker_args = array(); foreach($jobs as $idx => $job) $worker_args[] = array($job, $includes_map_file, $force, $verbose); return run_background_gamectl_workers('config_worker', $worker_args); } function _config_worker_func(array $job, array $includes_map, $force, $verbose) { global $CONFIG_INIT_WORKER_FUNC; if(is_callable($CONFIG_INIT_WORKER_FUNC)) $CONFIG_INIT_WORKER_FUNC(); list($idx, $files) = $job; if($verbose) echo "Worker $idx (" . sizeof($files) . ") started\n"; $results = array(); foreach($files as $file_idx => $file) { if($verbose && $file_idx > 0 && ($file_idx % 500) == 0) echo "Worker $idx progress: " . round($file_idx / sizeof($files) * 100) . "% ...\n"; $cache_file = config_get_cache_path($file); $is_stale = true; if(!$force) { $file_deps = array($file); if(isset($includes_map[$file])) $file_deps = array_merge($file_deps, $includes_map[$file]); $is_stale = need_to_regen($cache_file, $file_deps); } if($is_stale) _config_invalidate_cache($file, $cache_file); $results[$file] = array($cache_file, $is_stale); } if($verbose) echo "Worker $idx done\n"; return $results; } function _config_invalidate_cache($file, $cache_file) : ConfigCacheEntry { $cache_payload_file = config_get_cache_payload_path($file); //TODO: pass it from above? $base_dir = config_map_base_dir($file); list($proto_id, $_) = config_ensure_header($base_dir, $file); if(!$proto_id) throw new Exception("Bad proto_id: {$proto_id}"); $GLOBALS['CONFIG_CURRENT_PROTO_ID'] = $proto_id; $GLOBALS['CONFIG_EXTRAS'] = ConfigCacheEntryExtras::create(); $GLOBALS['CONFIG_BHL_FUNCS'] = array(); $pres = config_parse(config_base_dirs(), $file); if($pres->error !== 0) throw new Exception("Error({$pres->error}) while loading JSON in {$file}:\n" . $pres->error_descr); $includes = config_get_module_includes($pres->jsm_module); $config = config_load_ex($base_dir, $file, $pres->parsed_arr, $proto_id); $payload_data = config_msgpack_pack($config->export()); $cache_entry = new ConfigCacheEntry(); $cache_entry->id = $config->id; $cache_entry->config = $config; $cache_entry->strid = $config->strid; $cache_entry->cache_file = $cache_file; $cache_entry->class = get_class($config); $cache_entry->class_id = $config->getClassId(); $cache_entry->payload_file = $cache_payload_file; $cache_entry->file = normalize_path($file); $cache_entry->includes = $includes; $cache_entry->extras = $GLOBALS['CONFIG_EXTRAS']; $cache_entry->refs = config_extract_refs($pres->normalized_jzon); ensure_write($cache_file, ConfigCacheEntry::serialize($cache_entry)); ensure_write($cache_payload_file, $payload_data); return $cache_entry; } function config_find_by_alias(ConfigFetchResult $cache_entries, $strid) : ConfigCacheEntry { if(array_key_exists($strid, $cache_entries->by_alias)) return $cache_entries->by_alias[$strid]; throw new Exception("Failed to find config by alias '$strid'!"); return null; } function config_find_by_id(ConfigFetchResult $cache_entries, $id) : ConfigCacheEntry { if(array_key_exists($id, $cache_entries->by_id)) return $cache_entries->by_id[$id]; throw new Exception("Failed to find config by id '$id'!"); return null; } function config_find_by_path(ConfigFetchResult $cache_entries, $path) : ConfigCacheEntry { if(array_key_exists($path, $cache_entries->by_path)) return $cache_entries->by_path[$path]; throw new Exception("Failed to find config by path '$path'!"); return null; } function config_fetch_by_path(string $path, $force_stale = false, $use_cache = true, $includes_map_file = null) : ConfigCacheEntry { $ces = config_fetch_ex(array($path), $force_stale, $verbose = false, $includes_map_file, null, $use_cache); if(!$ces) throw new Exception("Config not found at path '$path'"); return $ces->all[0]; } function config_fetch_all($force_stale = false, $use_cache = true, $includes_map_file = null) { return config_fetch_ex(config_scan_files(), $force_stale, $verbose = false, $includes_map_file, null, $use_cache); } class ConfigCacheEntryExtras { private static $klass = ConfigCacheEntryExtras::class; static function init($project_specific_klass) { self::$klass = $project_specific_klass; } static function create() { return new self::$klass(); } function export() { $as_array = get_object_vars($this); return $as_array; } function import($as_array) { foreach($as_array as $field_name => $field_value) $this->$field_name = $field_value; } } class ConfigCacheEntry { const FMT_BINARY = 0; const FMT_LZ4 = 1; const FMT_FILE_REF = 2; public $class; public $class_id; public $id; public $strid; public $cache_file; //NOTE: actual payload is stored in a separate file for faster incremental retrievals public $payload_file; public $file; public $includes = array(); public $refs = array(); public $extras; public $_config; public $_payload; static function serialize($ce) { $d = $ce->export(); return serialize($d); } static function unserialize($str) { $d = @unserialize($str); if(!is_array($d)) return null; $ce = new ConfigCacheEntry(); $ce->import($d); return $ce; } function __construct() { $this->extras = ConfigCacheEntryExtras::create(); } function __get($name) { if($name === "config") { if($this->_config === null) { $klass = $this->class; $data = config_msgpack_unpack($this->payload); $this->_config = new $klass($data); } return $this->_config; } else if($name === "payload") { if($this->_payload !== null) return $this->_payload; //if payload not set directly not storing it return ensure_read($this->payload_file); } else throw new Exception("No such property '$name'"); } function __set($name, $v) { if($name === "config") $this->_config = $v; else if($name === "payload") $this->_payload = $v; else throw new Exception("No such property '$name'"); } function export() { $d = array(); $d[] = $this->class; $d[] = $this->class_id; $d[] = $this->id; $d[] = $this->strid; $d[] = $this->cache_file; $d[] = $this->payload_file; $d[] = $this->file; $d[] = $this->includes; $d[] = $this->refs; $d[] = $this->extras->export(); return $d; } function import(array $d) { $extras = array(); list( $this->class, $this->class_id, $this->id, $this->strid, $this->cache_file, $this->payload_file, $this->file, $this->includes, $this->refs, $extras, ) = $d; $this->extras->import($extras); } } function config_get_cache_path($file) { return config_get_tmp_build_path($file . '.cacheb'); } function config_get_cache_payload_path($file) { return config_get_tmp_build_path($file . '.pdata'); } function config_get_id($conf_dir, $file_path) { return config_file2id($conf_dir, $file_path); } function config_get_strid($conf_dir, $file_path) { return config_file2strid($conf_dir, $file_path); } function config_extract_refs($src, $as_map = true) { $refs = array(); if(preg_match_all('~"(@[^"]+)"~', $src, $matches)) { foreach($matches[1] as $ref) { if(!isset($refs[$ref])) $refs[$ref] = 1; ++$refs[$ref]; } } return $as_map ? $refs : array_keys($refs); } function config_get_header($file, &$proto_id, &$alias) { $h = fopen($file, "r"); $line = fgets($h, 256); fclose($h); if(preg_match('~\{\s*/\*\s*proto_id\s*=\s*(\d+)\s*;\s*alias\s*=\s*([^\s]+)~', $line, $matches)) { $proto_id = (int)$matches[1]; $alias = $matches[2]; return true; } else return false; } function config_set_header($contents, $proto_id, $alias, &$is_success) { $contents = preg_replace('~\s*\{~', "{ /* proto_id = {$proto_id} ; alias = {$alias} */", $contents, 1/*limit*/, $count); $is_success = $count == 1; return $contents; } function config_extract_header($contents) { return preg_replace('~(\s*\{)\s*/\*\s*proto_id\s*=\s*\d+\s*;\s*alias\s*=\s*[^\s]+\s*\*/~', '$1', $contents); } function config_ensure_header($conf_dir, $file, $force = false) { if(config_get_header($file, $curr_proto_id, $curr_alias)) { $alias = config_get_strid($conf_dir, $file); if($force) $curr_proto_id = config_get_id($conf_dir, $file); //NOTE: keeping current proto id intact if not forced if($force || $curr_alias !== $alias) { $lines = file($file); //TODO: why not using config_set_header(..) ? $lines[0] = preg_replace('~\s*\{\s*/\*\s*proto_id\s*=\s*\d+\s*;\s*alias\s*=\s*[^\s]+\s*\*/~', "{ /* proto_id = {$curr_proto_id} ; alias = {$alias} */", $lines[0], 1/*limit*/, $count); if($count != 1) throw new Exception("Could not set header for '$file' in line: {$lines[0]}"); ensure_write_if_differs($file, join("", $lines)); } return array($curr_proto_id, $alias); } else { $proto_id = config_get_id($conf_dir, $file); $alias = config_get_strid($conf_dir, $file); $lines = file($file); $lines[0] = config_set_header($lines[0], $proto_id, $alias, $is_success); if(!$is_success) throw new Exception("Could not set header for '$file' in line: {$lines[0]}"); ensure_write($file, join("", $lines)); return array($proto_id, $alias); } } function config_load($conf_dir, $conf_path) { list($proto_id, $_) = config_ensure_header($conf_dir, $conf_path); if(!$proto_id) throw new Exception("Bad proto_id: {$proto_id}"); $pres = config_parse($conf_dir, $conf_path); if($pres->error !== 0) throw new Exception("Error({$pres->error}) while loading JSON from {$conf_path}:\n" . $pres->error_descr); return config_load_ex($conf_dir, $conf_path, $pres->parsed_arr, $proto_id); } function conf2json($conf, $json_flags = 0) { $arr = $conf->export(true); $arr['class'] = get_class($conf); unset($arr['id']); unset($arr['strid']); $json = json_encode($arr, $json_flags); $json = str_replace(array('\\\\', '\\n', '\/'), array('\\', "\n", '/'), $json); return $json; } class ConfigParseResult { public $error = 0; public $error_descr; public $normalized_jzon = ''; public $parsed_arr; public $jsm_module; } function config_parse(array $base_dirs, $file) : ConfigParseResult { $res = new ConfigParseResult(); $normalized_jzon = ''; try { $jsm = new \JSM($base_dirs, $file); list($normalized_jzon, $jsm_module) = $jsm->process(); $res->normalized_jzon = $normalized_jzon; $res->jsm_module = $jsm_module; } catch(Exception $e) { $res->error = 1; $res->error_descr = "File '$file':\n" . $e->getMessage() . "\n" . $e->getTraceAsString(); return $res; } $decode_res = config_check_and_decode_jzon($normalized_jzon); if($decode_res[0] === 0) { $res->parsed_arr = $decode_res[2]; } else { $res->error = $decode_res[0]; $res->error_descr = "File '$file':\n" . $decode_res[1]; } return $res; } function config_check_and_decode_jzon($json) { try { $arr = \jzon_parse($json); return array(0, "", $arr); } catch(Exception $e) { return array(1, $e->getMessage(), array()); } } function config_load_ex($conf_dir, $file, array $arr, $id = null) { if(!isset($arr['class']) || !isset($arr['class'][0])) throw new Exception("Class is not set in file '$file'."); $klass = $arr['class']; unset($arr['class']); if($id === null) $id = config_file2id($conf_dir, $file); $cnf = null; try { if(!class_exists($klass)) throw new Exception("No such class '$klass'"); $cnf = new $klass; $cnf->import($arr, true); } catch(Exception $e) { throw new Exception($e->getMessage() . " in file '{$file}'"/* . $e->getTraceAsString()*/); } $cnf->id = $id; $cnf->strid = config_file2strid($conf_dir, $file); return $cnf; } function config_is_file($filename) { return (strrpos($filename, '.conf.js') === (strlen($filename) - 8)); } function config_file2id($conf_dir, $filename) { $nfilename = config_make_path($conf_dir, $filename); return config_crc28($nfilename); } function config_file2strid($conf_dir, $filename) { $strid = config_make_path($conf_dir, $filename); $fname_idx = strrpos($strid, "/"); $fname_idx = strpos($strid, ".", $fname_idx); $strid = substr($strid, 0, $fname_idx); return "@".$strid; } function config_make_path($conf_dir, $path) { return ltrim(str_replace(normalize_path($conf_dir, true/*nix*/), '', normalize_path($path, true/*nix*/)), '/'); } function config_str2id($str) { if(strpos($str, '@') === 0) $str = substr($str, 1) . '.conf.js'; return config_crc28($str); } function config_crc28($what) { return crc32($what) & 0xFFFFFFF; } function config_get_module_includes(\JSM_Module $cm) { $includes = array(); foreach($cm->getIncludes() as $include => $_) { //maybe we should take .php includes into account as well? if(config_str_ends_with($include, ".php")) continue; $includes[] = $include; } return $includes; } function config_includes_map_find_text_origin(array $map, $file, $text) { if(!isset($map[$file])) return array($file); $tpls = $map[$file]; $tpls[] = $file; $res = array(); foreach($tpls as $tpl) { $content = ensure_read($tpl); $content = i18n_decode_string($content); if(strpos($content, $text) !== FALSE) $res[] = $tpl; } return $res; } function config_str_replace_in_files($subj, $repl, array $files) { foreach($files as $file) { $contents = ensure_read($file); $new_contents = str_replace($subj, $repl, $contents); if($new_contents !== $contents) ensure_write($file, $new_contents); } } function config_str_ends_with($haystack, $needle) { // search forward starting from end minus needle length characters return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0 && strpos($haystack, $needle, $temp) !== false); } function config_walk_fields($proto, $callback) { if(!method_exists($proto, 'CLASS_FIELDS_PROPS')) return; foreach($proto->CLASS_FIELDS_PROPS() as $field => $tokens) { $value = $proto->$field; $callback($proto, $field, $value, $tokens); if(is_object($value)) config_walk_fields($value, $callback); else if(is_array($value)) { foreach($value as $num => $item) { if(is_object($item)) config_walk_fields($item, $callback); } } } } if(function_exists('msgpack_pack')) { function config_msgpack_pack($data) { $prev = ini_set('msgpack.use_str8_serialization', '0'); $res = msgpack_pack($data); if($prev != '0') ini_set('msgpack.use_str8_serialization', $prev); return $res; } function config_msgpack_unpack($data) { return msgpack_unpack($data); } } else { function config_msgpack_pack($data) { include_once(__DIR__ . '/msgpack/msgpack_custom.inc.php'); $packer = new \MessagePackCustom(); return $packer->pack($data); } function config_msgpack_unpack($data) { return \MessagePack\MessagePack::unpack($data); } }