--------------------------------------------------------------------------
-- YD编辑器的主配置和代码文件
-- 本文件编码为UTF-8，在处理字符串时要注意使用iconv库转换
--------------------------------------------------------------------------

-- 设置路径
package.path = package.path .. ";" .. sys.get_editor_root_dir():string() .. "/lua/?.lua"
package.cpath = package.cpath .. ";" .. sys.get_editor_root_dir():string() .. "/modules/?.dll"

-- 载入一些库
require "alien"
require "lanes"
require "std"
require "titan_engine"
require "pl.app"
require "socket"
require "msvcrt"
require "oil"

-- 本地魔兽版本号
local_warcraft_version = nil

-- YDWE的菜单句柄
ydwe_menu_handle = nil

-- 脚本用编码转换器
iconv_utf8_to_locale = nil

-- 标志当前平台是否支持.net
dotnet_support = false

-- YDTriggger是否载入
yd_trigger_support = false

-- 本函数在编辑器启动时调用，可以在本函数中载入一些插件
function on_startup()
	log.debug("********************* on startup start *********************")

	-- 载入库（悲剧，如果加载时载入可能会崩溃）
	--require "iup"
	--log.debug("IUP loaded successfully.")
	
	local success, err = pcall(require, "luacom")
	if success then
		log.debug("LUACOM loaded successfully.")
	else
		log.debug("LUACOM loading failed.")
	end

	-- 载入SFMPQ
	-- 初始化sfmpq
	sfmpq.initialize()
	
	-- 初始化reg
	reg.initialize()
	
	-- 刷新配置数据
	reload_configuration()

	-- 载入.net组件（必须这个时候载入）
	dotnet_support = global_config:get_integer("ThirdPartyPlugin.EnableDotNetSupport", 1) == 1 and IsDotNet35VersionInstalled() and IsDotNet20VersionInstalled() and is_dotnet_installed()
	-- 是否成功
	if dotnet_support then
		local success, err = pcall(require, "luanet")
		if success then
			log.debug(".net component loaded successfully.")
			require "CLRPackage"
			DotNet = {}
			DotNet.Reflection = CLRPackage("System.Reflection", "System.Reflection")

			-- 启动着色器
			local Colorizer = assembly_load_from((sys.get_editor_root_dir() / "bin" / "Colorizer.dll"):file_string())
			if Colorizer then
				success, err = pcall(luanet.type(Colorizer:GetType("WEInit")).begin)
				if success then
					log.debug("WE Colorizer loaded successfully.")
				else
					log.warn("WE Colorizer loading failed..")
				end
			else
				log.warn("Cannot find WE Colorizer")
			end
		else
			log.error("Failed to load .net components")
		end
	else
		log.debug("Current platform does not support .net")
	end

	-- 加载YDClient.dll
	load_yd_client(22, 11)
	
	-- YDStatistics.dll
	load_yd_statistics()
	
	-- 检测UI和Units目录
	check_conflicting_ui()

	-- 检查魔兽目录下是否有可能引起冲突的文件夹
	clear_potential_conflicting()

	-- 获取game.dll的路径
	local game_dll_path = sys.get_warcraft_dir() / "game.dll"

	-- 检测魔兽的版本
	local version_info = sys.get_module_version_info(game_dll_path)
	local_warcraft_version = tonumber(version_info.FileVersionMS)
	log.trace(string.format(
		(local_warcraft_version >= 1.24 and "New" or "Old") ..
		" version warcraft %s detected", version_info.FileVersionMS
	))
	
	local script_warcraft_version, e = get_war3_version_from_script()
	
	if e and 0 == global_config:get_integer("MapSave.Option", 0) then
		if (((local_warcraft_version < 1.24) and (script_warcraft_version >= 1.24))
			or ((local_warcraft_version >= 1.24) and (script_warcraft_version < 1.24))) then
		
			sys.message_dialog(0, 
				__("检测到您的魔兽Game.dll和War3Patch.mpq的版本不一致，这可能是由版本转换器造成的，这会导致您无法保存和测试地图，强烈建议您使用正确的魔兽补丁还原您的魔兽。"), 
				_("YDWE"), 
				bit.bor(MB_ICONQUESTION, MB_OK))
		end
	end

	-- 尝试载入TESH
	if global_config:get_integer("ThirdPartyPlugin.EnableTesh", 1) == 1 then
		load_normal_shared_object(sys.get_editor_root_dir() / "tesh" / "TESH.dll")
	end

	-- 载入注入代码配置
	scan_for_inject_code(sys.get_editor_root_dir() / "jass")

	load_mpq_in_mpq_dir("yd.mpq")

	-- 显示感谢信息
	show_credit()

	-- 安装其他的hook、Patch等
	initialize_we_hacks()

	-- YDTrigger专用
	yd_trigger_support = load_yd_trigger()

	-- YDFont DLL路径
	local yd_font_dll_path = sys.get_editor_root_dir() / "Font" / "YDFont.dll"

	if fs.exists(yd_font_dll_path) and load_normal_shared_object(yd_font_dll_path) ~= 0 then
		log.debug("YD Font loaded successfully")
	else
		log.warn("Cannot load " .. yd_font_dll_path:filename())
	end

	log.debug("********************* on startup end *********************")
end

function get_war3_version_from_script()
	local common_j_path = sys.get_editor_root_dir() / "jasshelper" / "common.j"
	if ar.native_generic_extract_file(common_j_path, "scripts\\common.j") then
		local f, e = io.open(common_j_path:file_string(), "rb")
		if f then
			local s = f:read("*a")
			f:close()
			if s:find("StringHash") then
				return 1.24, true 
			else
				return 1.20, true
			end
		end
	end
	
	return 0., false
end

-- 本函数在编辑器关闭时调用，可以在本函数中作一些清理工作
function on_exit()
	log.debug("********************* on exit start *********************")
	log.debug("********************* on exit end *********************")
end

-- 本函数当保存地图时调用
-- 返回值：布尔值，true表示保存成功，false表示保存失败
function on_save()
	log.debug("********************* on save start *********************")

	-- 刷新配置数据
	reload_configuration()

	-- 获取当前保存的地图名
	local map_path = sys.get_trigger_map()
	log.trace("Saving " .. map_path:file_string())

	-- 获取保存类型
	local save_type = global_config:get_integer("MapSave.Option", 0)

	-- 获取保存选项
	local save_option = {
		-- 是否启用JassHelper
		enable_jasshelper = (global_config:get_integer("ScriptCompiler.EnableJassHelper", 1) == 1),
		-- 是否是调试模式
		enable_jasshelper_debug = (global_config:get_integer("ScriptCompiler.EnableJassHelperDebug", 0) == 1),
		-- 是否优化地图
		enable_jasshelper_optimization = (global_config:get_integer("ScriptCompiler.EnableJassHelperOptimization", 1) == 1),
		-- 是否启用cJass
		enable_cjass = (global_config:get_integer("ScriptCompiler.EnableCJass", 0) == 1),
		-- 目标魔兽版本
		runtime_version = determine_map_version(save_type, local_warcraft_version),
		-- 代码注入选项
		script_injection = global_config:get_integer("ScriptInjection.Option", 0),
		-- 是否启用YDTrigger
		enable_yd_trigger = yd_trigger_support
	}

	if save_option.enable_cjass then
		save_option.script_injection = false
	end
	
	-- 如果保存双份，需要预先拷贝
	local map_path_aux = nil
	if save_type == 3 then
		-- 决定第二份的路径
		if save_option.runtime_version < 1.24 then
			map_path_aux = generate_aux_map_path_of_path(map_path, "hashtable")
		else
			map_path_aux = generate_aux_map_path_of_path(map_path, "returnbug")
		end

		log.trace("Making copy of the original map.")

		-- true代表覆盖现有文件。false不会覆盖
		pcall(fs.copy_file, map_path, map_path_aux, true)
	end

	-- 编译地图
	local result = compile_map(map_path, save_option)

	-- 如果有第二份，此时处理第二份
	if map_path_aux then
		log.trace("Processing second map")
		save_option.runtime_version = save_option.runtime_version >= 1.24 and 1.20 or 1.24
		compile_map(map_path_aux, save_option)
	end

	log.debug("********************* on save end *********************")
	return result
end


-- 本函数在测试地图时使用
-- application - 应用程序，字符串对象
-- command_line - 命令行，字符串对象
-- 返回：true表示成功，false表示失败
function on_test(application, command_line)
	log.debug("********************* on test start *********************")

	-- 刷新配置数据
	reload_configuration()

	-- 获取当前测试的地图名
	local map_path = sys.get_trigger_map()
	log.debug("Testing " .. map_path:file_string())

	-- 附加命令行
	local command_aux = ""

	-- 是否OpenGL方式？
	if global_config:get_integer("MapTest.LaunchOpenGL", 0) == 1 then
		command_aux = command_aux .. " -opengl"
	end

	-- 是否窗口方式？
	if global_config:get_integer("MapTest.LaunchWindowed", 0) == 1 then
		command_aux = command_aux .. " -window"
	end

	-- 启动魔兽开始测试...
	local result = sys.process_spawn(application, command_line .. command_aux, nil, false)
	
	--local editor_dir = sys.get_editor_root_dir()
	--local result = sys.process_spawn_with_dll(application, command_line .. command_aux, nil, editor_dir / "bin" / "YD.dll", false)

	log.debug("********************* on test end *********************")
	return result
end

-- 在WE载入MSS引擎解码器时调用，过滤载入的dll
-- provider_path - 解码器路径，fs.path对象
-- 必须返回布尔值，true表示允许载入，false屏蔽
function on_mss_load_provider_library(provider_path)
	log.debug("Loading provider " .. provider_path:file_string())

	-- 全部放行
	return true
end

-- 基础菜单ID
YDWE_MENU_ID_BASE = 48886
-- 配置菜单ID
YDWE_MENU_ID_CONFIG = YDWE_MENU_ID_BASE
-- 编译信息菜单ID
YDWE_MENU_ID_COMPILE_INFO = YDWE_MENU_ID_BASE + 1
-- 显示JassHelper版本菜单ID
YDWE_MENU_ID_SHOW_JASSHELPER_VERSION = YDWE_MENU_ID_BASE + 2
-- 显示cJass版本菜单ID
YDWE_MENU_ID_SHOW_CJASS_VERSION = YDWE_MENU_ID_BASE + 3
-- 打开官网菜单ID
YDWE_MENU_ID_OFFICIAL_SITE = YDWE_MENU_ID_BASE + 4
-- 文件关联设置菜单ID
YDWE_MENU_ID_FILE_ASSOCIATION = YDWE_MENU_ID_BASE + 5
-- 导出文件菜单ID
YDWE_MENU_ID_FILE_EXPORT = YDWE_MENU_ID_BASE + 6
-- 编码转换菜单ID
YDWE_MENU_ID_ENCODING_CONVERSION = YDWE_MENU_ID_BASE + 7
-- 感谢信息显示菜单ID
YDWE_MENU_ID_SHOW_CREDIT = YDWE_MENU_ID_BASE + 8

-- 初始化菜单
function on_menuinit()
	log.debug("********************* on menuinit start *********************")

	-- 首先创建新菜单
	ydwe_menu_handle = gui.create_menu()

	-- 添加到菜单末尾
	gui.append_menu(gui.get_main_menu(), bit.bor(MF_STRING, MF_POPUP), ydwe_menu_handle, _("&YDWE"))

	-- 添加子菜单
	gui.append_menu(ydwe_menu_handle, MF_STRING, YDWE_MENU_ID_CONFIG, _("YDWE &Config"))
	gui.append_menu(ydwe_menu_handle, MF_STRING, YDWE_MENU_ID_COMPILE_INFO, _("Show las&t compile result"))
	gui.append_menu(ydwe_menu_handle, MF_STRING, YDWE_MENU_ID_SHOW_JASSHELPER_VERSION, _("Show J&assHelper version"))
	gui.append_menu(ydwe_menu_handle, MF_STRING, YDWE_MENU_ID_SHOW_CJASS_VERSION, _("Show c&Jass version"))
	gui.append_menu(ydwe_menu_handle, MF_STRING, YDWE_MENU_ID_OFFICIAL_SITE, _("Launch YDWE &official website"))

	if fs.exists(sys.get_editor_root_dir() / "FileAssocHelper.exe") then
		gui.append_menu(ydwe_menu_handle, MF_STRING, YDWE_MENU_ID_FILE_ASSOCIATION, _("&Set file association"))
	end

	gui.append_menu(ydwe_menu_handle, MF_STRING, YDWE_MENU_ID_FILE_EXPORT, _("&Export WE file"))
	gui.append_menu(ydwe_menu_handle, MF_STRING, YDWE_MENU_ID_ENCODING_CONVERSION, _("UTF-8 to &native"))

	gui.append_menu(ydwe_menu_handle, MF_STRING, YDWE_MENU_ID_SHOW_CREDIT, _("Cre&dits"))

	-- 刷新菜单
	gui.draw_menu_bar(gui.get_main_window())

	log.debug("********************* on menuinit end *********************")
end

-- 当WE的窗口过程收到消息时执行
function on_windowevent(window_handle, message, wparam, lparam)
	-- 只处理菜单信息
	if message == WM_COMMAND then
		-- 菜单ID（低16位）
		local menu_id = bit.band(wparam, 0xFFFF)

		-- 根据ID判断操作
		if menu_id == YDWE_MENU_ID_CONFIG then
			launch_config()
		elseif menu_id == YDWE_MENU_ID_COMPILE_INFO then
			show_last_error()
		elseif menu_id == YDWE_MENU_ID_SHOW_JASSHELPER_VERSION then
			show_jasshelper_version()
		elseif menu_id == YDWE_MENU_ID_SHOW_CJASS_VERSION then
			show_cjass_version()
		elseif menu_id == YDWE_MENU_ID_OFFICIAL_SITE then
			open_offical_site()
		elseif menu_id == YDWE_MENU_ID_FILE_ASSOCIATION then
			file_association_set()
		elseif menu_id == YDWE_MENU_ID_FILE_EXPORT then
			file_export()
		elseif menu_id == YDWE_MENU_ID_ENCODING_CONVERSION then
			encoding_conversion()
		elseif menu_id == YDWE_MENU_ID_SHOW_CREDIT then
			show_credit()
		end
	end
end

-- 启动配置对话框
function launch_config()
	local config_program = sys.get_editor_root_dir() / "EverConfig.exe"
	local command_line = string.format('"%s"', config_program:file_string())
	sys.shell_execute(command_line)
end

-- 显示最近一次编译错误
function show_last_error()
	local jasshelper_path = sys.get_editor_root_dir() / "jasshelper" / "jasshelper.exe"
	local command_line = string.format('"%s" --showerrors', jasshelper_path:file_string())
	sys.spawn_dir(command_line, sys.get_editor_root_dir())
end

-- 显示JassHelper版本
function show_jasshelper_version()
	local jasshelper_path = sys.get_editor_root_dir() / "jasshelper" / "jasshelper.exe"
	local command_line = string.format('"%s" --about', jasshelper_path:file_string())
	sys.spawn_dir(command_line, sys.get_editor_root_dir())
end

-- 显示cJass版本
function show_cjass_version()
	local cjass_path = sys.get_editor_root_dir() / "AdicHelper" / "AdicHelper.exe"
	local command_line = cjass_path:file_string()
	sys.spawn_dir(command_line, sys.get_editor_root_dir())
end

-- 打开官网
function open_offical_site()
	sys.shell_execute("http://www.ydwe.net")
end

-- 文件关联设置
function file_association_set()
	sys.shell_execute("FileAssocHelper.exe")
end

-- 导出WE文件
function file_export()
	-- 打开对话框让用户输入
	local ok, file_path_string = gui.prompt_for_input(
		0, 																		-- 父窗口句柄
		_("Export file"),														-- 标题栏
		_("Please enter the file path to export. Currently you can export only one file each time"),																				-- 提示语句
		"",																		-- 文本编辑区初始文字
		_("OK"),																-- “确定”按钮文本
		_("Cancel")																-- “取消"按钮文本
	)

	if ok then
		if ar.native_generic_file_exists(file_path_string) then
			-- 弹出选择保存文件对话框
			local ok, file = gui.choose_save_file(
				0, 														-- HWND
				"*.*\0*.*\0", 											-- Filter
				fs.path(file_path_string):filename(),					-- Initial file name
				nil, 													-- Initial Dir
				_("Save file")											-- Title
			)
			if ok then
				-- 解压文件
				ar.native_generic_extract_file(fs.path(file), file_path_string)
			end
		else
			sys.message_dialog(
				0,
				_("The file you have entered does not exist."),
				_("YDWE"),
				bit.bor(MB_ICONERROR, MB_OK)
			)
		end
	end
end

-- 编码转换
function encoding_conversion()
	local str = sys.get_clipboard_text()
	if str then
		str = __(str)
		if str then sys.set_clipboard_text(str) else iconv_utf8_to_locale:reset() end
	end
end

-- 显示制作者和感谢信息
function show_credit()
	sys.we_message_show("    ------------------------------------------------------------", 0)
	sys.we_message_show("           Welcome to YDWE, the best WE mod in China!           ", 0)
	sys.we_message_show("    ------------------------------------------------------------", 0)
	sys.we_message_show("    Director:   Everguo", 0)
	sys.we_message_show("    UI & Jass:  Fetrix_sai, Everguo, Warft_TigerCN, Wataloo & Xylign", 0)
	sys.we_message_show("    Program:    Actboy168, Aeris & Warft_TigerCN", 0)
	sys.we_message_show("    Art:        C kuhn", 0)
	sys.we_message_show("    Official Site: http://www.ydwe.net", 0)
	sys.we_message_show("    *** SPECIAL THANKS ***", 0)
	sys.we_message_show("    JassNewGenPack for ideas at www.wc3c.net", 0)
	sys.we_message_show("    Vexorian for his jasshelper compiler", 0)
	sys.we_message_show("    ADOLF and VD for their cjass compiler & TESH", 0)
	sys.we_message_show("    ...", 0)
	sys.we_message_show("    And all users & supporters, including", 0)
	sys.we_message_show("    YOU", 0)
end

-- 载入YDTrigger
-- 参数无，返回true表示成功，false表示失败
function load_yd_trigger()
	-- 结果
	local result = false
	-- YDTrigger DLL路径
	local yd_trigger_dll_path = sys.get_editor_root_dir() / "YDTrigger" / "YDTrigger.dll"
	-- YDTrigger MPQ路径
	local yd_trigger_mpq_path = sys.get_editor_root_dir() / "YDTrigger" / "YDTrigger.mpq"

	if global_config:get_integer("ScriptCompiler.EnableCJass", 0) == 0 then
		-- 是否允许?
		if global_config:get_integer("ThirdPartyPlugin.EnableYDTrigger", 1) == 1 then
			if fs.exists(yd_trigger_dll_path) and load_normal_shared_object(yd_trigger_dll_path) ~= 0 then
				if fs.exists(yd_trigger_mpq_path) and ar.native_open_archive(yd_trigger_mpq_path, 15) ~= 0 then
					result = true
					log.debug("YD Trigger loaded successfully")
				else
					log.warn("Cannot load " .. yd_trigger_mpq_path:filename())
				end
			else
				log.warn("Cannot load " .. yd_trigger_dll_path:filename())
			end
		else
			log.debug("YD Trigger disabled.")
		end
	end

	return result
end

-- WE的Hack函数，放这里避免污染全局名字空间
local wehack = {}

-- BOOL __fastcall trigger_editor_editbox_copy(const char *source)
-- hook函数，执行转码
-- source - 目标字符串
-- 返回值：TRUE(1)表示成功，FALSE(0)表示失败
function wehack.trigger_editor_editbox_copy_hook(source)
	-- 刷新配置数据
	reload_configuration()

	-- 是否转编码？
	if global_config:get_integer("FeatureToggle.EnableTriggerCopyEncodingAutoConversion", 0) == 1 and source then
		-- 转换
		source = __(source)
	end

	-- 调用原来的函数完成工作
	return wehack.original_trigger_editor_editbox_copy(source)
end

-- 初始化Hack
-- 参数无，返回值无
function initialize_we_hacks()
	-- CInvoke上下文，必须先初始化
	wehack.cinvoke_context = ccontext.new()

	-------------------------------------------------------------------------------------
	-- 自动转码

	-- 自动转码函数地址
	wehack.trigger_editor_editbox_copy = 0x71FE90
	-- 自动转码hook函数
	wehack.trigger_editor_editbox_copy_hook_callback
		= wehack.cinvoke_context:new_callback("fastcall", Clong, wehack.trigger_editor_editbox_copy_hook, Cstring)
	-- 利用TitanEngine新建一个hook
	local result = TE_HooksInsertNewRedirection(
		wehack.trigger_editor_editbox_copy,	-- hook目标地址
		mem.pointer_to_number(cinv.callback_to_ptr(wehack.trigger_editor_editbox_copy_hook_callback)), -- hook函数
		TEE_HOOK_NRM_JUMP	-- hook方式
	) and TE_HooksEnableRedirection(wehack.trigger_editor_editbox_copy, false)	-- false代表只启用一个hook而不是所有的
	if result then
		-- 通过trampoline构造原始函数
		wehack.original_trigger_editor_editbox_copy = wehack.cinvoke_context:create_function(
			"fastcall",
			Clong,
			TE_HooksGetTrampolineAddress(wehack.trigger_editor_editbox_copy),	-- 获得桥接地址
			Cstring
		)
		log.debug("Trigger editor copy hook installed successfully")
	else
		log.error("Trigger editor copy hook installing failed.")
	end

	-------------------------------------------------------------------------------------
	-- 显示内部ID

	-- 需要载入的DLL
	local hack_dll_path = sys.get_editor_root_dir() / "bin" / "YDInternalIdShow.dll"
	-- 如果允许
	if global_config:get_integer("FeatureToggle.EnableShowInternalAttributeId", 0) == 1 then
		-- 文件存在
		if fs.exists(hack_dll_path) then
			-- 载入DLL
			wehack.dll_internal_id_show = sys.load_library_by_path(hack_dll_path)
			if wehack.dll_internal_id_show ~= 0 then
				log.debug("WE internal ID show hook loaded.")
			else
				log.error(hack_dll_path:filename() .. " loading failed.")
			end
		else
			log.error(hack_dll_path:filename() .. " does not exist.")
		end
	end
end

-------------------------------------------------------------------------------------
-- 实用例程

-- 检查魔兽目录下是否有可能引起冲突的文件夹
function check_conflicting_ui()
	-- 获取魔兽路径
	local war_path = sys.get_warcraft_dir()
	-- 需要检测的目录列表
	local dir_table = {"UI", "Units"}

	-- 检测所有的目录
	for index, dir in ipairs(dir_table) do
		local dir_path = war_path / dir
		-- 如果检测出
		if fs.exists(dir_path) then
			-- 提示用户
			if sys.message_dialog(0, string.format(_("YDWE has detected that there is a directory named \"%s\" located in Warcraft 3 installation directory. It may prevent YDWE from working. Do you want to delete it?"), dir), _("YDWE"), bit.bor(MB_ICONQUESTION, MB_YESNO)) == IDYES then
				-- 删除
				pcall(fs.remove_all, dir_path)
			end
		end
	end
end

-- 清除可能引起冲突的文件
function clear_potential_conflicting()
	-- 获取魔兽路径
	local war_path = sys.get_warcraft_dir()
	-- 需要清理的文件列表
	local file_list = {"MiscData.txt", "TriggerData.txt", "TriggerStrings.txt", "WorldEditData.txt", "WorldEditLayout.txt", "WorldEditStrings.txt", "war3map.j", "Scripts/blizzard.j", "Scripts/common.j"}

	for index, file in ipairs(file_list) do
		pcall(fs.remove, war_path / file)
	end
end

-- 在新建物体的时候调用，本函数根据用户的操作决定新ID值
-- default_id - 整数类型，系统生成的ID
-- 返回值：新建物体的最终ID，必须是整数类型
function on_prompt_for_new_id(default_id)
	-- 刷新配置数据
	reload_configuration()

	-- 如果没有选择手动输入则直接返回
	if global_config:get_integer("FeatureToggle.EnableManualNewId", 0) == 0 then
		return default_id
	end

	-- 循环直到输入合法或者放弃
	while true do
		-- 打开对话框让用户输入
		local ok, id_string = gui.prompt_for_input(
			0, 																		-- 父窗口句柄
			_("New Object Id"),														-- 标题栏
			_("Please input new object ID, or cancel to use the default one."),		-- 提示语句
			sys.object_id_int_to_string(default_id),								-- 文本编辑区初始文字
			_("OK"),																-- “确定”按钮文本
			_("Cancel")																-- “取消"按钮文本
		)

		-- 用户点了确定，验证输入是否合法。如果点了取消，使用系统默认
		if ok then
			-- 检查输入是否合法（字符串长度是否为4）
			if #id_string == 4 then
				-- 合法，转换为整数返回
				return sys.object_id_string_to_int(id_string)
			else
				-- 提示错误
				sys.message_dialog(
					0,
					_("You have entered an invalid ID. The ID must contain just 4 letters or digits. It cannot contain chars other than those in ASCII."),
					_("YDWE"),
					bit.bor(MB_ICONERROR, MB_OK)
				)
			end
		else
			return default_id
		end
	end
end

--------------------------------------------------------------------------

-- 从主程序的mpq目录下载入MPQ
-- mpqname - MPQ的文件名
-- 返回值：MPQ句柄
function load_mpq_in_mpq_dir(mpqname)
	local result = 0
	-- 获取主程序所在的目录
	local editor_dir = sys.get_editor_root_dir()
	-- 定位到主程序目录下mpq目录内的MPQ文件
	local mpq = editor_dir / "mpq" /mpqname

	-- 文件存在否？
	if fs.exists(mpq) then
		result = ar.native_open_archive(mpq, 14)
		if (result ~= 0) then
			log.debug("Loaded " .. mpq:filename())
		else
			log.debug("Cannot load " .. mpq:filename())
		end
	else
		log.debug("Cannot find " .. mpq:filename())
	end

	return result
end

-- 载入一个DLL
-- so_path - dll路径
-- 返回值：DLL句柄
function load_normal_shared_object(so_path)
	local result = 0

	if fs.exists(so_path) then
		result = sys.load_library_by_path(so_path)
		if result ~= 0 then
			log.trace(string.format("%s loaded", so_path:filename()))
		else
			log.trace(string.format("Cannot load %s", so_path:filename()))
		end
	else
		log.trace(string.format("Cannot find %s", so_path:filename()))
	end

	return result
end

-- 载入.net程序集
-- path - 路径，string或者fs.path
-- 返回：成功返回Assembly，失败返回nil和错误
function assembly_load_from(path)
	-- 如果传了一个fs.path，获得string
	if type(path) == "userdata" then
		path = path:file_string()
	end

	-- 载入程序集
    local suc, res = pcall(
		function()
			return DotNet.Reflection.Assembly.LoadFrom(path)
		end
	)
    if suc then
        return res
    else
        return nil, res
    end
end

-- 是否安装了.net?
-- 自知者明(285953441) 下午 10:18:42
-- 根据Applied Microsoft .NET Framework Programming上的说法，是检查
-- %windir%\system32\MSCorEE.dll是否存在
-- TODO: 悲剧的版本问题（无法检测.net的版本）。。。
-- 返回：true表示安装，false表示没有
function is_dotnet_installed()
	local kernel32 = alien.load("kernel32.dll")
	local ExpandEnvironmentStringsA = kernel32.ExpandEnvironmentStringsA
	ExpandEnvironmentStringsA:types{ ret = "long", abi = "stdcall", "string", "pointer", "long" }
	local buffer = mem.circular_buffer(512, 0)

	ExpandEnvironmentStringsA("%SystemRoot%", buffer:linearize(), 512)
	return iox.PathFileExists(
		iox.PathAppend(
			buffer:shrunken_c_str(),
			"system32\\MSCorEE.dll"
		)
	)
end

function IsDotNet20VersionInstalled()
	local value
	local size = 4
	reg.LoadValue(reg.HKEY_LOCAL_MACHINE, "Software\\Microsoft\\NET Framework Setup\\NDP\\v2.0.50727", "Install", nil, value, size)
	return value ~= 0
end

function IsDotNet35VersionInstalled()
	local value
	local size = 4
	reg.LoadValue(reg.HKEY_LOCAL_MACHINE, "Software\\Microsoft\\NET Framework Setup\\NDP\\v3.5", "Install", nil, value, size)
	return value ~= 0
end

-- 重新载入配置内容
function reload_configuration()
	-- 配置文件存在否
	if fs.exists(global_config_file) then
		global_config:reload(global_config_file)
	end
end

-- 给地图名末尾，扩展名前添加内容
-- 效果：("abc.w3x", "def") -> "abcdef.w3x"
-- map_path_string - 地图文件名或者路径
-- some_string - 需要添加的字符串
-- 返回：字符串，见效果
function generate_aux_map_path_string_of_string(map_path_string, some_string)
	if #map_path_string >= 4 then
		return map_path_string:sub(1, -4) .. some_string .. map_path_string:sub(-4)
	else
		return some_string .. map_path_string
	end
end

-- 功能同上，但第一个参数是fs.path
function generate_aux_map_path_string_of_path(map_path, some_string)
	return generate_aux_map_path_string_of_string(map_path:file_string(), some_string)
end

-- 功能同上，但第一个参数是fs.path，并且返回fs.path
function generate_aux_map_path_of_path(map_path, some_string)
	return fs.path(generate_aux_map_path_string_of_path(map_path, some_string))
end

-- 启动并等待程序执行完毕，然后检测返回值是否为0
-- command_line - 命令行
-- start_path - 启动时的当前目录
-- 返回：true表明执行成功*并且*目标程序返回值为0，false表明执行失败*或者*目标程序返回值不是0
function spawn_dir_wait_return(command_line, start_path)
	local success, result = sys.spawn_dir_wait(command_line, start_path)
	if success then
		log.trace(string.format("Executed %s, returned %d", command_line, result))
	else
		log.trace(string.format("Executed %s failed", command_line))
	end
	return success and (result == 0)
end

-- 使用JassHelper编译地图
-- map_path - 地图路径，fs.path对象
-- common_j_path - common.j路径，fs.path对象
-- blizzard_j_path - blizzard.j路径，fs.path对象
-- option - 编译选项, table，目前支持参数：
-- 	enable_jasshelper - 启用JassHelper，true/false
--	enable_jasshelper_debug - 启用JassHelper的Debug，true/false
--	enable_jasshelper_optimization - 启用优化，true/false
-- 返回：true编译成功，false编译失败
function jasshelper_compile_map(map_path, common_j_path, blizzard_j_path, option)
	-- 确定JassHelper编译器的路径
	local jasshelper_path = sys.get_editor_root_dir() / "jasshelper" / "jasshelper.exe"
	-- 参数
	local parameter = ""

	-- 需要做vJass编译？
	if option.enable_jasshelper then
		-- debug选项（--debug）
		if option.enable_jasshelper_debug then
			parameter = parameter .. " --debug"
		end
		-- （关闭）优化选项（--nooptimize）
		if not option.enable_jasshelper_optimization then
			parameter = parameter .. " --nooptimize"
		end
	else
		-- 不编译vJass选项（--nopreprocessor）
		parameter = parameter .. " --nopreprocessor"
	end

	-- 生成命令行
	local command_line = string.format('"%s"%s "%s" "%s" "%s"',
		jasshelper_path:file_string(),
		parameter,
		common_j_path:file_string(),
		blizzard_j_path:file_string(),
		map_path:file_string()
	)

	-- 执行并获取结果
	return spawn_dir_wait_return(command_line, sys.get_editor_root_dir())
end

-- 使用cJass编译地图
-- map_path - 地图路径，fs.path对象
-- option - 附加编译选项, table，支持选项为：
--	enable_jasshelper_debug - 启用Debug模式，true/false
--	runtime_version - 魔兽版本，数
-- 返回：true编译成功，false编译失败
function cjass_compile_map(map_path, option)
	-- 确定cJass编译器的路径
	local cjass_path = sys.get_editor_root_dir() / "AdicHelper" / "AdicHelper.exe"

	-- 确定附加参数
	local parameter = option.runtime_version >= 1.24 and " /v24" or " /v23"
	parameter = parameter .. (option.enable_jasshelper_debug and " /dbg" or "")

	-- 命令行
	local command_line = string.format('"%s"%s /mappars="%s"',
		cjass_path:file_string(),
		parameter,
		map_path:file_string()
	)

	-- 执行并获取结果
	return spawn_dir_wait_return(command_line, sys.get_editor_root_dir())
end

-- 注入代码到Jass代码文件（最常见的是war3map.j）中
-- map_script_path - war3map.j的路径，fs.path对象
-- inject_code_path_table - 所有需要注入的代码文件路径，table，table中可以是
-- 		string - 此时为YDWE / "jass" 目录下的对应名称的文件
--		fs.path - 此时取其路径
-- 注：该table必须是数组形式的，哈希表形式的不处理
-- 返回值：0 - 成功；-1 - 出错失败；1 - 什么都没做
function inject_code_to_jass_script(map_script_path, inject_code_path_table)
	-- 结果
	local result = 1
	if inject_code_path_table and #inject_code_path_table > 0 then
		-- 默认成功
		result = 0
		log.trace("Writing code to " .. map_script_path:filename())
			
		-- 打开文件供写入（追加模式）
		local map_script_file, e = io.open(map_script_path:file_string(), "ab+")
		if map_script_file then
		
			if global_config:get_integer("ScriptCompiler.EnableCJass", 0) == 0 then
				map_script_file:write("#include <YDTrigger/LeakMonitorMacro.h>\r\n")
			end
			
			-- 循环处理每个需要注入的文件
			for index, inject_code_path in ipairs(inject_code_path_table) do
				local inject_code_path_string = nil
				if type(inject_code_path) == "string" then
					if inject_code_path:find("\\") or inject_code_path:find("/") then
						inject_code_path_string = inject_code_path
					else
						inject_code_path_string = (sys.get_editor_root_dir() / "jass" / inject_code_path):file_string()
					end
				else
					inject_code_path_string = inject_code_path:file_string()
				end
				-- 打开代码文件
				local f, e = io.open(inject_code_path_string, "rb")
				if f then
					log.trace("Injecting " .. inject_code_path_string)
					-- 读取所有内容
					local code_content = f:read("*a")
					-- 关闭文件
					f:close()

					-- 插入代码到原文件最后
					map_script_file:write(code_content)
					-- 写上一个换行符（我日他妈的cJass你居然不支持Linux格式的换行符）
					map_script_file:write("\r\n")
					-- 成功
					log.trace("Injection completed")
				else
					result = -1
					log.error("Error occured when reading code to inject.")
					log.error(e)
				end
			end
			
			if global_config:get_integer("ScriptCompiler.EnableCJass", 0) == 0 then
				map_script_file:write("#include <YDTrigger/LeakMonitorMacro.h>\r\n")
			end
				
			-- 关闭文件
			map_script_file:close()
		else
			result = -1
			log.error("Error occured when writing code to map script")
			log.error(e)
		end
	end

	return result
end


-- 确定应当把地图保存为适合老版本（< 1.24）还是新版本（>= 1.24）的
-- 对于保存为双份，按照当前魔兽版本处理
-- save_type - 保存类型，0到3的取值，意义同设置程序，从上到下依次为0到3
-- runtime_version - 魔兽版本，数
-- 返回值: 数，如果保存的目标版本和当前版本一致，返回当前版本，否则视情况返回1.20或者1.24（和当前版本不同）
function determine_map_version(save_type, runtime_version)
	-- 固定旧版本
	if save_type == 1 then
		-- 如果当前版本也是旧版本，返回当前版本
		if runtime_version < 1.24 then
			return runtime_version
		else
			-- 否则返回一个旧版本的典型代表（1.20）
			return 1.20
		end
	-- 固定新版本
	elseif save_type == 2 then
		-- 如果当前版本也是新版本，返回当前版本
		if runtime_version >= 1.24 then
			return runtime_version
		else
			-- 否则返回一个新版本的典型代表（1.24）
			return 1.24
		end
	else
		-- 按照当前版本或者双份
		return runtime_version
	end
end

IsInjectSlkFile = false

-- 编译地图
-- map_path - 地图路径，string或者fs.path
-- option - 编译选项，table类型，支持的选项：
--	enable_preprocessor - 启用wave预处理器，true/false
-- 	enable_jasshelper - 启用JassHelper，true/false
--	enable_jasshelper_debug - 启用JassHelper的Debug，true/false
--	enable_jasshelper_optimization - 启用优化，true/false
--	enable_cjass - 启用cJass编译器，true/false
--	script_injection - 代码注入选项，整数，0表示智能注入，1表示不注入
--	runtime_version - 魔兽版本，数
-- 返回：true表示成功，false表示失败
function compile_map(map_path, option)
	-- 统一处理fs.path
	if type(map_path) == "string" then map_path = fs.path(map_path) end
	-- 结果
	local result = nil
	
	-- 准备common.j以及blizzard.j
	local common_j_path, blizzard_j_path
	-- 是否和当前版本一致？
	if mathx.fequ(local_warcraft_version, option.runtime_version, 1e-10) then
		-- 默认从当前魔兽中解压出两个文件
		common_j_path, blizzard_j_path = prepare_jass_libs(map_path)
	else
		-- 如果目标版本和当前魔兽版本不一致，将使用默认版本
		common_j_path, blizzard_j_path = get_default_jass_libs(option.runtime_version)
	end

	-- 如果JassHelper开启，执行正常编译
	if option.enable_jasshelper then
		-- 根据注入类型决定注入内容
		IsInjectSlkFile = false
		
		result = update_map_archive_file(map_path, "war3map.j",
			-- 解压缩地图脚本，处理然后写回
			function (map_handle, in_script_path)
				-- 开始处理
				log.trace("Processing " .. in_script_path:filename())

				-- 根据注入选项进行处理（由于Lua的closure，此处可以访问“父”函数的局部变量）
				if option.script_injection == 0 then
					inject_code_to_jass_script(
						in_script_path,
						detect_code_injection(in_script_path, option)
					)
				end

				-- 开始预处理脚本
				log.trace("Preprocessing " .. in_script_path:filename())

				-- 输出脚本位置
				local out_script_path = sys.get_editor_root_dir() / "jasshelper" / "file.processed"
				-- 预处理
				--early_parse_map_script(in_script_path, out_script_path, option)

				-- Wave预处理
				local exit_code, out, err, preprocessed_path
				-- 启用cJass则不预处理
				if option.enable_cjass then
					exit_code = 0
					preprocessed_path = in_script_path
				else
					exit_code, out, err, preprocessed_path = preprocessor(in_script_path, option)
				end
				
				early_parse_map_script_2(map_handle, preprocessed_path, out_script_path)
				
				-- 退出码0代表成功
				if exit_code == 0 then
					-- 读取所有内容，以供二次替换
					local content = load_file(out_script_path)
					-- 将特殊符号替换为换行符
					if not(content and save_file(preprocessed_path, content:gsub('YD"感谢所有支持和爱护YDWE的朋友"DY', "\r\n"))) then
						sys.message_dialog(0, _("Preprocessor failed."), _("Error"), bit.bor(MB_ICONERROR, MB_OK))
						preprocessed_path = nil
					end
				else
					if out and err then
						-- 显示出错信息
						local message = string.format(_("Preprocessor failed with message:\nstdout:%s\nstderr: %s"), out, err)
						sys.message_dialog(0, message, _("Error"), bit.bor(MB_ICONERROR, MB_OK))
					else
						sys.message_dialog(0, _("Cannot start preprocessor process."), _("Error"), bit.bor(MB_ICONERROR, MB_OK))
					end
					preprocessed_path = nil
				end
				
				return preprocessed_path
			end
		)

		if IsInjectSlkFile then
			add_map_archive_file(map_path, "units\\abilitydata.slk")
			--会掉线
			--add_map_archive_file(map_path, "units\\abilitymetadata.slk")
		end
		
		-- 开始调用编译工具编译
		if result then
			-- 调用cJass
			if option.enable_cjass then
				result = cjass_compile_map(map_path, option)
				if result then
					log.debug("CJass compilation succeeded.")
				end
			end

			-- 调用jasshelepr
			if result then
				result = jasshelper_compile_map(map_path, common_j_path, blizzard_j_path, option)
				if result then
					log.debug("JassHelper compilation succeeded.")
				end
			else
				log.warn("CJass compilation failed.")
			end
		else
			log.warn("Early processing stage completed with error.")
		end
	else
		-- 只做语法检查
		result = jasshelper_compile_map(map_path, common_j_path, blizzard_j_path, option)
	end

	return result
end

-- 计算文件的MD5值
-- file_path - 文件路径, 必须是fs.path类型
-- 返回MD5值(string),出错返回nil
function md5_checksum_file(file_path)
	local content = load_file(file_path)
	return content and crypto.evp.digest("md5", content) or nil
end

-- 载入一个文件的内容
-- file_path - 文件路径, 必须是fs.path类型
-- 返回文件内容, nil表示出错
function load_file(file_path)
	local f, e = io.open(file_path:file_string(), "rb")

	if f then
		local content = f:read("*a")
		f:close()
		return content
	else
		return nil, e
	end
end

-- 保存一个文件的内容
-- file_path - 文件路径, 必须是fs.path类型
-- content - 文件内容,必须是字符串
-- 返回true表示成功,false表示失败
function save_file(file_path, content)
	local f, e = io.open(file_path:file_string(), "wb")

	if f then
		f:write(content)
		f:close()
		return true
	else
		return false, e
	end
end

-- 注入代码表
inject_code_table_new = {}
inject_code_table_old = {}

-- 扫描注入代码
-- config_dir - 需要扫描的路径
-- 返回值无，修改全局变量inject_code_table_new以及inject_code_table_old
-- inject_code_table_new - 新版（1.24）函数表
-- inject_code_table_old - 旧版函数表
function scan_for_inject_code(config_dir)
	log.trace("Scanning for inject files in " .. config_dir:file_string())

	-- 遍历目录
	for file in lfs.dir(config_dir:file_string()) do
		local full_path = config_dir / file
		local full_path_string = full_path:file_string()
		local mode = lfs.attributes(full_path_string).mode

		-- 筛选合适的文件
		if mode == "file"		-- 是普通文件
			and #file > 4							-- 文件名长度大于4
			and file:sub(-4):lower() == ".cfg"		-- 扩展名为.cfg
		then
			-- 插入新表
			local table_new = {}
			local table_old = {}

			-- 解析状态，默认0
			-- 0 - 1.24/1.20通用
			-- 1 - 1.24专用
			-- 2 - 1.20专用
			local state = 0

			-- 循环处理每一行
			for line in io.lines(full_path_string) do
				-- 插入函数名
				local trimed = line:trim()
				if trimed ~= "" and trimed:sub(1, 1) ~= "#" then
					if trimed == "[general]" then
						state = 0
					elseif trimed == "[new]" then
						state = 1
					elseif trimed == "[old]" then
						state = 2
					else
						if state == 0 then
							table.insert(table_new, trimed)
							table.insert(table_old, trimed)
						elseif state == 1 then
							table.insert(table_new, trimed)
						elseif state == 2 then
							table.insert(table_old, trimed)
						end
					end
				end
			end

			-- 插入全局表中（替换文件扩展名）
			local substitution = config_dir / (file:sub(1, -5) .. ".j")
			if #table_old > 0 then
				inject_code_table_old[substitution] = table_old
			end

			if #table_new > 0 then
				inject_code_table_new[substitution] = table_new
			end

			log.trace("Scanned " .. file)
		elseif mode == "directory"				-- 是目录
			and file ~= "." and file ~= ".."	-- 不是特殊目录
		then
			-- 递归处理
			scan_for_inject_code(full_path)
		end
	end
end

-- 侦测需要注入哪些代码
-- map_script_path - 脚本的路径，fs.path变量
-- option - 选项，table类型，支持成员：
-- 	runtime_version - 数，表示魔兽版本
-- 返回：一个table，数组形式，包含所有需要注入的文件名（注意不是fs.path）
function detect_code_injection(map_script_path, option)
	-- 结果变量
	local result = nil

	-- 读入所有文本
	local f, e = io.open(map_script_path:file_string(), "rb")
	-- 文件存在
	if f then
		-- 读入所有东西
		local s = f:read("*a")
		-- 结果
		result = {}

		-- 检查是否有需要注入的函数
		local inject_code_table = option.runtime_version >= 1.24 and inject_code_table_new or inject_code_table_old
		
		local GeneralBounsSystemFile = sys.get_editor_root_dir() / "jass" / "YDWEGeneralBounsSystem.j"
		
		for file, function_table in pairs(inject_code_table) do			
		    if GeneralBounsSystemFile:file_string() == file:file_string() then
				for i, function_name in ipairs(function_table) do
					if s:find(function_name) then
					    IsInjectSlkFile = true
						table.insert(result, file)
						break
					end
				end
			else
				for i, function_name in ipairs(function_table) do
					if s:find(function_name) then
						table.insert(result, file)
						break
					end
				end
			end
		end

		-- 关闭文件
		f:close()
	else
		log.error("Error occured when opening map script.")
		log.error(e)
	end

	return result
end

-- 准备魔兽争霸3的Jass库函数（common.j和blizzard.j）供语法检查用
-- 如果地图中有，则优先使用地图的，否则使用自带的
-- map_path - 地图路径，fs.path对象
-- 返回2个值：cj路径，bj路径，都是fs.path。
function prepare_jass_libs(map_path)
	-- 获取jasshelper目录
	local jasshelper_dir = sys.get_editor_root_dir() / "jasshelper"

	-- 预设的目标路径
	local common_j_path = jasshelper_dir / "common.j"
	local blizzard_j_path = jasshelper_dir / "blizzard.j"

	-- 从魔兽中解压缩两个重要文件到jasshelper目录（供语法检查用）
	local mpq_handle = ar.stormlib_open_archive(map_path, 0, 0)
	if mpq_handle ~= 0 then
		-- 如果地图中导入了，优先使用地图的

		if ar.stormlib_has_file(mpq_handle, "scripts\\common.j") then
			ar.stormlib_extract_file(mpq_handle, common_j_path, "scripts\\common.j")
		else
			ar.native_generic_extract_file(common_j_path, "scripts\\common.j")
		end

		if ar.stormlib_has_file(mpq_handle, "scripts\\blizzard.j") then
			ar.stormlib_extract_file(mpq_handle, blizzard_j_path, "scripts\\blizzard.j")
		else
			ar.native_generic_extract_file(blizzard_j_path, "scripts\\blizzard.j")
		end

		ar.stormlib_close_archive(mpq_handle)
	else
		ar.native_generic_extract_file(common_j_path, "scripts\\common.j")
		ar.native_generic_extract_file(blizzard_j_path, "scripts\\blizzard.j")
		log.warn("Cannot open map archive, using default bj and cj instead.")
	end

	return common_j_path, blizzard_j_path
end

-- 根据版本获取YDWE自带的Jass库函数（bj和cj）路径
-- version - 魔兽版本，数
-- 返回：cj路径，bj路径，都是fs.path
function get_default_jass_libs(version)
	if version >= 1.24 then
		return (sys.get_editor_root_dir() / "jass" / "system" / "ht" / "common.j"),
			(sys.get_editor_root_dir() / "jass" / "system" / "ht" / "blizzard.j")
	else
		return (sys.get_editor_root_dir() / "jass" / "system" / "rb" / "common.j"),
			(sys.get_editor_root_dir() / "jass" / "system" / "rb" / "blizzard.j")
	end
end

-- 解析并预处理地图代码
-- map_script_path - 地图脚本路径, fs.path对象
-- out_script_path - 输出脚本路径
-- option - 预处理选项，table，支持的值有
-- 		runtime_version - 数，表示魔兽版本
-- 返回：true - 解析成功，false - 失败
function early_parse_map_script(map_script_path, out_script_path, option)
	-- 结果
	local result = false
	-- 是新版本？
	local is_new = option.runtime_version >= 1.24

	-- 如果文件存在则处理
	if fs.exists(map_script_path) then
		-- 打开以供写入
		local f, e = io.open(out_script_path:file_string(), "wb")
		if f then
			-- 状态指示器
			-- 0 - 预处理块外
			-- 1 - new预处理块内
			-- 2 - old预处理块内
			local state = 0
			-- 是否有错误发生
			local err = false

			-- 循环处理每一行
			for line in io.lines(map_script_path:file_string()) do
				if line:match("//!%s*version%s+new") then			-- 新版预处理指示符
					if state == 0 then
						state = 1
					else
						err = true
						log.error("Map script preprocessor error: mixed new in old")
					end
				elseif line:match("//!%s*version%s+old") then		-- 旧版预处理指示符
					if state == 0 then
						state = 2
					else
						err = true
						log.error("Map script preprocessor error: mixed old in new")
					end
				elseif line:match("//!%s*version%s+end") then		-- 预处理终止符
					if state == 1 or state == 2 then
						state = 0
					else
						err = true
						log.error("Map script preprocessor error: unexpected end preprocessor symbol")
					end
				else
					if state == 0
						or (state == 1 and is_new)
						or (state == 2 and not is_new)
					then
						-- 直接写入
						f:write(line)
						-- 写上一个换行符（我日他妈的cJass你居然不支持Linux格式的换行符）
						f:write("\r\n")
					end
				end
			end

			if state ~= 0 then
				err = true
				log.error("Map script preprocessor error: unclosed preprocessor symbol")
			end

			-- 关闭文件
			f:close()

			if not err then result = true end
		else
			log.error("Error in opening output script")
			log.error(e)
		end
	else
		log.error(map_script_path .. "Does not exist!")
	end

	return result
end

-- 解析并预处理地图代码
-- map_script_path - 地图脚本路径, fs.path对象
-- out_script_path - 输出脚本路径
-- 返回：true - 解析成功，false - 失败
function early_parse_map_script_2(mpq_handle, map_script_path, out_script_path)
	-- 结果
	local result = false

	-- 如果文件存在则处理
	if fs.exists(map_script_path) then
		-- 打开以供写入
		local f, e = io.open(out_script_path:file_string(), "wb")
		local export_file = nil
		local export_file_name  = nil
		local extract_file_path = nil
		
		if f then
			-- 状态指示器
			-- 0 - 预处理块外
			-- 1 - 预处理块内
			local state = 0
			-- 是否有错误发生
			local err = false

			-- 循环处理每一行
			for line in io.lines(map_script_path:file_string()) do
				if line:match("//!%s*ydwe_export%s*%(.*%)") then
					local _,_,match_file_name = line:find("//!%s*ydwe_export%s*%((.*)%)")
					if match_file_name == "" then
						if export_file then
							export_file:close()
							export_file = nil
							ar.stormlib_add_or_replace_file(
								mpq_handle,
								extract_file_path,
								export_file_name,
								MPQ_FILE_REPLACEEXISTING)
						end
						state = 0
					else
						_,_,match_file_name = match_file_name:find("\"(.*)\"")
						if match_file_name then
							if export_file then
								export_file:close()
								export_file = nil
								ar.stormlib_add_or_replace_file(
									mpq_handle,
									extract_file_path,
									export_file_name,
									MPQ_FILE_REPLACEEXISTING)
							end
							extract_file_path = sys.get_editor_root_dir() / "logs" / "export" / match_file_name
							fs.create_directories(extract_file_path:parent_path())
							export_file, _ = io.open(extract_file_path:file_string(), "wb")
							state = 1
						else
							state = 0
						end
					end
					export_file_name = match_file_name
				else
					if state == 0 then
						-- 直接写入
						f:write(line)
						-- 写上一个换行符（我日他妈的cJass你居然不支持Linux格式的换行符）
						f:write("\r\n")
					else
						if export_file then
							export_file:write(line)
							export_file:write("\r\n")
						end
					end
				end
			end

			if state ~= 0 then
				err = true
				log.error("Map script preprocessor error: unclosed preprocessor symbol")
			end

			-- 关闭文件
			f:close()

			if export_file then
				export_file:close()
				export_file = nil
				ar.stormlib_add_or_replace_file(
						mpq_handle,
						extract_file_path,
						export_file_name,
						MPQ_FILE_REPLACEEXISTING)
			end
			if not err then result = true end
		else
			log.error("Error in opening output script")
			log.error(e)
		end
	else
		log.error(map_script_path .. "Does not exist!")
	end

	return result
end

-- 使用管道方式打开一个进程
function pipe_open(...)
	local in_rd, in_wr = io.pipe()
	local out_rd, out_wr = io.pipe()
	local err_rd, err_wr = io.pipe()
	local proc, err = os.spawn{stdin = in_rd, stdout = out_wr, stderr = err_wr, ...}

	-- 关闭管道不用的一端
	in_rd:close()
	out_wr:close()
	err_wr:close()
	if proc then
		return proc, out_rd, err_rd, in_wr
	else
		in_wr:close()
		out_rd:close()
		err_rd:close()
		return proc, err, nil, nil
	end
end


-- 预处理代码
-- code_path - 输入文件路径
-- option - 预处理选项，table，支持的值有
-- 	runtime_version - 数，表示魔兽版本
-- 	enable_jasshelper_debug - 布尔值，是否是调试模式
--	enable_yd_trigger - 布尔值，是否启用YD触发器
-- 返回：number, info, path - 子进程返回值；预处理输出信息；输出文件路径
function preprocessor(code_path, option)
	-- Wave的路径
	local wave_path = sys.get_editor_root_dir() / "wave" / "Wave.exe"
	-- 配置文件的路径
	local wave_config_file_path = sys.get_editor_root_dir() / "wave" / "WaveConfig.cfg"
	-- Jass文件路径
	local jass_dir_path = sys.get_editor_root_dir() / "jass"
	-- 输出路径
	local out_file_path = code_path:parent_path() / (code_path:stem() .. ".i")
	-- Wave系统包含路径
	local wave_sysinclude_dir = sys.get_editor_root_dir() / "wave" / "include"
	-- 强制包含文件路径
	local force_file_path = wave_sysinclude_dir / "WaveForce.i"

	-- 生成配置文件
	local f, e = io.open(wave_config_file_path:file_string(), "w")
	if f then
		-- 写入内容
		f:write("--autooutput\n")
		f:write(string.format('--sysinclude="%s"\n', wave_sysinclude_dir:file_string()))
		f:write(string.format('--sysinclude="%s"\n', (sys.get_editor_root_dir() / "YDTrigger"):file_string()))
		f:write(string.format('--include="%s"\n', jass_dir_path:file_string()))
		f:write(string.format('--define=WARCRAFT_VERSION=%d\n', 100 * option.runtime_version))
		if option.enable_jasshelper_debug then
			f:write('--define=DEBUG=1\n')
		end
		if global_config:get_integer("ScriptInjection.Option", 0) == 0 then
			f:write("--define=SCRIPT_INJECTION=1\n")
		end
		if not option.enable_yd_trigger then
			f:write('--define=DISABLE_YDTRIGGER=1\n')
		end
		if fs.exists(force_file_path) then
			f:write(string.format('--forceinclude=%s\n', force_file_path:filename()))
		end
		f:write("--extended\n--c99\n--preserve=2\n--line=0\n")
		f:close()

		-- 启动进程
		local proc, out_rd, err_rd, in_wr = pipe_open(
			string.format('"%s"', wave_path:file_string()),
			string.format('@"%s"', wave_config_file_path:file_string()),
			string.format('"%s"', code_path:file_string())
		)
		if proc then
			local out = out_rd:read("*a")
			local err = err_rd:read("*a")
			local exit_code = proc:wait()
			return exit_code, out, err, out_file_path
		else
			return -1, nil, nil, out_file_path
		end
	else
		return -1, nil, nil, out_file_path
	end
end

-- 转换为本地语言
-- str - UTF-8编码的文本ID
-- 返回：本地语言、本地编码（操作系统相关）的字符串
-- 注意：1. 找不到翻译信息的则保留英文
--       2. 本文件编码为UTF-8，因此所有硬编码的字符串均为UTF-8编码，要注意转换
function _(str)
	return iconv_utf8_to_locale:iconv(i18n.gettext(str))
end

-- 转换为本地编码
-- str - UTF-8编码的文本
-- 返回：本地编码（操作系统相关）的字符串
-- 注意：本文件编码为UTF-8，因此所有硬编码的字符串均为UTF-8编码，要注意转换
function __(str)
	return iconv_utf8_to_locale:iconv(str)
end

-- 扩展数学库
mathx = {}
-- 比较2个浮点数
-- a, b - 要比较的2个浮点数
-- eps - 精确度阈值
-- 返回值：true表示2个数的差在精确阈值内（相等），false表明不在阈值内
function mathx.fequ(a, b, eps)
	return math.abs(a - b) < eps
end

-- 初始化编码转换器
function initialize_encode_converter()
	iconv_utf8_to_locale = iconv.open(iconv.locale_charset() .. "//TRANSLIT//IGNORE", "UTF-8")
	iconv_locale_to_utf8 = iconv.open("UTF-8//TRANSLIT//IGNORE", iconv.locale_charset())
end

-- 初始化国际化组件
function initialize_gettext()
	i18n.setlocale(1729, "")	-- 1729 - LC_MESSAGES
	i18n.textdomain("evereditorinject");
	i18n.bindtextdomain("evereditorinject", (sys.get_editor_root_dir() / "share" / "locale"):string())
end

-- SMFPQ的包
sfmpq = {}

-- 默认函数
-- 总是返回FALSE
function sfmpq.dummy(...)
	return 0
end

-- 初始化sfmpq的函数
-- 参数无，返回值无
function sfmpq.initialize()
	if fs.exists(sys.get_editor_root_dir() / "bin" / "sfmpq.dll") then
		sfmpq.sfmpq_library = alien.load("bin\\sfmpq.dll")

		-- MPQHANDLE SFMPQAPI WINAPI MpqOpenArchiveForUpdate(LPCSTR lpFileName, DWORD dwFlags, DWORD dwMaximumFilesInArchive);
		sfmpq.sfmpq_library.MpqOpenArchiveForUpdate:types {"string","long","long",abi="stdcall",ret="long"}
		sfmpq.MpqOpenArchiveForUpdate = sfmpq.sfmpq_library.MpqOpenArchiveForUpdate

		-- BOOL SFMPQAPI WINAPI MpqCompactArchive(MPQHANDLE hMPQ);
		sfmpq.sfmpq_library.MpqCompactArchive:types {"long",abi="stdcall",ret="long"}
		sfmpq.MpqCompactArchive = sfmpq.sfmpq_library.MpqCompactArchive

		-- DWORD SFMPQAPI WINAPI MpqCloseUpdatedArchive(MPQHANDLE hMPQ, DWORD dwUnknown2);
		sfmpq.sfmpq_library.MpqCloseUpdatedArchive:types {"long","long",abi="stdcall",ret="long"}
		sfmpq.MpqCloseUpdatedArchive = sfmpq.sfmpq_library.MpqCloseUpdatedArchive
		
		-- BOOL SFMPQAPI WINAPI MpqAddFileToArchiveEx(MPQHANDLE hMPQ, LPCSTR lpSourceFileName, LPCSTR lpDestFileName, DWORD dwFlags, DWORD dwCompressionType, DWORD dwCompressLevel);
		sfmpq.sfmpq_library.MpqAddFileToArchiveEx:types {"long","string","string","long","long","long",abi="stdcall",ret="long"}
		sfmpq.MpqAddFileToArchiveEx = sfmpq.sfmpq_library.MpqAddFileToArchiveEx

		log.debug("SFMPQ successfully loaded")
	else
		log.warn("SFMPQ cannot be found, MPQ compact functions will not work")
	end
end
-- reg的包
reg = {KEY_READ = 0x20019, HKEY_CURRENT_USER = 0x80000001, HKEY_LOCAL_MACHINE = 0x80000002}

-- 默认函数
-- 总是返回FALSE
function reg.dummy(...)
	return 0
end

-- 初始化reg的函数
-- 参数无，返回值无
function reg.initialize()
	reg.reg_library = alien.load("ADVAPI32.DLL")
		
	reg.reg_library.RegOpenKeyExA:types {abi="stdcall";ret="long";
		"long"--[[hKey]],
		"string"--[[lpSubKey]],
		"long"--[[ulOptions]],
		"long"--[[samDesired]],
		"pointer"--[[phkResult]]}
	reg.RegOpenKeyExA = reg.reg_library.RegOpenKeyExA
	reg.reg_library.RegQueryValueExA:types {abi="stdcall";ret="long";
		"long"--[[hKey]],
		"string"--[[lpValueName]],
		"pointer"--[[lpReserved]],
		"pointer"--[[lpType]],
		"pointer"--[[lpData]],
		"pointer"--[[lpcbData]]}
	reg.RegQueryValueExA = reg.reg_library.RegQueryValueExA
	
	reg.reg_library.RegCloseKey:types {abi="stdcall";ret="long";
		"long"--[[hKey]]}
	reg.RegCloseKey = reg.reg_library.RegCloseKey
end

function reg.LoadValue(hBaseKey, szSubKey, szValueName, lpType, lpData, lpcbData)
	local hKey
	local nErrCode
	
	nErrCode = reg.RegOpenKeyExA(hBaseKey, szSubKey, 0, reg.KEY_READ, hKey)
	if (0 == nErrCode) then
		nErrCode = reg.RegQueryValueExA(hKey, szValueName, 0, lpType, lpData, lpcbData)
		reg.RegCloseKey(hKey)
	end
	return nErrCode
end


function load_yd_client(major, minor)
	local result = false
	local yd_client_dll_path = sys.get_editor_root_dir() / "bin" / "YDClient.dll"

	if fs.exists(yd_client_dll_path) then
		local YDClientDll = alien.load(yd_client_dll_path:file_string())
		if 0 ~= YDClientDll then
			YDClientDll.Start:types {abi="cdecl";ret="long"; "long"--[[nMajor]], "long"--[[nMinor]]}
			if 0 ~= YDClientDll.Start(major, minor) then
				result = true
				log.debug("YDClient.dll loaded successfully")
			else
				log.warn("Failed in YDClient.dll! Start")
			end
		else
			log.warn("Cannot load " .. yd_client_dll_path:filename())
		end
	else
		log.warn("Cannot load " .. yd_client_dll_path:filename())
	end
	return result
end

function load_yd_statistics()
	local result = false
	local yd_statistics_dll_path = sys.get_editor_root_dir() / "bin" / "YDStatistics.dll"
	local config_path = sys.get_editor_root_dir() / "EverConfig.cfg"

	if fs.exists(yd_statistics_dll_path) then
		local YDStatistics = alien.load(yd_statistics_dll_path:file_string())
		if 0 ~= YDStatistics then
			YDStatistics.PostConfig:types {abi="stdcall";ret="long"; "string" --[[lpszFileName]]}
			if 0 ~= YDStatistics.PostConfig(config_path:file_string()) then
				result = true
				log.debug("YDStatistics.dll loaded successfully")
			else
				log.warn("Failed in YDStatistics.dll! PostConfig")
			end
		else
			log.warn("Cannot load " .. yd_statistics_dll_path:filename())
		end
	else
		log.warn("Cannot load " .. yd_statistics_dll_path:filename())
	end
	return result
end

-- 添加文件给地图
-- map_path - 地图路径，fs.path
-- file_path - 需要加入的文件的路径，fs.path
-- path_in_archive - 地图压缩包中的路径，string
-- 返回值：true表示成功，false表示失败
function insert_map_archive_file(map_path, file_path, path_in_archive)
	map_path = (type(map_path) == "userdata") and map_path:file_string() or map_path
	-- 结果
	local result = false

	log.trace("Add " .. path_in_archive)
	
	-- 打开MPQ（地图）
	local mpq_handle = sfmpq.MpqOpenArchiveForUpdate(map_path, 4, 0)
	if mpq_handle ~= 0 then
		if 0 ~= sfmpq.MpqAddFileToArchiveEx(
			mpq_handle,
			file_path:file_string(),
			path_in_archive,
			0x00201,
			2,
			9
		) then
			result = true
		end

		-- 关闭地图
		sfmpq.MpqCloseUpdatedArchive(mpq_handle, 0)
	else
		log.error("Cannot open map archive " .. map_path)
	end

	return result
end

-- 添加文件给地图
-- map_path - 地图路径，fs.path
-- path_in_archive - 地图压缩包中的路径，string
-- 返回值：true表示成功，false表示失败
function add_map_archive_file(map_path, path_in_archive)
	local result = false
	local extract_file_path = sys.get_editor_root_dir() / "logs" / path_in_archive
	fs.create_directories(extract_file_path:parent_path())
	if ar.native_generic_extract_file(extract_file_path, path_in_archive) then
		result = insert_map_archive_file(map_path, extract_file_path, path_in_archive)

		-- 删除临时文件
		pcall(fs.remove_all, extract_file_path)
	else
		log.error("Cannot extract " .. path_in_archive)
	end
	return result
end

-- 从地图中解压出文件来然后调用回调函数更新
-- map_path - 地图路径，fs.path
-- path_in_archive - 地图压缩包中的路径，string
-- process_function - 函数，必须接收一个fs.path对象，返回一个fs.path对象
-- 形如 function (in_path) return out_path end
-- 返回值：true表示成功，false表示失败
function update_map_archive_file(map_path, path_in_archive, process_function)
	-- 结果
	local result = false
	log.trace("Updating " .. map_path:filename())

	-- 打开MPQ（地图）
	local mpq_handle = ar.stormlib_open_archive(map_path, 0, 0)
	if mpq_handle ~= 0 then
		-- 确定解压路径
		local extract_file_path = sys.get_editor_root_dir() / "jasshelper" / "file.out"
		-- 将文件解压
		if ar.stormlib_has_file(mpq_handle, path_in_archive) and
			ar.stormlib_extract_file(mpq_handle, extract_file_path, path_in_archive)
		then
			log.trace(path_in_archive .. " has been extracted from " .. map_path:filename())

			-- 调用处理函数处理
			local success, out_file_path = pcall(process_function, mpq_handle, extract_file_path)
			-- 如果函数正常结束（没有出错）
			if success then
				-- 如果函数成功完成任务
				if out_file_path then
					-- 替换文件
					if ar.stormlib_add_or_replace_file(
						mpq_handle,
						out_file_path,
						path_in_archive,
						MPQ_FILE_REPLACEEXISTING
					) then
						log.trace("Archive update succeeded.")
						result = true
					else
						log.error("Error occurred when write back")
					end
				else
					-- 出现了错误--
					log.error("Processor function cannot complete its task.")
				end
			else
				-- 记录出错原因
				log.error(out_file_path)
			end

			-- 删除临时文件
			pcall(fs.remove_all, extract_file_path)
		else
			log.error("Cannot extract " .. path_in_archive)
		end

		-- 关闭地图
		ar.stormlib_close_archive(mpq_handle)
	else
		log.error("Cannot open map archive" .. map_path:file_string())
	end

	return result
end

----------------------------------------------------------------------

-- 初始化代码
function initialize()
	-- 初始化编码转换器
	initialize_encode_converter()

	-- 初始化国际化组件
	initialize_gettext()

	-- 初始化配置文件
	global_config_file = sys.get_editor_root_dir() / "EverConfig.cfg"
	global_config = sys.config_property(global_config_file)
end

-- 初始化
initialize()
