local xml = CScriptXmlInit() xml:ParseFile("pda_taskboard.xml") local SINGLETON = nil function get_ui() SINGLETON = SINGLETON or pda_taskboard_tab() return SINGLETON end -- Constructor. class "pda_taskboard_tab" (CUIScriptWnd) function pda_taskboard_tab:__init() super() self.rows = {} self:InitControls() end -- Initialise the interface. function pda_taskboard_tab:InitControls() self:SetWndRect(Frect():set(0, 0, 1024, 768)) -- Main frame. xml:ParseFile("pda_taskboard.xml") xml:InitFrame("frame1", self) xml:InitFrame("frame2", self) -- Refresh tasks button self.refresh_tasks_btn = xml:Init3tButton("btn_refresh_tasks", self) self:Register(self.refresh_tasks_btn, "refresh_tasks_btn") self:AddCallback("refresh_tasks_btn", ui_events.BUTTON_CLICKED, refresh_tasks_factory(self), self) -- Taskboard self.list = xml:InitScrollView("list", self) self:Register(self.list, "list") end ------------------- TASKBOARD class "ui_taskboard_row" (CUIWindow) function ui_taskboard_row:__init(parent, row) super(parent, row) self.frame = xml:InitFrame("frame2", self) -- Subcomponents self.task_category = xml:InitTextWnd("task_category", self) self.icon_field = xml:InitStatic("icon_field", self) self.stalker_info = xml:InitTextWnd("stalker_info", self) self.task_icon_field = xml:InitStatic("task_icon_field", self) self.task_details_field = xml:InitTextWnd("task_details_field", self) self.task_full_description_field = xml:InitTextWnd("task_full_description_field", self) self.task_accept_btn = xml:Init3tButton("btn_accept_task", self) self.task_next_btn = xml:Init3tButton("btn_next_task", self) parent.rows[row] = self parent:Register(self.task_accept_btn, "task_accept_btn_" .. row) parent:AddCallback("task_accept_btn_" .. row, ui_events.BUTTON_CLICKED, accept_task_callback_factory(parent, row), self) parent:Register(self.task_next_btn, "task_next_btn_" .. row) parent:AddCallback("task_next_btn_" .. row, ui_events.BUTTON_CLICKED, next_task_in_category_callback_factory(parent, row), self) end local prepared_tasks_data = {} local current_board_state = {} function reset_taskboard(pda_tab) clear_taskboard_ui(pda_tab) local npc_list = get_nearby_npcs() trigger_generate_available_tasks(npc_list) prepared_tasks_data = get_prepared_task_data(npc_list) local sorted_keys = get_sorted_keys(prepared_tasks_data) -- Basically let XRay do its stuff with setting up logic for _, task_effect in ipairs(sorted_keys) do local task_data = prepared_tasks_data[task_effect][1] prepare_task( task_data ) end -- Delay required for task details from actor message to be properly collected CreateTimeEvent("taskboard_delay_render", "taskboard_delay_render", 0.05, function () -- Generate taskboard entries local i = 1 for _, task_effect in ipairs(sorted_keys) do -- Save the order of the rows and currently viewed item from the category to be able to update a single row later current_board_state[i] = { task_effect = task_effect, current_index = 1 } update_task_entry(pda_tab, i, prepared_tasks_data[task_effect][1]) i = i + 1 end a_taskboard_utils.adjust_rows(pda_tab) return true end) end currently_processed_npc_id = nil -- required for fetch quest override function prepare_task(task_data) currently_processed_npc_id = task_data.npc_id local on_job_descr = task_data.task_id and task_manager.task_ini:r_string_ex(task_data.task_id,"on_job_descr") if (on_job_descr) then local cond = xr_logic.parse_condlist(db.actor,"task_manager","condlist",on_job_descr) if (cond) then xr_logic.pick_section_from_condlist(db.actor,db.actor,cond) end end local fetch = task_data.task_id and task_manager.task_ini:r_string_ex(task_data.task_id,"fetch_descr") if (fetch) then axr_task_manager.trigger_fetch_func(task_data.task_id) end task_data.task_description = get_long_task_description(task_data) currently_processed_npc_id = nil end function get_long_task_description(task_data) -- Mechanic quests are using standard "job_descr" in the dialog manager instead of the fetch one, even though they're -- technically a fetch task. This requires a small exception local is_mechanic_task = string.find(task_data.task_id, "mechanic_task") local base_desc = game.translate_string( (not is_mechanic_task and axr_task_manager.get_fetch_task_description( task_data.task_id )) or axr_task_manager.get_task_job_description( task_data.task_id ) ) return string.format( base_desc, dialogs._FETCH_TEXT or "" ) end function accept_task_callback_factory(pda_tab, row_index) return function() accept_task(pda_tab, row_index) end end function accept_task(pda_tab, row_index) local entry_info = current_board_state[row_index] local task_details = prepared_tasks_data[entry_info.task_effect][entry_info.current_index] currently_processed_npc_id = task_details.npc_id task_manager.get_task_manager():give_task(task_details.task_id, task_details.npc_id) currently_processed_npc_id = nil clear_cached_data() reset_taskboard(pda_tab) end function next_task_in_category_callback_factory(pda_tab, row_index) return function() next_task_in_category(pda_tab, row_index) end end function next_task_in_category(pda_tab, row_index) local entry_info = current_board_state[row_index] local next_task = prepared_tasks_data[entry_info.task_effect][entry_info.current_index + 1] if next_task then local stalker = level.object_by_id(next_task.npc_id) if stalker then prepare_task( next_task ) CreateTimeEvent("next_task_delay", "next_task_delay", 0.05, function () entry_info.current_index = entry_info.current_index + 1 update_task_entry(pda_tab, row_index, next_task) a_taskboard_utils.adjust_rows(pda_tab) return true end) else -- In case the taskgiver has been deleted by the engine, reset the whole taskboard. Creating workaround -- for deleted stalkers generates several edge cases and adds unnecessary complexity to the code - resetting is easier. reset_taskboard(pda_tab) end end end function refresh_tasks_factory(pda_tab) local refresh_disabled = false local function temporary_disable_refresh() local delay = 2 refresh_disabled = true CreateTimeEvent("reenable_refresh", "reenable_refresh", delay, function () refresh_disabled = false return true end) end return function () if (not refresh_disabled) then temporary_disable_refresh() clear_cached_data() reset_taskboard(pda_tab) end end end function clear_cached_data() z_taskboard_overrides.clear_tasks_info() prepared_tasks_data = {} current_board_state = {} end function clear_taskboard_ui(pda_tab) -- Erase all pre-existing information. pda_tab.list:Clear() pda_tab.rows = {} end local excluded_npc_sections = {"bar_duty_security_squad_leader"} function get_nearby_npcs() local radius = config.scanning_range local npc_list = {} level.iterate_nearest(db.actor:position(), radius, function (obj) local is_alive_friendly_stalker = IsStalker(obj) and obj:alive() and obj:relation(db.actor) ~= game_object.enemy local is_excluded_npc = has_value(excluded_npc_sections, obj:section()) local is_companion = obj:has_info("npcx_is_companion") if is_alive_friendly_stalker and not is_excluded_npc and (is_companion and config.companions_give_tasks or not is_companion) then table.insert(npc_list, obj) end end) -- Special case for Sidorovich and Forester. local function add_by_sid(sid) local id = story_objects.object_id_by_story_id[sid] if (id) then local npc = db.storage[id] and db.storage[id].object if (npc and db.actor:position():distance_to(npc:position()) <= radius) then table.insert(npc_list, npc) end end end add_by_sid("esc_m_trader") add_by_sid("red_tech_forester") return npc_list end local actually_is_sim = { "bar_visitors_garik_stalker_guard", -- Garik "bar_visitors_zhorik_stalker_guard2", -- Zhorik "bar_arena_guard", -- Liolik "bar_dolg_general_zoneguard_stalker", -- "Get out of here stalker" guy "mil_smart_terrain_7_7_freedom_bodyguard_stalker" -- Lukash's bodyguards } function trigger_generate_available_tasks(npc_list) for _,npc in pairs(npc_list) do -- Sim npcs are random roaming stalkers. Non sims are traders, mechanics, etc. local is_sim = string.find(npc:section(), "sim_") currently_processed_npc_id = npc:id() if has_value(actually_is_sim, npc:section()) then axr_task_manager.generate_available_tasks(npc, not is_sim) else axr_task_manager.generate_available_tasks(npc, is_sim) end currently_processed_npc_id = nil end end -- Tasks that should not be picked by the taskboard (e. g. ones that are not accessible without it) local excluded_tasks = { "jup_b19_freedom_yar_task_1", -- Yar's CoC task - bugged "merc_pri_a18_mech_mlr_task_1", -- Outskirts merc mechanic task for tools - bugged "agr_u_bandit_boss_task_1" -- Reefer's task (Agro underground) - it doesn't use the standard setup functions and breaks the board logic } function get_prepared_task_data(npc_list) -- Split available tasks into categories based on their init effect local result = {} for _,stalker in pairs(npc_list) do local stalker_task_list = axr_task_manager.available_tasks[stalker:id()] or {} for _, task_id in pairs(stalker_task_list) do local task_effect, has_details = get_task_effect(task_id) if not has_value(excluded_tasks, task_id) then if not result[task_effect] then result[task_effect] = {} end table.insert(result[task_effect], { npc_id = stalker:id(), task_id = task_id, has_details = has_details }) end end end return result end function get_task_effect(task_id) -- (task_id: string) => task_effect: string, has_details: boolean -- Support for mods that add more tasks and dispatch effects of their own and can't be categorized using the base Anomaly task types local task_category_override = task_manager.task_ini:r_string_ex(task_id,"task_category_override") if task_category_override then return task_category_override end local raw_task_effect = task_manager.task_ini:r_string_ex(task_id,"on_job_descr") or task_manager.task_ini:r_string_ex(task_id,"fetch_func") local task_effect = string.gmatch(raw_task_effect or "", "=(.*)%(")() or "rest" task_effect = normalize_task_effect(task_effect) -- If there's no category string defined for a given effect/category override, default to `rest` to avoid ugly strings -- Also makes it easy for modders to just dump all their tasks into `Other tasks` category by simply not defining category override in ini local has_invalid_category = string.find(game.translate_string('st_pda_caption_' .. task_effect), '_') if has_invalid_category then return "rest", not not raw_task_effect else return task_effect, not not raw_task_effect end end function update_task_entry(pda_tab, i, task_details) if (not pda_tab.rows[i]) then pda_tab.list:AddWindow(ui_taskboard_row(pda_tab, i)) end pda_tab.rows[i].task_category:SetText(game.translate_string('st_pda_caption_' .. current_board_state[i].task_effect)) local stalker = level.object_by_id(task_details.npc_id) local stalker_comm = stalker:character_community() local stalker_icon = stalker:character_icon() local stalker_name = stalker:character_name() stalker_icon = stalker_icon and stalker_icon ~= "" and stalker_icon or "ui\\ui_noise" pda_tab.rows[i].icon_field:InitTexture(stalker_icon) local stalker_info = stalker_name .. "\\n" .. game.translate_string("ui_st_community") .. ": " .. game.translate_string(stalker_comm) pda_tab.rows[i].stalker_info:SetText(stalker_info) local more_task_details = get_more_task_details(i) or {} pda_tab.rows[i].task_icon_field:InitTexture(more_task_details.task_icon or "ui\\ui_noise") local details = (more_task_details.task_title or "") .. "\\n" .. (string.gsub(more_task_details.task_details or "", "\\n ", "\\n")) pda_tab.rows[i].task_details_field:SetText(details) local full_desc = task_details.task_description or "" pda_tab.rows[i].task_full_description_field:SetText(full_desc) pda_tab.rows[i].task_next_btn:Show(has_more_task_in_category(i)) end function has_more_task_in_category(i) local entry_info = current_board_state[i] local next_task = prepared_tasks_data[entry_info.task_effect][entry_info.current_index + 1] return not not next_task end function get_igi_task_details(i) if not igi_generic_task then return end if not (igi_description and igi_description.get_task_text_values) then return end local task_id = prepared_tasks_data.rest[i] and prepared_tasks_data.rest[i].task_id local CACHE = task_id and igi_generic_task.TASK_SETUP[task_id] if CACHE then local title, text, icon = igi_description.get_task_text_values(CACHE) return { task_title = title, task_details = text, task_icon = icon } end end -- This function is a pure fuckery and crime against the code, but there's literally no other way to couple the -- task data with the details received through overridden actor method. function get_more_task_details(i) local entry_info = current_board_state[i] local task_data = prepared_tasks_data[entry_info.task_effect][entry_info.current_index] if entry_info.current_index == 1 then -- If no new task has been requested for a given category, index of details will be the same as the index of row (or at least I didn't observe any -- deviations from that rule for now) if(task_data.has_details) then return z_taskboard_overrides.tasks_info[i - get_processed_tasks_with_no_info_count(i)] elseif igi_generic_task then return get_igi_task_details(entry_info.current_index) else return end else -- If new task has been requested, the details will be appended at the end of the array, so we can easily read them if(task_data.has_details) then return z_taskboard_overrides.tasks_info[#z_taskboard_overrides.tasks_info] elseif igi_generic_task then return get_igi_task_details(entry_info.current_index) else return end end end function get_processed_tasks_with_no_info_count(i) local count = 0 for index, entry_info in ipairs(current_board_state) do local task_data = prepared_tasks_data[entry_info.task_effect][entry_info.current_index] if index < i and not task_data.has_details then count = count + 1 end end return count end function has_value(tab, val) for index, value in ipairs(tab) do if value == val then return true end end return false end function get_sorted_keys(tab) local result = {} for key, _ in pairs(tab) do table.insert(result, key) end table.sort(result) return result end -- Some effects have different names even though they perform pretty much the same actions as the generic/simulation task effects -- Those should be pushed into the same category to prevent queued actor messages from being discarded normalizer = { setup_supplies_fetch_task_lostzone_patch = "setup_fetch_task", setup_generic_fetch_task = "setup_fetch_task", drx_sl_create_quest_stash = "setup_bounty_task", -- The actor message in base vanilla files is sent to the wrong queue here setup_dominance_task = "setup_assault_task", -- Same as above - this actually scraps the whole Dominance category and puts these tasks as assault tasks multifetch_on_job_descr = "setup_fetch_task" -- Unnecessary thing, but there's only one task in this category } function normalize_task_effect(task_effect) return normalizer[task_effect] or task_effect end local function load_defaults() local t = {} local op = pda_taskboard_mcm.op for i, v in ipairs(op.gr) do if v.def ~= nil then t[v.id] = v.def end end return t end -- Default config config = load_defaults() local function load_settings() config = load_defaults() if ui_mcm then for k, v in pairs(config) do config[k] = ui_mcm.get("pda_taskboard/" .. k) end end end function on_key_release(key) if key == config.taskboard_key then local pda_menu = ActorMenu.get_pda_menu() local pda3d = get_console_cmd(1,"g_3d_pda") if not (pda_menu:IsShown()) and db.actor:item_in_slot(8) then if (pda3d) then db.actor:activate_slot(8) else pda_menu:ShowDialog(true) end pda_menu:SetActiveSubdialog("eptTaskboard") elseif (pda_menu:IsShown()) then if (pda3d) then db.actor:activate_slot(0) else pda_menu:HideDialog() end end end end function on_game_start() RegisterScriptCallback("on_game_load", load_settings) RegisterScriptCallback("on_option_change", load_settings) RegisterScriptCallback("on_key_release",on_key_release) end