首先,先解释lua的加载函数 require。通常 require是遵循package.searchers(加载模块的表)的规则执行加载文件。package.searchers这张表内每项都是查找器函数。require是依次调用这些查找器,来尝试找到并加载文件。
用于已经加载过的文件, 它使用储存在package.preload已有的值。
用于查找 Lua 库的加载库。 它使用储存在package.path中的路径来做查找工作。
用于查找 C 库的加载库,它使用储存在 package.cpath中的路径来做查找工作。
从 C 路径中查找指定模块的根名字。 做mod,只需要关注前俩个就好了,其他的也管不着。
package.path是key-value类型的表。key是存储着,使用require("xxxx")里的第一个参数。value存储情况分两种,。其一,如果加载成功没能返回一个非空值,value设置为ture;其二,如果加载成功返回一个非空值,那么将值赋予value。通常情况就是,lua文件末尾return的值,饥荒源码中是返回表。 require时,查找 package.loaded 表是否存在非nil值,存在说明文件已经被加载过了,那么将直接返回package.loaded 表中存储的值。否则会,进行下一个查找器。
package.cpath是存储路径的字符串。其内存储着多条路径,是通过";"分割开了。从左往右依次设置为默认路径来查找,将第一次查找并执行的结果返回。如果都尝试没能找到,将执行下一个查找器。
package.path = MODS_ROOT..mod.modname.."\\scripts\\?.lua;"..package.path 每次进入存档世界加载饥荒mod时,将更改package.cpath值。MODS_ROOT是“../mods/”,modname是mod名称。 举例,"../mods/11cs\scripts\?.lua;scripts\?.lua;scriptlibs\?.lua"就是只加载了一个mod的情况下。 所以有的mod使用覆盖法,就是将scripts文件夹下同名路径的文件复制到mod中来修改。加载这个被覆盖的文件时,优先找到了mod文件夹里的文件,而非饥荒源码。
其源码内容在mods.lua文件里。核心是ModWrangler:LoadMods 函数。
function ModWrangler:LoadMods(worldgen)
if not MODS_ENABLED then
return
end
self.worldgen = worldgen or false
-- 第二部分: 专服下载mod
if not worldgen and TheNet:IsDedicated() then
self:LoadServerModsFile()
end
-- 第三部分:mod设置
local mod_overrides = {}
if not worldgen then
if IsInFrontEnd() then
KnownModIndex:ClearAllTempModFlags()
self:DisableAllServerMods()
end
KnownModIndex:UpdateModInfo()
mod_overrides = KnownModIndex:LoadModOverides()
KnownModIndex:ApplyEnabledOverrides(mod_overrides)
end
-- 第四部分:mod设置
local moddirs = KnownModIndex:GetModsToLoad(self.worldgen)
for i,modname in ipairs(moddirs) do
if self.worldgen == false or (self.worldgen == true and KnownModIndex:IsModCompatibleWithMode(modname)) then
table.insert(self.modnames, modname)
if self.worldgen == false then
if TheNet:GetIsServer() and not TheNet:IsDedicated() then
local options = KnownModIndex:LoadModConfigurationOptions(modname, false)
KnownModIndex:SetTempModConfigData({[modname] = options})
KnownModIndex:LoadModConfigurationOptions(modname, true)
else
KnownModIndex:LoadModConfigurationOptions(modname, not TheNet:GetIsServer())
end
end
local initenv = KnownModIndex:GetModInfo(modname)
local env = CreateEnvironment(modname, self.worldgen)
env.modinfo = initenv
table.insert( self.mods, env )
local loadmsg = "Loading mod: "..ModInfoname(modname).." Version:"..env.modinfo.version
if initenv.modinfo_message and initenv.modinfo_message ~= "" then
loadmsg = loadmsg .. " ("..initenv.modinfo_message..")"
end
print(loadmsg)
end
end
KnownModIndex:ApplyConfigOptionOverrides(mod_overrides)
local function modPrioritySort(a,b)
local apriority = (a.modinfo and a.modinfo.priority) or 0
local bpriority = (b.modinfo and b.modinfo.priority) or 0
if apriority == bpriority then
return tostring(a.modinfo and a.modinfo.name) > tostring(b.modinfo and b.modinfo.name)
else
return apriority > bpriority
end
end
table.sort(self.mods, modPrioritySort)
for i,mod in ipairs(self.mods) do
table.insert(self.enabledmods, mod.modname)
package.path = MODS_ROOT..mod.modname.."\\scripts\\?.lua;"..package.path
local manifest
if((mod.modinfo.forcemanifest == nil and IsWorkshopMod(mod.modname)) or
(mod.modinfo.forcemanifest ~= nil and mod.modinfo.forcemanifest)) then
ManifestManager:LoadModManifest(mod.modname, mod.modinfo.version)
manifest = mod.modname
end
table.insert(package.assetpath, {path = MODS_ROOT..mod.modname.."\\", manifest = manifest})
self.currentlyloadingmod = mod.modname
self:InitializeModMain(mod.modname, mod, "modworldgenmain.lua")
if not self.worldgen then
self:InitializeModMain(mod.modname, mod, "modmain.lua")
end
self.currentlyloadingmod = nil
end
end 形参worldgen是代表是否在创建世界生成地图时执行。通常传入true。然后不启用mod就退出加载函数。
if not worldgen and TheNet:IsDedicated() then
self:LoadServerModsFile()
end 非生成地图时,且是专服情况下,执行LoadServerModsFile函数。
function ModWrangler:LoadServerModsFile()
local function ServerModSetup(product_id)
TheNet:ServerModSetup(product_id)
end
local function ServerModCollectionSetup(collection_id)
TheNet:ServerModCollectionSetup(collection_id)
end
local env = {
ServerModSetup = ServerModSetup,
ServerModCollectionSetup = ServerModCollectionSetup,
}
KnownModIndex:UpdateModInfo()
TheNet:BeginServerModSetup()
local filename = MODS_ROOT
if DIST_PLATFORM == "Rail" then
filename = filename.."dedicated_server_mods_setup_rail.lua"
else
filename = filename.."dedicated_server_mods_setup.lua"
end
local fn = kleiloadlua( filename )
if fn ~= nil then
local mods_err_fn = function(err)
print("########################################################")
print("#ERROR: Failure to load dedicated_server_mods_setup.lua:", err)
print("#Shutting down")
print("########################################################")
Shutdown()
end
if type(fn)=="string" then
mods_err_fn(fn)
else
setfenv(fn, env)
xpcall( fn, mods_err_fn )
end
end
TheNet:DownloadServerMods()
end env 为下面运行加载的文件添加运行环境,这个环境表就俩参数。对于开专服人来说,dedicated_server_mods_setup.lua文件是很熟悉了,每当需要自动更新的mod,就在这个文件里添加一行 'ServerModSetup("xxx")'代码 ,其中xxx代表的是对应mod编号。
这个是克雷未公开出来的函数,用来加载lua文件的。
setfenv是lua5.1的函数,用来设置环境的,参数1可以是函数也可以是数字,参数2是表,为环境。那么在这个环境中使用一个未定义的函数ServerModSetup,就会从环境中找,找到ServerModSetup函数执行。 xpcall是当执行参数1函数报错时,可以调用这个参数2处理函数来处理。
这个部分函数的,意义是为了专服可以自动更新mod。原理是,调用执行存储要更新mod或mod合集的lua文件。具体细节无从得知,我们只知道输入和输出,中间是个黑箱。TheNet调用的函数具体功能如何实现的一概不知。
local mod_overrides = {}
if not worldgen then
if IsInFrontEnd() then
KnownModIndex:ClearAllTempModFlags()
self:DisableAllServerMods()
end
KnownModIndex:UpdateModInfo()
mod_overrides = KnownModIndex:LoadModOverides()
KnownModIndex:ApplyEnabledOverrides(mod_overrides)
end 在是前端情况下,会执行清理临时mod标记,且禁止全部服务器mod。 其次,更新全部mod信息,客户端mod和服务器mod。 最后是,获取全部mod设置,覆盖掉启用的mod设置。
其核心代码是 InitializeModInfo函数
local env = {
folder_name = modname,
locale = LOC.GetLocaleCode(),
ChooseTranslationTable = function(tbl)
local locale = LOC.GetLocaleCode()
return tbl[locale] or tbl[1]
end,
}
local fn = kleiloadlua(MODS_ROOT..modname.."/modinfo.lua")
加载mod文件夹里的modinfo.lua文件,其执行环设置为env表内容。 最后会返回这个环境env表,作为信息。
所以,我们可以看到一个正常的modinfo.lua文件里包含下面代码。
name = "mod名称"
description = [[mod描述]]
author = "mod作者"
version = "0.0.0" --mod版本
api_version = 10 --api版本
dst_compatible = true --兼容
all_clients_require_mod = false --是否全部玩家都要加载
client_only_mod = true --客户端mod
icon_atlas = "xxx.xml" --mod显示图片资源
icon = "xxx.tex" --mod显示图片资源
forumthread = ""
configuration_options ={} --mod设置 这里的代码都是没有加local,说明是"全局的",声明在了环境中,env表内。
function ModIndex:LoadModOverides(shardGameIndex)
if TheNet:GetIsClient() then
return {}
end
local overrides = {}
local filename = "../modoverrides.lua"
local function onload(load_success, str)
if load_success == true then
local fn, message = loadstring(str)
if fn ~= nil then
local env = {}
local success, r = RunInEnvironment(fn, env)
if success and type(r) == "table" then
print("SUCCESS: Loaded modoverrides.lua")
overrides = r
else
print("ERROR: Failed to run code from modoverrides.lua")
end
else
print("ERROR: Failed to load modoverrides.lua")
end
end
end
shardGameIndex = shardGameIndex or ShardGameIndex
if not shardGameIndex then
require("shardindex")
shardGameIndex = ShardIndex()
shardGameIndex:Load()
end
if not TheNet:IsDedicated() and shardGameIndex:IsValid() and not shardGameIndex:GetServerData().use_legacy_session_path then
TheSim:GetPersistentStringInClusterSlot(shardGameIndex:GetSlot(), "Master", filename, onload)
else
TheSim:GetPersistentString(filename, onload)
end
return overrides
end 是客户端时,返回的是空表{}。 否则会从存档文件夹里,读取modoverrides.lua文件内容。将文件结果赋值给overrides,最后返回结果。如果是客户端的情况下,是读取Master文件夹里的modoverrides.lua文件。 最后,遍历这个overrides表,并覆盖掉对应mod设置。故modoverrides.lua内容是:
return { ["1_cs"]={ configuration_options={ }, enabled=true } } 返回的是张表。
可以看到,mod加载并非是仅有lua的require函数,还有饥荒提供的kleiloadlua函数,存档文件夹里文件的TheSim:GetPersistentStringInClusterSlot函数,加载client_save文件夹下内容的TheSim:GetPersistentString函数。 这里覆盖mod配置更多的是在进入客户端存档界面上,显示mod列表时的内容。
获取到全部启用的mod目录表,并且遍历它。将全部启用的mod信息和配置设置好。
local moddirs = KnownModIndex:GetModsToLoad(self.worldgen) 不是生成地图情况下;或者是生成地图,且该mod信息里的dst_compatible=true
self.worldgen == false or (self.worldgen == true and KnownModIndex:IsModCompatibleWithMode(modname)) 非生成世界时,将client_save\mod_config_data\modconfiguration_xxx文件加载到xxxmod里。modconfiguration是上次mod使用的配置。
if self.worldgen == false then
if TheNet:GetIsServer() and not TheNet:IsDedicated() then
local options = KnownModIndex:LoadModConfigurationOptions(modname, false)
KnownModIndex:SetTempModConfigData({[modname] = options})
KnownModIndex:LoadModConfigurationOptions(modname, true)
else
KnownModIndex:LoadModConfigurationOptions(modname, not TheNet:GetIsServer())
end
end 创建了mod的env环境后,添加到了self.mods表中。从日志中找到打印的内容。
local initenv = KnownModIndex:GetModInfo(modname)
local env = CreateEnvironment(modname, self.worldgen)
env.modinfo = initenv
table.insert( self.mods, env )
local loadmsg = "Loading mod: "..ModInfoname(modname).." Version:"..env.modinfo.version
if initenv.modinfo_message and initenv.modinfo_message ~= "" then
loadmsg = loadmsg .. " ("..initenv.modinfo_message..")"
end
print(loadmsg)
[00:00:51]:applying configuration_options from modoverrides.lua to mod workshop-2287303119 function CreateEnvironment(modname, isworldgen, isfrontend)
local modutil = require("modutil")
require("map/lockandkey")
local env =
{
-- lua
pairs = pairs,
ipairs = ipairs,
print = print,
math = math,
table = table,
type = type,
string = string,
tostring = tostring,
require = require,
Class = Class,
-- runtime
TUNING=TUNING,
-- worldgen
LEVELCATEGORY = LEVELCATEGORY,
GROUND = GROUND,
WORLD_TILES = WORLD_TILES,
LOCKS = LOCKS,
KEYS = KEYS,
LEVELTYPE = LEVELTYPE,
-- utility
GLOBAL = _G,
modname = modname,
MODROOT = MODS_ROOT..modname.."/",
}
if isworldgen == false then
env.CHARACTERLIST = GetActiveCharacterList()
end
env.env = env
--install our crazy loader!
env.modimport = function(modulename)
print("modimport: "..env.MODROOT..modulename)
if string.sub(modulename, #modulename-3,#modulename) ~= ".lua" then
modulename = modulename..".lua"
end
local result = kleiloadlua(env.MODROOT..modulename)
if result == nil then
error("Error in modimport: "..modulename.." not found!")
elseif type(result) == "string" then
error("Error in modimport: "..ModInfoname(modname).." importing "..modulename.."!\n"..result)
else
setfenv(result, env.env)
result()
end
end
modutil.InsertPostInitFunctions(env, isworldgen, isfrontend)
return env
end env表,添加了一些默认参数,一部分是lua本身的函数,一部分是饥荒提供的全局参数, "GLOBAL = _G"设置全局表了。提供了一个封装好的加载函数modimport。 InsertPostInitFunctions函数为env环境表添加了大量的饥荒提供的API,具体的nr可以去看modutil.lua文件。
直接对self.mods表的modinfo信息表中priority参数进行比较,大的在前,小的在后,相同值,比较name参数。
local function modPrioritySort(a,b)
local apriority = (a.modinfo and a.modinfo.priority) or 0
local bpriority = (b.modinfo and b.modinfo.priority) or 0
if apriority == bpriority then
return tostring(a.modinfo and a.modinfo.name) > tostring(b.modinfo and b.modinfo.name)
else
return apriority > bpriority
end
end
table.sort(self.mods, modPrioritySort) for i,mod in ipairs(self.mods) do
table.insert(self.enabledmods, mod.modname)
package.path = MODS_ROOT..mod.modname.."\\scripts\\?.lua;"..package.path
local manifest
if((mod.modinfo.forcemanifest == nil and IsWorkshopMod(mod.modname)) or
(mod.modinfo.forcemanifest ~= nil and mod.modinfo.forcemanifest)) then
ManifestManager:LoadModManifest(mod.modname, mod.modinfo.version)
manifest = mod.modname
end
table.insert(package.assetpath, {path = MODS_ROOT..mod.modname.."\\", manifest = manifest})
self.currentlyloadingmod = mod.modname
self:InitializeModMain(mod.modname, mod, "modworldgenmain.lua")
if not self.worldgen then
self:InitializeModMain(mod.modname, mod, "modmain.lua")
end
self.currentlyloadingmod = nil
end 依次将mod路径添加到package.path中。
package.path = MODS_ROOT..mod.modname.."\\scripts\\?.lua;"..package.path package.assetpath是饥荒定义的资源路径,比如说图片资源(images)、动画资源(anim)等。
self:InitializeModMain(mod.modname, mod, "modworldgenmain.lua")
if not self.worldgen then
self:InitializeModMain(mod.modname, mod, "modmain.lua")
end mod这里是指mod环境,InitializeModMain函数核心的加载是通过kleiloadlua执行的。在生成地图时,不会加载modmain.lua文件
加载mod的目标是,全部启用的mod。
mod配置文件,是存储于两个文件里,一是存档文件里的modoverrides.lua,二是配置client_save\mod_config_data\modconfiguration_xxx 这里包含了上次使用参数、默认参数等全部的信息。
mod详细信息是在modinfo.lua里,是这个文件的环境。
mod加载的环境里包含许多信息。
mod加载核心就加载三文件,modinfo.lua、modworldgenmain.lua、modmain.lua。在生成地图时,不会加载modmain.lua文件。
mod覆盖法是通过package.path的优先级实现的。
mod加载使用了多种文件加载方法。大部分都是黑箱。
即LoadMods函数执行的位置。
local function ModSafeStartup()
TheSim:ClearFileSystemAliases()
ModManager:LoadMods()
...
end 在client_save\modindex和client_save\boot_modindex文件被加载后执行。
local moddata = json.decode(GEN_MODDATA)
if moddata then
KnownModIndex:RestoreCachedSaveData(moddata.index)
ModManager:LoadMods(true)
end 这里是生成地图的时候了。
是在客户端前端的mod列表中,确认启用了mod时执行。
function ModWrangler:FrontendLoadMod(modname)
if not KnownModIndex:DoesModExistAnyVersion(modname) then
print(string.format("Tried frontend-loading mod '%s' but it doesn't exist.", modname))
return
end
print("FrontendLoadMod", modname)
KnownModIndex:LoadModConfigurationOptions(modname, false)
local initenv = KnownModIndex:GetModInfo(modname)
local env = CreateEnvironment(modname, self.worldgen)
local frontend_env = CreateEnvironment(modname, self.worldgen, true)
env.modinfo = deepcopy(initenv)
frontend_env.modinfo = initenv
local loadmsg = "Fontend-Loading mod: "..ModInfoname(modname).." Version:"..tostring(initenv.version)
if initenv.modinfo_message and initenv.modinfo_message ~= "" then
loadmsg = loadmsg .. " ("..initenv.modinfo_message..")"
end
print(loadmsg)
local oldpath = package.path
package.path = MODS_ROOT..modname.."\\scripts\\?.lua;"..package.path
table.insert(package.assetpath, {path = MODS_ROOT..modname.."\\"})
self.currentlyloadingmod = modname
self:InitializeModMain(modname, frontend_env, "modservercreationmain.lua", true)
self:InitializeModMain(modname, env, "modworldgenmain.lua", true)
self.currentlyloadingmod = nil
package.path = oldpath
table.remove(package.assetpath)
end 此时仅会加载执行modservercreationmain.lua和modworldgenmain.lua文件。 此时,modservercreationmain.lua文件的环境会多一个ReloadFrontEndAssets函数。