├── .gitignore
├── gamedata
├── configs
│ ├── ui
│ │ ├── pda_taskboard.xml
│ │ ├── textures_descr
│ │ │ └── ui_pda_taskboard.xml
│ │ ├── pda_taskboard_16.xml
│ │ └── pda_16.xml
│ └── text
│ │ ├── rus
│ │ ├── ui_pda_taskboard.xml
│ │ └── ui_mcm_pda_taskboard.xml
│ │ └── eng
│ │ ├── ui_mcm_pda_taskboard.xml
│ │ └── ui_pda_taskboard.xml
├── textures
│ └── ui
│ │ └── taskboard_icons.dds
└── scripts
│ ├── pda_taskboard_mcm.script
│ ├── a_taskboard_utils.script
│ ├── z_taskboard_overrides.script
│ ├── optimized_time_events.script
│ └── ui_pda_taskboard_tab.script
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | meta.ini
--------------------------------------------------------------------------------
/gamedata/configs/ui/pda_taskboard.xml:
--------------------------------------------------------------------------------
1 | #include "ui\pda_taskboard_16.xml"
--------------------------------------------------------------------------------
/gamedata/textures/ui/taskboard_icons.dds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lTheon/iTheon-PDA-Taskboard/HEAD/gamedata/textures/ui/taskboard_icons.dds
--------------------------------------------------------------------------------
/gamedata/configs/text/rus/ui_pda_taskboard.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lTheon/iTheon-PDA-Taskboard/HEAD/gamedata/configs/text/rus/ui_pda_taskboard.xml
--------------------------------------------------------------------------------
/gamedata/configs/text/rus/ui_mcm_pda_taskboard.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lTheon/iTheon-PDA-Taskboard/HEAD/gamedata/configs/text/rus/ui_mcm_pda_taskboard.xml
--------------------------------------------------------------------------------
/gamedata/configs/text/eng/ui_mcm_pda_taskboard.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PDA Taskboard
6 |
7 |
8 | PDA Taskboard
9 |
10 |
11 | Scanning range
12 |
13 |
14 | Taskboard PDA shortcut
15 |
16 |
17 | Allow companions to give tasks
18 |
19 |
20 |
--------------------------------------------------------------------------------
/gamedata/scripts/pda_taskboard_mcm.script:
--------------------------------------------------------------------------------
1 | -- Default config
2 | config = {
3 | scanning_range = 100,
4 | taskboard_key = DIK_keys.DIK_J,
5 | companions_give_tasks = false
6 | }
7 |
8 | op = {
9 | id = "pda_taskboard", sh = true, gr = {
10 | {id = "banner", type = "slide", size = {512, 50}, text="ui_mcm_pda_taskboard_banner", spacing = 20},
11 | {id = "scanning_range", type = "track", val = 2, min = 50, max = 300, step = 1, def = config.scanning_range},
12 | {id = "taskboard_key", type = "key_bind", val = 2, def = config.taskboard_key},
13 | {id = "companions_give_tasks" , type = "check", val = 1, def = config.companions_give_tasks},
14 | }
15 | }
16 |
17 | function on_mcm_load()
18 | return op
19 | end
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Rafał Gawlikowski
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/gamedata/configs/text/eng/ui_pda_taskboard.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Accept task
5 |
6 |
7 | Task limit reached
8 |
9 |
10 | Refresh tasks
11 |
12 |
13 | Next task
14 |
15 |
16 | Taskboard
17 |
18 |
19 |
20 | Bounty Tasks
21 |
22 |
23 | Camp Assault Tasks
24 |
25 |
26 | Spy Extraction Tasks
27 |
28 |
29 | Delivery Tasks
30 |
31 |
32 | Measure Tasks
33 |
34 |
35 | Fate Tasks
36 |
37 |
38 | Dominance Tasks
39 |
40 |
41 | Fetch Tasks
42 |
43 |
44 | Scripted Tasks
45 |
46 |
47 | Recover Mutant Data
48 |
49 |
50 | Other Tasks
51 |
52 |
53 |
--------------------------------------------------------------------------------
/gamedata/configs/ui/textures_descr/ui_pda_taskboard.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/gamedata/scripts/a_taskboard_utils.script:
--------------------------------------------------------------------------------
1 | -- These constants need to reflect the XML setup (where applicable)
2 | local stalker_icon_height = 64
3 | local task_icon_height = 47
4 | local row_vertical_padding = 10
5 | local row_margin_bottom = 5
6 | local row_width = 764
7 | local task_category_offset = 25
8 |
9 | function adjust_rows(pda_tab)
10 | for _, row in ipairs(pda_tab.rows) do
11 | row.stalker_info:AdjustHeightToText()
12 | row.task_details_field:AdjustHeightToText()
13 | row.task_full_description_field:AdjustHeightToText()
14 |
15 | local highest_column_height = math.max(
16 | get_stalker_info_column_height(row),
17 | get_task_details_column_height(row),
18 | get_task_full_description_column_height(row)
19 | )
20 |
21 | local row_height = row_vertical_padding * 2 + row_margin_bottom + highest_column_height + task_category_offset
22 | local frame_height = row_vertical_padding * 2 + highest_column_height + task_category_offset
23 |
24 | row.frame:SetHeight(frame_height);
25 | row:SetWndSize(vector2():set(row_width, row_height))
26 | end
27 |
28 | -- Force scrollable area to resize
29 | local fake = CUIWindow()
30 | pda_tab.list:AddWindow(fake)
31 | pda_tab.list:RemoveWindow(fake)
32 | pda_tab.list:SetScrollPos(pda_tab.list:GetCurrentScrollPos())
33 | end
34 |
35 | function get_stalker_info_column_height(row)
36 | return stalker_icon_height + row.stalker_info:GetHeight()
37 | end
38 |
39 | function get_task_details_column_height(row)
40 | return task_icon_height + row.task_details_field:GetHeight()
41 | end
42 |
43 | function get_task_full_description_column_height(row)
44 | return row.task_full_description_field:GetHeight()
45 | end
--------------------------------------------------------------------------------
/gamedata/scripts/z_taskboard_overrides.script:
--------------------------------------------------------------------------------
1 | tasks_info = {}
2 | function create_overrides()
3 | local original_give_talk_message2 = db.actor.give_talk_message2
4 | db.actor.give_talk_message2 = function (...)
5 | if ActorMenu.get_pda_menu():IsShown() then
6 | local _, task_title, task_details, task_icon = ...
7 | table.insert(tasks_info, {
8 | task_title = task_title,
9 | task_details = task_details,
10 | task_icon = task_icon
11 | })
12 | else
13 | original_give_talk_message2(...)
14 | end
15 |
16 | end
17 |
18 | local original_GetTalkingNpc = mob_trade.GetTalkingNpc
19 | mob_trade.GetTalkingNpc = function (...)
20 | if ui_pda_taskboard_tab.currently_processed_npc_id then
21 | return level.object_by_id(ui_pda_taskboard_tab.currently_processed_npc_id)
22 | else
23 | return original_GetTalkingNpc(...)
24 | end
25 | end
26 |
27 | local original_is_talking = db.actor.is_talking
28 | db.actor.is_talking = function (...)
29 | if ui_pda_taskboard_tab.currently_processed_npc_id then
30 | return true
31 | else
32 | return original_is_talking(...)
33 | end
34 | end
35 |
36 | local original_get_speaker = _G.get_speaker
37 | _G.get_speaker = function (...)
38 | if ui_pda_taskboard_tab.currently_processed_npc_id then
39 | return level.object_by_id(ui_pda_taskboard_tab.currently_processed_npc_id)
40 | else
41 | return original_get_speaker(...)
42 | end
43 | end
44 |
45 | local original_set_active_subdialog = pda.set_active_subdialog
46 | pda.set_active_subdialog = function (section)
47 | if (section == "eptTaskboard") then
48 | return ui_pda_taskboard_tab.get_ui()
49 | else
50 | return original_set_active_subdialog(section)
51 | end
52 | end
53 | end
54 |
55 | function clear_tasks_info()
56 | tasks_info = {}
57 | end
58 |
59 | function push_task_info(task_info)
60 | table.insert(tasks_info, task_info)
61 | end
62 |
63 | function on_game_start()
64 | RegisterScriptCallback("on_game_load", create_overrides)
65 | end
66 |
67 |
--------------------------------------------------------------------------------
/gamedata/configs/ui/pda_taskboard_16.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ui_inGame2_pda_buttons_background
8 |
9 |
10 |
11 | ui_inGame2_pda_buttons_background
12 |
13 |
14 |
15 | ui_inGame2_pda_taskboard_accept
16 | st_pda_accept_task
17 |
18 |
19 |
20 | ui_inGame2_pda_taskboard_task_limit_reached
21 | st_pda_task_limit_reached
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ui_inGame2_pda_taskboard_next
32 | st_pda_next_task
33 |
34 |
35 |
36 | ui_inGame2_pda_taskboard_refresh
37 | st_pda_refresh_tasks
38 |
39 |
40 |
41 |
42 |
43 |
44 | ui\ui_noise
45 |
46 |
47 |
48 |
49 |
50 |
51 | ui\ui_noise
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/gamedata/configs/ui/pda_16.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ui_inGame2_pda_texture
5 |
6 | ui_inGame2_pda_buttons_leftside
7 |
8 |
9 | ui_inGame2_pda_buttons_rightside
10 |
11 |
19 |
20 |
21 |
22 | ui\ui_console
23 |
24 |
25 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | ui_inGame2_pda_offline_t
35 | ui_inGame2_pda_offline_e
36 | ui_inGame2_pda_offline_h
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | ui_icons_PDA_tooltips
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | ui_inGame2_pda_battery_bar_background
55 |
56 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
76 |
77 |
87 |
88 |
98 |
99 |
109 |
110 |
120 |
121 |
131 |
132 |
142 |
143 |
153 |
154 |
155 |
156 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # iTheon-PDA-Taskboard v1.0.5a
2 |
3 | This mod adds a new taskboard tab to the PDA where you can accept all the dynamic tasks remotely
4 |
5 | How to use it?
6 | On Taskboard tab there's a Refresh tasks button - click it once and wait for the tasks to be processed (time depends on the population density in radius, but is pretty fast).
7 | You'll get a list of tasks split into categories. Next to the task description you will see two buttons. Accept task - self-explanatory - accepting a task automatically refreshes the board, as some tasks automatically remove others from the available list (vanilla Anomaly stuff). Next task - tells PDA to prepare info about the next task in category.
8 |
9 |
10 | Possible questions:
11 | Q: Why there's no `Previous task` button?
12 | A: In short - engine limitation. It's because of problems with getting additional info about the task (middle column). To add such a button, I'd need to modify a lot of base game files and this would lead to many compatibility issues with other mods.
13 |
14 | Q: Why I can take delivery and measure tasks remotely? They give me some items
15 | A: I wanted to add a screen blackout for those tasks, but it's annoying. I think it's better without it. You can omit those tasks if it hurts your realism
16 |
17 | Q: Some task details are messed up and are showing next to the wrong task. Why?
18 | A: This bug should be eradicated by now. If you still encounter it, please report to me. EDIT: Sadly - it's not eradicated. The performance improvements achieved through refactoring caused some new issues with asynchronous events. I'll be looking into this later. For now you can be sure that task category, task giver and long description are ALWAYS right - the task details paired with them might be wrong because of async events happening in wrong order.
19 |
20 | Q: One of my tasks has no image. Why?
21 | A: Some tasks don't have image - no worries.
22 |
23 | Q: Does it work with WTF?
24 | A: Technically yes, but some people report random issues. You should report those problems either to me or to Igi.
25 |
26 | Q: Some `Search stash` or `Drug runner` tasks appear in `Bounty tasks` category. Is that a bug?
27 | A: Yes - but it's a bug in vanilla Anomaly files. These tasks need to be categorized as `Bounty tasks` because their setup function dispatches a delayed function to a wrong queue (the bounty task queue). This causes some task details to be lost if a bugged task like this is processed simultaneously with a normal bounty task.
28 |
29 | Q: Why you didn't make one full board of tasks?
30 | A: Engine limitation. I've tried doing it in several different ways, but there needs to be a delay between fetching detailed info about tasks of similar type. With no delay, specific info about the task (e. g. target name and location) will not load properly. You could accept such a task, but you wouldn't know who exactly you have to kill and where he is before you accept it. With full list - even when I've set certain delays to 10 seconds - it sometimes happened anyway. With split list, there doesn't need to be any delay.
31 |
32 | Credits:
33 | Mr. Demonized - optimized time events script
34 | https://github.com/IIJTypmaH - Russian translation
35 | https://github.com/Igigog - WTF support
36 | https://github.com/flat34 - Rostok loner guards fix
37 |
38 | Changelog:
39 |
40 | v1.0.5a
41 | - Fix string formatting CTD
42 |
43 | v1.0.5
44 | - Disable Accept Task button when task limit is reached
45 |
46 | v1.0.4
47 | - Add more exceptions to sim stalkers table.
48 | - Enable task category overrides to let modders create their own categories
49 | - Add category fallback (to 'rest') for tasks missing proper task category (missing string translation for a category)
50 |
51 | v1.0.3
52 | - Scrapped the Dominance task category, as it is dispatching messages to the same queue as Assault tasks, causing bugs with getting task details. These tasks will now appear as assault tasks.
53 | - Companions will now not give you tasks by default, but you can turn it on in MCM
54 |
55 | v1.0.2
56 | - Fix for Rostok loner guards (by flat34)
57 |
58 | v1.0.1
59 | - Fix missing string for tracking device recovery task
60 |
61 | v1.0.0
62 | - Refreshed view with custom icons
63 | - * Task category captions
64 | - * Dynamic category row heights - no more weird shit with MCM configs for row height and text trimming
65 | - * Fixed bug with double-click required for `Next task` and `Accept task` buttons to work - now one click is enough
66 | - Massive code improvements
67 | - Potential fix for messed up task details
68 | - Exclude Reefer's task from the scanning
69 |
70 | v0.1.15
71 | - Fix CTD when trying to fetch Next Task and the taskgiver has been deleted by the engine
72 |
73 | v0.1.14
74 | - Add taskboard keybinding
75 | - Change order of the tabs (place taskboard next to map)
76 |
77 | v0.1.13
78 | - Exclude Gavrilenko from the list of processed npcs
79 |
80 | v0.1.12a
81 | - Add missing xml file
82 |
83 | v0.1.12
84 | - Add a separate PDA tab for taskboard
85 |
86 | v0.1.11
87 | - Exclude Outskirts merc mechanic tools task from the list of available tasks (it's impossible to take normally and can't be finished)
88 | - Adjust the task description fetching code - now the descriptions of the fetch tasks look exactly like they do in the dialog window
89 |
90 | v0.1.10
91 | - Exclude Yar's CoC task from the list of available tasks
92 |
93 | v0.1.9
94 | - Add WTF support by Igi
95 |
96 | v0.1.8
97 | - Add options to disable text trimming and increase the task row height
98 |
99 | v0.1.7
100 | - Speculative fix for rare bug with messed up details pairing (as a side effect the task categories will now always be rendered in a sorted order)
101 |
102 | v0.1.6
103 | - Fix accepting quests that assign you a temporary companion
104 |
105 | v0.1.5
106 | - Increase maximum number of simultaneously rendered task categories
107 | - Expose normalizer table for other mods
108 |
109 | v0.1.4
110 | - Add conditional shortening of the task description and details
111 |
112 | v0.1.3
113 | - Fix weird bugs caused by refreshing board when there was a task without details on it
114 |
115 | v0.1.2a
116 | - Fix task giver override not getting cleared after processing fetch tasks
117 |
118 | v0.1.2
119 | - Add Mr. Demonized's "optimized time events" script as an integral part of the addon (required for vanilla Anomaly)
120 |
121 | v0.1.1
122 | - Fix Sidor and Forester distance evaluation
123 | - Introduce delay for refreshing the tasks
124 | - Visual tweaks
125 | - Add mcm config
126 |
--------------------------------------------------------------------------------
/gamedata/scripts/optimized_time_events.script:
--------------------------------------------------------------------------------
1 | -- Optimized time events, using sorted arrays to peek only closest event to come
2 | -- Designed to fully reimplement existing timed events with all its quirks
3 | -- Written by demonized
4 |
5 | -- Original description
6 | --------------------------------------------------------------------------------------------------
7 | -- DELAYED EVENT QUEUE
8 | -------------------------------------------------------------------------------------------------------
9 | --[[
10 | -- Events must have a unique id. Such as object id or another identifier unique to the occasion.
11 | -- Action id must be unique to the specific Event. This allows a single event to have many queued
12 | -- actions waiting to happen.
13 | --
14 | -- Returning true will remove the queued action. Returning false will execute the action continuously.
15 | -- This allows for events to wait for a specific occurrence, such as triggering after a certain amount of
16 | -- time only when object is offline
17 | --
18 | -- param 1 - Event ID as type
19 | -- param 2 - Action ID as type
20 | -- param 3 - Timer in seconds as type
21 | -- param 4 - Function to execute as type
22 | -- extra params are passed to executing function as table as param 1
23 |
24 | -- see on_game_load or state_mgr_animation.script for example uses
25 | -- This does not persists through saves! So only use for non-important things.
26 | -- For example, do not try to destroy npcs unless you do not care that it can fail before player saved then loaded.
27 | --]]
28 |
29 | local ev_queue = {}
30 | local computed_ev_queue = {}
31 |
32 | local math_floor = math.floor
33 | local math_huge = math.huge
34 |
35 | local table_insert = table.insert
36 | local table_remove = table.remove
37 |
38 | local has_alife_info = has_alife_info
39 | local time_global = time_global
40 |
41 | local pairs = pairs
42 | local unpack = unpack
43 |
44 | local tg = 0
45 |
46 | local function print_table(table, subs)
47 |
48 | local sub
49 | if subs ~= nil then
50 | sub = subs
51 | else
52 | sub = ""
53 | end
54 | for k,v in pairs(table) do
55 | if type(v) == "table" then
56 | print_table(v, sub.."["..k.."]----->")
57 | elseif type(v) == "function" then
58 | printf(sub.."%s = function",k)
59 | elseif type(v) == "userdata" then
60 | if (v.x) then
61 | printf(sub.."%s = %s",k,utils_data.vector_to_string(v))
62 | else
63 | printf(sub.."%s = userdata", k)
64 | end
65 | elseif type(v) == "boolean" then
66 | if v == true then
67 | if(type(k)~="userdata") then
68 | printf(sub.."%s = true",k)
69 | else
70 | printf(sub.."userdata = true")
71 | end
72 | else
73 | if(type(k)~="userdata") then
74 | printf(sub.."%s = false", k)
75 | else
76 | printf(sub.."userdata = false")
77 | end
78 | end
79 | else
80 | if v ~= nil then
81 | printf(sub.."%s = %s", k,v)
82 | else
83 | printf(sub.."%s = nil", k,v)
84 | end
85 | end
86 | end
87 |
88 | end
89 |
90 | local function queue_timer_compare(a, b)
91 | return a.timer < b.timer
92 | end
93 |
94 | -- http://lua-users.org/wiki/BinaryInsert
95 | local function binary_insert(t, value, fcomp)
96 | -- Initialise compare function
97 | local fcomp = fcomp or function(a, b) return a < b end
98 |
99 | -- print_table(value)
100 |
101 | -- Initialise numbers
102 | local iStart, iEnd, iMid, iState = 1, #t, 1, 0
103 |
104 | if iEnd == 0 then
105 | t[1] = value
106 | -- printf("adding in beginning table empty")
107 | return 1
108 | end
109 |
110 | if fcomp(value, t[1]) then
111 | -- printf("adding in beginning %s of %s", 1, iEnd)
112 | table_insert(t, 1, value)
113 | return 1
114 | end
115 |
116 | if not fcomp(value, t[iEnd]) then
117 | -- printf("adding in end %s of %s", iEnd + 1, iEnd)
118 | local pos = iEnd + 1
119 | t[pos] = value
120 | return pos
121 | end
122 |
123 | -- Get insert position
124 | while iStart <= iEnd do
125 |
126 | -- calculate middle
127 | iMid = math_floor((iStart + iEnd) / 2)
128 |
129 | -- compare
130 | if fcomp(value, t[iMid]) then
131 | iEnd, iState = iMid - 1, 0
132 | else
133 | iStart, iState = iMid + 1, 1
134 | end
135 | end
136 |
137 | local pos = iMid + iState
138 | -- printf("adding in middle %s of %s", pos, iEnd)
139 | table_insert(t, pos, value)
140 | return pos
141 | end
142 |
143 | local function refresh_ev_queue()
144 | empty_table(computed_ev_queue)
145 | -- tg = time_global()
146 |
147 | local t = computed_ev_queue
148 | for ev_id,actions in pairs(ev_queue) do
149 | for act_id,act in pairs(actions) do
150 | if (act_id ~= "__size") then
151 | local d = {
152 | ev_id = ev_id,
153 | act_id = act_id,
154 | timer = act.timer,
155 | f = act.f,
156 | p = act.p
157 | }
158 | binary_insert(t, d, queue_timer_compare)
159 | end
160 | end
161 | end
162 | end
163 |
164 | local function find_ev_queue(ev_id, act_id)
165 | for i = 1, #computed_ev_queue do
166 | local t = computed_ev_queue[i]
167 | if t.ev_id == ev_id and t.act_id == act_id then
168 | return i
169 | end
170 | end
171 | end
172 |
173 | local function remove_ev_queue(ev_id, act_id)
174 | local pos = find_ev_queue(ev_id, act_id)
175 | if pos then return table_remove(computed_ev_queue, pos) end
176 | end
177 |
178 | function CreateTimeEvent(ev_id,act_id,timer,f,...)
179 | if not (ev_queue[ev_id]) then
180 | ev_queue[ev_id] = {}
181 | ev_queue[ev_id].__size = 0
182 | end
183 |
184 | if not (ev_queue[ev_id][act_id]) then
185 | local new_timer = time_global() + timer*1000
186 | ev_queue[ev_id][act_id] = {}
187 | ev_queue[ev_id][act_id].timer = new_timer
188 | ev_queue[ev_id][act_id].f = f
189 | ev_queue[ev_id][act_id].p = {...}
190 | ev_queue[ev_id].__size = ev_queue[ev_id].__size + 1
191 |
192 | local d = {
193 | ev_id = ev_id,
194 | act_id = act_id,
195 | timer = new_timer,
196 | f = f,
197 | p = {...}
198 | }
199 | binary_insert(computed_ev_queue, d, queue_timer_compare)
200 | end
201 | end
202 |
203 | function RemoveTimeEvent(ev_id,act_id)
204 | if (ev_queue[ev_id] and ev_queue[ev_id][act_id]) then
205 | ev_queue[ev_id][act_id] = nil
206 | ev_queue[ev_id].__size = ev_queue[ev_id].__size - 1
207 | remove_ev_queue(ev_id, act_id)
208 | end
209 | end
210 |
211 | function ResetTimeEvent(ev_id,act_id,timer)
212 | if (ev_queue[ev_id] and ev_queue[ev_id][act_id]) then
213 | local new_timer = time_global() + timer*1000
214 | ev_queue[ev_id][act_id].timer = new_timer
215 |
216 | local el = remove_ev_queue(ev_id, act_id)
217 | el.timer = new_timer
218 | binary_insert(computed_ev_queue, el, queue_timer_compare)
219 | end
220 | end
221 |
222 | local tg_past = 0
223 | local to_remove = {}
224 | function ProcessEventQueue(force)
225 | if has_alife_info("sleep_active") then
226 | return false
227 | end
228 |
229 | tg = time_global()
230 | -- if tg > tg_past then
231 | -- printf("tg %s", tg)
232 | -- printf("computed")
233 | -- print_table(computed_ev_queue)
234 | -- tg_past = tg + 100
235 | -- end
236 |
237 | local force_refresh
238 | for i = 1, #computed_ev_queue do
239 | local t = computed_ev_queue[i] or (function()
240 | force_refresh = true
241 | return { timer = math_huge }
242 | end)() -- Failsafe if event does not exist, refresh queue and postpone to next tick
243 |
244 | if tg < t.timer then
245 | break
246 | end
247 |
248 | if t.f(unpack(t.p)) == true then
249 | local t1 = ev_queue[t.ev_id]
250 | t1[t.act_id] = nil
251 | t1.__size = t1.__size - 1
252 | if t1.__size == 0 then
253 | ev_queue[t.ev_id] = nil
254 | end
255 | to_remove[#to_remove + 1] = {
256 | ev_id = t.ev_id,
257 | act_id = t.act_id
258 | }
259 | end
260 | end
261 |
262 | if force_refresh then
263 | refresh_ev_queue()
264 | empty_table(to_remove)
265 | elseif to_remove[1] then
266 | for i = 1, #to_remove do
267 | local t = to_remove[i]
268 | remove_ev_queue(t.ev_id, t.act_id)
269 | end
270 | empty_table(to_remove)
271 | end
272 |
273 | return false
274 | end
275 |
276 | function ProcessEventQueueState(m_data,save)
277 | if (save) then
278 | m_data.event_queue = ev_queue
279 | else
280 | ev_queue = m_data.event_queue or ev_queue
281 | end
282 | refresh_ev_queue()
283 | end
284 |
285 | _G.CreateTimeEvent = CreateTimeEvent
286 | _G.RemoveTimeEvent = RemoveTimeEvent
287 | _G.ResetTimeEvent = ResetTimeEvent
288 | _G.ProcessEventQueue = ProcessEventQueue
289 | _G.ProcessEventQueueState = ProcessEventQueueState
290 |
--------------------------------------------------------------------------------
/gamedata/scripts/ui_pda_taskboard_tab.script:
--------------------------------------------------------------------------------
1 | local xml = CScriptXmlInit()
2 | xml:ParseFile("pda_taskboard.xml")
3 |
4 | local SINGLETON = nil
5 | function get_ui()
6 | SINGLETON = SINGLETON or pda_taskboard_tab()
7 | return SINGLETON
8 | end
9 | -- Constructor.
10 | class "pda_taskboard_tab" (CUIScriptWnd)
11 | function pda_taskboard_tab:__init() super()
12 | self.rows = {}
13 | self:InitControls()
14 | end
15 |
16 | -- Initialise the interface.
17 | function pda_taskboard_tab:InitControls()
18 | self:SetWndRect(Frect():set(0, 0, 1024, 768))
19 |
20 | -- Main frame.
21 | xml:ParseFile("pda_taskboard.xml")
22 | xml:InitFrame("frame1", self)
23 | xml:InitFrame("frame2", self)
24 |
25 | -- Refresh tasks button
26 | self.refresh_tasks_btn = xml:Init3tButton("btn_refresh_tasks", self)
27 | self:Register(self.refresh_tasks_btn, "refresh_tasks_btn")
28 | self:AddCallback("refresh_tasks_btn", ui_events.BUTTON_CLICKED, refresh_tasks_factory(self), self)
29 |
30 | -- Taskboard
31 | self.list = xml:InitScrollView("list", self)
32 | self:Register(self.list, "list")
33 | end
34 |
35 |
36 | ------------------- TASKBOARD
37 | class "ui_taskboard_row" (CUIWindow)
38 | function ui_taskboard_row:__init(parent, row) super(parent, row)
39 | self.frame = xml:InitFrame("frame2", self)
40 |
41 | -- Subcomponents
42 | self.task_category = xml:InitTextWnd("task_category", self)
43 | self.icon_field = xml:InitStatic("icon_field", self)
44 | self.stalker_info = xml:InitTextWnd("stalker_info", self)
45 | self.task_icon_field = xml:InitStatic("task_icon_field", self)
46 | self.task_details_field = xml:InitTextWnd("task_details_field", self)
47 | self.task_full_description_field = xml:InitTextWnd("task_full_description_field", self)
48 | self.task_accept_btn = xml:Init3tButton("btn_accept_task", self)
49 | self.task_next_btn = xml:Init3tButton("btn_next_task", self)
50 |
51 | self.task_limit_reached_btn = xml:Init3tButton("btn_task_limit_reached", self)
52 | self.task_limit_reached_btn:Show(false);
53 | self.task_limit_reached_btn:TextControl():SetTextColor(utils_xml.get_color("red",true));
54 |
55 | parent.rows[row] = self
56 |
57 | parent:Register(self.task_accept_btn, "task_accept_btn_" .. row)
58 | parent:AddCallback("task_accept_btn_" .. row, ui_events.BUTTON_CLICKED, accept_task_callback_factory(parent, row), self)
59 |
60 | parent:Register(self.task_next_btn, "task_next_btn_" .. row)
61 | parent:AddCallback("task_next_btn_" .. row, ui_events.BUTTON_CLICKED, next_task_in_category_callback_factory(parent, row), self)
62 | end
63 |
64 | local prepared_tasks_data = {}
65 | local current_board_state = {}
66 | function reset_taskboard(pda_tab)
67 | clear_taskboard_ui(pda_tab)
68 | local npc_list = get_nearby_npcs()
69 | trigger_generate_available_tasks(npc_list)
70 | prepared_tasks_data = get_prepared_task_data(npc_list)
71 |
72 | local sorted_keys = get_sorted_keys(prepared_tasks_data)
73 | -- Basically let XRay do its stuff with setting up logic
74 | for _, task_effect in ipairs(sorted_keys) do
75 | local task_data = prepared_tasks_data[task_effect][1]
76 | prepare_task( task_data )
77 | end
78 |
79 | -- Delay required for task details from actor message to be properly collected
80 | CreateTimeEvent("taskboard_delay_render", "taskboard_delay_render", 0.05, function ()
81 | -- Generate taskboard entries
82 | local i = 1
83 | for _, task_effect in ipairs(sorted_keys) do
84 | -- Save the order of the rows and currently viewed item from the category to be able to update a single row later
85 | current_board_state[i] = {
86 | task_effect = task_effect,
87 | current_index = 1
88 | }
89 | update_task_entry(pda_tab, i, prepared_tasks_data[task_effect][1])
90 | i = i + 1
91 | end
92 | a_taskboard_utils.adjust_rows(pda_tab)
93 | return true
94 | end)
95 | end
96 |
97 | currently_processed_npc_id = nil -- required for fetch quest override
98 | function prepare_task(task_data)
99 | currently_processed_npc_id = task_data.npc_id
100 | local on_job_descr = task_data.task_id and task_manager.task_ini:r_string_ex(task_data.task_id,"on_job_descr")
101 | if (on_job_descr) then
102 | local cond = xr_logic.parse_condlist(db.actor,"task_manager","condlist",on_job_descr)
103 | if (cond) then
104 | xr_logic.pick_section_from_condlist(db.actor,db.actor,cond)
105 | end
106 | end
107 |
108 | local fetch = task_data.task_id and task_manager.task_ini:r_string_ex(task_data.task_id,"fetch_descr")
109 | if (fetch) then
110 | axr_task_manager.trigger_fetch_func(task_data.task_id)
111 | end
112 | task_data.task_description = get_long_task_description(task_data)
113 |
114 | currently_processed_npc_id = nil
115 | end
116 |
117 | function get_long_task_description(task_data)
118 | -- Mechanic quests are using standard "job_descr" in the dialog manager instead of the fetch one, even though they're
119 | -- technically a fetch task. This requires a small exception
120 | local is_mechanic_task = string.find(task_data.task_id, "mechanic_task")
121 | local base_desc = game.translate_string(
122 | (not is_mechanic_task and axr_task_manager.get_fetch_task_description( task_data.task_id )) or
123 | axr_task_manager.get_task_job_description( task_data.task_id )
124 | )
125 | return strformat( base_desc, dialogs._FETCH_TEXT or "" )
126 | end
127 |
128 | function accept_task_callback_factory(pda_tab, row_index)
129 | return function()
130 | accept_task(pda_tab, row_index)
131 | end
132 | end
133 |
134 | function accept_task(pda_tab, row_index)
135 | local entry_info = current_board_state[row_index]
136 | local task_details = prepared_tasks_data[entry_info.task_effect][entry_info.current_index]
137 | currently_processed_npc_id = task_details.npc_id
138 | task_manager.get_task_manager():give_task(task_details.task_id, task_details.npc_id)
139 | currently_processed_npc_id = nil
140 | clear_cached_data()
141 | reset_taskboard(pda_tab)
142 | end
143 |
144 | function next_task_in_category_callback_factory(pda_tab, row_index)
145 | return function()
146 | next_task_in_category(pda_tab, row_index)
147 | end
148 | end
149 |
150 | function next_task_in_category(pda_tab, row_index)
151 | local entry_info = current_board_state[row_index]
152 | local next_task = prepared_tasks_data[entry_info.task_effect][entry_info.current_index + 1]
153 |
154 | if next_task then
155 | local stalker = level.object_by_id(next_task.npc_id)
156 | if stalker then
157 | prepare_task( next_task )
158 |
159 | CreateTimeEvent("next_task_delay", "next_task_delay", 0.05, function ()
160 | entry_info.current_index = entry_info.current_index + 1
161 | update_task_entry(pda_tab, row_index, next_task)
162 | a_taskboard_utils.adjust_rows(pda_tab)
163 | return true
164 | end)
165 | else
166 | -- In case the taskgiver has been deleted by the engine, reset the whole taskboard. Creating workaround
167 | -- for deleted stalkers generates several edge cases and adds unnecessary complexity to the code - resetting is easier.
168 | reset_taskboard(pda_tab)
169 | end
170 | end
171 | end
172 |
173 | function refresh_tasks_factory(pda_tab)
174 | local refresh_disabled = false
175 | local function temporary_disable_refresh()
176 | local delay = 2
177 | refresh_disabled = true
178 | CreateTimeEvent("reenable_refresh", "reenable_refresh", delay, function ()
179 | refresh_disabled = false
180 | return true
181 | end)
182 | end
183 | return function ()
184 | if (not refresh_disabled) then
185 | temporary_disable_refresh()
186 | clear_cached_data()
187 | reset_taskboard(pda_tab)
188 | end
189 | end
190 | end
191 |
192 | function clear_cached_data()
193 | z_taskboard_overrides.clear_tasks_info()
194 | prepared_tasks_data = {}
195 | current_board_state = {}
196 | end
197 |
198 | function clear_taskboard_ui(pda_tab)
199 | -- Erase all pre-existing information.
200 | pda_tab.list:Clear()
201 | pda_tab.rows = {}
202 | end
203 |
204 | local excluded_npc_sections = {"bar_duty_security_squad_leader"}
205 | function get_nearby_npcs()
206 | local radius = config.scanning_range
207 | local npc_list = {}
208 | level.iterate_nearest(db.actor:position(), radius, function (obj)
209 | local is_alive_friendly_stalker = IsStalker(obj) and obj:alive() and obj:relation(db.actor) ~= game_object.enemy
210 | local is_excluded_npc = has_value(excluded_npc_sections, obj:section())
211 | local is_companion = obj:has_info("npcx_is_companion")
212 |
213 | if
214 | is_alive_friendly_stalker
215 | and not is_excluded_npc
216 | and (is_companion and config.companions_give_tasks or not is_companion) then
217 | table.insert(npc_list, obj)
218 | end
219 | end)
220 |
221 | -- Special case for Sidorovich and Forester.
222 | local function add_by_sid(sid)
223 | local id = story_objects.object_id_by_story_id[sid]
224 | if (id) then
225 | local npc = db.storage[id] and db.storage[id].object
226 | if (npc and db.actor:position():distance_to(npc:position()) <= radius) then
227 | table.insert(npc_list, npc)
228 | end
229 | end
230 | end
231 | add_by_sid("esc_m_trader")
232 | add_by_sid("red_tech_forester")
233 |
234 | return npc_list
235 | end
236 |
237 | local actually_is_sim = {
238 | "bar_visitors_garik_stalker_guard", -- Garik
239 | "bar_visitors_zhorik_stalker_guard2", -- Zhorik
240 | "bar_arena_guard", -- Liolik
241 | "bar_dolg_general_zoneguard_stalker", -- "Get out of here stalker" guy
242 | "mil_smart_terrain_7_7_freedom_bodyguard_stalker", -- Lukash's bodyguards
243 | "mil_smart_terrain_7_7_freedom_bodyguard2_stalker" -- Lukash's bodyguards
244 | }
245 |
246 | function trigger_generate_available_tasks(npc_list)
247 | for _,npc in pairs(npc_list) do
248 | -- Sim npcs are random roaming stalkers. Non sims are traders, mechanics, etc.
249 | local is_sim = string.find(npc:section(), "sim_")
250 | currently_processed_npc_id = npc:id()
251 | if has_value(actually_is_sim, npc:section()) then
252 | axr_task_manager.generate_available_tasks(npc, not is_sim)
253 | else
254 | axr_task_manager.generate_available_tasks(npc, is_sim)
255 | end
256 | currently_processed_npc_id = nil
257 | end
258 | end
259 |
260 | -- Tasks that should not be picked by the taskboard (e. g. ones that are not accessible without it)
261 | local excluded_tasks = {
262 | "jup_b19_freedom_yar_task_1", -- Yar's CoC task - bugged
263 | "merc_pri_a18_mech_mlr_task_1", -- Outskirts merc mechanic task for tools - bugged
264 | "agr_u_bandit_boss_task_1" -- Reefer's task (Agro underground) - it doesn't use the standard setup functions and breaks the board logic
265 | }
266 |
267 | function get_prepared_task_data(npc_list)
268 | -- Split available tasks into categories based on their init effect
269 | local result = {}
270 | for _,stalker in pairs(npc_list) do
271 | local stalker_task_list = axr_task_manager.available_tasks[stalker:id()] or {}
272 | for _, task_id in pairs(stalker_task_list) do
273 | local task_effect, has_details = get_task_effect(task_id)
274 | if not has_value(excluded_tasks, task_id) then
275 | if not result[task_effect] then
276 | result[task_effect] = {}
277 | end
278 | table.insert(result[task_effect], {
279 | npc_id = stalker:id(),
280 | task_id = task_id,
281 | has_details = has_details
282 | })
283 | end
284 | end
285 | end
286 | return result
287 | end
288 |
289 | function get_task_effect(task_id) -- (task_id: string) => task_effect: string, has_details: boolean
290 | -- Support for mods that add more tasks and dispatch effects of their own and can't be categorized using the base Anomaly task types
291 | local task_category_override = task_manager.task_ini:r_string_ex(task_id,"task_category_override")
292 | if task_category_override then return task_category_override end
293 |
294 |
295 | 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")
296 | local task_effect = string.gmatch(raw_task_effect or "", "=(.*)%(")() or "rest"
297 | task_effect = normalize_task_effect(task_effect)
298 |
299 | -- If there's no category string defined for a given effect/category override, default to `rest` to avoid ugly strings
300 | -- Also makes it easy for modders to just dump all their tasks into `Other tasks` category by simply not defining category override in ini
301 | local has_invalid_category = string.find(game.translate_string('st_pda_caption_' .. task_effect), '_')
302 | if has_invalid_category then
303 | return "rest", not not raw_task_effect
304 | else
305 | return task_effect, not not raw_task_effect
306 | end
307 | end
308 |
309 | function update_task_entry(pda_tab, i, task_details)
310 | if (not pda_tab.rows[i]) then
311 | pda_tab.list:AddWindow(ui_taskboard_row(pda_tab, i))
312 | end
313 | local row = pda_tab.rows[i]
314 |
315 | row.task_category:SetText(game.translate_string('st_pda_caption_' .. current_board_state[i].task_effect))
316 |
317 | local stalker = level.object_by_id(task_details.npc_id)
318 |
319 | local stalker_comm = stalker:character_community()
320 | local stalker_icon = stalker:character_icon()
321 | local stalker_name = stalker:character_name()
322 |
323 | stalker_icon = stalker_icon and stalker_icon ~= "" and stalker_icon or "ui\\ui_noise"
324 | row.icon_field:InitTexture(stalker_icon)
325 |
326 | local stalker_info = stalker_name .. "\\n" ..
327 | game.translate_string("ui_st_community") .. ": " .. game.translate_string(stalker_comm)
328 | row.stalker_info:SetText(stalker_info)
329 |
330 | local more_task_details = get_more_task_details(i) or {}
331 | row.task_icon_field:InitTexture(more_task_details.task_icon or "ui\\ui_noise")
332 |
333 | local details = (more_task_details.task_title or "") ..
334 | "\\n" ..
335 | (string.gsub(more_task_details.task_details or "", "\\n ", "\\n"))
336 | row.task_details_field:SetText(details)
337 |
338 | local full_desc = task_details.task_description or ""
339 | row.task_full_description_field:SetText(full_desc)
340 | row.task_next_btn:Show(has_more_task_in_category(i))
341 | manage_accept_button(stalker, row)
342 | end
343 |
344 | function manage_accept_button(stalker, row)
345 | local reached_limit = dialogs.has_tasks_by_npc(stalker)
346 | if reached_limit then
347 | row.task_limit_reached_btn:Show(true)
348 | row.task_accept_btn:Show(false)
349 | else
350 | row.task_limit_reached_btn:Show(false)
351 | row.task_accept_btn:Show(true)
352 | end
353 | end
354 |
355 | function has_more_task_in_category(i)
356 | local entry_info = current_board_state[i]
357 | local next_task = prepared_tasks_data[entry_info.task_effect][entry_info.current_index + 1]
358 | return not not next_task
359 | end
360 |
361 | function get_igi_task_details(i)
362 | if not igi_generic_task then return end
363 | if not (igi_description and igi_description.get_task_text_values) then return end
364 |
365 | local task_id = prepared_tasks_data.rest[i] and prepared_tasks_data.rest[i].task_id
366 | local CACHE = task_id and igi_generic_task.TASK_SETUP[task_id]
367 | if CACHE then
368 | local title, text, icon = igi_description.get_task_text_values(CACHE)
369 | return {
370 | task_title = title,
371 | task_details = text,
372 | task_icon = icon
373 | }
374 | end
375 | end
376 |
377 | -- This function is a pure fuckery and crime against the code, but there's literally no other way to couple the
378 | -- task data with the details received through overridden actor method.
379 | function get_more_task_details(i)
380 | local entry_info = current_board_state[i]
381 | local task_data = prepared_tasks_data[entry_info.task_effect][entry_info.current_index]
382 | if entry_info.current_index == 1 then
383 | -- 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
384 | -- deviations from that rule for now)
385 | if(task_data.has_details) then
386 | return z_taskboard_overrides.tasks_info[i - get_processed_tasks_with_no_info_count(i)]
387 | elseif igi_generic_task then
388 | return get_igi_task_details(entry_info.current_index)
389 | else return end
390 | else
391 | -- If new task has been requested, the details will be appended at the end of the array, so we can easily read them
392 | if(task_data.has_details) then
393 | return z_taskboard_overrides.tasks_info[#z_taskboard_overrides.tasks_info]
394 | elseif igi_generic_task then
395 | return get_igi_task_details(entry_info.current_index)
396 | else return end
397 | end
398 | end
399 |
400 | function get_processed_tasks_with_no_info_count(i)
401 | local count = 0
402 | for index, entry_info in ipairs(current_board_state) do
403 | local task_data = prepared_tasks_data[entry_info.task_effect][entry_info.current_index]
404 | if index < i and not task_data.has_details then
405 | count = count + 1
406 | end
407 | end
408 | return count
409 | end
410 |
411 | function has_value(tab, val)
412 | for index, value in ipairs(tab) do
413 | if value == val then
414 | return true
415 | end
416 | end
417 |
418 | return false
419 | end
420 |
421 | function get_sorted_keys(tab)
422 | local result = {}
423 | for key, _ in pairs(tab) do
424 | table.insert(result, key)
425 | end
426 | table.sort(result)
427 | return result
428 | end
429 |
430 | -- Some effects have different names even though they perform pretty much the same actions as the generic/simulation task effects
431 | -- Those should be pushed into the same category to prevent queued actor messages from being discarded
432 | normalizer = {
433 | setup_supplies_fetch_task_lostzone_patch = "setup_fetch_task",
434 | setup_generic_fetch_task = "setup_fetch_task",
435 | drx_sl_create_quest_stash = "setup_bounty_task", -- The actor message in base vanilla files is sent to the wrong queue here
436 | setup_dominance_task = "setup_assault_task", -- Same as above - this actually scraps the whole Dominance category and puts these tasks as assault tasks
437 | multifetch_on_job_descr = "setup_fetch_task" -- Unnecessary thing, but there's only one task in this category
438 | }
439 | function normalize_task_effect(task_effect)
440 | return normalizer[task_effect] or task_effect
441 | end
442 |
443 | local function load_defaults()
444 | local t = {}
445 | local op = pda_taskboard_mcm.op
446 | for i, v in ipairs(op.gr) do
447 | if v.def ~= nil then
448 | t[v.id] = v.def
449 | end
450 | end
451 | return t
452 | end
453 | -- Default config
454 | config = load_defaults()
455 |
456 | local function load_settings()
457 | config = load_defaults()
458 | if ui_mcm then
459 | for k, v in pairs(config) do
460 | config[k] = ui_mcm.get("pda_taskboard/" .. k)
461 | end
462 | end
463 | end
464 |
465 | function on_key_release(key)
466 | if key == config.taskboard_key then
467 | local pda_menu = ActorMenu.get_pda_menu()
468 | local pda3d = get_console_cmd(1,"g_3d_pda")
469 | if not (pda_menu:IsShown()) and db.actor:item_in_slot(8) then
470 | if (pda3d) then
471 | db.actor:activate_slot(8)
472 | else
473 | pda_menu:ShowDialog(true)
474 | end
475 |
476 | pda_menu:SetActiveSubdialog("eptTaskboard")
477 | elseif (pda_menu:IsShown()) then
478 | if (pda3d) then
479 | db.actor:activate_slot(0)
480 | else
481 | pda_menu:HideDialog()
482 | end
483 | end
484 | end
485 | end
486 |
487 | function on_game_start()
488 | RegisterScriptCallback("on_game_load", load_settings)
489 | RegisterScriptCallback("on_option_change", load_settings)
490 | RegisterScriptCallback("on_key_release",on_key_release)
491 | end
492 |
--------------------------------------------------------------------------------