├── .gitattributes
├── .gitignore
├── .justfile
├── .luarc.json
├── DOC.md
├── LICENSE.md
├── README.md
├── gamedata
├── configs
│ ├── igi_tasks
│ │ ├── base.ltx
│ │ └── dialogs
│ │ │ └── gitkeep.xml
│ └── text
│ │ ├── eng
│ │ ├── igi_task_text_basic.xml
│ │ └── igi_task_text_mcm.xml
│ │ └── rus
│ │ ├── igi_task_text_basic.xml
│ │ └── igi_task_text_mcm.xml
└── scripts
│ ├── igi_actions.script
│ ├── igi_ara.script
│ ├── igi_callbacks.script
│ ├── igi_description.script
│ ├── igi_dialogs.script
│ ├── igi_finder.script
│ ├── igi_generate.script
│ ├── igi_generic_task.script
│ ├── igi_helper.script
│ ├── igi_json.script
│ ├── igi_macros.script
│ ├── igi_mcm.script
│ ├── igi_mcm_builder.script
│ ├── igi_random.script
│ ├── igi_rewards.script
│ ├── igi_subtask.script
│ ├── igi_target_assault.script
│ ├── igi_target_escort.script
│ ├── igi_target_fetch.script
│ ├── igi_target_get.script
│ ├── igi_target_kill.script
│ ├── igi_target_return.script
│ ├── igi_target_shoot.script
│ ├── igi_target_visit.script
│ ├── igi_task_manager.script
│ ├── igi_taskdata.script
│ ├── igi_tests.script
│ ├── igi_text_processor.script
│ ├── igi_utils.script
│ ├── modxml_wtf.script
│ └── wtf.script
├── release.sh
├── test_framework.bat
└── upload_asset.sh
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Override language detection
2 | *.script linguist-language=Lua
3 | *.ltx linguist-language=INI
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.zip
2 | *.rar
3 | taskmaker/task
4 | taskmaker/__pycache__
5 | .vscode
6 | out.txt
7 | run.lua
--------------------------------------------------------------------------------
/.justfile:
--------------------------------------------------------------------------------
1 | set shell := ["powershell.exe", "-c"]
2 |
3 | run:
4 | ..\..\ModOrganizer.exe "moshortcut://:Anomaly (DX11-AVX)"
5 |
6 | mo2:
7 | ..\..\ModOrganizer.exe
8 |
9 | pack:
10 | #!/usr/bin/sh
11 | VERSION=$(grep '^TASKS_VERSION =' gamedata/scripts/igi_generic_task.script | sed 's/TASKS_VERSION = "\(.*\)".*/\1/')
12 | cd ..
13 | 7z a -tzip "WTF_$VERSION.zip" Weird_Tasks_Framework/gamedata GhenTuong_Task_Pack/gamedata Arszi_Task_Pack/gamedata community-task-pack/gamedata
14 |
--------------------------------------------------------------------------------
/.luarc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
3 | "Lua.diagnostics.disable": [
4 | "lowercase-global"
5 | ],
6 | "Lua.diagnostics.globals": [
7 | "igi_helper",
8 | "igi_random",
9 | "igi_activities",
10 | "character_community",
11 | "RegisterScriptCallback",
12 | "igi_callbacks",
13 | "printf",
14 | "igi_mcm",
15 | "db",
16 | "distance_2d_sqr",
17 | "igi_target",
18 | "alife_character_community",
19 | "CreateTimeEvent",
20 | "igi_subtask",
21 | "igi_setup",
22 | "igi_taskdata",
23 | "ini_file",
24 | "igi_utils",
25 | "game",
26 | "SIMBOARD",
27 | "callstack",
28 | "task_manager",
29 | "level",
30 | "simulation_objects",
31 | "get_object_community",
32 | "game_relations",
33 | "treasure_manager",
34 | "igi_text_processor",
35 | "dup_table",
36 | "empty_table",
37 | "xr_conditions",
38 | "xr_effects",
39 | "task_status_functor",
40 | "task_functor",
41 | "news_manager",
42 | "dialogs",
43 | "igi_precondition",
44 | "igi_description",
45 | "igi_rewards",
46 | "time_global",
47 | "igi_map_marks",
48 | "IsStalker",
49 | "get_object_story_id",
50 | "get_object_squad",
51 | "game_graph",
52 | "clsid",
53 | "safe_release_manager",
54 | "dynamic_news_helper",
55 | "utils_item",
56 | "axr_companions",
57 | "utils_data",
58 | "mob_trade",
59 | "exec_console_cmd",
60 | "ui_mcm",
61 | "igi_generic_task",
62 | "getFS",
63 | "axr_main",
64 | "igi_mcm_features",
65 | "ranks",
66 | "device",
67 | "load_var",
68 | "save_var",
69 | "copy_table",
70 | "size_table",
71 | "igi_models",
72 | "igi_finder",
73 | "ini_sys",
74 | "IsMonster",
75 | "vector",
76 | "alife_create",
77 | "alife_create_item",
78 | "utils_obj",
79 | "igi_unit_tests",
80 | "se_save_var",
81 | "sim_offline_combat",
82 | "IsItem",
83 | "itms_manager",
84 | "alife_release_id",
85 | "alife_release",
86 | "UnregisterScriptCallback",
87 | "igi_json",
88 | "lfs",
89 | "igi_synsugar",
90 | "igi_generate",
91 | "igi_macros",
92 | "alife",
93 | "ResetTimeEvent",
94 | "axr_task_manager",
95 | "tasks_clear_map",
96 | "is_squad_monster",
97 | "_g",
98 | "igi_target_basic",
99 | "debug_cmd_list",
100 | "get_story_se_object",
101 | "igi_mcm_builder",
102 | "str_explode",
103 | "sound_object",
104 | "command_line",
105 | "system_ini",
106 | "sim_board",
107 | "dialog_manager",
108 | "GetARGB",
109 | "UIInventory",
110 | "trade_manager",
111 | "DIK_keys",
112 | "EDDListType",
113 | "utils_ui",
114 | "ui_events",
115 | "CUIMessageBoxEx",
116 | "class",
117 | "inventory_upgrades",
118 | "CUIScriptWnd",
119 | "super",
120 | "Frect",
121 | "CScriptXmlInit",
122 | "key_bindings",
123 | "GetFontSmall",
124 | "vector2",
125 | "GetCursorPosition",
126 | "ui_item",
127 | "ui_inventory",
128 | "dik_to_bind",
129 | "actor_menu",
130 | "utils_xml",
131 | "item_weapon",
132 | "FitInRect",
133 | "ui_companion_inv",
134 | "get_hud",
135 | "actor_menu_inventory",
136 | "gameplay_disguise",
137 | "item_device",
138 | "UICellItem",
139 | "UICellContainer",
140 | "table_size",
141 | "UIInfoItem",
142 | "UIInfoUpgr",
143 | "CUIWindow",
144 | "UICellProperties",
145 | "SetCursorPosition",
146 | "UICellProperties_item",
147 | "CUIListBoxItem",
148 | "GetFontLetterica16Russian",
149 | "UIHint",
150 | "game_difficulties",
151 | "level_weathers",
152 | "net_packet",
153 | "xr_logic",
154 | "file_exists",
155 | "cfg_file",
156 | "log",
157 | "list_element",
158 | "GetFontLetterica18Russian",
159 | "UIWorkshop",
160 | "UIWorkshopCraft",
161 | "UIWorkshopUpgrade",
162 | "UIWorkshopRepair",
163 | "UIWorkshopState",
164 | "item_parts",
165 | "actor_effects",
166 | "game_achievements",
167 | "ui_pda_encyclopedia_tab",
168 | "game_statistics",
169 | "alife_storage_manager",
170 | "txr_routes",
171 | "mlr_utils",
172 | "msg_box_ui",
173 | "patrol",
174 | "UISleep",
175 | "item_tent",
176 | "bit_or",
177 | "FS",
178 | "ui_load_dialog",
179 | "ui_options",
180 | "user_name",
181 | "ui_sleep_dialog",
182 | "ui_debug_launcher",
183 | "save_item",
184 | "UISaveDialog",
185 | "GetFontMedium",
186 | "CUIStatic",
187 | "scene_item",
188 | "CUIListItemEx",
189 | "scenes_item_dialog",
190 | "pda_warfare_tab",
191 | "level_targets",
192 | "warfare",
193 | "relation_registry",
194 | "pda_relations_tab",
195 | "ui_companion_row",
196 | "game_ini",
197 | "psi_storm_manager",
198 | "pda_radio_tab",
199 | "pda_npc_tab",
200 | "item_radio",
201 | "faction_expansions",
202 | "pda_message_entry",
203 | "ActorMenu",
204 | "pda_encyclopedia_entry",
205 | "pda_encyclopedia_tab",
206 | "gamemode_ironman",
207 | "pda_contacts_tab",
208 | "ui_contact_row",
209 | "game_object",
210 | "story_objects",
211 | "ini_file_ex",
212 | "ui_ctrl_lighting",
213 | "game_autosave_new",
214 | "item_artefact",
215 | "UIOptions",
216 | "COptionsManager",
217 | "opt_controls",
218 | "ui_map_debug_ex",
219 | "xrs_debug_tools",
220 | "UINumpad",
221 | "UIMutantLoot",
222 | "xr_corpse_detection",
223 | "item_knife",
224 | "xr_sound",
225 | "warfare_options",
226 | "UINewGame",
227 | "sim_squad_scripted",
228 | "sim_squad_warfare",
229 | "pda",
230 | "bind_anomaly_zone",
231 | "main_menu",
232 | "anomaly_flavor",
233 | "ui_save_dialog",
234 | "ui_mm_faction_select",
235 | "IsGameTypeSingle",
236 | "level_input",
237 | "CSavedGameWrapper",
238 | "load_item",
239 | "UILoadDialog",
240 | "UIItemSheet",
241 | "context_props",
242 | "context_menu",
243 | "context_item",
244 | "freeplay_dialog",
245 | "ui_freeplay_dialog",
246 | "ui_itm_details",
247 | "utils",
248 | "multi_choice",
249 | "ui_dosimeter",
250 | "stalker_ids",
251 | "dynamic_news_manager",
252 | "property_evaluator",
253 | "eva_gather_itm",
254 | "game_setup",
255 | "act_gather_itm",
256 | "action_base",
257 | "xr_danger",
258 | "evaluator_gather_items",
259 | "xr_evaluators_id",
260 | "xr_actions_id",
261 | "world_property",
262 | "property_evaluator_const",
263 | "tasks_chimera_scan",
264 | "heli_alife",
265 | "CGeneralTask",
266 | "CGameTask",
267 | "task",
268 | "CRandomTask",
269 | "task_objects",
270 | "dialogs_zaton",
271 | "surge_manager",
272 | "fallout_manager",
273 | "CSurgeManager",
274 | "particles_object",
275 | "hit",
276 | "level_environment",
277 | "bind_crow",
278 | "xrs_facer",
279 | "state_lib",
280 | "evaluator_state_mgr_idle",
281 | "evaluator_state_mgr_idle_alife",
282 | "evaluator_state_mgr_idle_items",
283 | "act_state_mgr_to_idle",
284 | "evaluator_state_mgr_logic_active",
285 | "eva_state_mgr_end",
286 | "eva_state_mgr_locked",
287 | "state_mgr_weapon",
288 | "eva_state_mgr_locked_external",
289 | "act_state_mgr_end",
290 | "state_mgr_goap",
291 | "act_state_mgr_locked",
292 | "state_manager",
293 | "cast_planner",
294 | "CSightParams",
295 | "anim",
296 | "look",
297 | "state_lib_animpoint",
298 | "state_mgr_scenario",
299 | "state_mgr_pri_a15",
300 | "action_timer",
301 | "sr_timer",
302 | "action_teleport",
303 | "CSilence_zone",
304 | "PsyAntenna",
305 | "phantom_manager",
306 | "action_psy_antenna",
307 | "PPEffector",
308 | "effector",
309 | "action_postprocess",
310 | "effector_params",
311 | "color",
312 | "noise",
313 | "action_particle",
314 | "action_no_weapon",
315 | "fake_monster",
316 | "cond",
317 | "action_light",
318 | "action_idle",
319 | "CDeimos",
320 | "cam_effector_set",
321 | "sr_cutscene",
322 | "action_cutscene",
323 | "sr_light",
324 | "get_console",
325 | "entity_action",
326 | "bind_stalker_ext",
327 | "bind_container",
328 | "release_body_manager",
329 | "bit_and",
330 | "reload_system_ini",
331 | "dialogs_lostzone",
332 | "bind_to_dik",
333 | "actor_status",
334 | "UIIndicators",
335 | "evaluator_beh",
336 | "action_beh",
337 | "position_node",
338 | "axr_beh",
339 | "xr_combat_ignore",
340 | "modules",
341 | "smart_terrain",
342 | "stalker_generic",
343 | "axr_keybind",
344 | "UIWheelCompanion",
345 | "UICompanionList",
346 | "evaluator_fight_from_cover",
347 | "danger_object",
348 | "action_fight_from_cover",
349 | "rx_gl",
350 | "rx_ff",
351 | "xrs_dyn_music",
352 | "evaluator_npc_vs_box",
353 | "action_npc_vs_box",
354 | "bind_awr",
355 | "actor_status_thirst",
356 | "actor_status_sleep",
357 | "xr_gulag",
358 | "global_position",
359 | "death_manager",
360 | "utils_stpk",
361 | "pda_actor",
362 | "arszi_psy",
363 | "timer_global",
364 | "se_smart_cover",
365 | "create_ini_file",
366 | "object_binder",
367 | "gwr_wpn_m98_binder",
368 | "heli_combat",
369 | "heli_fire",
370 | "heli_fly",
371 | "heli_look",
372 | "heli_move",
373 | "ui_pda_npc_tab",
374 | "xr_wounded",
375 | "xr_meet",
376 | "xr_position",
377 | "se_item",
378 | "weather",
379 | "class_info",
380 | "gulag_general",
381 | "dialogs_axr_companion",
382 | "ui_debug_item",
383 | "lua_ext",
384 | "xr_zones",
385 | "UIBelt",
386 | "artefact_binder",
387 | "UICreateStash",
388 | "UICook",
389 | "bind_campfire",
390 | "device_binder",
391 | "UIMapKit",
392 | "UI3D_RF",
393 | "UIRecipe",
394 | "ui_workshop",
395 | "UIRepair",
396 | "UIWheelAmmo",
397 | "ui_wpn_params",
398 | "ui_debug_weather",
399 | "ItemProcessor",
400 | "ka_dialog",
401 | "WeatherManager",
402 | "mob_camp",
403 | "mob_state_mgr",
404 | "mob_combat",
405 | "evaluator_npc_vs_heli",
406 | "action_npc_vs_heli",
407 | "axr_npc_vs_box",
408 | "evaluator_radio_in_heli",
409 | "se_heli",
410 | "action_radio_in_heli",
411 | "evaluator_stalker_panic",
412 | "action_stalker_panic",
413 | "render_get_dx_level",
414 | "eva_turn_on_campfire",
415 | "act_turn_on_campfire",
416 | "xr_gather_items",
417 | "dialogs_yantar",
418 | "script_light",
419 | "anomaly_field_binder",
420 | "anomaly_zone_binder",
421 | "bind_anomaly_field",
422 | "mob_death",
423 | "mob_home",
424 | "mob_jump",
425 | "mob_remark",
426 | "mob_sound",
427 | "se_load_var",
428 | "igi_actions",
429 | "save_ctime",
430 | "starts_with",
431 | "parse_list",
432 | "strformat",
433 | "AC_ID",
434 | "igi_tests",
435 | "ChangeLevel",
436 | "VEC_ZERO",
437 | "get_object_by_id",
438 | "tmrs_tasks",
439 | "script_name",
440 | "TestsPopupMessages",
441 | "igi_tests_ui",
442 | "ui_popup_messages",
443 | "alife_object",
444 | "TeleportObject",
445 | "TeleportSquad",
446 | "flush",
447 | "wtf",
448 | "igi_dialogs",
449 | "modxml_wtf",
450 | "igi_target_kill",
451 | "igi_target_assault"
452 | ]
453 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Igor Anpilogov
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Weird Tasks Framework
2 |
3 | No README, huh?
4 |
--------------------------------------------------------------------------------
/gamedata/configs/igi_tasks/base.ltx:
--------------------------------------------------------------------------------
1 | #include "tables_*.ltx"
2 |
3 | [map_blacklist]
4 | l13_generators
5 | l12_stancia_2
6 | l12_stancia
7 | l11_pripyat
8 | l10_radar
9 | l11_hospital
10 | ;Underground
11 | jupiter_underground
12 | labx8
13 | l03u_agr_underground
14 | l04u_labx18
15 | l08u_brainlab
16 | l10u_bunker
17 | l12u_control_monolith
18 | l12u_sarcofag
19 | l13u_warlab
20 | fake_start
21 | ;Unbalanced
22 | l05_bar
23 |
24 | [smart_blacklist]
25 | gar_smart_terrain_6_3 ; Flea market
26 | mil_smart_terrain_4_8 ; Gatekeep spot at Barrier
27 |
28 | [trader_faction]
29 | guid_jup_stalker_garik = stalker
30 | dasc_trade_mlr = stalker
31 | bar_visitors_stalker_mechanic = dolg
32 | guid_pri_a15_mlr = stalker
33 | jup_b220_trapper = stalker
34 | val_smart_terrain_7_4_bandit_trader_stalker = bandit
35 | mil_smart_terrain_7_7_freedom_mechanic_stalker = freedom
36 | agr_smart_terrain_1_6_near_2_military_colonel_kovalski = army
37 | dasc_tech_mlr = stalker
38 | zat_b106_stalker_crab = stalker
39 | agr_smart_terrain_1_6_army_trader_stalker = army
40 | bar_visitors_garik_stalker_guard = stalker
41 | mil_smart_terrain_7_7_freedom_bodyguard2_stalker = freedom
42 | jup_a6_stalker_medik = stalker
43 | mar_base_owl_stalker_trader = csky
44 | esc_2_12_stalker_fanat = stalker
45 | bar_dolg_general_petrenko_stalker = dolg
46 | trader_monolith_kbo = monolith
47 | guid_dv_mal_mlr = bandit
48 | bar_zastava_2_commander = dolg
49 | jup_a12_bandit_guard = bandit
50 | devushka = stalker
51 | bar_dolg_general_zoneguard_stalker = dolg
52 | trader_pri_a15_mlr = stalker
53 | bar_dolg_medic = dolg
54 | bar_arena_guard = stalker
55 | zat_b7_bandit_boss_sultan = bandit
56 | pri_special_trader_mlr = killer
57 | zat_a2_stalker_mechanic = stalker
58 | mil_freedom_guid = freedom
59 | guid_marsh_mlr = csky
60 | zat_tech_mlr = stalker
61 | zat_a2_stalker_barmen = stalker
62 | zat_b106_stalker_garmata = stalker
63 | zat_b106_stalker_gonta = stalker
64 | cit_killers_merc_mechanic_stalker = killer
65 | guid_zan_stalker_locman = stalker
66 | zat_b7_stalker_victim_1 = stalker
67 | zat_stancia_trader_merc = killer
68 | zat_stancia_mech_merc = killer
69 | jup_b6_scientist_nuclear_physicist = ecolog
70 | zat_b18_noah = stalker
71 | yan_merc_03 = ecolog
72 | yan_merc_01 = ecolog
73 | bandit_main_base_medic_mlr = bandit
74 | val_smart_terrain_7_3_bandit_mechanic_stalker = bandit
75 | trucks_cemetery_bandit_trader = bandit
76 | trucks_cemetery_bandit_mechanic = bandit
77 | red_greh_trader = greh
78 | merc_pri_grifon_mlr = killer
79 | pri_monolith_monolith_mechanic_stalker = monolith
80 | yan_merc_02 = ecolog
81 | yan_povar_army_mlr = ecolog
82 | merc_pri_a18_mech_mlr = killer
83 | yan_stalker_sakharov = ecolog
84 | jup_b19_freedom_yar = freedom
85 | mil_smart_terrain_7_7_freedom_bodyguard_stalker = freedom
86 | mil_smart_terrain_7_7_freedom_leader_stalker = freedom
87 | mil_freedom_medic = freedom
88 | mil_smart_terrain_7_10_freedom_trader_stalker = freedom
89 | mar_smart_terrain_base_stalker_leader_marsh = csky
90 | mar_base_stalker_tech = csky
91 | mar_base_stalker_barmen = csky
92 | cit_killers_merc_barman_mlr = killer
93 | jup_b6_scientist_biochemist = ecolog
94 | jup_b6_scientist_tech = ecolog
95 | bar_dolg_leader = dolg
96 | jup_b217_stalker_tech = stalker
97 | jup_a6_freedom_leader = freedom
98 | jup_cont_trader_bandit = bandit
99 | jup_cont_mech_bandit = bandit
100 | jup_a12_bandit_cashier = bandit
101 | cit_killers_merc_trader_stalker = killer
102 | esc_3_16_military_trader = army
103 | cit_killers_merc_medic_stalker = killer
104 | ds_killer_guide_main_base = killer
105 | esc_smart_terrain_5_7_loner_mechanic_stalker = stalker
106 | bar_duty_security_squad_leader = dolg
107 | bar_visitors_zhorik_stalker_guard2 = stalker
108 | mechanic_monolith_kbo = monolith
109 | army_south_mechan_mlr = army
110 | guid_bar_stalker_navigator = stalker
111 | esc_2_12_stalker_wolf = stalker
112 | agr_smart_terrain_1_6_army_mechanic_stalker = army
113 | pri_monolith_monolith_trader_stalker = monolith
114 | red_greh_tech = greh
115 | agr_u_bandit_boss = bandit
116 | mechanic_army_yan_mlr = ecolog
117 | agr_1_6_medic_army_mlr = army
118 | zat_b22_stalker_medic = stalker
119 | jup_a6_freedom_trader_ashot = freedom
120 | agr_1_6_barman_army_mlr = army
121 | lider_monolith_haron = monolith
122 | bar_arena_manager = stalker
123 | bar_informator_mlr = stalker
124 | ; Actual traders
125 | bar_visitors_barman_stalker_trader = stalker
126 | esc_main_base_trader_mlr = stalker
127 | hunter_gar_trader = stalker
128 | baraholka_trader = stalker
129 | baraholka_trader_night = stalker
130 | jup_a6_stalker_barmen = stalker
131 | mar_smart_terrain_base_doctor = csky
132 | mar_smart_terrain_doc_doctor = stalker
133 | warlab_pod_1_stalker = ecolog
134 | warlab_pod_2_stalker = ecolog
135 | warlab_pod_3_stalker = ecolog
136 | warlab_pod_4_stalker = ecolog
137 | warlab_pod_5_stalker = ecolog
138 | warlab_pod_6_stalker = ecolog
139 | warlab_pod_7_stalker = ecolog
140 | zat_a2_stalker_nimble = stalker
141 | zat_b30_owl_stalker_trader = stalker
142 |
143 | [money_reward_mutants]
144 | zombie = 100
145 | tushkano = 50
146 | rat = 10
147 | boar = 200
148 | flesh = 150
149 | cat = 300
150 | fracture = 200
151 | snork = 1000
152 | lurker = 600
153 | bloodsucker = 3000
154 | psysucker = 3000
155 | psy_dog = 5000
156 | dog = 300
157 | karlik = 1000
158 | burer = 6000
159 | controller = 10000
160 | m_poltergeist = 7000
161 | m_controller_psy = 10000
162 | chimera = 15000
163 | gigant = 15000
164 |
165 | [monster_tier_factor]
166 | weak = 0.8
167 | normal = 1
168 | strong = 1.2
169 |
170 | [npc_tier_factor]
171 | 0 = 0.6
172 | 1 = 0.7
173 | 2 = 0.9
174 | 3 = 1.1
175 | 4 = 1.3
176 | 5 = 1.4
177 |
178 | [npc_tags]
179 | agr_smart_terrain_1_6_army_mechanic_stalker = Mechanic, Agroprom, Army
180 | agr_1_6_medic_army_mlr = Medic, Agroprom, Army
181 | agr_smart_terrain_1_6_near_2_military_colonel_kovalski = Leader, Agroprom, Army, Kuznetsov
182 | ; Bar
183 | bar_visitors_stalker_mechanic = Mechanic, Bar, Duty
184 | bar_dolg_medic = Medic, Bar, Duty
185 | bar_visitors_barman_stalker_trader = Barman, Trader, Loner, Bar, Barkeep
186 | bar_dolg_leader = Trader, Bar, Duty, Voronin
187 | bar_dolg_general_petrenko_stalker = Leader, Bar, Duty, Petrenko
188 | snitch = Bar, Loner, Snitch
189 | ; Darkscape
190 | dasc_tech_mlr = Mechanic, Darkscape, Loner
191 | ; Dark Valley
192 | val_smart_terrain_7_3_bandit_mechanic_stalker = Mechanic, DarkValley, Bandit
193 | bandit_main_base_medic_mlr = Medic, DarkValley, Bandit
194 | zat_b7_bandit_boss_sultan = Leader, DarkValley, Bandit, Sultan
195 | val_smart_terrain_7_4_bandit_trader_stalker = Trader, DarkValley, Bandit, Olivius
196 | ; Dead City
197 | cit_killers_merc_mechanic_stalker = Mechanic, DeadCity, Mercenary
198 | cit_killers_merc_medic_stalker = Medic, DeadCity, Mercenary
199 | cit_killers_merc_trader_stalker = Leader, Trader, DeadCity, Mercenary, Dushman
200 | cit_killers_merc_barman_mlr = Barman, Trader, DeadCity, Mercenary, Aslan
201 | ; Escape
202 | esc_smart_terrain_5_7_loner_mechanic_stalker = Mechanic, Escape, Loner
203 | army_south_mechan_mlr = Mechanic, Escape, Army
204 | esc_m_trader = Trader, Leader, Escape, Loner, Sidorovich
205 | esc_2_12_stalker_wolf = TaskGiver, Loner, Escape, Wolf
206 | esc_2_12_stalker_nimble = Trader, Loner, Escape, Nimble
207 | esc_3_16_military_trader = Trader, Army, Escape
208 | esc_2_12_stalker_fanat = TaskGiver, Loner, Escape, Fanatic
209 | ; Garbage
210 | hunter_gar_trader = Hunter, Loner, Garbage, Trader, Butcher
211 | baraholka_trader = Trader, Loner, Garbage
212 | baraholka_trader_night = Trader, Loner, Garbage, NightTrader
213 | ; Jupiter
214 | jup_b217_stalker_tech = Mechanic, Loner, Jupiter
215 | jup_cont_mech_bandit = Mechanic, Bandit, Jupiter
216 | mechanic_monolith_jup_depo = Mechanic, Monolith, Jupiter
217 | jup_a6_stalker_medik = Medic, Loner, Jupiter
218 | drx_sl_jup_a6_freedom_leader = Leader, Freedom, Jupiter, Loki
219 | jup_b6_scientist_tech = Mechanic, Ecolog, Jupiter, Tukarev
220 | jup_b220_trapper = Hunter, Loner, Jupiter, Trapper
221 | jup_b19_freedom_yar = TaskGiver, Freedom, Jupiter, Yar
222 | jup_b6_scientist_nuclear_physicist = Leader, Ecolog, Jupiter, Hermann
223 | ; Marsh
224 | mar_base_stalker_tech = Mechanic, ClearSky, Marsh
225 | mar_smart_terrain_base_doctor = Medic, ClearSky, Marsh
226 | mar_smart_terrain_base_stalker_leader_marsh = Leader, ClearSky, Marsh, Cold
227 | mar_base_owl_stalker_trader = Trader, ClearSky, Marsh, Spore
228 | mar_base_stalker_barmen = Barman, Trader, ClearSky, Marsh, Librarian
229 | ; Army Warehouses
230 | mil_smart_terrain_7_7_freedom_mechanic_stalker = Mechanic, Freedom, AW
231 | mil_freedom_medic = Medic, Freedom, AW
232 | mil_smart_terrain_7_7_freedom_leader_stalker = Leader, Freedom, AW, Lukash
233 | mil_smart_terrain_7_10_freedom_trader_stalker = Trader, Freedom, AW, Skinflint
234 | ; Pripyat 2
235 | pri_monolith_monolith_mechanic_stalker = Mechanic, Monolith, Pripyat2
236 | merc_pri_a18_mech_mlr = Mechanic, Mercenary, Pripyat2
237 | mechanic_monolith_kbo = Mechanic, Monolith, Pripyat2
238 | pri_monolith_monolith_trader_stalker = Trader, Monolith, Pripyat2, Rabbit
239 | lider_monolith_haron = Leader, Monolith, Pripyat2, Haron
240 | monolith_eidolon = TaskGiver, Monolith, Pripyat2, Eidolon
241 | merc_pri_grifon_mlr = Leader, Mercenary, Pripyat2, Griffin
242 | ; Red Forest
243 | red_greh_tech = Mechanic, Greh, RedForest
244 | ; Truck Cemetery
245 | trucks_cemetery_bandit_mechanic = Mechanic, Bandit, TruckCemetery
246 | ; Yantar
247 | mechanic_army_yan_mlr = Mechanic, Army, Yantar
248 | yan_stalker_sakharov = Leader, Ecolog, Yantar, Sakharov
249 | ; Zaton
250 | zat_a2_stalker_mechanic = Mechanic, Loner, Zaton
251 | zat_stancia_mech_merc = Mechanic, Mercenary, Zaton
252 | zat_tech_mlr = Mechanic, Loner, Zaton
253 | zat_b22_stalker_medic = Medic, Loner, Zaton
254 | zat_a2_stalker_barmen = Barman, Trader, Leader, Loner, Zaton, Beard
255 | zat_stancia_trader_merc = Trader, Mercenary, Zaton
256 |
--------------------------------------------------------------------------------
/gamedata/configs/igi_tasks/dialogs/gitkeep.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Igigog/Weird_Task_Framework/91058aade9f894636f80e4cc72a3d2ad56a83de9/gamedata/configs/igi_tasks/dialogs/gitkeep.xml
--------------------------------------------------------------------------------
/gamedata/configs/text/eng/igi_task_text_basic.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Job done. Time to return to task giver.
5 |
6 |
7 |
8 | WTF: Title not found
9 |
10 |
11 | WTF: Description not found
12 |
13 |
14 | WTF: Job description not found
15 |
16 |
17 | WTF: Finish not found, but congrats anyway
18 |
19 |
20 |
21 |
22 | Target
23 |
24 |
25 | Faction
26 |
27 |
28 | Location
29 |
30 |
31 | Something valuable
32 |
33 |
34 |
35 |
36 | Rewards:
37 |
38 |
39 | RUB
40 |
41 |
42 | goodwill
43 |
44 |
--------------------------------------------------------------------------------
/gamedata/configs/text/eng/igi_task_text_mcm.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 | WTF
8 |
9 |
10 |
13 |
14 | Autocomplete
15 |
16 |
17 | Mark WTF quests
18 |
19 |
20 | Show rewards
21 |
22 |
23 | (Utjan's) Fetch Text Thing
24 |
25 |
26 | (You have %s)
27 |
28 |
29 | (Arszi's) Realistic Assassinations
30 |
31 |
32 | Debug mode
33 |
34 |
35 | Disable preconditions
36 |
37 |
38 | Show crash message
39 |
40 |
41 | Run quest tests
42 |
43 |
44 | Run unit tests
45 |
46 |
47 | Dev mode
48 |
49 |
50 | Save on taking task
51 |
52 |
53 | Save on completing task
54 |
55 |
56 | Reset all task pack settings
57 |
58 |
59 |
60 | Disable
61 |
62 |
63 | Finish all active tasks
64 |
65 |
66 |
67 | General
68 |
69 |
70 |
71 | Static rewards
72 |
73 |
74 | >>> These values will replace framework's own dynamic rewards.
75 |
76 |
77 |
78 | Reward modifiers
79 |
80 |
81 | >>> These modifiers work alongside static as well as dynamic rewards. Change these if
82 | you want to change rewards without making them static.
83 |
84 |
85 |
86 |
87 | Reward multiplier - Money
88 |
89 |
90 | Reward multiplier - Goodwill
91 |
92 |
93 |
94 | Developer settings
95 |
96 |
97 | Rewards
98 |
99 |
100 | Flush logs
101 |
102 |
103 | Fast tests
104 |
105 |
106 | Cancel task
107 |
108 |
109 | WARNING! This quest is not supported in this version of WTF. Maybe try updating WTF?
110 |
111 |
--------------------------------------------------------------------------------
/gamedata/configs/text/rus/igi_task_text_basic.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Igigog/Weird_Task_Framework/91058aade9f894636f80e4cc72a3d2ad56a83de9/gamedata/configs/text/rus/igi_task_text_basic.xml
--------------------------------------------------------------------------------
/gamedata/configs/text/rus/igi_task_text_mcm.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Igigog/Weird_Task_Framework/91058aade9f894636f80e4cc72a3d2ad56a83de9/gamedata/configs/text/rus/igi_task_text_mcm.xml
--------------------------------------------------------------------------------
/gamedata/scripts/igi_actions.script:
--------------------------------------------------------------------------------
1 | local trace_dbg = igi_helper.trace_dbg
2 |
3 | function update_actions(CACHE)
4 | for _, entity in pairs(CACHE.entities) do
5 | process_actions(entity.actions, igi_text_processor.get_link_context(CACHE, entity))
6 | end
7 |
8 | process_actions(CACHE.actions, igi_text_processor.get_link_context(CACHE))
9 | end
10 |
11 | function process_actions(actions, link_context)
12 | if type(actions) ~= "table" then return end
13 | for _, action in pairs(actions) do
14 | if (not action._done) then
15 | if igi_text_processor.eval_logic_macro(action.when, link_context) then
16 | trace_dbg("Run action", action)
17 | action._done = not igi_text_processor.eval_logic_macro(action.run, link_context)
18 | end
19 | end
20 | end
21 | end
22 |
23 | function change_faction(id, faction)
24 | local se_squad = alife_object(id)
25 | if not se_squad or not se_squad.squad_members then return end
26 | for npc in se_squad:squad_members() do
27 | local member = get_object_by_id(npc.id)
28 | if member then
29 | member:set_character_community(faction, 0, 0)
30 | end
31 | end
32 | end
33 |
34 | function is_online(id)
35 | local se_obj = alife_object(id)
36 | if not se_obj then return end
37 |
38 | if se_obj.squad_members then
39 | se_obj = se_obj:squad_members()() -- stateful iterator, returns function
40 | for _,v in ipairs(db.OnlineStalkers) do
41 | if (v == se_obj.id) then
42 | return true
43 | end
44 | end
45 | return false
46 | end
47 | return se_obj.online
48 | end
49 |
50 | function is_low_condition(id, max_condition)
51 | local item = get_object_by_id(id)
52 | if not item then return false end
53 | return item:condition() < (max_condition / 100)
54 | end
55 |
56 | update_mark = function(id, mark)
57 | local has_spot = level.map_has_object_spot(id, mark) == 1
58 | local se_obj = alife_object(id)
59 | local object_in_world = se_obj and se_obj.parent_id == 65535
60 |
61 | if object_in_world and not has_spot then
62 | level.map_add_object_spot(id, mark, game.translate_string(mark))
63 | elseif (not object_in_world) and has_spot then
64 | level.map_remove_object_spot(id, mark)
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_ara.script:
--------------------------------------------------------------------------------
1 | function on_game_start()
2 | RegisterScriptCallback("npc_on_hit_callback", npc_on_hit_callback)
3 | RegisterScriptCallback("monster_on_hit_callback", npc_on_hit_callback)
4 | igi_callbacks.add_callback("on_entity_init", on_entity_init)
5 | igi_callbacks.add_callback("on_entity_del", on_entity_del)
6 |
7 | printf("WTF: Arszi Realistic Assassinations loaded.")
8 | end
9 |
10 | killed_by_player = {}
11 |
12 | igi_target_kill.Kill.ara_able = true
13 | igi_target_assault.Assault.ara_able = true
14 |
15 | function npc_on_hit_callback(npc,amount,local_direction,who,bone_index)
16 | if (not npc or not who) then return end
17 | if (not npc.id or not who.id) then return end
18 |
19 | if who:id() == 0 or se_load_var(who:id(), who:name(), "companion") then
20 | killed_by_player[npc:id()] = true
21 | end
22 | end
23 |
24 | function on_entity_init(CACHE, entity)
25 | if not igi_mcm.get_options_value("realistic_assassinations") then return end
26 | local is_ara = igi_taskdata.get_controller(entity, CACHE).ara_able
27 | if not is_ara then return end
28 |
29 |
30 | entity.ara_key = math.random()
31 | CACHE[entity.ara_key] = {
32 | {
33 | CONTROLLER = "@$ igi_ara.Ara",
34 | link_id = entity.ara_key,
35 | squads = entity.squads or {entity.id},
36 | }
37 | }
38 | igi_generic_task.add_entities(CACHE.task_id, entity.ara_key)
39 | end
40 |
41 | function on_entity_del(CACHE, entity)
42 | if entity.ara_key then
43 | igi_generic_task.remove_entities(CACHE.task_id, entity.ara_key)
44 | end
45 | end
46 |
47 | Ara = {}
48 |
49 | function Ara.on_init(entity)
50 | igi_helper.trace_assert(type(entity.squads) == "table", "ARA: entity.squads is not a table", entity)
51 | for _, squad_id in pairs(entity.squads) do
52 | local se_squad = alife_object(squad_id)
53 | igi_helper.trace_assert(se_squad and se_squad.squad_members, "ARA: id is not a squad", squad_id)
54 |
55 | entity.ara_to_kill = {}
56 | for member in se_squad:squad_members() do
57 | entity.ara_to_kill[member.id] = true
58 | end
59 | end
60 | end
61 |
62 | local function all_squads_dead(entity)
63 | for _, id in pairs(entity.squads) do
64 | if alife_object(id) then
65 | return false
66 | end
67 | end
68 | return true
69 | end
70 |
71 | function Ara.status(entity)
72 | if entity._status then return entity._status end
73 | for id in pairs(entity.ara_to_kill) do
74 | if killed_by_player[id] then
75 | entity._status = igi_subtask.TASK_STATUSES.COMPLETED
76 | return igi_subtask.TASK_STATUSES.COMPLETED
77 | end
78 | end
79 |
80 | if all_squads_dead(entity) then
81 | entity._status = igi_subtask.TASK_STATUSES.FAILED
82 | return igi_subtask.TASK_STATUSES.FAILED
83 | end
84 |
85 | return igi_subtask.TASK_STATUSES.RUNNING
86 | end
87 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_callbacks.script:
--------------------------------------------------------------------------------
1 | local callbacks = {
2 | on_get_taskdata = {},
3 | on_first_run = {},
4 | on_entity_init = {},
5 | on_entity_del = {},
6 | on_task_update = {},
7 | on_complete = {},
8 | on_fail = {},
9 | on_finish = {},
10 | on_subtask_status_change = {},
11 | on_before_rewarding = {},
12 | }
13 |
14 | function add_callback(name, func)
15 | if callbacks[name] then callbacks[name][func] = true end
16 | end
17 |
18 | function remove_callback(name, func)
19 | if callbacks[name] then callbacks[name][func] = nil end
20 | end
21 |
22 | function invoke_callbacks(callback_name, CACHE, ...)
23 | igi_helper.trace_dbg(callback_name, CACHE)
24 | for callback in pairs(callbacks[callback_name]) do
25 | callback(CACHE, ...)
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_description.script:
--------------------------------------------------------------------------------
1 |
2 | local trace_dbg = igi_helper.trace_dbg
3 |
4 | TEXT_HEADER = "igi_task_text_"
5 | function get_location_description(entity)
6 | return get_smart_name_by_id(entity.id)
7 | or (entity.id and dynamic_news_helper.GetPointDescription(alife_object(entity.id)))
8 | or nil
9 | end
10 |
11 | function get_smart_name_by_id(id)
12 | if tonumber(id) and SIMBOARD.smarts[id] then
13 | local smart = alife_object(id)
14 | return "st_" .. smart:name() .. "_name"
15 | end
16 | end
17 |
18 | function get_entity_description(entity)
19 | if type(entity.id) == "number" then
20 | local se_obj = alife_object(entity.id)
21 | local parent_npc = igi_helper.is_common_npc(se_obj.parent_id) and alife_object(se_obj.parent_id)
22 |
23 | return {
24 | locations = {get_location_description(entity)},
25 | factions = {se_obj.player_id or parent_npc and alife_character_community(parent_npc) or nil},
26 | targets = {parent_npc and parent_npc:character_name() or nil}
27 | }
28 | else
29 | return {
30 | factions = {entity.faction or (entity.section_name and ini_sys:r_string_ex(entity.section_name, "faction"))},
31 | targets = {entity.section_name and ini_sys:r_string_ex(entity.section_name, "inv_name_short")}
32 | }
33 | end
34 | end
35 |
36 |
37 | function get_description(CACHE)
38 | if not CACHE.description then
39 | CACHE.description = {
40 | targets = {},
41 | locations = {},
42 | factions = {},
43 | }
44 |
45 | for _, entity in pairs(CACHE.entities) do
46 | add_from_entity(CACHE.description, entity, CACHE)
47 | end
48 | end
49 |
50 | local content = {}
51 | for label, v in pairs(CACHE.description) do
52 | content[#content+1] = item_to_string(v, label)
53 | end
54 |
55 | return table.concat(content, "\\n").."\\n"..get_rewards_description(CACHE)
56 | end
57 |
58 | function get_rewards_description(CACHE)
59 | if not igi_mcm.get_options_value("show_rewards") then return "" end
60 | local low, high = igi_rewards.guess_rewards(CACHE)
61 | if (low.money == 0 and high.money == 0 and low.goodwill == 0 and high.goodwill == 0) then
62 | return ""
63 | end
64 |
65 | return game.translate_string(TEXT_HEADER .. "rewards")
66 | .. ' ' ..
67 | tostring(low.money) ..
68 | (high.money ~= low.money and "-" .. tostring(high.money) or "") ..
69 | " " .. game.translate_string(TEXT_HEADER .. "money") .. ", "
70 | .. tostring(low.goodwill) ..
71 | (high.goodwill ~= low.goodwill and "-" .. tostring(high.goodwill) or "") ..
72 | " " .. game.translate_string(TEXT_HEADER .. "goodwill")
73 | end
74 |
75 | function add_from_entity(description, entity, CACHE)
76 | if not entity.to_description then return end
77 |
78 | local controller = igi_taskdata.get_controller(entity, CACHE)
79 | local description_f = controller.get_description or get_entity_description
80 | local desc = description_f(entity)
81 | for k, tbl in pairs(desc) do
82 | add_all_values(description[k], tbl)
83 | end
84 | end
85 |
86 | function add_all_values(dest, src)
87 | for _, v in pairs(src) do
88 | dest[v] = true
89 | end
90 | end
91 |
92 | function item_to_string(item, label)
93 | if not next(item) then return "" end
94 |
95 | local details = {}
96 | for value in pairs(item) do
97 | details[#details+1] = game.translate_string(value)
98 | end
99 | table.sort(details)
100 |
101 | label = game.translate_string(TEXT_HEADER..label)
102 | return label..": "..table.concat(details, ', ')
103 | end
104 |
105 | function show_description(CACHE)
106 | if not CACHE.DESCRIPTION then
107 | return DefaultDescription.show_description(CACHE)
108 | end
109 |
110 | local link_context = igi_text_processor.get_link_context(CACHE)
111 | local descr = igi_text_processor.eval_logic_macro(CACHE.DESCRIPTION, link_context)
112 | return descr.show_description(CACHE)
113 | end
114 |
115 | DefaultDescription = {}
116 | function DefaultDescription.show_description(CACHE)
117 | local title, text, icon = get_task_text_values(CACHE)
118 |
119 | CreateTimeEvent(0, "igi_task_"..CACHE.task_id.."_setup", 0, function ()
120 | db.actor:give_talk_message2(title, text, icon, "iconed_answer_item")
121 | return true
122 | end)
123 | end
124 |
125 | function get_task_text_values(CACHE)
126 | local mark = igi_mcm.get_options_value("wtf_task_mark") and "[WTF] " or ""
127 | local title = mark .. get_task_text(CACHE.description_key, "name", CACHE.task_giver_id)
128 | local text = get_description(CACHE)
129 | local icon = CACHE.icon or "ui_iconsTotal_mutant"
130 | return title, text, icon
131 | end
132 |
133 | function get_task_text(desc_key, field, tg_id)
134 | desc_key = desc_key or ""
135 | local se_tg = alife_object(tg_id)
136 | local tg_name = se_tg:section_name() ~= "m_trader" and se_tg:section_name() or se_tg:name()
137 |
138 | -- returns exclusive task text if exists
139 | local text_id = desc_key.."_"..tg_name.."_"..field
140 | local text = game.translate_string(text_id)
141 | if text ~= text_id then return text end
142 |
143 | -- fallback to basic field for this task type
144 | local basic_text_id = desc_key.."_"..field
145 | text = game.translate_string(basic_text_id)
146 | if text ~= basic_text_id then return text end
147 |
148 | -- if (field ~= "done") then
149 | -- igi_helper.trace_error("No description for key: "..desc_key, text_id, basic_text_id)
150 | -- end
151 |
152 | -- fallback to basic field
153 | return game.translate_string(TEXT_HEADER..field)
154 | end
155 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_dialogs.script:
--------------------------------------------------------------------------------
1 | function on_game_start()
2 | RegisterScriptCallback("actor_on_first_update", igi_helper.safe(actor_on_first_update))
3 | end
4 |
5 | function actor_on_first_update()
6 | local safe_lookup = igi_helper.safe(lookup_data)
7 | for _, func in pairs(modxml_wtf.get_dialogs().funcs or {}) do
8 | local task = func[1]
9 | local data_key = func[2]
10 | data[task] = data[task] or {}
11 | data[task][data_key] = function (a, b)
12 | return safe_lookup(task, data_key, dialogs.who_is_npc(a, b))
13 | end
14 | end
15 | igi_helper.trace_dbg("dialog data", data)
16 | end
17 |
18 | data = {}
19 |
20 | function lookup_data(quest_id, data_key, npc)
21 | if not npc then return end
22 |
23 | local npc_id = npc:id()
24 | for _, CACHE in pairs(igi_generic_task.TASKS_CACHE) do
25 | if quest_id == CACHE.quest_id[1] .. '_' .. CACHE.quest_id[2] then
26 | for _, entity in ipairs(CACHE.entities) do
27 | if entity.id == npc_id and entity[data_key] then
28 | local out = entity[data_key]
29 | if type(out) == "string" then
30 | local link_context = igi_text_processor.get_link_context(CACHE, entity)
31 | return igi_text_processor.eval_logic_macro(out, link_context)
32 | end
33 |
34 | return out
35 | end
36 | end
37 | end
38 | end
39 |
40 | return nil
41 | end
42 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_finder.script:
--------------------------------------------------------------------------------
1 | -- Object finder functions
2 | local trace_assert = igi_helper.trace_assert
3 | local trace_dbg = igi_helper.trace_dbg
4 |
5 | function on_game_start()
6 | RegisterScriptCallback("actor_on_first_update", actor_on_first_update)
7 | blacklist = igi_helper.db_ini:collect_section("map_blacklist")
8 | smart_blacklist = igi_helper.db_ini:collect_section("smart_blacklist")
9 | end
10 |
11 | function actor_on_first_update()
12 | levels_by_distance, distance_by_level = sort_levels_by_distance(level.name())
13 | sort_smarts_by_level()
14 | end
15 |
16 | levels_by_distance = igi_utils.defaultdict(function () return {} end)
17 | distance_by_level = {}
18 | smarts_by_level = igi_utils.defaultdict(function () return {} end)
19 |
20 | ------------------------------
21 |
22 | function sort_levels_by_distance(start_level)
23 | local levels_by_distance = igi_utils.defaultdict(function () return {} end)
24 | local distance_by_level = {}
25 |
26 | local connections = txr_routes.list_map(false, true, false)
27 |
28 | levels_by_distance[0] = {[start_level] = true}
29 | distance_by_level[start_level] = 0
30 |
31 | local cur_distance = 0
32 | local changes = true
33 | while changes do
34 | cur_distance = cur_distance + 1
35 | changes = false
36 |
37 | for start_level in pairs(levels_by_distance[cur_distance-1]) do
38 | local map = txr_routes.get_map(start_level)
39 | local connected_maps = connections[map] or {}
40 |
41 | for _, con_map in pairs(connected_maps) do
42 | local con_level = txr_routes.get_section(con_map)
43 | if (con_level == '') then
44 | igi_helper.trace_dbg("empty section", con_map, map)
45 | end
46 | if con_level ~= '' and not distance_by_level[con_level] then
47 | changes = true
48 | levels_by_distance[cur_distance][con_level] = true
49 | distance_by_level[con_level] = cur_distance
50 | end
51 | end
52 | end
53 | end
54 |
55 | return levels_by_distance, distance_by_level
56 | end
57 |
58 | function sort_smarts_by_level()
59 | local sim = alife()
60 | local gg = game_graph()
61 |
62 | for name, smart in pairs(SIMBOARD.smarts_by_names) do
63 | local smart_level = sim:level_name(gg:vertex(smart.m_game_vertex_id):level_id())
64 | smarts_by_level[smart_level][name] = true
65 | end
66 | end
67 |
68 | local function is_smart_good(smart_id, smart_level)
69 | local smart_name = SIMBOARD.smarts[smart_id].smrt:name()
70 | local in_blacklist = smart_blacklist[smart_name] or blacklist[smart_level]
71 | local is_base = simulation_objects.base_smarts[smart_name]
72 | local is_available = simulation_objects.available_by_id[smart_id]
73 |
74 | return is_available and not (is_base or in_blacklist)
75 | end
76 | ------------
77 |
78 | local function check_location(se_obj, lower_bound, higher_bound)
79 | local obj_level = alife():level_name(igi_helper.get_object_level_id(se_obj))
80 | if blacklist[obj_level] then return false end
81 |
82 | local dist = distance_by_level[obj_level]
83 | if not dist then
84 | igi_helper.trace_dbg("igi_finder: map not connected?", obj_level)
85 | return false
86 | end
87 | return dist >= lower_bound and dist <= higher_bound
88 | end
89 |
90 | local function is_parent_enemy(se_obj)
91 | if se_obj.parent_id == 65535 then return end
92 | local my_faction = get_object_community(db.actor)
93 | local npc_faction = get_object_community(alife_object(se_obj.parent_id))
94 | return game_relations.is_factions_enemies(my_faction, npc_faction)
95 | end
96 |
97 | function find_objects_in_world(lower_bound, higher_bound, sections)
98 | trace_assert(next(sections), "No sections were given for searching")
99 |
100 | local sim = alife()
101 | local sim_object = sim.object
102 | local out = {}
103 | for id=1,65534 do
104 | local se_obj = sim_object(sim, id)
105 | if se_obj and se_obj.parent_id == 65535
106 | and sections[se_obj:section_name()]
107 | and check_location(se_obj, lower_bound, higher_bound) then
108 | out[#out+1] = id
109 | end
110 | end
111 |
112 | return out
113 | end
114 |
115 | function find_items_in_enemy(lower_bound, higher_bound, sections)
116 | trace_assert(next(sections) , "No sections were given for searching")
117 |
118 | local sim = alife()
119 | local sim_object = sim.object
120 | local out = {}
121 | for id=1,65534 do
122 | local se_obj = sim_object(sim, id)
123 | if se_obj
124 | and check_location(se_obj, lower_bound, higher_bound)
125 | and igi_helper.is_common_npc(se_obj.parent_id)
126 | and is_parent_enemy(se_obj) then
127 | for section in pairs(sections) do
128 | if string.find(se_obj:section_name(), section) then
129 | out[#out+1] = id
130 | break
131 | end
132 | end
133 | end
134 | end
135 | return out
136 | end
137 |
138 | function get_smarts_by_lvl(lvl)
139 | local out = {}
140 | for smart_name in pairs(smarts_by_level[lvl]) do
141 | local smart_id = SIMBOARD.smarts_by_names[smart_name].id
142 | if is_smart_good(smart_id, lvl) then
143 | out[#out+1] = smart_id
144 | end
145 | end
146 | return out
147 | end
148 |
149 | function get_levels(lower_bound, higher_bound, start_level)
150 | local lbd = start_level == nil and levels_by_distance or sort_levels_by_distance(start_level)
151 | local out = {}
152 | for i=lower_bound, higher_bound do
153 | for lvl in pairs(lbd[i]) do
154 | out[#out+1] = lvl
155 | end
156 | end
157 | return out
158 | end
159 |
160 | function get_smarts(lower_bound, higher_bound)
161 | local levels = get_levels(lower_bound, higher_bound)
162 | return wtf.flat_map(levels, get_smarts_by_lvl)
163 | end
164 |
165 | function get_stashes(lower_bound, higher_bound)
166 | local stashes = treasure_manager.caches
167 | local returned_stashes = {}
168 | for id, is_not_available in pairs(stashes) do
169 | local se_obj = alife_object(id)
170 | local suitable = se_obj and check_location(se_obj, lower_bound, higher_bound)
171 | if (not is_not_available) and suitable then
172 | returned_stashes[#returned_stashes+1] = id
173 | end
174 | end
175 | trace_dbg("Stashes: ", returned_stashes)
176 | return returned_stashes
177 | end
178 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_generate.script:
--------------------------------------------------------------------------------
1 | local trace_assert = igi_helper.trace_assert
2 |
3 | function generate(entity, link_context)
4 | if not entity.GEN then return {entity} end
5 |
6 | local gen_str = entity.GEN
7 | local link_id = entity.link_id
8 | entity.link_id = nil
9 | entity.GEN = nil
10 |
11 | local new_entities = igi_text_processor.eval_logic_macro(gen_str, link_context)(entity)
12 |
13 | new_entities[1].link_id = link_id
14 | new_entities[1]._GEN = gen_str
15 |
16 | return new_entities
17 | end
18 |
19 | function Amount(n)
20 | return function (entity)
21 | local new_entities = {}
22 | for _=1, n do
23 | new_entities[#new_entities+1] = dup_table(entity)
24 | end
25 | return new_entities
26 | end
27 | end
28 |
29 | function Split(field_in, field_out, count)
30 | return function (entity)
31 | local before = entity[field_in]
32 | trace_assert(type(before) == "table", "Split field is not a table", entity)
33 | trace_assert(field_out ~= nil, "Split out field is nil", entity)
34 |
35 | count = (count or #before)
36 | if #before < count then
37 | count = #before
38 | end
39 |
40 | if count < 1 then
41 | return {entity}
42 | end
43 |
44 | entity[field_in] = nil
45 |
46 | local new_entities = {}
47 | for _, val in ipairs(igi_utils.get_random_items(before, count)) do
48 | local t = dup_table(entity)
49 | t[field_out] = val
50 | new_entities[#new_entities+1] = t
51 | end
52 |
53 | new_entities[1][field_in] = before
54 | return new_entities
55 | end
56 | end
57 |
58 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_generic_task.script:
--------------------------------------------------------------------------------
1 | TASKS_VERSION = "4.2.2"
2 | TASK_SETUP = {}
3 | TASKS_CACHE = {}
4 |
5 | local trace_dbg = igi_helper.trace_dbg
6 | local trace_assert = igi_helper.trace_assert
7 | local TASK_STATUSES = igi_subtask.TASK_STATUSES
8 | local last_tasks_version
9 | NIL_ERROR = false
10 |
11 | function on_game_start()
12 | RegisterScriptCallback("save_state",save_state)
13 | RegisterScriptCallback("load_state",load_state)
14 | RegisterScriptCallback("actor_on_first_update", actor_on_first_update)
15 | printf("Weird Tasks Framework "..TASKS_VERSION.." initialised")
16 | end
17 |
18 | function actor_on_first_update()
19 | if NIL_ERROR then
20 | news_manager.send_tip(db.actor,
21 | "Something is nil! Check nonnil() calls. WTF is not initialised.", nil, nil,
22 | 30000)
23 | end
24 |
25 | if last_tasks_version ~= TASKS_VERSION then
26 | printf("Weird Tasks Framework: Updating " .. (TASKS_CACHE.TASKS_VERSION or "nil")
27 | .. " -> " .. TASKS_VERSION)
28 | news_manager.send_tip(db.actor, "WTF: Update complete. Glad to see you here. Welcome to WTF " .. TASKS_VERSION, nil, nil, 30000)
29 | end
30 | end
31 |
32 | function save_state(m_data)
33 | m_data.igi_tasks_cache = TASKS_CACHE
34 | m_data.igi_last_tasks_version = TASKS_VERSION
35 | end
36 |
37 | function load_state(m_data)
38 | TASKS_CACHE = m_data.igi_tasks_cache or {}
39 | last_tasks_version = m_data.igi_last_tasks_version
40 | end
41 |
42 | function get_cache(task_id)
43 | return TASKS_CACHE[task_id]
44 | end
45 |
46 | function get_setup_cache(task_id)
47 | return TASK_SETUP[task_id]
48 | end
49 |
50 | function process_macros(task_id, macro_level)
51 | local CACHE = get_cache(task_id)
52 | trace_assert(CACHE and macro_level, "No CACHE or macro_level in process_macros call")
53 | trace_assert(macro_level ~= "@", "@$ macros may not be processed manually.")
54 |
55 | trace_dbg("Before processing macros", macro_level, CACHE)
56 | igi_text_processor.resolve_and_link_cache(CACHE, macro_level)
57 | trace_dbg("After processing macros", macro_level, CACHE)
58 |
59 | return CACHE
60 | end
61 |
62 | function add_entities(task_id, ...)
63 | local CACHE = get_cache(task_id)
64 | trace_assert(CACHE ~= nil, "update_entities: CACHE not found for task_id", task_id)
65 |
66 | local args = {...}
67 | CACHE._queue = CACHE._queue or {add = {}, rem = {}}
68 | for _, v in ipairs(args) do
69 | trace_assert(type(CACHE[v]) == "table", "update_entities: entity table not found: ", v)
70 | CACHE._queue.add[#CACHE._queue.add+1] = v
71 | end
72 | return #args
73 | end
74 |
75 | function remove_entities(task_id, ...)
76 | local CACHE = get_cache(task_id)
77 | trace_assert(CACHE ~= nil, "update_entities: CACHE not found for task_id", task_id)
78 |
79 | local args = {...}
80 | CACHE._queue = CACHE._queue or {add = {}, rem = {}}
81 | for _, v in ipairs(args) do
82 | CACHE._queue.rem[#CACHE._queue.rem+1] = v
83 | end
84 | return #args
85 | end
86 | ---------------------------< Precondition >---------------------------
87 | function try_prepare_quest(task_id, task_data, tg_id)
88 | trace_dbg("validate "..task_id, task_data)
89 | trace_assert(task_data, "WTF: validate_task: no task data")
90 |
91 | local CACHE = igi_taskdata.finalize_task_cache(task_data, task_id, tg_id)
92 | igi_text_processor.resolve_and_link_cache(CACHE, "")
93 |
94 | CACHE.__entities = CACHE.entities
95 | CACHE.entities = {}
96 | igi_subtask.enable_entities('__entities', CACHE)
97 |
98 | igi_text_processor.resolve_and_link_cache(CACHE, "")
99 |
100 | if igi_mcm.get_task_value(CACHE.quest_id, "disabled") then return end
101 | if not igi_mcm.get_options_value("disable_preconditions") then
102 | for _, val in pairs(CACHE.preconditions or {}) do
103 | if not val then return end
104 | end
105 | end
106 |
107 | for _, val in pairs(CACHE.requirements or {}) do
108 | if not val then return end
109 | end
110 |
111 | trace_dbg("setup "..task_id, task_data)
112 | CACHE.preconditions = nil -- not needed anymore
113 | TASK_SETUP[task_id] = CACHE
114 | trace_dbg("CACHE after setup "..task_id, TASK_SETUP[task_id])
115 | return TASK_SETUP[task_id]
116 | end
117 |
118 | --< Effect >--------------------------------------------------
119 | function setup_quest(task_id)
120 | --This function will be called on_job_descr
121 | igi_description.show_description(get_setup_cache(task_id))
122 | end
123 | --< Init >----------------------------------------------------
124 | function initialise_CACHE(task_id)
125 | local CACHE = TASK_SETUP[task_id]
126 | TASKS_CACHE[task_id] = CACHE
127 | TASK_SETUP[task_id] = nil
128 |
129 | igi_subtask.init_entities('__entities', CACHE)
130 |
131 | process_macros(task_id, "1")
132 |
133 | igi_callbacks.invoke_callbacks("on_first_run", CACHE)
134 | end
135 |
136 | --< Status >--------------------------------------------------
137 | function quest_status(task_id)
138 | local CACHE = get_cache(task_id)
139 | ------------------------------------------------
140 | igi_subtask.update_entities(CACHE)
141 | igi_actions.update_actions(CACHE)
142 | igi_subtask.process_subtasks(CACHE)
143 | igi_callbacks.invoke_callbacks("on_task_update", CACHE)
144 |
145 | -------------------------------------------------
146 | if not CACHE._queue then
147 | if CACHE.status == TASK_STATUSES.FAILED then return "fail" end
148 | if CACHE.status == TASK_STATUSES.COMPLETED and not igi_rewards.has_material_rewards(CACHE) then
149 | return "complete"
150 | end
151 | end
152 |
153 | igi_subtask.update_current_map_target(CACHE)
154 | end
155 | --< Target >--------------------------------------------------
156 | function quest_target(task_id)
157 | --This function point to a task target in PDA
158 | local CACHE = get_cache(task_id)
159 |
160 | -- trace target in debug mode
161 | --[[ local target_id = CACHE.current_target_id
162 | if target_id and igi_mcm.get_options_value("debug") then
163 | local se_obj = alife_object(target_id)
164 | if se_obj then
165 | local sec = se_obj and se_obj:section_name() or "nil"
166 | trace_dbg("target "..tostring(target_id).." is "..sec)
167 | end
168 | end ]]
169 |
170 | return CACHE.quest_targets
171 | end
172 | --< Text >--------------------------------------------------
173 | function quest_text(task_id,field)
174 | --This function return a text for title_functor and descr_functor
175 | local CACHE = get_cache(task_id)
176 |
177 | if field == "descr" and CACHE.status ~= TASK_STATUSES.RUNNING then
178 | field = "done"
179 | end
180 |
181 | local text = igi_description.get_task_text(CACHE.description_key, field, CACHE.task_giver_id)
182 | local desc = field ~= "name" and ("\\n "..igi_description.get_description(CACHE)) or ""
183 | local mark = igi_mcm.get_options_value("wtf_task_mark") and "[WTF] " or ""
184 | return mark..text..desc
185 | end
186 | --< Reward >--------------------------------------------------
187 | function finish_quest(task_id)
188 | local CACHE = get_cache(task_id)
189 |
190 | igi_subtask.finish_all_subtasks(CACHE)
191 | igi_rewards.collect_and_give_rewards(CACHE)
192 |
193 | if (CACHE.status ~= igi_subtask.TASK_STATUSES.FAILED) then
194 | igi_callbacks.invoke_callbacks("on_complete", CACHE)
195 | igi_subtask.invoke_controller("on_complete", CACHE)
196 | process_macros(task_id, "on_complete")
197 | else
198 | igi_callbacks.invoke_callbacks("on_fail", CACHE)
199 | igi_subtask.invoke_controller("on_fail", CACHE)
200 | process_macros(task_id, "on_fail")
201 | end
202 |
203 | igi_callbacks.invoke_callbacks("on_finish", CACHE)
204 | process_macros(task_id, "on_finish")
205 |
206 | TASKS_CACHE[task_id] = nil
207 | end
208 |
209 | function first_finished_igi_task(tg_id)
210 | for task_id, cache in pairs(TASKS_CACHE) do
211 | if cache.task_giver_id == tg_id
212 | and (cache.status == TASK_STATUSES.READY_TO_FINISH
213 | or cache.status == TASK_STATUSES.COMPLETED) then
214 | return task_id
215 | end
216 | end
217 | end
218 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_helper.script:
--------------------------------------------------------------------------------
1 | db_ini = ini_file_ex("igi_tasks\\base.ltx")
2 |
3 | function get_task_name(task_id)
4 | return task_id[1], task_id[2]
5 | end
6 |
7 | function smart_by_name(name)
8 | return SIMBOARD.smarts_by_names[name]
9 | end
10 |
11 |
12 | function is_common_npc(id)
13 | --Parameter is correct | not world map | not player |
14 | local npc = id and (id ~= 65535) and (id ~= 0) and alife_object(id)
15 | --Exist | is stalker | alive |
16 | if not (npc and IsStalker(nil,npc:clsid()) and npc:alive()) then return false end
17 | --section_name has "sim_default", not "zombied"
18 | if not (string.find(npc:section_name(),"sim_default") and (not string.find(npc:section_name(),"zombied"))) then return false end
19 | --Not a special npc
20 | if not ((get_object_story_id(id) == nil) and (npc.group_id ~= 65535) and (get_object_story_id(npc.group_id) == nil)) then return false end
21 | --Squad
22 | local squad = get_object_squad(npc)
23 | if not (squad) then return false end
24 | --Smart
25 | local smart_id = squad.current_target_id
26 | local smrt = smart_id and SIMBOARD.smarts[smart_id]
27 | local smart = smrt and smrt.smrt
28 | local smart_name = smart and smart:name()
29 | --Fancy checking smart and squad
30 | if (smart_name and simulation_objects.base_smarts[smart_name] == true) then return false end
31 | if (squad:get_script_target()) then return false end
32 |
33 | return true
34 | end
35 |
36 | function init_squad(id)
37 | local se_obj = alife_object(id)
38 | se_obj.force_online = true
39 | return true
40 | end
41 |
42 | function create_on_smart(section_name, smart_id)
43 | local smart = SIMBOARD.smarts[smart_id].smrt
44 | local location = vector():set(
45 | smart.position.x,
46 | smart.position.y+1,
47 | smart.position.z)
48 |
49 | local se_obj = alife_create(section_name,
50 | location, smart.m_level_vertex_id, smart.m_game_vertex_id)
51 | return se_obj.id
52 | end
53 |
54 |
55 | function get_object_level_id(se_obj)
56 | trace_assert(se_obj, "No se_obj")
57 | if se_obj.parent_id ~= 65535 then
58 | -- inside inventory
59 | se_obj = alife_object(se_obj.parent_id)
60 | end
61 |
62 | ---@diagnostic disable-next-line: undefined-field
63 | return game_graph():vertex(se_obj.m_game_vertex_id):level_id()
64 | end
65 |
66 | function get_faction_enemies(faction)
67 | local excluded = {
68 | monolith = true,
69 | renegade = true,
70 | greh = true,
71 | isg = true,
72 | }
73 | local enemies_set = {}
74 | local factions = game_relations.factions_table
75 |
76 | for _, enemy_faction in pairs(factions) do
77 | if (game_relations.is_factions_enemies(faction, enemy_faction)) then
78 | if not excluded[enemy_faction] then
79 | enemies_set[enemy_faction] = true
80 | end
81 | end
82 | end
83 | return enemies_set
84 | end
85 |
86 | function pcall(f, ...)
87 | if type(f) ~= "function" then
88 | callstack()
89 | return false, "Not a function"
90 | end
91 | local xf = coroutine.create(f)
92 | local ok, res = coroutine.resume(xf, ...)
93 | if not ok then
94 | return ok, debug.traceback(xf, res .. "\\n")
95 | end
96 | return ok, res
97 | end
98 |
99 | function safe(f)
100 | return function (...)
101 | local ok, out = pcall(f, ...)
102 | if not ok then
103 | trace_error("Function call failed!", out)
104 | end
105 | return ok and out
106 | end
107 | end
108 |
109 | function trace_dbg(title, ...)
110 | if igi_mcm.get_options_value("debug") then
111 | printf("WTF DBG: "..title .. "\n" .. format_log({...}))
112 | printf("-------------------------")
113 | end
114 | end
115 |
116 | function trace_error(title, ...)
117 | local val = "WTF ERROR: " .. title .. "\n" .. format_log({...})
118 | printf(val)
119 | printf("-------------------------")
120 | if igi_mcm.get_options_value("debug") then
121 | news_manager.send_tip(db.actor, val:gsub("\n", "\\n"), nil, nil, 30000)
122 | end
123 | end
124 |
125 | function format_log(tbl)
126 | local val = {}
127 | for k, v in pairs(tbl) do
128 | if type(v) == "table" then
129 | val[#val+1] = utils_data.print_table(v, k, true)
130 | else
131 | val[#val + 1] = tostring(v)
132 | end
133 | end
134 | return table.concat(val, "\n")
135 | end
136 |
137 | function trace_assert(val, err, ...)
138 | if not val then
139 | trace_dbg("Assertion failed! " .. err, ...)
140 | assert(nil, "WTF: "..err .. "\n" .. format_log({...}))
141 | end
142 | return val
143 | end
144 |
145 | function get_community_by_id(id)
146 | local se_obj = assert(alife_object(id))
147 | ---@diagnostic disable-next-line: undefined-field
148 | if se_obj:clsid() == clsid.online_offline_group_s then
149 | se_obj = assert(alife_object(se_obj:commander_id()))
150 | end
151 |
152 | if se_obj:section_name() == "m_trader" then
153 | return "stalker" -- Sid, Forester
154 | end
155 |
156 | local community = assert(alife_character_community(se_obj))
157 | if community:find("trader") then
158 | return db_ini:r_value("trader_faction", se_obj:section_name())
159 | end
160 | return community
161 | end
162 |
163 | function is_valid_section(section_name)
164 | igi_helper.trace_assert(type(section_name) == "string", "Section is not a string", section_name)
165 | return ini_sys:section_exist(section_name)
166 | end
167 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_json.script:
--------------------------------------------------------------------------------
1 | --
2 | -- json.lua
3 | --
4 | -- Copyright (c) 2020 rxi
5 | --
6 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of
7 | -- this software and associated documentation files (the "Software"), to deal in
8 | -- the Software without restriction, including without limitation the rights to
9 | -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 | -- of the Software, and to permit persons to whom the Software is furnished to do
11 | -- so, subject to the following conditions:
12 | --
13 | -- The above copyright notice and this permission notice shall be included in all
14 | -- copies or substantial portions of the Software.
15 | --
16 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | -- SOFTWARE.
23 | --
24 |
25 | local json = { _version = "0.1.2" }
26 |
27 | -------------------------------------------------------------------------------
28 | -- Encode
29 | -------------------------------------------------------------------------------
30 |
31 | local encode
32 |
33 | local escape_char_map = {
34 | [ "\\" ] = "\\",
35 | [ "\"" ] = "\"",
36 | [ "\b" ] = "b",
37 | [ "\f" ] = "f",
38 | [ "\n" ] = "n",
39 | [ "\r" ] = "r",
40 | [ "\t" ] = "t",
41 | }
42 |
43 | local escape_char_map_inv = { [ "/" ] = "/" }
44 | for k, v in pairs(escape_char_map) do
45 | escape_char_map_inv[v] = k
46 | end
47 |
48 |
49 | local function escape_char(c)
50 | return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
51 | end
52 |
53 |
54 | local function encode_nil(val)
55 | return "null"
56 | end
57 |
58 |
59 | local function encode_table(val, stack)
60 | local res = {}
61 | stack = stack or {}
62 |
63 | -- Circular reference?
64 | if stack[val] then error("circular reference") end
65 |
66 | stack[val] = true
67 |
68 | if rawget(val, 1) ~= nil or next(val) == nil then
69 | -- Treat as array -- check keys are valid and it is not sparse
70 | local n = 0
71 | for k in pairs(val) do
72 | if type(k) ~= "number" then
73 | error("invalid table: mixed or invalid key types")
74 | end
75 | n = n + 1
76 | end
77 | if n ~= #val then
78 | error("invalid table: sparse array")
79 | end
80 | -- Encode
81 | for i, v in ipairs(val) do
82 | table.insert(res, encode(v, stack))
83 | end
84 | stack[val] = nil
85 | return "[" .. table.concat(res, ",") .. "]"
86 |
87 | else
88 | -- Treat as an object
89 | for k, v in pairs(val) do
90 | if type(k) ~= "string" then
91 | error("invalid table: mixed or invalid key types")
92 | end
93 | table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
94 | end
95 | stack[val] = nil
96 | return "{" .. table.concat(res, ",") .. "}"
97 | end
98 | end
99 |
100 |
101 | local function encode_string(val)
102 | return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
103 | end
104 |
105 |
106 | local function encode_number(val)
107 | -- Check for NaN, -inf and inf
108 | if val ~= val or val <= -math.huge or val >= math.huge then
109 | error("unexpected number value '" .. tostring(val) .. "'")
110 | end
111 | return string.format("%.14g", val)
112 | end
113 |
114 |
115 | local type_func_map = {
116 | [ "nil" ] = encode_nil,
117 | [ "table" ] = encode_table,
118 | [ "string" ] = encode_string,
119 | [ "number" ] = encode_number,
120 | [ "boolean" ] = tostring,
121 | }
122 |
123 |
124 | encode = function(val, stack)
125 | local t = type(val)
126 | local f = type_func_map[t]
127 | if f then
128 | return f(val, stack)
129 | end
130 | error("unexpected type '" .. t .. "'")
131 | end
132 |
133 |
134 | function json.encode(val)
135 | return ( encode(val) )
136 | end
137 |
138 |
139 | -------------------------------------------------------------------------------
140 | -- Decode
141 | -------------------------------------------------------------------------------
142 |
143 | local parse
144 |
145 | local function create_set(...)
146 | local res = {}
147 | for i = 1, select("#", ...) do
148 | res[ select(i, ...) ] = true
149 | end
150 | return res
151 | end
152 |
153 | local space_chars = create_set(" ", "\t", "\r", "\n")
154 | local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
155 | local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
156 | local literals = create_set("true", "false", "null")
157 |
158 | local literal_map = {
159 | [ "true" ] = true,
160 | [ "false" ] = false,
161 | [ "null" ] = nil,
162 | }
163 |
164 |
165 | local function next_char(str, idx, set, negate)
166 | for i = idx, #str do
167 | if set[str:sub(i, i)] ~= negate then
168 | return i
169 | end
170 | end
171 | return #str + 1
172 | end
173 |
174 |
175 | local function decode_error(str, idx, msg)
176 | local line_count = 1
177 | local col_count = 1
178 | for i = 1, idx - 1 do
179 | col_count = col_count + 1
180 | if str:sub(i, i) == "\n" then
181 | line_count = line_count + 1
182 | col_count = 1
183 | end
184 | end
185 | error( string.format("%s at line %d col %d", msg, line_count, col_count) )
186 | end
187 |
188 |
189 | local function codepoint_to_utf8(n)
190 | -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
191 | local f = math.floor
192 | if n <= 0x7f then
193 | return string.char(n)
194 | elseif n <= 0x7ff then
195 | return string.char(f(n / 64) + 192, n % 64 + 128)
196 | elseif n <= 0xffff then
197 | return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
198 | elseif n <= 0x10ffff then
199 | return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
200 | f(n % 4096 / 64) + 128, n % 64 + 128)
201 | end
202 | error( string.format("invalid unicode codepoint '%x'", n) )
203 | end
204 |
205 |
206 | local function parse_unicode_escape(s)
207 | local n1 = tonumber( s:sub(1, 4), 16 )
208 | local n2 = tonumber( s:sub(7, 10), 16 )
209 | -- Surrogate pair?
210 | if n2 then
211 | return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
212 | else
213 | return codepoint_to_utf8(n1)
214 | end
215 | end
216 |
217 |
218 | local function parse_string(str, i)
219 | local res = ""
220 | local j = i + 1
221 | local k = j
222 |
223 | while j <= #str do
224 | local x = str:byte(j)
225 |
226 | if x < 32 then
227 | decode_error(str, j, "control character in string")
228 |
229 | elseif x == 92 then -- `\`: Escape
230 | res = res .. str:sub(k, j - 1)
231 | j = j + 1
232 | local c = str:sub(j, j)
233 | if c == "u" then
234 | local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
235 | or str:match("^%x%x%x%x", j + 1)
236 | or decode_error(str, j - 1, "invalid unicode escape in string")
237 | res = res .. parse_unicode_escape(hex)
238 | j = j + #hex
239 | else
240 | if not escape_chars[c] then
241 | decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
242 | end
243 | res = res .. escape_char_map_inv[c]
244 | end
245 | k = j + 1
246 |
247 | elseif x == 34 then -- `"`: End of string
248 | res = res .. str:sub(k, j - 1)
249 | return res, j + 1
250 | end
251 |
252 | j = j + 1
253 | end
254 |
255 | decode_error(str, i, "expected closing quote for string")
256 | end
257 |
258 |
259 | local function parse_number(str, i)
260 | local x = next_char(str, i, delim_chars)
261 | local s = str:sub(i, x - 1)
262 | local n = tonumber(s)
263 | if not n then
264 | decode_error(str, i, "invalid number '" .. s .. "'")
265 | end
266 | return n, x
267 | end
268 |
269 |
270 | local function parse_literal(str, i)
271 | local x = next_char(str, i, delim_chars)
272 | local word = str:sub(i, x - 1)
273 | if not literals[word] then
274 | decode_error(str, i, "invalid literal '" .. word .. "'")
275 | end
276 | return literal_map[word], x
277 | end
278 |
279 |
280 | local function parse_array(str, i)
281 | local res = {}
282 | local n = 1
283 | i = i + 1
284 | while 1 do
285 | local x
286 | i = next_char(str, i, space_chars, true)
287 | -- Empty / end of array?
288 | if str:sub(i, i) == "]" then
289 | i = i + 1
290 | break
291 | end
292 | -- Read token
293 | x, i = parse(str, i)
294 | res[n] = x
295 | n = n + 1
296 | -- Next token
297 | i = next_char(str, i, space_chars, true)
298 | local chr = str:sub(i, i)
299 | i = i + 1
300 | if chr == "]" then break end
301 | if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
302 | end
303 | return res, i
304 | end
305 |
306 |
307 | local function parse_object(str, i)
308 | local res = {}
309 | i = i + 1
310 | while 1 do
311 | local key, val
312 | i = next_char(str, i, space_chars, true)
313 | -- Empty / end of object?
314 | if str:sub(i, i) == "}" then
315 | i = i + 1
316 | break
317 | end
318 | -- Read key
319 | if str:sub(i, i) ~= '"' then
320 | decode_error(str, i, "expected string for key")
321 | end
322 | key, i = parse(str, i)
323 | -- Read ':' delimiter
324 | i = next_char(str, i, space_chars, true)
325 | if str:sub(i, i) ~= ":" then
326 | decode_error(str, i, "expected ':' after key")
327 | end
328 | i = next_char(str, i + 1, space_chars, true)
329 | -- Read value
330 | val, i = parse(str, i)
331 | -- Set
332 | res[key] = val
333 | -- Next token
334 | i = next_char(str, i, space_chars, true)
335 | local chr = str:sub(i, i)
336 | i = i + 1
337 | if chr == "}" then break end
338 | if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
339 | end
340 | return res, i
341 | end
342 |
343 |
344 | local char_func_map = {
345 | [ '"' ] = parse_string,
346 | [ "0" ] = parse_number,
347 | [ "1" ] = parse_number,
348 | [ "2" ] = parse_number,
349 | [ "3" ] = parse_number,
350 | [ "4" ] = parse_number,
351 | [ "5" ] = parse_number,
352 | [ "6" ] = parse_number,
353 | [ "7" ] = parse_number,
354 | [ "8" ] = parse_number,
355 | [ "9" ] = parse_number,
356 | [ "-" ] = parse_number,
357 | [ "t" ] = parse_literal,
358 | [ "f" ] = parse_literal,
359 | [ "n" ] = parse_literal,
360 | [ "[" ] = parse_array,
361 | [ "{" ] = parse_object,
362 | }
363 |
364 |
365 | parse = function(str, idx)
366 | local chr = str:sub(idx, idx)
367 | local f = char_func_map[chr]
368 | if f then
369 | return f(str, idx)
370 | end
371 | decode_error(str, idx, "unexpected character '" .. chr .. "'")
372 | end
373 |
374 |
375 | function json.decode(str)
376 | if type(str) ~= "string" then
377 | error("expected argument of type string, got " .. type(str))
378 | end
379 | local res, idx = parse(str, next_char(str, 1, space_chars, true))
380 | idx = next_char(str, idx, space_chars, true)
381 | if idx <= #str then
382 | decode_error(str, idx, "trailing garbage")
383 | end
384 | return res
385 | end
386 |
387 | function get_json()
388 | return json
389 | end
390 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_macros.script:
--------------------------------------------------------------------------------
1 | local trace_dbg = igi_helper.trace_dbg
2 | local Set = igi_utils.Set
3 |
4 | local current_tg_faction = nil
5 | local function get_faction(keyword)
6 | if keyword == "actor" then
7 | return db.actor:character_community()
8 | elseif keyword == 'taskgiver' then
9 | return current_tg_faction
10 | else
11 | return keyword
12 | end
13 | end
14 |
15 | local evaluate, tokenize
16 | function Faction(tg_id, args_str)
17 | current_tg_faction = igi_helper.get_community_by_id(tg_id)
18 | -- args checker needed
19 | local tokens = tokenize(args_str)
20 | local factions = evaluate(tokens, 1, #tokens)
21 | if type(factions) == 'table' then
22 | local _, faction = igi_utils.random_table_element(factions)
23 | return faction or 'stalker'
24 | else
25 | return get_faction(factions)
26 | end
27 | end
28 |
29 | ----------------BOOLEAN----------------------------------
30 |
31 | local blacklist = {
32 | monolith = true,
33 | renegade = true,
34 | greh = true,
35 | isg = true,
36 | }
37 | local function get_viable_factions()
38 | local factions = {}
39 | for _, faction in pairs(game_relations.factions_table) do
40 | if not blacklist[faction] then
41 | factions[faction] = true
42 | end
43 | end
44 | return factions
45 | end
46 |
47 | local function convert_to_faction_set(faction)
48 | trace_dbg("convert to faction", faction)
49 | if type(faction) == 'string' then
50 | return {[get_faction(faction)] = true}
51 | end
52 | return faction
53 | end
54 |
55 | local function get_of_factions(key, faction)
56 | local is_in = key == "enemy" and game_relations.is_factions_enemies or game_relations.is_factions_friends
57 | local tbl = {}
58 | for k_faction in pairs(get_viable_factions()) do
59 | if is_in(faction, k_faction) then
60 | tbl[k_faction] = true
61 | end
62 | end
63 | return tbl
64 | end
65 |
66 | local function and_func(left, right)
67 | right = convert_to_faction_set(right)
68 | left = convert_to_faction_set(left)
69 | return Set.intersection(left, right)
70 | end
71 |
72 | local function or_func(left, right)
73 | right = convert_to_faction_set(right)
74 | left = convert_to_faction_set(left)
75 | return Set.union(left, right)
76 | end
77 |
78 | local function not_func(left, right)
79 | right = convert_to_faction_set(right)
80 | return Set.difference(get_viable_factions(), right)
81 | end
82 |
83 | local function of_func(left, right)
84 | right = convert_to_faction_set(right)
85 |
86 | local newset = {}
87 | for faction in pairs(right) do
88 | for of_faction in pairs(get_of_factions(left, faction)) do
89 | newset[of_faction] = true
90 | end
91 | end
92 | return newset
93 | end
94 |
95 | function tokenize(str)
96 | local tbl = {}
97 | str = str:gsub("%(", "%( "):gsub('%)', ' %)')
98 | for substr in str:gmatch("[^%s]+") do
99 | tbl[#tbl+1] = substr
100 | end
101 | return tbl
102 | end
103 |
104 | local function evaluate_no_par(tokens, tstart, tend)
105 | if tstart > tend then return nil end
106 | if tstart == tend then return tokens[tstart] end
107 | local last_and, last_or, last_not, last_of
108 | for i=tend,tstart,-1 do
109 | local token = tokens[i]
110 | if token == "and" then
111 | last_and = i
112 | elseif token == "or" then
113 | last_or = i
114 | elseif token == "not" then
115 | last_not = i
116 | elseif token == "of" then
117 | last_of = i
118 | end
119 | end
120 | if last_and then
121 | return and_func(evaluate_no_par(tokens, tstart, last_and-1), evaluate_no_par(tokens, last_and+1, tend))
122 | elseif last_or then
123 | return or_func(evaluate_no_par(tokens, tstart, last_or-1), evaluate_no_par(tokens, last_or+1, tend))
124 | elseif last_not then
125 | return not_func(evaluate_no_par(tokens, tstart, last_not-1),evaluate_no_par(tokens, last_not+1, tend))
126 | elseif last_of then
127 | return of_func(evaluate_no_par(tokens, tstart, last_of-1),evaluate_no_par(tokens, last_of+1, tend))
128 | end
129 | end
130 |
131 | function evaluate(tokens, tstart, tend)
132 | if tstart > tend then return nil end
133 | if tstart == tend then return tokens[tstart] end
134 |
135 | local new_tokens = {}
136 | local par_count = 0
137 | local par_start
138 | for i=tstart,tend do
139 | local token = tokens[i]
140 | if token == '(' then
141 | if par_count == 0 then
142 | par_start = i
143 | end
144 | par_count = par_count + 1
145 |
146 | elseif token == ')' then
147 | par_count = par_count - 1
148 | if par_count == 0 then
149 | new_tokens[#new_tokens + 1] = evaluate(tokens, par_start+1, i-1)
150 | end
151 | elseif par_count == 0 then
152 | new_tokens[#new_tokens + 1] = token
153 | end
154 | end
155 | return evaluate_no_par(new_tokens, 1, #new_tokens)
156 | end
157 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_mcm.script:
--------------------------------------------------------------------------------
1 | Tree = igi_mcm_builder.Tree
2 | Page = igi_mcm_builder.Page
3 | ImageWithText = igi_mcm_builder.ImageWithText
4 | Checkbox = igi_mcm_builder.Checkbox
5 | Trackbar = igi_mcm_builder.Trackbar
6 | Title = igi_mcm_builder.Title
7 | Line = igi_mcm_builder.Line
8 | InputField = igi_mcm_builder.InputField
9 | Description = igi_mcm_builder.Description
10 |
11 | function on_game_start()
12 | assert(ui_mcm, "Weird Tasks Framework: MCM not installed. Fuck you.")
13 | autosave_on_game_start()
14 | autocomplete_on_game_start()
15 | end
16 |
17 | function get_options_value(option_id)
18 | return ui_mcm.get("igi_tasks/Options/"..option_id)
19 | end
20 |
21 | function get_task_value(quest_id, key)
22 | if not quest_id then return end
23 | local prefix, task_name = quest_id[1], quest_id[2]
24 | return ui_mcm.get("igi_tasks/" .. prefix .. "/" .. task_name .. "/" .. key)
25 | end
26 |
27 | function reset_all_tasks(task, prefix)
28 | for task_id in pairs(igi_generic_task.TASKS_CACHE) do
29 | if string.find(task_id, prefix..task) then
30 | task_manager.get_task_manager():set_task_completed(task_id)
31 | end
32 | end
33 | end
34 |
35 | local function build_task_page(task, prefix, task_data)
36 | local page = Page.new(task):text("igi_task_text_"..prefix.."_"..task.."_name")
37 | page:add(Title.new(task):text("igi_task_text_"..prefix.."_"..task.."_name"))
38 |
39 | if (type(task_data.CREDITS) == "string") then
40 | page:add(igi_mcm_builder.Description.new(task):text("Credits: " .. task_data.CREDITS))
41 | end
42 |
43 | if (not igi_taskdata.is_supported_version(task_data)) then
44 | page:add(igi_mcm_builder.Description.new("unsupported"):text("igi_task_text_mcm_unsupported_wtf_version"))
45 | end
46 |
47 | page:add(
48 | Checkbox.new("disabled")
49 | :hint("igi_tasks_disable_task")
50 | )
51 | page:add(Trackbar.new("money_reward_coeff")
52 | :minmax(0, 5)
53 | :hint("igi_tasks_money_reward_coeff"))
54 | page:add(Trackbar.new("goodwill_reward_coeff")
55 | :minmax(0, 5)
56 | :hint("igi_tasks_goodwill_reward_coeff"))
57 | return page
58 | end
59 |
60 | local function get_options_page()
61 | local page = Page.new("Options")
62 | page:add(ImageWithText.new("title")
63 | :image("ui_options_slider_player")
64 | :text("ui_mcm_igi_tasks_title"))
65 |
66 | -- Settings
67 | page:add(Checkbox.new("autosave_before"):default(false))
68 | page:add(Checkbox.new("autosave_after"):default(false))
69 | page:add(Checkbox.new("autocomplete"):default(false))
70 | page:add(Checkbox.new("realistic_assassinations"):default(false))
71 | page:add(Checkbox.new("utjan_fetch_thing"):default(true))
72 | page:add(Checkbox.new("wtf_task_mark"):default(true))
73 | page:add(Checkbox.new("wtf_crash_message"):default(true))
74 |
75 | page:add(Line.new())
76 | page:add(Title.new("rewards"):text("ui_mcm_igi_tasks_rewards"))
77 | page:add(Checkbox.new("show_rewards"):default(true))
78 | page:add(Trackbar.new("money_reward_coeff")
79 | :minmax(0, 5)
80 | :hint("igi_tasks_money_reward_coeff"))
81 | page:add(Trackbar.new("goodwill_reward_coeff")
82 | :minmax(0, 5)
83 | :hint("igi_tasks_goodwill_reward_coeff"))
84 |
85 | page:add(Line.new())
86 | page:add(Title.new("devzone"):text("ui_mcm_igi_tasks_devzone"))
87 | page:add(igi_mcm_builder.List.new("cancel_task")
88 | :dont_translate()
89 | :content({ function ()
90 | local out = {{1, "-"}}
91 | for k in pairs(igi_generic_task.TASKS_CACHE) do
92 | out[#out+1] = k ~= "TASKS_VERSION" and {k, k} or nil
93 | end
94 | return out
95 | end})
96 | :current_value({ function ()
97 | return {1, "-"}
98 | end})
99 | :callback({ function (task_id)
100 | task_manager.get_task_manager():set_task_completed(task_id)
101 | end}))
102 | page:add(Checkbox.new("debug"):default(false))
103 | page:add(Checkbox.new("disable_preconditions"):default(false))
104 | page:add(Checkbox.new("fast_tests"):default(false))
105 | page:add(Checkbox.new("integration_tests_quest")
106 | :current_value({ function() return false end })
107 | :callback({ function() igi_tests.start_integration_tests() end, }))
108 | page:add(Checkbox.new("unit_tests")
109 | :current_value({ function() return false end })
110 | :callback({ function() igi_tests.start_normal_tests() end, }))
111 | page:add(Checkbox.new("flush_logs")
112 | :current_value({ function() return false end })
113 | :callback({ function() flush() end, }))
114 | return page
115 | end
116 |
117 | local function reset_tasks_on_new_framework()
118 | local TASKS_VERSION = igi_generic_task.TASKS_VERSION
119 | local old_version = axr_main.config:r_value("igi_tasks", "tasks_version", 0)
120 | if old_version ~= TASKS_VERSION then
121 | printf("Updating WTF: ", old_version, "=>", TASKS_VERSION)
122 | axr_main.config:w_value("igi_tasks", "tasks_version", TASKS_VERSION)
123 | axr_main.config:save()
124 | end
125 | end
126 |
127 | function on_mcm_load()
128 | reset_tasks_on_new_framework()
129 | local tree = Tree.new("igi_tasks")
130 | tree:add_page(get_options_page())
131 |
132 | for prefix, names in pairs(igi_taskdata.get_all_quests()) do
133 | local subtree = Tree.new(prefix):text(prefix)
134 | for task_name, task_data in pairs(names) do
135 | subtree:add_page(build_task_page(task_name, prefix, task_data))
136 | end
137 | tree:add_subtree(subtree)
138 | end
139 | return tree:build()
140 | end
141 |
142 | ---------------- AUTOSAVE -------------------
143 |
144 | function autosave_on_first_run(CACHE)
145 | if not get_options_value("autosave_before") then return end
146 | local title = igi_description.get_task_text(CACHE.description_key, "name", CACHE.task_giver_id)
147 | CreateTimeEvent("igi_mcm_features", "autosave_before", 0, function()
148 | exec_console_cmd("save " .. "task " .. title .. " started")
149 | return true
150 | end)
151 | end
152 |
153 | function autosave_on_finish(CACHE)
154 | if not get_options_value("autosave_after") then return end
155 | local title = igi_description.get_task_text(CACHE.description_key, "name", CACHE.task_giver_id)
156 | CreateTimeEvent("igi_mcm_features", "autosave_after", 0, function()
157 | exec_console_cmd("save " .. "task " .. title .. " finished")
158 | return true
159 | end)
160 | end
161 |
162 | function autosave_on_game_start()
163 | igi_callbacks.add_callback("on_first_run", autosave_on_first_run)
164 | igi_callbacks.add_callback("on_finish", autosave_on_finish)
165 | end
166 |
167 | ---------------- AUTOCOMPLETE -------------------
168 |
169 | function autocomplete(CACHE)
170 | if not get_options_value("autocomplete") then return end
171 | if CACHE.status ~= igi_subtask.TASK_STATUSES.COMPLETED then return end
172 | task_manager.get_task_manager():set_task_completed(CACHE.task_id)
173 | end
174 |
175 | function autocomplete_on_game_start()
176 | igi_callbacks.add_callback("on_task_update", autocomplete)
177 | end
178 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_mcm_builder.script:
--------------------------------------------------------------------------------
1 | assert(string.find(ui_mcm.VERSION, "1.%d.%d"), "MCM Builder: Unsupported MCM version")
2 |
3 |
4 | AbstractOption = {
5 | -- Not an actual class, just a pile of methods used by
6 | -- everyone else
7 |
8 | input_type = function (self, typ)
9 | -- tells the script what kind of value
10 | -- the option is storing / dealing with
11 | if typ == "string" then
12 | self.val = 0
13 | elseif typ == "boolean" then
14 | self.val = 1
15 | elseif typ == "float" then
16 | self.val = 2
17 | else
18 | assert(nil, "MCM Builder: unknown type: "..tostring(typ))
19 | end
20 | return self
21 | end,
22 |
23 | cmd = function (self, cmd)
24 | -- Tie an option to a console command
25 | -- Refer to MCM manual for the documentation
26 | assert(not self.def, "MCM Builder: default value does not work with cmd enabled")
27 | self.cmd = cmd
28 | return self
29 | end,
30 |
31 | default = function (self, def)
32 | -- value or table {function, parameters}
33 | -- Default value of an option
34 | assert(not rawget(self,"cmd"), "MCM Builder: default value does not work with cmd enabled")
35 | if type(def) == "table" then
36 | assert(type(def[1]) == "function", "MCM Builder: default table without a function")
37 | end
38 | self.def = def
39 | return self
40 | end,
41 |
42 | current_value = function (self, curr)
43 | -- table {function, parameters}
44 | -- get current value of an option by executing
45 | -- the declared function, instead of reading it from axr_options.ltx
46 | assert(type(curr[1]) == "function", "MCM Builder: current value without a function")
47 | self.curr = curr
48 | return self
49 | end,
50 |
51 | callback = function (self, tbl)
52 | -- table {function, parameters}
53 | -- Execute a function when option's changes get applied
54 | -- The value of the option is added to the end of the parameters list.
55 | assert(type(tbl[1]) == "function", "MCM Builder: callback without a function")
56 | self.functor = tbl
57 | return self
58 | end,
59 |
60 | text = function (self, text)
61 | -- String to show, it will be translated
62 | self.text = text
63 | return self
64 | end,
65 |
66 | hint = function (self, hint)
67 | -- Override default name / desc rule to replace
68 | -- the translation of an option with a custom one,
69 | -- should be set without "ui_mcm_" and "_desc"
70 | self.hint = hint
71 | return self
72 | end,
73 |
74 | color = function (self, r, g, b, a)
75 | -- determines the color of the text
76 | assert(type(a + r + g + b) == "number", "MCM: invalid color representation")
77 | self.clr = {a,r,b,g}
78 | return self
79 | end,
80 |
81 | minmax = function (self, min, max)
82 | -- Minimum/maximum for an option
83 | assert(self.val == 2, "MCM Builder: minmax does not work if input type is not float")
84 | assert(min + max, "MCM Builder: bad minmax values")
85 | self.min = min
86 | self.max = max
87 | return self
88 | end,
89 |
90 | content_pairs = function (self, content)
91 | -- table {double pairs}
92 | -- Declares option's selection list
93 | for _, v in ipairs(content) do
94 | assert(#v == 2, "MCM Builder: not a pair")
95 | end
96 | self.content = content
97 | return self
98 | end,
99 |
100 | image = function (self, link)
101 | -- Link to texture you want to show
102 | self.link = link
103 | return self
104 | end,
105 |
106 | dont_translate = function (self)
107 | -- Usually, the 2nd key of pairs in content table are
108 | -- strings to show on the UI, by translating "opt_str_lst_(string)".
109 | -- When we set [no_str] to true, it will show
110 | -- the string from table as it is without
111 | -- translations or "opt_str_lst_"
112 | -- For TrackBars: no_str won't show value next to the slider
113 | self.no_str = true
114 | return self
115 | end,
116 |
117 | precondition = function (self, prec)
118 | -- table {function, parameters}
119 | -- Show the option on UI if the precondition function returns true
120 | assert(type(prec[1]) == "function", "MCM Builder: precondition without a function")
121 | self.precondition = prec
122 | return self
123 | end,
124 |
125 | postcondition = function (self, postc)
126 | -- table {function, parameters}
127 | -- Option won't execute its functor when changes are applied,
128 | -- unless if the postcondition function returns true
129 | assert(type(postc[1]) == "function", "MCM Builder: postcondition without a function")
130 | self.postcondition = postc
131 | return self
132 | end,
133 | }
134 |
135 | Tree = {
136 | -- Structural element; You'll probably need at least one to start.
137 | -- Don't forget to :build() at the end.
138 | _cls = "Tree",
139 |
140 | new = function (id)
141 | assert(type(id) == "string", "MCM Builder: no id given")
142 | local t = {id = id, sh = false, gr = {}}
143 | setmetatable(t, {__index = Tree})
144 | return t
145 | end,
146 |
147 | add_subtree = function (self, subtree)
148 | -- Add another Tree on the next MCM level
149 | assert(subtree._cls == "Tree", "MCM Builder: not a Tree")
150 | assert(not subtree._subtree, "MCM Builder: Tree too deep")
151 | subtree._subtree = true -- only can add pages to subtree
152 | self._subtree = true -- can't add self to another tree
153 | self.gr[#self.gr+1] = subtree
154 | return self
155 | end,
156 |
157 | add_page = function (self, page)
158 | -- Add Page. Page is final destination.
159 | assert(page._cls == "Page", "MCM Builder: not a Page")
160 | self.gr[#self.gr+1] = page
161 | return self
162 | end,
163 |
164 | build = function (self)
165 | -- Remove OOP features from everything. Needed as some OOP functions
166 | -- collide with actual data MCM uses
167 | setmetatable(self, nil)
168 | self._subtree = nil
169 | for _, v in pairs(self) do
170 | if type(v) == "table" then
171 | Tree.build(v)
172 | end
173 | end
174 | return self
175 | end,
176 |
177 | group = function (self, group_id)
178 | -- allows you to give options tree a group id,
179 | -- to connect them when you want to use "Apply to all"
180 | -- button for options
181 | assert(type(group_id) == "string", "MCM Builder: not a text")
182 | self.id_gr = group_id
183 | self.apply_to_all = true
184 | return self
185 | end,
186 |
187 | text = AbstractOption.text,
188 | }
189 |
190 | Page = {
191 | -- Final destination; Options are added to this one.
192 | -- Is added to a Tree, choosing it gives you the page
193 | -- You probably can use it without a Tree, if you only need
194 | -- one page.
195 | _cls = "Page",
196 |
197 | new = function (id)
198 | assert(type(id) == "string", "MCM Builder: no id given")
199 | local t = {id=id, sh=true, gr={}}
200 | setmetatable(t, {__index = Page})
201 | return t
202 | end,
203 |
204 | add = function (self, widget)
205 | -- Add widget to self
206 | assert(widget._widget, "MCM Builder: Trying to add not a widget")
207 | self.gr[#self.gr+1] = widget
208 | return self
209 | end,
210 |
211 | merge = function (self, page)
212 | -- Add everything from another Page to self
213 | assert(page._cls == "Page", "MCM Builder: not a Page")
214 | for _, widget in pairs(page.gr) do
215 | self:add(widget)
216 | end
217 | return self
218 | end,
219 |
220 | precondition = AbstractOption.precondition,
221 | text_on_fail = function (self, text)
222 | -- Text to show when precondition fails
223 | assert(type(text) == "string", "MCM Builder: not a text")
224 | self.output = text
225 | return self
226 | end,
227 |
228 | group = Tree.group,
229 | text = AbstractOption.text,
230 | build = Tree.build,
231 | }
232 |
233 | Checkbox = {
234 | -- Literally boolean option, no bells and whistles
235 | _cls = "Checkbox",
236 | _widget = true,
237 |
238 | new = function (id)
239 | assert(type(id) == "string", "MCM Builder: no id given")
240 | local t = {id=id, type = "check", val = 1}
241 | setmetatable(t, {__index = Checkbox})
242 | return t
243 | end,
244 |
245 | default = AbstractOption.default,
246 | hint = AbstractOption.hint,
247 | cmd = AbstractOption.cmd,
248 |
249 | precondition = AbstractOption.precondition,
250 | callback = AbstractOption.callback,
251 | postcondition = AbstractOption.postcondition,
252 |
253 | current_value = AbstractOption.current_value,
254 | }
255 |
256 | List = {
257 | -- List of strings, useful for options with too many selections
258 | _cls = "List",
259 | _widget = true,
260 |
261 | new = function (id)
262 | assert(type(id) == "string", "MCM Builder: no id given")
263 | local t = {id=id, type = "list", val = 0}
264 | setmetatable(t, {__index = List})
265 | return t
266 | end,
267 |
268 | content = function (self, tbl)
269 | self.content = tbl
270 | return self
271 | end,
272 |
273 | default = AbstractOption.default,
274 | content_pairs = AbstractOption.content_pairs,
275 | input_type = AbstractOption.input_type,
276 | cmd = AbstractOption.cmd,
277 | dont_translate = AbstractOption.dont_translate,
278 |
279 | precondition = AbstractOption.precondition,
280 | callback = AbstractOption.callback,
281 | postcondition = AbstractOption.postcondition,
282 |
283 | current_value = AbstractOption.current_value,
284 | hint = AbstractOption.hint,
285 | }
286 |
287 | InputField = {
288 | -- Input box, you can type a value of your choice
289 | _cls = "InputField",
290 | _widget = true,
291 |
292 | new = function (id)
293 | assert(type(id) == "string", "MCM Builder: no id given")
294 | local t = {id = id, type = "input", val = 0}
295 | setmetatable(t, {__index = InputField})
296 | return t
297 | end,
298 |
299 | default = AbstractOption.default,
300 | minmax = AbstractOption.minmax,
301 | input_type = AbstractOption.input_type,
302 | cmd = AbstractOption.cmd,
303 |
304 | precondition = AbstractOption.precondition,
305 | callback = AbstractOption.callback,
306 | postcondition = AbstractOption.postcondition,
307 |
308 | current_value = AbstractOption.current_value,
309 | hint = AbstractOption.hint,
310 | }
311 |
312 | RadioBox = {
313 | -- Radio box, select one out of many choices.
314 | _cls = "RadioBox",
315 | _widget = true,
316 |
317 | new = function (id)
318 | assert(type(id) == "string", "MCM Builder: no id given")
319 | local t = {id = id, type = "radio_h", val = 0}
320 | setmetatable(t, {__index = RadioBox})
321 | return t
322 | end,
323 |
324 | vertical = function (self)
325 | -- Makes vertical, lol
326 | self.type = "radio_v"
327 | self.force_horz = nil
328 | return self
329 | end,
330 |
331 | force_horizontal = function (self)
332 | -- Force the radio buttons into horizental layout,
333 | -- despite their number
334 | assert(self.type == "radio_h", "MCM Builder: force_horizontal on vertical radiobox")
335 | self.force_horz = true
336 | return self
337 | end,
338 |
339 | input_type = AbstractOption.input_type,
340 | cmd = AbstractOption.cmd,
341 | dont_translate = AbstractOption.dont_translate,
342 |
343 | precondition = AbstractOption.precondition,
344 | callback = AbstractOption.callback,
345 | postcondition = AbstractOption.postcondition,
346 |
347 | current_value = AbstractOption.current_value,
348 | hint = AbstractOption.hint,
349 | }
350 |
351 | Trackbar = {
352 | -- Track bar, easy way to control numeric options with min/max values
353 | _cls = "Trackbar",
354 | _widget = true,
355 |
356 | new = function (id)
357 | assert(type(id) == "string", "MCM Builder: no id given")
358 | local t = {id=id, type = "track", val = 2, min = 0, max = 2, step = 0.1, def = 1}
359 | setmetatable(t, {__index = Trackbar})
360 | return t
361 | end,
362 |
363 | step = function (self, step)
364 | -- USE increment INSTEAD! THIS ONE WILL CRASH.
365 | assert(type(step) == "number", "MCM Builder: step is not a number")
366 | self.step = step
367 | return self
368 | end,
369 |
370 | increment = function (self, step)
371 | -- Set step
372 | assert(type(step) == "number", "MCM Builder: step is not a number")
373 | self.step = step
374 | return self
375 | end,
376 |
377 | precision = function (self, prec)
378 | -- allowed number of zeros in a number
379 | self.prec = prec
380 | return self
381 | end,
382 |
383 | minmax = AbstractOption.minmax,
384 | default = AbstractOption.default,
385 | cmd = AbstractOption.cmd,
386 | dont_translate = AbstractOption.dont_translate,
387 |
388 | precondition = AbstractOption.precondition,
389 | callback = AbstractOption.callback,
390 | postcondition = AbstractOption.postcondition,
391 |
392 | current_value = AbstractOption.current_value,
393 | hint = AbstractOption.hint,
394 | }
395 |
396 | KeybindBox = {
397 | -- Button that registers a keypress after being clicked.
398 | _cls = "KeybindBox",
399 | _widget = true,
400 |
401 | new = function (id)
402 | assert(type(id) == "string", "MCM Builder: no id given")
403 | local t = {id=id, type = "key_bind", val = 2}
404 | setmetatable(t, {__index = KeybindBox})
405 | return t
406 | end,
407 |
408 | cmd = AbstractOption.cmd,
409 | precondition = AbstractOption.precondition,
410 | hint = AbstractOption.hint,
411 | }
412 |
413 |
414 | Line = {
415 | -- Literally a useless line
416 | _cls = "Line",
417 | _widget = true,
418 |
419 | new = function ()
420 | local t = {id = "line", type = "line"}
421 | setmetatable(t, {__index = Line})
422 | return t
423 | end,
424 | }
425 |
426 | Image = {
427 | -- Literally a useless image
428 | _cls = "Image",
429 | _widget = true,
430 |
431 | new = function (id)
432 | assert(type(id) == "string", "MCM Builder: no id given")
433 | local t = {id=id, type="image"}
434 | setmetatable(t, {__index = Image})
435 | return t
436 | end,
437 |
438 | image = AbstractOption.image,
439 | }
440 |
441 | ImageWithText = {
442 | -- Useless image on the left, maybe useful text on the right
443 | _cls = "ImageWithText",
444 | _widget = true,
445 |
446 | new = function (id)
447 | assert(type(id) == "string", "MCM Builder: no id given")
448 | local t = {id=id, type="slide", size={512,50}}
449 | setmetatable(t, {__index = ImageWithText})
450 | return t
451 | end,
452 |
453 | size = function (self, size)
454 | -- custom size for the texture
455 | assert(#size == 2, "MCM Builder: unknown size type")
456 | self.size = size
457 | return self
458 | end,
459 |
460 | stretch = function (self)
461 | -- force the texture to stretch or not
462 | self.stretch = true
463 | return self
464 | end,
465 |
466 | position = function (self, x, y)
467 | -- position
468 | assert(x + y, "MCM Builder: bad position arguments")
469 | self.pos = {x, y}
470 | return self
471 | end,
472 |
473 | v_offset = function (self, offset)
474 | -- height offset to add extra space
475 | assert(type(offset) == "number", "MCM Builder: offset is not a number")
476 | self.spacing = offset
477 | return self
478 | end,
479 |
480 | image = AbstractOption.image,
481 | text = AbstractOption.text,
482 | }
483 |
484 | Title = {
485 | -- Big Fucking Text
486 | _cls = "Title",
487 | _widget = true,
488 |
489 | new = function (id)
490 | assert(type(id) == "string", "MCM Builder: no id given")
491 | local t = {id = id, type = "title", align = "c"}
492 | setmetatable(t, {__index = Title})
493 | return t
494 | end,
495 |
496 | align = function (self, str)
497 | -- determines the alignment of the title
498 | if str == "center" then
499 | self.align = 'c'
500 | elseif str == "right" then
501 | self.align = 'r'
502 | elseif str == "left" then
503 | self.align = 'l'
504 | else
505 | assert(nil, "MCM Builder: unknown alignment: "..str)
506 | end
507 |
508 | return self
509 | end,
510 |
511 | color = AbstractOption.color,
512 | text = AbstractOption.text,
513 | }
514 |
515 | Description = {
516 | -- Small text, left alignment
517 | _cls = "Description",
518 | _widget = true,
519 |
520 | new = function (id)
521 | assert(type(id) == "string", "MCM Builder: no id given")
522 | local t = {id = id, type = "desc"}
523 | setmetatable(t, {__index = Description})
524 | return t
525 | end,
526 |
527 | text = AbstractOption.text,
528 | }
529 |
530 | --[[
531 | -- Example script
532 |
533 | Tree = igi_mcm_builder.Tree
534 | Page = igi_mcm_builder.Page
535 | Checkbox = igi_mcm_builder.Checkbox
536 | Title = igi_mcm_builder.Title
537 | Line = igi_mcm_builder.Line
538 | Description = igi_mcm_builder.Description
539 |
540 | function on_mcm_load()
541 | local tree = Tree.new("MCM_Builder")
542 | local page = Page.new("With Checkbox")
543 | local check = Checkbox.new("My_cool_checkbox"):default(true)
544 | page:add(check)
545 |
546 | local page_two = Page.new("With_description_and_line")
547 | local title = Title.new("uwu")
548 | :text("Title??? Omegalul")
549 | :color(200,100,50,255)
550 | page_two:add(title)
551 | page_two:add(Description.new("descr"):text("What a nice description"))
552 | page_two:add(Line.new())
553 |
554 | return tree:add_page(page):add_page(page_two):build()
555 | end
556 |
557 | ]]
558 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_random.script:
--------------------------------------------------------------------------------
1 | local sin = math.sin
2 | local floor = math.floor
3 | local offset
4 |
5 | function on_game_start()
6 | RegisterScriptCallback("on_game_load", on_game_load)
7 | random_seed = device():time_global()
8 | end
9 |
10 | function on_game_load()
11 | if not load_var(db.actor, "igi_tasks_random") then
12 | save_var(db.actor, "igi_tasks_random", math.random(1, 65535))
13 | end
14 | offset = load_var(db.actor, "igi_tasks_random") / 3709 -- unique for every game
15 | end
16 |
17 | function _set_seed(seed)
18 | local sd = 0
19 | if type(seed) == "string" then
20 | function a(h)
21 | sd = sd + string.byte(h)
22 | end
23 | seed:gsub(".", a)
24 | random_seed = sd * offset
25 | elseif type(seed) == "number" then
26 | random_seed = seed * offset
27 | else
28 | igi_helper.trace_assert(nil, "random: bad seed of type "..type(seed))
29 | end
30 | end
31 |
32 | function rand(b, a)
33 | local rand_num = (((sin(random_seed)/2+0.5)*10000)%100)/100 -- from SO
34 | random_seed = rand_num
35 |
36 | if a and b then
37 | return floor((b-a+.99)*rand_num) + a -- random integer from a to b
38 | elseif b then
39 | return floor((b-.01)*rand_num) + 1 -- random integer from 1 to b
40 | else
41 | return rand_num
42 | end
43 |
44 | return 0.5
45 | end
46 |
47 | function set_seed(task_id)
48 | local game_time = game.get_game_time()
49 | local game_date = game_time:dateToString(game.CTime.DateToDay)
50 | _set_seed(task_id..game_date)
51 | end
52 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_rewards.script:
--------------------------------------------------------------------------------
1 | local trace_dbg = igi_helper.trace_dbg
2 | local trace_assert = igi_helper.trace_assert
3 | local TASK_STATUSES = igi_subtask.TASK_STATUSES
4 | local set = igi_utils.Set.from_list
5 |
6 | DEFAULT_REWARDER = {
7 | has_material_rewards = function (self, CACHE)
8 | local _, high = self:guess_rewards(CACHE)
9 | return high.money > 0
10 | end,
11 |
12 | split_to_rewards = function (self, points, quest_id)
13 | local money = Money.points_to_value(points * 0.8)
14 | local goodwill = Goodwill.points_to_value(points * 0.2)
15 | return {
16 | money = math.floor(money
17 | * igi_mcm.get_options_value("money_reward_coeff")
18 | * igi_mcm.get_task_value(quest_id, "money_reward_coeff")
19 | ),
20 | goodwill = math.ceil(goodwill
21 | * igi_mcm.get_options_value("goodwill_reward_coeff")
22 | * igi_mcm.get_task_value(quest_id, "goodwill_reward_coeff")
23 | ),
24 | }
25 | end,
26 |
27 | guess_rewards = function (self, CACHE)
28 | local low, high = get_reward_bounds(CACHE.entities, false, CACHE)
29 | trace_assert(low ~= nil, "Low reward bound is nil")
30 | if high == nil then
31 | high = low
32 | end
33 | return self:split_to_rewards(low, CACHE.quest_id), self:split_to_rewards(high, CACHE.quest_id)
34 | end
35 | }
36 |
37 | _STATIC_REWARDER = {
38 | money = 0,
39 | goodwill = 0,
40 |
41 | has_material_rewards = function (self, CACHE)
42 | return self.money > 0
43 | end,
44 |
45 | split_to_rewards = function (self, points, quest_id)
46 | -- Ignores points, manually adjust to economy
47 | local multi = game_difficulties.get_eco_factor("rewards") or 1
48 | return {
49 | money = math.floor(self.money * multi * igi_mcm.get_options_value("money_reward_coeff")),
50 | goodwill = math.ceil(self.goodwill * multi * igi_mcm.get_options_value("goodwill_reward_coeff"))
51 | }
52 | end,
53 |
54 | guess_rewards = function(self, CACHE)
55 | local s = self:split_to_rewards(0, CACHE.quest_id)
56 | return s, s
57 | end
58 | }
59 | function Static(money_and_goodwill)
60 | igi_helper.trace_assert(money_and_goodwill.money, "Static rewarder without money set")
61 | igi_helper.trace_assert(money_and_goodwill.goodwill, "Static rewarder without goodwill set")
62 | return setmetatable(money_and_goodwill, {__index = _STATIC_REWARDER})
63 | end
64 |
65 | local function get_rewarder(CACHE)
66 | if not CACHE.rewarder then
67 | return DEFAULT_REWARDER
68 | end
69 | local link_context = igi_text_processor.get_link_context(CACHE)
70 | return igi_text_processor.eval_logic_macro(CACHE.rewarder, link_context)
71 | end
72 |
73 | function has_material_rewards(CACHE)
74 | return get_rewarder(CACHE):has_material_rewards(CACHE)
75 | end
76 |
77 | function guess_rewards(CACHE)
78 | return get_rewarder(CACHE):guess_rewards(CACHE)
79 | end
80 |
81 | function collect_and_give_rewards(CACHE)
82 | local total = collect_rewards(CACHE.entities, CACHE)
83 | local rewards = get_rewarder(CACHE):split_to_rewards(total, CACHE.quest_id)
84 |
85 | trace_dbg("rewards", CACHE.rewarder or "DEFAULT_REWARDER", rewards)
86 | igi_callbacks.invoke_callbacks("on_before_rewarding", CACHE, rewards)
87 | local faction = igi_helper.get_community_by_id(CACHE.task_giver_id)
88 | give_rewards(rewards, faction)
89 | end
90 |
91 | function collect_rewards(entities, CACHE)
92 | local low, high = get_reward_bounds(entities, true, CACHE)
93 | if (low ~= high) then
94 | igi_helper.trace_error("Rewards did not converge, low: " .. low .. " high: " .. high)
95 | end
96 | return low
97 | end
98 |
99 | function get_reward_bounds(entities, only_completed, CACHE)
100 | local lower_bound = 0
101 | local higher_bound = 0
102 | for _, entity in pairs(entities) do
103 | local controller = igi_taskdata.get_controller(entity, CACHE)
104 | if controller.complexity and ((not only_completed) or entity.status == TASK_STATUSES.COMPLETED) then
105 | local low, high = controller.complexity(entity)
106 | if high == nil then high = low end
107 | lower_bound = lower_bound + low
108 | higher_bound = higher_bound + high
109 | end
110 | end
111 | local multi = game_difficulties.get_eco_factor("rewards") or 1
112 | return lower_bound * multi, higher_bound * multi
113 | end
114 |
115 | function give_rewards(rewards, faction)
116 | if (rewards.money) then
117 | Money.give(rewards.money)
118 | end
119 |
120 | if (rewards.goodwill) then
121 | Goodwill.give(rewards.goodwill, faction)
122 | end
123 | end
124 |
125 | Goodwill = {
126 | give = function (amount, faction)
127 | if amount == 0 then return end
128 | xr_effects.inc_faction_goodwill_to_actor(db.actor, nil, { faction, amount, true })
129 | end,
130 |
131 | points_to_value = function(points)
132 | return points / 50
133 | end
134 | }
135 |
136 | Money = {
137 | give = function (amount)
138 | if amount ~= 0 then
139 | dialogs.relocate_money(db.actor, amount, "in")
140 | end
141 | end,
142 |
143 | points_to_value = function (points)
144 | return points
145 | end
146 | }
147 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_subtask.script:
--------------------------------------------------------------------------------
1 | TASK_STATUSES = {
2 | RUNNING = nil, -- no value means running
3 | COMPLETED = "complete",
4 | FAILED = "fail",
5 | READY_TO_FINISH = "READY_TO_FINISH",
6 | }
7 |
8 | function finish_all_subtasks(CACHE)
9 | if CACHE.status ~= TASK_STATUSES.FAILED then
10 | CACHE.status = TASK_STATUSES.COMPLETED
11 | end
12 |
13 | for _, entity in pairs(CACHE.entities) do
14 | if entity.status == TASK_STATUSES.READY_TO_FINISH then
15 | entity.status = CACHE.status
16 | end
17 | end
18 | end
19 |
20 | function update_current_map_target(CACHE)
21 | if CACHE.status == TASK_STATUSES.COMPLETED
22 | or CACHE.status == TASK_STATUSES.READY_TO_FINISH then
23 | CACHE.quest_targets = {CACHE.task_giver_id}
24 | else
25 | CACHE.quest_targets = get_quest_target(CACHE.entities, CACHE)
26 | end
27 | end
28 |
29 | function process_subtasks(CACHE)
30 | local updated = false
31 | for _, entity in pairs(CACHE.entities) do
32 | if entity.status == TASK_STATUSES.RUNNING
33 | or entity.status == TASK_STATUSES.READY_TO_FINISH then
34 | if update_entity_status(entity, CACHE) then
35 | igi_callbacks.invoke_callbacks("on_subtask_status_change", CACHE, entity)
36 | updated = true
37 | end
38 | end
39 | end
40 |
41 | if updated then
42 | CACHE.status = get_task_status(CACHE.entities, CACHE)
43 | end
44 |
45 | return updated
46 | end
47 |
48 | function get_quest_target(entities, CACHE)
49 | local got_first = false
50 | local targets = {}
51 | for _, entity in ipairs(entities) do
52 | local controller = igi_taskdata.get_controller(entity, CACHE)
53 | if controller.status and controller.quest_target
54 | and entity.status == TASK_STATUSES.RUNNING then
55 | if (not got_first or entity.subtask) then
56 | got_first = true
57 | targets[#targets+1] = controller.quest_target(entity)
58 | end
59 | end
60 | end
61 | return targets
62 | end
63 |
64 | function get_task_status(entities, CACHE)
65 | local ready_to_finish = false
66 | local running = false
67 | for _, entity in pairs(entities) do
68 | local controller = igi_taskdata.get_controller(entity, CACHE)
69 | if controller.status then
70 | if entity.status == TASK_STATUSES.FAILED and not entity.optional then
71 | return TASK_STATUSES.FAILED
72 | end
73 |
74 | if entity.status == TASK_STATUSES.RUNNING then
75 | running = true
76 | elseif entity.status == TASK_STATUSES.READY_TO_FINISH then
77 | ready_to_finish = true
78 | end
79 | end
80 | end
81 |
82 | if running then return TASK_STATUSES.RUNNING
83 | elseif ready_to_finish then return TASK_STATUSES.READY_TO_FINISH
84 | else return TASK_STATUSES.COMPLETED end
85 | end
86 |
87 | function update_entity_status(entity, CACHE)
88 | local controller = igi_taskdata.get_controller(entity, CACHE)
89 | if not controller.status then return false end
90 | local new_status = controller.status(entity)
91 | if new_status == entity.status then return false end
92 | entity.status = new_status
93 | return true
94 | end
95 |
96 | function update_entities(CACHE)
97 | if not CACHE._queue then return end
98 | igi_helper.trace_dbg("Update: updating entities", CACHE)
99 | for _, v in ipairs(CACHE._queue.add) do
100 | enable_entities(v, CACHE)
101 | init_entities(v, CACHE)
102 | end
103 |
104 | igi_helper.trace_dbg("Update: After adding", CACHE)
105 | for _, v in ipairs(CACHE._queue.rem) do
106 | disable_entity(v, CACHE)
107 | end
108 |
109 | CACHE._queue = nil
110 | igi_helper.trace_dbg("Update: finish", CACHE)
111 | end
112 |
113 | function enable_entities(key, CACHE)
114 | igi_helper.trace_assert(key ~= nil and type(CACHE[key]) == "table", "Can't enable entities: No table for key " .. tostring(key) .. ".")
115 |
116 | igi_helper.trace_dbg("Enable: enable entities", key, CACHE[key])
117 |
118 | local to_add = CACHE[key]
119 |
120 | -- Add and generate
121 | for k, entity in ipairs(to_add) do
122 | igi_helper.trace_dbg("Enable: Processing entity", entity)
123 | entity._group = key
124 |
125 | local link_context = igi_text_processor.get_link_context(CACHE, entity)
126 | local new_entities = igi_generate.generate(entity, link_context)
127 |
128 | for _, new_entity in ipairs(new_entities) do
129 | CACHE.entities[#CACHE.entities+1] = new_entity
130 | end
131 | to_add[k] = nil
132 | end
133 |
134 | CACHE[key] = nil
135 | igi_helper.trace_dbg("Enable: after run", key, CACHE)
136 | end
137 |
138 | function disable_entity(key, CACHE)
139 | -- De-initialize
140 | local target
141 | for _, v in pairs(CACHE.entities) do
142 | if v.link_id == key then
143 | target = v
144 | break
145 | end
146 | end
147 |
148 | if not target then return end
149 |
150 | local controller = igi_taskdata.get_controller(target, CACHE)
151 | if controller.on_del then
152 | controller.on_del(target)
153 | end
154 |
155 | local link_context = igi_text_processor.get_link_context(CACHE)
156 | igi_text_processor.resolve_table_macros(target, "del", link_context)
157 |
158 | target["-CONTROLLER"] = target.CONTROLLER
159 | target["-actions"] = target.actions
160 |
161 | target.CONTROLLER = nil
162 | target.actions = nil
163 |
164 | igi_callbacks.invoke_callbacks("on_entity_del", CACHE, target)
165 | end
166 |
167 | function init_entities(key, CACHE)
168 | -- Initialize
169 | for i=1, #CACHE.entities do
170 | local entity = CACHE.entities[i]
171 | if entity._group == key then
172 | local link_context = igi_text_processor.get_link_context(CACHE, entity)
173 | igi_text_processor.resolve_table_macros(entity, "init", link_context)
174 |
175 | local controller = igi_taskdata.get_controller(entity, CACHE)
176 | if controller.on_init then
177 | controller.on_init(entity)
178 | end
179 |
180 | igi_callbacks.invoke_callbacks("on_entity_init", CACHE, entity)
181 | end
182 | end
183 | end
184 |
185 | function invoke_controller(callback, CACHE, ...)
186 | for _, entity in ipairs(CACHE.entities) do
187 | local controller = igi_taskdata.get_controller(entity, CACHE)
188 | if controller[callback] then
189 | controller[callback](entity, ...)
190 | end
191 | end
192 | end
193 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_target_assault.script:
--------------------------------------------------------------------------------
1 | local trace_assert = igi_helper.trace_assert
2 | local TASK_STATUSES = igi_subtask.TASK_STATUSES
3 |
4 | local function is_legit_squad(squad)
5 | local section = squad and squad:section_name()
6 | return squad and (not section:find("tushkano")) and (not section:find("rat"))
7 | end
8 |
9 | local function is_squad_at_smart(se_squad, smart_id)
10 | local scripted_target = se_squad:get_script_target()
11 | if scripted_target then
12 | return scripted_target == smart_id
13 | end
14 |
15 | if se_squad.current_target_id ~= smart_id then
16 | return false
17 | end
18 |
19 | if se_squad.current_action ~= 1 then
20 | return false
21 | end
22 |
23 | if not is_legit_squad(se_squad) then
24 | return false
25 | end
26 |
27 | return true
28 | end
29 |
30 | local function is_completed(obj_data)
31 | trace_assert(SIMBOARD.smarts[obj_data.id], "assault but not smart")
32 |
33 | local cleared = true
34 | for _, sq_id in pairs(obj_data.squads) do
35 | local squad = alife_object(sq_id)
36 | if squad then
37 | cleared = false
38 | if not (squad.first_update) then break end
39 | squad.stay_time = game.get_game_time()
40 | squad.force_online = true
41 | end
42 | end
43 |
44 | return cleared
45 | end
46 |
47 | local function add_target_squads(entity)
48 | entity.squads = {}
49 | local smart = SIMBOARD.smarts[entity.id]
50 | for sq_id in pairs(smart.squads) do
51 | local se_squad = alife_object(sq_id)
52 | local actor_faction = character_community(db.actor)
53 | local sq_faction = se_squad:get_squad_community()
54 | if is_squad_at_smart(se_squad, entity.id) and
55 | game_relations.is_factions_enemies(actor_faction, sq_faction) then
56 | table.insert(entity.squads, se_squad.id)
57 | end
58 | end
59 | end
60 |
61 | Assault = {}
62 | function Assault.on_init(entity)
63 | trace_assert(type(entity.id) == "number", "Assault: entity.id is not a number", entity)
64 | trace_assert(alife_object(entity.id), "Assault: no server object for this id", entity)
65 | trace_assert(SIMBOARD.smarts[entity.id], "Assault: entity.id is not a smart", entity)
66 | add_target_squads(entity)
67 | end
68 |
69 | local function mark_distant_squads(entity)
70 | local smart_position = alife_object(entity.id).position
71 |
72 | for _, sq_id in pairs(entity.squads) do
73 | local se_squad = alife_object(sq_id)
74 | if se_squad then
75 | local is_marked = level.map_has_object_spot(se_squad.id, "red_location") == 1
76 | local is_nearby = smart_position:distance_to_sqr(se_squad.position) < 2500
77 |
78 | if not (is_nearby or is_marked) then
79 | level.map_add_object_spot(se_squad.id, "red_location", "st_ui_pda_task_unknown_enemy")
80 | elseif is_nearby and is_marked then
81 | level.map_remove_object_spot(se_squad.id, "red_location")
82 | end
83 | end
84 | end
85 | end
86 |
87 | function Assault.status(entity)
88 | mark_distant_squads(entity)
89 | if is_completed(entity) then return TASK_STATUSES.COMPLETED end
90 | return TASK_STATUSES.RUNNING
91 | end
92 |
93 | function Assault.quest_target(entity)
94 | return entity.id
95 | end
96 |
97 | function Assault.complexity(entity)
98 | return 5000
99 | end
100 |
101 | function Assault.test(entity)
102 | local assert_test = igi_tests.assert_test
103 | entity._test_stage = (entity._test_stage or 0) + 1
104 |
105 | if entity._test_stage == 1 then
106 | local se_obj = alife_object(entity.id)
107 | assert_test(se_obj, "Entity does not exist")
108 | if igi_mcm.get_options_value("fast_tests") then
109 | for _, sq_id in pairs(entity.squads) do
110 | local se_squad = alife_object(sq_id)
111 | if se_squad then
112 | igi_tests.teleport_to_player(se_squad)
113 | end
114 | end
115 | else
116 | igi_tests.travel_to_se_obj(se_obj)
117 | end
118 | elseif entity._test_stage == 2 then
119 | for _, sq_id in pairs(entity.squads) do
120 | local se_squad = alife_object(sq_id)
121 | if se_squad then
122 | for se_npc in se_squad:squad_members() do
123 | local npc = get_object_by_id(se_npc.id)
124 | npc:kill(db.actor)
125 | end
126 | get_object_by_id(se_squad:commander_id()):kill(db.actor)
127 | end
128 | end
129 | elseif entity._test_stage == 4 then
130 | assert_test(entity.status == "COMPLETED", "Quest did not complete after killing")
131 | return true
132 | end
133 | end
134 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_target_escort.script:
--------------------------------------------------------------------------------
1 | local TASK_STATUSES = igi_subtask.TASK_STATUSES
2 | local trace_assert = igi_helper.trace_assert
3 |
4 | function make_companion(entity)
5 | local squad = alife_object(entity.id)
6 |
7 | axr_companions.companion_squads[squad.id] = squad
8 | for k in squad:squad_members() do
9 | local se_obj = k.object or k.id and alife_object(k.id)
10 | se_save_var(se_obj.id,se_obj:name(),"companion",true)
11 | se_save_var(se_obj.id,se_obj:name(),"companion_cannot_dismiss",true)
12 | se_save_var(se_obj.id,se_obj:name(),"companion_cannot_teleport",entity.cant_teleport == "true")
13 | end
14 |
15 | -- Add to ignore offline combat simulation list
16 | sim_offline_combat.task_squads[squad.id] = true
17 | end
18 |
19 | Escort = {}
20 | function Escort.on_init(entity)
21 | trace_assert(type(entity.id) == "number", "Escort: entity.id is not a number", entity)
22 | trace_assert(alife_object(entity.id), "Escort: no server object for this id", entity)
23 | trace_assert(alife_object(entity.id):commander_id(), "Escort: entity.id is not a squad", entity)
24 | end
25 |
26 | function Escort.status(entity)
27 | return TASK_STATUSES.READY_TO_FINISH
28 | end
29 |
30 | function Escort.on_del(entity)
31 | local se_squad = alife_object(entity.id)
32 | local _ = se_squad and axr_companions.dismiss_special_squad(se_squad)
33 | end
34 |
35 | function Escort.test(entity)
36 | local assert_test = igi_tests.assert_test
37 | local se_squad = alife_object(entity.id)
38 | for member in se_squad:squad_members() do
39 | local se_npc = alife_object(member.id)
40 | assert_test(
41 | se_load_var(se_npc.id, se_npc:name(), "companion"), "Not in actor's squad")
42 | end
43 | assert_test(entity.status == "READY_TO_FINISH", "Quest did not complete")
44 | return true
45 | end
46 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_target_fetch.script:
--------------------------------------------------------------------------------
1 | local TASK_STATUSES = igi_subtask.TASK_STATUSES
2 | local trace_assert = igi_helper.trace_assert
3 |
4 | function ready_to_finish(obj_data)
5 | local ids = get_fetched_items(obj_data)
6 | local amount = obj_data.amount or 1
7 | for id, itm_amount in pairs(ids) do
8 | if amount <= 0 then
9 | ids[id] = nil
10 | end
11 | amount = amount - itm_amount
12 | end
13 | obj_data._ids = ids
14 | obj_data._complexity = calculate_complexity(obj_data) * 1.25
15 | return amount <= 0
16 | end
17 |
18 | function get_fetched_items(obj_data)
19 | local section = obj_data.section_name
20 | local total = 0
21 | local ids = {}
22 | local is_multi = IsItem("multiuse",section)
23 | local is_ammo = IsItem("ammo",section)
24 |
25 | local function itr(temp, obj)
26 | if not string.find(obj:section(), section) then return end
27 | if is_multi then
28 | ids[obj:id()] = obj:get_remaining_uses()
29 | total = total + obj:get_remaining_uses()
30 | elseif is_ammo then
31 | ids[obj:id()] = obj:ammo_get_count()
32 | total = total + obj:ammo_get_count()
33 | else
34 | ids[obj:id()] = 1
35 | total = total + 1
36 | end
37 | end
38 | db.actor:iterate_inventory(itr, nil)
39 | return ids, total
40 | end
41 |
42 | function get_item_base_cost(section)
43 | local max_uses = IsItem("multiuse",section) or 1
44 | ---@diagnostic disable-next-line: undefined-field
45 | local cost = ini_sys:r_float_ex(section,"cost") * (1 / max_uses)
46 | return cost
47 | end
48 |
49 | local function is_scaled_by_condition(sec)
50 | if IsItem("multiuse",sec) then return false end
51 | if IsItem("device",sec) then return false end
52 | if IsItem("battery", sec) then return false end
53 | return true
54 | end
55 |
56 | function give_arti_container(obj)
57 | local sec = obj:section()
58 |
59 | if (string.find(sec, "(lead.-_box)",3)) then
60 | alife_create_item("lead_box", db.actor)
61 | elseif (string.find(sec, "(af.-_iam)",3)) then
62 | alife_create_item("af_iam", db.actor)
63 | elseif (string.find(sec, "(af.-_aac)",3)) then
64 | alife_create_item("af_aac", db.actor)
65 | elseif (string.find(sec, "(af.-_aam)",3)) then
66 | alife_create_item("af_aam", db.actor)
67 | end
68 | end
69 |
70 | function release_fetch_items(entity)
71 | local amount = entity.amount or 1
72 | for id, itm_amount in pairs(entity._ids) do
73 | amount = amount - itm_amount
74 | local obj = assert(get_object_by_id(id))
75 | if obj:section() ~= entity.section_name then
76 | give_arti_container(obj)
77 | end
78 | alife_release(alife_object(id))
79 | end
80 |
81 | if amount < 0 then
82 | alife_create_item(entity.section_name, db.actor, {uses = -amount})
83 | end
84 | end
85 |
86 | Fetch = {}
87 | function Fetch.on_init(entity)
88 | trace_assert(type(entity.section_name) == "string", "Fetch: entity.section_name is not a string", entity)
89 | ---@diagnostic disable-next-line: undefined-field
90 | trace_assert(ini_sys:section_exist(entity.section_name), "Fetch: entity section does not exist", entity)
91 | end
92 |
93 | function Fetch.status(entity)
94 | if ready_to_finish(entity) then return TASK_STATUSES.READY_TO_FINISH end
95 | return TASK_STATUSES.RUNNING
96 | end
97 |
98 | function Fetch.quest_target(entity)
99 | return -1
100 | end
101 |
102 | function Fetch.get_description(entity)
103 | ---@diagnostic disable-next-line: undefined-field
104 | local item_name = ini_sys:r_string_ex(entity.section_name, "inv_name")
105 | local str = game.translate_string(item_name)..", "..tostring(entity.amount or 1)
106 | if igi_mcm.get_options_value("utjan_fetch_thing") then
107 | local _, amount = get_fetched_items(entity)
108 | str = str .. " " .. string.format(game.translate_string("igi_tasks_utjan_fetch_need"), amount)
109 | end
110 | return {
111 | targets = {str}
112 | }
113 | end
114 |
115 | function calculate_complexity(entity)
116 | local base_cost = get_item_base_cost(entity.section_name)
117 | local scaled_by_condition = is_scaled_by_condition(entity.section_name)
118 | if entity._ids and scaled_by_condition then
119 | local money = 0
120 | for id in pairs(entity._ids) do
121 | local obj = get_object_by_id(id)
122 | if obj then
123 | local condition = obj:condition()
124 | money = money + base_cost * condition * condition
125 | end
126 | end
127 | return money
128 | end
129 | return get_item_base_cost(entity.section_name) * (entity.amount or 1)
130 | end
131 |
132 | function Fetch.complexity(entity)
133 | if entity._complexity then return entity._complexity end
134 | return calculate_complexity(entity) * 1.25
135 | end
136 |
137 | function Fetch.on_complete(entity)
138 | release_fetch_items(entity)
139 | news_manager.relocate_item(db.actor, "out", entity.section_name, entity.amount or 1)
140 | end
141 |
142 | function Fetch.test(entity)
143 | local assert_test = igi_tests.assert_test
144 | entity._test_stage = (entity._test_stage or 0) + 1
145 |
146 | if entity._test_stage == 1 then
147 | local se_actor = alife_object(0)
148 | for _=1, (entity.amount or 1) do
149 | alife_create_item(entity.section_name, se_actor)
150 | end
151 |
152 | elseif entity._test_stage == 3 then
153 | assert_test(entity.status == "READY_TO_FINISH", "Quest did not complete")
154 | return true
155 | end
156 | end
157 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_target_get.script:
--------------------------------------------------------------------------------
1 | local TASK_STATUSES = igi_subtask.TASK_STATUSES
2 | local trace_assert = igi_helper.trace_assert
3 |
4 | Get = {}
5 | function Get.on_init(entity)
6 | trace_assert(type(entity.id) == "number", "Get: entity.id is not a number", entity)
7 | trace_assert(alife_object(entity.id), "Get: no server object for this id", entity)
8 | end
9 |
10 | function Get.status(entity)
11 | local se_obj = alife_object(entity.id)
12 | if not se_obj then return TASK_STATUSES.FAILED end
13 | if se_obj.parent_id == 0 then return TASK_STATUSES.COMPLETED end
14 | return TASK_STATUSES.RUNNING
15 | end
16 |
17 | function Get.quest_target(obj_data)
18 | local se_obj = assert(alife_object(obj_data.id))
19 | if se_obj.parent_id == 65535 then
20 | return obj_data.id
21 | else
22 | return se_obj.parent_id
23 | end
24 | end
25 |
26 | function Get.complexity(entity)
27 | return 0
28 | end
29 |
30 | function Get.test(entity)
31 | local assert_test = igi_tests.assert_test
32 | entity._test_stage = (entity._test_stage or 0) + 1
33 | if not entity._test_stage == 1 then
34 | local se_obj = alife_object(entity.id)
35 | assert_test(se_obj, "Entity does not exist")
36 | if igi_mcm.get_options_value("fast_tests") then
37 | igi_tests.teleport_to_player(se_obj)
38 | else
39 | igi_tests.travel_to_se_obj(se_obj)
40 | end
41 |
42 | elseif entity._test_stage == 2 then
43 | local se_obj = alife_object(entity.id)
44 | local obj = get_object_by_id(se_obj.id)
45 | db.actor:transfer_item(obj, db.actor)
46 |
47 | elseif entity._test_stage == 3 then
48 | assert_test(entity.status == "COMPLETED", "Quest did not complete")
49 | return true
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_target_kill.script:
--------------------------------------------------------------------------------
1 | local trace_assert = igi_helper.trace_assert
2 | local TASK_STATUSES = igi_subtask.TASK_STATUSES
3 |
4 | Kill = {}
5 | function Kill.on_init(entity)
6 | trace_assert(type(entity.id) == "number", "Kill: entity.id is not a number", entity)
7 | trace_assert(alife_object(entity.id), "Kill: no server object for this id", entity)
8 | trace_assert(alife_object(entity.id):commander_id(), "Kill: entity.id is not a squad", entity)
9 |
10 | entity._complexity = calculate_complexity(entity)
11 | end
12 |
13 | function Kill.status(entity)
14 | if not alife_object(entity.id) then return TASK_STATUSES.COMPLETED end
15 | return TASK_STATUSES.RUNNING
16 | end
17 |
18 | function Kill.quest_target(entity)
19 | return entity.id
20 | end
21 |
22 | local function get_monster_value(se_npc)
23 | local npc_section = se_npc:section_name()
24 | local factor = 1
25 | for k, new_factor in pairs(igi_helper.db_ini:collect_section('monster_tier_factor')) do
26 | if string.find(npc_section, k) then
27 | factor = assert(tonumber(new_factor))
28 | break
29 | end
30 | end
31 |
32 | local value = 100
33 | for k, new_value in pairs(igi_helper.db_ini:collect_section('money_reward_mutants')) do
34 | if string.find(npc_section, k) then
35 | value = assert(tonumber(new_value))
36 | break
37 | end
38 | end
39 | return value*factor
40 | end
41 |
42 | local function get_npc_value(se_npc)
43 | local value = 1000
44 | local tier = string.match(se_npc:section_name(), "%d") or ""
45 | igi_helper.trace_dbg(se_npc:section_name(), tier)
46 | local factor = igi_helper.db_ini:r_value('npc_tier_factor', tier, 2)
47 |
48 | return value*(factor or 1)
49 | end
50 |
51 | function calculate_complexity(entity)
52 | local reward = 0
53 | local se_squad = assert(alife_object(entity.id))
54 | local faction = se_squad:get_squad_community()
55 |
56 | if string.find(faction, "monster") then
57 | for se_npc in se_squad:squad_members() do
58 | se_npc = alife_object(se_npc.id)
59 | reward = reward + get_monster_value(se_npc)
60 | end
61 | else
62 | for se_npc in se_squad:squad_members() do
63 | se_npc = alife_object(se_npc.id)
64 | reward = reward + get_npc_value(se_npc)
65 | end
66 | end
67 |
68 | return reward
69 | end
70 |
71 | function Kill.complexity(entity)
72 | if entity._complexity then return entity._complexity end
73 | return 1000, 5000
74 | end
75 |
76 | function Kill.test(entity)
77 | local assert_test = igi_tests.assert_test
78 | entity._test_stage = (entity._test_stage or 0) + 1
79 |
80 | if entity._test_stage == 1 then
81 | local se_obj = alife_object(entity.id)
82 | assert_test(se_obj, "Kill entity does not exist")
83 | assert_test(se_obj:squad_members(), "Kill entity is not a squad")
84 | if igi_mcm.get_options_value("fast_tests") then
85 | igi_tests.teleport_to_player(se_obj)
86 | else
87 | igi_tests.travel_to_se_obj(se_obj)
88 | end
89 |
90 | elseif entity._test_stage == 2 then
91 | local se_obj = alife_object(entity.id)
92 | for se_npc in se_obj:squad_members() do
93 | local npc = assert(get_object_by_id(se_npc.id))
94 | npc:kill(db.actor)
95 | end
96 | get_object_by_id(se_obj:commander_id()):kill(db.actor)
97 |
98 | elseif entity._test_stage == 4 then
99 | assert_test(entity.status == "COMPLETED", "Quest did not complete after killing")
100 | return true
101 | end
102 | end
103 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_target_return.script:
--------------------------------------------------------------------------------
1 | local TASK_STATUSES = igi_subtask.TASK_STATUSES
2 | local trace_assert = igi_helper.trace_assert
3 |
4 | Return = {}
5 | function Return.on_init(entity)
6 | trace_assert(type(entity.id) == "number", "Return: entity.id is not a number", entity)
7 | trace_assert(alife_object(entity.id), "Return: no server object for this id", entity)
8 | end
9 |
10 | function Return.status(entity)
11 | local se_obj = alife_object(entity.id)
12 | if not se_obj then return TASK_STATUSES.FAILED end
13 | if se_obj.parent_id == 0 then return TASK_STATUSES.READY_TO_FINISH end
14 | return TASK_STATUSES.RUNNING
15 | end
16 |
17 | function Return.on_complete(obj_data)
18 | local obj = assert(alife_object(obj_data.id))
19 | news_manager.relocate_item(db.actor, "out", obj:section_name(), 1)
20 | alife_release(obj)
21 | end
22 |
23 | function Return.quest_target(obj_data)
24 | local se_obj = alife_object(obj_data.id)
25 | if not se_obj then return end
26 |
27 | return se_obj.parent_id == 65535 and obj_data.id or se_obj.parent_id
28 | end
29 |
30 | function get_item_cost(section_name)
31 | ---@diagnostic disable-next-line: undefined-field
32 | config_cost = tonumber(ini_sys:r_string_ex(section_name, "cost"))
33 | if config_cost == 0 then
34 | return 500
35 | end
36 | return config_cost
37 | end
38 |
39 |
40 | function get_item_weight(section_name)
41 | ---@diagnostic disable-next-line: undefined-field
42 | return tonumber(ini_sys:r_string_ex(section_name, "inv_weight"))
43 | end
44 |
45 | function Return.complexity(entity)
46 | local section = entity.section_name or (type(entity.id) == "number" and alife_object(entity.id):section_name())
47 | if not section then
48 | return 1000, 5000
49 | end
50 |
51 | local weight = math.max(1, get_item_weight(section))
52 | return get_item_cost(section) * weight / 32
53 | end
54 |
55 | function Return.test(entity)
56 | local assert_test = igi_tests.assert_test
57 | entity._test_stage = (entity._test_stage or 0) + 1
58 |
59 | if entity._test_stage == 1 then
60 | local se_obj = alife_object(entity.id)
61 | assert_test(se_obj, "Entity does not exist")
62 | if igi_mcm.get_options_value("fast_tests") then
63 | igi_tests.teleport_to_player(se_obj)
64 | else
65 | igi_tests.travel_to_se_obj(se_obj)
66 | end
67 |
68 | elseif entity._test_stage == 2 then
69 | local se_obj = alife_object(entity.id)
70 | local obj = get_object_by_id(se_obj.id)
71 | db.actor:transfer_item(obj, db.actor)
72 |
73 | elseif entity._test_stage == 3 then
74 | assert_test(entity.status == "READY_TO_FINISH", "Quest did not complete")
75 | return true
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_target_shoot.script:
--------------------------------------------------------------------------------
1 | local TASK_STATUSES = igi_subtask.TASK_STATUSES
2 | local trace_assert = igi_helper.trace_assert
3 |
4 | is_shotted = {}
5 | local callback_is_set = {}
6 |
7 | function on_game_start()
8 | RegisterScriptCallback("save_state", save_state)
9 | RegisterScriptCallback("load_state", load_state)
10 | end
11 |
12 | function save_state(m_data)
13 | m_data.igi_tasks_shoot_shotted = is_shotted
14 | end
15 |
16 | function load_state(m_data)
17 | is_shotted = m_data.igi_tasks_shoot_shotted or {}
18 | end
19 |
20 | local function create_callback_function(squad_id, target_ids, weapon, ammo_type)
21 | local function f(npc, shit)
22 | igi_helper.trace_dbg("shoot: callback: id", npc:id())
23 | if not target_ids[npc:id()] then return end
24 | if shit.draftsman:id() ~= 0 then return end
25 |
26 | local wpn = get_object_by_id(shit.weapon_id)
27 | if weapon and ((not wpn) or wpn:section() ~= weapon) then return end
28 |
29 | if wpn and ammo_type then
30 | local ammo_type_number = wpn:get_ammo_type()
31 | local ammo_list = utils_item.get_ammo(wpn:section(), wpn:id())
32 | local ammo_section = ammo_list[ammo_type_number+1]
33 | if ammo_section ~= ammo_type then return end
34 | end
35 |
36 | is_shotted[squad_id] = true
37 | UnregisterScriptCallback("npc_on_before_hit", f)
38 | UnregisterScriptCallback("monster_on_before_hit", f)
39 | end
40 | return f
41 | end
42 |
43 | local function get_target_ids(squad_id, only_commander)
44 | local se_squad = assert(alife_object(squad_id))
45 | local ids = {}
46 | ids[se_squad:commander_id()] = true
47 | if not only_commander then
48 | for npc in se_squad:squad_members() do
49 | ids[npc.id] = true
50 | end
51 | end
52 | return ids
53 | end
54 |
55 | local function set_callback(squad_id, weapon, ammo_type, only_commander)
56 | local target_ids = get_target_ids(squad_id, only_commander)
57 | -- igi_helper.trace_dbg("shoot: targets", target_ids)
58 | local f = create_callback_function(squad_id, target_ids, weapon, ammo_type)
59 | RegisterScriptCallback("npc_on_before_hit", f)
60 | RegisterScriptCallback("monster_on_before_hit", f)
61 | callback_is_set[squad_id] = f
62 | end
63 |
64 | function is_failed(obj_data)
65 | return (not is_shotted[obj_data.id]) and not alife_object(obj_data.id)
66 | end
67 |
68 | function is_complete(obj_data)
69 | if not is_shotted[obj_data.id] and not callback_is_set[obj_data.id] then
70 | igi_helper.trace_dbg("shoot: obj_data", obj_data)
71 | set_callback(obj_data.id, obj_data.weapon, obj_data.ammo_type, obj_data.only_commander)
72 | end
73 | return is_shotted[obj_data.id]
74 | end
75 |
76 | Shoot = {}
77 | function Shoot.on_init(entity)
78 | trace_assert(type(entity.id) == "number", "Shoot: entity.id is not a number", entity)
79 | trace_assert(alife_object(entity.id), "Shoot: no server object for this id", entity)
80 | trace_assert(alife_object(entity.id):commander_id(), "Shoot: entity.id is not a squad", entity)
81 | end
82 |
83 | function Shoot.status(entity)
84 | if is_failed(entity) then return TASK_STATUSES.FAILED end
85 | if is_complete(entity) then return TASK_STATUSES.COMPLETED end
86 | return TASK_STATUSES.RUNNING
87 | end
88 |
89 | function Shoot.on_del(obj_data)
90 | local callback = callback_is_set[obj_data.id]
91 | if callback then
92 | UnregisterScriptCallback("npc_on_before_hit", callback)
93 | UnregisterScriptCallback("monster_on_before_hit", callback)
94 | callback_is_set[obj_data.id] = nil
95 | end
96 | is_shotted[obj_data.id] = nil
97 | end
98 |
99 | function Shoot.quest_target(entity)
100 | return entity.id
101 | end
102 |
103 | function Shoot.test(entity)
104 | local assert_test = igi_tests.assert_test
105 | entity._test_stage = (entity._test_stage or 0) + 1
106 |
107 | if entity._test_stage == 1 then
108 | local se_obj = alife_object(entity.id)
109 | assert_test(se_obj, "Entity does not exist")
110 | local se_wpn = alife_create_item("wpn_mp133", alife_object(0))
111 | entity._test_wpn_id = se_wpn.id
112 | if igi_mcm.get_options_value("fast_tests") then
113 | igi_tests.teleport_to_player(se_obj)
114 | else
115 | igi_tests.travel_to_se_obj(se_obj)
116 | end
117 |
118 | elseif entity._test_stage == 2 then
119 | local obj = get_object_by_id(entity._test_wpn_id)
120 | db.actor:make_item_active(obj)
121 |
122 | local se_squad = alife_object(entity.id)
123 | ---@diagnostic disable-next-line: undefined-field
124 | alife():teleport_object(se_squad:commander_id(),db.actor:game_vertex_id(),db.actor:level_vertex_id(),db.actor:position())
125 |
126 | elseif entity._test_stage == 4 then
127 | local se_squad = alife_object(entity.id)
128 | local npc = assert(get_object_by_id(se_squad:commander_id()))
129 | local dir = npc:position():sub(db.actor:position())
130 | db.actor:set_actor_direction(-dir:getH())
131 | level.press_action(DIK_keys.MOUSE_1)
132 | level.release_action(DIK_keys.MOUSE_1)
133 |
134 | elseif entity._test_stage == 5 then
135 | assert_test(entity.status == "READY_TO_FINISH", "Quest did not complete")
136 | ---@diagnostic disable-next-line: undefined-field
137 | alife():release(assert(alife_object(entity._test_wpn_id)))
138 | return true
139 | end
140 | end
141 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_target_visit.script:
--------------------------------------------------------------------------------
1 | local trace_assert = igi_helper.trace_assert
2 | MAX_DISTANCE_SQR = 2500
3 |
4 | local function distance(position_1, position_2)
5 | local x = position_1.x - position_2.x
6 | local z = position_1.z - position_2.z
7 | return x*x + z*z
8 | end
9 |
10 | local function get_level_id(se_obj)
11 | ---@diagnostic disable-next-line: undefined-field
12 | return game_graph():vertex(se_obj.m_game_vertex_id):level_id()
13 | end
14 |
15 | function is_complete(obj_data)
16 | local target = assert(alife_object(obj_data.id))
17 | local actor = assert(alife_object(0))
18 | if get_level_id(target) ~= get_level_id(actor) then
19 | return false end
20 | return distance(target.position, actor.position) < MAX_DISTANCE_SQR
21 | end
22 |
23 | Visit = {}
24 | function Visit.on_init(entity)
25 | trace_assert(type(entity.id) == "number", "Visit: entity.id is not a number", entity)
26 | trace_assert(alife_object(entity.id), "Visit: no server object for this id", entity)
27 | end
28 |
29 | function Visit.status(subtask)
30 | if is_complete(subtask) then return igi_subtask.TASK_STATUSES.COMPLETED end
31 | return igi_subtask.TASK_STATUSES.RUNNING
32 | end
33 |
34 | function Visit.quest_target(entity)
35 | return entity.id
36 | end
37 |
38 | function Visit.complexity(entity)
39 | return 500
40 | end
41 |
42 | function Visit.test(entity)
43 | local assert_test = igi_tests.assert_test
44 | entity._test_stage = (entity._test_stage or 0) + 1
45 |
46 | if entity._test_stage == 1 then
47 | local se_obj = alife_object(entity.id)
48 | assert_test(se_obj, "Entity does not exist")
49 | db.actor:set_actor_position(se_obj.position)
50 |
51 | elseif entity._test_stage == 2 then
52 | assert_test(entity.status == "COMPLETED", "Quest did not complete")
53 | return true
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_task_manager.script:
--------------------------------------------------------------------------------
1 | local function notnil(v)
2 | if (v == nil) then
3 | igi_generic_task.NIL_ERROR = true
4 | end
5 | igi_helper.trace_assert(v ~= nil)
6 | return v
7 | end
8 |
9 | local trace_dbg = notnil(igi_helper.trace_dbg)
10 | local trace_error = notnil(igi_helper.trace_error)
11 | local pcall = notnil(igi_helper.pcall)
12 |
13 | local Igi = {
14 | is_subset = notnil(igi_utils.Set.is_subset),
15 | get_task_text = notnil(igi_description.get_task_text),
16 | get_all_quests = notnil(igi_taskdata.get_all_quests),
17 | get_cache = notnil(igi_generic_task.get_cache),
18 | get_setup = notnil(igi_generic_task.get_setup_cache),
19 | get_task_cache = notnil(igi_taskdata.get_task_cache),
20 | TASK_STATUSES = notnil(igi_subtask.TASK_STATUSES),
21 | setup_quest = notnil(igi_generic_task.setup_quest),
22 | initialise_CACHE = notnil(igi_generic_task.initialise_CACHE),
23 | try_prepare_quest = notnil(igi_generic_task.try_prepare_quest),
24 | quest_status = notnil(igi_generic_task.quest_status),
25 | quest_text = notnil(igi_generic_task.quest_text),
26 | quest_target = notnil(igi_generic_task.quest_target),
27 | finish_quest = notnil(igi_generic_task.finish_quest),
28 | first_finished_igi_task = notnil(igi_generic_task.first_finished_igi_task),
29 | }
30 |
31 | function on_game_start()
32 | RegisterScriptCallback("load_state", load_state)
33 | RegisterScriptCallback("save_state", save_state)
34 | end
35 |
36 | NPC_TAGS = {}
37 | function get_npc_tags()
38 | if next(NPC_TAGS) then return NPC_TAGS end
39 | local section = igi_helper.db_ini:collect_section("npc_tags")
40 | for k, v in pairs(section) do
41 | section[k] = igi_utils.Set.from_list(parse_names(v))
42 | end
43 | NPC_TAGS = section
44 | return section
45 | end
46 |
47 | function pcall(f, ...)
48 | if type(f) ~= "function" then
49 | callstack()
50 | return false, "Not a function"
51 | end
52 | local xf = coroutine.create(f)
53 | local ok, res = coroutine.resume(xf, ...)
54 | if not ok then
55 | return ok, debug.traceback(xf, res .. "\\n")
56 | end
57 | return ok, res
58 | end
59 |
60 | REPEAT_TIMEOUT = 16200
61 | FINISHED_QUESTS = {}
62 | PRIOR_COUNTER = 0
63 |
64 | NPC_QUESTS = {}
65 | function get_all_quests_for_npc(npc)
66 | local npc_name = npc:section() ~= "m_trader" and npc:section() or npc:name()
67 | if not NPC_QUESTS[npc_name] then
68 | local quest_list = {}
69 | local ok, all_quests = pcall(Igi.get_all_quests)
70 | for k, quests in pairs(ok and all_quests or {}) do
71 | for quest_name, quest_table in pairs(quests) do
72 | if npc_has_quest(npc_name, quest_table) then
73 | quest_list[#quest_list+1] = {k, quest_name}
74 | end
75 | end
76 | end
77 | NPC_QUESTS[npc_name] = quest_list
78 | end
79 | trace_dbg("Npc quests: ", npc_name, NPC_QUESTS[npc_name])
80 | return NPC_QUESTS[npc_name]
81 | end
82 |
83 | function is_valid_quest(tg_id, task_data, task_id)
84 | if Igi.get_cache(task_id) then
85 | return false -- currently active
86 | end
87 |
88 | local last_finish = FINISHED_QUESTS[task_id]
89 | local timeout = task_data.repeat_timeout or REPEAT_TIMEOUT
90 | if last_finish and game.get_game_time():diffSec(last_finish) <= timeout then
91 | return false -- in timeout
92 | end
93 | FINISHED_QUESTS[task_id] = nil
94 |
95 | local ok, out = pcall(Igi.try_prepare_quest, task_id, task_data, tg_id)
96 | if not ok then
97 | on_task_crashed(out)
98 | end
99 | return ok and (out ~= nil)
100 | end
101 |
102 | function get_valid_quests_for_npc(npc)
103 | local tg_id = npc:id()
104 | local quest_list = {}
105 | for _, quest_id in pairs(get_all_quests_for_npc(npc)) do
106 | quest_list[#quest_list+1] = validate_task(quest_id, tg_id)
107 | end
108 | return quest_list
109 | end
110 |
111 | function validate_task(quest_id, tg_id)
112 | local task_id = quest_id[1] .. quest_id[2] .. tg_id
113 | local _ = pcall(igi_random.set_seed, task_id)
114 | local ok, task_data = pcall(Igi.get_task_cache, quest_id, task_id, tg_id)
115 | if not ok then
116 | igi_helper.trace_error("Task validation failed!", task_data)
117 | end
118 | if ok and is_valid_quest(tg_id, task_data, task_id) then
119 | return task_id
120 | end
121 | end
122 |
123 | old_generate_available_tasks = axr_task_manager.generate_available_tasks
124 | function axr_task_manager.generate_available_tasks(npc,is_sim)
125 | old_generate_available_tasks(npc, is_sim)
126 | inject_tasks(npc)
127 | end
128 |
129 |
130 | function npc_has_quest(npc_name, quest)
131 | if not get_npc_tags()[npc_name] then return false end
132 | local npc_tags = get_npc_tags()[npc_name]
133 | for _, quest_tags in pairs(quest.quest_givers or {}) do
134 | if Igi.is_subset(quest_tags, npc_tags) then
135 | return true
136 | end
137 | end
138 | return false
139 | end
140 |
141 | function inject_tasks(npc)
142 | trace_dbg("Injecting tasks! Before: ", axr_task_manager.available_tasks[npc:id()])
143 | local available_tasks = axr_task_manager.available_tasks[npc:id()]
144 | for _, quest_id in pairs(get_valid_quests_for_npc(npc)) do
145 | available_tasks[#available_tasks+1] = quest_id
146 | end
147 | trace_dbg("Injecting tasks! After: ", axr_task_manager.available_tasks[npc:id()])
148 | end
149 |
150 | old_get_first_finished_task = axr_task_manager.get_first_finished_task
151 | function axr_task_manager.get_first_finished_task(npc,is_sim)
152 | local task_id = old_get_first_finished_task(npc, is_sim)
153 | if not task_id then
154 | task_id = Igi.first_finished_igi_task(npc:id())
155 | end
156 | return task_id
157 | end
158 |
159 | old_drx_sl_text_mechanic_has_ordered_task_to_give = dialogs.drx_sl_text_mechanic_has_ordered_task_to_give
160 | function dialogs.drx_sl_text_mechanic_has_ordered_task_to_give( a, b )
161 | local npc = dialogs.who_is_npc(a, b)
162 | local igi_tasks = get_valid_quests_for_npc(npc)
163 |
164 | -- randomly sets task_id to nil when vanilla task exists to avoid only giving igi tasks
165 | local vanilla_task_chance = axr_task_manager.drx_sl_get_mechanic_task(npc) and 1 or 0
166 | local task_id = igi_tasks[math.random(#igi_tasks + vanilla_task_chance)]
167 |
168 | if task_id then
169 | local CACHE = Igi.get_setup(task_id)
170 | dialogs.last_task_id = task_id
171 | local ok = pcall(Igi.setup_quest, task_id)
172 | if ok then
173 | local ok, text = pcall(Igi.get_task_text, CACHE.description_key, "about", CACHE.task_giver_id)
174 | if ok then
175 | return text
176 | else
177 | on_task_crashed(text, task_id)
178 | end
179 | end
180 | end
181 |
182 | return old_drx_sl_text_mechanic_has_ordered_task_to_give(a, b)
183 | end
184 |
185 | IgiTask = {
186 | stage = 0,
187 | title = "TITLE_DOESNT_EXIST",
188 | descr = "DESCR_DOESNT_EXIST",
189 | icon = "ui_pda2_mtask_overlay",
190 | prior = 2000,
191 | update_delay = 1000,
192 | last_update_time = 0,
193 | condlist = {},
194 | status = "normal",
195 | spot = "secondary_task_location",
196 | dont_send_update_news = false,
197 |
198 | new = function (id)
199 | local task_table = Igi.get_setup(id)
200 | local t = {
201 | type = "igi",
202 | id = id,
203 | icon = task_table.icon,
204 | storyline = task_table.storyline,
205 | update_delay = task_table.update_delay,
206 | task_giver_id = task_table.task_giver_id,
207 | temp_tasks = {},
208 | all_targets = {},
209 | }
210 | if task_table.storyline then
211 | t.spot = "storyline_task_location"
212 | end
213 | return setmetatable(t, IgiTask._mt)
214 | end,
215 |
216 | get_title = function (self)
217 | return self.title
218 | end,
219 |
220 | get_icon_name = function (self)
221 | return self.icon
222 | end,
223 |
224 | give_task = function(self)
225 | local ok, err = pcall(function(id)
226 | Igi.initialise_CACHE(id)
227 | Igi.quest_status(id) -- run once to setup current target
228 | self.current_title = Igi.quest_text(id, "name")
229 | self.current_descr = Igi.quest_text(id, "text")
230 | self.current_target = Igi.quest_target(id)[1]
231 | end, self.id)
232 | if not ok then
233 | on_task_crashed(err)
234 | return
235 | end
236 | self.status = "selected"
237 |
238 | local t = CGameTask()
239 | t:set_id(self.id)
240 | ---@diagnostic disable-next-line: undefined-field
241 | t:set_type(self.storyline and task.storyline or task.additional)
242 | t:set_title(self.current_title)
243 | t:set_description(self.current_descr)
244 | t:set_priority(get_prior(self.prior, 0))
245 | t:set_icon_name(self.icon)
246 | t:add_complete_func("task_manager.task_complete")
247 | t:add_fail_func("task_manager.task_fail")
248 |
249 | if self.current_target ~= nil then
250 | t:set_map_location(self.spot)
251 | t:set_map_object_id(self.current_target)
252 | end
253 | self.t = t
254 | db.actor:give_task(t, 0, false, 0)
255 | end,
256 |
257 | check_task = function (self)
258 | -- Timer for less pressure
259 | local tg = time_global()
260 | if (tg < self.last_update_time) then
261 | return
262 | end
263 | self.last_update_time = tg + self.update_delay
264 |
265 | self.t = self.t or db.actor:get_task(self.id,true)
266 | if (self.t == nil) then -- task is most likely in timeout
267 | return
268 | end
269 |
270 | local task_updated = false
271 |
272 | local ok, t_title = pcall(Igi.quest_text, self.id, "name")
273 | if not ok then
274 | on_task_crashed(t_title, self.id)
275 | return
276 | end
277 | if self.current_title ~= t_title then
278 | task_updated = true
279 | self.current_title = t_title
280 | self.t:set_title(t_title)
281 | end
282 |
283 | local ok, t_descr = pcall(Igi.quest_text, self.id, "text")
284 | if not ok then
285 | on_task_crashed(t_descr, self.id)
286 | return
287 | end
288 | if self.current_descr ~= t_descr then
289 | task_updated = true
290 | self.current_descr = t_descr
291 | self.t:set_description(t_descr)
292 | end
293 |
294 | local ok, t_targets = pcall(Igi.quest_target, self.id)
295 | if not ok then
296 | on_task_crashed(t_targets, self.id)
297 | return
298 | end
299 | if (self.current_target ~= t_targets[1]) then
300 | task_updated = true
301 | self.current_target = t_targets[1]
302 | if not self.current_target then
303 | self.t:remove_map_locations(false)
304 | else
305 | self.t:change_map_location(self.spot, self.current_target)
306 | end
307 | end
308 |
309 | if self:update_temp_tasks(t_targets) then
310 | task_updated = true
311 | end
312 |
313 | if task_updated then
314 | self:sync_temp_tasks()
315 | end
316 |
317 | if task_updated and not self.dont_send_update_news then
318 | news_manager.send_task(db.actor, "updated", self.t)
319 | end
320 |
321 | -- status functor
322 | local ok, t = pcall(Igi.quest_status, self.id)
323 | if not ok then
324 | on_task_crashed(t, self.id)
325 | return
326 | end
327 | if t == "complete" or t == "fail" or t == "reversed" then
328 | self.last_check_task = t
329 | end
330 | end,
331 |
332 | sync_temp_tasks = function (self)
333 | for _, task_id in pairs(self.temp_tasks) do
334 | local tsk = task_manager.get_task_manager().task_info[task_id]
335 | if not tsk then
336 | on_task_crashed("Can't find temporary task object", self.id)
337 | return
338 | end
339 |
340 | TmpTask.update(tsk, self)
341 | end
342 | end,
343 |
344 | update_temp_tasks = function (self, new_targets)
345 | local changed = false
346 | self.all_targets = self.all_targets or {}
347 | self.temp_tasks = self.temp_tasks or {}
348 | if #new_targets ~= #self.all_targets then
349 | changed = true
350 | end
351 | for k, id in pairs(self.all_targets) do
352 | if new_targets[k] ~= id then
353 | changed = true
354 | end
355 | end
356 |
357 | if changed then
358 | for id, task_id in pairs(self.temp_tasks) do
359 | if new_targets[1] == id or not igi_utils.table_find(new_targets, id) then
360 | task_manager.get_task_manager():set_task_cancelled(task_id)
361 | self.temp_tasks[id] = nil
362 | end
363 | end
364 |
365 | for k, id in pairs(new_targets) do
366 | if k ~= 1 and not self.temp_tasks[id] then
367 | local prior = get_prior(self.prior, k)
368 | self.temp_tasks[id] = give_temp_task(self.id, id, prior)
369 | end
370 | end
371 | end
372 |
373 | self.all_targets = new_targets
374 | return changed
375 | end,
376 |
377 | deactivate_task = function (self, tsk)
378 | self.check_time = nil
379 | self.last_check_task = nil
380 | self.status = "normal"
381 |
382 | if tmrs_tasks then -- xcvb's task timers
383 | tmrs_tasks.active_tasks[self.id] = nil
384 | end
385 |
386 | if self.last_check_task == "fail" then
387 | local ok, err = pcall(Igi.finish_quest, self.id)
388 | if not ok then
389 | on_task_crashed(err)
390 | end
391 | news_manager.send_task(db.actor, "fail", tsk)
392 | end
393 | end,
394 |
395 | save_state = function (self)
396 | --utils_data.debug_write(strformat("CGeneralTask:save_state %s BEFORE",self.id))
397 | if (self.t == nil) then
398 | if (self.repeat_timeout == nil or self.timeout == nil) then
399 | return
400 | end
401 | if (game.get_game_time():diffSec(self.timeout) > self.repeat_timeout) then
402 | return
403 | end
404 | end
405 |
406 | local t = dup_table(self)
407 | t.t = nil
408 | t.last_update_time = nil
409 | return t
410 | end,
411 |
412 | give_reward = function(self)
413 | local ok, err = pcall(Igi.finish_quest, self.id)
414 | if not ok then
415 | on_task_crashed(err)
416 | end
417 | FINISHED_QUESTS[self.id] = game.get_game_time()
418 | if tmrs_tasks then -- xcvb's task timers
419 | tmrs_tasks.active_tasks[self.id] = nil
420 | end
421 | end,
422 |
423 | save = function () end,
424 | load = function () end,
425 | }
426 | IgiTask._mt = {__index = IgiTask}
427 |
428 | TEMP_TASKS = {}
429 | TmpTask = {
430 | stage = 0,
431 | title = "TITLE_DOESNT_EXIST",
432 | descr = "DESCR_DOESNT_EXIST",
433 | icon = "ui_pda2_mtask_overlay",
434 | prior = 2000,
435 | update_delay = 1000,
436 | last_update_time = 0,
437 | condlist = {},
438 | status = "normal",
439 | spot = "secondary_task_location",
440 | dont_send_update_news = true,
441 |
442 | new = function (id, target_id, prior)
443 | local t = {
444 | type = "wtf_tmp",
445 | id = id,
446 | task_giver_id = 0,
447 | current_target = target_id,
448 | prior = prior
449 | }
450 | return setmetatable(t, {__index = TmpTask})
451 | end,
452 |
453 | get_title = function (self)
454 | return self.title
455 | end,
456 |
457 | get_icon_name = function (self)
458 | return self.icon
459 | end,
460 |
461 | give_task = function(self)
462 | local t = CGameTask()
463 | t:set_id(self.id)
464 | ---@diagnostic disable-next-line: undefined-field
465 | t:set_type(self.storyline and task.storyline or task.additional)
466 | t:set_title(self.current_title)
467 | t:set_description(self.current_descr)
468 | t:set_priority(self.prior)
469 | t:set_icon_name(self.icon)
470 | t:add_complete_func("task_manager.task_complete")
471 | t:add_fail_func("task_manager.task_fail")
472 |
473 | if self.current_target ~= nil then
474 | t:set_map_location(self.spot)
475 | t:set_map_object_id(self.current_target)
476 | end
477 | db.actor:give_task(t, 0, false, 0)
478 | end,
479 |
480 | update = function (self, tsk)
481 | local t = db.actor:get_task(self.id,true)
482 | if not t then
483 | on_task_crashed("Temporary task has no task object", self.id)
484 | return
485 | end
486 | t:set_type(tsk.storyline and task.storyline or task.additional)
487 | t:set_title('\149 ' .. tsk.current_title)
488 | t:set_description(tsk.current_descr)
489 | t:set_priority(tsk.prior)
490 | t:set_icon_name(tsk.icon)
491 | end,
492 |
493 | check_task = function() end,
494 | deactivate_task = function() end,
495 | save_state = function() end,
496 | give_reward = function() end,
497 | save = function () end,
498 | load = function () end,
499 | }
500 |
501 | function task_manager.save_state(m_data)
502 | m_data.task_info = {}
503 | m_data.task_objects = {}
504 | m_data.igi_task_objects = {}
505 | m_data.wtf_temp_task_objects = {}
506 |
507 | local tm = task_manager.get_task_manager()
508 |
509 | for k,v in pairs(tm.task_info) do
510 | if (v.type == "igi") then
511 | m_data.igi_task_objects[k] = v:save_state(m_data)
512 | elseif v.type == "wtf_tmp" then
513 | m_data.wtf_temp_task_objects[k] = v
514 | else
515 | m_data.task_info[k] = true
516 | tm.task_info[k]:save_state(m_data)
517 | end
518 | end
519 | end
520 |
521 | function task_manager.load_state(m_data)
522 | local tm = task_manager.get_task_manager()
523 |
524 | for task_id in pairs(m_data.task_info or {}) do
525 | local obj = task_objects.CGeneralTask(task_id)
526 | if (obj:load_state(m_data) == true) then
527 | tm.task_info[task_id] = obj
528 | end
529 | end
530 |
531 | for task_id, tbl in pairs(m_data.igi_task_objects or {}) do
532 | tm.task_info[task_id] = setmetatable(tbl, IgiTask._mt)
533 | end
534 |
535 | for task_id, tbl in pairs(m_data.wtf_temp_task_objects or {}) do
536 | tm.task_info[task_id] = setmetatable(tbl, {__index = TmpTask})
537 | end
538 |
539 | m_data.task_info = nil
540 | m_data.igi_task_objects = nil
541 | m_data.wtf_temp_task_objects = nil
542 | end
543 |
544 | function give_temp_task(task_id, target_id, prior)
545 | local tmp_task_id = "__wtf_temp-" .. task_id .. "-" .. tostring(target_id)
546 | local task = TmpTask.new(tmp_task_id, target_id, prior)
547 | task_manager.get_task_manager().task_info[tmp_task_id] = task
548 | task:give_task()
549 | return tmp_task_id
550 | end
551 |
552 | old_give_task = task_manager.CRandomTask.give_task
553 | function task_manager.CRandomTask.give_task(self, task_id,task_giver_id)
554 | if Igi.get_setup(task_id) then
555 | local task = IgiTask.new(task_id)
556 | self.task_info[task_id] = task
557 | task:give_task()
558 | else
559 | old_give_task(self, task_id, task_giver_id)
560 | end
561 | end
562 |
563 | old_set_task_cancelled = task_manager.CRandomTask.set_task_cancelled
564 | function task_manager.CRandomTask.set_task_cancelled(self, task_id,task_giver_id)
565 | old_set_task_cancelled(self, task_id, task_giver_id)
566 | local CACHE = Igi.get_cache(task_id)
567 | if CACHE then
568 | CACHE.status = igi_subtask.TASK_STATUSES.FAILED
569 | Igi.finish_quest(task_id)
570 | end
571 | end
572 |
573 | old_get_task_complete_text = axr_task_manager.get_task_complete_text
574 | function axr_task_manager.get_task_complete_text(task_id)
575 | local CACHE = Igi.get_cache(task_id)
576 | if CACHE then
577 | local ok, text = pcall(Igi.get_task_text, CACHE.description_key, "finish", CACHE.task_giver_id)
578 | return ok and text or ""
579 | end
580 |
581 | return old_get_task_complete_text(task_id)
582 | end
583 |
584 | old_get_task_job_description = axr_task_manager.get_task_job_description
585 | function axr_task_manager.get_task_job_description( task_id )
586 | local CACHE = Igi.get_cache(task_id) or Igi.get_setup(task_id)
587 | if CACHE then
588 | local ok, text = pcall(Igi.get_task_text, CACHE.description_key, "about", CACHE.task_giver_id)
589 | return ok and text or ""
590 | end
591 |
592 | return old_get_task_job_description(task_id)
593 | end
594 |
595 | old_text_npc_has_task = dialogs.text_npc_has_task
596 | function dialogs.text_npc_has_task(a,b)
597 | local npc = dialogs.who_is_npc(a, b)
598 | local task_id = axr_task_manager.available_tasks[npc:id()] and axr_task_manager.available_tasks[npc:id()][1]
599 | local CACHE = Igi.get_setup(task_id)
600 |
601 | if CACHE then
602 | local ok, text = pcall(function()
603 | Igi.setup_quest(task_id)
604 | return Igi.get_task_text(CACHE.description_key, "about", CACHE.task_giver_id)
605 | end)
606 | if not ok then
607 | on_task_crashed(text)
608 | end
609 | return ok and text or "Uhh... Something went wrong. Sorry :) - Igi"
610 | end
611 |
612 | return old_text_npc_has_task(a, b)
613 | end
614 |
615 | function save_state(m_data)
616 | m_data.igi_finished_quests = FINISHED_QUESTS
617 | m_data.wtf_prior_counter = PRIOR_COUNTER
618 | end
619 |
620 | function load_state(m_data)
621 | FINISHED_QUESTS = m_data.igi_finished_quests or FINISHED_QUESTS
622 | PRIOR_COUNTER = m_data.wtf_prior_counter or 0
623 | end
624 |
625 | crash_count = 0
626 | function on_task_crashed(err, task_id)
627 | if task_id then
628 | task_manager.get_task_manager():set_task_failed(task_id)
629 | end
630 | trace_error("Task crashed: ", err, callstack(false, true))
631 | trace_error("Task " .. (task_id and task_id .. " " or "")
632 | .. "crashed! Check PDA Messages or logs for full stack trace. Error:\\n"
633 | .. (string.match(err, "^(.-)\nstack traceback:.*$") or '??'))
634 | crash_count = crash_count + 1
635 |
636 | if (crash_count == 1 and igi_mcm.get_options_value("wtf_crash_message")) then
637 | CreateTimeEvent("igi_send_crash_msg", 0, 0, function()
638 | news_manager.send_tip(db.actor,
639 | "[" ..
640 | tostring(crash_count) ..
641 | "x] " ..
642 | "Weird Tasks Framework crashed! Sorry :(. I've taken care of it, you can continue your playthrough.", nil,
643 | nil, 30000)
644 | crash_count = 0
645 | flush()
646 | return true
647 | end)
648 | end
649 | end
650 |
651 | actor_on_task_callback_before = bind_stalker_ext.actor_on_task_callback
652 | function bind_stalker_ext.actor_on_task_callback(binder, _task, _state)
653 | local id = _task:get_id()
654 |
655 | local rtask = id and task_manager.get_task_manager().task_info[id]
656 | if rtask and rtask.type == "wtf_tmp" then
657 | return
658 | end
659 | actor_on_task_callback_before(binder, _task, _state)
660 | end
661 |
662 | give_task_before = task_objects.CGeneralTask.give_task
663 | function task_objects.CGeneralTask.give_task(self)
664 | self.prior = get_prior(self.prior, 0)
665 | give_task_before(self)
666 | end
667 |
668 | function get_prior(prior, count)
669 | PRIOR_COUNTER = (PRIOR_COUNTER + 1) % 1024
670 | local ending = 63 - (count % 64)
671 | return (prior * 1024 * 64) + (PRIOR_COUNTER * 64) + ending
672 | end
673 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_taskdata.script:
--------------------------------------------------------------------------------
1 | local trace_dbg = igi_helper.trace_dbg
2 | local trace_assert = igi_helper.trace_assert
3 |
4 | -------------------------------------------------
5 | -- JSON functions
6 | -------------------------------------------------
7 | local json = igi_json.get_json()
8 |
9 | local function get_game_path()
10 | local info = debug.getinfo(1,'S');
11 | local script_path = info.source:match[[^@?(.*[\/])[^\/]-$]]
12 | local game_path = script_path:match("(.*)gamedata"):gsub("/", "\\")
13 | return game_path
14 | end
15 |
16 | local default_tasks_path = get_game_path().."gamedata\\configs\\igi_tasks\\tasks\\"
17 |
18 | local function get_file_content(path)
19 | file = io.open(path, "rb")
20 | if not file then return end
21 | local content = file:read "*a" -- *a or *all reads the whole file
22 | file:close()
23 | return content
24 | end
25 |
26 | function get_task_table(task_name, prefix)
27 | local task_path = prefix .. "\\" .. task_name .. ".json"
28 | local content = get_file_content(default_tasks_path .. task_path)
29 | if not content then return {} end
30 | local task_table = json.decode(content)
31 |
32 | task_table.description_key = task_table.description_key
33 | or (igi_description.TEXT_HEADER..prefix.."_"..task_name)
34 | task_table.quest_id = {prefix, task_name}
35 | return task_table
36 | end
37 |
38 | local IGI_QUESTS = {}
39 | function get_all_quests()
40 | if not next(IGI_QUESTS) then
41 | local packs = getFS():file_list_open(
42 | "$game_config$", "igi_tasks\\tasks\\", 2 + 4 + 8)
43 | for i = 0, packs:Size() - 1 do
44 | local prefix = string.gsub(packs:GetAt(i), "\\", "")
45 | IGI_QUESTS[prefix] = {}
46 |
47 | local files = getFS():file_list_open(
48 | "$game_config$", "igi_tasks\\tasks\\"..prefix.."\\", 1 + 8)
49 |
50 | for j=0, files:Size()-1 do
51 | local quest_name = string.match(files:GetAt(j), "(.*)%.json$")
52 | if quest_name then
53 | local ok, task_table = pcall(get_task_table, quest_name, prefix)
54 | IGI_QUESTS[prefix][quest_name] = ok and task_table or nil
55 | end
56 | end
57 | end
58 |
59 | IGI_QUESTS["hf"] = nil -- delete outdated HF WTF patch
60 | end
61 | return IGI_QUESTS
62 | end
63 |
64 | function supported_wtf_versions()
65 | return {
66 | ["4.0"] = true,
67 | ["4.1"] = true,
68 | ["4.2"] = true,
69 | }
70 | end
71 |
72 | function is_supported_version(task_tbl)
73 | return supported_wtf_versions()[task_tbl.WTF_VERSION]
74 | end
75 |
76 | function supported_versions_list()
77 | local list = {}
78 | for k in pairs(supported_wtf_versions()) do
79 | list[#list+1] = k
80 | end
81 | table.sort(list)
82 | return list
83 | end
84 |
85 | local function get_task_data(quest_id)
86 | local prefix, task_name = igi_helper.get_task_name(quest_id)
87 | local task_tbl = get_task_table(task_name, prefix)
88 | trace_assert(task_tbl, "No such task: " .. tostring(quest_id or "nil"))
89 | if not is_supported_version(task_tbl) then
90 | igi_helper.trace_error("[" .. prefix .. ":" .. task_name ..
91 | '] - Unsupported WTF_VERSION (' .. tostring(task_tbl.WTF_VERSION)
92 | .. ')! Supported are: \\n"' .. table.concat(supported_versions_list(), '", "') .. '"')
93 | assert(nil)
94 | end
95 |
96 | igi_callbacks.invoke_callbacks("on_get_taskdata", task_tbl, quest_id)
97 | return task_tbl
98 | end
99 |
100 | -------------------------------------------------
101 | -- global functions
102 | -------------------------------------------------
103 | function get_task_cache(quest_id, task_id, tg_id)
104 | return finalize_task_cache(get_task_data(quest_id), task_id, tg_id)
105 | end
106 |
107 | function finalize_task_cache(task_data, task_id, tg_id)
108 | local CACHE = dup_table(task_data)
109 | CACHE.quest_givers = nil -- not needed
110 | CACHE.task_id = task_id
111 | CACHE.task_giver_id = tg_id
112 |
113 | trace_dbg("finalized CACHE", CACHE)
114 | return CACHE
115 | end
116 |
117 | local NO_CONTROLLER = {}
118 | function get_controller(entity, CACHE)
119 | if not entity.CONTROLLER then return NO_CONTROLLER end
120 | trace_assert(CACHE, "get_controller: no cache")
121 | local link_context = igi_text_processor.get_link_context(CACHE, entity)
122 | local controller = igi_text_processor.eval_logic_macro(entity.CONTROLLER, link_context)
123 | trace_assert(type(controller) == "table", "Controller is not a table", entity.CONTROLLER)
124 | return controller
125 | end
126 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_tests.script:
--------------------------------------------------------------------------------
1 | local trace_assert = igi_helper.trace_assert
2 | local trace_dbg = igi_helper.trace_dbg
3 | local pcall = igi_helper.pcall
4 |
5 | function on_game_start()
6 | RegisterScriptCallback("save_state", save_state)
7 | RegisterScriptCallback("load_state", load_state)
8 | RegisterScriptCallback("actor_on_first_update", actor_on_first_update)
9 | end
10 |
11 | RESULT = {
12 | SUCCESS = "SUCCESS",
13 | FAIL = "FAIL",
14 | DISABLED = "DISABLED"
15 | }
16 |
17 | STATE = {
18 | current_tests_name = nil,
19 | current_test = nil,
20 | last_test = nil,
21 | current_test_state = nil,
22 | tests_history = nil,
23 | }
24 |
25 | TESTS = {}
26 |
27 | function save_state(m_data)
28 | m_data.igi_tests_state = STATE
29 | end
30 |
31 | function load_state(m_data)
32 | STATE = m_data.igi_tests_state or STATE
33 | end
34 |
35 | function actor_on_first_update()
36 | if not STATE.current_tests_name then return end
37 | if STATE.current_tests_name == "normal" then
38 | CreateTimeEvent("igi_tests", "tests", 2, normal_tests_worker)
39 | CreateTimeEvent("igi_tests", "current_test", 2, continue_test_worker)
40 | else
41 | CreateTimeEvent("igi_tests", "tests", 2, integration_tests_worker)
42 | CreateTimeEvent("igi_tests", "current_test", 2, continue_test_worker)
43 | end
44 | end
45 |
46 | function prepare_mock_quest(cache, id)
47 | local tg_id = get_story_se_object("bar_visitors_barman_stalker_trader").id
48 | return igi_generic_task.try_prepare_quest(id, cache, tg_id)
49 | end
50 |
51 | function register_test(id, f, state)
52 | TESTS[id] = {f, state}
53 | end
54 |
55 | local flatenned_quests = {}
56 | function flat_quests()
57 | if not next(flatenned_quests) then
58 | for prefix, quests in pairs(igi_taskdata.get_all_quests()) do
59 | for quest_name, cache in pairs(quests) do
60 | flatenned_quests[prefix..":"..quest_name] = cache
61 | end
62 | end
63 | end
64 | return flatenned_quests
65 | end
66 |
67 | local function report_tests_history()
68 | local report = {}
69 | local count_successful = 0
70 | local count_disabled = 0
71 | for test_case, result in pairs(STATE.tests_history) do
72 | if result == RESULT.SUCCESS then
73 | count_successful = count_successful + 1
74 | elseif result == RESULT.DISABLED then
75 | count_disabled = count_disabled + 1
76 | else
77 | report[#report+1] = test_case..": %c[d_red]fail%c[0,255,255,255]"
78 | end
79 | end
80 |
81 | news_manager.send_tip(db.actor,
82 | "Tests finished. Report:\\n"
83 | .."["..count_disabled.."]: %c[0,155,155,155]disabled%c[0,255,255,255]\\n \\n"
84 | .."["..count_successful.."]: %c[d_green]success%c[0,255,255,255]\\n \\n"
85 | ..table.concat(report, "\\n \\n"), nil, nil, 30000)
86 | trace_dbg("Test report:", STATE.tests_history)
87 | end
88 |
89 | function start_integration_tests()
90 | xrs_debug_tools.debug_invis = true
91 | STATE.current_tests_name = "quest"
92 | STATE.tests_history = {}
93 | CreateTimeEvent("igi_tests", "tests", 2, integration_tests_worker)
94 | end
95 |
96 | function integration_tests_worker()
97 | if STATE.current_test then return end
98 | local caches = flat_quests()
99 | local id, next_cache = next(caches, STATE.last_test)
100 | STATE.last_test = id
101 | if id then
102 | start_integration_test(id, next_cache)
103 | return
104 | end
105 |
106 | STATE.current_tests_name = nil
107 | report_tests_history()
108 | return true
109 | end
110 |
111 | function start_integration_test(id, cache)
112 | trace_assert(not STATE.current_test, "Already running a test!")
113 | trace_dbg("Running test: "..id, cache)
114 | STATE.current_test = id
115 |
116 | local ok, mock = pcall(prepare_mock_quest, cache, id)
117 |
118 | if ok and mock then
119 | task_manager.get_task_manager():give_task(id)
120 | CreateTimeEvent("igi_tests", "current_test", 0, continue_test_worker)
121 | elseif cache.quest_id and igi_mcm.get_task_value(cache.quest_id, "disabled") then
122 | STATE.tests_history[STATE.current_test] = RESULT.DISABLED
123 | finish_test()
124 | else
125 | assert_test(nil, "Can't prepare quest", ok, mock)
126 | end
127 | end
128 |
129 | local function choose_entity(CACHE)
130 | trace_dbg("choose entity", CACHE)
131 | local entity = CACHE.entities[CACHE._test_entity_id] or {}
132 | local controller = igi_taskdata.get_controller(entity, CACHE)
133 | while entity.status ~= igi_subtask.TASK_STATUSES.RUNNING
134 | or not controller.status do
135 | local k
136 | k, entity = next(CACHE.entities, CACHE._test_entity_id)
137 | controller = igi_taskdata.get_controller(entity or {}, CACHE)
138 | CACHE._test_entity_id = k
139 | if not k then
140 | trace_dbg("chosen: ", "nil")
141 | return nil
142 | end
143 | end
144 | trace_dbg("chosen: ", CACHE._test_entity_id)
145 | return entity
146 | end
147 |
148 | function continue_test_worker()
149 | local id = STATE.current_test
150 | trace_dbg("continuing test "..id, igi_generic_task.TASKS_CACHE[id])
151 | local CACHE = igi_generic_task.TASKS_CACHE[id]
152 | if not CACHE then
153 | finish_test()
154 | return true
155 | end
156 | local entity = choose_entity(CACHE)
157 | if not entity then
158 | assert_test(CACHE.status == "COMPLETED" or CACHE.status == "READY_TO_FINISH", "Quest was not completed")
159 | finish_test()
160 | return true
161 | end
162 |
163 | local controller = igi_taskdata.get_controller(entity, CACHE)
164 | if not controller.test then
165 | assert_test(controller.test, "Test is not implemented for controller " .. entity.CONTROLLER)
166 | finish_test()
167 | return true
168 | end
169 |
170 | local ok, finished = pcall(controller.test, entity)
171 | if not ok then
172 | assert_test(nil, finished)
173 | finish_test()
174 | return true
175 | end
176 |
177 | if finished then
178 | finish_test()
179 | return true
180 | end
181 | ResetTimeEvent("igi_tests", "current_test", 1)
182 | end
183 |
184 | function finish_test()
185 | if not STATE.current_test then return end
186 | trace_dbg("Test concluded! "..STATE.current_test)
187 | if (STATE.current_tests_name ~= "normal") then
188 | task_manager.get_task_manager():set_task_completed(STATE.current_test)
189 | end
190 |
191 | if not STATE.tests_history[STATE.current_test] then
192 | STATE.tests_history[STATE.current_test] = RESULT.SUCCESS
193 | end
194 |
195 | STATE.current_test = nil
196 | flush()
197 | end
198 |
199 | function assert_test(successful, reason, ...)
200 | if successful then return successful end
201 | actor_menu.set_fade_msg("WTF: test "..STATE.current_test.." failed: "..reason, 10, {G = 10, B = 10})
202 | trace_dbg("Test unsuccessful! ", STATE.current_test, reason, ...)
203 | STATE.tests_history[STATE.current_test] = RESULT.FAIL
204 | finish_test()
205 | end
206 |
207 | local function get_level_id(se_obj)
208 | ---@diagnostic disable-next-line: undefined-field
209 | return game_graph():vertex(se_obj.m_game_vertex_id):level_id()
210 | end
211 |
212 | function teleport_to_player(se_obj)
213 | assert(se_obj:section_name() ~= "smart_terrain", "Are you teleporting smarts?")
214 |
215 | if se_obj and se_obj.commander_id then
216 | TeleportSquad(se_obj, db.actor:position(), db.actor:level_vertex_id(), db.actor:game_vertex_id())
217 | else
218 | TeleportObject(se_obj.id, db.actor:position(), db.actor:level_vertex_id(), db.actor:game_vertex_id())
219 | end
220 | end
221 |
222 | function travel_to_se_obj(se_obj)
223 | ---@diagnostic disable-next-line: undefined-field
224 | if get_level_id(se_obj) == get_level_id(alife():object(0)) then
225 | db.actor:set_actor_position(se_obj.position)
226 | else
227 | assert(not igi_mcm.get_options_value("fast_tests"), "Don't change levels when fasts tests are enabled")
228 | ChangeLevel(se_obj.position, se_obj.m_level_vertex_id, se_obj.m_game_vertex_id, VEC_ZERO, false)
229 | end
230 | end
231 |
232 | function start_normal_tests()
233 | xrs_debug_tools.debug_invis = true
234 | STATE.current_tests_name = "normal"
235 | STATE.tests_history = {}
236 | CreateTimeEvent("igi_tests", "tests", 1, normal_tests_worker)
237 | end
238 |
239 | function normal_tests_worker()
240 | if STATE.current_test then return end
241 | local id, data = next(TESTS, STATE.last_test)
242 | STATE.last_test = id
243 | if not id then
244 | STATE.current_tests_name = nil
245 | report_tests_history()
246 | return true
247 | end
248 |
249 | trace_dbg("Running test: "..id, data)
250 | news_manager.send_tip(db.actor, "Test started: "..id, nil, nil, 2000)
251 | STATE.current_test = id
252 | STATE.current_test_state = dup_table(data[2])
253 | CreateTimeEvent("igi_tests", "current_test", 0, continue_normal_test_worker)
254 | end
255 |
256 | function continue_normal_test_worker()
257 | if not STATE.current_test then return true end
258 | local id = STATE.current_test
259 | trace_dbg("continuing test "..id, STATE.current_test_state)
260 | local ok, next_iteration = pcall(TESTS[id][1], STATE.current_test_state)
261 | ResetTimeEvent("igi_tests", "current_test", type(next_iteration) == "number" and next_iteration or 1)
262 |
263 | -- in case it failed current test is reset
264 | if STATE.current_test then
265 | assert_test(ok, "Test function crashed", next_iteration)
266 | end
267 | if not next_iteration and STATE.current_test then
268 | finish_test()
269 | return true
270 | end
271 | end
272 |
--------------------------------------------------------------------------------
/gamedata/scripts/igi_text_processor.script:
--------------------------------------------------------------------------------
1 | trace_assert = igi_helper.trace_assert
2 |
3 | local fCache = {}
4 | local function eval(v)
5 | local f = fCache[v]
6 | if f == nil then
7 | f = loadstring("return " .. v)
8 | fCache[v] = f
9 | end
10 | assert(f, "Function cannot be compiled: " .. (v or nil))
11 | return f()
12 | end
13 |
14 | LinkContext = {
15 | _NO_THIS = {},
16 |
17 | new = function(self, CACHE)
18 | local link_context = {
19 | CACHE = CACHE,
20 | this = self._NO_THIS,
21 | }
22 | for _, entity in pairs(CACHE.entities) do
23 | if entity.link_id then
24 | trace_assert(link_context[entity.link_id] == nil, "Entity already in context", link_context, entity)
25 | link_context[entity.link_id] = entity
26 | end
27 | end
28 | return setmetatable(link_context, { __index = self })
29 | end,
30 |
31 | set_this = function (self, entity)
32 | self.this = entity or LinkContext._NO_THIS
33 | return self
34 | end,
35 |
36 | get = function (self, link)
37 | return self:get_table(link)[link.field]
38 | end,
39 |
40 | get_table = function (self, link)
41 | local tbl = self[link.link_id]
42 | igi_helper.trace_assert(tbl, "Linker can't find tbl for link:", self)
43 | return tbl
44 | end,
45 | }
46 |
47 | Link = {
48 | new = function (link_id, field)
49 | local t = {
50 | link_id = link_id,
51 | field = field,
52 | }
53 |
54 | return setmetatable(t, {__index = Link})
55 | end,
56 |
57 | from_str = function (self, str)
58 | local link_id, field = str:match("%|([^%.]+)%.(.+)%|")
59 | trace_assert(link_id and field, "Link can't be resolved: " .. str)
60 | return Link.new(link_id, field)
61 | end,
62 |
63 | remap_this = function (self, mapping)
64 | if mapping and self.link_id == "this" then
65 | self.link_id = mapping
66 | end
67 | return self
68 | end
69 | }
70 |
71 | Macro = {
72 | link_context = {},
73 |
74 | new = function(self, str)
75 | if type(str) ~= "string" then return end
76 | local level, is_auto, macro = string.match(str, "([^%$%@]*)(%@?)%$(.*)")
77 | if not macro then return end
78 |
79 | local t = {
80 | level = level,
81 | is_auto = is_auto ~= '',
82 | macro = macro,
83 | orig = str
84 | }
85 |
86 | return setmetatable(t, { __index = self })
87 | end,
88 |
89 | _without_links = function (self)
90 | return string.gsub(self.macro, "%b||", function(dep)
91 | local link = Link:from_str(dep)
92 | igi_helper.trace_assert(LinkContext.get(Macro.link_context, link) ~= nil, "Value for a link can't be found!", dep, Macro.link_context)
93 | return "igi_text_processor.Macro.link_context." .. link.link_id .. '.' .. link.field
94 | end)
95 | end,
96 |
97 | resolve = function(self, link_context)
98 | igi_helper.trace_dbg("Resolving macro", self.orig)
99 |
100 | Macro.link_context = link_context
101 | local out = eval(self:_without_links())
102 |
103 | trace_assert(out ~= nil, "Macro returned nil", self.orig)
104 | trace_assert(self.is_auto or type(out) ~= "userdata", "Macro returned userdata", self.orig)
105 |
106 | return out
107 | end,
108 |
109 | assert_level = function (self, level)
110 | local is_empty_auto = self.is_auto and self.level == ""
111 | igi_helper.trace_assert(self.level == level or is_empty_auto, "Macro is of wrong level", level, self.orig)
112 | return self
113 | end,
114 |
115 | link_iterator = function (self)
116 | return string.gmatch(self.macro, "%b||")
117 | end
118 | }
119 |
120 | -- Careful if you want to cache it: generate step invalidates link context
121 | function get_link_context(CACHE, entity)
122 | return LinkContext:new(CACHE):set_this(entity)
123 | end
124 |
125 | function has_macro(str)
126 | return Macro:new(str) ~= nil
127 | end
128 |
129 | local function resolve_macros(tbl, level, link_context, transient_fields)
130 | for k, v in pairs(tbl) do
131 | if type(v) == 'table' then
132 | resolve_macros(v, level, link_context, transient_fields)
133 | elseif type(v) == 'string' then
134 | local macro = Macro:new(v)
135 | if (macro and macro.level == level and (not macro.is_auto)) then
136 | tbl[k] = _resolve_linked_macro(macro, level, link_context, transient_fields)
137 | end
138 | end
139 | end
140 | end
141 |
142 | local function collect_links(macro, link_context, buf, this_mapping)
143 | igi_helper.trace_assert(#buf < 10000, "Overflow while resolving macro (>10000 links). Circular dependency?", macro)
144 |
145 | for dep in macro:link_iterator() do
146 | local link = Link:from_str(dep):remap_this(this_mapping)
147 |
148 | buf[#buf + 1] = link
149 | local val = link_context:get(link)
150 |
151 | local macro = Macro:new(val)
152 | if macro then
153 | collect_links(macro, link_context, buf, link.link_id)
154 | end
155 | end
156 | return buf
157 | end
158 |
159 | local function resolve_all_links(links, level, link_context, transient_fields)
160 | for i = #links, 1, -1 do
161 | local link = links[i]
162 | local tbl = link_context:get_table(link)
163 | local before = tbl[link.field]
164 | igi_helper.trace_assert(before ~= nil, "Link is nil", link)
165 |
166 | local macro = Macro:new(before)
167 | if macro then
168 | macro:assert_level(level)
169 | local out = macro:resolve(link_context:set_this(tbl))
170 |
171 | if macro.is_auto then
172 | transient_fields[tbl] = transient_fields[tbl] or {}
173 | transient_fields[tbl][link.field] = before
174 | end
175 |
176 | tbl[link.field] = out
177 | end
178 | end
179 | end
180 |
181 | function _resolve_linked_macro(macro, level, link_context, transient_fields)
182 | local links = collect_links(macro, link_context, {})
183 | resolve_all_links(links, level, link_context, transient_fields)
184 | return macro:resolve(link_context)
185 | end
186 |
187 | local function reset_transient_fields(transient_fields)
188 | for tbl, fields in pairs(transient_fields) do
189 | for k, v in pairs(fields) do
190 | tbl[k] = v
191 | end
192 | end
193 | end
194 |
195 | function resolve_table_macros(tbl, level, link_context)
196 | local transient_fields = {}
197 | resolve_macros(tbl, level, link_context, transient_fields)
198 | reset_transient_fields(transient_fields)
199 | end
200 |
201 | function resolve_and_link_cache(CACHE, level)
202 | local link_context = igi_text_processor.get_link_context(CACHE)
203 | local transient_fields = {}
204 |
205 | for _, entity in pairs(CACHE.entities) do
206 | resolve_macros(entity, level, link_context:set_this(entity), transient_fields)
207 | end
208 |
209 | resolve_macros(CACHE, level, link_context, transient_fields)
210 | reset_transient_fields(transient_fields)
211 | end
212 |
213 | function eval_logic_macro(str, link_context)
214 | local macro = Macro:new(str)
215 | trace_assert(macro and macro.is_auto and macro.level == '', "Not a @$ macro", str)
216 | local transient_fields = {}
217 | local resolved = _resolve_linked_macro(macro, '', link_context, transient_fields)
218 | reset_transient_fields(transient_fields)
219 | return resolved
220 | end
221 |
222 | --------------- Testing --------------------------
223 | --[[
224 | igi_helper = {
225 | trace_assert = function(...) return assert(...) end,
226 | trace_dbg = print
227 | }
228 |
229 | loadstring = load
230 |
231 | _G.igi_text_processor = _G
232 |
233 | trace_assert = igi_helper.trace_assert
234 |
235 | CACHE = {
236 | entities = {
237 | {
238 | id = 1234,
239 | huh = '@$ |this.id|',
240 | br1 = "@$ |this.id|",
241 | br3 = "$ |this.br2|",
242 | br2 = "$ |this.br1|",
243 | brbrbr = "$ |this.br3|",
244 | str = "@$ |this.brbrbr|"
245 | }
246 | }
247 | }
248 |
249 | resolve_and_link_cache(CACHE, '')
250 | print(eval_logic_macro(CACHE.entities[1].str, get_link_context(CACHE, CACHE.entities[1])))
251 | print(CACHE.entities[1].br1)
252 | ]]
--------------------------------------------------------------------------------
/gamedata/scripts/igi_utils.script:
--------------------------------------------------------------------------------
1 | Set = {
2 | union = function(self, set2)
3 | local new_set = dup_table(self)
4 | for k in pairs(set2) do
5 | new_set[k] = true
6 | end
7 | return new_set
8 | end,
9 |
10 | intersection = function(self, set2)
11 | local new_set = {}
12 | for k in pairs(set2) do
13 | new_set[k] = self[k]
14 | end
15 | return new_set
16 | end,
17 |
18 | difference = function(self, set2)
19 | local new_set = dup_table(self)
20 | for k in pairs(set2) do
21 | new_set[k] = nil
22 | end
23 | return new_set
24 | end,
25 |
26 | from_list = function(list)
27 | local new_set = {}
28 | for _, v in pairs(list) do
29 | new_set[v] = true
30 | end
31 | return new_set
32 | end,
33 |
34 | is_subset = function (set1, set2)
35 | for k in pairs(set1) do
36 | if not set2[k] then
37 | return false
38 | end
39 | end
40 | return true
41 | end,
42 | }
43 |
44 | function random_table_element(tbl)
45 | local keyset = {}
46 | for k in pairs(tbl) do
47 | table.insert(keyset, k)
48 | end
49 | -- now you can reliably return a random key
50 | local random_key = keyset[igi_random.rand(#keyset)]
51 | local random_elem = tbl[random_key]
52 | return random_elem, random_key
53 | end
54 |
55 | function random_table_value(tbl)
56 | local val, _ = random_table_element(tbl)
57 | return val
58 | end
59 |
60 | function random_table_key(tbl)
61 | local _, k = random_table_element(tbl)
62 | return k
63 | end
64 |
65 | function choose(...)
66 | local list = { ... }
67 | return list[igi_random.rand(#list)]
68 | end
69 |
70 | function get_random_items(orig_list, amount)
71 | if not orig_list or #orig_list < amount then
72 | return nil, "Not enough items"
73 | end
74 |
75 | local set = {}
76 | local list = {}
77 | for i=1, amount do
78 | local item
79 | repeat
80 | item = orig_list[igi_random.rand(#orig_list)]
81 | until not set[item]
82 | list[#list + 1] = item
83 | set[item] = true
84 | end
85 | return list
86 | end
87 |
88 | function defaultdict(factory)
89 | local tbl = {}
90 | local metatbl = {}
91 | if type(factory) == "function" then
92 | metatbl.__index = function (self, k)
93 | local v = factory()
94 | self[k] = v
95 | return v
96 | end
97 | else
98 | metatbl.__index = function (self, k)
99 | local v = factory
100 | self[k] = v
101 | return v
102 | end
103 | end
104 | setmetatable(tbl, metatbl)
105 | return tbl
106 | end
107 |
108 | function table_find(tbl, elem)
109 | for k, v in pairs(tbl) do
110 | if v == elem then
111 | return k
112 | end
113 | end
114 | end
115 |
--------------------------------------------------------------------------------
/gamedata/scripts/modxml_wtf.script:
--------------------------------------------------------------------------------
1 | function on_xml_read()
2 | RegisterScriptCallback("on_xml_read", function(xml_file_name, xml_obj)
3 | if xml_file_name == [[gameplay\dialogs.xml]] then
4 | for xml in pairs(get_dialog_xmls()) do
5 | printf("wtf xml %s", xml)
6 | xml_obj:insertFromXMLFile(xml)
7 | end
8 | end
9 | end)
10 | end
11 |
12 | function on_game_start()
13 | RegisterScriptCallback("on_specific_character_dialog_list", function(character_id, dialog_list)
14 | for dialog in pairs(get_dialogs().dialogs or {}) do
15 | dialog_list:add(dialog)
16 | end
17 | end)
18 | end
19 |
20 | DIALOGS = {}
21 | function get_dialogs()
22 | if not next(DIALOGS) then
23 | local parser = slaxml.SLAXML()
24 | local parser_options = {stripWhitespace = true}
25 |
26 | for xml in pairs(get_dialog_xmls()) do
27 | local out = getFS():r_open('$game_config$', xml):r_stringZ()
28 |
29 | local ok, xml_table = pcall(parser.dom, parser, out, parser_options)
30 | if ok then
31 | collect_dialog_data(xml_table)
32 | end
33 |
34 | end
35 | end
36 |
37 | return DIALOGS
38 | end
39 |
40 | function get_dialog_xmls()
41 | local out = {}
42 | local files = getFS():file_list_open(
43 | "$game_config$", "igi_tasks\\dialogs\\", bit_or(FS.FS_ListFiles,FS.FS_RootOnly))
44 | for i = 0, files:Size() - 1 do
45 | local xml_name = files:GetAt(i)
46 | out["igi_tasks\\dialogs\\"..xml_name] = true
47 | end
48 |
49 | return out
50 | end
51 |
52 | function collect_dialog_data(tbl)
53 | if tbl.name == 'action' or tbl.name == 'precondition' then
54 | local task, data_key = string.match(tbl.kids[1].value, "igi_dialogs%.data%.([^%.]*)%.(.*)")
55 | if task and data_key then
56 | DIALOGS.funcs = DIALOGS.funcs or {}
57 | DIALOGS.funcs[#DIALOGS.funcs+1] = {task, data_key}
58 | end
59 | elseif tbl.name == 'dialog' then
60 | DIALOGS.dialogs = DIALOGS.dialogs or {}
61 | DIALOGS.dialogs[tbl.attr.id] = true
62 | end
63 |
64 | for _, v in pairs(tbl.kids or {}) do
65 | collect_dialog_data(v)
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/gamedata/scripts/wtf.script:
--------------------------------------------------------------------------------
1 | function keys(tbl)
2 | local out = {}
3 | for k in pairs(tbl) do
4 | out[#out+1] = k
5 | end
6 | return out
7 | end
8 |
9 | function values(tbl)
10 | local out = {}
11 | for _, v in pairs(tbl) do
12 | out[#out+1] = v
13 | end
14 | return out
15 | end
16 |
17 | function flat_map(tbl, f)
18 | local out = {}
19 | for _, v in ipairs(tbl) do
20 | local fout = f(v)
21 | for _, v2 in ipairs(fout) do
22 | out[#out+1] = v2
23 | end
24 | end
25 | return out
26 | end
27 |
28 | function map(tbl, f)
29 | local out = {}
30 | for _, v in ipairs(tbl) do
31 | out[#out+1] = f(v)
32 | end
33 | return out
34 | end
35 |
36 | function filter(tbl, f)
37 | local out = {}
38 | for _, v in ipairs(tbl) do
39 | if f(v) then
40 | out[#out+1] = v
41 | end
42 | end
43 | return out
44 | end
45 |
46 | function set(tbl, k, v)
47 | tbl[k] = v
48 | return v
49 | end
50 |
51 | function shuffle(tbl)
52 | local t = dup_table(tbl)
53 | for i = 1, #t, 1 do
54 | local index = igi_random.rand(i)
55 | t[index], t[i] = t[i], t[index]
56 | end
57 | return t
58 | end
59 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | VERSION=$(grep '^TASKS_VERSION =' gamedata/scripts/igi_generic_task.script | sed 's/TASKS_VERSION = "\(.*\)".*/\1/')
4 | LASTCOMMIT=$(git log --oneline | head -n 1 | sed 's/\(.......\).*/\1/')
5 | git tag $VERSION $LASTCOMMIT
6 | git push origin $VERSION
7 |
8 | echo "Authorization: token $GITHUB_TOKEN"
9 | curl \
10 | -X POST \
11 | -H "Accept: application/vnd.github.v3+json" \
12 | -H "Content-Type:application/json" \
13 | -H "Authorization: token $GITHUB_TOKEN" \
14 | https://api.github.com/repos/Igigog/Weird_Tasks_Framework/releases \
15 | -d "{\"tag_name\":\"$VERSION\"}" || exit
16 |
17 | NAME="Weird_Tasks_Framework_$VERSION"
18 | NAME_FULL="${NAME}_DEV"
19 | cd .. && zip -r -q "Igi_Tasks/$NAME_FULL.zip" Igi_Tasks && cd -
20 |
21 | mkdir ./"$NAME"
22 | cp -r gamedata ./"$NAME"/gamedata
23 | zip -r -q "$NAME.zip" "$NAME"
24 | rm -r ./"$NAME"
25 |
26 | ./upload_asset.sh owner=Igigog repo=Weird_Tasks_Framework tag=$VERSION filename="$NAME.zip"
27 | ./upload_asset.sh owner=Igigog repo=Weird_Tasks_Framework tag=$VERSION filename="$NAME_FULL.zip"
28 |
29 | rm ./"$NAME.zip"
30 | rm ./"$NAME_FULL.zip"
31 |
32 | echo DONE!
33 |
--------------------------------------------------------------------------------
/test_framework.bat:
--------------------------------------------------------------------------------
1 | @RD /S /Q "D:\Games\Anomaly\MO2\mods\Weird_Tasks_Framework"
2 | XCOPY /e /i /q "D:\Tasks\Weird_Tasks_Framework" "D:\Games\Anomaly\MO2\mods\Weird_Tasks_Framework"
3 | START "" "D:\Desktop\MO2Anomaly.lnk"
--------------------------------------------------------------------------------
/upload_asset.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | #
3 | # Author: Stefan Buck
4 | # https://gist.github.com/stefanbuck/ce788fee19ab6eb0b4447a85fc99f447
5 | #
6 | #
7 | # This script accepts the following parameters:
8 | #
9 | # * owner
10 | # * repo
11 | # * tag
12 | # * filename
13 | # * github_api_token
14 | #
15 | # Script to upload a release asset using the GitHub API v3.
16 | #
17 | # Example:
18 | #
19 | # upload-github-release-asset.sh github_api_token=TOKEN owner=stefanbuck repo=playground tag=v0.1.0 filename=./build.zip
20 | #
21 |
22 | # Check dependencies.
23 | set -e
24 | xargs=$(which gxargs || which xargs)
25 |
26 | # Validate settings.
27 | [ "$TRACE" ] && set -x
28 |
29 | CONFIG=$@
30 |
31 | for line in $CONFIG; do
32 | eval "$line"
33 | done
34 |
35 | # Define variables.
36 | GH_API="https://api.github.com"
37 | GH_REPO="$GH_API/repos/$owner/$repo"
38 | GH_TAGS="$GH_REPO/releases/tags/$tag"
39 | github_api_token=$GITHUB_TOKEN
40 | AUTH="Authorization: token $github_api_token"
41 | WGET_ARGS="--content-disposition --auth-no-challenge --no-cookie"
42 | CURL_ARGS="-LJO#"
43 |
44 | if [[ "$tag" == 'LATEST' ]]; then
45 | GH_TAGS="$GH_REPO/releases/latest"
46 | fi
47 |
48 | # Validate token.
49 | curl -o /dev/null -sH "$AUTH" $GH_REPO || { echo "Error: Invalid repo, token or network issue!"; exit 1; }
50 |
51 | # Read asset tags.
52 | response=$(curl -sH "$AUTH" $GH_TAGS)
53 |
54 | # Get ID of the asset based on given filename.
55 | eval $(echo "$response" | grep -m 1 "id.:" | grep -w id | tr : = | tr -cd '[[:alnum:]]=')
56 | [ "$id" ] || { echo "Error: Failed to get release id for tag: $tag"; echo "$response" | awk 'length($0)<100' >&2; exit 1; }
57 |
58 | # Upload asset
59 | echo "Uploading asset... $localAssetPath" >&2
60 |
61 | # Construct url
62 | GH_ASSET="https://uploads.github.com/repos/$owner/$repo/releases/$id/assets?name=$(basename $filename)"
63 |
64 | curl --data-binary @"$filename" -H "Authorization: token $github_api_token" -H "Content-Type: application/octet-stream" $GH_ASSET
--------------------------------------------------------------------------------