├── .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 | 4 | WTF 5 | 6 | 7 | WTF 8 | 9 | 10 | 11 | Options 12 | 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 --------------------------------------------------------------------------------