├── README.md ├── .gitignore ├── assets ├── image1.png └── image2.png ├── mods ├── tommy_debug │ ├── @tommy_debug.pack │ ├── README.md │ └── script │ │ └── all_scripted.lua ├── helloworld_campaign │ └── script │ │ └── campaign │ │ └── mod │ │ └── 3k_helloworld_campaign.lua ├── tommy_read_ca_official_instance │ └── script │ │ └── campaign │ │ └── mod │ │ └── 3k_tommy_read_ca_officail_instance.lua └── tommy_randomized_start │ ├── README.md │ └── script │ └── campaign │ └── mod │ └── 3k_tommy_randomized_start.lua ├── fixtures └── instance_patch.lua ├── mods_common ├── randomized_start_military_force │ └── script │ │ ├── _lib │ │ ├── common_tommy_debug_try_catch.lua │ │ └── common_tommy_randomized_start.lua │ │ └── campaign │ │ └── mod │ │ └── wh2_tommy_randomized_start.lua └── wh2_tommy_debug │ └── script │ └── all_scripted.lua ├── luaformat ├── LICENSE └── daily └── 1_HELLOW_WORLD.md /README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.code-workspace 2 | tmp/ 3 | -------------------------------------------------------------------------------- /assets/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rifkyprayoga/totalwar-3k-lua-mod-pratices/HEAD/assets/image1.png -------------------------------------------------------------------------------- /assets/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rifkyprayoga/totalwar-3k-lua-mod-pratices/HEAD/assets/image2.png -------------------------------------------------------------------------------- /mods/tommy_debug/@tommy_debug.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rifkyprayoga/totalwar-3k-lua-mod-pratices/HEAD/mods/tommy_debug/@tommy_debug.pack -------------------------------------------------------------------------------- /mods/tommy_debug/README.md: -------------------------------------------------------------------------------- 1 | # tommy_debug.pack 2 | 3 | 把日志文件输出改成固定名称`script_log.txt`,方便通过`tail -f`进行调试 4 | 5 | [@tommy_debug.pack](https://github.com/tommyZZM/totalwar-3k-lua-mod-pratices/raw/master/mods/tommy_debug/%40tommy_debug.pack) 6 | -------------------------------------------------------------------------------- /fixtures/instance_patch.lua: -------------------------------------------------------------------------------- 1 | local proto = {}; 2 | 3 | function proto:new() 4 | return self; 5 | end 6 | 7 | function proto:method() 8 | return 1; 9 | end 10 | 11 | local obj = proto:new(); 12 | 13 | print(obj:method()) -- 1 14 | 15 | obj.method_origin = obj.method; 16 | 17 | function obj:method() 18 | return self:method_origin() + 1; 19 | end 20 | 21 | print(obj:method()) -- 2 22 | -------------------------------------------------------------------------------- /mods_common/randomized_start_military_force/script/_lib/common_tommy_debug_try_catch.lua: -------------------------------------------------------------------------------- 1 | out("common_tommy_lib/debug_try_catch.lua ****************"); 2 | 3 | local common_tommy_debug_try_catch = {is_debug = true} 4 | 5 | function common_tommy_debug_try_catch.catch(what) return what[1] end 6 | 7 | function common_tommy_debug_try_catch.try(what) 8 | if (not _debug.is_debug) then return what[2](result); end 9 | status, result = pcall(what[1]) 10 | if not status then what[2](result) end 11 | return result 12 | end 13 | 14 | return common_tommy_debug_try_catch 15 | -------------------------------------------------------------------------------- /luaformat: -------------------------------------------------------------------------------- 1 | column_limit: 120 2 | indent_width: 2 3 | use_tab: false 4 | tab_width: 2 5 | continuation_indent_width: 2 6 | spaces_before_call: 1 7 | keep_simple_control_block_one_line: true 8 | keep_simple_function_one_line: true 9 | align_args: true 10 | break_after_functioncall_lp: false 11 | break_before_functioncall_rp: false 12 | align_parameter: true 13 | chop_down_parameter: false 14 | break_after_functiondef_lp: false 15 | break_before_functiondef_rp: false 16 | align_table_field: true 17 | break_after_table_lb: true 18 | break_before_table_rb: true 19 | chop_down_table: false 20 | chop_down_kv_table: true 21 | table_sep: "," 22 | extra_sep_at_table_end: false 23 | break_after_operator: true 24 | double_quote_to_single_quote: false 25 | single_quote_to_double_quote: false 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 tommyZZM 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 | -------------------------------------------------------------------------------- /mods/helloworld_campaign/script/campaign/mod/3k_helloworld_campaign.lua: -------------------------------------------------------------------------------- 1 | __write_output_to_logfile = true; --- <--- 记得在发布时注释掉 2 | __logfile_path = "script_log.txt"; --- <--- 记得在发布时注释掉 3 | 4 | out("my_mod | 3k_helloworld.lua hello world"); -- MOD文件加载时会调用此lua文件 5 | 6 | local _debug = {is_debug = true} -- local 调试变量,避免影响全局,其他mod 7 | 8 | function _debug:catch(what) return what[1] end 9 | 10 | function _debug:try(what) 11 | if (not _debug.is_debug) then return what[2](result); end 12 | status, result = pcall(what[1]) 13 | if not status then what[2](result) end 14 | return result 15 | end 16 | 17 | local function hello_world() 18 | -- 在这实现mod功能 19 | out("my_mod | first_tick_callback hello world!"); 20 | out(xxx); --<-- 报错引用未定义xxx变量 21 | end 22 | 23 | local function RUN_hello_world() -- 游戏初始化时会调用函数 24 | _debug:try { 25 | function() 26 | hello_world() -- 用try catch将整个mod函数体实现包裹起来 27 | end, 28 | _debug:catch{function(error) script_error('my_mod | CAUGHT ERROR: ' .. error); end} 29 | } 30 | end 31 | 32 | -- 添加游戏初始化的回调,执行 RUN_hello_world 函数 33 | cm:add_first_tick_callback(function(context) RUN_hello_world(context) end); 34 | -------------------------------------------------------------------------------- /mods/tommy_read_ca_official_instance/script/campaign/mod/3k_tommy_read_ca_officail_instance.lua: -------------------------------------------------------------------------------- 1 | local _debug = {is_debug = true} 2 | 3 | function _debug:catch(what) return what[1] end 4 | 5 | function _debug:try(what) 6 | if (not _debug.is_debug) then return what[2](result); end 7 | status, result = pcall(what[1]) 8 | if not status then what[2](result) end 9 | return result 10 | end 11 | 12 | cm:add_first_tick_callback(function(context) 13 | out("3k_tommy_read_ca_officail_instance ------ START") 14 | 15 | _debug:try{ 16 | function() 17 | for key, _ in pairs(getmetatable(core)) do out("global core:" .. key); end 18 | 19 | out("3k_tommy_read_ca_officail_instance ------") 20 | 21 | for key, _ in pairs(getmetatable(cm)) do out("global cm:" .. key); end 22 | 23 | out("3k_tommy_read_ca_officail_instance ------") 24 | 25 | local faction_list = cm:query_model():world():faction_list(); 26 | local faction_0 = faction_list:item_at(0); 27 | local faction_handle = cm:modify_faction(faction_0:name()); 28 | 29 | for key, _ in pairs(getmetatable(faction_handle)) do out("instance modify_faction:" .. key); end 30 | 31 | out("3k_tommy_read_ca_officail_instance ------") 32 | 33 | local region_list_world = cm:query_model():world():region_manager():region_list(); 34 | local region_0 = region_list_world:item_at(0); 35 | 36 | local region_handle = cm:modify_region(region_0:name()); 37 | 38 | for key, _ in pairs(getmetatable(region_0)) do out("instance region_0:" .. key); end 39 | 40 | for key, _ in pairs(getmetatable(region_handle)) do out("instance region_handle:" .. key); end 41 | end, _debug:catch{function(error) script_error('3k_tommy_randomized_start.lua | CAUGHT ERROR: ' .. error); end} 42 | } 43 | 44 | local model = context:query_model(); 45 | local faction_me = model:local_faction(); 46 | local faction_me_handle = cm:modify_faction(faction_me:name()); 47 | local region_list_world = cm:query_model():world():region_manager():region_list(); 48 | for i = 0, region_list_world:num_items() - 1 do 49 | local region = region_list_world:item_at(i); 50 | faction_me_handle:make_region_seen_in_shroud(region:name()); 51 | faction_me_handle:make_region_visible_in_shroud(region:name()); 52 | end; 53 | 54 | out("3k_tommy_read_ca_officail_instance ------ END") 55 | end); 56 | -------------------------------------------------------------------------------- /mods/tommy_randomized_start/README.md: -------------------------------------------------------------------------------- 1 | # [WIP]随机开局 Randomized Start 2 | 3 | 随机放置所有初始曲部的地图位置,使得每一次新游戏都完全不一样,每次开新周目都不用面对一模一样的初始场景。 4 | 5 | 目前该MOD只会影响在野外的曲部,在据点的曲部不会影响。玩家和AI曲部都会有效。 6 | 7 | randomized position for all military forces in wild (outside the settlement) in the initial map, in order to make campaign strategic situation different every new game. Both PLAYER and AI effects. 8 | 9 | This mod doesn't need any translation in the game, because the lua script never change the text data. ALL LANGUAGE SUITABLE. 10 | 11 | [img]https://i.imgur.com/fVVaDCS.gif[/img] 12 | 13 | 随机放置规则: 14 | 1) 已有领土的阵营,会在已拥有领土地区和初始地区随机选择位置放置曲部 15 | 2) 汉王朝阵营,会按照曲部所在初始位置所在的区域随机选择位置(避免过于混乱) 16 | 3) 无初始领土的阵营(例如郑酱)会在全地图中随机选择一个位置,如果初始位置废弃会自动设置给该阵营 17 | 4) 对于190年刘备,194年孙策,会按照初始位置所在区域以及临近区域随机选择一个位置 18 | 5) 如果找不到合适的随机位置会自动使用默认位置 19 | 20 | Reposition Rules: 21 | 1) For faction OWNS INITIAL REGIONS, random choose positions in the own initial territories and its adjacent territories 22 | 2) For HAN EMPIRE ROYAL faction, each military forces random choose positions from its located initial territory, (prevent randomize make too many mass, because Han empire has too many initial territories) 23 | 3) For faction OWNS NO REGION (such as ZHANG JIANG), random choose positions in the whole map 24 | 4) For LIU BEI in 190, SUN CE in 194 random choose positions from its located initial territory and adjacent territories.(because their initial mission target needs) 25 | 5) IF the script of this mod didn't find a suitable position, military force will stay in the default position. 26 | 27 | 已实现功能: 28 | 1) 随机初始位置生成,并放置曲部位置 29 | 2) 初始镜头会自动移动到主要曲部中 30 | 31 | Features: 32 | 1) random position for all military forces in wild. 33 | 2) initial camera will auto locate at the primary military force which randomized. 34 | 35 | ---- 2020.3.24 更新 ---- 36 | - 优化了曲部随机位置经常会重叠的问题,会稍微增加初始化的时间,现在位置会更加随机了 37 | - 增加功能无初始领土的阵营(规则3)随机到的初始位置会自动给该阵营,避免开局无事可做 38 | - 增加190年刘备,194年孙策,会按照初始位置所在区域以及临近区域随机选择一个位置(跟他们的任务有关) 39 | 40 | - Optimized the issue that the random positions of military forces often overlap. It will slightly increase the initialization time. Now the positions will be more random. 41 | - For faction owns NO territories(in rules 3), if the region of random initial position was abandoned(not capture by other faction), this region will be automatically given to the faction, avoiding player can do nothing at the start of game. 42 | - add rules for LIU BEI in 190, SUN CE in 194 43 | ---- 44 | 45 | 技术上来说,这个MOD,兼容所有DLC,以及所有其他 !!非通过CA初始事件!! 修改初始位置的MOD 46 | 47 | Technically, this MOD is compatible with all DLC, and other MOD which not chaning the initial position by CA official lua initial events. 48 | 49 | 本mod还在开发中,更多功能后续待补充,有问题欢迎在评论区反馈... 50 | 51 | This mod is still work in progress, more features may implement in the future, welcome discuss and report issues in the comments. 52 | -------------------------------------------------------------------------------- /mods_common/randomized_start_military_force/script/_lib/common_tommy_randomized_start.lua: -------------------------------------------------------------------------------- 1 | out("common_tommy_randomized_start.lua ****************"); 2 | 3 | package.path = package.path ..';../?.lua'; 4 | 5 | -- local _debug = require("common_tommy_debug_try_catch") 6 | 7 | local _debug = {is_debug = true} 8 | 9 | function _debug:catch(what) return what[1] end 10 | 11 | function _debug:try(what) 12 | if (not _debug.is_debug) then return what[2](result); end 13 | status, result = pcall(what[1]) 14 | if not status then what[2](result) end 15 | return result 16 | end 17 | 18 | local common_tommy_randomized_start = {} 19 | 20 | local random_call_offset = 0; 21 | local function better_math_randomseed() 22 | random_call_offset = random_call_offset + 1; 23 | math.randomseed(tonumber(tostring(6 * random_call_offset + os.time() * 2 * random_call_offset):reverse():sub(1, 8))); 24 | if (random_call_offset > 100) then random_call_offset = 0; end 25 | end 26 | 27 | function common_tommy_randomized_start.better_math_randomseed() 28 | return better_math_randomseed() 29 | end 30 | 31 | local function random_split_2(min, range) 32 | local rangeToRand = (min + range); 33 | better_math_randomseed(); 34 | local randIntl = (math.random() * rangeToRand * 2) - (rangeToRand); 35 | local edgeLeftIntl = ((randIntl / math.abs(randIntl)) * min); 36 | return edgeLeftIntl + (randIntl - edgeLeftIntl); 37 | end 38 | 39 | function common_tommy_randomized_start.random_split_2(min, range) 40 | return random_split_2(min, range) 41 | end 42 | 43 | local function _is_faction_local_human(context, faction, params) 44 | local model = params.cm_query_model() 45 | return faction:is_human() and faction:name() == params.cm_local_faction_name() 46 | end 47 | 48 | function common_tommy_randomized_start.is_faction_local_human(context, faction, params) 49 | return _is_faction_local_human(context, faction, params) 50 | end 51 | 52 | local function _get_character_name_readable(character) 53 | local str_name_localised_string = effect.get_localised_string(character:get_surname()); 54 | local str_forename_localised_string = effect.get_localised_string(character:get_forename()); 55 | return ("" .. str_name_localised_string .. "" .. str_forename_localised_string .. ""); 56 | end 57 | 58 | local function _get_bool_str(boo) return (boo and "true" or "false"); end 59 | 60 | function common_tommy_randomized_start.get_bool_str(boo) 61 | return _get_bool_str(boo) 62 | end 63 | 64 | -- get character info mation readable 65 | local function _get_character_infomation_readable(character) 66 | return ("character:" .. _get_character_name_readable(character) .. ", faction[" .. character:faction():name() .. 67 | "], logical pos[" .. character:logical_position_x() .. ", " .. character:logical_position_y() .. 68 | "], display pos[" .. character:display_position_x() .. ", " .. character:display_position_y() .. 69 | "], has_region:" .. _get_bool_str(character:has_region()) .. ", in_settlement:" .. 70 | _get_bool_str(character:in_settlement())); 71 | end 72 | 73 | function common_tommy_randomized_start.get_character_infomation_readable(context, faction) 74 | return _get_character_infomation_readable(context, faction) 75 | end 76 | 77 | local function _list_ca_to_mormal_table(list_ca) 78 | local result = {} 79 | for i = 0, list_ca:num_items() - 1 do result[i + 1] = list_ca:item_at(i); end 80 | return result; 81 | end 82 | 83 | function common_tommy_randomized_start.list_ca_to_mormal_table(region_list) 84 | return _list_ca_to_mormal_table(region_list) 85 | end 86 | 87 | -- main function 88 | local function tommy_randomize_start(context, params) 89 | out("tommy_randomize_start() | START of script "); 90 | if not cm:is_new_game() then 91 | out("tommy_randomize_start() | not cm:is_new_game()"); 92 | return false; 93 | end 94 | out("tommy_randomize_start() | cm:is_new_game()"); 95 | MAPPING_PICKED_POSITIONS_COUNT = {} 96 | 97 | local faction_list = params.cm_query_model():world():faction_list(); 98 | 99 | for f = 0, faction_list:num_items() - 1 do 100 | local faction = faction_list:item_at(f); 101 | local isHasMilitaryForceFaction = not faction:military_force_list():is_empty(); 102 | if (isHasMilitaryForceFaction and not faction:is_null_interface() and not faction:is_dead()) then 103 | params.randomizer(context, faction, params); 104 | end 105 | -- if faction:is_human() then 106 | -- local isHasMilitaryForceFaction = not faction:military_force_list():is_empty(); 107 | -- if (isHasMilitaryForceFaction) then tommy_randomize_all_characters_for_faction(faction); end 108 | -- end 109 | end 110 | 111 | out("tommy_randomize_start() | END of script "); 112 | end 113 | 114 | function common_tommy_randomized_start.run(context, params) 115 | if (_debug.is_debug) then 116 | out('3k_tommy_randomized_start.lua | run debug'); 117 | _debug:try{ 118 | function() tommy_randomize_start(context, params); end, 119 | _debug:catch{function(error) script_error('3k_tommy_randomized_start.lua | CAUGHT ERROR: ' .. error); end} 120 | } 121 | else 122 | out('3k_tommy_randomized_start.lua | run'); 123 | tommy_randomize_start(context, params); 124 | end 125 | out('3k_tommy_randomized_start.lua | end'); 126 | return true; 127 | end 128 | 129 | return common_tommy_randomized_start 130 | -------------------------------------------------------------------------------- /daily/1_HELLOW_WORLD.md: -------------------------------------------------------------------------------- 1 | # 三国全面战争使用lua进行MOD开发的简要记录 (2020.03.24) 2 | 3 | 周末尝试了一下初步研究并尝试了一下使用lua进行三国全面战争MOD开发的游戏玩法(花了几百块买了个编程游戏 4 | 5 | 开发了一个放置军事单位位置的 MOD 并且发布到了创意工坊上。[[WIP]随机开局 Randomized Start (Position)](https://steamcommunity.com/sharedfiles/filedetails/?id=2031427773)。欢迎在游戏中使用以及搜藏点赞(明示)。 6 | 7 | 该MOD的源码在 [../mods/tommy_randomized_start](../mods/tommy_randomized_start),可作参考。 8 | 9 | 本日志记录这次开发了解到的知识,以便有需要的读者参考。 10 | 11 | ## 基础知识 12 | 13 | 首先使用LUA开发MOD需要一些必备的工具 14 | 15 | - **CA的.pack文件解包工具**,建议使用 [rpfm](https://github.com/Frodo45127/rpfm/releases) 16 | 17 | - **代码编辑器**,建议使用 [VSCode](https://code.visualstudio.com/) 或 [Atom](https://atom.io/),本作者使用的是 VSCode 18 | 19 | - **git**和**Windows GUN 控制台工具**,用于管理代码和查看日志,推荐使用 [Git Bash](https://gitforwindows.org/) ,集成了`MINGW64`,安装时建议勾选添加到右键菜单 20 | 21 | 如果你熟悉`git`你也可以使用`git`来管理你的代码迭代 22 | 23 | 继续阅读之前,建议先通过解包工具,解包CA的`script`下所有目录和`lua`文件放到一个目录,并添加到编辑器`workspace`,以便通过`Ctrl+F`随时阅读CA的源代码。 24 | 25 | 因为没有官方公开的文档,所以几乎所有API都需要通过阅读源码来了解,本文中也会包括一些官方代码的引述。**通过变量命名结合注释可以判断一个API的功能** 26 | 27 | 也可以通过解包工具来查看工坊上其他使用 lua 脚本开发的 MOD,其他系列的全战例如战锤的 MOD 也可作为参考,但是不同系列的全战的 API 并不完全一样(命名有很多不同,但是代码组织是类似的)。 28 | 29 | #### Hello World 30 | 31 | 从一个`Hello World`开始,展开我目前所知道的情报。 32 | 33 | 首先三国全战的MOD的lua脚本加载必须放置在特定的目录才会被游戏加载。 34 | 35 | 不同类型的场景会有不同的加载时机,例如**战役地图**和**战斗地图**从MOD加载lua的目录是不同的。 36 | 37 | 用 VSCode 打开官方解包后的`script/`目录,在目录中全局搜索`campaign/mod`可以找到在`_lib\lib_mod_loader.lua`加载从MOD加载lua的目录。 38 | 39 | ```lua 40 | -- load mods here 41 | if core:is_campaign() then 42 | -- 本文注释: 如果core(根据名称和上下文应该是当前运行时的核心调度模块)是campaign(战役) 43 | -- LOADING CAMPAIGN MODS 44 | 45 | -- load mods on NewSession 46 | core:add_listener( 47 | "new_session_mod_scripting_loader", 48 | "NewSession", 49 | true, 50 | function(context) 51 | 52 | core:load_mods( 53 | "/script/_lib/mod/", -- general script library mods 54 | "/script/campaign/mod/", -- root campaign folder 55 | -- ... 56 | "/script/campaign/" .. CampaignName .. "/mod/" 57 | ); 58 | end, 59 | true 60 | ); 61 | 62 | ``` 63 | 64 | 通读`_lib\lib_mod_loader.lua`文件,里面还提供了`is_battle`战斗地图从MOD加载lua的路径。 65 | 66 | > 仅猜测,如果要做一个全局士气调整的功能,有没有可能可以不通过修改db数据?直接通过脚本进行管理,这样达到较大的兼容性和较少的工作量。 67 | 68 | 继续我们的`Hello World` 69 | 70 | 在我们的mod文件夹创建一个文件命名为`3k_helloworld_campaign.lua`,实际上你可以命名你喜欢的名字,但建议多加一点形容词,避免和其他mod冲突 71 | 72 | ``` 73 | - hello_world 74 | - script 75 | - campaign 76 | - mod 77 | - 3k_helloworld_campaign.lua <-- 78 | ``` 79 | 80 | 在里面输入以下内容(这里的`my_mod`前缀可以改成其他特殊标识,用来从日志筛选的) 81 | 82 | ```lua 83 | out("my_mod | 3k_helloworld.lua hello world"); -- MOD文件加载时会调用此lua文件 84 | 85 | local function RUN_hello_world() -- 游戏初始化时会调用函数 86 | out("my_mod | first_tick_callback hello world!"); 87 | end 88 | 89 | -- 添加游戏初始化的回调,执行 RUN_hello_world 函数 90 | cm:add_first_tick_callback(function(context) RUN_hello_world(context) end); 91 | ``` 92 | 93 | 这样我们的`Hello World`lua就编写好啦,然后再用**rpfm**将`script`目录打包到pack里,并放置到游戏data目录中,并在启动器勾选,这样我们的`Hello World`就在游戏里执行啦 94 | 95 | ``` 96 | - hello_world 97 | - script <-- 打包这个目录 98 | - campaign 99 | - mod 100 | - 3k_helloworld_campaign.lua 101 | ``` 102 | 103 | 你可以基于这个文件开始你的MOD制作,请把文件命名成你需要的名字 104 | 105 | ## 开发调试 106 | 107 | #### 日志输出 out(...) 108 | 109 | 我们把`Hello World`就在游戏里执行后,并不会产生任何效果,因为默认`out`方法输出的日志是不会写入到日志文件的。需要通过一处变量设置打开。 110 | 111 | 有三个方法可以打开调试配置。 112 | 113 | - 方法1 直接设置全局变量,**如果想要简单,推荐使用这样** 114 | 115 | 或者你也可以在自己的lua中通过全局变量配置,这是最简单的方式。 116 | 117 | 但记得发布mod的时候注释掉输出,避免在用户文件中意外产生大量的日志信息。 118 | 119 | ```lua 120 | __write_output_to_logfile = true; --- <--- 记得在发布时注释掉 121 | __logfile_path = "script_log.txt"; --- <--- 记得在发布时注释掉 122 | 123 | out("my_mod | 3k_helloworld.lua hello world"); -- MOD文件加载时会调用此lua文件 124 | 125 | local function RUN_hello_world() -- 游戏初始化时会调用函数 126 | out("my_mod | first_tick_callback hello world!"); 127 | end 128 | 129 | -- 添加游戏初始化的回调,执行 RUN_hello_world 函数 130 | cm:add_first_tick_callback(function(context) RUN_hello_world(context) end); 131 | ``` 132 | 133 | - 方法2. 替换`script\all_scripted.lua`文件 134 | 135 | 你可以安装这个mod [Script Debug Activator](https://steamcommunity.com/sharedfiles/filedetails/?id=1791910561&searchtext=debug) ,启用后会打开官方的默认日志输出 136 | 137 | CA官方默认的日志会自动按照分钟时间分片输出到游戏目录中,形如`scripts_log_.txt` 138 | 139 | **但!** 140 | 141 | 本作者认为这种时间分片方式并不是很有利于调试,因此此处介绍一下我输出日志和阅读筛查日志的方式 142 | 143 | 我是把日志输出设置为固定文件`scripts_log.txt`,并通过`tail -f`或者`tail -f | grep `的方式动态输出文件到控制台中。 144 | 145 | ```lua 146 | ++ -- local filename = "script_log_" .. os.date("%d".."".."%m".."".."%y".."_".."%H".."".."%M") .. ".txt"; 147 | ++ local filename = "script_log.txt"; 148 | ``` 149 | 150 | 改动后的文件已打包成一个单独MOD`.pack`, 可以下载并放置到data文件中并启用,[../mods/tommy_debug](../mods/tommy_debug) **推荐使用** 151 | 152 | 注意请不要将`script\all_scripted.lua`打包进你的MOD,以免导致崩溃。 153 | 154 | 假设你选择了固定文件的配置,打开调试信息之后,游戏运行时,日志数据就会写入到游戏目录中的`script_log.txt`中。 155 | 156 | 然后你就可以通过(UNIX)控制台的`tail -f`命令输出日志, 如图。 157 | 158 | ``` 159 | > /game/steamapps/common/Total War THREE KINGDOMS$ 160 | 161 | tail -f ./script_log.txt 162 | 163 | ``` 164 | 165 | ``` 166 | ### 过滤你自己mod的关键字my_mod, 或其他 167 | > /game/steamapps/common/Total War THREE KINGDOMS$ 168 | 169 | tail -f ./script_log.txt | grep my_mod 170 | 171 | ``` 172 | 173 | ![tail -f](../assets/image1.png) 174 | 175 | #### 异常捕获 try...catch 176 | 177 | 除了日志输出之外,异常捕获的功能也特别重要! 178 | 179 | CA在全战里内置的lua引擎在遇到lua异常时并不会抛出异常信息到日志中,而是会**静默失败**。 180 | 181 | 从日志上看到的行为就是前一段代码还有日志输出,后一段代码突然消失不见了。如果遇到这个情况很有可能就是代码中遇到了错误异常。 182 | 183 | lua并没有内置`try...catch`语句,需要使用者自行实现,在搜索引擎可以查到很多`try...catch`的实现方式。 184 | 185 | 我在gist上找到一个比较简短而且可用的`try...catch`实现 186 | 187 | https://gist.github.com/cwarden/1207556/a3c7caa194cad0c22871ac650159b40a88ecd702 188 | 189 | 可以把`try...catch`放置在你的lua文件头部,例如 190 | 191 | ```lua 192 | __write_output_to_logfile = true; --- <--- 记得在发布时注释掉 193 | __logfile_path = "script_log.txt"; --- <--- 记得在发布时注释掉 194 | 195 | out("my_mod | 3k_helloworld.lua hello world"); -- MOD文件加载时会调用此lua文件 196 | 197 | local _debug = {is_debug = true} -- local 调试变量,避免影响全局,其他mod 198 | 199 | function _debug:catch(what) return what[1] end 200 | 201 | function _debug:try(what) 202 | if (not _debug.is_debug) then return what[2](result); end 203 | status, result = pcall(what[1]) 204 | if not status then what[2](result) end 205 | return result 206 | end 207 | 208 | local function hello_world() 209 | -- 在这实现mod功能 210 | out("my_mod | first_tick_callback hello world!"); 211 | out(xxx); --<-- 报错: 引用未定义xxx变量 212 | end 213 | 214 | local function RUN_hello_world() -- 游戏初始化时会调用函数 215 | _debug:try { 216 | function() 217 | hello_world() -- 用try catch将整个mod函数体实现包裹起来 218 | end, 219 | _debug:catch{function(error) script_error('my_mod | CAUGHT ERROR: ' .. error); end} 220 | } 221 | end 222 | 223 | -- 添加游戏初始化的回调,执行 RUN_hello_world 函数 224 | cm:add_first_tick_callback(function(context) RUN_hello_world(context) end); 225 | ``` 226 | 227 | 这样mod执行的时候,就会报错抛出异常, 并且在日志中查看到。 228 | 229 | 这样可以极大的提高开发时排错调试的效率。 230 | 231 | #### 一个功能的调试经历 232 | 233 | 在开发[[WIP]随机开局 Randomized Start (Position)](https://steamcommunity.com/sharedfiles/filedetails/?id=2031427773)MOD的过程中。 234 | 235 | 遇到一个最困难的问题就是不知道如何设置初始镜头到玩家曲部的位置。 236 | 237 | 本作者认为这个是比随机点更难的功能。消耗了大量的调试成本。 238 | 239 | 通过简单调用CA的API`cm:set_camera_position`和`cm:scroll_camera_from_current`并没有产生任何效果,代码也没有报错。 240 | 241 | 后来通过日志排查,输出信息,首先定位到一个地方 242 | 243 | 就是玩家的曲部重新移动位置之后,曲部单位所在的坐标并不会立刻改变,通过在源码`scripts`中全局搜索,发现了一个异步回调的接口`cm:callback`,类似于`JavaScript`里的`setTimeout` 244 | 245 | 这个应该跟CA的lua引擎背后的原生代码Native Code的类似事件循环的机制有关。副作用并不会立刻生效。lua仅仅是原生代码的前端。 246 | 247 | 该接口的简单用法,参数1是回调函数,参数2是延时(猜测单位是秒) 248 | 249 | ```lua 250 | out("pos:"..general:display_position_x()..","..general:display_position_y()) 251 | cm:teleport_character(general, final_x, final_y); 252 | out("pos:"..general:display_position_x()..","..general:display_position_y()) -- 新的位置并不会返回 253 | cm:callback(function() 254 | out("pos:"..general:display_position_x()..","..general:display_position_y()) 255 | -- 位置改变才会生效,是否正确生效要 256 | end, 1) -- 延时一段时间执行 257 | ``` 258 | 259 | 解决了这个问题之后又遇到另外一个问题,即使我获取了正确的位置,我仍然无法把镜头设置到指定的位置 260 | 261 | 通过日志输出的信息,和游戏中的界面行为,我猜测一定是在某个事件之后,CA把镜头重置了,通过在通过在源码`scripts`中全局搜索关键字`cm:set_camera_position`,排查定位到了和初始介绍视频相关的逻辑有关,相关代码在`_lib\lib_campaign_cutscene.lua` 262 | 263 | ```lua 264 | function campaign_cutscene:skip_action(advice_being_dismissed) 265 | -- ... 266 | -- reposition camera if we have a skip camera (this is delayed in case the cindy scene is still running) 267 | if self.skip_cam_x then 268 | if self.cindy_camera_specified then 269 | cm:callback(function() cm:set_camera_position(self.skip_cam_x, self.skip_cam_y, self.skip_cam_d, self.skip_cam_b, self.skip_cam_h) end, 0.1); 270 | else 271 | cm:set_camera_position(self.skip_cam_x, self.skip_cam_y, self.skip_cam_d, self.skip_cam_b, self.skip_cam_h); 272 | end; 273 | 274 | elseif self.restore_cam_time >= 0 then 275 | self:restore_camera_and_release(true); 276 | return; 277 | end; 278 | -- ... 279 | end 280 | ``` 281 | 282 | 结束介绍视频的时候,把镜头给重置了,这里我遇到了一个难题,因为结束介绍视频这个代码点位,并没有任何事件回调可以让我调用,简单使用延时执行的效果也十分不稳定。 283 | 284 | 我只得寻找一个事件回调,在这个镜头重置之后,再设置新的镜头位置。 285 | 286 | 通过阅读`scripts_log.txt`日志 287 | 288 | ``` 289 | [out] <68.9s> === progress_on_loading_screen_dismissed() - loading screen with name [campaign] has been dismissed, waiting for it to finish animating 290 | [out] <69.7s> === progress_on_loading_screen_dismissed() - loading screen with name [campaign] has finished animating, proceeding 291 | [out] <69.7s> * Stealing ESC key 292 | [out] <70.7s> * Releasing ESC key 293 | [out] <70.7s> cutscene_intro_play 294 | [out] <70.7s> * Stealing user input 295 | [out] <70.7s> >> enable_event_panel_auto_open() preventing event panels 296 | [out] <70.8s> * Stealing ESC key 297 | [out] <70.8s> * CinematicTrigger event received with id start_cinematic - no callback registered for this id 298 | [out] <71.8s> Campaign_Cutscene_intro has been skipped 299 | [ui] <71.8s> interface_function() called, function_name is clear_all_info_text 300 | [ui] <71.8s> uicomponent is root > advice_interface 301 | [out] <71.8s> Stopping cinematic playback 302 | [out] <71.8s> * Releasing ESC key 303 | ``` 304 | 305 | 找到了退出介绍视频的点位,通过全局搜索这些日志文本,筛选到事件关键词`CinematicTrigger` 306 | 307 | 但这个事件点位还是无效。 308 | 309 | 在阅读代码的过程中,知道`core:add_listener`是用于侦听事件回调的,既然有回调就会有发出事件,很快就从上下文代码中得出`core:trigger_event`是触发事件用的。 310 | 311 | 但是`CinematicTrigger`并不是通过`core:trigger_event`触发的,我猜测是原生代码事件。 312 | 313 | 通过全局搜索`core:trigger_event`我找到另外一个事件`ScriptEventCampaignCutsceneCompleted` 314 | 315 | ![ScriptEventCampaignCutsceneCompleted](../assets/image2.png) 316 | 317 | 这是一个未被现有文档记录的事件,即便是附录中的社区文档也没有记录。 318 | 319 | 通过名称可以看出这个事件就是我们要找的`ScriptEvent`,`Campaign`,`Cutscene`,`Completed`战役介绍视频结束事件。 320 | 321 | 最终使用这个事件配合`cm:callback`,实现了初始镜头定位的功能。 322 | 323 | mods\tommy_randomized_start\script\campaign\mod\3k_tommy_randomized_start.lua#198 324 | 325 | #### 已知的常用源码文件和API(仅战役地图,因为我还只做过战役地图里的事件处理) 326 | 327 | 全局对象, 下列关键字有助于帮助你阅读CA的lua源码,能找到很多官方功能的实现和有用的事件等。 328 | 329 | 列出了文件的路径,可以查看原型的所有方法类型。 330 | 331 | - `cm` (`_lib\lib_campaign_manager.lua`) 332 | 333 | - `core` (`_lib\lib_core.lua` ) 334 | 335 | - `core:add_listener` 添加事件侦听 336 | 337 | - `core:trigger_event` 触发事件 338 | 339 | ### 附:结语 340 | 341 | 希望本文对有需要的mod开发玩家有帮助。善于利用搜索引擎还有文本全局搜索的功能十分重要 342 | 343 | > 例如,假设你需要做一个根据每回合单位的补员状态,给一个阵营叠加一个扣钱的buff或者实践,就可以通过每个回合开始的事件来作为入口 344 | 345 | ``` 346 | 当每个回合开始事件 347 | 查询当前阵营所有军事单位 348 | 筛选正在补员的状态,并计数 349 | 叠加一个自定义的buff,可能需要配合db数据的修改 350 | ``` 351 | 352 | 但我并不知道每个回合开始事件,这个就需要搜索相关的关键字,顺藤摸瓜找到突破口进行开发。 353 | 354 | ### 附:已知的社区开发资料 355 | 356 | - [Total War 3k Lua API document](https://chadvandy.github.io/tw_modding_resources/3k/scripting_doc.html) - [GITHUB](https://github.com/chadvandy/tw_modding_resources) 357 | 358 | 目前最有用的文档,可以在上述文档中查询到大部分官方的API接口, 来自[Hello World! – Total War Modding](https://tw-modding.com/docs/lua-tutorials/) 359 | 360 | 例如,需要修改地区的所属阵营,但并不知道是什么接口,在文档中通过浏览器`ctrl+f`搜索关键字`region`就能找到相关的接口。 361 | 362 | - [Repl.it - Online Lua Editor and IDE - Fast, Powerful, Free](https://repl.it/languages/lua) 363 | 364 | 在线执行lua的工具,如果你像我一样完全没有接触过lua,需要对一些基本语法、基础接口和数据操作进行调试熟悉,可以使用这个页面工具。 365 | 366 | 367 | -------------------------------------------------------------------------------- /mods/tommy_debug/script/all_scripted.lua: -------------------------------------------------------------------------------- 1 | -- lib types for scripting libraries 2 | __lib_type_battle = 0; 3 | __lib_type_campaign = 1; 4 | __lib_type_frontend = 2; 5 | 6 | -- store the starting time of this session 7 | lua_start_time = os.clock(); 8 | 9 | -- gets a timestamp string 10 | function get_timestamp() 11 | return "<" .. string.format("%.1f", os.clock() - lua_start_time) .. "s>"; 12 | end; 13 | 14 | 15 | -- throw a script error 16 | function script_error(msg, stack_level) 17 | local ast_line = "********************"; 18 | 19 | stack_level = stack_level or 0; 20 | 21 | -- do output 22 | print(ast_line); 23 | print("SCRIPT ERROR, timestamp " .. get_timestamp()); 24 | print(msg); 25 | print(""); 26 | print(debug.traceback()); 27 | print(ast_line); 28 | -- assert(false, msg .. "\n" .. debug.traceback()); 29 | 30 | -- logfile output 31 | if __write_output_to_logfile then 32 | local file = io.open(__logfile_path, "a"); 33 | 34 | if file then 35 | file:write(ast_line .. "\n"); 36 | file:write("SCRIPT ERROR, timestamp " .. get_timestamp() .. "\n"); 37 | file:write(msg .. "\n"); 38 | file:write("\n"); 39 | file:write(debug.traceback() .. "\n"); 40 | file:write(ast_line .. "\n"); 41 | file:close(); 42 | end; 43 | end; 44 | end; 45 | 46 | 47 | -- script logging 48 | __write_output_to_logfile = true; 49 | __logfile_path = ""; 50 | 51 | 52 | if __write_output_to_logfile then 53 | -- create the logfile 54 | -- local filename = "script_log_" .. os.date("%d".."".."%m".."".."%y".."_".."%H".."".."%M") .. ".txt"; 55 | local filename = "script_log.txt"; 56 | 57 | _G.logfile_path = filename; 58 | 59 | local file, err_str = io.open(filename, "w"); 60 | 61 | if not file then 62 | __write_output_to_logfile = false; 63 | script_error("ERROR: tried to create logfile with filename " .. filename .. " but operation failed with error: " .. tostring(err_str)); 64 | else 65 | file:write("\n"); 66 | file:write("creating logfile " .. filename .. "\n"); 67 | file:write("\n"); 68 | file:close(); 69 | __logfile_path = _G.logfile_path; 70 | end; 71 | end; 72 | 73 | 74 | -- re-mapping of all output functions so that they support timestamps 75 | function remap_outputs(old_out) 76 | 77 | local out = {}; 78 | local output_functions = {}; 79 | 80 | for key in pairs(old_out) do 81 | table.insert(output_functions, key); 82 | end; 83 | 84 | -- create a tab level record for each output function, and store it at out.tab_levels 85 | local tab_levels = {}; 86 | for i = 1, #output_functions do 87 | tab_levels[output_functions[i]] = 0; 88 | end; 89 | tab_levels["out"] = 0; -- default tab 90 | out.tab_levels = tab_levels; 91 | 92 | -- go through each output function 93 | for i = 1, #output_functions do 94 | local current_func_name = output_functions[i]; 95 | 96 | out[current_func_name] = function(input) 97 | input = input or ""; 98 | 99 | local timestamp = get_timestamp(); 100 | local output_str = timestamp .. string.format("%" .. 11 - string.len(timestamp) .."s", " "); 101 | 102 | -- add in all required tab chars 103 | for i = 1, out["tab_levels"][current_func_name] do 104 | output_str = output_str .. "\t"; 105 | end; 106 | 107 | output_str = output_str .. tostring(input); 108 | 109 | old_out[current_func_name](output_str); 110 | 111 | -- logfile output 112 | if __write_output_to_logfile then 113 | local file = io.open(__logfile_path, "a"); 114 | if file then 115 | file:write("[" .. current_func_name .. "] " .. output_str .. "\n"); 116 | file:close(); 117 | end; 118 | end; 119 | end; 120 | end; 121 | 122 | -- also allow out to be directly called 123 | 124 | setmetatable( 125 | out, 126 | { 127 | __call = function(t, input) 128 | input = input or ""; 129 | 130 | local timestamp = get_timestamp(); 131 | local output_str = timestamp .. string.format("%" .. 11 - string.len(timestamp) .."s", " "); 132 | 133 | -- add in all required tab chars 134 | for i = 1, out.tab_levels["out"] do 135 | output_str = output_str .. "\t"; 136 | end; 137 | 138 | output_str = output_str .. input; 139 | print(output_str); 140 | 141 | -- logfile output 142 | if __write_output_to_logfile then 143 | local file = io.open(__logfile_path, "a"); 144 | if file then 145 | file:write("[out] " .. output_str .. "\n"); 146 | file:close(); 147 | end; 148 | end; 149 | end 150 | } 151 | ); 152 | 153 | -- add on functions inc, dec, cache and restore tab levels 154 | function out.inc_tab(func_name) 155 | func_name = func_name or "out"; 156 | 157 | local current_tab_level = out.tab_levels[func_name]; 158 | 159 | if not current_tab_level then 160 | script_error("ERROR: inc_tab() called but supplied output function name [" .. tostring(func_name) .. "] not recognised"); 161 | return false; 162 | end; 163 | 164 | out.tab_levels[func_name] = current_tab_level + 1; 165 | end; 166 | 167 | function out.dec_tab(func_name) 168 | func_name = func_name or "out"; 169 | 170 | local current_tab_level = out.tab_levels[func_name]; 171 | 172 | if not current_tab_level then 173 | script_error("ERROR: dec_tab() called but supplied output function name [" .. tostring(func_name) .. "] not recognised"); 174 | return false; 175 | end; 176 | 177 | if current_tab_level > 0 then 178 | out.tab_levels[func_name] = current_tab_level - 1; 179 | end; 180 | end; 181 | 182 | function out.cache_tab(func_name) 183 | func_name = func_name or "out"; 184 | 185 | local current_tab_level = out.tab_levels[func_name]; 186 | 187 | if not current_tab_level then 188 | script_error("ERROR: cache_tab() called but supplied output function name [" .. tostring(func_name) .. "] not recognised"); 189 | return false; 190 | end; 191 | 192 | -- store cached tab level elsewhere in the tab_levels table 193 | out.tab_levels["cached_" .. func_name] = current_tab_level; 194 | out.tab_levels[func_name] = 0; 195 | end; 196 | 197 | function out.restore_tab(func_name) 198 | func_name = func_name or "out"; 199 | 200 | local cached_tab_level = out.tab_levels["cached_" .. func_name]; 201 | 202 | if not cached_tab_level then 203 | script_error("ERROR: restore_tab() called but could find no cached tab value for supplied output function name [" .. tostring(func_name) .. "]"); 204 | return false; 205 | end; 206 | 207 | -- restore tab level, and clear the cached value 208 | out.tab_levels[func_name] = cached_tab_level; 209 | out.tab_levels["cached_" .. func_name] = nil; 210 | end; 211 | 212 | return out; 213 | end; 214 | 215 | 216 | -- call the remap function so that timestamped output is available immediately (script in other environments will have to re-call it) 217 | out = remap_outputs(out); 218 | 219 | -- returns a link to the global events table - a copy of this is stored in _G 220 | function get_events() 221 | if _G.events then 222 | return _G.events; 223 | else 224 | local events = require "script.events"; 225 | _G.events = events; 226 | return events; 227 | end; 228 | end; 229 | 230 | 231 | -- forceably clears and then requires a file 232 | function force_require(file) 233 | package.loaded[file] = nil; 234 | require (file); 235 | end; 236 | 237 | 238 | -- set up the random seed 239 | math.randomseed(os.time() + os.clock() * 1000); 240 | math.random(); math.random(); math.random(); math.random(); math.random(); 241 | 242 | 243 | -- function to load the script libraries. The __game_mode is set in battle_scripted.lua/campaign_scripted.lua/frontend_scripted.lua 244 | function load_script_libraries() 245 | -- path to the script folder 246 | package.path = package.path .. ";data/script/_lib/?.lua"; 247 | 248 | __script_libraries_loaded = true; 249 | 250 | -- loads in the script library header file, which queries the __game_mode and loads the appropriate library files 251 | force_require("lib_header"); 252 | end; 253 | 254 | 255 | -- functions to add event callbacks 256 | -- inserts the callback in the events[event] table (the events table being a collection of event tables, each of which contains a list 257 | -- of callbacks to be notified when that event occurs). If a user_defined_list is supplied, then an entry for this event/callback is added 258 | -- to that. This allows areas of the game to clear their listeners out on shutdown (the events table itself is global). 259 | function add_event_callback(event, callback, user_defined_list) 260 | if type(event) ~= "string" then 261 | script_error("ERROR: add_event_callback() called but supplied event [" .. tostring(event) .. "] is not a string"); 262 | return false; 263 | end; 264 | 265 | if type(events[event]) ~= "table" then 266 | events[event] = {}; 267 | end; 268 | 269 | if type(callback) ~= "function" then 270 | script_error("ERROR: add_event_callback() called but supplied callback [" .. tostring(callback) .. "] is not a function"); 271 | return false; 272 | end; 273 | 274 | table.insert(events[event], callback); 275 | 276 | -- if we have been supplied a user-defined table, add this event callback to that 277 | if type(user_defined_list) == "table" then 278 | local user_defined_event = {}; 279 | user_defined_event.event = event; 280 | user_defined_event.callback = callback; 281 | table.insert(user_defined_list, user_defined_event); 282 | end; 283 | end; 284 | 285 | 286 | -- function to clear callbacks in the supplied user defined list from the global events table. This can be called by areas of the game 287 | -- when they shutdown. 288 | function clear_event_callbacks(user_defined_list) 289 | if not type(user_defined_list) == "table" then 290 | script_error("ERROR: clear_event_callbacks() called but supplied user defined list [" .. tostring(user_defined_list) .. "] is not a table"); 291 | return false; 292 | end; 293 | 294 | local count = 0; 295 | 296 | -- for each entry in the supplied user-defined list, look in the relevant event table 297 | -- and try to find a matching callback event. If it's there, remove it. 298 | for i = 1, #user_defined_list do 299 | local current_event_name = user_defined_list[i].event; 300 | local current_event_callback = user_defined_list[i].callback; 301 | 302 | for j = 1, #events[current_event_name] do 303 | if events[current_event_name][j] == current_event_callback then 304 | count = count + 1; 305 | table.remove(events[current_event_name], j); 306 | break; 307 | end; 308 | end; 309 | end; 310 | 311 | -- overwrite the user defined list 312 | user_defined_list = {}; 313 | 314 | return count; 315 | end; 316 | 317 | 318 | -- debug function to print the events table 319 | -- supply a single 'true' argument to print the full table including events that have no listeners (will produce a lot of output) 320 | function print_events_table(full) 321 | full = not not full; 322 | 323 | local ast_line = "**********************"; 324 | print(ast_line); 325 | print("Printing Events Table: " .. tostring(events)); 326 | print(ast_line); 327 | 328 | local count = 0; 329 | 330 | for current_event_name, current_event_table in pairs(events) do 331 | if current_event_name ~= "_NAME" and current_event_name ~= "_PACKAGE" then 332 | local sizeof_current_event_table = #current_event_table; 333 | if full or sizeof_current_event_table > 0 then 334 | if sizeof_current_event_table == 1 then 335 | print("\tevent " .. current_event_name .. " [" .. tostring(current_event_table) .. "] contains 1 event"); 336 | else 337 | print("\tevent " .. current_event_name .. " [" .. tostring(current_event_table) .. "] contains " .. sizeof_current_event_table .. " events"); 338 | end; 339 | 340 | count = count + 1; 341 | 342 | for i = 1, sizeof_current_event_table do 343 | print("\t\t" .. tostring(current_event_table[i])); 344 | end; 345 | end; 346 | end; 347 | end; 348 | print(ast_line); 349 | if count == 1 then 350 | print("Listed 1 event"); 351 | else 352 | print("Listed " .. count .. " events"); 353 | end; 354 | print(ast_line); 355 | end; 356 | 357 | 358 | events = get_events(); 359 | -------------------------------------------------------------------------------- /mods_common/randomized_start_military_force/script/campaign/mod/wh2_tommy_randomized_start.lua: -------------------------------------------------------------------------------- 1 | out("wh2_tommy_randomized_start.lua ****************"); 2 | 3 | package.path = package.path ..';../lib/?.lua'; 4 | 5 | local randomized_start = require('common_tommy_randomized_start') 6 | 7 | local _debug = require('common_tommy_debug_try_catch') 8 | 9 | local function _get_primary_military_force_position(faction) 10 | local targ_x = nil; 11 | local targ_y = nil; 12 | local targ_region = nil; 13 | if (faction:has_faction_leader() and faction:faction_leader():has_military_force()) then 14 | out("tommy_get_primary_military_force_position | has_faction_leader and faction_leader:has_military_force"); 15 | targ_x = faction:faction_leader():display_position_x(); 16 | targ_y = faction:faction_leader():display_position_y(); 17 | targ_region = faction:faction_leader():region(); 18 | else 19 | local mf_list_item_0 = faction:military_force_list():item_at(0); 20 | if mf_list_item_0:has_general() then 21 | out("tommy_get_primary_military_force_position | mf_list_item_0:has_general"); 22 | local general = mf_list_item_0:general_character(); 23 | targ_x = general:display_position_x(); 24 | targ_y = general:display_position_y(); 25 | targ_region = general:region(); 26 | elseif (faction:has_capital_region()) then 27 | out("tommy_get_primary_military_force_position | has_capital_region"); 28 | local capital_sttlement = faction:capital_region():settlement(); 29 | targ_x = capital_sttlement:display_position_x(); 30 | targ_y = capital_sttlement:display_position_y(); 31 | targ_region = faction:capital_region(); 32 | end 33 | end 34 | out("tommy_get_primary_military_force_position | return " .. targ_x .. "," .. targ_y .. "," .. 35 | randomized_start.get_bool_str(not is_nil(targ_region))); 36 | return targ_x, targ_y, targ_region; 37 | end 38 | 39 | local function cm_query_model() 40 | return cm:model() 41 | end 42 | 43 | local function cm_local_faction_name() 44 | return cm:get_local_faction(true) 45 | end 46 | 47 | function _apply_religion_to_faction(faction) 48 | --out("ROY | faction: "..tostring(faction:name())); 49 | local religion = faction:state_religion(); 50 | --out("ROY | State Religion: "..tostring(religion)); 51 | 52 | local regionList = faction:region_list(); 53 | 54 | for r = 0, regionList:num_items() - 1 do 55 | local region = regionList:item_at(r); 56 | --out("ROY | Region: "..tostring(region)); 57 | 58 | local effect_bundle = {}; 59 | 60 | if religion == "wh2_main_religion_skaven" then 61 | --out("ROY | skaven yesyes: "..tostring(region:religion_proportion("wh2_main_religion_skaven"))); 62 | effect_bundle = "roy_effect_bundle_religion_skaven"; 63 | elseif religion == "wh_main_religion_undeath" then 64 | --out("ROY | twilight: "..tostring(region:religion_proportion("wh_main_religion_undeath"))); 65 | effect_bundle = "roy_effect_bundle_religion_undeath"; 66 | elseif religion == "wh_main_religion_chaos" then 67 | --out("ROY | breakbeat chaos: "..tostring(region:religion_proportion("wh_main_religion_chaos"))); 68 | effect_bundle = "roy_effect_bundle_religion_chaos"; 69 | elseif religion == "wh_main_religion_untainted" then 70 | --out("ROY | untainted: "..tostring(region:religion_proportion("wh_main_religion_untainted"))); 71 | effect_bundle = "roy_effect_bundle_religion_untainted"; 72 | end; 73 | 74 | --out("ROY | apply religion: "..tostring(effect_bundle)); 75 | cm:apply_effect_bundle_to_region(effect_bundle, region:name(), 5); 76 | end; 77 | end; 78 | 79 | function _repair_settlements_for_faction(faction) 80 | --out("ROY | Roy_Repair_Settlements_For_Faction | START of function "..tostring(faction:name())); 81 | local regionList = faction:region_list(); 82 | 83 | for i = 0, regionList:num_items() -1 do 84 | local region = regionList:item_at(i); 85 | local region_Name = region:name(); 86 | local settlement = region:settlement(); 87 | 88 | local chain = region:slot_list():item_at(0):building():chain(); 89 | out("ROY | CHAIN: "..tostring(chain)); 90 | 91 | local targetBuilding = building_Upgrades[chain]; 92 | out("ROY | TARGET BUILDING: "..tostring(targetBuilding)); 93 | 94 | --cm:instantly_upgrade_building_in_region(region_Name, 0, targetBuilding); 95 | cm:region_slot_instantly_upgrade_building(settlement:primary_slot(), targetBuilding); 96 | local newBuildingName = region:slot_list():item_at(0):building():name(); 97 | --out("ROY | NEW BUILDING NAME: "..tostring(newBuildingName)); 98 | 99 | local possiblePortSlot = region:slot_list():item_at(1); 100 | --out("ROY | possiblePortSlot: "..tostring(possiblePortSlot)); 101 | 102 | ----------------- Check if port is in the settlement (excluding fortress_gates) --------------------- 103 | if (not string.match(newBuildingName, "fortress_gate")) and (not string.match(newBuildingName, "empire_fort")) then 104 | if possiblePortSlot:has_building() then 105 | local building = possiblePortSlot:building(); 106 | --out("ROY | POSSIBLE PORT BUILDING NAME: "..tostring(building:name())); 107 | if string.match(building:name(), "port") and string.match(building:name(), "ruin") then 108 | chain = building:chain(); 109 | out("ROY | PORT CHAIN: "..tostring(chain)); 110 | 111 | targetBuilding = port_Upgrades[chain]; 112 | out("ROY | TARGET PORT: "..tostring(targetBuilding)); 113 | 114 | --cm:instantly_upgrade_building_in_region(region_Name, 1, targetBuilding); 115 | cm:region_slot_instantly_upgrade_building(settlement:port_slot(), targetBuilding); 116 | end; 117 | end; 118 | end; 119 | 120 | cm:heal_garrison(cm:get_region(region_Name):cqi()); 121 | end; 122 | --out("ROY | Roy_Repair_Settlements_For_Faction | END of function "); 123 | end; 124 | 125 | local function randomizer(context, faction, params) 126 | out("wh2_tommy_randomized | randomizer | START of script | (" .. faction:name() .. ")"); 127 | -- local character_list = faction:character_list(); 128 | local region_list = faction:region_list(); 129 | local region_list_world = cm_query_model():world():region_manager():region_list(); 130 | local is_faction_local_me = randomized_start.is_faction_local_human(context, faction, params); 131 | local mforce_list = faction:military_force_list(); 132 | 133 | local region_list_normal = randomized_start.list_ca_to_mormal_table(region_list); 134 | local region_list_world_normal = randomized_start.list_ca_to_mormal_table(region_list_world); 135 | local _, __, region_primary = _get_primary_military_force_position(faction); 136 | -- local region_list_adjacent_normal = randomized_start.list_ca_to_mormal_table(region_primary:adjacent_region_list()) 137 | 138 | -- local region_name_list = {}; 139 | 140 | local is_faction_has_no_region = region_list:num_items() == 0; 141 | local is_faction_has_1_region = region_list:num_items() == 1; 142 | 143 | if (faction:has_faction_leader()) then faction_leader = faction:faction_leader(); end 144 | 145 | for i = 0, mforce_list:num_items() - 1 do 146 | local force = mforce_list:item_at(i); 147 | if force:is_armed_citizenry() == false and force:has_general() == true then 148 | local general = force:general_character(); 149 | out("wh2_tommy_randomized | randomizer | " .. randomized_start.get_character_infomation_readable(general)); 150 | -- 挑选不在建筑物的角色(例如在城市或者农庄铁矿), 在建筑物的角色状态和野战可能有差别,需要特殊处理 151 | -- 此处本作者认为不需要处理这些角色, 让他们呆在原地就行 152 | if not general:in_settlement() and not general:in_port() then 153 | -- local region_name_list_patched = {unpack(region_name_list)}; -- 复制一份list数据 154 | -- local region_name_list_patched_length = #region_name_list_patched; 155 | -- local general_region = general:region() 156 | -- region_name_list_patched[region_name_list_patched_length + 1] = general:region():name(); 157 | local final_x = general:logical_position_x(); 158 | local final_y = general:logical_position_y(); 159 | final_x = final_x + randomized_start.random_split_2(0.66, 3.3); 160 | final_y = final_y + randomized_start.random_split_2(0.66, 3.3); 161 | out("wh2_tommy_randomized | randomizer | before teleport" .. 162 | " found_position:" .. final_x .. "," .. final_y); 163 | -- cm:teleport_character(general, final_x, final_y); 164 | cm:teleport_to(cm:char_lookup_str(general), final_x, final_y); 165 | end 166 | end 167 | end 168 | 169 | if is_faction_local_me then 170 | -- local faction_handle = cm:modify_faction(faction:name()); 171 | local faction_name = faction:name() 172 | local key_event_handle = "3k_tommy_randomized_start_reposition_camera"; 173 | if (cm:is_multiplayer()) then 174 | -- TODO: scroll_camera_from_current for multiplayer mode 175 | else 176 | out("wh2_tommy_randomized | randomizer | add_listener ScriptEventCampaignCutsceneCompleted"); 177 | core:add_listener(key_event_handle, "ScriptEventCampaignCutsceneCompleted", true, function(e) 178 | out("wh2_tommy_randomized | randomizer | on ScriptEventCampaignCutsceneCompleted"); 179 | _debug:try{ 180 | function() 181 | if (cm_query_model():turn_number() > 1) then 182 | core:remove_listener(key_event_handle); 183 | return false; 184 | end 185 | core:remove_listener(key_event_handle); 186 | local x_primary, y_primary, region_primary = _get_primary_military_force_position(faction); 187 | out( 188 | "wh2_tommy_randomized | randomizer | reposition camera | tommy_get_primary_military_force_position() " .. 189 | _get_bool_str(not is_nil(region_primary))); 190 | if (is_nil(region_primary)) then return false; end 191 | -- 让主要目标所在的区域可以显示 192 | cm:make_region_seen_in_shroud(faction_name, region_primary:name()); 193 | cm:make_region_visible_in_shroud(faction_name, region_primary:name()); 194 | -- for i = 0, region_list_world:num_items() - 1 do end 195 | local x, y, d, b, h = cm:get_camera_position(); 196 | cm:callback(function() 197 | out("wh2_tommy_randomized | randomizer | reposition camera | x_faction_leader:" .. x_primary .. 198 | ", y_faction_leader:" .. y_primary .. ", at:" .. region_primary:name()); 199 | cm:scroll_camera_from_current(1.5, nil, {x_primary, y_primary, d, b, h}); 200 | -- cm:set_camera_position(x_primary, y_primary, d, b, h); 201 | end, 1); 202 | 203 | -- if (region_primary:is_abandoned()) and is_faction_has_no_region then 204 | -- 如果初始区域是一个废弃区域就把这个区域设置给玩家 205 | -- cm:modify_model():get_modify_region(region_primary):settlement_gifted_as_if_by_payload(faction_handle); 206 | -- end 207 | return true; 208 | end, 209 | _debug:catch{function(error) script_error('3k_tommy_randomized_start.lua | CAUGHT ERROR: ' .. error); end} 210 | } 211 | end, true); 212 | end 213 | end 214 | 215 | cm:callback(function() 216 | _debug:try{ 217 | function() 218 | if (is_faction_has_no_region) then 219 | -- local faction_handle = cm:modify_faction(faction:name()); 220 | local faction_name = faction:name() 221 | local _, __, region_primary = tommy_get_primary_military_force_position(faction); 222 | out("wh2_tommy_randomized | randomizer | timeout:1 | faction:" .. faction:name() .. 223 | " final_region_primary:" .. region_primary:name() .. " subclture:" .. faction:subculture()) 224 | if (region_primary:is_abandoned()) and is_faction_local_me then 225 | -- 如果初始区域是一个废弃区域就把这个区域设置给玩家/AI 226 | -- cm:modify_model():get_modify_region(region_primary):settlement_gifted_as_if_by_payload(faction_handle); 227 | cm:transfer_region_to_faction(region_primary:name(), faction_name); 228 | _repair_settlements_for_faction(faction_name) 229 | _apply_religion_to_faction(faction_name) 230 | end 231 | -- enable_region_tech_for_subculture_bandits(faction, faction_handle, region_primary); 232 | end 233 | end, _debug:catch{function(error) script_error('3k_tommy_randomized_start.lua | CAUGHT ERROR: ' .. error); end} 234 | } 235 | end, 0.2); 236 | 237 | out("wh2_tommy_randomized | randomizer | END of script "); 238 | end 239 | 240 | local params = {} 241 | 242 | params.randomizer = randomizer 243 | params.cm_query_model = cm_query_model 244 | params.cm_local_faction_name = cm_local_faction_name 245 | 246 | cm:add_first_tick_callback(function(context) randomized_start.run(context, params) end); 247 | -------------------------------------------------------------------------------- /mods_common/wh2_tommy_debug/script/all_scripted.lua: -------------------------------------------------------------------------------- 1 | 2 | 3 | --- @loaded_in_battle 4 | --- @loaded_in_frontend 5 | --- @loaded_in_campaign 6 | 7 | 8 | 9 | 10 | -- lib types for scripting libraries 11 | __lib_type_battle = 0; 12 | __lib_type_campaign = 1; 13 | __lib_type_frontend = 2; 14 | __lib_type_autotest = 3; 15 | 16 | -- store the starting time of this session 17 | lua_start_time = os.clock(); 18 | 19 | -- gets a timestamp string 20 | function get_timestamp() 21 | return "<" .. string.format("%.1f", os.clock() - lua_start_time) .. "s>"; 22 | end; 23 | 24 | 25 | 26 | 27 | 28 | ---------------------------------------------------------------------------- 29 | --- @section Script Errors 30 | ---------------------------------------------------------------------------- 31 | 32 | 33 | --- @function script_error 34 | --- @desc Throws a script error with the supplied message, printing the lua callstack to the Lua console output spool. Useful for debugging. 35 | --- @p string message 36 | function script_error(msg) 37 | local ast_line = "********************"; 38 | 39 | -- do output 40 | print(ast_line); 41 | print("SCRIPT ERROR, timestamp " .. get_timestamp()); 42 | print(msg); 43 | print(""); 44 | print(debug.traceback("", 2)); 45 | print(ast_line); 46 | -- assert(false, msg .. "\n" .. debug.traceback()); 47 | 48 | -- logfile output 49 | if __write_output_to_logfile then 50 | local file = io.open(__logfile_path, "a"); 51 | 52 | if file then 53 | file:write(ast_line .. "\n"); 54 | file:write("SCRIPT ERROR, timestamp " .. get_timestamp() .. "\n"); 55 | file:write(msg .. "\n"); 56 | file:write("\n"); 57 | file:write(debug.traceback("", 2) .. "\n"); 58 | file:write(ast_line .. "\n"); 59 | file:close(); 60 | end; 61 | end; 62 | end; 63 | 64 | 65 | 66 | 67 | 68 | 69 | -- script logging 70 | 71 | do 72 | -- Make a file called "enable_console_logging" in the root of the script folder to enable console logging 73 | local file_str = effect.filesystem_lookup("/script/", "enable_console_logging"); 74 | __write_output_to_logfile = (file_str ~= ""); 75 | end; 76 | 77 | -- __write_output_to_logfile=true 78 | __logfile_path = ""; 79 | 80 | 81 | if __write_output_to_logfile then 82 | 83 | print("*** script logging enabled ***"); 84 | 85 | -- create the logfile 86 | -- local filename = "script_log_" .. os.date("%d".."".."%m".."".."%y".."_".."%H".."".."%M") .. ".txt"; 87 | local filename = "script_log.txt"; 88 | 89 | _G.logfile_path = filename; 90 | 91 | 92 | local file, err_str = io.open(filename, "w"); 93 | 94 | if not file then 95 | __write_output_to_logfile = false; 96 | script_error("ERROR: tried to create logfile with filename " .. filename .. " but operation failed with error: " .. tostring(err_str)); 97 | else 98 | file:write("\n"); 99 | file:write("creating logfile " .. filename .. "\n"); 100 | file:write("\n"); 101 | file:close(); 102 | __logfile_path = _G.logfile_path; 103 | end; 104 | end; 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | ---------------------------------------------------------------------------- 116 | --- @section Output 117 | ---------------------------------------------------------------------------- 118 | 119 | 120 | --- @function out 121 | --- @desc out is a table that provides multiple methods for outputting text to the various available debug console spools. It may be called as a function to output a string to the main Lua console spool, but the following table elements within it may also be called to output to different output spools: 122 | --- @desc
  • grudges
  • 123 | --- @desc
  • ui
  • 124 | --- @desc
  • chaos
  • 125 | --- @desc
  • traits
  • 126 | --- @desc
  • help_pages
  • 127 | --- @desc
  • interventions
  • 128 | --- @desc
  • invasions
  • 129 | --- @desc
  • design
  • 130 | --- @desc 131 | --- @desc out supplies four additional functions that can be used to show tab characters at the start of lines of output: 132 | --- @desc
    FunctionDescription
    out.inc_tabIncrements the number of tab characters shown at the start of the line by one.
    out.dec_tabDecrements the number of tab characters shown at the start of the line by one. Decrementing below zero has no effect.
    out.cache_tabCaches the number of tab characters currently set to be shown at the start of the line.
    out.restore_tabRestores the number of tab characters shown at the start of the line to that previously cached.
    133 | --- @desc Tab levels are managed per output spool. To each of these functions a string argument can be supplied which sets the name of the output spool to apply the modification to. Supply no argument or a blank string to modify the tab level of the main output spool. 134 | --- @p string output 135 | --- @new_example Standard output 136 | --- @example out("Hello World") 137 | --- @example out.inc_tab() 138 | --- @example out("indented") 139 | --- @example out.dec_tab() 140 | --- @example out("no longer indented") 141 | --- @result Hello World 142 | --- @result indented 143 | --- @result no longer indented 144 | --- @new_example UI tab 145 | --- @desc Output to the ui tab, with caching and restoring of tab levels 146 | --- @example out.ui("Hello UI tab") 147 | --- @example out.cache_tab("ui") 148 | --- @example out.inc_tab("ui") 149 | --- @example out.inc_tab("ui") 150 | --- @example out.inc_tab("ui") 151 | --- @example out.ui("very indented") 152 | --- @example out.restore_tab("ui") 153 | --- @example out.ui("not indented any more") 154 | --- @result Hello UI tab 155 | --- @result very indented 156 | --- @result not indented any more 157 | 158 | 159 | -- this function re-maps all output functions so that they support timestamps 160 | function remap_outputs(out_impl, suppress_new_session_output) 161 | 162 | -- Do not proceed if out_impl already has a metatable. This can happen if we're running in autotest mode and the game scripts have preconfigured the out table 163 | if getmetatable(out_impl) then 164 | return out_impl; 165 | end; 166 | 167 | -- construct a table to return 168 | local out = {}; 169 | 170 | -- construct an indexed list of output functions 171 | local output_functions = {}; 172 | for key in pairs(out_impl) do 173 | table.insert(output_functions, key); 174 | end; 175 | 176 | -- sort the indexed list (just for output purposes) 177 | table.sort(output_functions); 178 | 179 | -- create a tab level record for each output function, and store it at out.tab_levels 180 | local tab_levels = {}; 181 | for i = 1, #output_functions do 182 | tab_levels[output_functions[i]] = 0; 183 | end; 184 | tab_levels["out"] = 0; -- default tab 185 | out.tab_levels = tab_levels; 186 | 187 | local svr = ScriptedValueRegistry:new(); 188 | local game_uptime = os.clock(); 189 | 190 | -- map each output function 191 | for i = 1, #output_functions do 192 | local current_func_name = output_functions[i]; 193 | 194 | out[current_func_name] = function(str_from_script) 195 | str_from_script = tostring(str_from_script) or ""; 196 | 197 | -- get the current time at point of output 198 | local timestamp = get_timestamp(); 199 | 200 | -- we construct our output string as a table - the first two entries are the timestamp and some whitespace 201 | local output_str_table = {timestamp, string.format("%" .. 11 - string.len(timestamp) .."s", " ")}; 202 | 203 | -- add in all required tab chars 204 | for i = 1, out["tab_levels"][current_func_name] do 205 | table.insert(output_str_table, "\t"); 206 | end; 207 | 208 | -- finally add the intended output 209 | table.insert(output_str_table, str_from_script); 210 | 211 | -- turn the table of strings into a string 212 | local output_str = table.concat(output_str_table); 213 | 214 | -- print the output 215 | out_impl[current_func_name](output_str); 216 | 217 | -- log that this output tab has been touched 218 | svr:SavePersistentBool("out." .. current_func_name .. "_touched", true); 219 | 220 | -- logfile output 221 | if __write_output_to_logfile then 222 | local file = io.open(__logfile_path, "a"); 223 | if file then 224 | file:write("[" .. current_func_name .. "] " .. output_str .. "\n"); 225 | file:close(); 226 | end; 227 | end; 228 | end; 229 | 230 | -- if this tab has been touched in a previous session then write some new lines to it to differentiate this session's output 231 | if not suppress_new_session_output then 232 | if svr:LoadPersistentBool("out." .. current_func_name .. "_touched") then 233 | for i = 1, 10 do 234 | out_impl[current_func_name](""); 235 | end; 236 | out_impl[current_func_name]("* NEW SESSION, current game uptime: " .. game_uptime .. "s *"); 237 | out_impl[current_func_name](""); 238 | end; 239 | end; 240 | end; 241 | 242 | -- also allow out to be directly called 243 | setmetatable( 244 | out, 245 | { 246 | __call = function(t, str_from_script) 247 | str_from_script = tostring(str_from_script) or ""; 248 | 249 | -- get the current time at point of output 250 | local timestamp = get_timestamp(); 251 | 252 | -- we construct our output string as a table - the first two entries are the timestamp and some whitespace 253 | local output_str_table = {timestamp, string.format("%" .. 11 - string.len(timestamp) .."s", " ")}; 254 | 255 | -- add in all required tab chars 256 | for i = 1, out.tab_levels["out"] do 257 | table.insert(output_str_table, "\t"); 258 | end; 259 | 260 | -- finally add the intended output 261 | table.insert(output_str_table, str_from_script); 262 | 263 | -- turn the table of strings into a string 264 | local output_str = table.concat(output_str_table); 265 | 266 | -- print the output 267 | print(output_str); 268 | 269 | -- log that this output tab has been touched 270 | svr:SavePersistentBool("out_touched", true); 271 | 272 | -- logfile output 273 | if __write_output_to_logfile then 274 | local file = io.open(__logfile_path, "a"); 275 | if file then 276 | file:write("[out] " .. output_str .. "\n"); 277 | file:close(); 278 | end; 279 | end; 280 | end 281 | } 282 | ); 283 | 284 | -- if the main output tab has been touched in a previous session then write some new lines to it to differentiate this session's output 285 | if not suppress_new_session_output then 286 | for i = 1, 10 do 287 | print(""); 288 | end; 289 | 290 | print("* NEW SESSION, current game uptime: " .. game_uptime .. "s *"); 291 | 292 | if not svr:LoadPersistentBool("out_touched") then 293 | print(" available output spools:"); 294 | print("\tout"); 295 | for j = 1, #output_functions do 296 | print("\tout." .. output_functions[j]); 297 | end; 298 | print(""); 299 | print(""); 300 | end; 301 | 302 | print(""); 303 | end; 304 | 305 | -- add on functions inc, dec, cache and restore tab levels 306 | function out.inc_tab(func_name) 307 | func_name = func_name or "out"; 308 | 309 | local current_tab_level = out.tab_levels[func_name]; 310 | 311 | if not current_tab_level then 312 | script_error("ERROR: inc_tab() called but supplied output function name [" .. tostring(func_name) .. "] not recognised"); 313 | return false; 314 | end; 315 | 316 | out.tab_levels[func_name] = current_tab_level + 1; 317 | end; 318 | 319 | function out.dec_tab(func_name) 320 | func_name = func_name or "out"; 321 | 322 | local current_tab_level = out.tab_levels[func_name]; 323 | 324 | if not current_tab_level then 325 | script_error("ERROR: dec_tab() called but supplied output function name [" .. tostring(func_name) .. "] not recognised"); 326 | return false; 327 | end; 328 | 329 | if current_tab_level > 0 then 330 | out.tab_levels[func_name] = current_tab_level - 1; 331 | end; 332 | end; 333 | 334 | function out.cache_tab(func_name) 335 | func_name = func_name or "out"; 336 | 337 | local current_tab_level = out.tab_levels[func_name]; 338 | 339 | if not current_tab_level then 340 | script_error("ERROR: cache_tab() called but supplied output function name [" .. tostring(func_name) .. "] not recognised"); 341 | return false; 342 | end; 343 | 344 | -- store cached tab level elsewhere in the tab_levels table 345 | out.tab_levels["cached_" .. func_name] = current_tab_level; 346 | out.tab_levels[func_name] = 0; 347 | end; 348 | 349 | function out.restore_tab(func_name) 350 | func_name = func_name or "out"; 351 | 352 | local cached_tab_level = out.tab_levels["cached_" .. func_name]; 353 | 354 | if not cached_tab_level then 355 | script_error("ERROR: restore_tab() called but could find no cached tab value for supplied output function name [" .. tostring(func_name) .. "]"); 356 | return false; 357 | end; 358 | 359 | -- restore tab level, and clear the cached value 360 | out.tab_levels[func_name] = cached_tab_level; 361 | out.tab_levels["cached_" .. func_name] = nil; 362 | end; 363 | 364 | return out; 365 | end; 366 | 367 | 368 | -- call the remap function so that timestamped output is available immediately (script in other environments will have to re-call it) 369 | out = remap_outputs(out, __is_autotest); 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | -- set up the lua random seed 380 | -- use script-generated random numbers sparingly - it's always better to ask the game for a random number 381 | math.randomseed(os.time() + os.clock() * 1000); 382 | math.random(); math.random(); math.random(); math.random(); math.random(); 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | ---------------------------------------------------------------------------- 394 | --- @section Loading Script Libraries 395 | ---------------------------------------------------------------------------- 396 | 397 | 398 | --- @function force_require 399 | --- @desc Forceably unloads and requires a file by name. 400 | --- @p string filename 401 | function force_require(file) 402 | package.loaded[file] = nil; 403 | return require(file); 404 | end; 405 | 406 | 407 | --- @function load_script_libraries 408 | --- @desc One-shot function to load the script libraries. 409 | function load_script_libraries() 410 | -- path to the script folder 411 | package.path = package.path .. ";data/script/_lib/?.lua"; 412 | 413 | -- loads in the script library header file, which queries the __game_mode and loads the appropriate library files 414 | -- __game_mode is set in battle_scripted.lua/campaign_scripted.lua/frontend_scripted.lua 415 | force_require("lib_header"); 416 | end; 417 | 418 | 419 | 420 | 421 | -- functions to add event callbacks 422 | -- inserts the callback in the events[event] table (the events table being a collection of event tables, each of which contains a list 423 | -- of callbacks to be notified when that event occurs). If a user_defined_list is supplied, then an entry for this event/callback is added 424 | -- to that. This allows areas of the game to clear their listeners out on shutdown (the events table itself is global). 425 | function add_event_callback(event, callback, user_defined_list) 426 | 427 | if type(event) ~= "string" then 428 | script_error("ERROR: add_event_callback() called but supplied event [" .. tostring(event) .. "] is not a string"); 429 | return false; 430 | end; 431 | 432 | if type(events[event]) ~= "table" then 433 | events[event] = {}; 434 | end; 435 | 436 | if type(callback) ~= "function" then 437 | script_error("ERROR: add_event_callback() called but supplied callback [" .. tostring(callback) .. "] is not a function"); 438 | return false; 439 | end; 440 | 441 | table.insert(events[event], callback); 442 | 443 | -- if we have been supplied a user-defined table, add this event callback to that 444 | if type(user_defined_list) == "table" then 445 | local user_defined_event = {}; 446 | user_defined_event.event = event; 447 | user_defined_event.callback = callback; 448 | table.insert(user_defined_list, user_defined_event); 449 | end; 450 | end; 451 | 452 | 453 | -- function to clear callbacks in the supplied user defined list from the global events table. This can be called by areas of the game 454 | -- when they shutdown. 455 | function clear_event_callbacks(user_defined_list) 456 | if not type(user_defined_list) == "table" then 457 | script_error("ERROR: clear_event_callbacks() called but supplied user defined list [" .. tostring(user_defined_list) .. "] is not a table"); 458 | return false; 459 | end; 460 | 461 | local count = 0; 462 | 463 | -- for each entry in the supplied user-defined list, look in the relevant event table 464 | -- and try to find a matching callback event. If it's there, remove it. 465 | for i = 1, #user_defined_list do 466 | local current_event_name = user_defined_list[i].event; 467 | local current_event_callback = user_defined_list[i].callback; 468 | 469 | for j = 1, #events[current_event_name] do 470 | if events[current_event_name][j] == current_event_callback then 471 | count = count + 1; 472 | table.remove(events[current_event_name], j); 473 | break; 474 | end; 475 | end; 476 | end; 477 | 478 | -- overwrite the user defined list 479 | user_defined_list = {}; 480 | 481 | return count; 482 | end; 483 | 484 | 485 | 486 | events = force_require("script.events"); 487 | 488 | 489 | -------------------------------------------------------------------------------- /mods/tommy_randomized_start/script/campaign/mod/3k_tommy_randomized_start.lua: -------------------------------------------------------------------------------- 1 | out("3k_tommy_randomized_start.lua ****************"); 2 | 3 | -- local events = get_events(); 4 | 5 | local REGION_DONT_SPAWN_TOO_MUCH = { 6 | ["3k_main_bohai_capital"] = true, 7 | ["3k_main_dongjun_resource_1"] = true, 8 | ["3k_main_penchang_resource_1"] = true, 9 | ["3k_main_penchang_resource_2"] = true, 10 | -- 宝岛台湾 11 | ["3k_main_yizhou_resource_2"] = true, 12 | ["3k_main_yizhou_resource_1"] = true, 13 | ["3k_main_yizhou_island_capital"] = true, 14 | -- 宝岛海南 15 | ["3k_main_hepu_capital"] = true, 16 | ["3k_main_hepu_resource_1"] = true 17 | } 18 | 19 | local MAPPINT_REGION_NAME_TO_BANDIT_NETWORK_TECH_PREFIX = { 20 | ["north"] = "bing_outer", 21 | ["shangdang"] = "bing", 22 | ["shoufang"] = "bing", 23 | ["taiyuan"] = "bing", 24 | ["xihe"] = "bing", 25 | ["yanmen"] = "bing", 26 | ["anping"] = "ji", 27 | ["weijun"] = "ji", 28 | ["zhongshan"] = "ji", 29 | ["cangwu"] = "jiaozhi", 30 | ["gaoliang"] = "jiaozhi", 31 | ["hepu"] = "jiaozhi", 32 | ["jiaozhi"] = "jiaozhi", 33 | ["nanhai"] = "jiaozhi", 34 | ["yulin"] = "jiaozhi", 35 | ["changsha"] = "jing", 36 | ["jiangxia"] = "jing", 37 | ["jingzhou"] = "jing", 38 | ["lingling"] = "jing", 39 | ["nanyang"] = "jing", 40 | ["runan"] = "jing", 41 | ["wuling"] = "jing", 42 | ["xiangyang"] = "jing", 43 | ["anding"] = "liang", 44 | ["jincheng"] = "liang", 45 | ["west"] = "liang_outer", 46 | ["wudu"] = "liang", 47 | ["wuwei"] = "liang", 48 | ["beihai"] = "qing", 49 | ["donglai"] = "qing", 50 | ["pingyuan"] = "qing", 51 | ["taishan"] = "qing", 52 | ["changan"] = "sili", 53 | ["hedong"] = "sili", 54 | ["luoyang"] = "sili", 55 | ["donghai"] = "xu", 56 | ["guangling"] = "xu", 57 | ["langye"] = "xu", 58 | ["penchang"] = "xu", 59 | ["dongjun"] = "yan", 60 | ["henei"] = "yan", 61 | ["yingchuan"] = "yan", 62 | ["dongou"] = "yang", 63 | ["jianan"] = "yang", 64 | ["jianye"] = "yang", 65 | ["kuaiji"] = "yang", 66 | ["lujiang"] = "yang", 67 | ["luling"] = "yang", 68 | ["poyang"] = "yang", 69 | ["tongan"] = "yang", 70 | ["xindu"] = "yang", 71 | ["yangzhou"] = "yang", 72 | ["ye"] = "yang", 73 | ["yuzhang"] = "yang", 74 | ["badong"] = "yi", 75 | ["bajun"] = "yi", 76 | ["baxi"] = "yi", 77 | ["chengdu"] = "yi", 78 | ["fuling"] = "yi", 79 | ["hanzhong"] = "yi", 80 | ["jiangyang"] = "yi", 81 | ["jianning"] = "yi", 82 | ["shangyong"] = "yi", 83 | ["yizhou"] = "yi", 84 | ["zangke"] = "yi", 85 | ["bohai"] = "you", 86 | ["daijun"] = "you", 87 | ["east"] = "you_outer", 88 | ["north"] = "you_outer", 89 | ["youbeiping"] = "you", 90 | ["youzhou"] = "you", 91 | ["yu"] = "you", 92 | ["chenjun"] = "yu" 93 | } 94 | 95 | local _debug = {is_debug = true} 96 | 97 | function _debug:catch(what) return what[1] end 98 | 99 | function _debug:try(what) 100 | if (not _debug.is_debug) then return what[2](result); end 101 | status, result = pcall(what[1]) 102 | if not status then what[2](result) end 103 | return result 104 | end 105 | 106 | local random_call_offset = 0; 107 | local function better_math_randomseed() 108 | random_call_offset = random_call_offset + 1; 109 | math.randomseed(tonumber(tostring(6 * random_call_offset + os.time() * 2 * random_call_offset):reverse():sub(1, 8))); 110 | if (random_call_offset > 100) then random_call_offset = 0; end 111 | end 112 | 113 | local function random_split_2(min, range) 114 | local rangeToRand = (min + range); 115 | better_math_randomseed(); 116 | local randIntl = (math.random() * rangeToRand * 2) - (rangeToRand); 117 | local edgeLeftIntl = ((randIntl / math.abs(randIntl)) * min); 118 | return edgeLeftIntl + (randIntl - edgeLeftIntl); 119 | end 120 | 121 | local function tommy_is_faction_local_human(context, faction) 122 | local model = context:query_model(); 123 | return faction:is_human() and faction:name() == model:local_faction():name(); 124 | end 125 | 126 | local function tommy_get_character_name_readable(character) 127 | local str_name_localised_string = effect.get_localised_string(character:get_surname()); 128 | local str_forename_localised_string = effect.get_localised_string(character:get_forename()); 129 | return ("" .. str_name_localised_string .. "" .. str_forename_localised_string .. ""); 130 | end 131 | 132 | local function _get_bool_str(boo) return (boo and "true" or "false"); end 133 | 134 | -- get character info mation readable 135 | local function tommy_get_character_infomation_readable(character) 136 | return ("character:" .. tommy_get_character_name_readable(character) .. ", faction[" .. character:faction():name() .. 137 | "], logical pos[" .. character:logical_position_x() .. ", " .. character:logical_position_y() .. 138 | "], display pos[" .. character:display_position_x() .. ", " .. character:display_position_x() .. 139 | "], has_region:" .. _get_bool_str(character:has_region()) .. ", in_settlement:" .. 140 | _get_bool_str(character:in_settlement())); 141 | end 142 | 143 | local function tommy_list_ca_to_mormal_table(list_ca) 144 | local result = {} 145 | for i = 0, list_ca:num_items() - 1 do result[i + 1] = list_ca:item_at(i); end 146 | return result; 147 | end 148 | 149 | -- get primary military position of faction 150 | local function tommy_get_primary_military_force_position(faction) 151 | local targ_x = nil; 152 | local targ_y = nil; 153 | local targ_region = nil; 154 | if (faction:has_faction_leader() and faction:faction_leader():has_military_force()) then 155 | out("tommy_get_primary_military_force_position | has_faction_leader and faction_leader:has_military_force"); 156 | targ_x = faction:faction_leader():display_position_x(); 157 | targ_y = faction:faction_leader():display_position_y(); 158 | targ_region = faction:faction_leader():region(); 159 | else 160 | local mf_list_item_0 = faction:military_force_list():item_at(0); 161 | if mf_list_item_0:has_general() then 162 | out("tommy_get_primary_military_force_position | mf_list_item_0:has_general"); 163 | local general = mf_list_item_0:general_character(); 164 | targ_x = general:display_position_x(); 165 | targ_y = general:display_position_y(); 166 | targ_region = general:region(); 167 | elseif (faction:has_capital_region()) then 168 | out("tommy_get_primary_military_force_position | has_capital_region"); 169 | local capital_sttlement = faction:capital_region():settlement(); 170 | targ_x = capital_sttlement:display_position_x(); 171 | targ_y = capital_sttlement:display_position_y(); 172 | targ_region = faction:capital_region(); 173 | end 174 | end 175 | out("tommy_get_primary_military_force_position | return " .. targ_x .. "," .. targ_y .. "," .. 176 | _get_bool_str(not is_nil(targ_region))); 177 | return targ_x, targ_y, targ_region; 178 | end 179 | 180 | local MAPPING_PICKED_POSITIONS_COUNT = {}; 181 | local function _get_random_region_position_prevent_duplicated(region_name_list_patched, 182 | initial_positions_of_region_expand) 183 | local attemp_max = 3; 184 | local attemp = 1; 185 | local found_position = nil 186 | local found_region_name = nil 187 | while attemp <= attemp_max and is_nil(found_position) do 188 | better_math_randomseed(); 189 | local region_name_random_picked = region_name_list_patched[math.random(#region_name_list_patched)]; 190 | local position_list = initial_positions_of_region_expand[region_name_random_picked]; 191 | 192 | if (not is_nil(position_list)) then 193 | better_math_randomseed(); 194 | local index_random = math.random(#position_list); 195 | local position = position_list[index_random]; 196 | if (not is_nil(MAPPING_PICKED_POSITIONS_COUNT["" .. region_name_random_picked .. "_" .. index_random])) then 197 | found_position = position; 198 | found_region_name = region_name_random_picked; 199 | MAPPING_PICKED_POSITIONS_COUNT["" .. region_name_random_picked .. "_" .. index_random] = true; 200 | elseif (attemp >= 3) then 201 | local x, y = unpack(position); 202 | found_position = {x + random_split_2(0.66, 6), y + random_split_2(0.66, 6)}; 203 | found_region_name = region_name_random_picked; 204 | MAPPING_PICKED_POSITIONS_COUNT["" .. region_name_random_picked .. "_" .. index_random] = true; 205 | end 206 | end 207 | attemp = attemp + 1; 208 | end 209 | 210 | return found_position, found_region_name; 211 | end 212 | 213 | function enable_region_tech_for_subculture_bandits(faction, faction_handle, region_primary) 214 | if (faction:subculture() == "3k_dlc05_subculture_bandits") then 215 | local faction_name = faction:name(); 216 | local region_primary_name = region_primary:name(); 217 | local region_short_name = string.match(region_primary_name, '^3k_main_(%w+)_(%w+).*'); 218 | local prefix_tech = MAPPINT_REGION_NAME_TO_BANDIT_NETWORK_TECH_PREFIX[region_short_name]; 219 | out("tommy_randomize_all_characters_for_faction() | " .. faction_name .. " at " .. region_primary_name .. 220 | " in short " .. region_short_name); 221 | if (is_nil(prefix_tech)) then return false; end 222 | local tech_key = "3k_dlc05_tech_bandit_network_" .. prefix_tech .. "_" .. region_short_name; 223 | local result_is_enabled = false; 224 | -- local result_is_enabled = faction_handle:begin_tech_research(tech_key) 225 | out("tommy_randomize_all_characters_for_faction() | " .. faction_name .. " at " .. region_short_name .. 226 | " | amied tech:" .. tech_key .. " " .. _get_bool_str(result_is_enabled)); 227 | -- faction_handle:lock_technology(tech_key) 228 | -- faction_handle:unlock_technology(tech_key) 229 | -- if (faction_name == "3k_main_faction_zheng_jiang") then 230 | -- campaign_tutorial.faction_settings["3k_main_faction_zheng_jiang"] = { 231 | -- enemy_faction_key = "3k_main_faction_han_empire", 232 | -- target_settlement_key = region_primary_name, 233 | -- own_settlement_key = region_primary_name, 234 | -- own_settlement_building_index = 0, 235 | -- starting_force_position = nil, 236 | -- own_force_label_left = false, 237 | -- enemy_force_label_left = false 238 | -- } 239 | -- end 240 | 241 | return true; 242 | end 243 | return false; 244 | end 245 | 246 | -- randomize all characters for faction 247 | local function tommy_randomize_all_characters_for_faction(context, faction, initial_positions_of_region_expand) 248 | out("tommy_randomize_all_characters_for_faction() | START of script | (" .. faction:name() .. ")"); 249 | -- local character_list = faction:character_list(); 250 | local region_list = faction:region_list(); 251 | local region_list_world = cm:query_model():world():region_manager():region_list(); 252 | local is_faction_local_me = tommy_is_faction_local_human(context, faction); 253 | local mforce_list = faction:military_force_list(); 254 | 255 | local region_list_normal = tommy_list_ca_to_mormal_table(region_list); 256 | local region_list_world_normal = tommy_list_ca_to_mormal_table(region_list_world); 257 | local _, __, region_primary = tommy_get_primary_military_force_position(faction); 258 | local region_list_adjacent_normal = tommy_list_ca_to_mormal_table(region_primary:adjacent_region_list()) 259 | 260 | local region_name_list = {}; 261 | 262 | local is_faction_has_no_region = region_list:num_items() == 0; 263 | local is_faction_has_1_region = region_list:num_items() == 1; 264 | 265 | function get_region_name_list_from_adjacent() 266 | local result = {}; 267 | local region_list_normal = region_list_adjacent_normal; 268 | for r = 1, #region_list_normal do result[r] = region_list_normal[r]:name(); end 269 | out("tommy_randomize_all_characters_for_faction() | " .. faction:name() .. " | from adjacent region," .. #result); 270 | return result; 271 | end 272 | 273 | function get_region_name_list_from_all() 274 | local result = {}; 275 | local region_list_normal = region_list_world_normal; 276 | for r = 1, #region_list_normal do result[r] = region_list_normal[r]:name(); end 277 | out("tommy_randomize_all_characters_for_faction() | " .. faction:name() .. " | from all region," .. #result); 278 | return result; 279 | end 280 | 281 | if (faction:name() == "3k_main_faction_han_empire") then 282 | -- noop 283 | region_name_list = {} 284 | elseif (faction:name() == "3k_main_faction_liu_bei") then 285 | -- 190刘备, 选择初始区域的临近区域 286 | region_name_list = get_region_name_list_from_adjacent(); 287 | elseif (faction:name() == "3k_dlc05_faction_sun_ce") then 288 | -- 194孙策, 选择初始区域的临近区域 289 | region_name_list = get_region_name_list_from_adjacent(); 290 | elseif (is_faction_has_no_region) then 291 | -- 如果一个阵营没有初始地区,把地区池指定为全地图范围(然后就会全地图随机找个位置 292 | region_name_list = get_region_name_list_from_all(); 293 | elseif (is_faction_has_1_region) then 294 | region_name_list = get_region_name_list_from_adjacent(); 295 | else 296 | for r = 1, #region_list_normal do region_name_list[r] = region_list_normal[r]:name(); end 297 | out("tommy_randomize_all_characters_for_faction() | " .. faction:name() .. " | from own," .. region_list:num_items()); 298 | end 299 | 300 | if (faction:has_faction_leader()) then faction_leader = faction:faction_leader(); end 301 | 302 | for i = 0, mforce_list:num_items() - 1 do 303 | local force = mforce_list:item_at(i); 304 | if force:is_armed_citizenry() == false and force:has_general() == true then 305 | local general = force:general_character(); 306 | out("tommy_randomize_all_characters_for_faction() | " .. tommy_get_character_infomation_readable(general)); 307 | -- 挑选不在建筑物的角色(例如在城市或者农庄铁矿), 在建筑物的角色状态和野战可能有差别,需要特殊处理 308 | -- 此处本作者认为不需要处理这些角色, 让他们呆在原地就行 309 | if not general:in_settlement() and not general:in_port() then 310 | local region_name_list_patched = {unpack(region_name_list)}; -- 复制一份list数据 311 | local region_name_list_patched_length = #region_name_list_patched; 312 | region_name_list_patched[region_name_list_patched_length + 1] = general:region():name(); 313 | -- 1) 从当前角色所在地区(初始所在地区不等于拥有地区, 例如郑酱)和玩家拥有地区中随机选一个地区 314 | local position, region_name_random_picked = _get_random_region_position_prevent_duplicated( 315 | region_name_list_patched, initial_positions_of_region_expand); 316 | out("tommy_randomize_all_characters_for_faction() | before teleport, pick:" .. region_name_random_picked .. 317 | " found_position:" .. _get_bool_str(not is_nil(position))); 318 | if (not is_nil(position)) then 319 | local final_x, final_y = unpack(position); 320 | final_x = final_x + random_split_2(1, 0.96); 321 | final_y = final_y + random_split_2(1, 0.96); 322 | out("tommy_randomize_all_characters_for_faction() | before teleport, pick:" .. region_name_random_picked .. 323 | " found_position:" .. final_x .. "," .. final_y); 324 | cm:teleport_character(general, final_x, final_y); 325 | end 326 | end 327 | end 328 | end 329 | 330 | if is_faction_local_me then 331 | local faction_handle = cm:modify_faction(faction:name()); 332 | local key_event_handle = "3k_tommy_randomized_start_reposition_camera"; 333 | if (cm:is_multiplayer()) then 334 | -- TODO: scroll_camera_from_current for multiplayer mode 335 | else 336 | out("tommy_randomize_all_characters_for_faction() | add_listener ScriptEventCampaignCutsceneCompleted"); 337 | core:add_listener(key_event_handle, "ScriptEventCampaignCutsceneCompleted", true, function(e) 338 | out("tommy_randomize_all_characters_for_faction() | on ScriptEventCampaignCutsceneCompleted"); 339 | _debug:try{ 340 | function() 341 | if (cm:query_model():turn_number() > 1) then 342 | core:remove_listener(key_event_handle); 343 | return false; 344 | end 345 | core:remove_listener(key_event_handle); 346 | local x_primary, y_primary, region_primary = tommy_get_primary_military_force_position(faction); 347 | out( 348 | "tommy_randomize_all_characters_for_faction() | reposition camera | tommy_get_primary_military_force_position() " .. 349 | _get_bool_str(not is_nil(region_primary))); 350 | if (is_nil(region_primary)) then return false; end 351 | -- 让主要目标所在的区域可以显示 352 | faction_handle:make_region_seen_in_shroud(region_primary:name()); 353 | faction_handle:make_region_visible_in_shroud(region_primary:name()); 354 | -- for i = 0, region_list_world:num_items() - 1 do end 355 | local x, y, d, b, h = cm:get_camera_position(); 356 | cm:callback(function() 357 | out("tommy_randomize_all_characters_for_faction() | reposition camera | x_faction_leader:" .. x_primary .. 358 | ", y_faction_leader:" .. y_primary .. ", at:" .. region_primary:name()); 359 | cm:scroll_camera_from_current(1.5, nil, {x_primary, y_primary, d, b, h}); 360 | -- cm:set_camera_position(x_primary, y_primary, d, b, h); 361 | end, 1); 362 | 363 | -- if (region_primary:is_abandoned()) and is_faction_has_no_region then 364 | -- -- 如果初始区域是一个废弃区域就把这个区域设置给玩家 365 | -- cm:modify_model():get_modify_region(region_primary):settlement_gifted_as_if_by_payload(faction_handle); 366 | -- end 367 | return true; 368 | end, 369 | _debug:catch{function(error) script_error('3k_tommy_randomized_start.lua | CAUGHT ERROR: ' .. error); end} 370 | } 371 | end, true); 372 | end 373 | end 374 | 375 | cm:callback(function() 376 | _debug:try{ 377 | function() 378 | if (is_faction_has_no_region) then 379 | local faction_handle = cm:modify_faction(faction:name()); 380 | local _, __, region_primary = tommy_get_primary_military_force_position(faction); 381 | out("tommy_randomize_all_characters_for_faction() | timeout:1 | faction:" .. faction:name() .. 382 | " final_region_primary:" .. region_primary:name() .. " subclture:" .. faction:subculture()) 383 | if (region_primary:is_abandoned()) then 384 | -- 如果初始区域是一个废弃区域就把这个区域设置给玩家/AI 385 | cm:modify_model():get_modify_region(region_primary):settlement_gifted_as_if_by_payload(faction_handle); 386 | end 387 | enable_region_tech_for_subculture_bandits(faction, faction_handle, region_primary); 388 | end 389 | end, _debug:catch{function(error) script_error('3k_tommy_randomized_start.lua | CAUGHT ERROR: ' .. error); end} 390 | } 391 | end, 0.2); 392 | 393 | out("tommy_randomize_all_characters_for_faction() | END of script "); 394 | end 395 | 396 | local function _get_valid_spawn_location_in_region_until_different(faction, region_name, max_attemp) 397 | local spawn_round = 0; 398 | local attemp_ = max_attemp or 2 399 | local spawn_used_position_pool = {} 400 | local result = {}; 401 | while (spawn_round < attemp_) do 402 | local is_found_spawn, spawn_x, spawn_y = faction:get_valid_spawn_location_in_region(region_name, true); 403 | if (not spawn_used_position_pool["" .. spawn_x .. "_" .. spawn_y]) and is_found_spawn then 404 | result[#result + 1] = {spawn_x, spawn_y, region_name} 405 | else 406 | spawn_round = spawn_round + 1; 407 | end 408 | spawn_used_position_pool["" .. spawn_x .. "_" .. spawn_y] = true; 409 | end 410 | return result; 411 | end 412 | 413 | local function _get_all_valid_positions_of_region(faction_list) 414 | local region_list_world = cm:query_model():world():region_manager():region_list(); 415 | local region_list_world_raw = tommy_list_ca_to_mormal_table(region_list_world); 416 | local position_list = {} 417 | for f = 0, faction_list:num_items() - 1 do 418 | local faction = faction_list:item_at(f); 419 | local mforce_list = faction:military_force_list(); 420 | if (not faction:is_dead()) then 421 | out("tommy_randomize_start() | _get_all_valid_positions_of_region() | " .. faction:name()); 422 | 423 | -- get initial military force positions in wild 424 | for i = 0, mforce_list:num_items() - 1 do 425 | local force = mforce_list:item_at(i); 426 | if force:is_armed_citizenry() == false and force:has_general() == true then 427 | local general = force:general_character(); 428 | if not general:in_settlement() and not general:in_port() then 429 | local general_x = general:logical_position_x(); 430 | local general_y = general:logical_position_y(); 431 | position_list[#position_list + 1] = {general_x, general_y, general:region():name(), true} 432 | end 433 | end 434 | end 435 | 436 | -- get spawn position from CA get_valid_spawn_location_in_region API 437 | for j = 1, #region_list_world_raw do 438 | local region = region_list_world_raw[j] 439 | local positions_region_spawn = _get_valid_spawn_location_in_region_until_different(faction, region:name(), 3) 440 | for k = 1, #positions_region_spawn do 441 | local x, y, region_name = unpack(positions_region_spawn[k]); 442 | position_list[#position_list + 1] = {x, y, region_name, false} 443 | end 444 | end 445 | end 446 | end 447 | 448 | local position_region_mapping = {} 449 | 450 | out("tommy_randomize_start() | _get_all_valid_positions_of_region() | position_region_mapping from list, " .. 451 | #position_list); 452 | 453 | for p = 1, #position_list do 454 | local x, y, region_name, is_default = unpack(position_list[p]); 455 | local mapping = position_region_mapping[region_name] 456 | if (is_nil(mapping)) then mapping = {} end 457 | mapping["" .. x .. "," .. y] = {x, y, is_default}; 458 | position_region_mapping[region_name] = mapping; 459 | end 460 | 461 | return position_region_mapping; 462 | end 463 | 464 | -- main function 465 | local function tommy_randomize_start(context) 466 | out("tommy_randomize_start() | START of script "); 467 | if not cm:is_new_game() then 468 | out("tommy_randomize_start() | not cm:is_new_game()"); 469 | return false; 470 | end 471 | out("tommy_randomize_start() | cm:is_new_game()"); 472 | MAPPING_PICKED_POSITIONS_COUNT = {} 473 | 474 | local faction_list = cm:query_model():world():faction_list(); 475 | local initial_positions_of_region = _get_all_valid_positions_of_region(faction_list); 476 | 477 | local initial_positions_of_region_expand = {} 478 | for region_name, position_list in pairs(initial_positions_of_region) do 479 | local count = 0; 480 | local position_list_expand = {} 481 | for _, position in pairs(position_list) do 482 | count = count + 1; 483 | local x, y, is_default = unpack(position); 484 | out("tommy_randomize_start() | " .. region_name .. ": " .. x .. "," .. y .. "," .. _get_bool_str(is_default)); 485 | local random_offset_x = 0; 486 | local random_offset_y = 0; 487 | if (is_default) then 488 | count = count + 1; 489 | random_offset_x = random_split_2(0.6, 3); 490 | random_offset_y = random_split_2(0.6, 3); 491 | position_list_expand[#position_list_expand + 1] = {x + random_offset_x, y + random_offset_y} 492 | out("tommy_randomize_start() | " .. region_name .. ": " .. x + random_offset_x .. "," .. y + random_offset_y .. 493 | ", expanded"); 494 | else 495 | position_list_expand[#position_list_expand + 1] = {x, y} 496 | if (not is_nil(REGION_DONT_SPAWN_TOO_MUCH[region_name])) then 497 | count = count + 1; 498 | random_offset_x = random_split_2(0.6, 1); 499 | random_offset_y = random_split_2(0.6, 1); 500 | position_list_expand[#position_list_expand + 1] = {x + random_offset_x, y + random_offset_y} 501 | out( 502 | "tommy_randomize_start() | " .. region_name .. ": " .. x + random_offset_x .. "," .. y + random_offset_y .. 503 | ", expanded"); 504 | count = count + 1; 505 | random_offset_x = random_split_2(1, 1.6); 506 | random_offset_x = random_split_2(1, 1.6); 507 | position_list_expand[#position_list_expand + 1] = {x + random_offset_x, y + random_offset_y} 508 | out( 509 | "tommy_randomize_start() | " .. region_name .. ": " .. x + random_offset_x .. "," .. y + random_offset_y .. 510 | ", expanded"); 511 | else 512 | count = count + 1; 513 | random_offset_x = random_split_2(0.6, 5); 514 | random_offset_y = random_split_2(0.6, 5); 515 | position_list_expand[#position_list_expand + 1] = {x + random_offset_x, y + random_offset_y} 516 | out( 517 | "tommy_randomize_start() | " .. region_name .. ": " .. x + random_offset_x .. "," .. y + random_offset_y .. 518 | ", expanded"); 519 | count = count + 1; 520 | random_offset_x = random_split_2(2, 6.6); 521 | random_offset_y = random_split_2(2, 6.6); 522 | position_list_expand[#position_list_expand + 1] = {x + random_offset_x, y + random_offset_y} 523 | out( 524 | "tommy_randomize_start() | " .. region_name .. ": " .. x + random_offset_x .. "," .. y + random_offset_y .. 525 | ", expanded"); 526 | count = count + 1; 527 | random_offset_x = random_split_2(2, 10); 528 | random_offset_y = random_split_2(2, 10); 529 | position_list_expand[#position_list_expand + 1] = {x + random_offset_x, y + random_offset_y} 530 | out( 531 | "tommy_randomize_start() | " .. region_name .. ": " .. x + random_offset_x .. "," .. y + random_offset_y .. 532 | ", expanded"); 533 | end 534 | end 535 | end 536 | out("tommy_randomize_start() | " .. region_name .. " has " .. count); 537 | initial_positions_of_region_expand[region_name] = position_list_expand; 538 | end 539 | 540 | for f = 0, faction_list:num_items() - 1 do 541 | local faction = faction_list:item_at(f); 542 | local isHasMilitaryForceFaction = not faction:military_force_list():is_empty(); 543 | if (isHasMilitaryForceFaction and not faction:is_null_interface() and not faction:is_dead()) then 544 | tommy_randomize_all_characters_for_faction(context, faction, initial_positions_of_region_expand); 545 | end 546 | -- if faction:is_human() then 547 | -- local isHasMilitaryForceFaction = not faction:military_force_list():is_empty(); 548 | -- if (isHasMilitaryForceFaction) then tommy_randomize_all_characters_for_faction(faction); end 549 | -- end 550 | end 551 | 552 | out("tommy_randomize_start() | END of script "); 553 | end 554 | 555 | local function RUN_tommy_randomize_start(context) 556 | if (_debug.is_debug) then 557 | out('3k_tommy_randomized_start.lua | run debug'); 558 | _debug:try{ 559 | function() tommy_randomize_start(context); end, 560 | _debug:catch{function(error) script_error('3k_tommy_randomized_start.lua | CAUGHT ERROR: ' .. error); end} 561 | } 562 | else 563 | out('3k_tommy_randomized_start.lua | run'); 564 | tommy_randomize_start(context); 565 | end 566 | out('3k_tommy_randomized_start.lua | end'); 567 | return true; 568 | end 569 | 570 | cm:add_first_tick_callback(function(context) RUN_tommy_randomize_start(context) end); 571 | -- cm:add_first_tick_callback_sp_new(function(context) RUN_tommy_randomize_start(context) end); 572 | -- cm:add_first_tick_callback_mp_new(function(context) RUN_tommy_randomize_start(context) end); 573 | -- events = get_events(); 574 | -- events.FirstTickAfterWorldCreated[#events.FirstTickAfterWorldCreated+1] = 575 | -- cm.first_tick_callbacks[#cm.first_tick_callbacks + 1] = 576 | --------------------------------------------------------------------------------